Ejemplo n.º 1
0
class APIv1UserController(OAuth2OnlyController):
    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(api_section.account)
    def GET_me(self):
        """Returns the identity of the user currently authenticated via OAuth."""
        resp = IdentityJsonTemplate().data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("identity")
    @validate(
        VUser(),
        fields=VList(
            "fields",
            choices=PREFS_JSON_SPEC.spec.keys(),
            error=errors.errors.NON_PREFERENCE,
        ),
    )
    @api_doc(api_section.account, uri='/api/v1/me/prefs')
    def GET_prefs(self, fields):
        """Return the preference settings of the logged in user"""
        resp = PrefsJsonTemplate(fields).data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName('username'), )
    @api_doc(
        section=api_section.users,
        uri='/api/v1/user/{username}/trophies',
    )
    def GET_usertrophies(self, user):
        """Return a list of trophies for the a given user."""
        return self.api_wrapper(get_usertrophies(user))

    @require_oauth2_scope("identity")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/trophies',
    )
    def GET_trophies(self):
        """Return a list of trophies for the current user."""
        return self.api_wrapper(get_usertrophies(c.oauth_user))

    @require_oauth2_scope("mysubreddits")
    @validate(
        VUser(), )
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/karma',
    )
    def GET_karma(self):
        """Return a breakdown of subreddit karma."""
        karmas = c.oauth_user.all_karmas(include_old=False)
        resp = KarmaListJsonTemplate().render(karmas)
        return self.api_wrapper(resp.finalize())

    PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True)

    @require_oauth2_scope("account")
    @validate(
        VUser(),
        validated_prefs=PREFS_JSON_VALIDATOR,
    )
    @api_doc(api_section.account,
             json_model=PREFS_JSON_VALIDATOR,
             uri='/api/v1/me/prefs')
    def PATCH_prefs(self, validated_prefs):
        user_prefs = c.user.preferences()
        for short_name, new_value in validated_prefs.iteritems():
            pref_name = "pref_" + short_name
            user_prefs[pref_name] = new_value
        vprefs.filter_prefs(user_prefs, c.user)
        vprefs.set_prefs(c.user, user_prefs)
        c.user._commit()
        return self.api_wrapper(PrefsJsonTemplate().data(c.user))

    FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({
        "name":
        VAccountByName("name"),
        "note":
        VLength("note", 300),
    })
    FRIEND_JSON_VALIDATOR = VValidatedJSON("json",
                                           spec=FRIEND_JSON_SPEC,
                                           body=True)

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend=VAccountByName('username'),
        notes_json=FRIEND_JSON_VALIDATOR,
    )
    @api_doc(api_section.users,
             json_model=FRIEND_JSON_VALIDATOR,
             uri='/api/v1/me/friends/{username}')
    def PUT_friends(self, friend, notes_json):
        """Create or update a "friend" relationship.

        This operation is idempotent. It can be used to add a new
        friend, or update an existing friend (e.g., add/change the
        note on that friend)

        """
        err = None
        if 'name' in notes_json and notes_json['name'] != friend:
            # The 'name' in the JSON is optional, but if present, must
            # match the username from the URL
            err = errors.RedditError('BAD_USERNAME', fields='name')
        if 'note' in notes_json and not c.user.gold:
            err = errors.RedditError('GOLD_REQUIRED', fields='note')
        if err:
            self.on_validation_error(err)

        # See if the target is already an existing friend.
        # If not, create the friend relationship.
        friend_rel = Account.get_friend(c.user, friend)
        rel_exists = bool(friend_rel)
        if not friend_rel:
            friend_rel = c.user.add_friend(friend)
            response.status = 201

        if 'note' in notes_json:
            note = notes_json['note'] or ''
            if not rel_exists:
                # If this is a newly created friend relationship,
                # the cache needs to be updated before a note can
                # be applied
                c.user.friend_rels_cache(_update=True)
            c.user.add_friend_note(friend, note)
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('mysubreddits')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def GET_friends(self, friend_rel):
        """Get information about a specific 'friend', such as notes."""
        rel_view = FriendTableItem(friend_rel)
        return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view))

    @require_oauth2_scope('subscribe')
    @validate(
        VUser(),
        friend_rel=VFriendOfMine('username'),
    )
    @api_doc(api_section.users, uri='/api/v1/me/friends/{username}')
    def DELETE_friends(self, friend_rel):
        """Stop being friends with a user."""
        c.user.remove_friend(friend_rel._thing2)
        if c.user.gold:
            c.user.friend_rels_cache(_update=True)
        response.status = 204
