Пример #1
0
class LiveUpdateAdminController(RedditController):
    @validate(VAdmin())
    def GET_happening_now(self):
        featured_event_fullnames = get_all_featured_events()

        featured_events = {}
        for target, event_id in featured_event_fullnames.iteritems():
            event = LiveUpdateEvent._by_fullname(event_id)
            featured_events[target] = event

        return AdminPage(
                content=pages.HappeningNowAdmin(featured_events),
                title='live: happening now',
                nav_menus=[]
            ).render()

    @validate(
        VAdmin(),
        VModhash(),
        featured_thread=VLiveUpdateEventUrl('url'),
        target=VOneOf("target", [country.alpha2 for country in iso3166.countries]),
    )
    def POST_happening_now(self, featured_thread, target):
        if featured_thread:
            if not target:
                abort(400)

            NamedGlobals.set(HAPPENING_NOW_KEY,
                             {target: featured_thread._fullname})
        else:
            NamedGlobals.set(HAPPENING_NOW_KEY, None)

        self.redirect('/admin/happening-now')
Пример #2
0
class AdminToolController(RedditController):
    @validate(
        VAdmin(),
        recipient=nop('recipient'),
    )
    def GET_creddits(self, recipient):
        return AdminPage(content=AdminCreddits(recipient)).render()

    @validate(
        VAdmin(),
        recipient=nop('recipient'),
    )
    def GET_gold(self, recipient):
        return AdminPage(content=AdminGold(recipient)).render()
Пример #3
0
class ErrorlogController(RedditController):
    @validate(VAdmin())
    def GET_index(self):
        res = AdminPage(content=AdminErrorLog(),
                        title='error log',
                        show_sidebar=False).render()
        return res
Пример #4
0
class LiveUpdateAdminController(RedditController):
    @validate(VAdmin())
    def GET_happening_now(self):
        current_thread_id = NamedGlobals.get(HAPPENING_NOW_KEY, None)
        if current_thread_id:
            current_thread = LiveUpdateEvent._byID(current_thread_id)
        else:
            current_thread = None
        return AdminPage(content=pages.HappeningNowAdmin(current_thread),
                         title='live: happening now',
                         nav_menus=[]).render()

    @validate(
        VAdmin(),
        VModhash(),
        featured_thread=VLiveUpdateEventUrl('url'),
    )
    def POST_happening_now(self, featured_thread):
        NamedGlobals.set(HAPPENING_NOW_KEY,
                         getattr(featured_thread, '_id', None))

        self.redirect('/admin/happening-now')
