Exemple #1
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)
Exemple #2
0
class MultiApiController(RedditController):
    on_validation_error = staticmethod(abort_with_error)

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

    def _format_multi_list(self, multis, viewer, expand_srs):
        templ = LabeledMultiJsonTemplate(expand_srs)
        resp = [
            templ.render(multi).finalize() for multi in multis
            if multi.can_view(viewer)
        ]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        user=VAccountByName("username"),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(api_section.multis, uri="/api/multi/user/{username}")
    def GET_list_multis(self, user, expand_srs):
        """Fetch a list of public multis belonging to `username`"""
        multis = LabeledMulti.by_owner(user)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(
        sr=VSRByName('srname'),
        expand_srs=VBoolean("expand_srs"),
    )
    def GET_list_sr_multis(self, sr, expand_srs):
        """Fetch a list of public multis belonging to subreddit `srname`"""
        multis = LabeledMulti.by_owner(sr)
        return self._format_multi_list(multis, c.user, expand_srs)

    @require_oauth2_scope("read")
    @validate(VUser(), expand_srs=VBoolean("expand_srs"))
    @api_doc(api_section.multis, uri="/api/multi/mine")
    def GET_my_multis(self, expand_srs):
        """Fetch a list of multis belonging to the current user."""
        multis = LabeledMulti.by_owner(c.user)
        return self._format_multi_list(multis, c.user, expand_srs)

    def _format_multi(self, multi, expand_sr_info=False):
        multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi)
        return self.api_wrapper(multi_info.finalize())

    @require_oauth2_scope("read")
    @validate(
        multi=VMultiByPath("multipath", require_view=True),
        expand_srs=VBoolean("expand_srs"),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi, expand_srs):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi, expand_srs)

    def _check_new_multi_path(self, path_info):
        if path_info['prefix'] == 'r':
            return self._check_sr_multi_path(path_info)

        return self._check_user_multi_path(path_info)

    def _check_user_multi_path(self, path_info):
        if path_info['owner'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')
        return c.user

    def _check_sr_multi_path(self, path_info):
        try:
            sr = Subreddit._by_name(path_info['owner'])
        except NotFound:
            raise RedditError('SUBREDDIT_NOEXIST', code=404)

        if (not sr.is_moderator_with_perms(c.user, 'config')
                and not c.user_is_admin):
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

        return sr

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        srs = data.pop('subreddits', None)
        if srs is not None:
            multi.clear_srs()
            try:
                self._add_multi_srs(multi, srs)
            except:
                multi._revert()
                raise

        if 'icon_name' in data:
            try:
                multi.set_icon_by_name(data.pop('icon_name'))
            except:
                multi._revert()
                raise

        for key, val in data.iteritems():
            if key in WRITABLE_MULTI_FIELDS:
                setattr(multi, key, val)

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath", required=False),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        if not path_info and "path" in data:
            path_info = VMultiPath("").run(data["path"])
        elif 'display_name' in data:
            # if path not provided, create multi for user
            path = LabeledMulti.slugify(c.user, data['display_name'])
            path_info = VMultiPath("").run(path)

        if not path_info:
            raise RedditError('BAD_MULTI_PATH', code=400)

        owner = self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        owner = self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], owner)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info, rename=False):
        """Copy a multi to a user account."""

        to_owner = self._check_new_multi_path(to_path_info)

        # rename requires same owner
        if rename and from_multi.owner != to_owner:
            raise RedditError('MULTI_CANNOT_EDIT', code=400)

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info, display_name):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        if not to_path_info:
            if display_name:
                # if path not provided, copy multi to same owner
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info)

        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        if display_name:
            to_multi.display_name = display_name
        to_multi._commit()

        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            required=False,
            docs={"to": "destination multireddit url path"},
        ),
        display_name=VLength("display_name",
                             max_length=MAX_DISP_NAME,
                             empty_error=None),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info, display_name):
        """Rename a multi."""
        if not to_path_info:
            if display_name:
                path = LabeledMulti.slugify(from_multi.owner, display_name)
                to_path_info = VMultiPath("").run(path)
            else:
                raise RedditError('BAD_MULTI_PATH', code=400)

        to_multi = self._copy_multi(from_multi, to_path_info, rename=True)

        if display_name:
            to_multi.display_name = display_name
            to_multi._commit()
        from_multi.delete()

        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