Ejemplo n.º 2
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   collection=VCollection('collection'),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'),
                   platform=VOneOf('platform', ('mobile', 'desktop', 'all'), 
                                   default='all'))
    def GET_check_inventory(self, responder, sr, collection, location, start,
                            end, platform):
        if collection:
            target = Target(collection)
            sr = None
        else:
            sr = sr or Frontpage
            target = Target(sr.name)

        if not allowed_location_and_target(location, target):
            return abort(403, 'forbidden')

        available = inventory.get_available_pageviews(
                        target, start, end, location=location, platform=platform,
                        datestr=True)

        return {'inventory': available}

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink("link_id36"),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_freebie(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        if campaign_has_oversold_error(form, campaign):
            form.set_text(".freebie", _("target oversold, can't freebie"))
            return

        if promote.is_promo(link) and campaign:
            promote.free_campaign(link, campaign, c.user)
            form.redirect(promote.promo_edit_url(link))

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VByName("link"),
                   note=nop("note"))
    def POST_promote_note(self, form, jquery, link, note):
        if promote.is_promo(link):
            text = PromotionLog.add(link, note)
            form.find(".notes").children(":last").after(
                format_html("<p>%s</p>", text))

    @validatedForm(
        VSponsorAdmin(),
        VModhash(),
        thing = VByName("thing_id"),
        is_fraud=VBoolean("fraud"),
    )
    def POST_review_fraud(self, form, jquery, thing, is_fraud):
        if not promote.is_promo(thing):
            return

        promote.review_fraud(thing, is_fraud)

        button = jquery(".id-%s .fraud-button" % thing._fullname)
        button.text(_("fraud" if is_fraud else "not fraud"))
        form.fadeOut()

    @noresponse(VSponsorAdmin(),
                VModhash(),
                thing=VByName('id'))
    def POST_promote(self, thing):
        if promote.is_promo(thing):
            promote.accept_promotion(thing)

    @noresponse(VSponsorAdmin(),
                VModhash(),
                thing=VByName('id'),
                reason=nop("reason"))
    def POST_unpromote(self, thing, reason):
        if promote.is_promo(thing):
            promote.reject_promotion(thing, reason=reason)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link'),
                   campaign=VPromoCampaign('campaign'))
    def POST_refund_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign,
                                                      billable_impressions)
        refund_amount = promote.get_refund_amount(campaign, billable_amount)
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount,
                                    billable_impressions)
            form.set_text('.status', _('refund succeeded'))
        else:
            form.set_text('.status', _('refund not needed'))

    @validatedMultipartForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True,
                   rate_ip=True,
                   prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        title=VTitle('title'),
        url=VUrl('url', allow_self=False),
        selftext=VMarkdownLength('text', max_length=40000),
        kind=VOneOf('kind', ['link', 'self']),
        disable_comments=VBoolean("disable_comments"),
        sendreplies=VBoolean("sendreplies"),
        media_url=VUrl("media_url", allow_self=False,
                       valid_schemes=('http', 'https')),
        gifts_embed_url=VUrl("gifts_embed_url", allow_self=False,
                             valid_schemes=('http', 'https')),
        media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")),
        media_autoplay=VBoolean("media_autoplay"),
        media_override=VBoolean("media-override"),
        domain_override=VLength("domain", 100),
        third_party_tracking=VUrl("third_party_tracking"),
        third_party_tracking_2=VUrl("third_party_tracking_2"),
        is_managed=VBoolean("is_managed"),
        thumbnail_file=VUploadLength('file', 500*1024),
    )
    def POST_create_promo(self, form, jquery, username, title, url,
                          selftext, kind, disable_comments, sendreplies,
                          media_url, media_autoplay, media_override,
                          iframe_embed_url, media_url_type, domain_override,
                          third_party_tracking, third_party_tracking_2,
                          is_managed, thumbnail_file):
        return self._edit_promo(form, jquery, username, title, url,
                                selftext, kind, disable_comments, sendreplies,
                                media_url, media_autoplay, media_override,
                                iframe_embed_url, media_url_type, domain_override,
                                third_party_tracking, third_party_tracking_2,
                                is_managed, thumbnail_file=thumbnail_file)

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True,
                   rate_ip=True,
                   prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        title=VTitle('title'),
        url=VUrl('url', allow_self=False),
        selftext=VMarkdownLength('text', max_length=40000),
        kind=VOneOf('kind', ['link', 'self']),
        disable_comments=VBoolean("disable_comments"),
        sendreplies=VBoolean("sendreplies"),
        media_url=VUrl("media_url", allow_self=False,
                       valid_schemes=('http', 'https')),
        gifts_embed_url=VUrl("gifts_embed_url", allow_self=False,
                             valid_schemes=('http', 'https')),
        media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")),
        media_autoplay=VBoolean("media_autoplay"),
        media_override=VBoolean("media-override"),
        domain_override=VLength("domain", 100),
        third_party_tracking=VUrl("third_party_tracking"),
        third_party_tracking_2=VUrl("third_party_tracking_2"),
        is_managed=VBoolean("is_managed"),
        l=VLink('link_id36'),
    )
    def POST_edit_promo(self, form, jquery, username, title, url,
                        selftext, kind, disable_comments, sendreplies,
                        media_url, media_autoplay, media_override,
                        iframe_embed_url, media_url_type, domain_override,
                        third_party_tracking, third_party_tracking_2,
                        is_managed, l):
        return self._edit_promo(form, jquery, username, title, url,
                                selftext, kind, disable_comments, sendreplies,
                                media_url, media_autoplay, media_override,
                                iframe_embed_url, media_url_type, domain_override,
                                third_party_tracking, third_party_tracking_2,
                                is_managed, l=l)

    def _edit_promo(self, form, jquery, username, title, url,
                    selftext, kind, disable_comments, sendreplies,
                    media_url, media_autoplay, media_override,
                    iframe_embed_url, media_url_type, domain_override,
                    third_party_tracking, third_party_tracking_2,
                    is_managed, l=None, thumbnail_file=None):
        should_ratelimit = False
        is_self = kind == "self"
        is_link = not is_self
        if not c.user_is_sponsor:
            should_ratelimit = True

        if not should_ratelimit:
            c.errors.remove((errors.RATELIMIT, 'ratelimit'))

        # check for user override
        if not l and c.user_is_sponsor and username:
            try:
                user = Account._by_name(username)
            except NotFound:
                c.errors.add(errors.USER_DOESNT_EXIST, field="username")
                form.set_error(errors.USER_DOESNT_EXIST, "username")
                return

            if not user.email:
                c.errors.add(errors.NO_EMAIL_FOR_USER, field="username")
                form.set_error(errors.NO_EMAIL_FOR_USER, "username")
                return

            if not user.email_verified:
                c.errors.add(errors.NO_VERIFIED_EMAIL, field="username")
                form.set_error(errors.NO_VERIFIED_EMAIL, "username")
                return
        else:
            user = c.user

        # check for shame banned domains
        if form.has_errors("url", errors.DOMAIN_BANNED):
            g.stats.simple_event('spam.shame.link')
            return

        # demangle URL in canonical way
        if url:
            if isinstance(url, (unicode, str)):
                form.set_inputs(url=url)
            elif isinstance(url, tuple) or isinstance(url[0], Link):
                # there's already one or more links with this URL, but
                # we're allowing mutliple submissions, so we really just
                # want the URL
                url = url[0].url

        if kind == 'link':
            if form.has_errors('url', errors.NO_URL, errors.BAD_URL):
                return

        # users can change the disable_comments on promoted links
        if ((not l or not promote.is_promoted(l)) and
            (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or
             jquery.has_errors('ratelimit', errors.RATELIMIT))):
            return

        if kind == 'self' and form.has_errors('text', errors.TOO_LONG):
            return

        if not l:
            # creating a new promoted link
            l = promote.new_promotion(title, url if kind == 'link' else 'self',
                                      selftext if kind == 'self' else '',
                                      user, request.ip)
            if c.user_is_sponsor:
                l.managed_promo = is_managed
                l.domain_override = domain_override or None
                l.third_party_tracking = third_party_tracking or None
                l.third_party_tracking_2 = third_party_tracking_2 or None
            l._commit()

            # only set the thumbnail when creating a link
            if thumbnail_file:
                try:
                    force_thumbnail(l, thumbnail_file)
                    l._commit()
                except IOError:
                    pass

            form.redirect(promote.promo_edit_url(l))

        elif not promote.is_promo(l):
            return

        # changing link type is not allowed
        if ((l.is_self and kind == 'link') or
            (not l.is_self and kind == 'self')):
            c.errors.add(errors.NO_CHANGE_KIND, field="kind")
            form.set_error(errors.NO_CHANGE_KIND, "kind")
            return

        changed = False
        # live items can only be changed by a sponsor, and also
        # pay the cost of de-approving the link
        if not promote.is_promoted(l) or c.user_is_sponsor:
            if title and title != l.title:
                l.title = title
                changed = not c.user_is_sponsor

            if kind == 'link' and url and url != l.url:
                l.url = url
                changed = not c.user_is_sponsor

        # only trips if the title and url are changed by a non-sponsor
        if changed:
            promote.unapprove_promotion(l)

        # selftext can be changed at any time
        if kind == 'self':
            l.selftext = selftext

        # comment disabling and sendreplies is free to be changed any time.
        l.disable_comments = disable_comments
        l.sendreplies = sendreplies

        if c.user_is_sponsor:
            if (form.has_errors("media_url", errors.BAD_URL) or
                    form.has_errors("gifts_embed_url", errors.BAD_URL)):
                return

        scraper_embed = media_url_type == "scrape"
        media_url = media_url or None
        gifts_embed_url = gifts_embed_url or None

        if c.user_is_sponsor and scraper_embed and media_url != l.media_url:
            if media_url:
                media = _scrape_media(
                    media_url, autoplay=media_autoplay,
                    save_thumbnail=False, use_cache=True)

                if media:
                    l.set_media_object(media.media_object)
                    l.set_secure_media_object(media.secure_media_object)
                    l.media_url = media_url
                    l.gifts_embed_url = None
                    l.media_autoplay = media_autoplay
                else:
                    c.errors.add(errors.SCRAPER_ERROR, field="media_url")
                    form.set_error(errors.SCRAPER_ERROR, "media_url")
                    return
            else:
                l.set_media_object(None)
                l.set_secure_media_object(None)
                l.media_url = None
                l.gifts_embed_url = None
                l.media_autoplay = False

        if (c.user_is_sponsor and not scraper_embed and
                gifts_embed_url != l.gifts_embed_url):
            if gifts_embed_url:
                parsed = UrlParser(gifts_embed_url)
                if not is_subdomain(parsed.hostname, "redditgifts.com"):
                    c.errors.add(errors.BAD_URL, field="gifts_embed_url")
                    form.set_error(errors.BAD_URL, "gifts_embed_url")
                    return

                iframe = """
                    <iframe class="redditgifts-embed"
                            src="%(embed_url)s"
                            width="710" height="500" scrolling="no"
                            frameborder="0" allowfullscreen>
                    </iframe>
                """ % {'embed_url': websafe(gifts_embed_url)}
                media_object = {
                    'oembed': {
                        'description': 'redditgifts embed',
                        'height': 500,
                        'html': iframe,
                        'provider_name': 'redditgifts',
                        'provider_url': 'http://www.redditgifts.com/',
                        'title': 'redditgifts secret santa 2014',
                        'type': 'rich',
                        'width': 710},
                        'type': 'redditgifts'
                }
                l.set_media_object(media_object)
                l.set_secure_media_object(media_object)
                l.media_url = None
                l.gifts_embed_url = gifts_embed_url
                l.media_autoplay = False
            else:
                l.set_media_object(None)
                l.set_secure_media_object(None)
                l.media_url = None
                l.gifts_embed_url = None
                l.media_autoplay = False

        if c.user_is_sponsor:
            l.media_override = media_override
            l.domain_override = domain_override or None
            l.third_party_tracking = third_party_tracking or None
            l.third_party_tracking_2 = third_party_tracking_2 or None
            l.managed_promo = is_managed

        l._commit()
        form.redirect(promote.promo_edit_url(l))

    @validatedForm(
        VSponsorAdmin(),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        sr=VSubmitSR('sr', promotion=True),
    )
    def POST_add_roadblock(self, form, jquery, start, end, sr):
        if (form.has_errors('startdate', errors.BAD_DATE) or
                form.has_errors('enddate', errors.BAD_DATE)):
            return

        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', errors.BAD_DATE_RANGE)
            return

        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return

        PromotedLinkRoadblock.add(sr, start, end)
        jquery.refresh()

    @validatedForm(
        VSponsorAdmin(),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        sr=VSubmitSR('sr', promotion=True),
    )
    def POST_rm_roadblock(self, form, jquery, start, end, sr):
        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', errors.BAD_DATE_RANGE)
            return

        if start and end and sr:
            PromotedLinkRoadblock.remove(sr, start, end)
            jquery.refresh()

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        start=VDate('startdate'),
        end=VDate('enddate'),
        link=VLink('link_id36'),
        bid=VFloat('bid', coerce=False),
        target=VPromoTarget(),
        campaign_id36=nop("campaign_id36"),
        priority=VPriority("priority"),
        location=VLocation(),
        platform=VOneOf("platform", ("mobile", "desktop", "all"), default="desktop"),
        mobile_os=VList("mobile_os", choices=["iOS", "Android"]),
    )
    def POST_edit_campaign(self, form, jquery, link, campaign_id36,
                           start, end, bid, target, priority, location,
                           platform, mobile_os):
        if not link:
            return

        if platform in ('mobile', 'all') and not mobile_os:
            c.errors.add(errors.BAD_PROMO_MOBILE_OS, field='mobile_os')
            form.set_error(errors.BAD_PROMO_MOBILE_OS, 'mobile_os')
            return

        if platform == 'mobile' and priority.cpm:
            c.errors.add(errors.BAD_PROMO_MOBILE_PRIORITY, field='priority')
            form.set_error(errors.BAD_PROMO_MOBILE_PRIORITY, 'priority')
            return

        if not (c.user_is_sponsor or platform == 'desktop'):
            return abort(403, 'forbidden')

        if platform == 'desktop':
            mobile_os = None

        if not target:
            # run form.has_errors to populate the errors in the response
            form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                            errors.SUBREDDIT_NOTALLOWED,
                            errors.SUBREDDIT_REQUIRED)
            form.has_errors('collection', errors.COLLECTION_NOEXIST)
            form.has_errors('targeting', errors.INVALID_TARGET)
            return

        if not allowed_location_and_target(location, target):
            return abort(403, 'forbidden')

        cpm = PromotionPrices.get_price(c.user, target, location)

        if (form.has_errors('startdate', errors.BAD_DATE) or
                form.has_errors('enddate', errors.BAD_DATE)):
            return

        min_start, max_start, max_end = promote.get_date_limits(
            link, c.user_is_sponsor)

        if campaign_id36:
            promo_campaign = PromoCampaign._byID36(campaign_id36)
            if (promote.is_promoted(link) and
                    promo_campaign.start_date.date() <= min_start and
                    start != promo_campaign.start_date and
                    promo_campaign.is_paid):
                c.errors.add(errors.START_DATE_CANNOT_CHANGE, field='startdate')
                form.has_errors('startdate', errors.START_DATE_CANNOT_CHANGE)
                return
        elif start.date() < min_start:
            c.errors.add(errors.DATE_TOO_EARLY,
                         msg_params={'day': min_start.strftime("%m/%d/%Y")},
                         field='startdate')
            form.has_errors('startdate', errors.DATE_TOO_EARLY)
            return

        if start.date() > max_start:
            c.errors.add(errors.DATE_TOO_LATE,
                         msg_params={'day': max_start.strftime("%m/%d/%Y")},
                         field='startdate')
            form.has_errors('startdate', errors.DATE_TOO_LATE)
            return

        if end.date() > max_end:
            c.errors.add(errors.DATE_TOO_LATE,
                         msg_params={'day': max_end.strftime("%m/%d/%Y")},
                         field='enddate')
            form.has_errors('enddate', errors.DATE_TOO_LATE)
            return

        if end < start:
            c.errors.add(errors.BAD_DATE_RANGE, field='enddate')
            form.has_errors('enddate', errors.BAD_DATE_RANGE)
            return

        # Limit the number of PromoCampaigns a Link can have
        # Note that the front end should prevent the user from getting
        # this far
        existing_campaigns = list(PromoCampaign._by_link(link._id))
        if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK:
            c.errors.add(errors.TOO_MANY_CAMPAIGNS,
                         msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK},
                         field='title')
            form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
            return

        campaign = None
        if campaign_id36:
            try:
                campaign = PromoCampaign._byID36(campaign_id36, data=True)
            except NotFound:
                pass

            if campaign and (campaign._deleted or link._id != campaign.link_id):
                campaign = None

            if not campaign:
                return abort(404, 'not found')

        if priority.cpm:
            min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
            max_bid = None if c.user_is_sponsor else g.max_promote_bid

            if bid is None or bid < min_bid or (max_bid and bid > max_bid):
                c.errors.add(errors.BAD_BID, field='bid',
                             msg_params={'min': min_bid,
                                         'max': max_bid or g.max_promote_bid})
                form.has_errors('bid', errors.BAD_BID)
                return

            # you cannot edit the bid of a live ad unless it's a freebie
            if (campaign and bid != campaign.bid and
                promote.is_live_promo(link, campaign) and
                not campaign.is_freebie()):
                c.errors.add(errors.BID_LIVE, field='bid')
                form.has_errors('bid', errors.BID_LIVE)
                return

        else:
            bid = 0.   # Set bid to 0 as dummy value

        is_frontpage = (not target.is_collection and
                        target.subreddit_name == Frontpage.name)

        if not target.is_collection and not is_frontpage:
            # targeted to a single subreddit, check roadblock
            sr = target.subreddits_slow[0]
            roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end)
            if roadblock and not c.user_is_sponsor:
                msg_params = {"start": roadblock[0].strftime('%m/%d/%Y'),
                              "end": roadblock[1].strftime('%m/%d/%Y')}
                c.errors.add(errors.OVERSOLD, field='sr',
                             msg_params=msg_params)
                form.has_errors('sr', errors.OVERSOLD)
                return

        # Check inventory
        campaign = campaign if campaign_id36 else None
        if not priority.inventory_override:
            oversold = has_oversold_error(form, campaign, start, end, bid, cpm,
                                          target, location)
            if oversold:
                return

        dates = (start, end)
        if campaign:
            promote.edit_campaign(link, campaign, dates, bid, cpm, target,
                                  priority, location, platform, mobile_os)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, target,
                                            priority, location, platform, mobile_os)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(VSponsor('link_id36'),
                   VModhash(),
                   l=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_delete_campaign(self, form, jquery, l, campaign):
        if not campaign or not l or l._id != campaign.link_id:
            return abort(404, 'not found')

        promote.delete_campaign(l, campaign)

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   link=VLink('link_id36'),
                   campaign=VPromoCampaign("campaign_id36"))
    def POST_terminate_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        promote.terminate_campaign(link, campaign)
        rc = RenderableCampaign.from_campaigns(link, campaign)
        jquery.update_campaign(campaign._fullname, rc.render_html())

    @validatedForm(
        VSponsor('link'),
        VModhash(),
        link=VByName("link"),
        campaign=VPromoCampaign("campaign"),
        customer_id=VInt("customer_id", min=0),
        pay_id=VInt("account", min=0),
        edit=VBoolean("edit"),
        address=ValidAddress(
            ["firstName", "lastName", "company", "address", "city", "state",
             "zip", "country", "phoneNumber"]
        ),
        creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"]),
    )
    def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id,
                        edit, address, creditcard):
        if not g.authorizenetapi:
            return

        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        # Check inventory
        if campaign_has_oversold_error(form, campaign):
            return

        # check the campaign dates are still valid (user may have created
        # the campaign a few days ago)
        min_start, max_start, max_end = promote.get_date_limits(
            link, c.user_is_sponsor)

        if campaign.start_date.date() > max_start:
            msg = _("please change campaign start date to %(date)s or earlier")
            date = format_date(max_start, format="short", locale=c.locale)
            msg %= {'date': date}
            form.set_text(".status", msg)
            return

        if campaign.start_date.date() < min_start:
            msg = _("please change campaign start date to %(date)s or later")
            date = format_date(min_start, format="short", locale=c.locale)
            msg %= {'date': date}
            form.set_text(".status", msg)
            return

        new_payment = not pay_id

        address_modified = new_payment or edit
        if address_modified:
            address_fields = ["firstName", "lastName", "company", "address",
                              "city", "state", "zip", "country", "phoneNumber"]
            card_fields = ["cardNumber", "expirationDate", "cardCode"]

            if (form.has_errors(address_fields, errors.BAD_ADDRESS) or
                    form.has_errors(card_fields, errors.BAD_CARD)):
                return

            pay_id = edit_profile(c.user, address, creditcard, pay_id)

            if pay_id:
                promote.new_payment_method(user=c.user, ip=request.ip, address=address, link=link)

        if pay_id:
            success, reason = promote.auth_campaign(link, campaign, c.user,
                                                    pay_id)

            if success:
                hooks.get_hook("promote.campaign_paid").call(link=link, campaign=campaign)
                if not address and g.authorizenetapi:
                    profiles = get_account_info(c.user).paymentProfiles
                    profile = {p.customerPaymentProfileId: p for p in profiles}[pay_id]

                    address = profile.billTo

                promote.successful_payment(link, campaign, request.ip, address)

                jquery.payment_redirect(promote.promo_edit_url(link), new_payment, campaign.bid)
                return
            else:
                promote.failed_payment_method(c.user, link)
                msg = reason or _("failed to authenticate card. sorry.")
                form.set_text(".status", msg)
        else:
            promote.failed_payment_method(c.user, link)
            form.set_text(".status", _("failed to authenticate card. sorry."))

    @validate(
        VSponsor("link_name"),
        VModhash(),
        link=VByName('link_name'),
        file=VUploadLength('file', 500*1024),
        img_type=VImageType('img_type'),
    )
    def POST_link_thumb(self, link=None, file=None, img_type='jpg'):
        if not link or (promote.is_promoted(link) and not c.user_is_sponsor):
            # only let sponsors edit thumbnails of live promos
            return abort(403, 'forbidden')

        force_thumbnail(link, file, file_type=".%s" % img_type)
        link._commit()
        return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors,
                             form_id="image-upload").render()

    @validate(
        VSponsor("link_name"),
        VModhash(),
        link=VByName('link_name'),
        file=VUploadLength('file', 500*1024),
        img_type=VImageType('img_type'),
    )
    def POST_link_mobile_ad_image(self, link=None, file=None, img_type='jpg'):
        if not (link and c.user_is_sponsor and file):
            # only sponsors can set the mobile img
            return abort(403, 'forbidden')

        force_mobile_ad_image(link, file, file_type=".%s" % img_type)
        link._commit()
        return UploadedImage(_('saved'), link.mobile_ad_url, "", errors=errors,
                             form_id="mobile-ad-image-upload").render()
