Esempio n. 1
0
class PromoteApiController(ApiController):
    @json_validate(sr=VSubmitSR('sr', promotion=True),
                   collection=VCollection('collection'),
                   location=VLocation(),
                   start=VDate('startdate'),
                   end=VDate('enddate'))
    def GET_check_inventory(self, responder, sr, collection, location, start,
                            end):
        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,
                                                      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("<p>" + websafe(text) +
                                                        "</p>")

    @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'))

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'),
        VShamedDomain('url'),
        username=VLength('username', 100, empty_error=None),
        l=VLink('link_id36'),
        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),
        is_managed=VBoolean("is_managed"),
    )
    def POST_edit_promo(self, form, jquery, username, l, title, url, selftext,
                        kind, disable_comments, sendreplies, media_url,
                        media_autoplay, media_override, gifts_embed_url,
                        media_url_type, domain_override, is_managed):

        should_ratelimit = False
        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)
            l.domain_override = domain_override or None
            if c.user_is_sponsor:
                l.managed_promo = is_managed
            l._commit()
            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.managed_promo = is_managed

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

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_add_roadblock(self, form, jquery, dates, sr):
        if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors(
                'enddate', errors.BAD_DATE, errors.BAD_DATE_RANGE)):
            return
        if form.has_errors('sr', errors.SUBREDDIT_NOEXIST,
                           errors.SUBREDDIT_NOTALLOWED,
                           errors.SUBREDDIT_REQUIRED):
            return
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.add(sr, sd, ed)
            jquery.refresh()

    @validatedForm(VSponsorAdmin(),
                   VModhash(),
                   dates=VDateRange(['startdate', 'enddate'],
                                    reference_date=promote.promo_datetime_now),
                   sr=VSubmitSR('sr', promotion=True))
    def POST_rm_roadblock(self, form, jquery, dates, sr):
        if dates and sr:
            sd, ed = dates
            PromotedLinkRoadblock.remove(sr, sd, ed)
            jquery.refresh()

    @validatedForm(
        VSponsor('link_id36'),
        VModhash(),
        dates=VDateRange(['startdate', 'enddate'],
                         earliest=timedelta(days=g.min_promote_future),
                         latest=timedelta(days=g.max_promote_future),
                         reference_date=promote.promo_datetime_now,
                         business_days=True,
                         sponsor_override=True),
        link=VLink('link_id36'),
        bid=VFloat('bid', coerce=False),
        target=VPromoTarget(),
        campaign_id36=nop("campaign_id36"),
        priority=VPriority("priority"),
        location=VLocation(),
    )
    def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid,
                           target, priority, location):
        if not link:
            return

        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

        start, end = dates or (None, None)

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

        cpm = PromotionPrices.get_price(target, location)

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

        # check that start is not so late that authorization hold will expire
        if not c.user_is_sponsor:
            max_start = promote.get_max_startdate()
            if start > 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

        # 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)
            except NotFound:
                pass

        if campaign and link._id != campaign.link_id:
            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

        if campaign:
            promote.edit_campaign(link, campaign, dates, bid, cpm, target,
                                  priority, location)
        else:
            campaign = promote.new_campaign(link, dates, bid, cpm, target,
                                            priority, location)
        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 that start is not so late that authorization hold will expire
        max_start = promote.get_max_startdate()
        if campaign.start_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

        # check the campaign start date is still valid (user may have created
        # the campaign a few days ago)
        now = promote.promo_datetime_now()
        min_start = now + timedelta(days=g.min_promote_future)
        if campaign.start_date.date() < min_start.date():
            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

        address_modified = not pay_id 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)

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

            if success:
                form.redirect(promote.promo_edit_url(link))
                return

        msg = reason or _("failed to authenticate card. sorry.")
        form.set_text(".status", msg)

    @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 link and (not promote.is_promoted(link) or c.user_is_sponsor):
            errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="")

            # thumnails for promoted links can change and therefore expire
            force_thumbnail(link, file, file_type=".%s" % img_type)

            if any(errors.values()):
                return UploadedImage("",
                                     "",
                                     "upload",
                                     errors=errors,
                                     form_id="image-upload").render()
            else:
                link._commit()
                return UploadedImage(_('saved'),
                                     thumbnail_url(link),
                                     "",
                                     errors=errors,
                                     form_id="image-upload").render()
