class OEmbedController(MinimalController): def try_pagecache(self): pass @validate( url=VUrl('url'), parent=VBoolean("parent", default=False), live=VBoolean("live", default=False), ) def GET_oembed(self, url, parent, live): """Get the oEmbed response for a URL, if any exists. Spec: http://www.oembed.com/ Optional parameters (parent, live) are passed through as embed options to oEmbed renderers. """ response.content_type = "application/json" thing = url_to_thing(url) if not thing: abort(404) embed_options = { "parent": parent, "live": live, } try: return scriptsafe_dumps(_oembed_for(thing, **embed_options)) except ForbiddenError: abort(403) except NotImplementedError: abort(404)
class GoldApiController(RedditController): @validatedForm( VUser(), VGold(), VModhash(), public=VBoolean("public"), snoo_color=VSnooColor("snoo_color"), unvalidated_components=VJSON("components"), ) def POST_snoovatar(self, form, jquery, public, snoo_color, unvalidated_components): if form.has_errors("components", errors.NO_TEXT, errors.TOO_LONG, errors.BAD_STRING, ): return if form.has_errors("snoo_color", errors.BAD_CSS_COLOR): return try: tailors = g.plugins["gold"].tailors_data validated = {} for tailor in tailors: tailor_name = tailor["name"] component = unvalidated_components.get(tailor_name) # if the tailor requires a selection, ensure there is one if not tailor["allow_clear"]: require(component) # ensure this dressing exists dressing = component.get("dressingName") if dressing: for d in tailor["dressings"]: if dressing == d["name"]: break else: raise RequirementException validated[tailor_name] = component except RequirementException: c.errors.add(errors.INVALID_SNOOVATAR, field="components") form.has_errors("components", errors.INVALID_SNOOVATAR) return SnoovatarsByAccount.save( user=c.user, name="snoo", public=public, snoo_color=snoo_color, components=validated, )
class OEmbedController(MinimalController): def pre(self): c.user = g.auth_provider.get_authenticated_account() if c.user and c.user._deleted: c.user = None c.user_is_loggedin = bool(c.user) @validate(url=VUrl('url'), parent=VBoolean("parent", default=False), live=VBoolean("live", default=False), omitscript=VBoolean("omitscript", default=False), callback=VBoolean("callback", default=False)) def GET_oembed(self, url, parent, live, omitscript, callback): """Get the oEmbed response for a URL, if any exists. Spec: http://www.oembed.com/ Optional parameters (parent, live) are passed through as embed options to oEmbed renderers. """ if callback: response.content_type = "application/javascript" else: response.content_type = "application/json" thing = url_to_thing(url) if not thing: abort(404) embed_options = { "parent": parent, "live": live, "omitscript": omitscript } try: return scriptsafe_dumps(_oembed_for(thing, **embed_options)) except ForbiddenError: abort(403) except NotImplementedError: abort(404)
class AdzerkApiController(api.ApiController): @csrf_exempt @validate( srnames=VPrintable("srnames", max_length=2100), is_mobile_web=VBoolean('is_mobile_web'), ) def POST_request_promo(self, srnames, is_mobile_web): self.OPTIONS_request_promo() if not srnames: return srnames = srnames.split('+') # request multiple ads in case some are hidden by the builder due # to the user's hides/preferences response = adzerk_request(srnames, mobile_web=is_mobile_web) if not response: g.stats.simple_event('adzerk.request.no_promo') return res_by_campaign = {r.campaign: r for r in response} tuples = [promote.PromoTuple(r.link, 1., r.campaign) for r in response] builder = CampaignBuilder(tuples, wrap=default_thing_wrapper(), keep_fn=promote.promo_keep_fn, num=1, skip=True) listing = LinkListing(builder, nextprev=False).listing() promote.add_trackers(listing.things, c.site) if listing.things: g.stats.simple_event('adzerk.request.valid_promo') w = listing.things[0] r = res_by_campaign[w.campaign] up = UrlParser(r.imp_pixel) up.hostname = "pixel.redditmedia.com" w.adserver_imp_pixel = up.unparse() w.adserver_click_url = r.click_url w.num = "" return responsive(w.render(), space_compress=True) else: g.stats.simple_event('adzerk.request.skip_promo')
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 WikiController(RedditController): allow_stylesheets = True @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/{page}', uses_site=True) @validate(pv=VWikiPageAndVersion(('page', 'v', 'v2'), required=False, restricted=False, allow_hidden_revision=False), page_name=VWikiPageName('page', error_on_name_normalized=True)) def GET_wiki_page(self, pv, page_name): """Return the content of a wiki page If `v` is given, show the wiki page as it was at that version If both `v` and `v2` are given, show a diff of the two """ message = None if c.errors.get(('PAGE_NAME_NORMALIZED', 'page')): url = join_urls(c.wiki_base_url, page_name) return self.redirect(url) page, version, version2 = pv if not page: is_api = c.render_style in extensions.API_TYPES if this_may_revise(): if is_api: self.handle_error(404, 'PAGE_NOT_CREATED') errorpage = WikiNotFound(page=page_name) request.environ['usable_error_content'] = errorpage.render() elif is_api: self.handle_error(404, 'PAGE_NOT_FOUND') self.abort404() if version: edit_by = version.get_author() edit_date = version.date else: edit_by = page.get_author() edit_date = page._get('last_edit_date') diffcontent = None if not version: content = page.content if c.is_wiki_mod and page.name in page_descriptions: message = page_descriptions[page.name] else: message = _("viewing revision from %s") % timesince(version.date) if version2: t1 = timesince(version.date) t2 = timesince(version2.date) timestamp1 = _("%s ago") % t1 timestamp2 = _("%s ago") % t2 message = _("comparing revisions from %(date_1)s and %(date_2)s") \ % {'date_1': t1, 'date_2': t2} diffcontent = make_htmldiff(version.content, version2.content, timestamp1, timestamp2) content = version2.content else: message = _("viewing revision from %s ago") % timesince( version.date) content = version.content renderer = RENDERERS_BY_PAGE.get(page.name, 'wiki') return WikiPageView(content, alert=message, v=version, diff=diffcontent, may_revise=this_may_revise(page), edit_by=edit_by, edit_date=edit_date, page=page.name, renderer=renderer).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions/{page}', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') @validate(page=VWikiPage(('page'), restricted=False)) def GET_wiki_revisions(self, num, after, reverse, count, page): """Retrieve a list of revisions of this wiki `page`""" revisions = page.get_revisions() wikiuser = c.user if c.user_is_loggedin else None builder = WikiRevisionBuilder(revisions, user=wikiuser, sr=c.site, num=num, reverse=reverse, count=count, after=after, skip=not c.is_wiki_mod, wrap=default_thing_wrapper(), page=page) listing = WikiRevisionListing(builder).listing() return WikiRevisions(listing, page=page.name, may_revise=this_may_revise(page)).render() @validate(wp=VWikiPageRevise('page'), page=VWikiPageName('page')) def GET_wiki_create(self, wp, page): api = c.render_style in extensions.API_TYPES error = c.errors.get(('WIKI_CREATE_ERROR', 'page')) if error: error = error.msg_params if wp[0]: return self.redirect(join_urls(c.wiki_base_url, wp[0].name)) elif api: if error: self.handle_error(403, **error) else: self.handle_error(404, 'PAGE_NOT_CREATED') elif error: error_msg = '' if error['reason'] == 'PAGE_NAME_LENGTH': error_msg = _( "this wiki cannot handle page names of that magnitude! please select a page name shorter than %d characters" ) % error['max_length'] elif error['reason'] == 'PAGE_CREATED_ELSEWHERE': error_msg = _( "this page is a special page, please go into the subreddit settings and save the field once to create this special page" ) elif error['reason'] == 'PAGE_NAME_MAX_SEPARATORS': error_msg = _( 'a max of %d separators "/" are allowed in a wiki page name.' ) % error['max_separators'] return BoringPage(_("Wiki error"), infotext=error_msg).render() else: return WikiCreate(page=page, may_revise=True).render() @validate(wp=VWikiPageRevise('page', restricted=True, required=True)) def GET_wiki_revise(self, wp, page, message=None, **kw): wp = wp[0] previous = kw.get('previous', wp._get('revision')) content = kw.get('content', wp.content) if not message and wp.name in page_descriptions: message = page_descriptions[wp.name] return WikiEdit(content, previous, alert=message, page=wp.name, may_revise=True).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/revisions', uses_site=True) @paginated_listing(max_page_size=100, backend='cassandra') def GET_wiki_recent(self, num, after, reverse, count): """Retrieve a list of recently changed wiki pages in this subreddit""" revisions = WikiRevision.get_recent(c.site) wikiuser = c.user if c.user_is_loggedin else None builder = WikiRecentRevisionBuilder(revisions, num=num, count=count, reverse=reverse, after=after, wrap=default_thing_wrapper(), skip=not c.is_wiki_mod, user=wikiuser, sr=c.site) listing = WikiRevisionListing(builder).listing() return WikiRecent(listing).render() @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/pages', uses_site=True) def GET_wiki_listing(self): """Retrieve a list of wiki pages in this subreddit""" def check_hidden(page): return page.listed and this_may_view(page) pages, linear_pages = WikiPage.get_listing(c.site, filter_check=check_hidden) return WikiListing(pages, linear_pages).render() def GET_wiki_redirect(self, page='index'): return self.redirect(str("%s/%s" % (c.wiki_base_url, page)), code=301) @require_oauth2_scope("wikiread") @api_doc(api_section.wiki, uri='/wiki/discussions/{page}', uses_site=True) @base_listing @validate(page=VWikiPage('page', restricted=True)) def GET_wiki_discussions(self, page, num, after, reverse, count): """Retrieve a list of discussions about this wiki `page`""" page_url = add_sr("%s/%s" % (c.wiki_base_url, page.name)) builder = url_links_builder(page_url, num=num, after=after, reverse=reverse, count=count) listing = LinkListing(builder).listing() return WikiDiscussions(listing, page=page.name, may_revise=this_may_revise(page)).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate(page=VWikiPage('page', restricted=True, modonly=True)) def GET_wiki_settings(self, page): """Retrieve the current permission settings for `page`""" settings = { 'permlevel': page._get('permlevel', 0), 'listed': page.listed } mayedit = page.get_editor_accounts() restricted = (not page.special) and page.restricted show_editors = not restricted return WikiSettings(settings, mayedit, show_settings=not page.special, page=page.name, show_editors=show_editors, restricted=restricted, may_revise=True).render() @require_oauth2_scope("modwiki") @api_doc(api_section.wiki, uri='/wiki/settings/{page}', uses_site=True) @validate(VModhash(), page=VWikiPage('page', restricted=True, modonly=True), permlevel=VInt('permlevel'), listed=VBoolean('listed')) def POST_wiki_settings(self, page, permlevel, listed): """Update the permissions and visibility of wiki `page`""" oldpermlevel = page.permlevel try: page.change_permlevel(permlevel) except ValueError: self.handle_error(403, 'INVALID_PERMLEVEL') if page.listed != listed: page.listed = listed page._commit() verb = 'Relisted' if listed else 'Delisted' description = '%s page %s' % (verb, page.name) ModAction.create(c.site, c.user, 'wikipagelisted', description=description) if oldpermlevel != permlevel: description = 'Page: %s, Changed from %s to %s' % ( page.name, oldpermlevel, permlevel) ModAction.create(c.site, c.user, 'wikipermlevel', description=description) return self.GET_wiki_settings(page=page.name) def on_validation_error(self, error): RedditController.on_validation_error(self, error) if error.code: self.handle_error(error.code, error.name) def handle_error(self, code, reason=None, **data): abort(reddit_http_error(code, reason, **data)) def pre(self): RedditController.pre(self) if g.disable_wiki and not c.user_is_admin: self.handle_error(403, 'WIKI_DOWN') if not c.site._should_wiki: self.handle_error(404, 'NOT_WIKIABLE') # /r/mod for an example frontpage = isinstance(c.site, DefaultSR) c.wiki_base_url = join_urls(c.site.path, 'wiki') c.wiki_api_url = join_urls(c.site.path, '/api/wiki') c.wiki_id = g.default_sr if frontpage else c.site.name self.editconflict = False c.is_wiki_mod = (c.user_is_admin or c.site.is_moderator_with_perms( c.user, 'wiki')) if c.user_is_loggedin else False c.wikidisabled = False mode = c.site.wikimode if not mode or mode == 'disabled': if not c.is_wiki_mod: self.handle_error(403, 'WIKI_DISABLED') else: c.wikidisabled = True # Redirects from the old wiki def GET_faq(self): return self.GET_wiki_redirect(page='faq') GET_help = GET_wiki_redirect
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 PromoteController(ListingController): where = 'promoted' render_cls = PromotePage @property def title_text(self): return _('promoted by you') @classmethod @memoize('live_by_subreddit', time=300) def live_by_subreddit(cls, sr): if sr == Frontpage: sr_id = '' else: sr_id = sr._id r = LiveAdWeights.get([sr_id]) return [i.link for i in r[sr_id]] @classmethod @memoize('subreddits_with_promos', time=3600) def subreddits_with_promos(cls): sr_ids = LiveAdWeights.get_live_subreddits() srs = Subreddit._byID(sr_ids, return_dict=False) sr_names = sorted([sr.name for sr in srs], key=lambda s: s.lower()) return sr_names @property def menus(self): filters = [ NamedButton('all_promos', dest=''), NamedButton('future_promos'), NamedButton('unpaid_promos'), NamedButton('rejected_promos'), NamedButton('pending_promos'), NamedButton('live_promos'), ] menus = [ NavMenu(filters, base_path='/promoted', title='show', type='lightdrop') ] if self.sort == 'live_promos' and c.user_is_sponsor: sr_names = self.subreddits_with_promos() buttons = [NavButton(name, name) for name in sr_names] frontbutton = NavButton('FRONTPAGE', Frontpage.name, aliases=[ '/promoted/live_promos/%s' % urllib.quote(Frontpage.name) ]) buttons.insert(0, frontbutton) buttons.insert(0, NavButton('all', '')) menus.append( NavMenu(buttons, base_path='/promoted/live_promos', title='subreddit', type='lightdrop')) return menus def keep_fn(self): def keep(item): if item.promoted and not item._deleted: return True else: return False return keep def query(self): if c.user_is_sponsor: if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos" and self.sr: return self.live_by_subreddit(self.sr) elif self.sort == 'live_promos': return queries.get_all_live_links() elif self.sort == 'underdelivered': q = queries.get_underdelivered_campaigns() campaigns = PromoCampaign._by_fullname(list(q), data=True, return_dict=False) link_ids = [camp.link_id for camp in campaigns] return [Link._fullname_from_id36(to36(id)) for id in link_ids] return queries.get_all_promoted_links() else: if self.sort == "future_promos": return queries.get_unapproved_links(c.user._id) elif self.sort == "pending_promos": return queries.get_accepted_links(c.user._id) elif self.sort == "unpaid_promos": return queries.get_unpaid_links(c.user._id) elif self.sort == "rejected_promos": return queries.get_rejected_links(c.user._id) elif self.sort == "live_promos": return queries.get_live_links(c.user._id) return queries.get_promoted_links(c.user._id) @validate(VSponsor(), sr=nop('sr')) def GET_listing(self, sr=None, sort="", **env): if not c.user_is_loggedin or not c.user.email_verified: return self.redirect("/ad_inq") self.sort = sort self.sr = None if sr and sr == Frontpage.name: self.sr = Frontpage elif sr: try: self.sr = Subreddit._by_name(sr) except NotFound: pass return ListingController.GET_listing(self, **env) GET_index = GET_listing @validate(VSponsor()) def GET_new_promo(self): return PromotePage('content', content=PromoteLinkNew()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkForm(link, rendered) page = PromotePage('new_promo', content=form) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @json_validate(sr=VSubmitSR('sr', promotion=True), start=VDate('startdate'), end=VDate('enddate')) def GET_check_inventory(self, responder, sr, start, end): sr = sr or Frontpage available_by_datestr = inventory.get_available_pageviews(sr, start, end, datestr=True) return {'inventory': available_by_datestr} @validate(VSponsor(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_graph(self, dates): start, end, bad_dates = _check_dates(dates) return PromotePage("graph", content=Promote_Graph( start, end, bad_dates=bad_dates)).render() @validate(VSponsorAdmin(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_admingraph(self, dates): start, end, bad_dates = _check_dates(dates) content = Promote_Graph(start, end, bad_dates=bad_dates, admin_view=True) if c.render_style == 'csv': return content.as_csv() return PromotePage("admingraph", content=content).render() # ## POST controllers below @validatedForm(VSponsorAdmin(), link=VLink("link_id"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if campaign_has_oversold_error(form, campaign): form.set_html(".freebie", "target oversold, can't freebie") return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after("<p>" + text + "</p>") @noresponse(VSponsorAdmin(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validate(VSponsorAdmin(), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_refund(self, link, campaign): if campaign.link_id != link._id: return self.abort404() content = RefundPage(link, campaign) return Reddit("refund", content=content, show_sidebar=False).render() @validatedForm(VSponsorAdmin(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = campaign.bid - billable_amount if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount) form.set_html('.status', _('refund succeeded')) else: form.set_html('.status', _('refund not needed')) @validatedForm(VSponsor('link_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100)) def POST_edit_promo(self, form, jquery, ip, l, title, url, disable_comments, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or form.has_errors('url', errors.NO_URL, errors.BAD_URL) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url, c.user, ip) elif promote.is_promo(l): changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) # comment disabling is free to be changed any time. l.disable_comments = disable_comments if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage('content', content=Roadblocks()).render() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates PromotedLinkRoadblock.add(sr, sd, ed) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates PromotedLinkRoadblock.remove(sr, sd, ed) jquery.refresh() @validatedForm(VSponsor('link_id'), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), link=VLink('link_id'), bid=VBid('bid', min=0, max=g.max_promote_bid, coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10)) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, sr, targeting): if not link: return start, end = dates or (None, None) author = Account._byID(link.author_id, data=True) cpm = author.cpm_selfserve_pennies if (start and end and not promote.is_accepted(link) and not c.user_is_sponsor): # if the ad is not approved already, ensure the start date # is at least 2 days in the future start = start.date() end = end.date() now = promote.promo_datetime_now() future = make_offset_date(now, g.min_promote_future, business_days=True) if start < future.date(): c.errors.add(errors.BAD_FUTURE_DATE, msg_params=dict(day=g.min_promote_future), field="startdate") if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return if form.has_errors('bid', errors.BAD_BID): return if campaign_id36: # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) if (bid != campaign.bid and campaign.start_date < datetime.now(g.tz) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return except NotFound: pass min_bid = 0 if c.user_is_sponsor else g.min_promote_bid if bid is None or bid < min_bid: c.errors.add(errors.BAD_BID, field='bid', msg_params={ 'min': min_bid, 'max': g.max_promote_bid }) form.has_errors('bid', errors.BAD_BID) return if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = { "start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y') } c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return elif targeting == 'none': sr = None # Check inventory campaign_id = campaign._id if campaign_id36 else None if has_oversold_error(form, campaign_id, start, end, bid, cpm, sr): return if campaign_id36 is not None: campaign = PromoCampaign._byID36(campaign_id36) promote.edit_campaign(link, campaign, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) else: campaign = promote.new_campaign(link, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) @validatedForm(VSponsor('link_id'), VModhash(), l=VLink('link_id'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if l and campaign: promote.delete_campaign(l, campaign) @validatedForm(VSponsor('container'), VModhash(), user=VExistingUname('name'), thing=VByName('container')) def POST_traffic_viewer(self, form, jquery, user, thing): """ Adds a user to the list of users allowed to view a promoted link's traffic page. """ if not form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): form.set_inputs(name="") form.set_html(".status:first", _("added")) if promote.add_traffic_viewer(thing, user): user_row = TrafficViewerList(thing).user_row( 'traffic_viewer', user) jquery(".traffic_viewer-table").show().find( "table").insert_table_rows(user_row) # send the user a message msg = user_added_messages['traffic']['pm']['msg'] subj = user_added_messages['traffic']['pm']['subject'] if msg and subj: d = dict(url=thing.make_permalink_slow(), traffic_url=promote.promo_traffic_url(thing), title=thing.title) msg = msg % d item, inbox_rel = Message._new(c.user, user, subj, msg, request.ip) queries.new_message(item, inbox_rel) @validatedForm(VSponsor('container'), VModhash(), iuser=VByName('id'), thing=VByName('container')) def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): if thing and iuser: promote.rm_traffic_viewer(thing, iuser) @validatedForm( VSponsor('link'), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], allowed_countries=g.allowed_pay_countries), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): # Check inventory if campaign_has_oversold_error(form, campaign): return address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], errors.BAD_ADDRESS) or form.has_errors( ["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign( link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html( ".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if not campaign.link_id == link._id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() def GET_link_thumb(self, *a, **kw): """ See GET_upload_sr_image for rationale """ return "nothing to see here." @validate(VSponsor("link_id"), link=VByName('link_id'), file=VLength('file', 500 * 1024), img_type=VImageType('img_type')) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".%s" % img_type) except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate(VSponsorAdmin(), launchdate=VDate('ondate'), dates=VDateRange(['startdate', 'enddate']), query_type=VOneOf('q', ('started_on', 'between'), default=None)) def GET_admin(self, launchdate=None, dates=None, query_type=None): return PromoAdminTool(query_type=query_type, launchdate=launchdate, start=dates[0], end=dates[1]).render() @validate(VSponsorAdminOrAdminSecret('secret'), start=VDate('startdate'), end=VDate('enddate'), link_text=nop('link_text'), owner=VAccountByName('owner')) def GET_report(self, start, end, link_text=None, owner=None): now = datetime.now(g.tz).replace(hour=0, minute=0, second=0, microsecond=0) end = end or now - timedelta(days=1) start = start or end - timedelta(days=7) links = [] bad_links = [] owner_name = owner.name if owner else '' if owner: promo_weights = PromotionWeights.get_campaigns(start, end, author_id=owner._id) campaign_ids = [pw.promo_idx for pw in promo_weights] campaigns = PromoCampaign._byID(campaign_ids, data=True) link_ids = {camp.link_id for camp in campaigns.itervalues()} links.extend(Link._byID(link_ids, data=True, return_dict=False)) if link_text is not None: id36s = link_text.replace(',', ' ').split() try: links_from_text = Link._byID36(id36s, data=True) except NotFound: links_from_text = {} bad_links = [id36 for id36 in id36s if id36 not in links_from_text] links.extend(links_from_text.values()) content = PromoteReport(links, link_text, owner_name, bad_links, start, end) if c.render_style == 'csv': return content.as_csv() else: return PromotePage('report', content=content).render()
def run(self, geo): if geo in iso3166.countries_by_alpha2: return geo else: return None EVENT_CONFIGURATION_VALIDATORS = { "title": VLength("title", max_length=120), "description": VMarkdownLength("description", max_length=120, empty_error=None), "resources": VMarkdownLength("resources", max_length=10000, empty_error=None), "nsfw": VBoolean("nsfw"), } def is_event_configuration_valid(form): if form.has_errors("title", errors.NO_TEXT, errors.TOO_LONG): return False if form.has_errors("description", errors.TOO_LONG): return False if form.has_errors("resources", errors.TOO_LONG): return False return True
class AdzerkApiController(api.ApiController): @csrf_exempt @allow_oauth2_access @validate( srnames=VPrintable("srnames", max_length=2100), is_mobile_web=VBoolean('is_mobile_web'), platform=VOneOf("platform", [ "desktop", "mobile_web", "mobile_native", ], default=None), loid=nop('loid', None), is_refresh=VBoolean("is_refresh", default=False), ) def POST_request_promo(self, srnames, is_mobile_web, platform, loid, is_refresh): self.OPTIONS_request_promo() if not srnames: return # backwards compat if platform is None: platform = "mobile_web" if is_mobile_web else "desktop" srnames = srnames.split('+') # request multiple ads in case some are hidden by the builder due # to the user's hides/preferences response = adzerk_request(srnames, self.get_uid(loid), platform=platform) if not response: g.stats.simple_event('adzerk.request.no_promo') return # for adservers, adzerk returns markup so we pass it to the client if isinstance(response, AdserverResponse): g.stats.simple_event('adzerk.request.adserver') return responsive(response.body) res_by_campaign = {r.campaign: r for r in response} adserver_click_urls = {r.campaign: r.click_url for r in response} tuples = [promote.PromoTuple(r.link, 1., r.campaign) for r in response] builder = CampaignBuilder(tuples, wrap=default_thing_wrapper(), keep_fn=promote.promo_keep_fn, num=1, skip=True) listing = LinkListing(builder, nextprev=False).listing() promote.add_trackers(listing.things, c.site, adserver_click_urls=adserver_click_urls) promote.update_served(listing.things) if listing.things: g.stats.simple_event('adzerk.request.valid_promo') if is_refresh: g.stats.simple_event('adzerk.request.auto_refresh') w = listing.things[0] r = res_by_campaign[w.campaign] up = UrlParser(r.imp_pixel) up.hostname = "pixel.redditmedia.com" w.adserver_imp_pixel = up.unparse() w.adserver_upvote_pixel = r.upvote_pixel w.adserver_downvote_pixel = r.downvote_pixel w.adserver_click_url = r.click_url w.num = "" return responsive(w.render(), space_compress=True) else: g.stats.simple_event('adzerk.request.skip_promo') def get_uid(self, loid): if c.user_is_loggedin: return c.user._id36 elif loid: return loid else: return None
class GoldApiController(RedditController): @json_validate(VUser(), VGold(), VModhash(), deal=VLength('deal', 100)) def POST_claim_gold_partner_deal_code(self, responder, deal): try: return {'code': GoldPartnerDealCode.claim_code(c.user, deal)} except GoldPartnerCodesExhaustedError: return { 'error': 'GOLD_PARTNER_CODES_EXHAUSTED', 'explanation': _("sorry, we're out of codes!") } @validatedForm( VUser(), VGold(), VModhash(), public=VBoolean("public"), snoo_color=VSnooColor("snoo_color"), unvalidated_components=VJSON("components"), ) def POST_snoovatar(self, form, jquery, public, snoo_color, unvalidated_components): if form.has_errors( "components", errors.NO_TEXT, errors.TOO_LONG, errors.BAD_STRING, ): return if form.has_errors("snoo_color", errors.BAD_CSS_COLOR): return try: tailors = g.plugins["gold"].tailors_data validated = {} for tailor in tailors: tailor_name = tailor["name"] component = unvalidated_components.get(tailor_name) # if the tailor requires a selection, ensure there is one if not tailor["allow_clear"]: require(component) # ensure this dressing exists dressing = component.get("dressingName") if dressing: for d in tailor["dressings"]: if dressing == d["name"]: break else: raise RequirementException validated[tailor_name] = component except RequirementException: c.errors.add(errors.INVALID_SNOOVATAR, field="components") form.has_errors("components", errors.INVALID_SNOOVATAR) return SnoovatarsByAccount.save( user=c.user, name="snoo", public=public, snoo_color=snoo_color, components=validated, )
class MultiApiController(RedditController): on_validation_error = staticmethod(abort_with_error) def pre(self): set_extension(request.environ, "json") RedditController.pre(self) def _format_multi_list(self, multis, viewer, expand_srs): templ = LabeledMultiJsonTemplate(expand_srs) resp = [ templ.render(multi).finalize() for multi in multis if multi.can_view(viewer) ] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( user=VAccountByName("username"), expand_srs=VBoolean("expand_srs"), ) @api_doc(api_section.multis, uri="/api/multi/user/{username}") def GET_list_multis(self, user, expand_srs): """Fetch a list of public multis belonging to `username`""" multis = LabeledMulti.by_owner(user) return self._format_multi_list(multis, c.user, expand_srs) @require_oauth2_scope("read") @validate( sr=VSRByName('srname'), expand_srs=VBoolean("expand_srs"), ) def GET_list_sr_multis(self, sr, expand_srs): """Fetch a list of public multis belonging to subreddit `srname`""" multis = LabeledMulti.by_owner(sr) return self._format_multi_list(multis, c.user, expand_srs) @require_oauth2_scope("read") @validate(VUser(), expand_srs=VBoolean("expand_srs")) @api_doc(api_section.multis, uri="/api/multi/mine") def GET_my_multis(self, expand_srs): """Fetch a list of multis belonging to the current user.""" multis = LabeledMulti.by_owner(c.user) return self._format_multi_list(multis, c.user, expand_srs) def _format_multi(self, multi, expand_sr_info=False): multi_info = LabeledMultiJsonTemplate(expand_sr_info).render(multi) return self.api_wrapper(multi_info.finalize()) @require_oauth2_scope("read") @validate( multi=VMultiByPath("multipath", require_view=True), expand_srs=VBoolean("expand_srs"), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}", uri_variants=['/api/filter/{filterpath}'], ) def GET_multi(self, multi, expand_srs): """Fetch a multi's data and subreddit list by name.""" return self._format_multi(multi, expand_srs) def _check_new_multi_path(self, path_info): if path_info['prefix'] == 'r': return self._check_sr_multi_path(path_info) return self._check_user_multi_path(path_info) def _check_user_multi_path(self, path_info): if path_info['owner'].lower() != c.user.name.lower(): raise RedditError('MULTI_CANNOT_EDIT', code=403, fields='multipath') return c.user def _check_sr_multi_path(self, path_info): try: sr = Subreddit._by_name(path_info['owner']) except NotFound: raise RedditError('SUBREDDIT_NOEXIST', code=404) if (not sr.is_moderator_with_perms(c.user, 'config') and not c.user_is_admin): raise RedditError('MULTI_CANNOT_EDIT', code=403, fields='multipath') return sr def _add_multi_srs(self, multi, sr_datas): srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas) for sr in srs.itervalues(): if isinstance(sr, FakeSubreddit): raise RedditError('MULTI_SPECIAL_SUBREDDIT', msg_params={'path': sr.path}, code=400) sr_props = {} for sr_data in sr_datas: try: sr = srs[sr_data['name']] except KeyError: raise RedditError('SUBREDDIT_NOEXIST', code=400) else: # name is passed in via the API data format, but should not be # stored on the model. del sr_data['name'] sr_props[sr] = sr_data try: multi.add_srs(sr_props) except TooManySubredditsError as e: raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409) return sr_props def _write_multi_data(self, multi, data): srs = data.pop('subreddits', None) if srs is not None: multi.clear_srs() try: self._add_multi_srs(multi, srs) except: multi._revert() raise if 'icon_name' in data: try: multi.set_icon_by_name(data.pop('icon_name')) except: multi._revert() raise for key, val in data.iteritems(): if key in WRITABLE_MULTI_FIELDS: setattr(multi, key, val) multi._commit() return multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath", required=False), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def POST_multi(self, path_info, data): """Create a multi. Responds with 409 Conflict if it already exists.""" if not path_info and "path" in data: path_info = VMultiPath("").run(data["path"]) elif 'display_name' in data: # if path not provided, create multi for user path = LabeledMulti.slugify(c.user, data['display_name']) path_info = VMultiPath("").run(path) if not path_info: raise RedditError('BAD_MULTI_PATH', code=400) owner = self._check_new_multi_path(path_info) try: LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], owner) response.status = 201 else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def PUT_multi(self, path_info, data): """Create or update a multi.""" owner = self._check_new_multi_path(path_info) try: multi = LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], owner) response.status = 201 self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), ) @api_doc(api_section.multis, extends=GET_multi) def DELETE_multi(self, multi): """Delete a multi.""" multi.delete() def _copy_multi(self, from_multi, to_path_info, rename=False): """Copy a multi to a user account.""" to_owner = self._check_new_multi_path(to_path_info) # rename requires same owner if rename and from_multi.owner != to_owner: raise RedditError('MULTI_CANNOT_EDIT', code=400) try: LabeledMulti._byID(to_path_info['path']) except tdb_cassandra.NotFound: to_multi = LabeledMulti.copy(to_path_info['path'], from_multi, owner=to_owner) else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') return to_multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_view=True, kinds='m'), to_path_info=VMultiPath( "to", required=False, docs={"to": "destination multireddit url path"}, ), display_name=VLength("display_name", max_length=MAX_DISP_NAME, empty_error=None), ) @api_doc( api_section.multis, uri="/api/multi/copy", ) def POST_multi_copy(self, from_multi, to_path_info, display_name): """Copy a multi. Responds with 409 Conflict if the target already exists. A "copied from ..." line will automatically be appended to the description. """ if not to_path_info: if display_name: # if path not provided, copy multi to same owner path = LabeledMulti.slugify(from_multi.owner, display_name) to_path_info = VMultiPath("").run(path) else: raise RedditError('BAD_MULTI_PATH', code=400) to_multi = self._copy_multi(from_multi, to_path_info) from_path = from_multi.path to_multi.copied_from = from_path if to_multi.description_md: to_multi.description_md += '\n\n' to_multi.description_md += _('copied from %(source)s') % { # force markdown linking since /user/foo is not autolinked 'source': '[%s](%s)' % (from_path, from_path) } to_multi.visibility = 'private' if display_name: to_multi.display_name = display_name to_multi._commit() return self._format_multi(to_multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_edit=True, kinds='m'), to_path_info=VMultiPath( "to", required=False, docs={"to": "destination multireddit url path"}, ), display_name=VLength("display_name", max_length=MAX_DISP_NAME, empty_error=None), ) @api_doc( api_section.multis, uri="/api/multi/rename", ) def POST_multi_rename(self, from_multi, to_path_info, display_name): """Rename a multi.""" if not to_path_info: if display_name: path = LabeledMulti.slugify(from_multi.owner, display_name) to_path_info = VMultiPath("").run(path) else: raise RedditError('BAD_MULTI_PATH', code=400) to_multi = self._copy_multi(from_multi, to_path_info, rename=True) if display_name: to_multi.display_name = display_name to_multi._commit() from_multi.delete() return self._format_multi(to_multi) def _get_multi_subreddit(self, multi, sr): resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True), sr=VSRByName('srname'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/r/{srname}", uri_variants=['/api/filter/{filterpath}/r/{srname}'], ) def GET_multi_subreddit(self, multi, sr): """Get data about a subreddit in a multi.""" return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr_name=VSubredditName('srname', allow_language_srs=True), data=VValidatedJSON("model", multi_sr_data_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def PUT_multi_subreddit(self, multi, sr_name, data): """Add a subreddit to a multi.""" new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs) data['name'] = sr_name sr_props = self._add_multi_srs(multi, [data]) sr = sr_props.items()[0][0] multi._commit() if new: response.status = 201 return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr=VSRByName('srname'), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def DELETE_multi_subreddit(self, multi, sr): """Remove a subreddit from a multi.""" multi.del_srs(sr) multi._commit() def _format_multi_description(self, multi): resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True, kinds='m'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/description", ) def GET_multi_description(self, multi): """Get a multi's description.""" return self._format_multi_description(multi) @require_oauth2_scope("read") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True, kinds='m'), data=VValidatedJSON('model', multi_description_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_description) def PUT_multi_description(self, multi, data): """Change a multi's markdown description.""" multi.description_md = data['body_md'] multi._commit() return self._format_multi_description(multi)
class PromoteApiController(ApiController): @json_validate(sr=VSubmitSR('sr', promotion=True), location=VLocation(), start=VDate('startdate'), end=VDate('enddate')) def GET_check_inventory(self, responder, sr, location, start, end): sr = sr or Frontpage if not location or not location.country: available = inventory.get_available_pageviews(sr, start, end, datestr=True) else: available = inventory.get_available_pageviews_geotargeted( sr, location, start, end, datestr=True) return {'inventory': available} @validatedForm(VSponsorAdmin(), VModhash(), link=VLink("link_id36"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') if campaign_has_oversold_error(form, campaign): form.set_html(".freebie", "target oversold, can't freebie") return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), VModhash(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after("<p>" + websafe(text) + "</p>") @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = promote.get_refund_amount(campaign, billable_amount) if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount, billable_impressions) form.set_html('.status', _('refund succeeded')) else: form.set_html('.status', _('refund not needed')) @validatedForm(VSponsor('link_id36'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), username=VLength('username', 100, empty_error=None), l=VLink('link_id36'), title=VTitle('title'), url=VUrl('url', allow_self=False), selftext=VSelfText('text'), kind=VOneOf('kind', ['link', 'self']), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), sendreplies=VBoolean("sendreplies"), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100)) def POST_edit_promo(self, form, jquery, ip, username, l, title, url, selftext, kind, disable_comments, sendreplies, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for user override if not l and c.user_is_sponsor and username: try: user = Account._by_name(username) except NotFound: c.errors.add(errors.USER_DOESNT_EXIST, field="username") form.set_error(errors.USER_DOESNT_EXIST, "username") return if not user.email: c.errors.add(errors.NO_EMAIL_FOR_USER, field="username") form.set_error(errors.NO_EMAIL_FOR_USER, "username") return if not user.email_verified: c.errors.add(errors.NO_VERIFIED_EMAIL, field="username") form.set_error(errors.NO_VERIFIED_EMAIL, "username") return else: user = c.user # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url if kind == 'link': if form.has_errors('url', errors.NO_URL, errors.BAD_URL): return # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url if kind == 'link' else 'self', selftext if kind == 'self' else '', user, ip) elif promote.is_promo(l): # changing link type is not allowed if ((l.is_self and kind == 'link') or (not l.is_self and kind == 'self')): c.errors.add(errors.NO_CHANGE_KIND, field="kind") form.set_error(errors.NO_CHANGE_KIND, "kind") return changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if kind == 'link' and url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed: promote.unapprove_promotion(l) # selftext can be changed at any time if kind == 'self': l.selftext = selftext # comment disabling and sendreplies is free to be changed any time. l.disable_comments = disable_comments l.sendreplies = sendreplies if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], reference_date=promote.promo_datetime_now), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors( 'enddate', errors.BAD_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates PromotedLinkRoadblock.add(sr, sd, ed) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], reference_date=promote.promo_datetime_now), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates PromotedLinkRoadblock.remove(sr, sd, ed) jquery.refresh() @validatedForm(VSponsor('link_id36'), VModhash(), dates=VDateRange( ['startdate', 'enddate'], earliest=timedelta(days=g.min_promote_future), latest=timedelta(days=g.max_promote_future), reference_date=promote.promo_datetime_now, business_days=True, sponsor_override=True), link=VLink('link_id36'), bid=VFloat('bid', coerce=False), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10), priority=VPriority("priority"), location=VLocation()) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, sr, targeting, priority, location): if not link: return start, end = dates or (None, None) if location and sr and not c.user_is_sponsor: # only sponsors can geotarget on subreddits location = None if location and location.metro: cpm = g.cpm_selfserve_geotarget_metro.pennies elif location: cpm = g.cpm_selfserve_geotarget_country.pennies else: author = Account._byID(link.author_id, data=True) cpm = author.cpm_selfserve_pennies if (form.has_errors('startdate', errors.BAD_DATE, errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE) or form.has_errors('enddate', errors.BAD_DATE, errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return campaign = None if campaign_id36: try: campaign = PromoCampaign._byID36(campaign_id36) except NotFound: pass if campaign and link._id != campaign.link_id: return abort(404, 'not found') if priority.cpm: min_bid = 0 if c.user_is_sponsor else g.min_promote_bid max_bid = None if c.user_is_sponsor else g.max_promote_bid if bid is None or bid < min_bid or (max_bid and bid > max_bid): c.errors.add(errors.BAD_BID, field='bid', msg_params={ 'min': min_bid, 'max': max_bid or g.max_promote_bid }) form.has_errors('bid', errors.BAD_BID) return # you cannot edit the bid of a live ad unless it's a freebie if (campaign and bid != campaign.bid and promote.is_live_promo(link, campaign) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return else: bid = 0. # Set bid to 0 as dummy value if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = { "start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y') } c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return elif targeting == 'none': sr = None # Check inventory campaign = campaign if campaign_id36 else None if not priority.inventory_override: oversold = has_oversold_error(form, campaign, start, end, bid, cpm, sr, location) if oversold: return if campaign: promote.edit_campaign(link, campaign, dates, bid, cpm, sr, priority, location) else: campaign = promote.new_campaign(link, dates, bid, cpm, sr, priority, location) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm(VSponsor('link_id36'), VModhash(), l=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if not campaign or not l or l._id != campaign.link_id: return abort(404, 'not found') promote.delete_campaign(l, campaign) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_terminate_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') promote.terminate_campaign(link, campaign) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm(VSponsor('link'), VModhash(), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ]), creditcard=ValidCard( ["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') # Check inventory if campaign_has_oversold_error(form, campaign): return address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], errors.BAD_ADDRESS) or form.has_errors( ["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign( link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html( ".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link_name"), VModhash(), link=VByName('link_name'), file=VUploadLength('file', 500 * 1024), img_type=VImageType('img_type')) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".%s" % img_type) except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render()
class ModmailController(OAuth2OnlyController): def pre(self): # Set user_is_admin property on context, # normally set but this controller does not inherit # from RedditController super(ModmailController, self).pre() admin_usernames = [ name.lower() for name in g.live_config['modmail_admins'] ] c.user_is_admin = False if c.user_is_loggedin: c.user_is_admin = c.user.name.lower() in admin_usernames VNotInTimeout().run() def post(self): Session.remove() super(ModmailController, self).post() @require_oauth2_scope('modmail') @validate( srs=VSRByNames('entity', required=False), after=VModConversation('after', required=False), limit=VInt('limit', num_default=25), sort=VOneOf('sort', options=('recent', 'mod', 'user'), default='recent'), state=VOneOf('state', options=('new', 'inprogress', 'mod', 'notifications', 'archived', 'highlighted', 'all'), default='all'), ) def GET_conversations(self, srs, after, limit, sort, state): """Get conversations for logged in user or subreddits Querystring Params: entity -- name of the subreddit or a comma separated list of subreddit names (i.e. iama, pics etc) limit -- number of elements to retrieve (default: 25) after -- the id of the last item seen sort -- parameter on how to sort the results, choices: recent: max(last_user_update, last_mod_update) mod: last_mod_update user: last_user_update state -- this parameter lets users filter messages by state choices: new, inprogress, mod, notifications, archived, highlighted, all """ # Retrieve subreddits in question, if entities are passed # check if a user is a moderator for the passed entities. # If no entities are passed grab all subreddits the logged in # user moderates and has modmail permissions for modded_entities = {} modded_srs = c.user.moderated_subreddits('mail') modded_srs = {sr._fullname: sr for sr in modded_srs} if srs: for sr in srs.values(): if sr._fullname in modded_srs: modded_entities[sr._fullname] = sr else: return self.send_error(403, errors.BAD_SR_NAME, fields='entity') else: modded_entities = modded_srs if not modded_entities: return self.send_error(404, errors.SR_NOT_FOUND, fields='entity') # Retrieve conversations for given entities conversations = ModmailConversation.get_mod_conversations( modded_entities.values(), viewer=c.user, limit=limit, after=after, sort=sort, state=state) conversation_ids = [] conversations_dict = {} messages_dict = {} author_ids = [] # Extract author ids to query for all accounts at once for conversation in conversations: author_ids.extend(conversation.author_ids) author_ids.extend(conversation.mod_action_account_ids) # Query for associated account object of authors and serialize the # conversation in the correct context authors = self._try_get_byID(author_ids, Account, ignore_missing=True) for conversation in conversations: conversation_ids.append(conversation.id36) conversations_dict[ conversation.id36] = conversation.to_serializable( authors, modded_entities[conversation.owner_fullname], ) latest_message = conversation.messages[0] messages_dict[ latest_message.id36] = latest_message.to_serializable( modded_entities[conversation.owner_fullname], authors[latest_message.author_id], c.user, ) return simplejson.dumps({ 'viewerId': c.user._fullname, 'conversationIds': conversation_ids, 'conversations': conversations_dict, 'messages': messages_dict, }) @require_oauth2_scope('modmail') @validate( entity=VSRByName('srName'), subject=VLength('subject', max_length=100), body=VMarkdownLength('body'), is_author_hidden=VBoolean('isAuthorHidden', default=False), to=VModConvoRecipient('to', required=False), ) def POST_conversations(self, entity, subject, body, is_author_hidden, to): """Creates a new conversation for a particular SR This endpoint will create a ModmailConversation object as well as the first ModmailMessage within the ModmailConversation object. POST Params: srName -- the human readable name of the subreddit subject -- the subject of the first message in the conversation body -- the body of the first message in the conversation isAuthorHidden -- boolean on whether the mod name should be hidden (only mods can use this flag) to -- name of the user that a mod wants to create a convo with (only mods can use this flag) """ self._feature_enabled_check(entity) # make sure the user is not muted when creating a new conversation if entity.is_muted(c.user) and not c.user_is_admin: return self.send_error(400, errors.USER_MUTED) # validate post params if (errors.USER_BLOCKED, to) in c.errors: return self.send_error(400, errors.USER_BLOCKED, fields='to') elif (errors.USER_DOESNT_EXIST, to) in c.errors: return self.send_error(404, errors.USER_DOESNT_EXIST, fields='to') if to and not isinstance(to, Account): return self.send_error( 422, errors.NO_SR_TO_SR_MESSAGE, fields='to', ) # only mods can set a 'to' parameter if (not entity.is_moderator_with_perms(c.user, 'mail') and to): return self.send_error(403, errors.MOD_REQUIRED, fields='to') if to and entity.is_muted(to): return self.send_error( 400, errors.MUTED_FROM_SUBREDDIT, fields='to', ) try: conversation = ModmailConversation( entity, c.user, subject, body, is_author_hidden=is_author_hidden, to=to, ) except MustBeAModError: return self.send_error(403, errors.MOD_REQUIRED, fields='isAuthorHidden') except Exception as e: g.log.error('Failed to save conversation: {}'.format(e)) return self.send_error(500, errors.CONVERSATION_NOT_SAVED) # Create copy of the message in the legacy messaging system as well if to: message, inbox_rel = Message._new( c.user, to, subject, body, request.ip, sr=entity, from_sr=is_author_hidden, create_modmail=False, ) else: message, inbox_rel = Message._new( c.user, entity, subject, body, request.ip, create_modmail=False, ) queries.new_message(message, inbox_rel) conversation.set_legacy_first_message_id(message._id) # Get author associated account object for serialization # of the newly created conversation object authors = self._try_get_byID(conversation.author_ids, Account, ignore_missing=True) response.status_code = 201 serializable_convo = conversation.to_serializable(authors, entity, all_messages=True, current_user=c.user) messages = serializable_convo.pop('messages') mod_actions = serializable_convo.pop('modActions') g.events.new_modmail_event( 'ss.send_modmail_message', conversation, message=conversation.messages[0], msg_author=c.user, sr=entity, request=request, context=c, ) return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, 'modActions': mod_actions, }) @require_oauth2_scope('modmail') @validate( conversation=VModConversation('conversation_id'), mark_read=VBoolean('markRead', default=False), ) def GET_mod_messages(self, conversation, mark_read): """Returns all messages for a given conversation id Url Params: conversation_id -- this is the id of the conversation you would like to grab messages for Querystring Param: markRead -- if passed the conversation will be marked read when the conversation is returned """ self._validate_vmodconversation() sr = self._try_get_subreddit_access(conversation, admin_override=True) authors = self._try_get_byID(list( set(conversation.author_ids) | set(conversation.mod_action_account_ids)), Account, ignore_missing=True) serializable_convo = conversation.to_serializable(authors, sr, all_messages=True, current_user=c.user) messages = serializable_convo.pop('messages') mod_actions = serializable_convo.pop('modActions') # Get participant user info for conversation try: userinfo = self._get_modmail_userinfo(conversation, sr=sr) except ValueError: userinfo = {} except NotFound: return self.send_error(404, errors.USER_DOESNT_EXIST) if mark_read: conversation.mark_read(c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='read', request=request, context=c, ) return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, 'modActions': mod_actions, 'user': userinfo, }) @require_oauth2_scope('modmail') def GET_modmail_enabled_srs(self): # sr_name, sr_icon, subsriber_count, most_recent_action modded_srs = c.user.moderated_subreddits('mail') enabled_srs = [ modded_sr for modded_sr in modded_srs if feature.is_enabled('new_modmail', subreddit=modded_sr.name) ] recent_convos = ModmailConversation.get_recent_convo_by_sr(enabled_srs) results = {} for sr in enabled_srs: results.update({ sr._fullname: { 'id': sr._fullname, 'name': sr.name, 'icon': sr.icon_img, 'subscribers': sr._ups, 'lastUpdated': recent_convos.get(sr._fullname), } }) return simplejson.dumps({'subreddits': results}) @require_oauth2_scope('modmail') @validate( conversation=VModConversation('conversation_id'), msg_body=VMarkdownLength('body'), is_author_hidden=VBoolean('isAuthorHidden', default=False), is_internal=VBoolean('isInternal', default=False), ) def POST_mod_messages(self, conversation, msg_body, is_author_hidden, is_internal): """Creates a new message for a particular ModmailConversation URL Params: conversation_id -- id of the conversation to post a new message to POST Params: body -- this is the message body isAuthorHidden -- boolean on whether to hide author, i.e. respond as the subreddit isInternal -- boolean to signify a moderator only message """ self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) # make sure the user is not muted before posting a message if sr.is_muted(c.user): return self.send_error(400, errors.USER_MUTED) if conversation.is_internal and not is_internal: is_internal = True is_mod = sr.is_moderator(c.user) if not is_mod and is_author_hidden: return self.send_error( 403, errors.MOD_REQUIRED, fields='isAuthorHidden', ) elif not is_mod and is_internal: return self.send_error( 403, errors.MOD_REQUIRED, fields='isInternal', ) try: if not conversation.is_internal and not conversation.is_auto: participant = conversation.get_participant_account() if participant and sr.is_muted(participant): return self.send_error( 400, errors.MUTED_FROM_SUBREDDIT, ) except NotFound: pass try: new_message = conversation.add_message( c.user, msg_body, is_author_hidden=is_author_hidden, is_internal=is_internal, ) except: return self.send_error(500, errors.MODMAIL_MESSAGE_NOT_SAVED) # Add the message to the legacy messaging system as well (unless it's # an internal message on a non-internal conversation, since we have no # way to hide specific messages from the external participant) legacy_incompatible = is_internal and not conversation.is_internal if (conversation.legacy_first_message_id and not legacy_incompatible): first_message = Message._byID(conversation.legacy_first_message_id) subject = conversation.subject if not subject.startswith('re: '): subject = 're: ' + subject # Retrieve the participant to decide whether to send the message # to the sr or to the participant. If the currently logged in user # is the same as the participant then address the message to the # sr. recipient = sr if not is_internal: try: participant = ( ModmailConversationParticipant.get_participant( conversation.id)) is_participant = ( (c.user._id == participant.account_id) and not sr.is_moderator_with_perms(c.user, 'mail')) if not is_participant: recipient = Account._byID(participant.account_id) except NotFound: pass message, inbox_rel = Message._new( c.user, recipient, subject, msg_body, request.ip, parent=first_message, from_sr=is_author_hidden, create_modmail=False, ) queries.new_message(message, inbox_rel) serializable_convo = conversation.to_serializable( entity=sr, all_messages=True, current_user=c.user, ) messages = serializable_convo.pop('messages') g.events.new_modmail_event( 'ss.send_modmail_message', conversation, message=new_message, msg_author=c.user, sr=sr, request=request, context=c, ) response.status_code = 201 return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, }) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_highlight(self, conversation): """Marks a conversation as highlighted.""" self._validate_vmodconversation() self._try_get_subreddit_access(conversation) conversation.add_action(c.user, 'highlighted') conversation.add_highlight() # Retrieve updated conversation to be returned updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='highlight', request=request, context=c, ) return simplejson.dumps(updated_convo) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def DELETE_highlight(self, conversation): """Removes a highlight from a conversation.""" self._validate_vmodconversation() self._try_get_subreddit_access(conversation) conversation.add_action(c.user, 'unhighlighted') conversation.remove_highlight() # Retrieve updated conversation to be returned updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='unhighlight', request=request, context=c, ) return simplejson.dumps(updated_convo) @require_oauth2_scope('modmail') @validate(ids=VList('conversationIds')) def POST_unread(self, ids): """Marks conversations as unread for the user. Expects a list of conversation IDs. """ if not ids: return self.send_error(400, 'Must pass an id or list of ids.') try: ids = [int(id, base=36) for id in ids] except: return self.send_error(422, 'Must pass base 36 ids.') try: convos = self._get_conversation_access(ids) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) ModmailConversationUnreadState.mark_unread( c.user, [convo.id for convo in convos]) @require_oauth2_scope('modmail') @validate(ids=VList('conversationIds')) def POST_read(self, ids): """Marks a conversations as read for the user. Expects a list of conversation IDs. """ if not ids: return self.send_error(400, 'Must pass an id or list of ids.') try: ids = [int(id, base=36) for id in ids] except: return self.send_error(422, 'Must pass base 36 ids.') try: convos = self._get_conversation_access(ids) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) response.status_code = 204 ModmailConversationUnreadState.mark_read( c.user, [convo.id for convo in convos]) @require_oauth2_scope('modmail') @validate( ids=VList('conversationIds'), archive=VBoolean('archive', default=True), ) def POST_archive_status(self, ids, archive): try: convos = self._get_conversation_access( [int(id, base=36) for id in ids]) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) convo_ids = [] for convo in convos: if convo.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, fields='conversationIds', ) convo_ids.append(convo.id) if not archive: ModmailConversation.set_states( convo_ids, ModmailConversation.STATE['inprogress']) else: ModmailConversation.set_states( convo_ids, ModmailConversation.STATE['archived']) response.status_code = 204 @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_archive(self, conversation): self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if sr.is_moderator_with_perms(c.user, 'mail'): if conversation.state == ModmailConversation.STATE['archived']: response.status_code = 204 return if conversation.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, ) conversation.add_action(c.user, 'archived') conversation.set_state('archived') updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='archive', request=request, context=c, ) return simplejson.dumps(updated_convo) else: return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_unarchive(self, conversation): self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if sr.is_moderator_with_perms(c.user, 'mail'): if conversation.state != ModmailConversation.STATE['archived']: response.status_code = 204 return if conversation.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, ) conversation.add_action(c.user, 'unarchived') conversation.set_state('inprogress') updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='unarchive', request=request, context=c, ) return simplejson.dumps(updated_convo) else: return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) @require_oauth2_scope('modmail') def GET_unread_convo_count(self): """Endpoint to retrieve the unread conversation count by category""" convo_counts = ModmailConversation.unread_convo_count(c.user) return simplejson.dumps(convo_counts) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def GET_modmail_userinfo(self, conversation): # validate that the currently logged in user is a mod # of the subreddit associated with the conversation self._try_get_subreddit_access(conversation, admin_override=True) try: userinfo = self._get_modmail_userinfo(conversation) except (ValueError, NotFound): return self.send_error(404, errors.USER_DOESNT_EXIST) return simplejson.dumps(userinfo) @require_oauth2_scope('identity') @validate(conversation=VModConversation('conversation_id')) def POST_mute_participant(self, conversation): if conversation.is_internal or conversation.is_auto: return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) sr = Subreddit._by_fullname(conversation.owner_fullname) try: participant = conversation.get_participant_account() except NotFound: return self.send_error(404, errors.USER_DOESNT_EXIST) if not sr.can_mute(c.user, participant): return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) if not c.user_is_admin: if not sr.is_moderator_with_perms(c.user, 'access', 'mail'): return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) if sr.use_quotas: sr_ratelimit = SimpleRateLimit( name="sr_muted_%s" % sr._id36, seconds=g.sr_quota_time, limit=g.sr_muted_quota, ) if not sr_ratelimit.record_and_check(): return self.send_error(403, errors.SUBREDDIT_RATELIMIT) # Add the mute record but only if successful create the # appropriate notifications, this prevents duplicate # notifications from being sent added = sr.add_muted(participant) if not added: return simplejson.dumps( self._convo_to_serializable(conversation, all_messages=True)) MutedAccountsBySubreddit.mute(sr, participant, c.user) permalink = conversation.make_permalink() # Create the appropriate objects to be displayed on the # mute moderation log, use the permalink to the new modmail # system ModAction.create(sr, c.user, 'muteuser', target=participant, description=permalink) sr.add_rel_note('muted', participant, permalink) # Add the muted mod action to the conversation conversation.add_action(c.user, 'muted', commit=True) result = self._get_updated_convo(conversation.id, c.user) result['user'] = self._get_modmail_userinfo(conversation, sr=sr) return simplejson.dumps(result) @require_oauth2_scope('identity') @validate(conversation=VModConversation('conversation_id')) def POST_unmute_participant(self, conversation): if conversation.is_internal or conversation.is_auto: return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) sr = Subreddit._by_fullname(conversation.owner_fullname) try: participant = conversation.get_participant_account() except NotFound: abort(404, errors.USER_DOESNT_EXIST) if not c.user_is_admin: if not sr.is_moderator_with_perms(c.user, 'access', 'mail'): return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) removed = sr.remove_muted(participant) if not removed: return simplejson.dumps( self._convo_to_serializable(conversation, all_messages=True)) MutedAccountsBySubreddit.unmute(sr, participant) ModAction.create(sr, c.user, 'unmuteuser', target=participant) conversation.add_action(c.user, 'unmuted', commit=True) result = self._get_updated_convo(conversation.id, c.user) result['user'] = self._get_modmail_userinfo(conversation, sr=sr) return simplejson.dumps(result) def _get_modmail_userinfo(self, conversation, sr=None): if conversation.is_internal: raise ValueError('Cannot get userinfo for internal conversations') if not sr: sr = Subreddit._by_fullname(conversation.owner_fullname) # Retrieve the participant associated with the conversation try: account = conversation.get_participant_account() if not account: raise ValueError('No account associated with convo') permatimeout = (account.in_timeout and account.days_remaining_in_timeout == 0) if account._deleted or permatimeout: raise ValueError('User info is inaccessible') except NotFound: raise NotFound('Unable to retrieve conversation participant') # Fetch the mute and ban status of the participant as it relates # to the subreddit associated with the conversation. mute_status = sr.is_muted(account) ban_status = sr.is_banned(account) # Parse the ban status and retrieve the length of the ban, # then output the data into a serialiazable dict ban_result = { 'isBanned': bool(ban_status), 'reason': '', 'endDate': None, 'isPermanent': False } if ban_status: ban_result['reason'] = getattr(ban_status, 'note', '') ban_duration = sr.get_tempbans('banned', account.name) ban_duration = ban_duration.get(account.name) if ban_duration: ban_result['endDate'] = ban_duration.isoformat() else: ban_result['isPermanent'] = True ban_result['endDate'] = None # Parse the mute status and retrieve the length of the ban, # then output the data into the serialiazable dict mute_result = { 'isMuted': bool(mute_status), 'endDate': None, 'reason': '' } if mute_status: mute_result['reason'] = getattr(mute_status, 'note', '') muted_items = sr.get_muted_items(account.name) mute_duration = muted_items.get(account.name) if mute_duration: mute_result['endDate'] = mute_duration.isoformat() # Retrieve the participants post and comment fullnames from cache post_fullnames = [] comment_fullnames = [] if not account._spam: post_fullnames = list(queries.get_submitted(account, 'new', 'all'))[:100] comment_fullnames = list( queries.get_comments(account, 'new', 'all'))[:100] # Retrieve the associated link objects for posts and comments # using the retrieve fullnames, afer the link objects are retrieved # create a serializable dict with the the necessary information from # the endpoint. lookup_fullnames = list(set(post_fullnames) | set(comment_fullnames)) posts = Thing._by_fullname(lookup_fullnames) serializable_posts = {} for fullname in post_fullnames: if len(serializable_posts) == 3: break post = posts[fullname] if post.sr_id == sr._id and not post._deleted: serializable_posts[fullname] = { 'title': post.title, 'permalink': post.make_permalink(sr, force_domain=True), 'date': post._date.isoformat(), } # Extract the users most recent comments associated with the # subreddit sr_comments = [] for fullname in comment_fullnames: if len(sr_comments) == 3: break comment = posts[fullname] if comment.sr_id == sr._id and not comment._deleted: sr_comments.append(comment) # Retrieve all associated link objects (combines lookup) comment_links = Link._byID( [sr_comment.link_id for sr_comment in sr_comments]) # Serialize all of the user's sr comments serializable_comments = {} for sr_comment in sr_comments: comment_link = comment_links[sr_comment.link_id] comment_body = sr_comment.body if len(comment_body) > 140: comment_body = '{:.140}...'.format(comment_body) serializable_comments[sr_comment._fullname] = { 'title': comment_link.title, 'comment': comment_body, 'permalink': sr_comment.make_permalink(comment_link, sr, force_domain=True), 'date': sr_comment._date.isoformat(), } return { 'id': account._fullname, 'name': account.name, 'created': account._date.isoformat(), 'banStatus': ban_result, 'isShadowBanned': account._spam, 'muteStatus': mute_result, 'recentComments': serializable_comments, 'recentPosts': serializable_posts, } def _get_updated_convo(self, convo_id, user): # Retrieve updated conversation to be returned updated_convo = ModmailConversation._byID(convo_id, current_user=user) return self._convo_to_serializable(updated_convo, all_messages=True) def _convo_to_serializable(self, conversation, all_messages=False): serialized_convo = conversation.to_serializable( all_messages=all_messages, current_user=c.user) messages = serialized_convo.pop('messages') mod_actions = serialized_convo.pop('modActions') return { 'conversations': serialized_convo, 'messages': messages, 'modActions': mod_actions, } def _validate_vmodconversation(self): if (errors.CONVERSATION_NOT_FOUND, 'conversation_id') in c.errors: return self.send_error(404, errors.CONVERSATION_NOT_FOUND) def _get_conversation_access(self, ids): validated_convos = [] conversations = ModmailConversation._byID(ids) # fetch all srs that a user has modmail permissions to # transform sr to be a dict with a key being the sr fullname # and the value being the sr object itself modded_srs = c.user.moderated_subreddits('mail') sr_by_fullname = { sr._fullname: sr for sr in modded_srs if feature.is_enabled('new_modmail', subreddit=sr.name) } for conversation in tup(conversations): if sr_by_fullname.get(conversation.owner_fullname): validated_convos.append(conversation) else: raise ValueError('Invalid conversation id(s).') return validated_convos def _try_get_byID(self, ids, thing_class, return_dict=True, ignore_missing=False): """Helper method to lookup objects by id for a given model or return a 404 if not found""" try: return thing_class._byID(ids, return_dict=return_dict, ignore_missing=ignore_missing) except NotFound: return self.send_error(404, errors.THING_NOT_FOUND, explanation='{} not found'.format( thing_class.__name__)) except: return self.send_error(422, 'Invalid request') def _try_get_subreddit_access(self, conversation, admin_override=False): sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if (not sr.is_moderator_with_perms(c.user, 'mail') and not (admin_override and c.user_is_admin)): return self.send_error( 403, errors.SUBREDDIT_NO_ACCESS, ) return sr def _feature_enabled_check(self, sr): if not feature.is_enabled('new_modmail', subreddit=sr.name): return self.send_error(403, errors.SR_FEATURE_NOT_ENABLED) def send_error(self, code, error, fields=None, explanation=None): abort( reddit_http_error( code=code or error.code, error_name=error, explanation=explanation, fields=tup(fields), ))
class SponsorListingController(PromoteListingController): titles = dict( PromoteListingController.titles.items() + { 'underdelivered': N_('underdelivered promoted links'), 'reported': N_('reported promoted links'), 'house': N_('house promoted links'), }.items()) base_path = '/sponsor/promoted' @property def title_text(self): return _('promos on reddit') @property def menus(self): if self.sort in {'underdelivered', 'reported', 'house'}: menus = [] else: menus = super(SponsorListingController, self).menus menus.append( NavMenu([ QueryButton("exclude managed", dest=None, query_param='include_managed'), QueryButton("include managed", dest="yes", query_param='include_managed'), ], base_path=request.path, type='lightdrop')) if self.sort == 'live_promos': srnames = promote.all_live_promo_srnames() buttons = [NavButton('all', '', use_params=True)] try: srnames.remove(Frontpage.name) frontbutton = NavButton('FRONTPAGE', Frontpage.name, use_params=True, aliases=[ '/promoted/live_promos/%s' % urllib.quote(Frontpage.name) ]) buttons.append(frontbutton) except KeyError: pass srnames = sorted(srnames, key=lambda name: name.lower()) buttons.extend( NavButton(name, name, use_params=True) for name in srnames) base_path = self.base_path + '/live_promos' menus.append( NavMenu(buttons, base_path=base_path, title='subreddit', type='lightdrop')) return menus @classmethod @memoize('live_by_subreddit', time=300) def _live_by_subreddit(cls, sr_names): promotuples = promote.get_live_promotions(sr_names) return [pt.link for pt in promotuples] def live_by_subreddit(cls, sr): return cls._live_by_subreddit([sr.name]) @classmethod @memoize('house_link_names', time=60) def get_house_link_names(cls): now = promote.promo_datetime_now() campaign_ids = PromotionWeights.get_campaign_ids(now) q = PromoCampaign._query(PromoCampaign.c._id.in_(campaign_ids), PromoCampaign.c.priority_name == 'house', data=True) link_names = { Link._fullname_from_id36(to36(camp.link_id)) for camp in q } return sorted(link_names, reverse=True) def keep_fn(self): base_keep_fn = PromoteListingController.keep_fn(self) def keep(item): if not self.include_managed and item.managed_promo: return False return base_keep_fn(item) return keep def query(self): if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos" and self.sr: return self.live_by_subreddit(self.sr) elif self.sort == 'live_promos': return queries.get_all_live_links() elif self.sort == 'underdelivered': q = queries.get_underdelivered_campaigns() campaigns = PromoCampaign._by_fullname(list(q), data=True, return_dict=False) link_ids = [camp.link_id for camp in campaigns] return [Link._fullname_from_id36(to36(id)) for id in link_ids] elif self.sort == 'reported': return queries.get_reported_links(Subreddit.get_promote_srid()) elif self.sort == 'house': return self.get_house_link_names() elif self.sort == 'all': return queries.get_all_promoted_links() @validate( VSponsorAdmin(), srname=nop('sr'), include_managed=VBoolean("include_managed"), ) def GET_listing(self, srname=None, include_managed=False, sort="all", **kw): self.sort = sort self.sr = None self.include_managed = include_managed if srname: try: self.sr = Subreddit._by_name(srname) except NotFound: pass return ListingController.GET_listing(self, **kw)
class PromoteApiController(ApiController): @json_validate(sr=VSubmitSR('sr', promotion=True), collection=VCollection('collection'), location=VLocation(), start=VDate('startdate'), end=VDate('enddate')) def GET_check_inventory(self, responder, sr, collection, location, start, end): if collection: target = Target(collection) sr = None else: sr = sr or Frontpage target = Target(sr.name) if not allowed_location_and_target(location, target): return abort(403, 'forbidden') available = inventory.get_available_pageviews(target, start, end, location=location, datestr=True) return {'inventory': available} @validatedForm(VSponsorAdmin(), VModhash(), link=VLink("link_id36"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') if campaign_has_oversold_error(form, campaign): form.set_text(".freebie", _("target oversold, can't freebie")) return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), VModhash(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after("<p>" + websafe(text) + "</p>") @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = promote.get_refund_amount(campaign, billable_amount) if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount, billable_impressions) form.set_text('.status', _('refund succeeded')) else: form.set_text('.status', _('refund not needed')) @validatedForm( VSponsor('link_id36'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), username=VLength('username', 100, empty_error=None), l=VLink('link_id36'), title=VTitle('title'), url=VUrl('url', allow_self=False), selftext=VMarkdownLength('text', max_length=40000), kind=VOneOf('kind', ['link', 'self']), disable_comments=VBoolean("disable_comments"), sendreplies=VBoolean("sendreplies"), media_url=VUrl("media_url", allow_self=False, valid_schemes=('http', 'https')), gifts_embed_url=VUrl("gifts_embed_url", allow_self=False, valid_schemes=('http', 'https')), media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")), media_autoplay=VBoolean("media_autoplay"), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100), is_managed=VBoolean("is_managed"), ) def POST_edit_promo(self, form, jquery, username, l, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, gifts_embed_url, media_url_type, domain_override, is_managed): should_ratelimit = False if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for user override if not l and c.user_is_sponsor and username: try: user = Account._by_name(username) except NotFound: c.errors.add(errors.USER_DOESNT_EXIST, field="username") form.set_error(errors.USER_DOESNT_EXIST, "username") return if not user.email: c.errors.add(errors.NO_EMAIL_FOR_USER, field="username") form.set_error(errors.NO_EMAIL_FOR_USER, "username") return if not user.email_verified: c.errors.add(errors.NO_VERIFIED_EMAIL, field="username") form.set_error(errors.NO_VERIFIED_EMAIL, "username") return else: user = c.user # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url if kind == 'link': if form.has_errors('url', errors.NO_URL, errors.BAD_URL): return # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if kind == 'self' and form.has_errors('text', errors.TOO_LONG): return if not l: # creating a new promoted link l = promote.new_promotion(title, url if kind == 'link' else 'self', selftext if kind == 'self' else '', user, request.ip) l.domain_override = domain_override or None if c.user_is_sponsor: l.managed_promo = is_managed l._commit() form.redirect(promote.promo_edit_url(l)) elif not promote.is_promo(l): return # changing link type is not allowed if ((l.is_self and kind == 'link') or (not l.is_self and kind == 'self')): c.errors.add(errors.NO_CHANGE_KIND, field="kind") form.set_error(errors.NO_CHANGE_KIND, "kind") return changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link if not promote.is_promoted(l) or c.user_is_sponsor: if title and title != l.title: l.title = title changed = not c.user_is_sponsor if kind == 'link' and url and url != l.url: l.url = url changed = not c.user_is_sponsor # only trips if the title and url are changed by a non-sponsor if changed: promote.unapprove_promotion(l) # selftext can be changed at any time if kind == 'self': l.selftext = selftext # comment disabling and sendreplies is free to be changed any time. l.disable_comments = disable_comments l.sendreplies = sendreplies if c.user_is_sponsor: if (form.has_errors("media_url", errors.BAD_URL) or form.has_errors("gifts_embed_url", errors.BAD_URL)): return scraper_embed = media_url_type == "scrape" media_url = media_url or None gifts_embed_url = gifts_embed_url or None if c.user_is_sponsor and scraper_embed and media_url != l.media_url: if media_url: media = _scrape_media(media_url, autoplay=media_autoplay, save_thumbnail=False, use_cache=True) if media: l.set_media_object(media.media_object) l.set_secure_media_object(media.secure_media_object) l.media_url = media_url l.gifts_embed_url = None l.media_autoplay = media_autoplay else: c.errors.add(errors.SCRAPER_ERROR, field="media_url") form.set_error(errors.SCRAPER_ERROR, "media_url") return else: l.set_media_object(None) l.set_secure_media_object(None) l.media_url = None l.gifts_embed_url = None l.media_autoplay = False if (c.user_is_sponsor and not scraper_embed and gifts_embed_url != l.gifts_embed_url): if gifts_embed_url: parsed = UrlParser(gifts_embed_url) if not is_subdomain(parsed.hostname, "redditgifts.com"): c.errors.add(errors.BAD_URL, field="gifts_embed_url") form.set_error(errors.BAD_URL, "gifts_embed_url") return iframe = """ <iframe class="redditgifts-embed" src="%(embed_url)s" width="710" height="500" scrolling="no" frameborder="0" allowfullscreen> </iframe> """ % { 'embed_url': websafe(gifts_embed_url) } media_object = { 'oembed': { 'description': 'redditgifts embed', 'height': 500, 'html': iframe, 'provider_name': 'redditgifts', 'provider_url': 'http://www.redditgifts.com/', 'title': 'redditgifts secret santa 2014', 'type': 'rich', 'width': 710 }, 'type': 'redditgifts' } l.set_media_object(media_object) l.set_secure_media_object(media_object) l.media_url = None l.gifts_embed_url = gifts_embed_url l.media_autoplay = False else: l.set_media_object(None) l.set_secure_media_object(None) l.media_url = None l.gifts_embed_url = None l.media_autoplay = False if c.user_is_sponsor: l.media_override = media_override l.domain_override = domain_override or None l.managed_promo = is_managed l._commit() form.redirect(promote.promo_edit_url(l)) @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], reference_date=promote.promo_datetime_now), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors( 'enddate', errors.BAD_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates PromotedLinkRoadblock.add(sr, sd, ed) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], reference_date=promote.promo_datetime_now), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates PromotedLinkRoadblock.remove(sr, sd, ed) jquery.refresh() @validatedForm( VSponsor('link_id36'), VModhash(), dates=VDateRange(['startdate', 'enddate'], earliest=timedelta(days=g.min_promote_future), latest=timedelta(days=g.max_promote_future), reference_date=promote.promo_datetime_now, business_days=True, sponsor_override=True), link=VLink('link_id36'), bid=VFloat('bid', coerce=False), target=VPromoTarget(), campaign_id36=nop("campaign_id36"), priority=VPriority("priority"), location=VLocation(), ) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, target, priority, location): if not link: return if not target: # run form.has_errors to populate the errors in the response form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED) form.has_errors('collection', errors.COLLECTION_NOEXIST) form.has_errors('targeting', errors.INVALID_TARGET) return start, end = dates or (None, None) if not allowed_location_and_target(location, target): return abort(403, 'forbidden') cpm = PromotionPrices.get_price(target, location) if (form.has_errors('startdate', errors.BAD_DATE, errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE) or form.has_errors('enddate', errors.BAD_DATE, errors.DATE_TOO_EARLY, errors.DATE_TOO_LATE, errors.BAD_DATE_RANGE)): return # check that start is not so late that authorization hold will expire if not c.user_is_sponsor: max_start = promote.get_max_startdate() if start > max_start: c.errors.add( errors.DATE_TOO_LATE, msg_params={'day': max_start.strftime("%m/%d/%Y")}, field='startdate') form.has_errors('startdate', errors.DATE_TOO_LATE) return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return campaign = None if campaign_id36: try: campaign = PromoCampaign._byID36(campaign_id36) except NotFound: pass if campaign and link._id != campaign.link_id: return abort(404, 'not found') if priority.cpm: min_bid = 0 if c.user_is_sponsor else g.min_promote_bid max_bid = None if c.user_is_sponsor else g.max_promote_bid if bid is None or bid < min_bid or (max_bid and bid > max_bid): c.errors.add(errors.BAD_BID, field='bid', msg_params={ 'min': min_bid, 'max': max_bid or g.max_promote_bid }) form.has_errors('bid', errors.BAD_BID) return # you cannot edit the bid of a live ad unless it's a freebie if (campaign and bid != campaign.bid and promote.is_live_promo(link, campaign) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return else: bid = 0. # Set bid to 0 as dummy value is_frontpage = (not target.is_collection and target.subreddit_name == Frontpage.name) if not target.is_collection and not is_frontpage: # targeted to a single subreddit, check roadblock sr = target.subreddits_slow[0] roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = { "start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y') } c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return # Check inventory campaign = campaign if campaign_id36 else None if not priority.inventory_override: oversold = has_oversold_error(form, campaign, start, end, bid, cpm, target, location) if oversold: return if campaign: promote.edit_campaign(link, campaign, dates, bid, cpm, target, priority, location) else: campaign = promote.new_campaign(link, dates, bid, cpm, target, priority, location) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm(VSponsor('link_id36'), VModhash(), l=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if not campaign or not l or l._id != campaign.link_id: return abort(404, 'not found') promote.delete_campaign(l, campaign) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_terminate_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') promote.terminate_campaign(link, campaign) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm(VSponsor('link'), VModhash(), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ]), creditcard=ValidCard( ["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): if not g.authorizenetapi: return if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') # Check inventory if campaign_has_oversold_error(form, campaign): return # check that start is not so late that authorization hold will expire max_start = promote.get_max_startdate() if campaign.start_date > max_start: msg = _("please change campaign start date to %(date)s or earlier") date = format_date(max_start, format="short", locale=c.locale) msg %= {'date': date} form.set_text(".status", msg) return # check the campaign start date is still valid (user may have created # the campaign a few days ago) now = promote.promo_datetime_now() min_start = now + timedelta(days=g.min_promote_future) if campaign.start_date.date() < min_start.date(): msg = _("please change campaign start date to %(date)s or later") date = format_date(min_start, format="short", locale=c.locale) msg %= {'date': date} form.set_text(".status", msg) return address_modified = not pay_id or edit if address_modified: address_fields = [ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ] card_fields = ["cardNumber", "expirationDate", "cardCode"] if (form.has_errors(address_fields, errors.BAD_ADDRESS) or form.has_errors(card_fields, errors.BAD_CARD)): return pay_id = edit_profile(c.user, address, creditcard, pay_id) reason = None if pay_id: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) if success: form.redirect(promote.promo_edit_url(link)) return msg = reason or _("failed to authenticate card. sorry.") form.set_text(".status", msg) @validate(VSponsor("link_name"), VModhash(), link=VByName('link_name'), file=VUploadLength('file', 500 * 1024), img_type=VImageType('img_type')) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if link and (not promote.is_promoted(link) or c.user_is_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".%s" % img_type) if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render()
class PromoteController(ListingController): where = 'promoted' render_cls = PromotePage @property def title_text(self): return _('promoted by you') def keep_fn(self): def keep(item): if item.promoted and not item._deleted: return True else: return False return keep def query(self): if c.user_is_sponsor: if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos": return queries.get_all_live_links() return queries.get_all_promoted_links() else: if self.sort == "future_promos": return queries.get_unapproved_links(c.user._id) elif self.sort == "pending_promos": return queries.get_accepted_links(c.user._id) elif self.sort == "unpaid_promos": return queries.get_unpaid_links(c.user._id) elif self.sort == "rejected_promos": return queries.get_rejected_links(c.user._id) elif self.sort == "live_promos": return queries.get_live_links(c.user._id) return queries.get_promoted_links(c.user._id) @validate(VSponsor()) def GET_listing(self, sort="", **env): if not c.user_is_loggedin or not c.user.email_verified: return self.redirect("/ad_inq") self.sort = sort return ListingController.GET_listing(self, **env) GET_index = GET_listing @validate(VSponsor()) def GET_new_promo(self): return PromotePage('content', content=PromoteLinkForm()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkForm(link=link, listing=rendered, timedeltatext="") page = PromotePage('new_promo', content=form) return page.render() # For development. Should eventually replace GET_edit_promo @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo_cpm(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkFormCpm(link=link, listing=rendered, timedeltatext="") page = PromotePage('new_promo', content=form) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @validate(VSponsor(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_graph(self, dates): start, end, bad_dates = _check_dates(dates) return PromotePage("graph", content=Promote_Graph( start, end, bad_dates=bad_dates) ).render() @validate(VSponsorAdmin(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_admingraph(self, dates): start, end, bad_dates = _check_dates(dates) content = Promote_Graph(start, end, bad_dates=bad_dates, admin_view=True) if c.render_style == 'csv': return content.as_csv() return PromotePage("admingraph", content=content).render() def GET_inventory(self, sr_name): ''' Return available inventory data as json for use in ajax calls ''' inv_start_date = promote.promo_datetime_now() inv_end_date = inv_start_date + timedelta(60) inventory = promote.get_available_impressions( sr_name, inv_start_date, inv_end_date, fuzzed=(not c.user_is_admin) ) dates = [] impressions = [] max_imps = 0 for date, imps in inventory.iteritems(): dates.append(date.strftime("%m/%d/%Y")) impressions.append(imps) max_imps = max(max_imps, imps) return json.dumps({'sr':sr_name, 'dates': dates, 'imps':impressions, 'max_imps':max_imps}) # ## POST controllers below @validatedForm(VSponsorAdmin(), link=VLink("link_id"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after( "<p>" + text + "</p>") @noresponse(VSponsorAdmin(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validatedForm(VSponsor('link_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), set_clicks=VBoolean("set_maximum_clicks"), max_clicks=VInt("maximum_clicks", min=0), set_views=VBoolean("set_maximum_views"), max_views=VInt("maximum_views", min=0), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100) ) def POST_edit_promo(self, form, jquery, ip, l, title, url, disable_comments, set_clicks, max_clicks, set_views, max_views, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: set_clicks = False set_views = False should_ratelimit = True if not set_clicks: max_clicks = None if not set_views: max_views = None if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or form.has_errors('url', errors.NO_URL, errors.BAD_URL) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url, c.user, ip) elif promote.is_promo(l): changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) if c.user_is_sponsor: l.maximum_clicks = max_clicks l.maximum_views = max_views # comment disabling is free to be changed any time. l.disable_comments = disable_comments if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage('content', content=Roadblocks()).render() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates promote.roadblock_reddit(sr.name, sd.date(), ed.date()) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates promote.unroadblock_reddit(sr.name, sd.date(), ed.date()) jquery.refresh() @validatedForm(VSponsor('link_id'), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), l=VLink('link_id'), bid=VFloat('bid', min=0, max=g.max_promote_bid, coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10)) def POST_edit_campaign(self, form, jquery, l, campaign_id36, dates, bid, sr, targeting): if not l: return start, end = dates or (None, None) if (start and end and not promote.is_accepted(l) and not c.user_is_sponsor): # if the ad is not approved already, ensure the start date # is at least 2 days in the future start = start.date() end = end.date() now = promote.promo_datetime_now() future = make_offset_date(now, g.min_promote_future, business_days=True) if start < future.date(): c.errors.add(errors.BAD_FUTURE_DATE, msg_params=dict(day=g.min_promote_future), field="startdate") if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(l._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return duration = max((end - start).days, 1) if form.has_errors('bid', errors.BAD_BID): return # minimum bid depends on user privilege and targeting, checked here # instead of in the validator b/c current duration is needed if c.user_is_sponsor: min_daily_bid = 0 elif targeting == 'one': min_daily_bid = g.min_promote_bid * 1.5 else: min_daily_bid = g.min_promote_bid if campaign_id36: # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) if (bid != campaign.bid and campaign.start_date < datetime.now(g.tz) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return except NotFound: pass if bid is None or bid / duration < min_daily_bid: c.errors.add(errors.BAD_BID, field='bid', msg_params={'min': min_daily_bid, 'max': g.max_promote_bid}) form.has_errors('bid', errors.BAD_BID) return if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return oversold = promote.is_roadblocked(sr.name, start, end) if oversold and not c.user_is_sponsor: msg_params = {"start": oversold[0].strftime('%m/%d/%Y'), "end": oversold[1].strftime('%m/%d/%Y')} c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return if targeting == 'none': sr = None if campaign_id36 is not None: campaign = PromoCampaign._byID36(campaign_id36) promote.edit_campaign(l, campaign, dates, bid, sr) r = promote.get_renderable_campaigns(l, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.sr, r.status) else: campaign = promote.new_campaign(l, dates, bid, sr) r = promote.get_renderable_campaigns(l, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.sr, r.status) @validatedForm(VSponsor('link_id'), VModhash(), l=VLink('link_id'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if l and campaign: promote.delete_campaign(l, campaign) @validatedForm(VSponsor('container'), VModhash(), user=VExistingUname('name'), thing=VByName('container')) def POST_traffic_viewer(self, form, jquery, user, thing): """ Adds a user to the list of users allowed to view a promoted link's traffic page. """ if not form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): form.set_inputs(name="") form.set_html(".status:first", _("added")) if promote.add_traffic_viewer(thing, user): user_row = TrafficViewerList(thing).user_row('traffic', user) jquery("#traffic-table").show( ).find("table").insert_table_rows(user_row) # send the user a message msg = user_added_messages['traffic']['pm']['msg'] subj = user_added_messages['traffic']['pm']['subject'] if msg and subj: d = dict(url=thing.make_permalink_slow(), traffic_url=promote.promo_traffic_url(thing), title=thing.title) msg = msg % d item, inbox_rel = Message._new(c.user, user, subj, msg, request.ip) queries.new_message(item, inbox_rel) @validatedForm(VSponsor('container'), VModhash(), iuser=VByName('id'), thing=VByName('container')) def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): if thing and iuser: promote.rm_traffic_viewer(thing, iuser) @validatedForm(VSponsor('link'), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress( ["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"], allowed_countries=g.allowed_pay_countries), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors(["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"], errors.BAD_ADDRESS) or form.has_errors(["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html(".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if not campaign.link_id == link._id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() def GET_link_thumb(self, *a, **kw): """ See GET_upload_sr_image for rationale """ return "nothing to see here." @validate(VSponsor("link_id"), link=VByName('link_id'), file=VLength('file', 500 * 1024)) def POST_link_thumb(self, link=None, file=None): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".jpg") except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate(VSponsorAdmin(), launchdate=VDate('ondate'), dates=VDateRange(['startdate', 'enddate']), query_type=VOneOf('q', ('started_on', 'between'), default=None)) def GET_admin(self, launchdate=None, dates=None, query_type=None): return PromoAdminTool(query_type=query_type, launchdate=launchdate, start=dates[0], end=dates[1]).render()