Ejemplo n.º 3
0
class ModmailController(OAuth2OnlyController):
    def pre(self):
        # Set user_is_admin property on context,
        # normally set but this controller does not inherit
        # from RedditController
        super(ModmailController, self).pre()

        admin_usernames = [
            name.lower() for name in g.live_config['modmail_admins']
        ]
        c.user_is_admin = False
        if c.user_is_loggedin:
            c.user_is_admin = c.user.name.lower() in admin_usernames

        VNotInTimeout().run()

    def post(self):
        Session.remove()
        super(ModmailController, self).post()

    @require_oauth2_scope('modmail')
    @validate(
        srs=VSRByNames('entity', required=False),
        after=VModConversation('after', required=False),
        limit=VInt('limit', num_default=25),
        sort=VOneOf('sort',
                    options=('recent', 'mod', 'user'),
                    default='recent'),
        state=VOneOf('state',
                     options=('new', 'inprogress', 'mod', 'notifications',
                              'archived', 'highlighted', 'all'),
                     default='all'),
    )
    def GET_conversations(self, srs, after, limit, sort, state):
        """Get conversations for logged in user or subreddits

        Querystring Params:
        entity   -- name of the subreddit or a comma separated list of
                    subreddit names (i.e. iama, pics etc)
        limit    -- number of elements to retrieve (default: 25)
        after    -- the id of the last item seen
        sort     -- parameter on how to sort the results, choices:
                    recent: max(last_user_update, last_mod_update)
                    mod: last_mod_update
                    user: last_user_update
        state    -- this parameter lets users filter messages by state
                    choices: new, inprogress, mod, notifications,
                    archived, highlighted, all
        """

        # Retrieve subreddits in question, if entities are passed
        # check if a user is a moderator for the passed entities.
        # If no entities are passed grab all subreddits the logged in
        # user moderates and has modmail permissions for
        modded_entities = {}
        modded_srs = c.user.moderated_subreddits('mail')
        modded_srs = {sr._fullname: sr for sr in modded_srs}

        if srs:
            for sr in srs.values():
                if sr._fullname in modded_srs:
                    modded_entities[sr._fullname] = sr
                else:
                    return self.send_error(403,
                                           errors.BAD_SR_NAME,
                                           fields='entity')
        else:
            modded_entities = modded_srs

        if not modded_entities:
            return self.send_error(404, errors.SR_NOT_FOUND, fields='entity')

        # Retrieve conversations for given entities
        conversations = ModmailConversation.get_mod_conversations(
            modded_entities.values(),
            viewer=c.user,
            limit=limit,
            after=after,
            sort=sort,
            state=state)

        conversation_ids = []
        conversations_dict = {}
        messages_dict = {}
        author_ids = []

        # Extract author ids to query for all accounts at once
        for conversation in conversations:
            author_ids.extend(conversation.author_ids)
            author_ids.extend(conversation.mod_action_account_ids)

        # Query for associated account object of authors and serialize the
        # conversation in the correct context
        authors = self._try_get_byID(author_ids, Account, ignore_missing=True)
        for conversation in conversations:
            conversation_ids.append(conversation.id36)

            conversations_dict[
                conversation.id36] = conversation.to_serializable(
                    authors,
                    modded_entities[conversation.owner_fullname],
                )

            latest_message = conversation.messages[0]
            messages_dict[
                latest_message.id36] = latest_message.to_serializable(
                    modded_entities[conversation.owner_fullname],
                    authors[latest_message.author_id],
                    c.user,
                )

        return simplejson.dumps({
            'viewerId': c.user._fullname,
            'conversationIds': conversation_ids,
            'conversations': conversations_dict,
            'messages': messages_dict,
        })

    @require_oauth2_scope('modmail')
    @validate(
        entity=VSRByName('srName'),
        subject=VLength('subject', max_length=100),
        body=VMarkdownLength('body'),
        is_author_hidden=VBoolean('isAuthorHidden', default=False),
        to=VModConvoRecipient('to', required=False),
    )
    def POST_conversations(self, entity, subject, body, is_author_hidden, to):
        """Creates a new conversation for a particular SR

        This endpoint will create a ModmailConversation object as
        well as the first ModmailMessage within the ModmailConversation
        object.

        POST Params:
        srName          -- the human readable name of the subreddit
        subject         -- the subject of the first message in the conversation
        body            -- the body of the first message in the conversation
        isAuthorHidden  -- boolean on whether the mod name should be hidden
                           (only mods can use this flag)
        to              -- name of the user that a mod wants to create a convo
                           with (only mods can use this flag)
        """
        self._feature_enabled_check(entity)

        # make sure the user is not muted when creating a new conversation
        if entity.is_muted(c.user) and not c.user_is_admin:
            return self.send_error(400, errors.USER_MUTED)

        # validate post params
        if (errors.USER_BLOCKED, to) in c.errors:
            return self.send_error(400, errors.USER_BLOCKED, fields='to')
        elif (errors.USER_DOESNT_EXIST, to) in c.errors:
            return self.send_error(404, errors.USER_DOESNT_EXIST, fields='to')

        if to and not isinstance(to, Account):
            return self.send_error(
                422,
                errors.NO_SR_TO_SR_MESSAGE,
                fields='to',
            )

        # only mods can set a 'to' parameter
        if (not entity.is_moderator_with_perms(c.user, 'mail') and to):
            return self.send_error(403, errors.MOD_REQUIRED, fields='to')

        if to and entity.is_muted(to):
            return self.send_error(
                400,
                errors.MUTED_FROM_SUBREDDIT,
                fields='to',
            )

        try:
            conversation = ModmailConversation(
                entity,
                c.user,
                subject,
                body,
                is_author_hidden=is_author_hidden,
                to=to,
            )
        except MustBeAModError:
            return self.send_error(403,
                                   errors.MOD_REQUIRED,
                                   fields='isAuthorHidden')
        except Exception as e:
            g.log.error('Failed to save conversation: {}'.format(e))
            return self.send_error(500, errors.CONVERSATION_NOT_SAVED)

        # Create copy of the message in the legacy messaging system as well
        if to:
            message, inbox_rel = Message._new(
                c.user,
                to,
                subject,
                body,
                request.ip,
                sr=entity,
                from_sr=is_author_hidden,
                create_modmail=False,
            )
        else:
            message, inbox_rel = Message._new(
                c.user,
                entity,
                subject,
                body,
                request.ip,
                create_modmail=False,
            )
        queries.new_message(message, inbox_rel)
        conversation.set_legacy_first_message_id(message._id)

        # Get author associated account object for serialization
        # of the newly created conversation object
        authors = self._try_get_byID(conversation.author_ids,
                                     Account,
                                     ignore_missing=True)

        response.status_code = 201
        serializable_convo = conversation.to_serializable(authors,
                                                          entity,
                                                          all_messages=True,
                                                          current_user=c.user)
        messages = serializable_convo.pop('messages')
        mod_actions = serializable_convo.pop('modActions')

        g.events.new_modmail_event(
            'ss.send_modmail_message',
            conversation,
            message=conversation.messages[0],
            msg_author=c.user,
            sr=entity,
            request=request,
            context=c,
        )

        return simplejson.dumps({
            'conversation': serializable_convo,
            'messages': messages,
            'modActions': mod_actions,
        })

    @require_oauth2_scope('modmail')
    @validate(
        conversation=VModConversation('conversation_id'),
        mark_read=VBoolean('markRead', default=False),
    )
    def GET_mod_messages(self, conversation, mark_read):
        """Returns all messages for a given conversation id

        Url Params:
        conversation_id -- this is the id of the conversation you would
                           like to grab messages for

        Querystring Param:
        markRead -- if passed the conversation will be marked read when the
                    conversation is returned
        """
        self._validate_vmodconversation()
        sr = self._try_get_subreddit_access(conversation, admin_override=True)
        authors = self._try_get_byID(list(
            set(conversation.author_ids)
            | set(conversation.mod_action_account_ids)),
                                     Account,
                                     ignore_missing=True)
        serializable_convo = conversation.to_serializable(authors,
                                                          sr,
                                                          all_messages=True,
                                                          current_user=c.user)

        messages = serializable_convo.pop('messages')
        mod_actions = serializable_convo.pop('modActions')

        # Get participant user info for conversation
        try:
            userinfo = self._get_modmail_userinfo(conversation, sr=sr)
        except ValueError:
            userinfo = {}
        except NotFound:
            return self.send_error(404, errors.USER_DOESNT_EXIST)

        if mark_read:
            conversation.mark_read(c.user)
            g.events.new_modmail_event(
                'ss.modmail_mark_thread',
                conversation,
                mark_type='read',
                request=request,
                context=c,
            )

        return simplejson.dumps({
            'conversation': serializable_convo,
            'messages': messages,
            'modActions': mod_actions,
            'user': userinfo,
        })

    @require_oauth2_scope('modmail')
    def GET_modmail_enabled_srs(self):
        # sr_name, sr_icon, subsriber_count, most_recent_action
        modded_srs = c.user.moderated_subreddits('mail')
        enabled_srs = [
            modded_sr for modded_sr in modded_srs
            if feature.is_enabled('new_modmail', subreddit=modded_sr.name)
        ]
        recent_convos = ModmailConversation.get_recent_convo_by_sr(enabled_srs)

        results = {}
        for sr in enabled_srs:
            results.update({
                sr._fullname: {
                    'id': sr._fullname,
                    'name': sr.name,
                    'icon': sr.icon_img,
                    'subscribers': sr._ups,
                    'lastUpdated': recent_convos.get(sr._fullname),
                }
            })

        return simplejson.dumps({'subreddits': results})

    @require_oauth2_scope('modmail')
    @validate(
        conversation=VModConversation('conversation_id'),
        msg_body=VMarkdownLength('body'),
        is_author_hidden=VBoolean('isAuthorHidden', default=False),
        is_internal=VBoolean('isInternal', default=False),
    )
    def POST_mod_messages(self, conversation, msg_body, is_author_hidden,
                          is_internal):
        """Creates a new message for a particular ModmailConversation

        URL Params:
        conversation_id -- id of the conversation to post a new message to

        POST Params:
        body            -- this is the message body
        isAuthorHidden  -- boolean on whether to hide author, i.e. respond as
                           the subreddit
        isInternal      -- boolean to signify a moderator only message
        """
        self._validate_vmodconversation()

        sr = Subreddit._by_fullname(conversation.owner_fullname)
        self._feature_enabled_check(sr)

        # make sure the user is not muted before posting a message
        if sr.is_muted(c.user):
            return self.send_error(400, errors.USER_MUTED)

        if conversation.is_internal and not is_internal:
            is_internal = True

        is_mod = sr.is_moderator(c.user)
        if not is_mod and is_author_hidden:
            return self.send_error(
                403,
                errors.MOD_REQUIRED,
                fields='isAuthorHidden',
            )
        elif not is_mod and is_internal:
            return self.send_error(
                403,
                errors.MOD_REQUIRED,
                fields='isInternal',
            )

        try:
            if not conversation.is_internal and not conversation.is_auto:
                participant = conversation.get_participant_account()

                if participant and sr.is_muted(participant):
                    return self.send_error(
                        400,
                        errors.MUTED_FROM_SUBREDDIT,
                    )
        except NotFound:
            pass

        try:
            new_message = conversation.add_message(
                c.user,
                msg_body,
                is_author_hidden=is_author_hidden,
                is_internal=is_internal,
            )
        except:
            return self.send_error(500, errors.MODMAIL_MESSAGE_NOT_SAVED)

        # Add the message to the legacy messaging system as well (unless it's
        # an internal message on a non-internal conversation, since we have no
        # way to hide specific messages from the external participant)
        legacy_incompatible = is_internal and not conversation.is_internal
        if (conversation.legacy_first_message_id and not legacy_incompatible):
            first_message = Message._byID(conversation.legacy_first_message_id)
            subject = conversation.subject
            if not subject.startswith('re: '):
                subject = 're: ' + subject

            # Retrieve the participant to decide whether to send the message
            # to the sr or to the participant. If the currently logged in user
            # is the same as the participant then address the message to the
            # sr.
            recipient = sr
            if not is_internal:
                try:
                    participant = (
                        ModmailConversationParticipant.get_participant(
                            conversation.id))

                    is_participant = (
                        (c.user._id == participant.account_id)
                        and not sr.is_moderator_with_perms(c.user, 'mail'))

                    if not is_participant:
                        recipient = Account._byID(participant.account_id)
                except NotFound:
                    pass

            message, inbox_rel = Message._new(
                c.user,
                recipient,
                subject,
                msg_body,
                request.ip,
                parent=first_message,
                from_sr=is_author_hidden,
                create_modmail=False,
            )
            queries.new_message(message, inbox_rel)

        serializable_convo = conversation.to_serializable(
            entity=sr,
            all_messages=True,
            current_user=c.user,
        )
        messages = serializable_convo.pop('messages')

        g.events.new_modmail_event(
            'ss.send_modmail_message',
            conversation,
            message=new_message,
            msg_author=c.user,
            sr=sr,
            request=request,
            context=c,
        )

        response.status_code = 201
        return simplejson.dumps({
            'conversation': serializable_convo,
            'messages': messages,
        })

    @require_oauth2_scope('modmail')
    @validate(conversation=VModConversation('conversation_id'))
    def POST_highlight(self, conversation):
        """Marks a conversation as highlighted."""
        self._validate_vmodconversation()
        self._try_get_subreddit_access(conversation)
        conversation.add_action(c.user, 'highlighted')
        conversation.add_highlight()

        # Retrieve updated conversation to be returned
        updated_convo = self._get_updated_convo(conversation.id, c.user)

        g.events.new_modmail_event(
            'ss.modmail_mark_thread',
            conversation,
            mark_type='highlight',
            request=request,
            context=c,
        )

        return simplejson.dumps(updated_convo)

    @require_oauth2_scope('modmail')
    @validate(conversation=VModConversation('conversation_id'))
    def DELETE_highlight(self, conversation):
        """Removes a highlight from a conversation."""
        self._validate_vmodconversation()
        self._try_get_subreddit_access(conversation)
        conversation.add_action(c.user, 'unhighlighted')
        conversation.remove_highlight()

        # Retrieve updated conversation to be returned
        updated_convo = self._get_updated_convo(conversation.id, c.user)

        g.events.new_modmail_event(
            'ss.modmail_mark_thread',
            conversation,
            mark_type='unhighlight',
            request=request,
            context=c,
        )

        return simplejson.dumps(updated_convo)

    @require_oauth2_scope('modmail')
    @validate(ids=VList('conversationIds'))
    def POST_unread(self, ids):
        """Marks conversations as unread for the user.

        Expects a list of conversation IDs.
        """
        if not ids:
            return self.send_error(400, 'Must pass an id or list of ids.')

        try:
            ids = [int(id, base=36) for id in ids]
        except:
            return self.send_error(422, 'Must pass base 36 ids.')

        try:
            convos = self._get_conversation_access(ids)
        except ValueError:
            return self.send_error(
                403,
                errors.INVALID_CONVERSATION_ID,
                fields='conversationIds',
            )

        ModmailConversationUnreadState.mark_unread(
            c.user, [convo.id for convo in convos])

    @require_oauth2_scope('modmail')
    @validate(ids=VList('conversationIds'))
    def POST_read(self, ids):
        """Marks a conversations as read for the user.

        Expects a list of conversation IDs.
        """
        if not ids:
            return self.send_error(400, 'Must pass an id or list of ids.')

        try:
            ids = [int(id, base=36) for id in ids]
        except:
            return self.send_error(422, 'Must pass base 36 ids.')

        try:
            convos = self._get_conversation_access(ids)
        except ValueError:
            return self.send_error(
                403,
                errors.INVALID_CONVERSATION_ID,
                fields='conversationIds',
            )

        response.status_code = 204
        ModmailConversationUnreadState.mark_read(
            c.user, [convo.id for convo in convos])

    @require_oauth2_scope('modmail')
    @validate(
        ids=VList('conversationIds'),
        archive=VBoolean('archive', default=True),
    )
    def POST_archive_status(self, ids, archive):
        try:
            convos = self._get_conversation_access(
                [int(id, base=36) for id in ids])
        except ValueError:
            return self.send_error(
                403,
                errors.INVALID_CONVERSATION_ID,
                fields='conversationIds',
            )

        convo_ids = []
        for convo in convos:
            if convo.is_internal:
                return self.send_error(
                    422,
                    errors.CONVERSATION_NOT_ARCHIVABLE,
                    fields='conversationIds',
                )
            convo_ids.append(convo.id)

        if not archive:
            ModmailConversation.set_states(
                convo_ids, ModmailConversation.STATE['inprogress'])
        else:
            ModmailConversation.set_states(
                convo_ids, ModmailConversation.STATE['archived'])

        response.status_code = 204

    @require_oauth2_scope('modmail')
    @validate(conversation=VModConversation('conversation_id'))
    def POST_archive(self, conversation):
        self._validate_vmodconversation()
        sr = Subreddit._by_fullname(conversation.owner_fullname)
        self._feature_enabled_check(sr)

        if sr.is_moderator_with_perms(c.user, 'mail'):
            if conversation.state == ModmailConversation.STATE['archived']:
                response.status_code = 204
                return

            if conversation.is_internal:
                return self.send_error(
                    422,
                    errors.CONVERSATION_NOT_ARCHIVABLE,
                )

            conversation.add_action(c.user, 'archived')
            conversation.set_state('archived')
            updated_convo = self._get_updated_convo(conversation.id, c.user)

            g.events.new_modmail_event(
                'ss.modmail_mark_thread',
                conversation,
                mark_type='archive',
                request=request,
                context=c,
            )

            return simplejson.dumps(updated_convo)
        else:
            return self.send_error(403, errors.INVALID_MOD_PERMISSIONS)

    @require_oauth2_scope('modmail')
    @validate(conversation=VModConversation('conversation_id'))
    def POST_unarchive(self, conversation):
        self._validate_vmodconversation()
        sr = Subreddit._by_fullname(conversation.owner_fullname)
        self._feature_enabled_check(sr)

        if sr.is_moderator_with_perms(c.user, 'mail'):
            if conversation.state != ModmailConversation.STATE['archived']:
                response.status_code = 204
                return

            if conversation.is_internal:
                return self.send_error(
                    422,
                    errors.CONVERSATION_NOT_ARCHIVABLE,
                )

            conversation.add_action(c.user, 'unarchived')
            conversation.set_state('inprogress')
            updated_convo = self._get_updated_convo(conversation.id, c.user)

            g.events.new_modmail_event(
                'ss.modmail_mark_thread',
                conversation,
                mark_type='unarchive',
                request=request,
                context=c,
            )

            return simplejson.dumps(updated_convo)
        else:
            return self.send_error(403, errors.INVALID_MOD_PERMISSIONS)

    @require_oauth2_scope('modmail')
    def GET_unread_convo_count(self):
        """Endpoint to retrieve the unread conversation count by
        category"""

        convo_counts = ModmailConversation.unread_convo_count(c.user)
        return simplejson.dumps(convo_counts)

    @require_oauth2_scope('modmail')
    @validate(conversation=VModConversation('conversation_id'))
    def GET_modmail_userinfo(self, conversation):
        # validate that the currently logged in user is a mod
        # of the subreddit associated with the conversation
        self._try_get_subreddit_access(conversation, admin_override=True)
        try:
            userinfo = self._get_modmail_userinfo(conversation)
        except (ValueError, NotFound):
            return self.send_error(404, errors.USER_DOESNT_EXIST)

        return simplejson.dumps(userinfo)

    @require_oauth2_scope('identity')
    @validate(conversation=VModConversation('conversation_id'))
    def POST_mute_participant(self, conversation):

        if conversation.is_internal or conversation.is_auto:
            return self.send_error(400, errors.CANT_RESTRICT_MODERATOR)

        sr = Subreddit._by_fullname(conversation.owner_fullname)

        try:
            participant = conversation.get_participant_account()
        except NotFound:
            return self.send_error(404, errors.USER_DOESNT_EXIST)

        if not sr.can_mute(c.user, participant):
            return self.send_error(400, errors.CANT_RESTRICT_MODERATOR)

        if not c.user_is_admin:
            if not sr.is_moderator_with_perms(c.user, 'access', 'mail'):
                return self.send_error(403, errors.INVALID_MOD_PERMISSIONS)

            if sr.use_quotas:
                sr_ratelimit = SimpleRateLimit(
                    name="sr_muted_%s" % sr._id36,
                    seconds=g.sr_quota_time,
                    limit=g.sr_muted_quota,
                )
                if not sr_ratelimit.record_and_check():
                    return self.send_error(403, errors.SUBREDDIT_RATELIMIT)

        # Add the mute record but only if successful create the
        # appropriate notifications, this prevents duplicate
        # notifications from being sent
        added = sr.add_muted(participant)

        if not added:
            return simplejson.dumps(
                self._convo_to_serializable(conversation, all_messages=True))

        MutedAccountsBySubreddit.mute(sr, participant, c.user)
        permalink = conversation.make_permalink()

        # Create the appropriate objects to be displayed on the
        # mute moderation log, use the permalink to the new modmail
        # system
        ModAction.create(sr,
                         c.user,
                         'muteuser',
                         target=participant,
                         description=permalink)
        sr.add_rel_note('muted', participant, permalink)

        # Add the muted mod action to the conversation
        conversation.add_action(c.user, 'muted', commit=True)

        result = self._get_updated_convo(conversation.id, c.user)
        result['user'] = self._get_modmail_userinfo(conversation, sr=sr)

        return simplejson.dumps(result)

    @require_oauth2_scope('identity')
    @validate(conversation=VModConversation('conversation_id'))
    def POST_unmute_participant(self, conversation):

        if conversation.is_internal or conversation.is_auto:
            return self.send_error(400, errors.CANT_RESTRICT_MODERATOR)

        sr = Subreddit._by_fullname(conversation.owner_fullname)

        try:
            participant = conversation.get_participant_account()
        except NotFound:
            abort(404, errors.USER_DOESNT_EXIST)

        if not c.user_is_admin:
            if not sr.is_moderator_with_perms(c.user, 'access', 'mail'):
                return self.send_error(403, errors.INVALID_MOD_PERMISSIONS)

        removed = sr.remove_muted(participant)
        if not removed:
            return simplejson.dumps(
                self._convo_to_serializable(conversation, all_messages=True))

        MutedAccountsBySubreddit.unmute(sr, participant)
        ModAction.create(sr, c.user, 'unmuteuser', target=participant)
        conversation.add_action(c.user, 'unmuted', commit=True)

        result = self._get_updated_convo(conversation.id, c.user)
        result['user'] = self._get_modmail_userinfo(conversation, sr=sr)

        return simplejson.dumps(result)

    def _get_modmail_userinfo(self, conversation, sr=None):
        if conversation.is_internal:
            raise ValueError('Cannot get userinfo for internal conversations')

        if not sr:
            sr = Subreddit._by_fullname(conversation.owner_fullname)

        # Retrieve the participant associated with the conversation
        try:
            account = conversation.get_participant_account()

            if not account:
                raise ValueError('No account associated with convo')

            permatimeout = (account.in_timeout
                            and account.days_remaining_in_timeout == 0)

            if account._deleted or permatimeout:
                raise ValueError('User info is inaccessible')
        except NotFound:
            raise NotFound('Unable to retrieve conversation participant')

        # Fetch the mute and ban status of the participant as it relates
        # to the subreddit associated with the conversation.
        mute_status = sr.is_muted(account)
        ban_status = sr.is_banned(account)

        # Parse the ban status and retrieve the length of the ban,
        # then output the data into a serialiazable dict
        ban_result = {
            'isBanned': bool(ban_status),
            'reason': '',
            'endDate': None,
            'isPermanent': False
        }

        if ban_status:
            ban_result['reason'] = getattr(ban_status, 'note', '')

            ban_duration = sr.get_tempbans('banned', account.name)
            ban_duration = ban_duration.get(account.name)

            if ban_duration:
                ban_result['endDate'] = ban_duration.isoformat()
            else:
                ban_result['isPermanent'] = True
                ban_result['endDate'] = None

        # Parse the mute status and retrieve the length of the ban,
        # then output the data into the serialiazable dict
        mute_result = {
            'isMuted': bool(mute_status),
            'endDate': None,
            'reason': ''
        }

        if mute_status:
            mute_result['reason'] = getattr(mute_status, 'note', '')

            muted_items = sr.get_muted_items(account.name)
            mute_duration = muted_items.get(account.name)
            if mute_duration:
                mute_result['endDate'] = mute_duration.isoformat()

        # Retrieve the participants post and comment fullnames from cache
        post_fullnames = []
        comment_fullnames = []
        if not account._spam:
            post_fullnames = list(queries.get_submitted(account, 'new',
                                                        'all'))[:100]

            comment_fullnames = list(
                queries.get_comments(account, 'new', 'all'))[:100]

        # Retrieve the associated link objects for posts and comments
        # using the retrieve fullnames, afer the link objects are retrieved
        # create a serializable dict with the the necessary information from
        # the endpoint.
        lookup_fullnames = list(set(post_fullnames) | set(comment_fullnames))
        posts = Thing._by_fullname(lookup_fullnames)

        serializable_posts = {}
        for fullname in post_fullnames:
            if len(serializable_posts) == 3:
                break

            post = posts[fullname]
            if post.sr_id == sr._id and not post._deleted:
                serializable_posts[fullname] = {
                    'title': post.title,
                    'permalink': post.make_permalink(sr, force_domain=True),
                    'date': post._date.isoformat(),
                }

        # Extract the users most recent comments associated with the
        # subreddit
        sr_comments = []
        for fullname in comment_fullnames:
            if len(sr_comments) == 3:
                break

            comment = posts[fullname]
            if comment.sr_id == sr._id and not comment._deleted:
                sr_comments.append(comment)

        # Retrieve all associated link objects (combines lookup)
        comment_links = Link._byID(
            [sr_comment.link_id for sr_comment in sr_comments])

        # Serialize all of the user's sr comments
        serializable_comments = {}
        for sr_comment in sr_comments:
            comment_link = comment_links[sr_comment.link_id]
            comment_body = sr_comment.body
            if len(comment_body) > 140:
                comment_body = '{:.140}...'.format(comment_body)

            serializable_comments[sr_comment._fullname] = {
                'title':
                comment_link.title,
                'comment':
                comment_body,
                'permalink':
                sr_comment.make_permalink(comment_link, sr, force_domain=True),
                'date':
                sr_comment._date.isoformat(),
            }

        return {
            'id': account._fullname,
            'name': account.name,
            'created': account._date.isoformat(),
            'banStatus': ban_result,
            'isShadowBanned': account._spam,
            'muteStatus': mute_result,
            'recentComments': serializable_comments,
            'recentPosts': serializable_posts,
        }

    def _get_updated_convo(self, convo_id, user):
        # Retrieve updated conversation to be returned
        updated_convo = ModmailConversation._byID(convo_id, current_user=user)

        return self._convo_to_serializable(updated_convo, all_messages=True)

    def _convo_to_serializable(self, conversation, all_messages=False):

        serialized_convo = conversation.to_serializable(
            all_messages=all_messages, current_user=c.user)
        messages = serialized_convo.pop('messages')
        mod_actions = serialized_convo.pop('modActions')

        return {
            'conversations': serialized_convo,
            'messages': messages,
            'modActions': mod_actions,
        }

    def _validate_vmodconversation(self):
        if (errors.CONVERSATION_NOT_FOUND, 'conversation_id') in c.errors:
            return self.send_error(404, errors.CONVERSATION_NOT_FOUND)

    def _get_conversation_access(self, ids):
        validated_convos = []
        conversations = ModmailConversation._byID(ids)

        # fetch all srs that a user has modmail permissions to
        # transform sr to be a dict with a key being the sr fullname
        # and the value being the sr object itself
        modded_srs = c.user.moderated_subreddits('mail')
        sr_by_fullname = {
            sr._fullname: sr
            for sr in modded_srs
            if feature.is_enabled('new_modmail', subreddit=sr.name)
        }

        for conversation in tup(conversations):
            if sr_by_fullname.get(conversation.owner_fullname):
                validated_convos.append(conversation)
            else:
                raise ValueError('Invalid conversation id(s).')

        return validated_convos

    def _try_get_byID(self,
                      ids,
                      thing_class,
                      return_dict=True,
                      ignore_missing=False):
        """Helper method to lookup objects by id for a
        given model or return a 404 if not found"""

        try:
            return thing_class._byID(ids,
                                     return_dict=return_dict,
                                     ignore_missing=ignore_missing)
        except NotFound:
            return self.send_error(404,
                                   errors.THING_NOT_FOUND,
                                   explanation='{} not found'.format(
                                       thing_class.__name__))
        except:
            return self.send_error(422, 'Invalid request')

    def _try_get_subreddit_access(self, conversation, admin_override=False):
        sr = Subreddit._by_fullname(conversation.owner_fullname)
        self._feature_enabled_check(sr)

        if (not sr.is_moderator_with_perms(c.user, 'mail')
                and not (admin_override and c.user_is_admin)):
            return self.send_error(
                403,
                errors.SUBREDDIT_NO_ACCESS,
            )

        return sr

    def _feature_enabled_check(self, sr):
        if not feature.is_enabled('new_modmail', subreddit=sr.name):
            return self.send_error(403, errors.SR_FEATURE_NOT_ENABLED)

    def send_error(self, code, error, fields=None, explanation=None):
        abort(
            reddit_http_error(
                code=code or error.code,
                error_name=error,
                explanation=explanation,
                fields=tup(fields),
            ))
Ejemplo n.º 4
0
class APIv1Controller(OAuth2ResourceController):
    def pre(self):
        OAuth2ResourceController.pre(self)
        self.authenticate_with_token()
        self.run_sitewide_ratelimits()

    def try_pagecache(self):
        pass

    @staticmethod
    def on_validation_error(error):
        abort_with_error(error, error.code or 400)

    @require_oauth2_scope("identity")
    @api_doc(api_section.account)
    def GET_me(self):
        """Returns the identity of the user currently authenticated via OAuth."""
        resp = IdentityJsonTemplate().data(c.oauth_user)
        return self.api_wrapper(resp)

    @require_oauth2_scope("identity")
    @validate(
        fields=VList(
            "fields",
            choices=PREFS_JSON_SPEC.spec.keys(),
            error=errors.errors.NON_PREFERENCE,
        ), )
    @api_doc(api_section.account, uri='/api/v1/me/prefs')
    def GET_prefs(self, fields):
        """Return the preference settings of the logged in user"""
        resp = PrefsJsonTemplate(fields).data(c.oauth_user)
        return self.api_wrapper(resp)

    def _get_usertrophies(self, user):
        trophies = Trophy.by_account(user)

        def visible_trophy(trophy):
            return trophy._thing2.awardtype != 'invisible'

        trophies = filter(visible_trophy, trophies)
        resp = TrophyListJsonTemplate().render(trophies)
        return self.api_wrapper(resp.finalize())

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName('username'), )
    @api_doc(
        section=api_section.users,
        uri='/api/v1/user/{username}/trophies',
    )
    def GET_usertrophies(self, user):
        """Return a list of trophies for the a given user."""
        return self._get_usertrophies(user)

    @require_oauth2_scope("identity")
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/trophies',
    )
    def GET_trophies(self):
        """Return a list of trophies for the current user."""
        return self._get_usertrophies(c.oauth_user)

    @require_oauth2_scope("mysubreddits")
    @api_doc(
        section=api_section.account,
        uri='/api/v1/me/karma',
    )
    def GET_karma(self):
        """Return a breakdown of subreddit karma."""
        karmas = c.oauth_user.all_karmas(include_old=False)
        resp = KarmaListJsonTemplate().render(karmas)
        return self.api_wrapper(resp.finalize())

    PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True)

    @require_oauth2_scope("account")
    @validate(validated_prefs=PREFS_JSON_VALIDATOR)
    @api_doc(api_section.account,
             json_model=PREFS_JSON_VALIDATOR,
             uri='/api/v1/me/prefs')
    def PATCH_prefs(self, validated_prefs):
        user_prefs = c.user.preferences()
        for short_name, new_value in validated_prefs.iteritems():
            pref_name = "pref_" + short_name
            if pref_name == "pref_content_langs":
                new_value = vprefs.format_content_lang_pref(new_value)
            user_prefs[pref_name] = new_value
        vprefs.filter_prefs(user_prefs, c.user)
        vprefs.set_prefs(c.user, user_prefs)
        c.user._commit()
        return self.api_wrapper(PrefsJsonTemplate().data(c.user))