Esempio n. 2
0
class LiveUpdateController(RedditController):
    def __before__(self, event):
        RedditController.__before__(self)

        if event:
            try:
                c.liveupdate_event = LiveUpdateEvent._byID(event)
            except tdb_cassandra.NotFound:
                pass

        if not c.liveupdate_event:
            self.abort404()

        if c.user_is_loggedin:
            c.liveupdate_permissions = \
                    c.liveupdate_event.get_permissions(c.user)

            # revoke some permissions from everyone after closing
            if c.liveupdate_event.state != "live":
                c.liveupdate_permissions = (c.liveupdate_permissions
                    .without("update")
                    .without("close")
                )

            if c.user_is_admin:
                c.liveupdate_permissions = ContributorPermissionSet.SUPERUSER
        else:
            c.liveupdate_permissions = ContributorPermissionSet.NONE

        if c.liveupdate_event.banned and not c.liveupdate_permissions:
            error_page = RedditError(
                title=_("this thread has been banned"),
                message="",
                image="subreddit-banned.png",
            )
            request.environ["usable_error_content"] = error_page.render()
            self.abort403()

        if (c.liveupdate_event.nsfw and
                not c.over18 and
                request.host != g.media_domain and  # embeds are special
                c.render_style == "html"):
            return self.intermediate_redirect("/over18", sr_path=False)

    @require_oauth2_scope("read")
    @validate(
        num=VLimit("limit", default=25, max_limit=100),
        after=VLiveUpdateID("after"),
        before=VLiveUpdateID("before"),
        count=VCount("count"),
        is_embed=VBoolean("is_embed", docs={"is_embed": "(internal use only)"}),
        style_sr=VSRByName("stylesr"),
    )
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}",
        supports_rss=True,
        notes=[paginated_listing.doc_note],
    )
    def GET_listing(self, num, after, before, count, is_embed, style_sr):
        """Get a list of updates posted in this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """

        # preemptively record activity for clients that don't send pixel pings.
        # this won't capture their continued visit, but will at least show a
        # correct activity count for short lived connections.
        record_activity(c.liveupdate_event._id)

        reverse = False
        if before:
            reverse = True
            after = before

        query = LiveUpdateStream.query([c.liveupdate_event._id],
                                       count=num, reverse=reverse)
        if after:
            query.column_start = after
        builder = LiveUpdateBuilder(query=query, skip=True,
                                    reverse=reverse, num=num,
                                    count=count)
        listing = pages.LiveUpdateListing(builder)
        wrapped_listing = listing.listing()

        if c.user_is_loggedin:
            report_type = LiveUpdateReportsByAccount.get_report(
                c.user, c.liveupdate_event)
        else:
            report_type = None

        content = pages.LiveUpdateEventApp(
            event=c.liveupdate_event,
            listing=wrapped_listing,
            show_sidebar=not is_embed,
            report_type=report_type,
        )

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + "/about.json",
            Wrapped(c.liveupdate_event),
        )

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + ".json",
            wrapped_listing,
        )

        if not is_embed:
            return pages.LiveUpdateEventAppPage(
                content=content,
                page_classes=['liveupdate-app'],
            ).render()
        else:
            # ensure we're off the cookie domain before allowing embedding
            if request.host != g.media_domain:
                abort(404)
            c.allow_framing = True

            # interstitial redirects and nsfw settings are funky on the media
            # domain. just disable nsfw embeds.
            if c.liveupdate_event.nsfw:
                embed_page = pages.LiveUpdateEventEmbed(
                    content=pages.LiveUpdateNSFWEmbed(),
                )
                request.environ["usable_error_content"] = embed_page.render()
                abort(403)

            embed_page = pages.LiveUpdateEventEmbed(
                content=content,
                page_classes=['liveupdate-app'],
            )

            if style_sr and getattr(style_sr, "type", "private") != "private":
                c.can_apply_styles = True
                c.allow_styles = True
                embed_page.subreddit_stylesheet_url = \
                    Reddit.get_subreddit_stylesheet_url(style_sr)

            return embed_page.render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/updates/{update_id}",
    )
    def GET_focus(self, target):
        """Get details about a specific update in a live thread."""
        try:
            target = uuid.UUID(target)
        except (TypeError, ValueError):
            self.abort404()

        try:
            update = LiveUpdateStream.get_update(c.liveupdate_event, target)
        except tdb_cassandra.NotFound:
            self.abort404()

        if update.deleted:
            self.abort404()

        query = FocusQuery([update])

        builder = LiveUpdateBuilder(
            query=query, skip=True, reverse=True, num=1, count=0)
        listing = pages.LiveUpdateListing(builder)
        wrapped_listing = listing.listing()

        c.js_preload.set_wrapped(
            "/live/" + c.liveupdate_event._id + ".json",
            wrapped_listing,
        )

        content = pages.LiveUpdateFocusApp(
            event=c.liveupdate_event,
            listing=wrapped_listing,
        )

        return pages.LiveUpdateEventFocusPage(
            content=content,
            focused_update=update,
            page_classes=["liveupdate-focus"],
        ).render()

    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/about",
    )
    def GET_about(self):
        """Get some basic information about the live thread.

        See also: [/api/live/*thread*/edit](#POST_api_live_{thread}_edit).

        """
        if not is_api():
            self.abort404()
        content = Wrapped(c.liveupdate_event)
        return pages.LiveUpdateEventPage(content=content).render()

    @require_oauth2_scope("read")
    @base_listing
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/discussions",
        supports_rss=True,
    )
    def GET_discussions(self, num, after, reverse, count):
        """Get a list of reddit submissions linking to this thread."""
        builder = url_links_builder(
            url="/live/" + c.liveupdate_event._id,
            num=num,
            after=after,
            reverse=reverse,
            count=count,
        )
        listing = LinkListing(builder).listing()
        return pages.LiveUpdateEventPage(
            content=listing,
        ).render()

    def GET_edit(self):
        if not (c.liveupdate_permissions.allow("settings") or
                c.liveupdate_permissions.allow("close")):
            abort(403)

        return pages.LiveUpdateEventPage(
            content=pages.LiveUpdateEventConfiguration(),
        ).render()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("settings"),
        VModhash(),
        **EVENT_CONFIGURATION_VALIDATORS
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_edit(self, form, jquery, title, description, resources, nsfw):
        """Configure the thread.

        Requires the `settings` permission for this thread.

        See also: [/live/*thread*/about.json](#GET_live_{thread}_about.json).

        """
        if not is_event_configuration_valid(form):
            return

        changes = {}
        if title != c.liveupdate_event.title:
            changes["title"] = title
        if description != c.liveupdate_event.description:
            changes["description"] = description
            changes["description_html"] = safemarkdown(description, nofollow=True) or ""
        if resources != c.liveupdate_event.resources:
            changes["resources"] = resources
            changes["resources_html"] = safemarkdown(resources, nofollow=True) or ""
        if nsfw != c.liveupdate_event.nsfw:
            changes["nsfw"] = nsfw

        if changes:
            _broadcast(type="settings", payload=changes)

        c.liveupdate_event.title = title
        c.liveupdate_event.description = description
        c.liveupdate_event.resources = resources
        c.liveupdate_event.nsfw = nsfw
        c.liveupdate_event._commit()

        amqp.add_item("liveupdate_event_edited", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "editor_fullname": c.user._fullname,
        }))

        form.set_html(".status", _("saved"))
        form.refresh()

    # TODO: pass listing params on
    @require_oauth2_scope("read")
    @api_doc(
        section=api_section.live,
        uri="/live/{thread}/contributors",
    )
    def GET_contributors(self):
        """Get a list of users that contribute to this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor), and
        [/api/live/*thread*/rm_contributor]
        (#POST_api_live_{thread}_rm_contributor).

        """
        editable = c.liveupdate_permissions.allow("manage")

        content = [pages.LinkBackToLiveUpdate()]

        contributors = c.liveupdate_event.contributors
        invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event)

        contributor_builder = LiveUpdateContributorBuilder(
            c.liveupdate_event, contributors, editable)
        contributor_listing = pages.LiveUpdateContributorListing(
            c.liveupdate_event,
            contributor_builder,
            has_invite=c.user_is_loggedin and c.user._id in invites,
            is_contributor=c.user_is_loggedin and c.user._id in contributors,
        ).listing()
        content.append(contributor_listing)

        if editable:
            invite_builder = LiveUpdateInvitedContributorBuilder(
                c.liveupdate_event, invites, editable)
            invite_listing = pages.LiveUpdateInvitedContributorListing(
                c.liveupdate_event,
                invite_builder,
                editable=editable,
            ).listing()
            content.append(invite_listing)

        return pages.LiveUpdateEventPage(
            content=PaneStack(content),
        ).render()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VExistingUname("name"),
        type_and_perms=VLiveUpdatePermissions("type", "permissions"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_invite_contributor(self, form, jquery, user, type_and_perms):
        """Invite another user to contribute to the thread.

        Requires the `manage` permission for this thread.  If the recipient
        accepts the invite, they will be granted the permissions specified.

        See also: [/api/live/*thread*/accept_contributor_invite]
        (#POST_api_live_{thread}_accept_contributor_invite), and
        [/api/live/*thread*/rm_contributor_invite]
        (#POST_api_live_{thread}_rm_contributor_invite).

        """
        if form.has_errors("name", errors.USER_DOESNT_EXIST,
                                   errors.NO_USER):
            return
        if form.has_errors("type", errors.INVALID_PERMISSION_TYPE):
            return
        if form.has_errors("permissions", errors.INVALID_PERMISSIONS):
            return

        type, permissions = type_and_perms

        invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event)
        if user._id in invites or user._id in c.liveupdate_event.contributors:
            c.errors.add(errors.LIVEUPDATE_ALREADY_CONTRIBUTOR, field="name")
            form.has_errors("name", errors.LIVEUPDATE_ALREADY_CONTRIBUTOR)
            return

        if len(invites) >= g.liveupdate_invite_quota:
            c.errors.add(errors.LIVEUPDATE_TOO_MANY_INVITES, field="name")
            form.has_errors("name", errors.LIVEUPDATE_TOO_MANY_INVITES)
            return

        LiveUpdateContributorInvitesByEvent.create(
            c.liveupdate_event, user, permissions)
        queries.add_contributor(c.liveupdate_event, user)

        # TODO: make this i18n-friendly when we have such a system for PMs
        send_system_message(
            user,
            subject="invitation to contribute to " + c.liveupdate_event.title,
            body=INVITE_MESSAGE % {
                "title": c.liveupdate_event.title,
                "url": "/live/" + c.liveupdate_event._id,
            },
        )

        amqp.add_item("new_liveupdate_contributor", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "inviter_fullname": c.user._fullname,
            "invitee_fullname": user._fullname,
        }))

        # add the user to the table
        contributor = LiveUpdateContributor(user, permissions)
        user_row = pages.InvitedLiveUpdateContributorTableItem(
            contributor, c.liveupdate_event, editable=True)
        jquery(".liveupdate_contributor_invite-table").show(
            ).find("table").insert_table_rows(user_row)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VUser(),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_leave_contributor(self, form, jquery):
        """Abdicate contributorship of the thread.

        See also: [/api/live/*thread*/accept_contributor_invite]
        (#POST_api_live_{thread}_accept_contributor_invite), and
        [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        c.liveupdate_event.remove_contributor(c.user)
        queries.remove_contributor(c.liveupdate_event, c.user)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VByName("id", thing_cls=Account),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_rm_contributor_invite(self, form, jquery, user):
        """Revoke an outstanding contributor invite.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        LiveUpdateContributorInvitesByEvent.remove(
            c.liveupdate_event, user)
        queries.remove_contributor(c.liveupdate_event, user)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VUser(),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_accept_contributor_invite(self, form, jquery):
        """Accept a pending invitation to contribute to the thread.

        See also: [/api/live/*thread*/leave_contributor]
        (#POST_api_live_{thread}_leave_contributor).

        """
        try:
            permissions = LiveUpdateContributorInvitesByEvent.get(
                c.liveupdate_event, c.user)
        except InviteNotFoundError:
            c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND)
            form.set_error(errors.LIVEUPDATE_NO_INVITE_FOUND, None)
            return

        LiveUpdateContributorInvitesByEvent.remove(
            c.liveupdate_event, c.user)

        c.liveupdate_event.add_contributor(c.user, permissions)
        jquery.refresh()

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VExistingUname("name"),
        type_and_perms=VLiveUpdatePermissions("type", "permissions"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_set_contributor_permissions(self, form, jquery, user, type_and_perms):
        """Change a contributor or contributor invite's permissions.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor) and
        [/api/live/*thread*/rm_contributor]
        (#POST_api_live_{thread}_rm_contributor).

        """
        if form.has_errors("name", errors.USER_DOESNT_EXIST,
                                   errors.NO_USER):
            return
        if form.has_errors("type", errors.INVALID_PERMISSION_TYPE):
            return
        if form.has_errors("permissions", errors.INVALID_PERMISSIONS):
            return

        type, permissions = type_and_perms
        if type == "liveupdate_contributor":
            if user._id not in c.liveupdate_event.contributors:
                c.errors.add(errors.LIVEUPDATE_NOT_CONTRIBUTOR, field="user")
                form.has_errors("user", errors.LIVEUPDATE_NOT_CONTRIBUTOR)
                return

            c.liveupdate_event.update_contributor_permissions(user, permissions)
        elif type == "liveupdate_contributor_invite":
            try:
                LiveUpdateContributorInvitesByEvent.get(
                    c.liveupdate_event, user)
            except InviteNotFoundError:
                c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND, field="user")
                form.has_errors("user", errors.LIVEUPDATE_NO_INVITE_FOUND)
                return
            else:
                LiveUpdateContributorInvitesByEvent.update_invite_permissions(
                    c.liveupdate_event, user, permissions)

        row = form.closest("tr")
        editor = row.find(".permissions").data("PermissionEditor")
        editor.onCommit(permissions.dumps())

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("manage"),
        VModhash(),
        user=VByName("id", thing_cls=Account),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_rm_contributor(self, form, jquery, user):
        """Revoke another user's contributorship.

        Requires the `manage` permission for this thread.

        See also: [/api/live/*thread*/invite_contributor]
        (#POST_api_live_{thread}_invite_contributor).

        """
        c.liveupdate_event.remove_contributor(user)
        queries.remove_contributor(c.liveupdate_event, user)

    @require_oauth2_scope("submit")
    @validatedForm(
        VLiveUpdateContributorWithPermission("update"),
        VModhash(),
        text=VMarkdownLength("body", max_length=4096),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_update(self, form, jquery, text):
        """Post an update to the thread.

        Requires the `update` permission for this thread.

        See also: [/api/live/*thread*/strike_update]
        (#POST_api_live_{thread}_strike_update), and
        [/api/live/*thread*/delete_update]
        (#POST_api_live_{thread}_delete_update).

        """
        if form.has_errors("body", errors.NO_TEXT,
                                   errors.TOO_LONG):
            return

        # create and store the new update
        update = LiveUpdate(data={
            "author_id": c.user._id,
            "body": text,
            "_spam": c.user._spam,
        })

        hooks.get_hook("liveupdate.update").call(update=update)

        LiveUpdateStream.add_update(c.liveupdate_event, update)

        # tell the world about our new update
        builder = LiveUpdateBuilder(None)
        wrapped = builder.wrap_items([update])[0]
        rendered = wrapped.render(style="api")
        _broadcast(type="update", payload=rendered)

        amqp.add_item("new_liveupdate_update", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "author_fullname": c.user._fullname,
            "liveupdate_id": str(update._id),
            "body": text,
        }))

        liveupdate_events.update_event(update, context=c, request=request)

        # reset the submission form
        t = form.find("textarea")
        t.attr('rows', 3).html("").val("")

    @require_oauth2_scope("edit")
    @validatedForm(
        VModhash(),
        update=VLiveUpdate("id"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_delete_update(self, form, jquery, update):
        """Delete an update from the thread.

        Requires that specified update must have been authored by the user or
        that you have the `edit` permission for this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if form.has_errors("id", errors.NO_THING_ID):
            return

        if not (c.liveupdate_permissions.allow("edit") or
                (c.user_is_loggedin and update.author_id == c.user._id)):
            abort(403)

        update.deleted = True
        LiveUpdateStream.add_update(c.liveupdate_event, update)
        liveupdate_events.update_event(update, context=c, request=request)

        _broadcast(type="delete", payload=update._fullname)

    @require_oauth2_scope("edit")
    @validatedForm(
        VModhash(),
        update=VLiveUpdate("id"),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_strike_update(self, form, jquery, update):
        """Strike (mark incorrect and cross out) the content of an update.

        Requires that specified update must have been authored by the user or
        that you have the `edit` permission for this thread.

        See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update).

        """
        if form.has_errors("id", errors.NO_THING_ID):
            return

        if not (c.liveupdate_permissions.allow("edit") or
                (c.user_is_loggedin and update.author_id == c.user._id)):
            abort(403)

        update.stricken = True
        LiveUpdateStream.add_update(c.liveupdate_event, update)

        liveupdate_events.update_event(
            update, stricken=True, context=c, request=request
        )
        _broadcast(type="strike", payload=update._fullname)

    @require_oauth2_scope("livemanage")
    @validatedForm(
        VLiveUpdateContributorWithPermission("close"),
        VModhash(),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_close_thread(self, form, jquery):
        """Permanently close the thread, disallowing future updates.

        Requires the `close` permission for this thread.

        """
        close_event(c.liveupdate_event)
        liveupdate_events.close_event(context=c, request=request)

        form.refresh()

    @require_oauth2_scope("report")
    @validatedForm(
        VUser(),
        VModhash(),
        report_type=VOneOf("type", pages.REPORT_TYPES),
    )
    @api_doc(
        section=api_section.live,
    )
    def POST_report(self, form, jquery, report_type):
        """Report the thread for violating the rules of reddit."""
        if form.has_errors("type", errors.INVALID_OPTION):
            return

        if c.user._spam or c.user.ignorereports:
            return

        already_reported = LiveUpdateReportsByAccount.get_report(
            c.user, c.liveupdate_event)
        if already_reported:
            self.abort403()

        LiveUpdateReportsByAccount.create(
            c.user, c.liveupdate_event, type=report_type)
        queries.report_event(c.liveupdate_event)

        liveupdate_events.report_event(
            report_type, context=c, request=request
        )

        amqp.add_item("new_liveupdate_report", json.dumps({
            "event_fullname": c.liveupdate_event._fullname,
            "reporter_fullname": c.user._fullname,
            "reason": report_type,
        }))

        try:
            default_subreddit = Subreddit._by_name(g.default_sr)
        except NotFound:
            pass
        else:
            not_yet_reported = g.ratelimitcache.add(
                "rl:lu_reported_" + str(c.liveupdate_event._id), 1, time=3600)
            if not_yet_reported:
                send_system_message(
                    default_subreddit,
                    subject="live thread reported",
                    body=REPORTED_MESSAGE % {
                        "title": c.liveupdate_event.title,
                        "url": "/live/" + c.liveupdate_event._id,
                        "reason": pages.REPORT_TYPES[report_type],
                    },
                )

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_approve(self, form, jquery):
        c.liveupdate_event.banned = False
        c.liveupdate_event._commit()

        queries.unreport_event(c.liveupdate_event)
        liveupdate_events.ban_event(context=c, request=request)

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_ban(self, form, jquery):
        c.liveupdate_event.banned = True
        c.liveupdate_event.banned_by = c.user.name
        c.liveupdate_event._commit()

        queries.unreport_event(c.liveupdate_event)
        liveupdate_events.ban_event(context=c, request=request)
class VCountryCode(Validator):
    def param_docs(self):
        return {self.param: "an ISO 3166-1 alpha-2 code"}

    def run(self, geo):
        if geo in iso3166.countries_by_alpha2:
            return geo
        else:
            return None


EVENT_CONFIGURATION_VALIDATORS = {
    "title": VLength("title", max_length=120),
    "description": VMarkdownLength("description",
                                   max_length=120,
                                   empty_error=None),
    "resources": VMarkdownLength("resources",
                                 max_length=10000,
                                 empty_error=None),
    "nsfw": VBoolean("nsfw"),
}


def is_event_configuration_valid(form):
    if form.has_errors("title", errors.NO_TEXT, errors.TOO_LONG):
        return False

    if form.has_errors("description", errors.TOO_LONG):
        return False
Esempio n. 4
0
multi_sr_data_json_spec = VValidatedJSON.Object({
    'name':
    VSubredditName('name', allow_language_srs=True),
})

multi_json_spec = VValidatedJSON.Object({
    'visibility':
    VOneOf('visibility', ('private', 'public')),
    'subreddits':
    VValidatedJSON.ArrayOf(multi_sr_data_json_spec),
})

multi_description_json_spec = VValidatedJSON.Object({
    'body_md':
    VMarkdownLength('body_md', max_length=10000, empty_error=None),
})


class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

    def pre(self):
        set_extension(request.environ, "json")
        RedditController.pre(self)

    @require_oauth2_scope("read")
    @validate(VUser())
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self):
        """Fetch a list of multis belonging to the current user."""
Esempio n. 5
0
)
from r2.lib.errors import RedditError

multi_sr_data_json_spec = VValidatedJSON.Object({
    'name':
    VSubredditName('name', allow_language_srs=True),
})

MAX_DESC = 10000
MAX_DISP_NAME = 50
WRITABLE_MULTI_FIELDS = ('visibility', 'description_md', 'display_name',
                         'key_color', 'weighting_scheme')

multi_json_spec = VValidatedJSON.PartialObject({
    'description_md':
    VMarkdownLength('description_md', max_length=MAX_DESC, empty_error=None),
    'display_name':
    VLength('display_name', max_length=MAX_DISP_NAME),
    'icon_name':
    VOneOf('icon_name', g.multi_icons + ("", None)),
    'key_color':
    VColor('key_color'),
    'visibility':
    VOneOf('visibility', ('private', 'public', 'hidden')),
    'weighting_scheme':
    VOneOf('weighting_scheme', ('classic', 'fresh')),
    'subreddits':
    VValidatedJSON.ArrayOf(multi_sr_data_json_spec),
})

multi_description_json_spec = VValidatedJSON.Object({
Esempio n. 6
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),
            ))