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 FreeToPlayApiController(RedditController): @validate(VUser(), VModhash(), item_name=VRequired('item', errors.NO_NAME), target=VByName('target')) def POST_use_item(self, item_name, target): try: inventory.consume_item(c.user, item_name) except inventory.NoSuchItemError: abort(400) c.user.f2p = "participated" c.user._commit() item = items.get_item(item_name) if not item.is_target_valid(target): abort(400) item.on_use(c.user, target) return json.dumps(c.state_changes)
class BetaModeController(RedditController): @validate( VUser(), name=VPrintable('name', 15), ) def GET_beta(self, name): user_exempt = beta_user_exempt(c.user) if name != g.beta_name or (g.beta_require_admin and not user_exempt): abort(404) content = BetaSettings( beta_name=g.beta_name, beta_title=g.beta_title, description_md=g.beta_description_md[0], feedback_sr=g.beta_feedback_sr, enabled=c.beta, require_gold=g.beta_require_gold and not user_exempt, has_gold=c.user_is_loggedin and c.user.gold, ) return BoringPage( pagename=g.beta_title, content_id='beta-settings', content=content, show_sidebar=False, ).render() def GET_disable(self, **kwargs): # **kwargs included above to swallow pylons env arguments passed in # due to argspec inspection of decorator **kwargs. return BoringPage( pagename=_('disabling beta'), content_id='beta-disable', content=BetaDisable(), show_sidebar=False, ).render()
class MultiApiController(RedditController): on_validation_error = staticmethod(abort_with_error) def pre(self): set_extension(request.environ, "json") RedditController.pre(self) @require_oauth2_scope("read") @validate(VUser()) @api_doc(api_section.multis, uri="/api/multi/mine") def GET_my_multis(self): """Fetch a list of multis belonging to the current user.""" multis = LabeledMulti.by_owner(c.user) wrapped = wrap_things(*multis) resp = [w.render() for w in wrapped] return self.api_wrapper(resp) def _format_multi(self, multi): resp = wrap_things(multi)[0].render() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate(multi=VMultiByPath("multipath", require_view=True)) @api_doc( api_section.multis, uri="/api/multi/{multipath}", uri_variants=['/api/filter/{filterpath}'], ) def GET_multi(self, multi): """Fetch a multi's data and subreddit list by name.""" return self._format_multi(multi) def _check_new_multi_path(self, path_info): if path_info['username'].lower() != c.user.name.lower(): raise RedditError('MULTI_CANNOT_EDIT', code=403, fields='multipath') def _add_multi_srs(self, multi, sr_datas): srs = Subreddit._by_name(sr_data['name'] for sr_data in sr_datas) for sr in srs.itervalues(): if isinstance(sr, FakeSubreddit): raise RedditError('MULTI_SPECIAL_SUBREDDIT', msg_params={'path': sr.path}, code=400) sr_props = {} for sr_data in sr_datas: try: sr = srs[sr_data['name']] except KeyError: raise RedditError('SUBREDDIT_NOEXIST', code=400) else: # name is passed in via the API data format, but should not be # stored on the model. del sr_data['name'] sr_props[sr] = sr_data try: multi.add_srs(sr_props) except TooManySubredditsError as e: raise RedditError('MULTI_TOO_MANY_SUBREDDITS', code=409) return sr_props def _write_multi_data(self, multi, data): multi.visibility = data['visibility'] multi.clear_srs() try: self._add_multi_srs(multi, data['subreddits']) except: multi._revert() raise multi._commit() return multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def POST_multi(self, path_info, data): """Create a multi. Responds with 409 Conflict if it already exists.""" self._check_new_multi_path(path_info) try: LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], c.user) response.status = 201 else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), path_info=VMultiPath("multipath"), data=VValidatedJSON("model", multi_json_spec), ) @api_doc(api_section.multis, extends=GET_multi) def PUT_multi(self, path_info, data): """Create or update a multi.""" self._check_new_multi_path(path_info) try: multi = LabeledMulti._byID(path_info['path']) except tdb_cassandra.NotFound: multi = LabeledMulti.create(path_info['path'], c.user) response.status = 201 self._write_multi_data(multi, data) return self._format_multi(multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), ) @api_doc(api_section.multis, extends=GET_multi) def DELETE_multi(self, multi): """Delete a multi.""" multi.delete() def _copy_multi(self, from_multi, to_path_info): self._check_new_multi_path(to_path_info) to_owner = Account._by_name(to_path_info['username']) try: LabeledMulti._byID(to_path_info['path']) except tdb_cassandra.NotFound: to_multi = LabeledMulti.copy(to_path_info['path'], from_multi, owner=to_owner) else: raise RedditError('MULTI_EXISTS', code=409, fields='multipath') return to_multi @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_view=True, kinds='m'), to_path_info=VMultiPath( "to", docs={"to": "destination multireddit url path"}, ), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/copy", ) def POST_multi_copy(self, from_multi, to_path_info): """Copy a multi. Responds with 409 Conflict if the target already exists. A "copied from ..." line will automatically be appended to the description. """ to_multi = self._copy_multi(from_multi, to_path_info) from_path = from_multi.path to_multi.copied_from = from_path if to_multi.description_md: to_multi.description_md += '\n\n' to_multi.description_md += _('copied from %(source)s') % { # force markdown linking since /user/foo is not autolinked 'source': '[%s](%s)' % (from_path, from_path) } to_multi.visibility = 'private' to_multi._commit() return self._format_multi(to_multi) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), from_multi=VMultiByPath("from", require_edit=True, kinds='m'), to_path_info=VMultiPath( "to", docs={"to": "destination multireddit url path"}, ), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/rename", ) def POST_multi_rename(self, from_multi, to_path_info): """Rename a multi.""" to_multi = self._copy_multi(from_multi, to_path_info) from_multi.delete() return self._format_multi(to_multi) def _get_multi_subreddit(self, multi, sr): resp = LabeledMultiJsonTemplate.sr_props(multi, [sr])[0] return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True), sr=VSRByName('srname'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/r/{srname}", uri_variants=['/api/filter/{filterpath}/r/{srname}'], ) def GET_multi_subreddit(self, multi, sr): """Get data about a subreddit in a multi.""" return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr_name=VSubredditName('srname', allow_language_srs=True), data=VValidatedJSON("model", multi_sr_data_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def PUT_multi_subreddit(self, multi, sr_name, data): """Add a subreddit to a multi.""" new = not any(sr.name.lower() == sr_name.lower() for sr in multi.srs) data['name'] = sr_name sr_props = self._add_multi_srs(multi, [data]) sr = sr_props.items()[0][0] multi._commit() if new: response.status = 201 return self._get_multi_subreddit(multi, sr) @require_oauth2_scope("subscribe") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True), sr=VSRByName('srname'), ) @api_doc(api_section.multis, extends=GET_multi_subreddit) def DELETE_multi_subreddit(self, multi, sr): """Remove a subreddit from a multi.""" multi.del_srs(sr) multi._commit() def _format_multi_description(self, multi): resp = LabeledMultiDescriptionJsonTemplate().render(multi).finalize() return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( VUser(), multi=VMultiByPath("multipath", require_view=True, kinds='m'), ) @api_doc( api_section.multis, uri="/api/multi/{multipath}/description", ) def GET_multi_description(self, multi): """Get a multi's description.""" return self._format_multi_description(multi) @require_oauth2_scope("read") @validate( VUser(), VModhash(), multi=VMultiByPath("multipath", require_edit=True, kinds='m'), data=VValidatedJSON('model', multi_description_json_spec), ) @api_doc(api_section.multis, extends=GET_multi_description) def PUT_multi_description(self, multi, data): """Change a multi's markdown description.""" multi.description_md = data['body_md'] multi._commit() return self._format_multi_description(multi)
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 QrCodeController(RedditController): @validate( meetup=validators.VMeetup("codename"), ) def GET_portal(self, meetup): if meetup.state != "closed": if c.user_is_loggedin: content = pages.MeetupPortal(meetup=meetup) else: content = pages.LoggedOutMeetupPortal(meetup=meetup) else: content = pages.ClosedMeetupPortal(meetup=meetup) return pages.MeatspacePage(content=content, page_classes=["meatspace-portal"]).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_configure_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.ConversationStarterSelector(meetup, c.user) return pages.MeatspacePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), topic=validators.VConversationStarter("topic"), ) def GET_badge(self, meetup, topic): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.QrCodeBadge(meetup, c.user, topic) return pages.MeatspaceBadgePage(content=content).render() @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_mobile_badge(self, meetup): if meetup.state not in BADGE_STATES: return redirect_to("/meetup/%s" % str(meetup._id)) content = pages.MobileQrCodeBadge(meetup, c.user) return content.render() @validate( VUser(), meetup=validators.VMeetup("codename"), other=VExistingUname("user"), connected_with=VExistingUname("connected-with"), code=VInt("code"), ) def GET_connect(self, meetup, other, code, connected_with): if meetup.state not in CONNECT_STATES: self.abort404() content = pages.QrCodeForm( meetup=meetup, other=other, code=code, connected_with=connected_with, ) return pages.MeatspacePage(content=content).render() @validatedForm( VUser(), meetup=validators.VMeetup("codename"), other=VExistingUname("username"), code=VInt("code"), ) def POST_connect(self, form, jquery, meetup, other, code): if meetup.state not in CONNECT_STATES: self.abort403() jquery("body .connection-success").hide() if form.has_errors("username", errors.NO_USER, errors.USER_DOESNT_EXIST): return if c.user == other: c.errors.add(errors.MEETUP_NOT_WITH_SELF, field="username") form.set_error(errors.MEETUP_NOT_WITH_SELF, "username") return expected_code = utils.make_secret_code(meetup, other) if code != expected_code: g.log.warning("%r just tried an invalid code on %r", c.user.name, other.name) c.errors.add(errors.MEETUP_INVALID_CODE, field="code") form.set_error(errors.MEETUP_INVALID_CODE, "code") return models.MeetupConnections._connect(meetup, c.user, other) models.MeetupConnectionsByAccount._connect(meetup, c.user, other) g.stats.simple_event("meetup.connection") form.redirect("/meetup/%s/connect?connected-with=%s" % (meetup._id, other.name)) @validate( VUser(), meetup=validators.VMeetup("codename"), ) def GET_connections(self, meetup): all_connections = models.MeetupConnectionsByAccount._connections( meetup, c.user) connections = [a for a in all_connections if not a._deleted] content = pages.QrCodeConnections( meetup=meetup, connections=connections, ) return pages.MeatspacePage(content=content).render() @validate(meetup=validators.VMeetup("codename")) def GET_connect_shortlink(self, meetup, user, code): if meetup.state not in CONNECT_STATES: self.abort404() params = urllib.urlencode({ "user": user, "code": code, }) return redirect_to("/meetup/%s/connect?%s" % (str(meetup._id), params), _code=301)
class APIv1GoldController(OAuth2OnlyController): def _gift_using_creddits(self, recipient, months=1, thing_fullname=None, proxying_for=None): with creddits_lock(c.user): if not c.user.employee and c.user.gold_creddits < months: err = RedditError("INSUFFICIENT_CREDDITS") self.on_validation_error(err) note = None buyer = c.user if c.user.name.lower() in g.live_config["proxy_gilding_accounts"]: note = "proxy-%s" % c.user.name if proxying_for: try: buyer = Account._by_name(proxying_for) except NotFound: pass send_gift( buyer=buyer, recipient=recipient, months=months, days=months * 31, signed=False, giftmessage=None, thing_fullname=thing_fullname, note=note, ) if not c.user.employee: c.user.gold_creddits -= months c.user._commit() @require_oauth2_scope("creddits") @validate( VUser(), target=VByName("fullname"), ) @api_doc( api_section.gold, uri="/api/v1/gold/gild/{fullname}", ) def POST_gild(self, target): if not isinstance(target, (Comment, Link)): err = RedditError("NO_THING_ID") self.on_validation_error(err) if target.subreddit_slow.quarantine: err = RedditError("GILDING_NOT_ALLOWED") self.on_validation_error(err) self._gift_using_creddits( recipient=target.author_slow, thing_fullname=target._fullname, proxying_for=request.POST.get("proxying_for"), ) @require_oauth2_scope("creddits") @validate( VUser(), user=VAccountByName("username"), months=VInt("months", min=1, max=36), ) @api_doc( api_section.gold, uri="/api/v1/gold/give/{username}", ) def POST_give(self, user, months): self._gift_using_creddits( recipient=user, months=months, proxying_for=request.POST.get("proxying_for"), )
class DonateController(RedditController): def GET_closed(self): return pages.DonatePage( title=_("reddit donate"), content=pages.DonateClosed(), ).render() @validate( eligible=VAccountEligible(), organization=VOrganization("organization"), ) def GET_landing(self, eligible, organization): if not feature.is_enabled('reddit_donate'): return self.abort404() if c.user_is_loggedin: nomination_count = DonationNominationsByAccount.count(c.user) else: nomination_count = None if organization: wrapped_organization = inject_nomination_status([organization]) else: wrapped_organization = None content = pages.DonateLanding(eligible=eligible, ) og_data = { "site_name": "reddit.com", } if organization: og_data[ "title"] = "reddit donate: vote for %s" % organization.data[ "DisplayName"] og_data["url"] = join_urls( g.origin, "donate?organization=%s" % organization.data["EIN"]) else: og_data["title"] = "reddit donate: giving 10% back" og_data["url"] = join_urls(g.origin, "donate") return pages.DonatePage( title=_("reddit donate"), content=content, og_data=og_data, extra_js_config={ "unloadedNominations": nomination_count, "accountIsEligible": eligible, "organization": wrapped_organization, }, ).render() @validatedForm( VUser(), VModhash(), VRatelimit(rate_user=True, prefix="donate_nominate_"), VAccountEligible(), organization=VOrganization("organization"), ) def POST_nominate(self, form, jquery, organization): if not feature.is_enabled('reddit_donate'): return self.abort404() if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION): return if form.has_errors("eligible", errors.DONATE_ACCOUNT_NOT_ELIGIBLE): return if form.has_errors("ratelimit", errors.RATELIMIT): return else: VRatelimit.ratelimit( rate_user=True, prefix="donate_nominate_", seconds=NOMINATION_COOLDOWN, ) DonationNominationsByAccount.nominate( c.user, organization, ) @validatedForm( VUser(), VModhash(), organization=VOrganization("organization"), ) def POST_unnominate(self, form, jquery, organization): if not feature.is_enabled('reddit_donate'): return self.abort404() if form.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION): return DonationNominationsByAccount.unnominate( c.user, organization, ) @json_validate( organization=VOrganization("organization"), ) def GET_organization(self, responder, organization): """Look up a single org by EIN.""" if not feature.is_enabled('reddit_donate'): return self.abort404() if responder.has_errors("organization", errors.DONATE_UNKNOWN_ORGANIZATION): return wrapped = inject_nomination_status([organization]) return wrapped[0] @json_validate( prefix=VLength("prefix", min_length=3, max_length=100), ) def GET_search(self, responder, prefix): """Get organizations by display-name prefix.""" if not feature.is_enabled('reddit_donate'): return self.abort404() if responder.has_errors("prefix", errors.TOO_LONG, errors.TOO_SHORT): return organizations = DonationOrganizationsByPrefix.byPrefix(prefix) return inject_nomination_status(organizations) @json_validate( VUser(), ) def GET_nominations(self, responder): if not feature.is_enabled('reddit_donate'): return self.abort404() nominated_org_ids = DonationNominationsByAccount.get_for(c.user) orgs = DonationOrganization.byEIN(nominated_org_ids) wrapped = inject_nomination_status(orgs, assume_nominated=True) return wrapped
class StripeController(GoldPaymentController): name = 'stripe' webhook_secret = g.STRIPE_WEBHOOK_SECRET event_type_mappings = { 'charge.succeeded': 'succeeded', 'charge.failed': 'failed', 'charge.refunded': 'refunded', 'customer.created': 'noop', 'transfer.created': 'noop', 'transfer.paid': 'noop', } @classmethod def process_response(cls): event_dict = json.loads(request.body) event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY) status = event.type if cls.event_type_mappings.get(status) == 'noop': return status, None, None, None, None charge = event.data.object description = charge.description try: passthrough, buyer_name = description.split('-', 1) except ValueError: g.log.error('stripe_error on charge: %s', charge) raise transaction_id = 'S%s' % charge.id pennies = charge.amount months, days = months_and_days_from_pennies(pennies) return status, passthrough, transaction_id, pennies, months @validatedForm(VUser(), token=nop('stripeToken'), passthrough=VPrintable("passthrough", max_length=50), pennies=VInt('pennies'), months=VInt("months")) def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months): """ Submit charge to stripe. Called by GoldPayment form. This submits the charge to stripe, and gold will be applied once we receive a webhook from stripe. """ try: payment_blob = validate_blob(passthrough) except GoldException as e: # This should never happen. All fields in the payment_blob # are validated on creation form.set_html('.status', _('something bad happened, try again later')) g.log.debug('POST_goldcharge: %s' % e.message) return penny_months, days = months_and_days_from_pennies(pennies) if not months or months != penny_months: form.set_html('.status', _('stop trying to trick the form')) return stripe.api_key = g.STRIPE_SECRET_KEY try: customer = stripe.Customer.create(card=token) if (customer['active_card']['address_line1_check'] == 'fail' or customer['active_card']['address_zip_check'] == 'fail'): form.set_html('.status', _('error: address verification failed')) form.find('.stripe-submit').removeAttr('disabled').end() return if customer['active_card']['cvc_check'] == 'fail': form.set_html('.status', _('error: cvc check failed')) form.find('.stripe-submit').removeAttr('disabled').end() return charge = stripe.Charge.create( amount=pennies, currency="usd", customer=customer['id'], description='%s-%s' % (passthrough, c.user.name) ) except stripe.CardError as e: form.set_html('.status', 'error: %s' % e.message) form.find('.stripe-submit').removeAttr('disabled').end() except stripe.InvalidRequestError as e: form.set_html('.status', _('invalid request')) except stripe.APIConnectionError as e: form.set_html('.status', _('api error')) except stripe.AuthenticationError as e: form.set_html('.status', _('connection error')) except stripe.StripeError as e: form.set_html('.status', _('error')) g.log.error('stripe error: %s' % e) else: form.set_html('.status', _('payment submitted')) # webhook usually sends near instantly, send a message in case subject = _('gold payment') msg = _('your payment is being processed and gold will be' ' delivered shortly') send_system_message(c.user, subject, msg)
class OAuth2FrontendController(RedditController): def check_for_bearer_token(self): pass def pre(self): RedditController.pre(self) require_https() def _abort_oauth_error(self, error): g.stats.simple_event('oauth2.errors.%s' % error) abort(BadRequestError(error)) def _check_redirect_uri(self, client, redirect_uri): if (errors.OAUTH2_INVALID_CLIENT, 'client_id') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_CLIENT) if not redirect_uri or redirect_uri != client.redirect_uri: self._abort_oauth_error(errors.OAUTH2_INVALID_REDIRECT_URI) def _check_response_type_and_scope(self, response_type, scope): if (errors.INVALID_OPTION, 'response_type') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_RESPONSE_TYPE) if (errors.OAUTH2_INVALID_SCOPE, 'scope') in c.errors: self._abort_oauth_error(errors.OAUTH2_INVALID_SCOPE) def _check_client_type_and_duration(self, response_type, client, duration): if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner self._abort_oauth_error(errors.OAUTH2_CONFIDENTIAL_TOKEN) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed self._abort_oauth_error(errors.OAUTH2_NO_REFRESH_TOKENS_ALLOWED) def _error_response(self, state, redirect_uri, as_fragment=False): """Return an error redirect.""" resp = {"state": state} if (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors: resp["error"] = "access_denied" elif (errors.INVALID_MODHASH, None) in c.errors: resp["error"] = "access_denied" else: resp["error"] = "invalid_request" final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment) return self.redirect(final_redirect, code=302) def _check_employee_grants(self, client, scope): if not c.user.employee or not client or not scope: return if client._id in g.employee_approved_clients: return if client._id in g.mobile_auth_allowed_clients: return # The identity scope doesn't leak much, and we don't mind if employees # prove their identity to some external service if scope.scopes == {"identity"}: return error_page = RedditError( title=_('this app has not been approved for use with employee accounts'), message="", ) request.environ["usable_error_content"] = error_page.render() self.abort403() @validate(VUser(), response_type = VOneOf("response_type", ("code", "token")), client = VOAuth2ClientID(), redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope = VOAuth2Scope(), state = VRequired("state", errors.NO_TEXT), duration = VOneOf("duration", ("temporary", "permanent"), default="temporary")) def GET_authorize(self, response_type, client, redirect_uri, scope, state, duration): """ First step in [OAuth 2.0](http://oauth.net/2/) authentication. End users will be prompted for their credentials (username/password) and asked if they wish to authorize the application identified by the **client_id** parameter with the permissions specified by the **scope** parameter. They are then redirected to the endpoint on the client application's side specified by **redirect_uri**. If the user granted permission to the application, the response will contain a **code** parameter with a temporary authorization code which can be exchanged for an access token at [/api/v1/access_token](#api_method_access_token). **redirect_uri** must match the URI configured for the client in the [app preferences](/prefs/apps). All errors will show a 400 error page along with some information on what option was wrong. """ self._check_employee_grants(client, scope) # Check redirect URI first; it will ensure client exists self._check_redirect_uri(client, redirect_uri) self._check_response_type_and_scope(response_type, scope) self._check_client_type_and_duration(response_type, client, duration) if not c.errors: return OAuth2AuthorizationPage(client, redirect_uri, scope, state, duration, response_type).render() else: self._abort_oauth_error(errors.INVALID_OPTION) @validate(VUser(), VModhash(fatal=False), client = VOAuth2ClientID(), redirect_uri = VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope = VOAuth2Scope(), state = VRequired("state", errors.NO_TEXT), duration = VOneOf("duration", ("temporary", "permanent"), default="temporary"), authorize = VRequired("authorize", errors.OAUTH2_ACCESS_DENIED), response_type = VOneOf("response_type", ("code", "token"), default="code")) def POST_authorize(self, authorize, client, redirect_uri, scope, state, duration, response_type): """Endpoint for OAuth2 authorization.""" self._check_employee_grants(client, scope) self._check_redirect_uri(client, redirect_uri) self._check_response_type_and_scope(response_type, scope) self._check_client_type_and_duration(response_type, client, duration) if c.errors: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) if response_type == "code": code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id36, scope, duration == "permanent") resp = {"code": code._id, "state": state} final_redirect = _update_redirect_uri(redirect_uri, resp) g.stats.simple_event('oauth2.POST_authorize.authorization_code_create') elif response_type == "token": device_id = get_device_id(client) token = OAuth2AccessToken._new( client_id=client._id, user_id=c.user._id36, scope=scope, device_id=device_id, ) resp = OAuth2AccessController._make_new_token_response(token) resp["state"] = state final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment=True) g.stats.simple_event('oauth2.POST_authorize.access_token_create') # If this is the first time the user is logging in with an official # mobile app, gild them if (g.live_config.get('mobile_gild_first_login') and not c.user.has_used_mobile_app and client._id in g.mobile_auth_gild_clients): buyer = Account.system_user() admintools.adjust_gold_expiration( c.user, days=g.mobile_auth_gild_time) create_gift_gold( buyer._id, c.user._id, g.mobile_auth_gild_time, datetime.now(g.tz), signed=True, note='first_mobile_auth') subject = 'Let there be gold! Reddit just sent you Reddit gold!' message = ( "Thank you for using the Reddit mobile app! As a thank you " "for logging in during launch week, you've been gifted %s of " "Reddit Gold.\n\n" "Reddit Gold is Reddit's premium membership program, which " "grants you: \n" "An ads-free experience in Reddit's mobile apps, and\n" "Extra site features on desktop\n\n" "Discuss and get help on the features and perks at " "r/goldbenefits." ) % g.mobile_auth_gild_message message += '\n\n' + strings.gold_benefits_msg send_system_message(c.user, subject, message, add_to_sent=False) c.user.has_used_mobile_app = True c.user._commit() return self.redirect(final_redirect, code=302)
class LiveUpdateEventsController(RedditController): def GET_home(self): return pages.LiveUpdateMetaPage( title=_("reddit live"), content=pages.LiveUpdateHome(), page_classes=["liveupdate-home"], ).render() @validate( VEmployee(), num=VLimit("limit", default=25, max_limit=100), after=VLiveUpdateEvent("after"), before=VLiveUpdateEvent("before"), count=VCount("count"), ) def GET_listing(self, filter, num, after, before, count): reverse = False if before: after = before reverse = True builder_cls = LiveUpdateEventBuilder wrapper = Wrapped listing_cls = Listing if filter == "open": title = _("live threads") query = queries.get_live_events("new", "all") elif filter == "closed": title = _("closed threads") query = queries.get_complete_events("new", "all") elif filter == "active": title = _("most active threads") query = queries.get_active_events() elif filter == "reported": if not c.user_is_admin: self.abort403() title = _("reported threads") query = queries.get_reported_events() builder_cls = LiveUpdateReportedEventBuilder wrapper = pages.LiveUpdateReportedEventRow listing_cls = pages.LiveUpdateReportedEventListing else: self.abort404() builder = builder_cls( query, num=num, after=after, reverse=reverse, count=count, wrap=wrapper, skip=True, ) listing = listing_cls(builder) return pages.LiveUpdateMetaPage( title=title, content=listing.listing(), ).render() @validate( VUser(), ) def GET_create(self): return pages.LiveUpdateMetaPage( title=_("create live thread"), content=pages.LiveUpdateCreate(), ).render() @require_oauth2_scope("submit") @validatedForm( VUser(), VModhash(), VRatelimit(rate_user=True, prefix="liveupdate_create_"), **EVENT_CONFIGURATION_VALIDATORS ) @api_doc( section=api_section.live, uri="/api/live/create", ) def POST_create(self, form, jquery, title, description, resources, nsfw): """Create a new live thread. Once created, the initial settings can be modified with [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates can be posted with [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if not is_event_configuration_valid(form): return # for simplicity, set the live-thread creation threshold at the # subreddit creation threshold if not c.user_is_admin and not c.user.can_create_subreddit: form.set_error(errors.CANT_CREATE_SR, "") c.errors.add(errors.CANT_CREATE_SR, field="") return if form.has_errors("ratelimit", errors.RATELIMIT): return VRatelimit.ratelimit( rate_user=True, prefix="liveupdate_create_", seconds=60) event = LiveUpdateEvent.new( id=None, title=title, description=description, resources=resources, banned=c.user._spam, nsfw=nsfw, ) event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER) queries.create_event(event) form.redirect("/live/" + event._id) form._send_data(id=event._id) liveupdate_events.create_event(event, context=c, request=request)
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 IpnController(RedditController): # Used when buying gold with creddits @validatedForm(VUser(), months=VInt("months"), passthrough=VPrintable("passthrough", max_length=50)) def POST_spendcreddits(self, form, jquery, months, passthrough): if months is None or months < 1: form.set_html(".status", _("nice try.")) return days = months * 31 if not passthrough: raise ValueError("/spendcreddits got no passthrough?") blob_key, payment_blob = get_blob(passthrough) if payment_blob["goldtype"] != "gift": raise ValueError("/spendcreddits payment_blob %s has goldtype %s" % (passthrough, payment_blob["goldtype"])) signed = payment_blob["signed"] giftmessage = _force_unicode(payment_blob["giftmessage"]) recipient_name = payment_blob["recipient"] if payment_blob["account_id"] != c.user._id: fmt = ("/spendcreddits payment_blob %s has userid %d " + "but c.user._id is %d") raise ValueError(fmt % passthrough, payment_blob["account_id"], c.user._id) try: recipient = Account._by_name(recipient_name) except NotFound: raise ValueError( "Invalid username %s in spendcreddits, buyer = %s" % (recipient_name, c.user.name)) if recipient._deleted: form.set_html(".status", _("that user has deleted their account")) return if not c.user.employee: if months > c.user.gold_creddits: raise ValueError( "%s is trying to sneak around the creddit check" % c.user.name) c.user.gold_creddits -= months c.user.gold_creddit_escrow += months c.user._commit() comment_id = payment_blob.get("comment") comment = send_gift(c.user, recipient, months, days, signed, giftmessage, comment_id) if not c.user.employee: c.user.gold_creddit_escrow -= months c.user._commit() payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30) form.set_html(".status", _("the gold has been delivered!")) form.find("button").hide() if comment: gilding_message = make_comment_gold_message(comment, user_gilded=True) jquery.gild_comment(comment_id, gilding_message, comment.gildings) @textresponse(paypal_secret=VPrintable('secret', 50), payment_status=VPrintable('payment_status', 20), txn_id=VPrintable('txn_id', 20), paying_id=VPrintable('payer_id', 50), payer_email=VPrintable('payer_email', 250), mc_currency=VPrintable('mc_currency', 20), mc_gross=VFloat('mc_gross'), custom=VPrintable('custom', 50)) def POST_ipn(self, paypal_secret, payment_status, txn_id, paying_id, payer_email, mc_currency, mc_gross, custom): parameters = request.POST.copy() # Make sure it's really PayPal if paypal_secret != g.PAYPAL_SECRET: log_text("invalid IPN secret", "%s guessed the wrong IPN secret" % request.ip, "warning") raise ValueError # Return early if it's an IPN class we don't care about response, psl = check_payment_status(payment_status) if response: return response # Return early if it's a txn_type we don't care about response, subscription = check_txn_type(parameters['txn_type'], psl) if subscription is None: subscr_id = None elif subscription == "new": subscr_id = parameters['subscr_id'] elif subscription == "cancel": cancel_subscription(parameters['subscr_id']) else: raise ValueError("Weird subscription: %r" % subscription) if response: return response # Check for the debug flag, and if so, dump the IPN dict if g.cache.get("ipn-debug"): g.cache.delete("ipn-debug") dump_parameters(parameters) if mc_currency != 'USD': raise ValueError("Somehow got non-USD IPN %r" % mc_currency) if not (txn_id and paying_id and payer_email and mc_gross): dump_parameters(parameters) raise ValueError("Got incomplete IPN") pennies = int(mc_gross * 100) months, days = months_and_days_from_pennies(pennies) # Special case: autorenewal payment existing = existing_subscription(subscr_id, paying_id, custom) if existing: if existing != "deleted account": create_claimed_gold("P" + txn_id, payer_email, paying_id, pennies, days, None, existing._id, c.start_time, subscr_id) admintools.engolden(existing, days) g.log.info("Just applied IPN renewal for %s, %d days" % (existing.name, days)) return "Ok" # More sanity checks that all non-autorenewals should pass: if not custom: dump_parameters(parameters) raise ValueError("Got IPN with txn_id=%s and no custom" % txn_id) self.finish(parameters, "P" + txn_id, payer_email, paying_id, subscr_id, custom, pennies, months, days) def finish(self, parameters, txn_id, payer_email, paying_id, subscr_id, custom, pennies, months, days): blob_key, payment_blob = get_blob(custom) buyer_id = payment_blob.get('account_id', None) if not buyer_id: dump_parameters(parameters) raise ValueError("No buyer_id in IPN with custom='%s'" % custom) try: buyer = Account._byID(buyer_id) except NotFound: dump_parameters(parameters) raise ValueError("Invalid buyer_id %d in IPN with custom='%s'" % (buyer_id, custom)) if subscr_id: buyer.gold_subscr_id = subscr_id instagift = False if payment_blob['goldtype'] in ('autorenew', 'onetime'): admintools.engolden(buyer, days) subject = _("Eureka! Thank you for investing in reddit gold!") message = _("Thank you for buying reddit gold. Your patronage " "supports the site and makes future development " "possible. For example, one month of reddit gold " "pays for 5 instance hours of reddit's servers.") message += "\n\n" + strings.gold_benefits_msg if g.lounge_reddit: message += "\n* " + strings.lounge_msg elif payment_blob['goldtype'] == 'creddits': buyer._incr("gold_creddits", months) buyer._commit() subject = _("Eureka! Thank you for investing in reddit gold " "creddits!") message = _("Thank you for buying creddits. Your patronage " "supports the site and makes future development " "possible. To spend your creddits and spread reddit " "gold, visit [/gold](/gold) or your favorite " "person's user page.") message += "\n\n" + strings.gold_benefits_msg + "\n\n" message += _("Thank you again for your support, and have fun " "spreading gold!") elif payment_blob['goldtype'] == 'gift': recipient_name = payment_blob.get('recipient', None) try: recipient = Account._by_name(recipient_name) except NotFound: dump_parameters(parameters) raise ValueError( "Invalid recipient_name %s in IPN/GC with custom='%s'" % (recipient_name, custom)) signed = payment_blob.get("signed", False) giftmessage = _force_unicode(payment_blob.get("giftmessage", "")) comment_id = payment_blob.get("comment") send_gift(buyer, recipient, months, days, signed, giftmessage, comment_id) instagift = True subject = _("Thanks for giving the gift of reddit gold!") message = _("Your classy gift to %s has been delivered.\n\n" "Thank you for gifting reddit gold. Your patronage " "supports the site and makes future development " "possible.") % recipient.name message += "\n\n" + strings.gold_benefits_msg + "\n\n" message += _("Thank you again for your support, and have fun " "spreading gold!") else: dump_parameters(parameters) raise ValueError("Got status '%s' in IPN/GC" % payment_blob['status']) # Reuse the old "secret" column as a place to record the goldtype # and "custom", just in case we need to debug it later or something secret = payment_blob['goldtype'] + "-" + custom if instagift: status = "instagift" else: status = "processed" create_claimed_gold(txn_id, payer_email, paying_id, pennies, days, secret, buyer_id, c.start_time, subscr_id, status=status) message = append_random_bottlecap_phrase(message) send_system_message(buyer, subject, message, distinguished='gold-auto') payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30)
class ButtonApiController(ApiController): @validate( VUser(), VModhash(), seconds_remaining=VInt('seconds', min=0, max=60), previous_seconds=VInt('prev_seconds'), tick_time=nop('tick_time'), tick_mac=nop('tick_mac'), ) def POST_press_button(self, seconds_remaining, previous_seconds, tick_time, tick_mac): if not g.live_config['thebutton_is_active']: return if c.user._date > ACCOUNT_CREATION_CUTOFF: return user_has_pressed = ButtonPressByUser.has_pressed(c.user) if user_has_pressed and not c.user.employee: return if has_timer_expired(): # time has expired: no longer possible to press the button return has_started = has_timer_started() if not has_started: # the timer can only be started through reddit-shell return cheater = False if (seconds_remaining is None or previous_seconds is None or tick_time is None or tick_mac is None): # incomplete info from client, just let them press it anyways seconds_remaining = max(0, int(get_seconds_left())) elif not check_tick_mac(previous_seconds, tick_time, tick_mac): # can't trust the values sent by the client seconds_remaining = max(0, int(get_seconds_left())) cheater = True else: # client sent a valid mac so we can trust: # previous_seconds - the timer value at the last tick # tick_time - the datetime at the last tick # check to make sure tick_time wasn't too long ago then = str_to_datetime(tick_time) now = datetime.now(g.tz) if then and (now - then).total_seconds() > 60: # client sent an old (but potentially valid) mac, etc. seconds_remaining = max(0, int(get_seconds_left())) cheater = True # GOTCHA: the client actually sends the same value for # previous_seconds and seconds_remaining so make sure those match. # If the client sent down its own ticking down timer as # seconds_remaining we would want to compare to previous_seconds to # make sure they weren't too far apart if previous_seconds != seconds_remaining: seconds_remaining = max(0, int(get_seconds_left())) cheater = True press_button(c.user) g.stats.simple_event("thebutton.press") if cheater: g.stats.simple_event("thebutton.cheater") # don't flair on first press (the starter) if not has_started: return if user_has_pressed: # don't flair on multiple employee presses return if cheater: flair_css = "cheater" elif seconds_remaining > 51: flair_css = "press-6" elif seconds_remaining > 41: flair_css = "press-5" elif seconds_remaining > 31: flair_css = "press-4" elif seconds_remaining > 21: flair_css = "press-3" elif seconds_remaining > 11: flair_css = "press-2" else: flair_css = "press-1" flair_text = "%ss" % seconds_remaining setattr(c.user, 'flair_%s_text' % g.live_config["thebutton_srid"], flair_text) setattr(c.user, 'flair_%s_css_class' % g.live_config["thebutton_srid"], flair_css) c.user._commit()
class APIv1GoldController(OAuth2ResourceController): handles_csrf = True def pre(self): OAuth2ResourceController.pre(self) if request.method != "OPTIONS": self.authenticate_with_token() self.set_up_user_context() self.run_sitewide_ratelimits() def try_pagecache(self): pass @staticmethod def on_validation_error(error): abort_with_error(error, error.code or 400) def _gift_using_creddits(self, recipient, months=1, thing_fullname=None, proxying_for=None): with creddits_lock(c.user): if not c.user.employee and c.user.gold_creddits < months: err = RedditError("INSUFFICIENT_CREDDITS") self.on_validation_error(err) note = None buyer = c.user if c.user.name.lower() in g.live_config["proxy_gilding_accounts"]: note = "proxy-%s" % c.user.name if proxying_for: try: buyer = Account._by_name(proxying_for) except NotFound: pass send_gift( buyer=buyer, recipient=recipient, months=months, days=months * 31, signed=False, giftmessage=None, thing_fullname=thing_fullname, note=note, ) if not c.user.employee: c.user.gold_creddits -= months c.user._commit() @require_oauth2_scope("creddits") @validate( VUser(), target=VByName("fullname"), ) @api_doc( api_section.gold, uri="/api/v1/gold/gild/{fullname}", ) def POST_gild(self, target): if not isinstance(target, (Comment, Link)): err = RedditError("NO_THING_ID") self.on_validation_error(err) self._gift_using_creddits( recipient=target.author_slow, thing_fullname=target._fullname, proxying_for=request.POST.get("proxying_for"), ) @require_oauth2_scope("creddits") @validate( VUser(), user=VAccountByName("username"), months=VInt("months", min=1, max=36), ) @api_doc( api_section.gold, uri="/api/v1/gold/give/{username}", ) def POST_give(self, user, months): self._gift_using_creddits( recipient=user, months=months, proxying_for=request.POST.get("proxying_for"), )
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 APIv1UserController(OAuth2OnlyController): @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc(api_section.account) def GET_me(self): """Returns the identity of the user currently authenticated via OAuth.""" resp = IdentityJsonTemplate().data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("identity") @validate( VUser(), fields=VList( "fields", choices=PREFS_JSON_SPEC.spec.keys(), error=errors.errors.NON_PREFERENCE, ), ) @api_doc(api_section.account, uri='/api/v1/me/prefs') def GET_prefs(self, fields): """Return the preference settings of the logged in user""" resp = PrefsJsonTemplate(fields).data(c.oauth_user) return self.api_wrapper(resp) @require_oauth2_scope("read") @validate( user=VAccountByName('username'), ) @api_doc( section=api_section.users, uri='/api/v1/user/{username}/trophies', ) def GET_usertrophies(self, user): """Return a list of trophies for the a given user.""" return self.api_wrapper(get_usertrophies(user)) @require_oauth2_scope("identity") @validate( VUser(), ) @api_doc( section=api_section.account, uri='/api/v1/me/trophies', ) def GET_trophies(self): """Return a list of trophies for the current user.""" return self.api_wrapper(get_usertrophies(c.oauth_user)) @require_oauth2_scope("mysubreddits") @validate( VUser(), ) @api_doc( section=api_section.account, uri='/api/v1/me/karma', ) def GET_karma(self): """Return a breakdown of subreddit karma.""" karmas = c.oauth_user.all_karmas(include_old=False) resp = KarmaListJsonTemplate().render(karmas) return self.api_wrapper(resp.finalize()) PREFS_JSON_VALIDATOR = VValidatedJSON("json", PREFS_JSON_SPEC, body=True) @require_oauth2_scope("account") @validate( VUser(), validated_prefs=PREFS_JSON_VALIDATOR, ) @api_doc(api_section.account, json_model=PREFS_JSON_VALIDATOR, uri='/api/v1/me/prefs') def PATCH_prefs(self, validated_prefs): user_prefs = c.user.preferences() for short_name, new_value in validated_prefs.iteritems(): pref_name = "pref_" + short_name user_prefs[pref_name] = new_value vprefs.filter_prefs(user_prefs, c.user) vprefs.set_prefs(c.user, user_prefs) c.user._commit() return self.api_wrapper(PrefsJsonTemplate().data(c.user)) FRIEND_JSON_SPEC = VValidatedJSON.PartialObject({ "name": VAccountByName("name"), "note": VLength("note", 300), }) FRIEND_JSON_VALIDATOR = VValidatedJSON("json", spec=FRIEND_JSON_SPEC, body=True) @require_oauth2_scope('subscribe') @validate( VUser(), friend=VAccountByName('username'), notes_json=FRIEND_JSON_VALIDATOR, ) @api_doc(api_section.users, json_model=FRIEND_JSON_VALIDATOR, uri='/api/v1/me/friends/{username}') def PUT_friends(self, friend, notes_json): """Create or update a "friend" relationship. This operation is idempotent. It can be used to add a new friend, or update an existing friend (e.g., add/change the note on that friend) """ err = None if 'name' in notes_json and notes_json['name'] != friend: # The 'name' in the JSON is optional, but if present, must # match the username from the URL err = errors.RedditError('BAD_USERNAME', fields='name') if 'note' in notes_json and not c.user.gold: err = errors.RedditError('GOLD_REQUIRED', fields='note') if err: self.on_validation_error(err) # See if the target is already an existing friend. # If not, create the friend relationship. friend_rel = Account.get_friend(c.user, friend) rel_exists = bool(friend_rel) if not friend_rel: friend_rel = c.user.add_friend(friend) response.status = 201 if 'note' in notes_json: note = notes_json['note'] or '' if not rel_exists: # If this is a newly created friend relationship, # the cache needs to be updated before a note can # be applied c.user.friend_rels_cache(_update=True) c.user.add_friend_note(friend, note) rel_view = FriendTableItem(friend_rel) return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view)) @require_oauth2_scope('mysubreddits') @validate( VUser(), friend_rel=VFriendOfMine('username'), ) @api_doc(api_section.users, uri='/api/v1/me/friends/{username}') def GET_friends(self, friend_rel): """Get information about a specific 'friend', such as notes.""" rel_view = FriendTableItem(friend_rel) return self.api_wrapper(FriendTableItemJsonTemplate().data(rel_view)) @require_oauth2_scope('subscribe') @validate( VUser(), friend_rel=VFriendOfMine('username'), ) @api_doc(api_section.users, uri='/api/v1/me/friends/{username}') def DELETE_friends(self, friend_rel): """Stop being friends with a user.""" c.user.remove_friend(friend_rel._thing2) if c.user.gold: c.user.friend_rels_cache(_update=True) response.status = 204
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 StripeController(GoldPaymentController): name = 'stripe' webhook_secret = g.STRIPE_WEBHOOK_SECRET event_type_mappings = { 'charge.succeeded': 'succeeded', 'charge.failed': 'failed', 'charge.refunded': 'refunded', 'charge.dispute.created': 'noop', 'charge.dispute.updated': 'noop', 'charge.dispute.closed': 'noop', 'customer.created': 'noop', 'customer.card.created': 'noop', 'customer.card.deleted': 'noop', 'transfer.created': 'noop', 'transfer.paid': 'noop', 'balance.available': 'noop', 'invoice.created': 'noop', 'invoice.updated': 'noop', 'invoice.payment_succeeded': 'noop', 'invoice.payment_failed': 'failed_subscription', 'invoiceitem.deleted': 'noop', 'customer.subscription.created': 'noop', 'customer.deleted': 'noop', 'customer.updated': 'noop', 'customer.subscription.deleted': 'noop', 'customer.subscription.trial_will_end': 'noop', 'customer.subscription.updated': 'noop', 'dummy': 'noop', } @classmethod def process_response(cls): event_dict = json.loads(request.body) event = stripe.Event.construct_from(event_dict, g.STRIPE_SECRET_KEY) status = event.type if status == 'invoice.created': # sent 1 hr before a subscription is charged or immediately for # a new subscription invoice = event.data.object customer_id = invoice.customer account = account_from_stripe_customer_id(customer_id) # if the charge hasn't been attempted (meaning this is 1 hr before # the charge) check that the account can receive the gold if (not invoice.attempted and (not account or (account and account._banned))): # there's no associated account - delete the subscription # to cancel the charge g.log.error('no account for stripe invoice: %s', invoice) try: customer = stripe.Customer.retrieve(customer_id) customer.delete() except stripe.InvalidRequestError: pass elif status == 'invoice.payment_failed': invoice = event.data.object customer_id = invoice.customer buyer = account_from_stripe_customer_id(customer_id) webhook = Webhook(subscr_id=customer_id, buyer=buyer) return status, webhook event_type = cls.event_type_mappings.get(status) if not event_type: raise ValueError('Stripe: unrecognized status %s' % status) elif event_type == 'noop': return status, None charge = event.data.object description = charge.description invoice_id = charge.invoice transaction_id = 'S%s' % charge.id pennies = charge.amount months, days = months_and_days_from_pennies(pennies) if status == 'charge.failed' and invoice_id: # we'll get an additional failure notification event of # "invoice.payment_failed", don't double notify return 'dummy', None elif status == 'charge.failed' and not description: # create_customer can POST successfully but fail to create a # customer because the card is declined. This will trigger a # 'charge.failed' notification but without description so we can't # do anything with it return 'dummy', None elif invoice_id: # subscription charge - special handling customer_id = charge.customer buyer = account_from_stripe_customer_id(customer_id) if not buyer: charge_date = datetime.fromtimestamp(charge.created, tz=g.tz) # don't raise exception if charge date is within the past hour # db replication lag may cause the account lookup to fail if charge_date < timeago('1 hour'): raise ValueError('no buyer for charge: %s' % charge.id) else: abort(404, "not found") webhook = Webhook(transaction_id=transaction_id, subscr_id=customer_id, pennies=pennies, months=months, goldtype='autorenew', buyer=buyer) return status, webhook else: try: passthrough, buyer_name = description.split('-', 1) except (AttributeError, ValueError): g.log.error('stripe_error on charge: %s', charge) raise webhook = Webhook(passthrough=passthrough, transaction_id=transaction_id, pennies=pennies, months=months) return status, webhook @classmethod @handle_stripe_error def create_customer(cls, form, token): description = c.user.name customer = stripe.Customer.create(card=token, description=description) if (customer['active_card']['address_line1_check'] == 'fail' or customer['active_card']['address_zip_check'] == 'fail'): form.set_html('.status', _('error: address verification failed')) form.find('.stripe-submit').removeAttr('disabled').end() return None elif customer['active_card']['cvc_check'] == 'fail': form.set_html('.status', _('error: cvc check failed')) form.find('.stripe-submit').removeAttr('disabled').end() return None else: return customer @classmethod @handle_stripe_error def charge_customer(cls, form, customer, pennies, passthrough): charge = stripe.Charge.create(amount=pennies, currency="usd", customer=customer['id'], description='%s-%s' % (passthrough, c.user.name)) return charge @classmethod @handle_stripe_error def set_creditcard(cls, form, user, token): if not user.has_stripe_subscription: return customer = stripe.Customer.retrieve(user.gold_subscr_id) customer.card = token customer.save() return customer @classmethod @handle_stripe_error def set_subscription(cls, form, customer, plan_id): subscription = customer.update_subscription(plan=plan_id) return subscription @classmethod @handle_stripe_error def cancel_subscription(cls, form, user): if not user.has_stripe_subscription: return customer = stripe.Customer.retrieve(user.gold_subscr_id) customer.delete() user.gold_subscr_id = None user._commit() subject = _('your gold subscription has been cancelled') message = _('if you have any questions please email %(email)s') message %= {'email': g.goldthanks_email} send_system_message(user, subject, message) return customer @validatedForm(VUser(), token=nop('stripeToken'), passthrough=VPrintable("passthrough", max_length=50), pennies=VInt('pennies'), months=VInt("months"), period=VOneOf("period", ("monthly", "yearly"))) def POST_goldcharge(self, form, jquery, token, passthrough, pennies, months, period): """ Submit charge to stripe. Called by GoldPayment form. This submits the charge to stripe, and gold will be applied once we receive a webhook from stripe. """ try: payment_blob = validate_blob(passthrough) except GoldException as e: # This should never happen. All fields in the payment_blob # are validated on creation form.set_html('.status', _('something bad happened, try again later')) g.log.debug('POST_goldcharge: %s' % e.message) return if period: plan_id = (g.STRIPE_MONTHLY_GOLD_PLAN if period == 'monthly' else g.STRIPE_YEARLY_GOLD_PLAN) if c.user.has_gold_subscription: form.set_html( '.status', _('your account already has a gold subscription')) return else: plan_id = None penny_months, days = months_and_days_from_pennies(pennies) if not months or months != penny_months: form.set_html('.status', _('stop trying to trick the form')) return customer = self.create_customer(form, token) if not customer: return if period: subscription = self.set_subscription(form, customer, plan_id) if not subscription: return c.user.gold_subscr_id = customer.id c.user._commit() status = _('subscription created') subject = _('reddit gold subscription') body = _('Your subscription is being processed and reddit gold ' 'will be delivered shortly.') else: charge = self.charge_customer(form, customer, pennies, passthrough) if not charge: return status = _('payment submitted') subject = _('reddit gold payment') body = _('Your payment is being processed and reddit gold ' 'will be delivered shortly.') form.set_html('.status', status) body = append_random_bottlecap_phrase(body) send_system_message(c.user, subject, body, distinguished='gold-auto') @validatedForm(VUser(), VModhash(), token=nop('stripeToken')) def POST_modify_subscription(self, form, jquery, token): customer = self.set_creditcard(form, c.user, token) if not customer: return form.set_html('.status', _('your payment details have been updated')) @validatedForm(VUser(), VModhash(), user=VByName('user')) def POST_cancel_subscription(self, form, jquery, user): if user != c.user and not c.user_is_admin: self.abort403() customer = self.cancel_subscription(form, user) if not customer: return form.set_html(".status", _("your subscription has been cancelled"))
class SteamController(RedditController): @staticmethod def make_post_login_url(): return add_sr("/f2p/steam/postlogin") @validate(VUser(), error=nop("error")) def GET_start(self, error): f2p_status = getattr(c.user, "f2p", None) error = bool(error) if f2p_status == "participated": return SteamPage(content=SteamStart(error=error)).render() elif f2p_status == "claiming": return SteamPage(content=SteamInProgress()).render() elif f2p_status == "claimed": return SteamPage(content=SteamStop()).render() return SteamPage(content=SteamSorry()).render() @validate(VUser(), VModhash()) def POST_auth(self): if getattr(c.user, "f2p", None) != "participated": abort(403) session = {} consumer = openid.consumer.consumer.Consumer(session, store=None) auth_request = consumer.begin(STEAM_AUTH_URL) post_login_url = self.make_post_login_url() url = auth_request.redirectURL(realm=g.origin, return_to=post_login_url) g.f2pcache.set("steam_session_%d" % c.user._id, session) g.log.debug("started steam auth for %s", c.user.name) return redirect_to(url) @validate(VUser()) def GET_postlogin(self): if getattr(c.user, "f2p", None) != "participated": return redirect_to("/f2p/steam") session = g.f2pcache.get("steam_session_%d" % c.user._id) if not session: return redirect_to("/f2p/steam?error=no_session") consumer = openid.consumer.consumer.Consumer(session, store=None) auth_response = consumer.complete(request.params, request.url) if auth_response.status == openid.consumer.consumer.CANCEL: return redirect_to("/f2p/steam?error=cancel") if auth_response.status != openid.consumer.consumer.SUCCESS: return redirect_to("/f2p/steam?error=not_success") steamid_match = STEAMID_EXTRACTOR.search(auth_response.identity_url) if not steamid_match: return redirect_to("/f2p/steam?error=id_mismatch") steamid = steamid_match.group(1) g.log.debug("successful steam auth for %r", steamid) with g.make_lock("f2p", "steam_claim_%d" % c.user._id): c.user._sync_latest() if c.user.f2p != "participated": return redirect_to("/f2p/steam") c.user.f2p = "claiming" c.user._commit() message = json.dumps({ "user-id": c.user._id, "steam-id": steamid, }) amqp.add_item(QNAME, message) return redirect_to("/f2p/steam")
class LiveUpdateEventsController(RedditController): def GET_home(self): return pages.LiveUpdateMetaPage( title=_("reddit live"), content=pages.LiveUpdateHome(), page_classes=["liveupdate-home"], ).render() @require_oauth2_scope("read") @api_doc( section=api_section.live, uri="/api/live/happening_now", ) def GET_happening_now(self): """ Get some basic information about the currently featured live thread. Returns an empty 204 response for api requests if no thread is currently featured. See also: [/api/live/*thread*/about](#GET_api_live_{thread}_about). """ if not is_api(): self.abort404() featured_event = get_featured_event() if not featured_event: response.status_code = 204 return c.liveupdate_event = featured_event content = Wrapped(featured_event) return pages.LiveUpdateEventPage(content).render() @validate( num=VLimit("limit", default=25, max_limit=100), after=VLiveUpdateEvent("after"), before=VLiveUpdateEvent("before"), count=VCount("count"), ) def GET_listing(self, filter, num, after, before, count): reverse = False if before: after = before reverse = True builder_cls = LiveUpdateEventBuilder wrapper = Wrapped listing_cls = Listing require_employee = True # for grepping: this is used like VEmployee if filter == "open": title = _("live threads") query = queries.get_live_events("new", "all") elif filter == "closed": title = _("closed threads") query = queries.get_complete_events("new", "all") elif filter == "active": title = _("most active threads") query = queries.get_active_events() elif filter == "reported": if not c.user_is_admin: self.abort403() title = _("reported threads") query = queries.get_reported_events() builder_cls = LiveUpdateReportedEventBuilder wrapper = pages.LiveUpdateReportedEventRow listing_cls = pages.LiveUpdateReportedEventListing elif filter == "happening_now": featured_events = get_all_featured_events() title = _("featured threads") query = sorted(set(featured_events.values())) builder_cls = featured_event_builder_factory(featured_events) wrapper = pages.LiveUpdateFeaturedEvent require_employee = False elif filter == "mine": if not c.user_is_loggedin: self.abort404() title = _("my live threads") query = queries.get_contributor_events(c.user) require_employee = False else: self.abort404() if require_employee and not c.user.employee: self.abort403() builder = builder_cls( query, num=num, after=after, reverse=reverse, count=count, wrap=wrapper, skip=True, ) listing = listing_cls(builder) return pages.LiveUpdateMetaPage( title=title, content=listing.listing(), ).render() @validate( VUser(), ) def GET_create(self): return pages.LiveUpdateMetaPage( title=_("create live thread"), content=pages.LiveUpdateCreate(), ).render() @require_oauth2_scope("submit") @validatedForm( VUser(), VModhash(), VRatelimit(rate_user=True, prefix="liveupdate_create_"), **EVENT_CONFIGURATION_VALIDATORS ) @api_doc( section=api_section.live, uri="/api/live/create", ) def POST_create(self, form, jquery, title, description, resources, nsfw): """Create a new live thread. Once created, the initial settings can be modified with [/api/live/*thread*/edit](#POST_api_live_{thread}_edit) and new updates can be posted with [/api/live/*thread*/update](#POST_api_live_{thread}_update). """ if not is_event_configuration_valid(form): return # for simplicity, set the live-thread creation threshold at the # subreddit creation threshold if not c.user_is_admin and not c.user.can_create_subreddit: form.set_error(errors.CANT_CREATE_SR, "") c.errors.add(errors.CANT_CREATE_SR, field="") return if form.has_errors("ratelimit", errors.RATELIMIT): return VRatelimit.ratelimit( rate_user=True, prefix="liveupdate_create_", seconds=60) event = LiveUpdateEvent.new( id=None, title=title, description=description, resources=resources, banned=c.user._spam, nsfw=nsfw, ) event.add_contributor(c.user, ContributorPermissionSet.SUPERUSER) queries.create_event(event) amqp.add_item("new_liveupdate_event", json.dumps({ "event_fullname": event._fullname, "creator_fullname": c.user._fullname, })) form.redirect("/live/" + event._id) form._send_data(id=event._id) liveupdate_events.create_event(event, context=c, request=request)
class RobinController(RedditController): def pre(self): RedditController.pre(self) if not feature.is_enabled("robin"): self.abort404() @validate( VUser(), VNotInTimeout(), ) def GET_join(self): room = RobinRoom.get_room_for_user(c.user) if room: return self.redirect("/robin") return RobinPage( title="robin", content=RobinJoin( robin_heavy_load=g.live_config.get('robin_heavy_load')), ).render() @validate( VAdmin(), ) def GET_all(self): return RobinPage( title="robin", content=RobinAll(), ).render() @validate( VAdmin(), ) def GET_admin(self): return RobinPage( title="robin", content=RobinAdmin(), ).render() @validate( VUser(), VNotInTimeout(), ) def GET_chat(self): room = RobinRoom.get_room_for_user(c.user) if not room: return self.redirect("/robin/join") return self._get_chat_page(room) @validate( VAdmin(), room=VRobinRoom("room_id", allow_admin=True), ) def GET_force_room(self, room): """Allow admins to view a specific room""" return self._get_chat_page(room) @validate( VAdmin(), user=VAccountByName("user"), ) def GET_user_room(self, user): """Redirect admins to a user's room""" room = RobinRoom.get_room_for_user(user) if not room: self.abort404() self.redirect("/robin/" + room.id) def _get_chat_page(self, room): path = posixpath.join("/robin", room.id, c.user._id36) websocket_url = websockets.make_url(path, max_age=3600) all_user_ids = room.get_all_participants() all_present_ids = room.get_present_participants() all_votes = room.get_all_votes() users = Account._byID(all_user_ids, data=True, stale=True) user_list = [] for user in users.itervalues(): if user._id in all_votes: vote = all_votes.get(user._id) else: vote = None user_list.append({ "name": user.name, "present": user._id in all_present_ids, "vote": vote, }) return RobinChatPage( title="chat in %s" % room.name, content=RobinChat(room=room), extra_js_config={ "robin_room_is_continued": room.is_continued, "robin_room_name": room.name, "robin_room_id": room.id, "robin_websocket_url": websocket_url, "robin_user_list": user_list, "robin_room_date": js_timestamp(room.date), "robin_room_reap_time": js_timestamp(get_reap_time(room)), }, ).render() def _has_exceeded_ratelimit(self, form, room): # grab the ratelimit (as average events per second) for the room's # current level, using the highest level configured that's not bigger # than the room. e.g. if ratelimits are defined for levels 1, 2, and 4 # and the room is level 3, this will give us the ratelimit specified # for 2. desired_avg_per_sec = 1 by_level = g.live_config.get("robin_ratelimit_avg_per_sec", {}) for level, avg_per_sec in sorted(by_level.items(), key=lambda (x, y): int(x)): if int(level) > room.level: break desired_avg_per_sec = avg_per_sec # now figure out how many events per window that means window_size = g.live_config.get("robin_ratelimit_window", 10) allowed_events_per_window = int(desired_avg_per_sec * window_size) try: # now figure out how much they've actually used ratelimit_key = "robin/{}".format(c.user._id36) time_slice = ratelimit.get_timeslice(window_size) usage = ratelimit.get_usage(ratelimit_key, time_slice) # ratelimit them if too much if usage >= allowed_events_per_window: g.stats.simple_event("robin.ratelimit.exceeded") period_end = datetime.datetime.utcfromtimestamp(time_slice.end) period_end_utc = period_end.replace(tzinfo=pytz.UTC) until_reset = utils.timeuntil(period_end_utc) c.errors.add(errors.RATELIMIT, {"time": until_reset}, field="ratelimit", code=429) form.has_errors("ratelimit", errors.RATELIMIT) return True # or record the usage and move on ratelimit.record_usage(ratelimit_key, time_slice) except ratelimit.RatelimitError as exc: g.log.warning("ratelimit error: %s", exc) return False @validatedForm( VUser(), VNotInTimeout(), VModhash(), room=VRobinRoom("room_id"), message=VLength("message", max_length=140), # TODO: do we want md? ) def POST_message(self, form, jquery, room, message): if self._has_exceeded_ratelimit(form, room): return if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG): return websockets.send_broadcast( namespace="/robin/" + room.id, type="chat", payload={ "from": c.user.name, "body": message, }, ) events.message( room=room, message=message, sent_dt=datetime.datetime.utcnow(), context=c, request=request, ) @validatedForm( VUser(), VNotInTimeout(), VModhash(), room=VRobinRoom("room_id"), vote=VOneOf("vote", VALID_VOTES), ) def POST_vote(self, form, jquery, room, vote): if self._has_exceeded_ratelimit(form, room): return if not vote: # TODO: error return? return g.stats.simple_event('robin.vote.%s' % vote) room.set_vote(c.user, vote) websockets.send_broadcast( namespace="/robin/" + room.id, type="vote", payload={ "from": c.user.name, "vote": vote, }, ) events.vote( room=room, vote=vote, sent_dt=datetime.datetime.utcnow(), context=c, request=request, ) @validatedForm( VUser(), VNotInTimeout(), VModhash(), ) def POST_join_room(self, form, jquery): if g.live_config.get('robin_heavy_load'): request.environ["usable_error_content"] = ( "Robin is currently experience high load.") abort(503) room = RobinRoom.get_room_for_user(c.user) if room: # user is already in a room, they should get redirected by the # frontend after polling /api/room_assignment.json return add_to_waitinglist(c.user) @validatedForm( VUser(), VModhash(), ) def POST_leave_room(self, form, jquery): room = RobinRoom.get_room_for_user(c.user) if not room: return room.remove_participants([c.user]) websockets.send_broadcast( namespace="/robin/" + room.id, type="users_abandoned", payload={ "users": [c.user.name], }, ) @json_validate( VUser(), VNotInTimeout(), ) def GET_room_assignment(self, responder): room = RobinRoom.get_room_for_user(c.user) if room: return {"roomId": room.id} @validatedForm( VAdmin(), VModhash(), ) def POST_admin_prompt(self, form, jquery): prompt_for_voting() @validatedForm( VAdmin(), VModhash(), ) def POST_admin_reap(self, form, jquery): reap_ripe_rooms() @validatedForm( VAdmin(), VModhash(), message=VLength("message", max_length=140), ) def POST_admin_broadcast(self, form, jquery, message): if form.has_errors("message", errors.NO_TEXT, errors.TOO_LONG): return websockets.send_broadcast( namespace="/robin", type="system_broadcast", payload={ "body": message, }, )
class OAuth2FrontendController(RedditController): def check_for_bearer_token(self): pass def pre(self): RedditController.pre(self) require_https() def _check_redirect_uri(self, client, redirect_uri): if not redirect_uri or not client or redirect_uri != client.redirect_uri: abort(ForbiddenError(errors.OAUTH2_INVALID_REDIRECT_URI)) def _error_response(self, state, redirect_uri, as_fragment=False): """Return an error redirect, but only if client_id and redirect_uri are valid.""" resp = {"state": state} if (errors.OAUTH2_INVALID_CLIENT, "client_id") in c.errors: resp["error"] = "unauthorized_client" elif (errors.OAUTH2_ACCESS_DENIED, "authorize") in c.errors: resp["error"] = "access_denied" elif (errors.BAD_HASH, None) in c.errors: resp["error"] = "access_denied" elif (errors.INVALID_OPTION, "response_type") in c.errors: resp["error"] = "unsupported_response_type" elif (errors.OAUTH2_INVALID_SCOPE, "scope") in c.errors: resp["error"] = "invalid_scope" else: resp["error"] = "invalid_request" final_redirect = _update_redirect_uri(redirect_uri, resp, as_fragment) return self.redirect(final_redirect, code=302) @validate(VUser(), response_type=VOneOf("response_type", ("code", "token")), client=VOAuth2ClientID(), redirect_uri=VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope=VOAuth2Scope(), state=VRequired("state", errors.NO_TEXT), duration=VOneOf("duration", ("temporary", "permanent"), default="temporary")) def GET_authorize(self, response_type, client, redirect_uri, scope, state, duration): """ First step in [OAuth 2.0](http://oauth.net/2/) authentication. End users will be prompted for their credentials (username/password) and asked if they wish to authorize the application identified by the **client_id** parameter with the permissions specified by the **scope** parameter. They are then redirected to the endpoint on the client application's side specified by **redirect_uri**. If the user granted permission to the application, the response will contain a **code** parameter with a temporary authorization code which can be exchanged for an access token at [/api/v1/access_token](#api_method_access_token). **redirect_uri** must match the URI configured for the client in the [app preferences](/prefs/apps). If **client_id** or **redirect_uri** is not valid, or if the call does not take place over SSL, a 403 error will be returned. For all other errors, a redirect to **redirect_uri** will be returned, with a **error** parameter indicating why the request failed. """ # Check redirect URI first; it will ensure client exists self._check_redirect_uri(client, redirect_uri) if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id")) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed c.errors.add((errors.INVALID_OPTION, "duration")) if not c.errors: return OAuth2AuthorizationPage(client, redirect_uri, scope, state, duration, response_type).render() else: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) @validate(VUser(), VModhash(fatal=False), client=VOAuth2ClientID(), redirect_uri=VRequired("redirect_uri", errors.OAUTH2_INVALID_REDIRECT_URI), scope=VOAuth2Scope(), state=VRequired("state", errors.NO_TEXT), duration=VOneOf("duration", ("temporary", "permanent"), default="temporary"), authorize=VRequired("authorize", errors.OAUTH2_ACCESS_DENIED), response_type=VOneOf("response_type", ("code", "token"), default="code")) def POST_authorize(self, authorize, client, redirect_uri, scope, state, duration, response_type): """Endpoint for OAuth2 authorization.""" if response_type == "token" and client.is_confidential(): # Prevent "confidential" clients from distributing tokens # in a non-confidential manner c.errors.add((errors.OAUTH2_INVALID_CLIENT, "client_id")) if response_type == "token" and duration != "temporary": # implicit grant -> No refresh tokens allowed c.errors.add((errors.INVALID_OPTION, "duration")) self._check_redirect_uri(client, redirect_uri) if c.errors: return self._error_response(state, redirect_uri, as_fragment=(response_type == "token")) if response_type == "code": code = OAuth2AuthorizationCode._new(client._id, redirect_uri, c.user._id36, scope, duration == "permanent") resp = {"code": code._id, "state": state} final_redirect = _update_redirect_uri(redirect_uri, resp) elif response_type == "token": token = OAuth2AccessToken._new(client._id, c.user._id36, scope) token_data = OAuth2AccessController._make_token_dict(token) token_data["state"] = state final_redirect = _update_redirect_uri(redirect_uri, token_data, as_fragment=True) return self.redirect(final_redirect, code=302)
class IpnController(RedditController): # Used when buying gold with creddits @validatedForm(VUser(), months = VInt("months"), passthrough = VPrintable("passthrough", max_length=50)) def POST_spendcreddits(self, form, jquery, months, passthrough): if months is None or months < 1: form.set_html(".status", _("nice try.")) return days = months * 31 if not passthrough: raise ValueError("/spendcreddits got no passthrough?") blob_key, payment_blob = get_blob(passthrough) if payment_blob["goldtype"] != "gift": raise ValueError("/spendcreddits payment_blob %s has goldtype %s" % (passthrough, payment_blob["goldtype"])) signed = payment_blob["signed"] giftmessage = _force_unicode(payment_blob["giftmessage"]) recipient_name = payment_blob["recipient"] if payment_blob["account_id"] != c.user._id: fmt = ("/spendcreddits payment_blob %s has userid %d " + "but c.user._id is %d") raise ValueError(fmt % passthrough, payment_blob["account_id"], c.user._id) try: recipient = Account._by_name(recipient_name) except NotFound: raise ValueError("Invalid username %s in spendcreddits, buyer = %s" % (recipient_name, c.user.name)) if recipient._deleted: form.set_html(".status", _("that user has deleted their account")) return if not c.user_is_admin: if months > c.user.gold_creddits: raise ValueError("%s is trying to sneak around the creddit check" % c.user.name) c.user.gold_creddits -= months c.user.gold_creddit_escrow += months c.user._commit() comment_id = payment_blob.get("comment") comment = send_gift(c.user, recipient, months, days, signed, giftmessage, comment_id) if not c.user_is_admin: c.user.gold_creddit_escrow -= months c.user._commit() payment_blob["status"] = "processed" g.hardcache.set(blob_key, payment_blob, 86400 * 30) form.set_html(".status", _("the gold has been delivered!")) form.find("button").hide() if comment: gilding_message = make_comment_gold_message(comment, user_gilded=True) jquery.gild_comment(comment_id, gilding_message, comment.gildings) @textresponse(full_sn = VLength('serial-number', 100)) def POST_gcheckout(self, full_sn): if full_sn: short_sn = full_sn.split('-')[0] g.log.error( "GOOGLE CHECKOUT: %s" % short_sn) trans = _google_ordernum_request(short_sn) # get the financial details auth = trans.find("authorization-amount-notification") custom = None cart = trans.find("shopping-cart") if cart: private_item_data = cart.find("merchant-private-item-data") if private_item_data: custom = str(private_item_data.contents[0]) if not auth: # see if the payment was declinded status = trans.findAll('financial-order-state') if 'PAYMENT_DECLINED' in [x.contents[0] for x in status]: g.log.error("google declined transaction found: '%s'" % short_sn) elif 'REVIEWING' not in [x.contents[0] for x in status]: g.log.error(("google transaction not found: " + "'%s', status: %s") % (short_sn, [x.contents[0] for x in status])) else: g.log.error(("google transaction status: " + "'%s', status: %s") % (short_sn, [x.contents[0] for x in status])) if custom: payment_blob = validate_blob(custom) buyer = payment_blob['buyer'] subject = _('gold order') msg = _('your order has been received and gold will' ' be delivered shortly. please bear with us' ' as google wallet payments can take up to an' ' hour to complete') try: send_system_message(buyer, subject, msg) except MessageError: g.log.error('gcheckout send_system_message failed') elif auth.find("financial-order-state" ).contents[0] == "CHARGEABLE": email = str(auth.find("email").contents[0]) payer_id = str(auth.find('buyer-id').contents[0]) if custom: days = None try: pennies = int(float(trans.find("order-total" ).contents[0])*100) months, days = months_and_days_from_pennies(pennies) if not months: raise ValueError("Bad pennies for %s" % short_sn) charged = trans.find("charge-amount-notification") if not charged: _google_charge_and_ship(short_sn) parameters = request.POST.copy() self.finish(parameters, "g%s" % short_sn, email, payer_id, None, custom, pennies, months, days) except ValueError, e: g.log.error(e) else: raise ValueError("Got no custom blob for %s" % short_sn) return (('<notification-acknowledgment ' + 'xmlns="http://checkout.google.com/schema/2" ' + 'serial-number="%s" />') % full_sn) else: