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')
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()
class ErrorlogController(RedditController): @validate(VAdmin()) def GET_index(self): res = AdminPage(content=AdminErrorLog(), title='error log', show_sidebar=False).render() return res
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')
class LiveUpdateController(RedditController): def __before__(self, event): RedditController.__before__(self) if event: try: c.liveupdate_event = LiveUpdateEvent._byID(event) except tdb_cassandra.NotFound: pass if not c.liveupdate_event: self.abort404() if c.user_is_loggedin: c.liveupdate_permissions = \ c.liveupdate_event.get_permissions(c.user) # revoke some permissions from everyone after closing if c.liveupdate_event.state != "live": c.liveupdate_permissions = (c.liveupdate_permissions .without("update") .without("close") ) if c.user_is_admin: c.liveupdate_permissions = ContributorPermissionSet.SUPERUSER else: c.liveupdate_permissions = ContributorPermissionSet.NONE if c.liveupdate_event.banned and not c.liveupdate_permissions: error_page = RedditError( title=_("this thread has been banned"), message="", image="subreddit-banned.png", ) request.environ["usable_error_content"] = error_page.render() self.abort403() if (c.liveupdate_event.nsfw and not c.over18 and request.host != g.media_domain and # embeds are special c.render_style == "html"): return self.intermediate_redirect("/over18", sr_path=False) @require_oauth2_scope("read") @validate( num=VLimit("limit", default=25, max_limit=100), after=VLiveUpdateID("after"), before=VLiveUpdateID("before"), count=VCount("count"), is_embed=VBoolean("is_embed", docs={"is_embed": "(internal use only)"}), style_sr=VSRByName("stylesr"), ) @api_doc( section=api_section.live, uri="/live/{thread}", supports_rss=True, notes=[paginated_listing.doc_note], ) def GET_listing(self, num, after, before, count, is_embed, style_sr): """Get a list of updates posted in this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ # preemptively record activity for clients that don't send pixel pings. # this won't capture their continued visit, but will at least show a # correct activity count for short lived connections. record_activity(c.liveupdate_event._id) reverse = False if before: reverse = True after = before query = LiveUpdateStream.query([c.liveupdate_event._id], count=num, reverse=reverse) if after: query.column_start = after builder = LiveUpdateBuilder(query=query, skip=True, reverse=reverse, num=num, count=count) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() if c.user_is_loggedin: report_type = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) else: report_type = None content = pages.LiveUpdateEventApp( event=c.liveupdate_event, listing=wrapped_listing, show_sidebar=not is_embed, report_type=report_type, ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + "/about.json", Wrapped(c.liveupdate_event), ) c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) if not is_embed: return pages.LiveUpdateEventAppPage( content=content, page_classes=['liveupdate-app'], ).render() else: # ensure we're off the cookie domain before allowing embedding if request.host != g.media_domain: abort(404) c.allow_framing = True # interstitial redirects and nsfw settings are funky on the media # domain. just disable nsfw embeds. if c.liveupdate_event.nsfw: embed_page = pages.LiveUpdateEventEmbed( content=pages.LiveUpdateNSFWEmbed(), ) request.environ["usable_error_content"] = embed_page.render() abort(403) embed_page = pages.LiveUpdateEventEmbed( content=content, page_classes=['liveupdate-app'], ) if style_sr and getattr(style_sr, "type", "private") != "private": c.can_apply_styles = True c.allow_styles = True embed_page.subreddit_stylesheet_url = \ Reddit.get_subreddit_stylesheet_url(style_sr) return embed_page.render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/updates/{update_id}", ) def GET_focus(self, target): """Get details about a specific update in a live thread.""" try: target = uuid.UUID(target) except (TypeError, ValueError): self.abort404() try: update = LiveUpdateStream.get_update(c.liveupdate_event, target) except tdb_cassandra.NotFound: self.abort404() if update.deleted: self.abort404() query = FocusQuery([update]) builder = LiveUpdateBuilder( query=query, skip=True, reverse=True, num=1, count=0) listing = pages.LiveUpdateListing(builder) wrapped_listing = listing.listing() c.js_preload.set_wrapped( "/live/" + c.liveupdate_event._id + ".json", wrapped_listing, ) content = pages.LiveUpdateFocusApp( event=c.liveupdate_event, listing=wrapped_listing, ) return pages.LiveUpdateEventFocusPage( content=content, focused_update=update, page_classes=["liveupdate-focus"], ).render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/about", ) def GET_about(self): """Get some basic information about the live thread. See also: [/api/live/*thread*/edit](#POST_api_live_{thread}_edit). """ if not is_api(): self.abort404() content = Wrapped(c.liveupdate_event) return pages.LiveUpdateEventPage(content=content).render() @require_oauth2_scope("read") @base_listing @api_doc( section=api_section.live, uri="/live/{thread}/discussions", supports_rss=True, ) def GET_discussions(self, num, after, reverse, count): """Get a list of reddit submissions linking to this thread.""" builder = url_links_builder( url="/live/" + c.liveupdate_event._id, num=num, after=after, reverse=reverse, count=count, ) listing = LinkListing(builder).listing() return pages.LiveUpdateEventPage( content=listing, ).render() def GET_edit(self): if not (c.liveupdate_permissions.allow("settings") or c.liveupdate_permissions.allow("close")): abort(403) return pages.LiveUpdateEventPage( content=pages.LiveUpdateEventConfiguration(), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("settings"), VModhash(), **EVENT_CONFIGURATION_VALIDATORS ) @api_doc( section=api_section.live, ) def POST_edit(self, form, jquery, title, description, resources, nsfw): """Configure the thread. Requires the `settings` permission for this thread. See also: [/live/*thread*/about.json](#GET_live_{thread}_about.json). """ if not is_event_configuration_valid(form): return changes = {} if title != c.liveupdate_event.title: changes["title"] = title if description != c.liveupdate_event.description: changes["description"] = description changes["description_html"] = safemarkdown(description, nofollow=True) or "" if resources != c.liveupdate_event.resources: changes["resources"] = resources changes["resources_html"] = safemarkdown(resources, nofollow=True) or "" if nsfw != c.liveupdate_event.nsfw: changes["nsfw"] = nsfw if changes: _broadcast(type="settings", payload=changes) c.liveupdate_event.title = title c.liveupdate_event.description = description c.liveupdate_event.resources = resources c.liveupdate_event.nsfw = nsfw c.liveupdate_event._commit() amqp.add_item("liveupdate_event_edited", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "editor_fullname": c.user._fullname, })) form.set_html(".status", _("saved")) form.refresh() # TODO: pass listing params on @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/live/{thread}/contributors", ) def GET_contributors(self): """Get a list of users that contribute to this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor), and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ editable = c.liveupdate_permissions.allow("manage") content = [pages.LinkBackToLiveUpdate()] contributors = c.liveupdate_event.contributors invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) contributor_builder = LiveUpdateContributorBuilder( c.liveupdate_event, contributors, editable) contributor_listing = pages.LiveUpdateContributorListing( c.liveupdate_event, contributor_builder, has_invite=c.user_is_loggedin and c.user._id in invites, is_contributor=c.user_is_loggedin and c.user._id in contributors, ).listing() content.append(contributor_listing) if editable: invite_builder = LiveUpdateInvitedContributorBuilder( c.liveupdate_event, invites, editable) invite_listing = pages.LiveUpdateInvitedContributorListing( c.liveupdate_event, invite_builder, editable=editable, ).listing() content.append(invite_listing) return pages.LiveUpdateEventPage( content=PaneStack(content), ).render() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_invite_contributor(self, form, jquery, user, type_and_perms): """Invite another user to contribute to the thread. Requires the `manage` permission for this thread. If the recipient accepts the invite, they will be granted the permissions specified. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/rm_contributor_invite] (#POST_api_live_{thread}_rm_contributor_invite). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms invites = LiveUpdateContributorInvitesByEvent.get_all(c.liveupdate_event) if user._id in invites or user._id in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_ALREADY_CONTRIBUTOR, field="name") form.has_errors("name", errors.LIVEUPDATE_ALREADY_CONTRIBUTOR) return if len(invites) >= g.liveupdate_invite_quota: c.errors.add(errors.LIVEUPDATE_TOO_MANY_INVITES, field="name") form.has_errors("name", errors.LIVEUPDATE_TOO_MANY_INVITES) return LiveUpdateContributorInvitesByEvent.create( c.liveupdate_event, user, permissions) queries.add_contributor(c.liveupdate_event, user) # TODO: make this i18n-friendly when we have such a system for PMs send_system_message( user, subject="invitation to contribute to " + c.liveupdate_event.title, body=INVITE_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, }, ) amqp.add_item("new_liveupdate_contributor", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "inviter_fullname": c.user._fullname, "invitee_fullname": user._fullname, })) # add the user to the table contributor = LiveUpdateContributor(user, permissions) user_row = pages.InvitedLiveUpdateContributorTableItem( contributor, c.liveupdate_event, editable=True) jquery(".liveupdate_contributor_invite-table").show( ).find("table").insert_table_rows(user_row) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_leave_contributor(self, form, jquery): """Abdicate contributorship of the thread. See also: [/api/live/*thread*/accept_contributor_invite] (#POST_api_live_{thread}_accept_contributor_invite), and [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(c.user) queries.remove_contributor(c.liveupdate_event, c.user) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor_invite(self, form, jquery, user): """Revoke an outstanding contributor invite. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, user) queries.remove_contributor(c.liveupdate_event, user) @require_oauth2_scope("livemanage") @validatedForm( VUser(), VModhash(), ) @api_doc( section=api_section.live, ) def POST_accept_contributor_invite(self, form, jquery): """Accept a pending invitation to contribute to the thread. See also: [/api/live/*thread*/leave_contributor] (#POST_api_live_{thread}_leave_contributor). """ try: permissions = LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, c.user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND) form.set_error(errors.LIVEUPDATE_NO_INVITE_FOUND, None) return LiveUpdateContributorInvitesByEvent.remove( c.liveupdate_event, c.user) c.liveupdate_event.add_contributor(c.user, permissions) jquery.refresh() @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VExistingUname("name"), type_and_perms=VLiveUpdatePermissions("type", "permissions"), ) @api_doc( section=api_section.live, ) def POST_set_contributor_permissions(self, form, jquery, user, type_and_perms): """Change a contributor or contributor invite's permissions. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor) and [/api/live/*thread*/rm_contributor] (#POST_api_live_{thread}_rm_contributor). """ if form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): return if form.has_errors("type", errors.INVALID_PERMISSION_TYPE): return if form.has_errors("permissions", errors.INVALID_PERMISSIONS): return type, permissions = type_and_perms if type == "liveupdate_contributor": if user._id not in c.liveupdate_event.contributors: c.errors.add(errors.LIVEUPDATE_NOT_CONTRIBUTOR, field="user") form.has_errors("user", errors.LIVEUPDATE_NOT_CONTRIBUTOR) return c.liveupdate_event.update_contributor_permissions(user, permissions) elif type == "liveupdate_contributor_invite": try: LiveUpdateContributorInvitesByEvent.get( c.liveupdate_event, user) except InviteNotFoundError: c.errors.add(errors.LIVEUPDATE_NO_INVITE_FOUND, field="user") form.has_errors("user", errors.LIVEUPDATE_NO_INVITE_FOUND) return else: LiveUpdateContributorInvitesByEvent.update_invite_permissions( c.liveupdate_event, user, permissions) row = form.closest("tr") editor = row.find(".permissions").data("PermissionEditor") editor.onCommit(permissions.dumps()) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("manage"), VModhash(), user=VByName("id", thing_cls=Account), ) @api_doc( section=api_section.live, ) def POST_rm_contributor(self, form, jquery, user): """Revoke another user's contributorship. Requires the `manage` permission for this thread. See also: [/api/live/*thread*/invite_contributor] (#POST_api_live_{thread}_invite_contributor). """ c.liveupdate_event.remove_contributor(user) queries.remove_contributor(c.liveupdate_event, user) @require_oauth2_scope("submit") @validatedForm( VLiveUpdateContributorWithPermission("update"), VModhash(), text=VMarkdownLength("body", max_length=4096), ) @api_doc( section=api_section.live, ) def POST_update(self, form, jquery, text): """Post an update to the thread. Requires the `update` permission for this thread. See also: [/api/live/*thread*/strike_update] (#POST_api_live_{thread}_strike_update), and [/api/live/*thread*/delete_update] (#POST_api_live_{thread}_delete_update). """ if form.has_errors("body", errors.NO_TEXT, errors.TOO_LONG): return # create and store the new update update = LiveUpdate(data={ "author_id": c.user._id, "body": text, "_spam": c.user._spam, }) hooks.get_hook("liveupdate.update").call(update=update) LiveUpdateStream.add_update(c.liveupdate_event, update) # tell the world about our new update builder = LiveUpdateBuilder(None) wrapped = builder.wrap_items([update])[0] rendered = wrapped.render(style="api") _broadcast(type="update", payload=rendered) amqp.add_item("new_liveupdate_update", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "author_fullname": c.user._fullname, "liveupdate_id": str(update._id), "body": text, })) liveupdate_events.update_event(update, context=c, request=request) # reset the submission form t = form.find("textarea") t.attr('rows', 3).html("").val("") @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_delete_update(self, form, jquery, update): """Delete an update from the thread. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.deleted = True LiveUpdateStream.add_update(c.liveupdate_event, update) liveupdate_events.update_event(update, context=c, request=request) _broadcast(type="delete", payload=update._fullname) @require_oauth2_scope("edit") @validatedForm( VModhash(), update=VLiveUpdate("id"), ) @api_doc( section=api_section.live, ) def POST_strike_update(self, form, jquery, update): """Strike (mark incorrect and cross out) the content of an update. Requires that specified update must have been authored by the user or that you have the `edit` permission for this thread. See also: [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if form.has_errors("id", errors.NO_THING_ID): return if not (c.liveupdate_permissions.allow("edit") or (c.user_is_loggedin and update.author_id == c.user._id)): abort(403) update.stricken = True LiveUpdateStream.add_update(c.liveupdate_event, update) liveupdate_events.update_event( update, stricken=True, context=c, request=request ) _broadcast(type="strike", payload=update._fullname) @require_oauth2_scope("livemanage") @validatedForm( VLiveUpdateContributorWithPermission("close"), VModhash(), ) @api_doc( section=api_section.live, ) def POST_close_thread(self, form, jquery): """Permanently close the thread, disallowing future updates. Requires the `close` permission for this thread. """ close_event(c.liveupdate_event) liveupdate_events.close_event(context=c, request=request) form.refresh() @require_oauth2_scope("report") @validatedForm( VUser(), VModhash(), report_type=VOneOf("type", pages.REPORT_TYPES), ) @api_doc( section=api_section.live, ) def POST_report(self, form, jquery, report_type): """Report the thread for violating the rules of reddit.""" if form.has_errors("type", errors.INVALID_OPTION): return if c.user._spam or c.user.ignorereports: return already_reported = LiveUpdateReportsByAccount.get_report( c.user, c.liveupdate_event) if already_reported: self.abort403() LiveUpdateReportsByAccount.create( c.user, c.liveupdate_event, type=report_type) queries.report_event(c.liveupdate_event) liveupdate_events.report_event( report_type, context=c, request=request ) amqp.add_item("new_liveupdate_report", json.dumps({ "event_fullname": c.liveupdate_event._fullname, "reporter_fullname": c.user._fullname, "reason": report_type, })) try: default_subreddit = Subreddit._by_name(g.default_sr) except NotFound: pass else: not_yet_reported = g.ratelimitcache.add( "rl:lu_reported_" + str(c.liveupdate_event._id), 1, time=3600) if not_yet_reported: send_system_message( default_subreddit, subject="live thread reported", body=REPORTED_MESSAGE % { "title": c.liveupdate_event.title, "url": "/live/" + c.liveupdate_event._id, "reason": pages.REPORT_TYPES[report_type], }, ) @validatedForm( VAdmin(), VModhash(), ) def POST_approve(self, form, jquery): c.liveupdate_event.banned = False c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event) liveupdate_events.ban_event(context=c, request=request) @validatedForm( VAdmin(), VModhash(), ) def POST_ban(self, form, jquery): c.liveupdate_event.banned = True c.liveupdate_event.banned_by = c.user.name c.liveupdate_event._commit() queries.unreport_event(c.liveupdate_event) liveupdate_events.ban_event(context=c, request=request)
class 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
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, }, )