class SponsorController(PromoteController): @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage(title=_("manage roadblocks"), content=Roadblocks()).render() @validate(VSponsorAdminOrAdminSecret('secret'), start=VDate('startdate'), end=VDate('enddate'), link_text=nop('link_text'), owner=VAccountByName('owner')) def GET_report(self, start, end, link_text=None, owner=None): now = datetime.now(g.tz).replace(hour=0, minute=0, second=0, microsecond=0) if not start or not end: start = promote.promo_datetime_now(offset=1).date() end = promote.promo_datetime_now(offset=8).date() c.errors.remove((errors.BAD_DATE, 'startdate')) c.errors.remove((errors.BAD_DATE, 'enddate')) end = end or now - timedelta(days=1) start = start or end - timedelta(days=7) links = [] bad_links = [] owner_name = owner.name if owner else '' if owner: campaign_ids = PromotionWeights.get_campaign_ids( start, end, author_id=owner._id) campaigns = PromoCampaign._byID(campaign_ids, data=True) link_ids = {camp.link_id for camp in campaigns.itervalues()} links.extend(Link._byID(link_ids, data=True, return_dict=False)) if link_text is not None: id36s = link_text.replace(',', ' ').split() try: links_from_text = Link._byID36(id36s, data=True) except NotFound: links_from_text = {} bad_links = [id36 for id36 in id36s if id36 not in links_from_text] links.extend(links_from_text.values()) content = PromoteReport(links, link_text, owner_name, bad_links, start, end) if c.render_style == 'csv': return content.as_csv() else: return PromotePage(title=_("sponsored link report"), content=content).render() @validate( VSponsorAdmin(), start=VDate('startdate', reference_date=promote.promo_datetime_now), end=VDate('enddate', reference_date=promote.promo_datetime_now), sr_name=nop('sr_name'), collection_name=nop('collection_name'), ) def GET_promote_inventory(self, start, end, sr_name, collection_name): if not start or not end: start = promote.promo_datetime_now(offset=1).date() end = promote.promo_datetime_now(offset=8).date() c.errors.remove((errors.BAD_DATE, 'startdate')) c.errors.remove((errors.BAD_DATE, 'enddate')) target = Target(Frontpage.name) if sr_name: try: sr = Subreddit._by_name(sr_name) target = Target(sr.name) except NotFound: c.errors.add(errors.SUBREDDIT_NOEXIST, field='sr_name') elif collection_name: collection = Collection.by_name(collection_name) if not collection: c.errors.add(errors.COLLECTION_NOEXIST, field='collection_name') else: target = Target(collection) content = PromoteInventory(start, end, target) return PromotePage(title=_("sponsored link inventory"), content=content).render() @validate( VSponsorAdmin(), user=VByName('name', thing_cls=Account), ) def GET_lookup_user(self, user): content = SponsorLookupUser(user=user) return PromotePage(title=_("lookup user"), content=content).render()
class PromoteController(ListingController): where = 'promoted' render_cls = PromotePage @property def title_text(self): return _('promoted by you') @classmethod @memoize('live_by_subreddit', time=300) def live_by_subreddit(cls, sr): if sr == Frontpage: sr_id = '' else: sr_id = sr._id r = LiveAdWeights.get([sr_id]) return [i.link for i in r[sr_id]] @classmethod @memoize('subreddits_with_promos', time=3600) def subreddits_with_promos(cls): sr_ids = LiveAdWeights.get_live_subreddits() srs = Subreddit._byID(sr_ids, return_dict=False) sr_names = sorted([sr.name for sr in srs], key=lambda s: s.lower()) return sr_names @property def menus(self): filters = [ NamedButton('all_promos', dest=''), NamedButton('future_promos'), NamedButton('unpaid_promos'), NamedButton('rejected_promos'), NamedButton('pending_promos'), NamedButton('live_promos'), ] menus = [ NavMenu(filters, base_path='/promoted', title='show', type='lightdrop') ] if self.sort == 'live_promos' and c.user_is_sponsor: sr_names = self.subreddits_with_promos() buttons = [NavButton(name, name) for name in sr_names] frontbutton = NavButton('FRONTPAGE', Frontpage.name, aliases=[ '/promoted/live_promos/%s' % urllib.quote(Frontpage.name) ]) buttons.insert(0, frontbutton) buttons.insert(0, NavButton('all', '')) menus.append( NavMenu(buttons, base_path='/promoted/live_promos', title='subreddit', type='lightdrop')) return menus def keep_fn(self): def keep(item): if item.promoted and not item._deleted: return True else: return False return keep def query(self): if c.user_is_sponsor: if self.sort == "future_promos": return queries.get_all_unapproved_links() elif self.sort == "pending_promos": return queries.get_all_accepted_links() elif self.sort == "unpaid_promos": return queries.get_all_unpaid_links() elif self.sort == "rejected_promos": return queries.get_all_rejected_links() elif self.sort == "live_promos" and self.sr: return self.live_by_subreddit(self.sr) elif self.sort == 'live_promos': return queries.get_all_live_links() elif self.sort == 'underdelivered': q = queries.get_underdelivered_campaigns() campaigns = PromoCampaign._by_fullname(list(q), data=True, return_dict=False) link_ids = [camp.link_id for camp in campaigns] return [Link._fullname_from_id36(to36(id)) for id in link_ids] return queries.get_all_promoted_links() else: if self.sort == "future_promos": return queries.get_unapproved_links(c.user._id) elif self.sort == "pending_promos": return queries.get_accepted_links(c.user._id) elif self.sort == "unpaid_promos": return queries.get_unpaid_links(c.user._id) elif self.sort == "rejected_promos": return queries.get_rejected_links(c.user._id) elif self.sort == "live_promos": return queries.get_live_links(c.user._id) return queries.get_promoted_links(c.user._id) @validate(VSponsor(), sr=nop('sr')) def GET_listing(self, sr=None, sort="", **env): if not c.user_is_loggedin or not c.user.email_verified: return self.redirect("/ad_inq") self.sort = sort self.sr = None if sr and sr == Frontpage.name: self.sr = Frontpage elif sr: try: self.sr = Subreddit._by_name(sr) except NotFound: pass return ListingController.GET_listing(self, **env) GET_index = GET_listing @validate(VSponsor()) def GET_new_promo(self): return PromotePage('content', content=PromoteLinkNew()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, wrapper=promote.sponsor_wrapper, skip=False) form = PromoteLinkForm(link, rendered) page = PromotePage('new_promo', content=form) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @json_validate(sr=VSubmitSR('sr', promotion=True), start=VDate('startdate'), end=VDate('enddate')) def GET_check_inventory(self, responder, sr, start, end): sr = sr or Frontpage available_by_datestr = inventory.get_available_pageviews(sr, start, end, datestr=True) return {'inventory': available_by_datestr} @validate(VSponsor(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_graph(self, dates): start, end, bad_dates = _check_dates(dates) return PromotePage("graph", content=Promote_Graph( start, end, bad_dates=bad_dates)).render() @validate(VSponsorAdmin(), dates=VDateRange(["startdate", "enddate"], max_range=timedelta(days=28), required=False)) def GET_admingraph(self, dates): start, end, bad_dates = _check_dates(dates) content = Promote_Graph(start, end, bad_dates=bad_dates, admin_view=True) if c.render_style == 'csv': return content.as_csv() return PromotePage("admingraph", content=content).render() # ## POST controllers below @validatedForm(VSponsorAdmin(), link=VLink("link_id"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if campaign_has_oversold_error(form, campaign): form.set_html(".freebie", "target oversold, can't freebie") return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after("<p>" + text + "</p>") @noresponse(VSponsorAdmin(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validate(VSponsorAdmin(), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_refund(self, link, campaign): if campaign.link_id != link._id: return self.abort404() content = RefundPage(link, campaign) return Reddit("refund", content=content, show_sidebar=False).render() @validatedForm(VSponsorAdmin(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = campaign.bid - billable_amount if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount) form.set_html('.status', _('refund succeeded')) else: form.set_html('.status', _('refund not needed')) @validatedForm(VSponsor('link_id'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), l=VLink('link_id'), title=VTitle('title'), url=VUrl('url', allow_self=False, lookup=False), ip=ValidIP(), disable_comments=VBoolean("disable_comments"), media_width=VInt("media-width", min=0), media_height=VInt("media-height", min=0), media_embed=VLength("media-embed", 1000), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100)) def POST_edit_promo(self, form, jquery, ip, l, title, url, disable_comments, media_height, media_width, media_embed, media_override, domain_override): should_ratelimit = False if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or form.has_errors('url', errors.NO_URL, errors.BAD_URL) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if not l: l = promote.new_promotion(title, url, c.user, ip) elif promote.is_promo(l): changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link trusted = c.user_is_sponsor or c.user.trusted_sponsor if not promote.is_promoted(l) or trusted: if title and title != l.title: l.title = title changed = not trusted if url and url != l.url: l.url = url changed = not trusted # only trips if the title and url are changed by a non-sponsor if changed and not promote.is_unpaid(l): promote.unapprove_promotion(l) if trusted and promote.is_unapproved(l): promote.accept_promotion(l) # comment disabling is free to be changed any time. l.disable_comments = disable_comments if c.user_is_sponsor or c.user.trusted_sponsor: if media_embed and media_width and media_height: l.media_object = dict(height=media_height, width=media_width, content=media_embed, type='custom') else: l.media_object = None l.media_override = media_override if getattr(l, "domain_override", False) or domain_override: l.domain_override = domain_override l._commit() form.redirect(promote.promo_edit_url(l)) @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage('content', content=Roadblocks()).render() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_add_roadblock(self, form, jquery, dates, sr): if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return if dates and sr: sd, ed = dates PromotedLinkRoadblock.add(sr, sd, ed) jquery.refresh() @validatedForm(VSponsorAdmin(), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), sr=VSubmitSR('sr', promotion=True)) def POST_rm_roadblock(self, form, jquery, dates, sr): if dates and sr: sd, ed = dates PromotedLinkRoadblock.remove(sr, sd, ed) jquery.refresh() @validatedForm(VSponsor('link_id'), VModhash(), dates=VDateRange(['startdate', 'enddate'], future=1, reference_date=promote.promo_datetime_now, business_days=False, sponsor_override=True), link=VLink('link_id'), bid=VBid('bid', min=0, max=g.max_promote_bid, coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), targeting=VLength("targeting", 10)) def POST_edit_campaign(self, form, jquery, link, campaign_id36, dates, bid, sr, targeting): if not link: return start, end = dates or (None, None) author = Account._byID(link.author_id, data=True) cpm = author.cpm_selfserve_pennies if (start and end and not promote.is_accepted(link) and not c.user_is_sponsor): # if the ad is not approved already, ensure the start date # is at least 2 days in the future start = start.date() end = end.date() now = promote.promo_datetime_now() future = make_offset_date(now, g.min_promote_future, business_days=True) if start < future.date(): c.errors.add(errors.BAD_FUTURE_DATE, msg_params=dict(day=g.min_promote_future), field="startdate") if (form.has_errors('startdate', errors.BAD_DATE, errors.BAD_FUTURE_DATE) or form.has_errors('enddate', errors.BAD_DATE, errors.BAD_FUTURE_DATE, errors.BAD_DATE_RANGE)): return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return if form.has_errors('bid', errors.BAD_BID): return if campaign_id36: # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) if (bid != campaign.bid and campaign.start_date < datetime.now(g.tz) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return except NotFound: pass min_bid = 0 if c.user_is_sponsor else g.min_promote_bid if bid is None or bid < min_bid: c.errors.add(errors.BAD_BID, field='bid', msg_params={ 'min': min_bid, 'max': g.max_promote_bid }) form.has_errors('bid', errors.BAD_BID) return if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): # checking to get the error set in the form, but we can't # check for rate-limiting if there's no subreddit return roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = { "start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y') } c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return elif targeting == 'none': sr = None # Check inventory campaign_id = campaign._id if campaign_id36 else None if has_oversold_error(form, campaign_id, start, end, bid, cpm, sr): return if campaign_id36 is not None: campaign = PromoCampaign._byID36(campaign_id36) promote.edit_campaign(link, campaign, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) else: campaign = promote.new_campaign(link, dates, bid, cpm, sr) r = promote.get_renderable_campaigns(link, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, r.duration, r.bid, r.spent, r.cpm, r.sr, r.status) @validatedForm(VSponsor('link_id'), VModhash(), l=VLink('link_id'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if l and campaign: promote.delete_campaign(l, campaign) @validatedForm(VSponsor('container'), VModhash(), user=VExistingUname('name'), thing=VByName('container')) def POST_traffic_viewer(self, form, jquery, user, thing): """ Adds a user to the list of users allowed to view a promoted link's traffic page. """ if not form.has_errors("name", errors.USER_DOESNT_EXIST, errors.NO_USER): form.set_inputs(name="") form.set_html(".status:first", _("added")) if promote.add_traffic_viewer(thing, user): user_row = TrafficViewerList(thing).user_row( 'traffic_viewer', user) jquery(".traffic_viewer-table").show().find( "table").insert_table_rows(user_row) # send the user a message msg = user_added_messages['traffic']['pm']['msg'] subj = user_added_messages['traffic']['pm']['subject'] if msg and subj: d = dict(url=thing.make_permalink_slow(), traffic_url=promote.promo_traffic_url(thing), title=thing.title) msg = msg % d item, inbox_rel = Message._new(c.user, user, subj, msg, request.ip) queries.new_message(item, inbox_rel) @validatedForm(VSponsor('container'), VModhash(), iuser=VByName('id'), thing=VByName('container')) def POST_rm_traffic_viewer(self, form, jquery, iuser, thing): if thing and iuser: promote.rm_traffic_viewer(thing, iuser) @validatedForm( VSponsor('link'), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], allowed_countries=g.allowed_pay_countries), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"])) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): # Check inventory if campaign_has_oversold_error(form, campaign): return address_modified = not pay_id or edit form_has_errors = False if address_modified: if (form.has_errors([ "firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber" ], errors.BAD_ADDRESS) or form.has_errors( ["cardNumber", "expirationDate", "cardCode"], errors.BAD_CARD)): form_has_errors = True elif g.authorizenetapi: pay_id = edit_profile(c.user, address, creditcard, pay_id) else: pay_id = 1 # if link is in use or finished, don't make a change if pay_id and not form_has_errors: # valid bid and created or existing bid id. # check if already a transaction if g.authorizenetapi: success, reason = promote.auth_campaign( link, campaign, c.user, pay_id) else: success = True if success: form.redirect(promote.promo_edit_url(link)) else: form.set_html( ".status", reason or _("failed to authenticate card. sorry.")) @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if not campaign.link_id == link._id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() def GET_link_thumb(self, *a, **kw): """ See GET_upload_sr_image for rationale """ return "nothing to see here." @validate(VSponsor("link_id"), link=VByName('link_id'), file=VLength('file', 500 * 1024), img_type=VImageType('img_type')) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if link and (not promote.is_promoted(link) or c.user_is_sponsor or c.user.trusted_sponsor): errors = dict(BAD_CSS_NAME="", IMAGE_ERROR="") try: # thumnails for promoted links can change and therefore expire force_thumbnail(link, file, file_type=".%s" % img_type) except cssfilter.BadImage: # if the image doesn't clean up nicely, abort errors["IMAGE_ERROR"] = _("bad image") if any(errors.values()): return UploadedImage("", "", "upload", errors=errors, form_id="image-upload").render() else: link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate(VSponsorAdmin(), launchdate=VDate('ondate'), dates=VDateRange(['startdate', 'enddate']), query_type=VOneOf('q', ('started_on', 'between'), default=None)) def GET_admin(self, launchdate=None, dates=None, query_type=None): return PromoAdminTool(query_type=query_type, launchdate=launchdate, start=dates[0], end=dates[1]).render() @validate(VSponsorAdminOrAdminSecret('secret'), start=VDate('startdate'), end=VDate('enddate'), link_text=nop('link_text'), owner=VAccountByName('owner')) def GET_report(self, start, end, link_text=None, owner=None): now = datetime.now(g.tz).replace(hour=0, minute=0, second=0, microsecond=0) end = end or now - timedelta(days=1) start = start or end - timedelta(days=7) links = [] bad_links = [] owner_name = owner.name if owner else '' if owner: promo_weights = PromotionWeights.get_campaigns(start, end, author_id=owner._id) campaign_ids = [pw.promo_idx for pw in promo_weights] campaigns = PromoCampaign._byID(campaign_ids, data=True) link_ids = {camp.link_id for camp in campaigns.itervalues()} links.extend(Link._byID(link_ids, data=True, return_dict=False)) if link_text is not None: id36s = link_text.replace(',', ' ').split() try: links_from_text = Link._byID36(id36s, data=True) except NotFound: links_from_text = {} bad_links = [id36 for id36 in id36s if id36 not in links_from_text] links.extend(links_from_text.values()) content = PromoteReport(links, link_text, owner_name, bad_links, start, end) if c.render_style == 'csv': return content.as_csv() else: return PromotePage('report', content=content).render()
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 APIv1GoldController(OAuth2ResourceController): def pre(self): OAuth2ResourceController.pre(self) 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): 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) send_gift( buyer=c.user, recipient=recipient, months=months, days=months * 31, signed=False, giftmessage=None, thing_fullname=thing_fullname, ) if not c.user.employee: c.user.gold_creddits -= months c.user._commit() @require_oauth2_scope("creddits") @validate( 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, ) @require_oauth2_scope("creddits") @validate( 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, )
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 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 PromoteController(RedditController): @validate(VSponsor()) def GET_new_promo(self): return PromotePage(title=_("create sponsored link"), content=PromoteLinkNew()).render() @validate(VSponsor('link'), link=VLink('link')) def GET_edit_promo(self, link): if not link or link.promoted is None: return self.abort404() rendered = wrap_links(link, skip=False) form = PromoteLinkEdit(link, rendered) page = PromotePage(title=_("edit sponsored link"), content=form, show_sidebar=False, extension_handling=False) return page.render() # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) def GET_edit_promo_campaign(self, campaign): if not campaign: return self.abort404() link = Link._byID(campaign.link_id) return self.redirect(promote.promo_edit_url(link)) @validate(VSponsorAdmin(), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_refund(self, link, campaign): if link._id != campaign.link_id: return self.abort404() content = RefundPage(link, campaign) return Reddit("refund", content=content, show_sidebar=False).render() @validate(VSponsorAdmin()) def GET_roadblock(self): return PromotePage(title=_("manage roadblocks"), content=Roadblocks()).render() @validate(VSponsor("link"), link=VLink("link"), campaign=VPromoCampaign("campaign")) def GET_pay(self, link, campaign): if link._id != campaign.link_id: return self.abort404() # no need for admins to play in the credit card area if c.user_is_loggedin and c.user._id != link.author_id: return self.abort404() if g.authorizenetapi: data = get_account_info(c.user) content = PaymentForm(link, campaign, customer_id=data.customerProfileId, profiles=data.paymentProfiles, max_profiles=PROFILE_LIMIT) else: content = None res = LinkInfoPage(link=link, content=content, show_sidebar=False) return res.render() @validate(VSponsorAdminOrAdminSecret('secret'), start=VDate('startdate'), end=VDate('enddate'), link_text=nop('link_text'), owner=VAccountByName('owner')) def GET_report(self, start, end, link_text=None, owner=None): now = datetime.now(g.tz).replace(hour=0, minute=0, second=0, microsecond=0) if not start or not end: start = promote.promo_datetime_now(offset=1).date() end = promote.promo_datetime_now(offset=8).date() c.errors.remove((errors.BAD_DATE, 'startdate')) c.errors.remove((errors.BAD_DATE, 'enddate')) end = end or now - timedelta(days=1) start = start or end - timedelta(days=7) links = [] bad_links = [] owner_name = owner.name if owner else '' if owner: promo_weights = PromotionWeights.get_campaigns(start, end, author_id=owner._id) campaign_ids = [pw.promo_idx for pw in promo_weights] campaigns = PromoCampaign._byID(campaign_ids, data=True) link_ids = {camp.link_id for camp in campaigns.itervalues()} links.extend(Link._byID(link_ids, data=True, return_dict=False)) if link_text is not None: id36s = link_text.replace(',', ' ').split() try: links_from_text = Link._byID36(id36s, data=True) except NotFound: links_from_text = {} bad_links = [id36 for id36 in id36s if id36 not in links_from_text] links.extend(links_from_text.values()) content = PromoteReport(links, link_text, owner_name, bad_links, start, end) if c.render_style == 'csv': return content.as_csv() else: return PromotePage(title=_("sponsored link report"), content=content).render() @validate( VSponsorAdmin(), start=VDate('startdate', reference_date=promote.promo_datetime_now), end=VDate('enddate', reference_date=promote.promo_datetime_now), sr_name=nop('sr_name'), collection_name=nop('collection_name'), ) def GET_promote_inventory(self, start, end, sr_name, collection_name): if not start or not end: start = promote.promo_datetime_now(offset=1).date() end = promote.promo_datetime_now(offset=8).date() c.errors.remove((errors.BAD_DATE, 'startdate')) c.errors.remove((errors.BAD_DATE, 'enddate')) target = Target(Frontpage.name) if sr_name: try: sr = Subreddit._by_name(sr_name) target = Target(sr.name) except NotFound: c.errors.add(errors.SUBREDDIT_NOEXIST, field='sr_name') elif collection_name: collection = Collection.by_name(collection_name) if not collection: c.errors.add(errors.COLLECTION_NOEXIST, field='collection_name') else: target = Target(collection) content = PromoteInventory(start, end, target) return PromotePage(title=_("sponsored link inventory"), content=content).render()
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 APIv1Controller(OAuth2ResourceController): def pre(self): OAuth2ResourceController.pre(self) self.authenticate_with_token() self.run_sitewide_ratelimits() def try_pagecache(self): pass @staticmethod def on_validation_error(error): abort_with_error(error, error.code or 400) @require_oauth2_scope("identity") @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( 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) def _get_usertrophies(self, user): trophies = Trophy.by_account(user) def visible_trophy(trophy): return trophy._thing2.awardtype != 'invisible' trophies = filter(visible_trophy, trophies) resp = TrophyListJsonTemplate().render(trophies) return self.api_wrapper(resp.finalize()) @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._get_usertrophies(user) @require_oauth2_scope("identity") @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._get_usertrophies(c.oauth_user) @require_oauth2_scope("mysubreddits") @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(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 if pref_name == "pref_content_langs": new_value = vprefs.format_content_lang_pref(new_value) 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))