Exemple #3
0
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."""
        multis = LabeledMulti.by_owner(c.user)
        wrapped = wrap_things(*multis)
        resp = [w.render() for w in wrapped]
        return self.api_wrapper(resp)

    def _format_multi(self, multi):
        resp = wrap_things(multi)[0].render()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(multi=VMultiByPath("multipath", require_view=True))
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}",
        uri_variants=['/api/filter/{filterpath}'],
    )
    def GET_multi(self, multi):
        """Fetch a multi's data and subreddit list by name."""
        return self._format_multi(multi)

    def _check_new_multi_path(self, path_info):
        if path_info['username'].lower() != c.user.name.lower():
            raise RedditError('MULTI_CANNOT_EDIT',
                              code=403,
                              fields='multipath')

    def _add_multi_srs(self, multi, sr_datas):
        srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas)

        for sr in srs.itervalues():
            if isinstance(sr, FakeSubreddit):
                raise RedditError('MULTI_SPECIAL_SUBREDDIT',
                                  msg_params={'path': sr.path},
                                  code=400)

        sr_props = {}
        for sr_data in sr_datas:
            try:
                sr = srs[sr_data['name']]
            except KeyError:
                raise RedditError('SUBREDDIT_NOEXIST', code=400)
            else:
                # name is passed in via the API data format, but should not be
                # stored on the model.
                del sr_data['name']
                sr_props[sr] = sr_data

        try:
            multi.add_srs(sr_props)
        except TooManySubredditsError as e:
            raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409)

        return sr_props

    def _write_multi_data(self, multi, data):
        multi.visibility = data['visibility']

        multi.clear_srs()
        try:
            self._add_multi_srs(multi, data['subreddits'])
        except:
            multi._revert()
            raise

        multi._commit()
        return multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def POST_multi(self, path_info, data):
        """Create a multi. Responds with 409 Conflict if it already exists."""

        self._check_new_multi_path(path_info)

        try:
            LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        path_info=VMultiPath("multipath"),
        data=VValidatedJSON("model", multi_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def PUT_multi(self, path_info, data):
        """Create or update a multi."""

        self._check_new_multi_path(path_info)

        try:
            multi = LabeledMulti._byID(path_info['path'])
        except tdb_cassandra.NotFound:
            multi = LabeledMulti.create(path_info['path'], c.user)
            response.status = 201

        self._write_multi_data(multi, data)
        return self._format_multi(multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
    )
    @api_doc(api_section.multis, extends=GET_multi)
    def DELETE_multi(self, multi):
        """Delete a multi."""
        multi.delete()

    def _copy_multi(self, from_multi, to_path_info):
        self._check_new_multi_path(to_path_info)

        to_owner = Account._by_name(to_path_info['username'])

        try:
            LabeledMulti._byID(to_path_info['path'])
        except tdb_cassandra.NotFound:
            to_multi = LabeledMulti.copy(to_path_info['path'],
                                         from_multi,
                                         owner=to_owner)
        else:
            raise RedditError('MULTI_EXISTS', code=409, fields='multipath')

        return to_multi

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_view=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/copy",
    )
    def POST_multi_copy(self, from_multi, to_path_info):
        """Copy a multi.

        Responds with 409 Conflict if the target already exists.

        A "copied from ..." line will automatically be appended to the
        description.

        """
        to_multi = self._copy_multi(from_multi, to_path_info)
        from_path = from_multi.path
        to_multi.copied_from = from_path
        if to_multi.description_md:
            to_multi.description_md += '\n\n'
        to_multi.description_md += _('copied from %(source)s') % {
            # force markdown linking since /user/foo is not autolinked
            'source': '[%s](%s)' % (from_path, from_path)
        }
        to_multi.visibility = 'private'
        to_multi._commit()
        return self._format_multi(to_multi)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        from_multi=VMultiByPath("from", require_edit=True, kinds='m'),
        to_path_info=VMultiPath(
            "to",
            docs={"to": "destination multireddit url path"},
        ),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/rename",
    )
    def POST_multi_rename(self, from_multi, to_path_info):
        """Rename a multi."""

        to_multi = self._copy_multi(from_multi, to_path_info)
        from_multi.delete()
        return self._format_multi(to_multi)

    def _get_multi_subreddit(self, multi, sr):
        resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0]
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True),
        sr=VSRByName('srname'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/r/{srname}",
        uri_variants=['/api/filter/{filterpath}/r/{srname}'],
    )
    def GET_multi_subreddit(self, multi, sr):
        """Get data about a subreddit in a multi."""
        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr_name=VSubredditName('srname', allow_language_srs=True),
        data=VValidatedJSON("model", multi_sr_data_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def PUT_multi_subreddit(self, multi, sr_name, data):
        """Add a subreddit to a multi."""

        new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs)

        data['name'] = sr_name
        sr_props = self._add_multi_srs(multi, [data])
        sr = sr_props.items()[0][0]
        multi._commit()

        if new:
            response.status = 201

        return self._get_multi_subreddit(multi, sr)

    @require_oauth2_scope("subscribe")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True),
        sr=VSRByName('srname'),
    )
    @api_doc(api_section.multis, extends=GET_multi_subreddit)
    def DELETE_multi_subreddit(self, multi, sr):
        """Remove a subreddit from a multi."""
        multi.del_srs(sr)
        multi._commit()

    def _format_multi_description(self, multi):
        resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize()
        return self.api_wrapper(resp)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        multi=VMultiByPath("multipath", require_view=True, kinds='m'),
    )
    @api_doc(
        api_section.multis,
        uri="/api/multi/{multipath}/description",
    )
    def GET_multi_description(self, multi):
        """Get a multi's description."""
        return self._format_multi_description(multi)

    @require_oauth2_scope("read")
    @validate(
        VUser(),
        VModhash(),
        multi=VMultiByPath("multipath", require_edit=True, kinds='m'),
        data=VValidatedJSON('model', multi_description_json_spec),
    )
    @api_doc(api_section.multis, extends=GET_multi_description)
    def PUT_multi_description(self, multi, data):
        """Change a multi's markdown description."""
        multi.description_md = data['body_md']
        multi._commit()
        return self._format_multi_description(multi)
Exemple #4
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),
            ))