Пример #5
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)
Пример #6
0
class WikiApiController(WikiController):
    @require_oauth2_scope("wikiedit")
    @validate(VModhash(),
              pageandprevious=VWikiPageRevise(('page', 'previous'),
                                              restricted=True),
              content=nop(('content')),
              page_name=VWikiPageName('page'),
              reason=VPrintable('reason', 256, empty_error=None))
    @api_doc(api_section.wiki, uri='/api/wiki/edit', uses_site=True)
    def POST_wiki_edit(self, pageandprevious, content, page_name, reason):
        """Edit a wiki `page`"""
        page, previous = pageandprevious

        if not page:
            error = c.errors.get(('WIKI_CREATE_ERROR', 'page'))
            if error:
                self.handle_error(403, **(error.msg_params or {}))
            if not c.user._spam:
                page = WikiPage.create(c.site, page_name)
        if c.user._spam:
            error = _("You are doing that too much, please try again later.")
            self.handle_error(415, 'SPECIAL_ERRORS', special_errors=[error])

        renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki')
        if renderer in ('wiki', 'reddit'):
            content = VMarkdown(('content'), renderer=renderer).run(content)

        # Use the raw POST value as we need to tell the difference between
        # None/Undefined and an empty string.  The validators use a default
        # value with both of those cases and would need to be changed.
        # In order to avoid breaking functionality, this was done instead.
        previous = previous._id if previous else request.POST.get('previous')
        try:
            # special validation methods
            if page.name == 'config/stylesheet':
                css_errors, parsed = c.site.parse_css(content, verify=False)
                if g.css_killswitch:
                    self.handle_error(403, 'STYLESHEET_EDIT_DENIED')
                if css_errors:
                    error_items = [CssError(x).message for x in css_errors]
                    self.handle_error(415,
                                      'SPECIAL_ERRORS',
                                      special_errors=error_items)
            elif page.name == "config/automoderator":
                try:
                    rules = Ruleset(content)
                except ValueError as e:
                    error_items = [e.message]
                    self.handle_error(415,
                                      "SPECIAL_ERRORS",
                                      special_errors=error_items)

            # special saving methods
            if page.name == "config/stylesheet":
                c.site.change_css(content, parsed, previous, reason=reason)
            else:
                try:
                    page.revise(content, previous, c.user._id36, reason=reason)
                except ContentLengthError as e:
                    self.handle_error(403,
                                      'CONTENT_LENGTH_ERROR',
                                      max_length=e.max_length)

                # continue storing the special pages as data attributes on the subreddit
                # object. TODO: change this to minimize subreddit get sizes.
                if page.special and page.name in ATTRIBUTE_BY_PAGE:
                    setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content)
                    c.site._commit()

                if page.special or c.is_wiki_mod:
                    description = modactions.get(page.name,
                                                 'Page %s edited' % page.name)
                    ModAction.create(c.site,
                                     c.user,
                                     "wikirevise",
                                     details=description,
                                     description=reason)
        except ConflictException as e:
            self.handle_error(409,
                              'EDIT_CONFLICT',
                              newcontent=e.new,
                              newrevision=page.revision,
                              diffcontent=e.htmldiff)
        return json.dumps({})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              page=VWikiPage('page'),
              act=VOneOf('act', ('del', 'add')),
              user=VExistingUname('username'))
    @api_doc(api_section.wiki,
             uri='/api/wiki/alloweditor/{act}',
             uses_site=True,
             uri_variants=[
                 '/api/wiki/alloweditor/%s' % act for act in ('del', 'add')
             ])
    def POST_wiki_allow_editor(self, act, page, user):
        """Allow/deny `username` to edit this wiki `page`"""
        if not user:
            self.handle_error(404, 'UNKNOWN_USER')
        elif act == 'del':
            page.remove_editor(user._id36)
        elif act == 'add':
            page.add_editor(user._id36)
        else:
            self.handle_error(400, 'INVALID_ACTION')
        return json.dumps({})

    @validate(
        VModhash(),
        VAdmin(),
        pv=VWikiPageAndVersion(('page', 'revision')),
        deleted=VBoolean('deleted'),
    )
    def POST_wiki_revision_delete(self, pv, deleted):
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        if deleted and page.revision == str(revision._id):
            self.handle_error(400, 'REVISION_IS_CURRENT')
        revision.admin_deleted = deleted
        revision._commit()
        return json.dumps({'status': revision.admin_deleted})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              pv=VWikiPageAndVersion(('page', 'revision')))
    @api_doc(api_section.wiki, uri='/api/wiki/hide', uses_site=True)
    def POST_wiki_revision_hide(self, pv):
        """Toggle the public visibility of a wiki page revision"""
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        return json.dumps({'status': revision.toggle_hide()})

    @require_oauth2_scope("modwiki")
    @validate(VModhash(),
              VWikiModerator(),
              pv=VWikiPageAndVersion(('page', 'revision')))
    @api_doc(api_section.wiki, uri='/api/wiki/revert', uses_site=True)
    def POST_wiki_revision_revert(self, pv):
        """Revert a wiki `page` to `revision`"""
        page, revision = pv
        if not revision:
            self.handle_error(400, 'INVALID_REVISION')
        content = revision.content
        reason = 'reverted back %s' % timesince(revision.date)
        if page.name == 'config/stylesheet':
            css_errors, parsed = c.site.parse_css(content)
            if css_errors:
                self.handle_error(403, 'INVALID_CSS')
            c.site.change_css(content,
                              parsed,
                              prev=None,
                              reason=reason,
                              force=True)
        else:
            try:
                page.revise(content,
                            author=c.user._id36,
                            reason=reason,
                            force=True)

                # continue storing the special pages as data attributes on the subreddit
                # object. TODO: change this to minimize subreddit get sizes.
                if page.special:
                    setattr(c.site, ATTRIBUTE_BY_PAGE[page.name], content)
                    c.site._commit()
            except ContentLengthError as e:
                self.handle_error(403,
                                  'CONTENT_LENGTH_ERROR',
                                  max_length=e.max_length)
        return json.dumps({})

    def pre(self):
        WikiController.pre(self)
        c.render_style = 'api'
        set_extension(request.environ, 'json')
class PlaceController(RedditController):
    def pre(self):
        RedditController.pre(self)

        if not PLACE_SUBREDDIT.can_view(c.user):
            self.abort403()

        if c.user.in_timeout:
            self.abort403()

        if c.user._spam:
            self.abort403()

    @validate(
        is_embed=VBoolean("is_embed"),
        is_webview=VBoolean("webview", default=False),
        is_palette_hidden=VBoolean('hide_palette', default=False),
    )
    @allow_oauth2_access
    def GET_canvasse(self, is_embed, is_webview, is_palette_hidden):
        # oauth will try to force the response into json
        # undo that here by hacking extension, content_type, and render_style
        try:
            del(request.environ['extension'])
        except:
            pass
        request.environ['content_type'] = "text/html; charset=UTF-8"
        request.environ['render_style'] = "html"
        set_content_type()

        websocket_url = websockets.make_url("/place", max_age=3600)

        content = PlaceCanvasse()

        js_config = {
            "place_websocket_url": websocket_url,
            "place_canvas_width": CANVAS_WIDTH,
            "place_canvas_height": CANVAS_HEIGHT,
            "place_cooldown": 0 if c.user_is_admin else PIXEL_COOLDOWN_SECONDS,
            "place_fullscreen": is_embed or is_webview,
            "place_hide_ui": is_palette_hidden,
        }

        if c.user_is_loggedin and not c.user_is_admin:
            js_config["place_wait_seconds"] = get_wait_seconds(c.user)

        # this is a sad duplication of the same from reddit_base :(
        # if c.user_is_loggedin:
        #     PLACE_SUBREDDIT.record_visitor_activity("logged_in", c.user._fullname)
        # elif c.loid.serializable:
        #     PLACE_SUBREDDIT.record_visitor_activity("logged_out", c.loid.loid)

        try:
            js_config["place_active_visitors"] = get_activity_count()
        except ActivityError:
            pass

        if is_embed:
            # ensure we're off the cookie domain before allowing embedding
            if request.host != g.media_domain:
                abort(404)
            c.allow_framing = True

        if is_embed or is_webview:
            return PlaceEmbedPage(
                title="place",
                content=content,
                extra_js_config=js_config,
            ).render()
        else:
            return PlacePage(
                title="place",
                content=content,
                extra_js_config=js_config,
            ).render()

    @json_validate(
        VUser(),    # NOTE: this will respond with a 200 with an error body
        VModhash(),
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
        color=VInt("color", min=0, max=15),
    )
    @allow_oauth2_access
    def POST_draw(self, responder, x, y, color):

       #if c.user._date >= ACCOUNT_CREATION_CUTOFF:
       #    self.abort403()

        if PLACE_SUBREDDIT.is_banned(c.user):
            self.abort403()

        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if color is None:
            c.errors.add(errors.BAD_COLOR, field="color")

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER) or
                responder.has_errors("color", errors.BAD_COLOR)):
            # TODO: return 400 with parsable error message?
            return

        if c.user_is_admin:
            wait_seconds = 0
        else:
            wait_seconds = get_wait_seconds(c.user)

        if wait_seconds > 2:
            response.status = 429
            request.environ['extra_error_data'] = {
                "error": 429,
                "wait_seconds": wait_seconds,
            }
            return

        Pixel.create(c.user, color, x, y)

        c.user.set_flair(
            subreddit=PLACE_SUBREDDIT,
            text="({x},{y}) {time}".format(x=x, y=y, time=time.time()),
            css_class="place-%s" % color,
        )

        websockets.send_broadcast(
            namespace="/place",
            type="place",
            payload={
                "author": c.user.name,
                "x": x,
                "y": y,
                "color": color,
            }
        )

        events.place_pixel(x, y, color)
        cooldown = 0 if c.user_is_admin else PIXEL_COOLDOWN_SECONDS
        return {
            'wait_seconds': cooldown,
        }

    @json_validate(
        VUser(),    # NOTE: this will respond with a 200 with an error body
        VAdmin(),
        VModhash(),
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
        width=VInt("width", min=1, max=ADMIN_RECT_DRAW_MAX_SIZE,
                   coerce=True, num_default=1),
        height=VInt("height", min=1, max=ADMIN_RECT_DRAW_MAX_SIZE,
                    coerce=True, num_default=1),
    )
    @allow_oauth2_access
    def POST_drawrect(self, responder, x, y, width, height):
        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER)):
            # TODO: return 400 with parsable error message?
            return

        # prevent drawing outside of the canvas
        width = min(CANVAS_WIDTH - x, width)
        height = min(CANVAS_HEIGHT - y, height)

        batch_payload = []

        for _x in xrange(x, x + width):
            for _y in xrange(y, y + height):
                pixel = Pixel.create(None, 0, _x, _y)
                payload = {
                    "author": '',
                    "x": _x,
                    "y": _y,
                    "color": 0,
                }
                batch_payload.append(payload)

        websockets.send_broadcast(
            namespace="/place",
            type="batch-place",
            payload=batch_payload,
        )

    @json_validate(
        VUser(),
    )
    @allow_oauth2_access
    def GET_time_to_wait(self, responder):
        if c.user._date >= ACCOUNT_CREATION_CUTOFF:
            self.abort403()

        if c.user_is_admin:
            wait_seconds = 0
        else:
            wait_seconds = get_wait_seconds(c.user)

        return {
            "wait_seconds": wait_seconds,
        }

    @json_validate(
        x=VInt("x", min=0, max=CANVAS_WIDTH, coerce=False),
        y=VInt("y", min=0, max=CANVAS_HEIGHT, coerce=False),
    )
    @allow_oauth2_access
    def GET_pixel(self, responder, x, y):
        if x is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="x",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_WIDTH,
                    },
                },
            )

        if y is None:
            # copy the error set by VNumber/VInt
            c.errors.add(
                error_name=errors.BAD_NUMBER,
                field="y",
                msg_params={
                    "range": _("%(min)d to %(max)d") % {
                        "min": 0,
                        "max": CANVAS_HEIGHT,
                    },
                },
            )

        if (responder.has_errors("x", errors.BAD_NUMBER) or
                responder.has_errors("y", errors.BAD_NUMBER)):
            return

        pixel = Pixel.get_pixel_at(x, y)
        if pixel and pixel["user_name"]:
            # pixels blanked out by admins will not have a user_name set
            return pixel
Пример #8
0
class RobinController(RedditController):
    def pre(self):
        RedditController.pre(self)
        if not feature.is_enabled("robin"):
            self.abort404()

    @validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_join(self):
        room = RobinRoom.get_room_for_user(c.user)
        if room:
            return self.redirect("/robin")

        return RobinPage(
            title="robin",
            content=RobinJoin(
                robin_heavy_load=g.live_config.get('robin_heavy_load')),
        ).render()

    @validate(
        VAdmin(), )
    def GET_all(self):
        return RobinPage(
            title="robin",
            content=RobinAll(),
        ).render()

    @validate(
        VAdmin(), )
    def GET_admin(self):
        return RobinPage(
            title="robin",
            content=RobinAdmin(),
        ).render()

    @validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_chat(self):
        room = RobinRoom.get_room_for_user(c.user)
        if not room:
            return self.redirect("/robin/join")

        return self._get_chat_page(room)

    @validate(
        VAdmin(),
        room=VRobinRoom("room_id", allow_admin=True),
    )
    def GET_force_room(self, room):
        """Allow admins to view a specific room"""
        return self._get_chat_page(room)

    @validate(
        VAdmin(),
        user=VAccountByName("user"),
    )
    def GET_user_room(self, user):
        """Redirect admins to a user's room"""
        room = RobinRoom.get_room_for_user(user)
        if not room:
            self.abort404()

        self.redirect("/robin/" + room.id)

    def _get_chat_page(self, room):
        path = posixpath.join("/robin", room.id, c.user._id36)
        websocket_url = websockets.make_url(path, max_age=3600)

        all_user_ids = room.get_all_participants()
        all_present_ids = room.get_present_participants()
        all_votes = room.get_all_votes()

        users = Account._byID(all_user_ids, data=True, stale=True)
        user_list = []

        for user in users.itervalues():
            if user._id in all_votes:
                vote = all_votes.get(user._id)
            else:
                vote = None

            user_list.append({
                "name": user.name,
                "present": user._id in all_present_ids,
                "vote": vote,
            })

        return RobinChatPage(
            title="chat in %s" % room.name,
            content=RobinChat(room=room),
            extra_js_config={
                "robin_room_is_continued": room.is_continued,
                "robin_room_name": room.name,
                "robin_room_id": room.id,
                "robin_websocket_url": websocket_url,
                "robin_user_list": user_list,
                "robin_room_date": js_timestamp(room.date),
                "robin_room_reap_time": js_timestamp(get_reap_time(room)),
            },
        ).render()

    def _has_exceeded_ratelimit(self, form, room):
        # grab the ratelimit (as average events per second) for the room's
        # current level, using the highest level configured that's not bigger
        # than the room.  e.g. if ratelimits are defined for levels 1, 2, and 4
        # and the room is level 3, this will give us the ratelimit specified
        # for 2.
        desired_avg_per_sec = 1
        by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {})
        for level, avg_per_sec in sorted(by_level.items(),
                                         key=lambda (x, y): int(x)):
            if int(level) > room.level:
                break
            desired_avg_per_sec = avg_per_sec

        # now figure out how many events per window that means
        window_size = g.live_config.get("robin_ratelimit_window", 10)
        allowed_events_per_window = int(desired_avg_per_sec * window_size)

        try:
            # now figure out how much they've actually used
            ratelimit_key = "robin/{}".format(c.user._id36)
            time_slice = ratelimit.get_timeslice(window_size)
            usage = ratelimit.get_usage(ratelimit_key, time_slice)

            # ratelimit them if too much
            if usage >= allowed_events_per_window:
                g.stats.simple_event("robin.ratelimit.exceeded")

                period_end = datetime.datetime.utcfromtimestamp(time_slice.end)
                period_end_utc = period_end.replace(tzinfo=pytz.UTC)
                until_reset = utils.timeuntil(period_end_utc)
                c.errors.add(errors.RATELIMIT, {"time": until_reset},
                             field="ratelimit",
                             code=429)
                form.has_errors("ratelimit", errors.RATELIMIT)

                return True

            # or record the usage and move on
            ratelimit.record_usage(ratelimit_key, time_slice)
        except ratelimit.RatelimitError as exc:
            g.log.warning("ratelimit error: %s", exc)
        return False

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
        room=VRobinRoom("room_id"),
        message=VLength("message", max_length=140),  # TODO: do we want md?
    )
    def POST_message(self, form, jquery, room, message):
        if self._has_exceeded_ratelimit(form, room):
            return

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

        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="chat",
            payload={
                "from": c.user.name,
                "body": message,
            },
        )

        events.message(
            room=room,
            message=message,
            sent_dt=datetime.datetime.utcnow(),
            context=c,
            request=request,
        )

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
        room=VRobinRoom("room_id"),
        vote=VOneOf("vote", VALID_VOTES),
    )
    def POST_vote(self, form, jquery, room, vote):
        if self._has_exceeded_ratelimit(form, room):
            return

        if not vote:
            # TODO: error return?
            return

        g.stats.simple_event('robin.vote.%s' % vote)

        room.set_vote(c.user, vote)
        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="vote",
            payload={
                "from": c.user.name,
                "vote": vote,
            },
        )

        events.vote(
            room=room,
            vote=vote,
            sent_dt=datetime.datetime.utcnow(),
            context=c,
            request=request,
        )

    @validatedForm(
        VUser(),
        VNotInTimeout(),
        VModhash(),
    )
    def POST_join_room(self, form, jquery):
        if g.live_config.get('robin_heavy_load'):
            request.environ["usable_error_content"] = (
                "Robin is currently experience high load.")
            abort(503)

        room = RobinRoom.get_room_for_user(c.user)
        if room:
            # user is already in a room, they should get redirected by the
            # frontend after polling /api/room_assignment.json
            return

        add_to_waitinglist(c.user)

    @validatedForm(
        VUser(),
        VModhash(),
    )
    def POST_leave_room(self, form, jquery):
        room = RobinRoom.get_room_for_user(c.user)
        if not room:
            return
        room.remove_participants([c.user])
        websockets.send_broadcast(
            namespace="/robin/" + room.id,
            type="users_abandoned",
            payload={
                "users": [c.user.name],
            },
        )

    @json_validate(
        VUser(),
        VNotInTimeout(),
    )
    def GET_room_assignment(self, responder):
        room = RobinRoom.get_room_for_user(c.user)
        if room:
            return {"roomId": room.id}

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_admin_prompt(self, form, jquery):
        prompt_for_voting()

    @validatedForm(
        VAdmin(),
        VModhash(),
    )
    def POST_admin_reap(self, form, jquery):
        reap_ripe_rooms()

    @validatedForm(
        VAdmin(),
        VModhash(),
        message=VLength("message", max_length=140),
    )
    def POST_admin_broadcast(self, form, jquery, message):
        if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG):
            return

        websockets.send_broadcast(
            namespace="/robin",
            type="system_broadcast",
            payload={
                "body": message,
            },
        )