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 PromoteApiController(ApiController): @json_validate(sr=VSubmitSR('sr', promotion=True), collection=VCollection('collection'), location=VLocation(), start=VDate('startdate'), end=VDate('enddate'), platform=VOneOf('platform', ('mobile', 'desktop', 'all'), default='all')) def GET_check_inventory(self, responder, sr, collection, location, start, end, platform): if collection: target = Target(collection) sr = None else: sr = sr or Frontpage target = Target(sr.name) if not allowed_location_and_target(location, target): return abort(403, 'forbidden') available = inventory.get_available_pageviews( target, start, end, location=location, platform=platform, datestr=True) return {'inventory': available} @validatedForm(VSponsorAdmin(), VModhash(), link=VLink("link_id36"), campaign=VPromoCampaign("campaign_id36")) def POST_freebie(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') if campaign_has_oversold_error(form, campaign): form.set_text(".freebie", _("target oversold, can't freebie")) return if promote.is_promo(link) and campaign: promote.free_campaign(link, campaign, c.user) form.redirect(promote.promo_edit_url(link)) @validatedForm(VSponsorAdmin(), VModhash(), link=VByName("link"), note=nop("note")) def POST_promote_note(self, form, jquery, link, note): if promote.is_promo(link): text = PromotionLog.add(link, note) form.find(".notes").children(":last").after( format_html("<p>%s</p>", text)) @validatedForm( VSponsorAdmin(), VModhash(), thing = VByName("thing_id"), is_fraud=VBoolean("fraud"), ) def POST_review_fraud(self, form, jquery, thing, is_fraud): if not promote.is_promo(thing): return promote.review_fraud(thing, is_fraud) button = jquery(".id-%s .fraud-button" % thing._fullname) button.text(_("fraud" if is_fraud else "not fraud")) form.fadeOut() @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id')) def POST_promote(self, thing): if promote.is_promo(thing): promote.accept_promotion(thing) @noresponse(VSponsorAdmin(), VModhash(), thing=VByName('id'), reason=nop("reason")) def POST_unpromote(self, thing, reason): if promote.is_promo(thing): promote.reject_promotion(thing, reason=reason) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link'), campaign=VPromoCampaign('campaign')) def POST_refund_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') billable_impressions = promote.get_billable_impressions(campaign) billable_amount = promote.get_billable_amount(campaign, billable_impressions) refund_amount = promote.get_refund_amount(campaign, billable_amount) if refund_amount > 0: promote.refund_campaign(link, campaign, billable_amount, billable_impressions) form.set_text('.status', _('refund succeeded')) else: form.set_text('.status', _('refund not needed')) @validatedMultipartForm( VSponsor('link_id36'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), username=VLength('username', 100, empty_error=None), title=VTitle('title'), url=VUrl('url', allow_self=False), selftext=VMarkdownLength('text', max_length=40000), kind=VOneOf('kind', ['link', 'self']), disable_comments=VBoolean("disable_comments"), sendreplies=VBoolean("sendreplies"), media_url=VUrl("media_url", allow_self=False, valid_schemes=('http', 'https')), gifts_embed_url=VUrl("gifts_embed_url", allow_self=False, valid_schemes=('http', 'https')), media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")), media_autoplay=VBoolean("media_autoplay"), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100), third_party_tracking=VUrl("third_party_tracking"), third_party_tracking_2=VUrl("third_party_tracking_2"), is_managed=VBoolean("is_managed"), thumbnail_file=VUploadLength('file', 500*1024), ) def POST_create_promo(self, form, jquery, username, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, iframe_embed_url, media_url_type, domain_override, third_party_tracking, third_party_tracking_2, is_managed, thumbnail_file): return self._edit_promo(form, jquery, username, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, iframe_embed_url, media_url_type, domain_override, third_party_tracking, third_party_tracking_2, is_managed, thumbnail_file=thumbnail_file) @validatedForm( VSponsor('link_id36'), VModhash(), VRatelimit(rate_user=True, rate_ip=True, prefix='create_promo_'), VShamedDomain('url'), username=VLength('username', 100, empty_error=None), title=VTitle('title'), url=VUrl('url', allow_self=False), selftext=VMarkdownLength('text', max_length=40000), kind=VOneOf('kind', ['link', 'self']), disable_comments=VBoolean("disable_comments"), sendreplies=VBoolean("sendreplies"), media_url=VUrl("media_url", allow_self=False, valid_schemes=('http', 'https')), gifts_embed_url=VUrl("gifts_embed_url", allow_self=False, valid_schemes=('http', 'https')), media_url_type=VOneOf("media_url_type", ("redditgifts", "scrape")), media_autoplay=VBoolean("media_autoplay"), media_override=VBoolean("media-override"), domain_override=VLength("domain", 100), third_party_tracking=VUrl("third_party_tracking"), third_party_tracking_2=VUrl("third_party_tracking_2"), is_managed=VBoolean("is_managed"), l=VLink('link_id36'), ) def POST_edit_promo(self, form, jquery, username, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, iframe_embed_url, media_url_type, domain_override, third_party_tracking, third_party_tracking_2, is_managed, l): return self._edit_promo(form, jquery, username, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, iframe_embed_url, media_url_type, domain_override, third_party_tracking, third_party_tracking_2, is_managed, l=l) def _edit_promo(self, form, jquery, username, title, url, selftext, kind, disable_comments, sendreplies, media_url, media_autoplay, media_override, iframe_embed_url, media_url_type, domain_override, third_party_tracking, third_party_tracking_2, is_managed, l=None, thumbnail_file=None): should_ratelimit = False is_self = kind == "self" is_link = not is_self if not c.user_is_sponsor: should_ratelimit = True if not should_ratelimit: c.errors.remove((errors.RATELIMIT, 'ratelimit')) # check for user override if not l and c.user_is_sponsor and username: try: user = Account._by_name(username) except NotFound: c.errors.add(errors.USER_DOESNT_EXIST, field="username") form.set_error(errors.USER_DOESNT_EXIST, "username") return if not user.email: c.errors.add(errors.NO_EMAIL_FOR_USER, field="username") form.set_error(errors.NO_EMAIL_FOR_USER, "username") return if not user.email_verified: c.errors.add(errors.NO_VERIFIED_EMAIL, field="username") form.set_error(errors.NO_VERIFIED_EMAIL, "username") return else: user = c.user # check for shame banned domains if form.has_errors("url", errors.DOMAIN_BANNED): g.stats.simple_event('spam.shame.link') return # demangle URL in canonical way if url: if isinstance(url, (unicode, str)): form.set_inputs(url=url) elif isinstance(url, tuple) or isinstance(url[0], Link): # there's already one or more links with this URL, but # we're allowing mutliple submissions, so we really just # want the URL url = url[0].url if kind == 'link': if form.has_errors('url', errors.NO_URL, errors.BAD_URL): return # users can change the disable_comments on promoted links if ((not l or not promote.is_promoted(l)) and (form.has_errors('title', errors.NO_TEXT, errors.TOO_LONG) or jquery.has_errors('ratelimit', errors.RATELIMIT))): return if kind == 'self' and form.has_errors('text', errors.TOO_LONG): return if not l: # creating a new promoted link l = promote.new_promotion(title, url if kind == 'link' else 'self', selftext if kind == 'self' else '', user, request.ip) if c.user_is_sponsor: l.managed_promo = is_managed l.domain_override = domain_override or None l.third_party_tracking = third_party_tracking or None l.third_party_tracking_2 = third_party_tracking_2 or None l._commit() # only set the thumbnail when creating a link if thumbnail_file: try: force_thumbnail(l, thumbnail_file) l._commit() except IOError: pass form.redirect(promote.promo_edit_url(l)) elif not promote.is_promo(l): return # changing link type is not allowed if ((l.is_self and kind == 'link') or (not l.is_self and kind == 'self')): c.errors.add(errors.NO_CHANGE_KIND, field="kind") form.set_error(errors.NO_CHANGE_KIND, "kind") return changed = False # live items can only be changed by a sponsor, and also # pay the cost of de-approving the link if not promote.is_promoted(l) or c.user_is_sponsor: if title and title != l.title: l.title = title changed = not c.user_is_sponsor if kind == 'link' and url and url != l.url: l.url = url changed = not c.user_is_sponsor # only trips if the title and url are changed by a non-sponsor if changed: promote.unapprove_promotion(l) # selftext can be changed at any time if kind == 'self': l.selftext = selftext # comment disabling and sendreplies is free to be changed any time. l.disable_comments = disable_comments l.sendreplies = sendreplies if c.user_is_sponsor: if (form.has_errors("media_url", errors.BAD_URL) or form.has_errors("gifts_embed_url", errors.BAD_URL)): return scraper_embed = media_url_type == "scrape" media_url = media_url or None gifts_embed_url = gifts_embed_url or None if c.user_is_sponsor and scraper_embed and media_url != l.media_url: if media_url: media = _scrape_media( media_url, autoplay=media_autoplay, save_thumbnail=False, use_cache=True) if media: l.set_media_object(media.media_object) l.set_secure_media_object(media.secure_media_object) l.media_url = media_url l.gifts_embed_url = None l.media_autoplay = media_autoplay else: c.errors.add(errors.SCRAPER_ERROR, field="media_url") form.set_error(errors.SCRAPER_ERROR, "media_url") return else: l.set_media_object(None) l.set_secure_media_object(None) l.media_url = None l.gifts_embed_url = None l.media_autoplay = False if (c.user_is_sponsor and not scraper_embed and gifts_embed_url != l.gifts_embed_url): if gifts_embed_url: parsed = UrlParser(gifts_embed_url) if not is_subdomain(parsed.hostname, "redditgifts.com"): c.errors.add(errors.BAD_URL, field="gifts_embed_url") form.set_error(errors.BAD_URL, "gifts_embed_url") return iframe = """ <iframe class="redditgifts-embed" src="%(embed_url)s" width="710" height="500" scrolling="no" frameborder="0" allowfullscreen> </iframe> """ % {'embed_url': websafe(gifts_embed_url)} media_object = { 'oembed': { 'description': 'redditgifts embed', 'height': 500, 'html': iframe, 'provider_name': 'redditgifts', 'provider_url': 'http://www.redditgifts.com/', 'title': 'redditgifts secret santa 2014', 'type': 'rich', 'width': 710}, 'type': 'redditgifts' } l.set_media_object(media_object) l.set_secure_media_object(media_object) l.media_url = None l.gifts_embed_url = gifts_embed_url l.media_autoplay = False else: l.set_media_object(None) l.set_secure_media_object(None) l.media_url = None l.gifts_embed_url = None l.media_autoplay = False if c.user_is_sponsor: l.media_override = media_override l.domain_override = domain_override or None l.third_party_tracking = third_party_tracking or None l.third_party_tracking_2 = third_party_tracking_2 or None l.managed_promo = is_managed l._commit() form.redirect(promote.promo_edit_url(l)) @validatedForm( VSponsorAdmin(), VModhash(), start=VDate('startdate'), end=VDate('enddate'), sr=VSubmitSR('sr', promotion=True), ) def POST_add_roadblock(self, form, jquery, start, end, sr): if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors('enddate', errors.BAD_DATE)): return if end < start: c.errors.add(errors.BAD_DATE_RANGE, field='enddate') form.has_errors('enddate', errors.BAD_DATE_RANGE) return if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED): return PromotedLinkRoadblock.add(sr, start, end) jquery.refresh() @validatedForm( VSponsorAdmin(), VModhash(), start=VDate('startdate'), end=VDate('enddate'), sr=VSubmitSR('sr', promotion=True), ) def POST_rm_roadblock(self, form, jquery, start, end, sr): if end < start: c.errors.add(errors.BAD_DATE_RANGE, field='enddate') form.has_errors('enddate', errors.BAD_DATE_RANGE) return if start and end and sr: PromotedLinkRoadblock.remove(sr, start, end) jquery.refresh() @validatedForm( VSponsor('link_id36'), VModhash(), start=VDate('startdate'), end=VDate('enddate'), link=VLink('link_id36'), bid=VFloat('bid', coerce=False), target=VPromoTarget(), campaign_id36=nop("campaign_id36"), priority=VPriority("priority"), location=VLocation(), platform=VOneOf("platform", ("mobile", "desktop", "all"), default="desktop"), mobile_os=VList("mobile_os", choices=["iOS", "Android"]), ) def POST_edit_campaign(self, form, jquery, link, campaign_id36, start, end, bid, target, priority, location, platform, mobile_os): if not link: return if platform in ('mobile', 'all') and not mobile_os: c.errors.add(errors.BAD_PROMO_MOBILE_OS, field='mobile_os') form.set_error(errors.BAD_PROMO_MOBILE_OS, 'mobile_os') return if platform == 'mobile' and priority.cpm: c.errors.add(errors.BAD_PROMO_MOBILE_PRIORITY, field='priority') form.set_error(errors.BAD_PROMO_MOBILE_PRIORITY, 'priority') return if not (c.user_is_sponsor or platform == 'desktop'): return abort(403, 'forbidden') if platform == 'desktop': mobile_os = None if not target: # run form.has_errors to populate the errors in the response form.has_errors('sr', errors.SUBREDDIT_NOEXIST, errors.SUBREDDIT_NOTALLOWED, errors.SUBREDDIT_REQUIRED) form.has_errors('collection', errors.COLLECTION_NOEXIST) form.has_errors('targeting', errors.INVALID_TARGET) return if not allowed_location_and_target(location, target): return abort(403, 'forbidden') cpm = PromotionPrices.get_price(c.user, target, location) if (form.has_errors('startdate', errors.BAD_DATE) or form.has_errors('enddate', errors.BAD_DATE)): return min_start, max_start, max_end = promote.get_date_limits( link, c.user_is_sponsor) if campaign_id36: promo_campaign = PromoCampaign._byID36(campaign_id36) if (promote.is_promoted(link) and promo_campaign.start_date.date() <= min_start and start != promo_campaign.start_date and promo_campaign.is_paid): c.errors.add(errors.START_DATE_CANNOT_CHANGE, field='startdate') form.has_errors('startdate', errors.START_DATE_CANNOT_CHANGE) return elif start.date() < min_start: c.errors.add(errors.DATE_TOO_EARLY, msg_params={'day': min_start.strftime("%m/%d/%Y")}, field='startdate') form.has_errors('startdate', errors.DATE_TOO_EARLY) return if start.date() > max_start: c.errors.add(errors.DATE_TOO_LATE, msg_params={'day': max_start.strftime("%m/%d/%Y")}, field='startdate') form.has_errors('startdate', errors.DATE_TOO_LATE) return if end.date() > max_end: c.errors.add(errors.DATE_TOO_LATE, msg_params={'day': max_end.strftime("%m/%d/%Y")}, field='enddate') form.has_errors('enddate', errors.DATE_TOO_LATE) return if end < start: c.errors.add(errors.BAD_DATE_RANGE, field='enddate') form.has_errors('enddate', errors.BAD_DATE_RANGE) return # Limit the number of PromoCampaigns a Link can have # Note that the front end should prevent the user from getting # this far existing_campaigns = list(PromoCampaign._by_link(link._id)) if len(existing_campaigns) > g.MAX_CAMPAIGNS_PER_LINK: c.errors.add(errors.TOO_MANY_CAMPAIGNS, msg_params={'count': g.MAX_CAMPAIGNS_PER_LINK}, field='title') form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return campaign = None if campaign_id36: try: campaign = PromoCampaign._byID36(campaign_id36, data=True) except NotFound: pass if campaign and (campaign._deleted or link._id != campaign.link_id): campaign = None if not campaign: return abort(404, 'not found') if priority.cpm: min_bid = 0 if c.user_is_sponsor else g.min_promote_bid max_bid = None if c.user_is_sponsor else g.max_promote_bid if bid is None or bid < min_bid or (max_bid and bid > max_bid): c.errors.add(errors.BAD_BID, field='bid', msg_params={'min': min_bid, 'max': max_bid or g.max_promote_bid}) form.has_errors('bid', errors.BAD_BID) return # you cannot edit the bid of a live ad unless it's a freebie if (campaign and bid != campaign.bid and promote.is_live_promo(link, campaign) and not campaign.is_freebie()): c.errors.add(errors.BID_LIVE, field='bid') form.has_errors('bid', errors.BID_LIVE) return else: bid = 0. # Set bid to 0 as dummy value is_frontpage = (not target.is_collection and target.subreddit_name == Frontpage.name) if not target.is_collection and not is_frontpage: # targeted to a single subreddit, check roadblock sr = target.subreddits_slow[0] roadblock = PromotedLinkRoadblock.is_roadblocked(sr, start, end) if roadblock and not c.user_is_sponsor: msg_params = {"start": roadblock[0].strftime('%m/%d/%Y'), "end": roadblock[1].strftime('%m/%d/%Y')} c.errors.add(errors.OVERSOLD, field='sr', msg_params=msg_params) form.has_errors('sr', errors.OVERSOLD) return # Check inventory campaign = campaign if campaign_id36 else None if not priority.inventory_override: oversold = has_oversold_error(form, campaign, start, end, bid, cpm, target, location) if oversold: return dates = (start, end) if campaign: promote.edit_campaign(link, campaign, dates, bid, cpm, target, priority, location, platform, mobile_os) else: campaign = promote.new_campaign(link, dates, bid, cpm, target, priority, location, platform, mobile_os) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm(VSponsor('link_id36'), VModhash(), l=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_delete_campaign(self, form, jquery, l, campaign): if not campaign or not l or l._id != campaign.link_id: return abort(404, 'not found') promote.delete_campaign(l, campaign) @validatedForm(VSponsorAdmin(), VModhash(), link=VLink('link_id36'), campaign=VPromoCampaign("campaign_id36")) def POST_terminate_campaign(self, form, jquery, link, campaign): if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') promote.terminate_campaign(link, campaign) rc = RenderableCampaign.from_campaigns(link, campaign) jquery.update_campaign(campaign._fullname, rc.render_html()) @validatedForm( VSponsor('link'), VModhash(), link=VByName("link"), campaign=VPromoCampaign("campaign"), customer_id=VInt("customer_id", min=0), pay_id=VInt("account", min=0), edit=VBoolean("edit"), address=ValidAddress( ["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"] ), creditcard=ValidCard(["cardNumber", "expirationDate", "cardCode"]), ) def POST_update_pay(self, form, jquery, link, campaign, customer_id, pay_id, edit, address, creditcard): if not g.authorizenetapi: return if not link or not campaign or link._id != campaign.link_id: return abort(404, 'not found') # Check inventory if campaign_has_oversold_error(form, campaign): return # check the campaign dates are still valid (user may have created # the campaign a few days ago) min_start, max_start, max_end = promote.get_date_limits( link, c.user_is_sponsor) if campaign.start_date.date() > max_start: msg = _("please change campaign start date to %(date)s or earlier") date = format_date(max_start, format="short", locale=c.locale) msg %= {'date': date} form.set_text(".status", msg) return if campaign.start_date.date() < min_start: msg = _("please change campaign start date to %(date)s or later") date = format_date(min_start, format="short", locale=c.locale) msg %= {'date': date} form.set_text(".status", msg) return new_payment = not pay_id address_modified = new_payment or edit if address_modified: address_fields = ["firstName", "lastName", "company", "address", "city", "state", "zip", "country", "phoneNumber"] card_fields = ["cardNumber", "expirationDate", "cardCode"] if (form.has_errors(address_fields, errors.BAD_ADDRESS) or form.has_errors(card_fields, errors.BAD_CARD)): return pay_id = edit_profile(c.user, address, creditcard, pay_id) if pay_id: promote.new_payment_method(user=c.user, ip=request.ip, address=address, link=link) if pay_id: success, reason = promote.auth_campaign(link, campaign, c.user, pay_id) if success: hooks.get_hook("promote.campaign_paid").call(link=link, campaign=campaign) if not address and g.authorizenetapi: profiles = get_account_info(c.user).paymentProfiles profile = {p.customerPaymentProfileId: p for p in profiles}[pay_id] address = profile.billTo promote.successful_payment(link, campaign, request.ip, address) jquery.payment_redirect(promote.promo_edit_url(link), new_payment, campaign.bid) return else: promote.failed_payment_method(c.user, link) msg = reason or _("failed to authenticate card. sorry.") form.set_text(".status", msg) else: promote.failed_payment_method(c.user, link) form.set_text(".status", _("failed to authenticate card. sorry.")) @validate( VSponsor("link_name"), VModhash(), link=VByName('link_name'), file=VUploadLength('file', 500*1024), img_type=VImageType('img_type'), ) def POST_link_thumb(self, link=None, file=None, img_type='jpg'): if not link or (promote.is_promoted(link) and not c.user_is_sponsor): # only let sponsors edit thumbnails of live promos return abort(403, 'forbidden') force_thumbnail(link, file, file_type=".%s" % img_type) link._commit() return UploadedImage(_('saved'), thumbnail_url(link), "", errors=errors, form_id="image-upload").render() @validate( VSponsor("link_name"), VModhash(), link=VByName('link_name'), file=VUploadLength('file', 500*1024), img_type=VImageType('img_type'), ) def POST_link_mobile_ad_image(self, link=None, file=None, img_type='jpg'): if not (link and c.user_is_sponsor and file): # only sponsors can set the mobile img return abort(403, 'forbidden') force_mobile_ad_image(link, file, file_type=".%s" % img_type) link._commit() return UploadedImage(_('saved'), link.mobile_ad_url, "", errors=errors, form_id="mobile-ad-image-upload").render()
class ModmailController(OAuth2OnlyController): def pre(self): # Set user_is_admin property on context, # normally set but this controller does not inherit # from RedditController super(ModmailController, self).pre() admin_usernames = [ name.lower() for name in g.live_config['modmail_admins'] ] c.user_is_admin = False if c.user_is_loggedin: c.user_is_admin = c.user.name.lower() in admin_usernames VNotInTimeout().run() def post(self): Session.remove() super(ModmailController, self).post() @require_oauth2_scope('modmail') @validate( srs=VSRByNames('entity', required=False), after=VModConversation('after', required=False), limit=VInt('limit', num_default=25), sort=VOneOf('sort', options=('recent', 'mod', 'user'), default='recent'), state=VOneOf('state', options=('new', 'inprogress', 'mod', 'notifications', 'archived', 'highlighted', 'all'), default='all'), ) def GET_conversations(self, srs, after, limit, sort, state): """Get conversations for logged in user or subreddits Querystring Params: entity -- name of the subreddit or a comma separated list of subreddit names (i.e. iama, pics etc) limit -- number of elements to retrieve (default: 25) after -- the id of the last item seen sort -- parameter on how to sort the results, choices: recent: max(last_user_update, last_mod_update) mod: last_mod_update user: last_user_update state -- this parameter lets users filter messages by state choices: new, inprogress, mod, notifications, archived, highlighted, all """ # Retrieve subreddits in question, if entities are passed # check if a user is a moderator for the passed entities. # If no entities are passed grab all subreddits the logged in # user moderates and has modmail permissions for modded_entities = {} modded_srs = c.user.moderated_subreddits('mail') modded_srs = {sr._fullname: sr for sr in modded_srs} if srs: for sr in srs.values(): if sr._fullname in modded_srs: modded_entities[sr._fullname] = sr else: return self.send_error(403, errors.BAD_SR_NAME, fields='entity') else: modded_entities = modded_srs if not modded_entities: return self.send_error(404, errors.SR_NOT_FOUND, fields='entity') # Retrieve conversations for given entities conversations = ModmailConversation.get_mod_conversations( modded_entities.values(), viewer=c.user, limit=limit, after=after, sort=sort, state=state) conversation_ids = [] conversations_dict = {} messages_dict = {} author_ids = [] # Extract author ids to query for all accounts at once for conversation in conversations: author_ids.extend(conversation.author_ids) author_ids.extend(conversation.mod_action_account_ids) # Query for associated account object of authors and serialize the # conversation in the correct context authors = self._try_get_byID(author_ids, Account, ignore_missing=True) for conversation in conversations: conversation_ids.append(conversation.id36) conversations_dict[ conversation.id36] = conversation.to_serializable( authors, modded_entities[conversation.owner_fullname], ) latest_message = conversation.messages[0] messages_dict[ latest_message.id36] = latest_message.to_serializable( modded_entities[conversation.owner_fullname], authors[latest_message.author_id], c.user, ) return simplejson.dumps({ 'viewerId': c.user._fullname, 'conversationIds': conversation_ids, 'conversations': conversations_dict, 'messages': messages_dict, }) @require_oauth2_scope('modmail') @validate( entity=VSRByName('srName'), subject=VLength('subject', max_length=100), body=VMarkdownLength('body'), is_author_hidden=VBoolean('isAuthorHidden', default=False), to=VModConvoRecipient('to', required=False), ) def POST_conversations(self, entity, subject, body, is_author_hidden, to): """Creates a new conversation for a particular SR This endpoint will create a ModmailConversation object as well as the first ModmailMessage within the ModmailConversation object. POST Params: srName -- the human readable name of the subreddit subject -- the subject of the first message in the conversation body -- the body of the first message in the conversation isAuthorHidden -- boolean on whether the mod name should be hidden (only mods can use this flag) to -- name of the user that a mod wants to create a convo with (only mods can use this flag) """ self._feature_enabled_check(entity) # make sure the user is not muted when creating a new conversation if entity.is_muted(c.user) and not c.user_is_admin: return self.send_error(400, errors.USER_MUTED) # validate post params if (errors.USER_BLOCKED, to) in c.errors: return self.send_error(400, errors.USER_BLOCKED, fields='to') elif (errors.USER_DOESNT_EXIST, to) in c.errors: return self.send_error(404, errors.USER_DOESNT_EXIST, fields='to') if to and not isinstance(to, Account): return self.send_error( 422, errors.NO_SR_TO_SR_MESSAGE, fields='to', ) # only mods can set a 'to' parameter if (not entity.is_moderator_with_perms(c.user, 'mail') and to): return self.send_error(403, errors.MOD_REQUIRED, fields='to') if to and entity.is_muted(to): return self.send_error( 400, errors.MUTED_FROM_SUBREDDIT, fields='to', ) try: conversation = ModmailConversation( entity, c.user, subject, body, is_author_hidden=is_author_hidden, to=to, ) except MustBeAModError: return self.send_error(403, errors.MOD_REQUIRED, fields='isAuthorHidden') except Exception as e: g.log.error('Failed to save conversation: {}'.format(e)) return self.send_error(500, errors.CONVERSATION_NOT_SAVED) # Create copy of the message in the legacy messaging system as well if to: message, inbox_rel = Message._new( c.user, to, subject, body, request.ip, sr=entity, from_sr=is_author_hidden, create_modmail=False, ) else: message, inbox_rel = Message._new( c.user, entity, subject, body, request.ip, create_modmail=False, ) queries.new_message(message, inbox_rel) conversation.set_legacy_first_message_id(message._id) # Get author associated account object for serialization # of the newly created conversation object authors = self._try_get_byID(conversation.author_ids, Account, ignore_missing=True) response.status_code = 201 serializable_convo = conversation.to_serializable(authors, entity, all_messages=True, current_user=c.user) messages = serializable_convo.pop('messages') mod_actions = serializable_convo.pop('modActions') g.events.new_modmail_event( 'ss.send_modmail_message', conversation, message=conversation.messages[0], msg_author=c.user, sr=entity, request=request, context=c, ) return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, 'modActions': mod_actions, }) @require_oauth2_scope('modmail') @validate( conversation=VModConversation('conversation_id'), mark_read=VBoolean('markRead', default=False), ) def GET_mod_messages(self, conversation, mark_read): """Returns all messages for a given conversation id Url Params: conversation_id -- this is the id of the conversation you would like to grab messages for Querystring Param: markRead -- if passed the conversation will be marked read when the conversation is returned """ self._validate_vmodconversation() sr = self._try_get_subreddit_access(conversation, admin_override=True) authors = self._try_get_byID(list( set(conversation.author_ids) | set(conversation.mod_action_account_ids)), Account, ignore_missing=True) serializable_convo = conversation.to_serializable(authors, sr, all_messages=True, current_user=c.user) messages = serializable_convo.pop('messages') mod_actions = serializable_convo.pop('modActions') # Get participant user info for conversation try: userinfo = self._get_modmail_userinfo(conversation, sr=sr) except ValueError: userinfo = {} except NotFound: return self.send_error(404, errors.USER_DOESNT_EXIST) if mark_read: conversation.mark_read(c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='read', request=request, context=c, ) return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, 'modActions': mod_actions, 'user': userinfo, }) @require_oauth2_scope('modmail') def GET_modmail_enabled_srs(self): # sr_name, sr_icon, subsriber_count, most_recent_action modded_srs = c.user.moderated_subreddits('mail') enabled_srs = [ modded_sr for modded_sr in modded_srs if feature.is_enabled('new_modmail', subreddit=modded_sr.name) ] recent_convos = ModmailConversation.get_recent_convo_by_sr(enabled_srs) results = {} for sr in enabled_srs: results.update({ sr._fullname: { 'id': sr._fullname, 'name': sr.name, 'icon': sr.icon_img, 'subscribers': sr._ups, 'lastUpdated': recent_convos.get(sr._fullname), } }) return simplejson.dumps({'subreddits': results}) @require_oauth2_scope('modmail') @validate( conversation=VModConversation('conversation_id'), msg_body=VMarkdownLength('body'), is_author_hidden=VBoolean('isAuthorHidden', default=False), is_internal=VBoolean('isInternal', default=False), ) def POST_mod_messages(self, conversation, msg_body, is_author_hidden, is_internal): """Creates a new message for a particular ModmailConversation URL Params: conversation_id -- id of the conversation to post a new message to POST Params: body -- this is the message body isAuthorHidden -- boolean on whether to hide author, i.e. respond as the subreddit isInternal -- boolean to signify a moderator only message """ self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) # make sure the user is not muted before posting a message if sr.is_muted(c.user): return self.send_error(400, errors.USER_MUTED) if conversation.is_internal and not is_internal: is_internal = True is_mod = sr.is_moderator(c.user) if not is_mod and is_author_hidden: return self.send_error( 403, errors.MOD_REQUIRED, fields='isAuthorHidden', ) elif not is_mod and is_internal: return self.send_error( 403, errors.MOD_REQUIRED, fields='isInternal', ) try: if not conversation.is_internal and not conversation.is_auto: participant = conversation.get_participant_account() if participant and sr.is_muted(participant): return self.send_error( 400, errors.MUTED_FROM_SUBREDDIT, ) except NotFound: pass try: new_message = conversation.add_message( c.user, msg_body, is_author_hidden=is_author_hidden, is_internal=is_internal, ) except: return self.send_error(500, errors.MODMAIL_MESSAGE_NOT_SAVED) # Add the message to the legacy messaging system as well (unless it's # an internal message on a non-internal conversation, since we have no # way to hide specific messages from the external participant) legacy_incompatible = is_internal and not conversation.is_internal if (conversation.legacy_first_message_id and not legacy_incompatible): first_message = Message._byID(conversation.legacy_first_message_id) subject = conversation.subject if not subject.startswith('re: '): subject = 're: ' + subject # Retrieve the participant to decide whether to send the message # to the sr or to the participant. If the currently logged in user # is the same as the participant then address the message to the # sr. recipient = sr if not is_internal: try: participant = ( ModmailConversationParticipant.get_participant( conversation.id)) is_participant = ( (c.user._id == participant.account_id) and not sr.is_moderator_with_perms(c.user, 'mail')) if not is_participant: recipient = Account._byID(participant.account_id) except NotFound: pass message, inbox_rel = Message._new( c.user, recipient, subject, msg_body, request.ip, parent=first_message, from_sr=is_author_hidden, create_modmail=False, ) queries.new_message(message, inbox_rel) serializable_convo = conversation.to_serializable( entity=sr, all_messages=True, current_user=c.user, ) messages = serializable_convo.pop('messages') g.events.new_modmail_event( 'ss.send_modmail_message', conversation, message=new_message, msg_author=c.user, sr=sr, request=request, context=c, ) response.status_code = 201 return simplejson.dumps({ 'conversation': serializable_convo, 'messages': messages, }) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_highlight(self, conversation): """Marks a conversation as highlighted.""" self._validate_vmodconversation() self._try_get_subreddit_access(conversation) conversation.add_action(c.user, 'highlighted') conversation.add_highlight() # Retrieve updated conversation to be returned updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='highlight', request=request, context=c, ) return simplejson.dumps(updated_convo) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def DELETE_highlight(self, conversation): """Removes a highlight from a conversation.""" self._validate_vmodconversation() self._try_get_subreddit_access(conversation) conversation.add_action(c.user, 'unhighlighted') conversation.remove_highlight() # Retrieve updated conversation to be returned updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='unhighlight', request=request, context=c, ) return simplejson.dumps(updated_convo) @require_oauth2_scope('modmail') @validate(ids=VList('conversationIds')) def POST_unread(self, ids): """Marks conversations as unread for the user. Expects a list of conversation IDs. """ if not ids: return self.send_error(400, 'Must pass an id or list of ids.') try: ids = [int(id, base=36) for id in ids] except: return self.send_error(422, 'Must pass base 36 ids.') try: convos = self._get_conversation_access(ids) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) ModmailConversationUnreadState.mark_unread( c.user, [convo.id for convo in convos]) @require_oauth2_scope('modmail') @validate(ids=VList('conversationIds')) def POST_read(self, ids): """Marks a conversations as read for the user. Expects a list of conversation IDs. """ if not ids: return self.send_error(400, 'Must pass an id or list of ids.') try: ids = [int(id, base=36) for id in ids] except: return self.send_error(422, 'Must pass base 36 ids.') try: convos = self._get_conversation_access(ids) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) response.status_code = 204 ModmailConversationUnreadState.mark_read( c.user, [convo.id for convo in convos]) @require_oauth2_scope('modmail') @validate( ids=VList('conversationIds'), archive=VBoolean('archive', default=True), ) def POST_archive_status(self, ids, archive): try: convos = self._get_conversation_access( [int(id, base=36) for id in ids]) except ValueError: return self.send_error( 403, errors.INVALID_CONVERSATION_ID, fields='conversationIds', ) convo_ids = [] for convo in convos: if convo.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, fields='conversationIds', ) convo_ids.append(convo.id) if not archive: ModmailConversation.set_states( convo_ids, ModmailConversation.STATE['inprogress']) else: ModmailConversation.set_states( convo_ids, ModmailConversation.STATE['archived']) response.status_code = 204 @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_archive(self, conversation): self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if sr.is_moderator_with_perms(c.user, 'mail'): if conversation.state == ModmailConversation.STATE['archived']: response.status_code = 204 return if conversation.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, ) conversation.add_action(c.user, 'archived') conversation.set_state('archived') updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='archive', request=request, context=c, ) return simplejson.dumps(updated_convo) else: return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def POST_unarchive(self, conversation): self._validate_vmodconversation() sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if sr.is_moderator_with_perms(c.user, 'mail'): if conversation.state != ModmailConversation.STATE['archived']: response.status_code = 204 return if conversation.is_internal: return self.send_error( 422, errors.CONVERSATION_NOT_ARCHIVABLE, ) conversation.add_action(c.user, 'unarchived') conversation.set_state('inprogress') updated_convo = self._get_updated_convo(conversation.id, c.user) g.events.new_modmail_event( 'ss.modmail_mark_thread', conversation, mark_type='unarchive', request=request, context=c, ) return simplejson.dumps(updated_convo) else: return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) @require_oauth2_scope('modmail') def GET_unread_convo_count(self): """Endpoint to retrieve the unread conversation count by category""" convo_counts = ModmailConversation.unread_convo_count(c.user) return simplejson.dumps(convo_counts) @require_oauth2_scope('modmail') @validate(conversation=VModConversation('conversation_id')) def GET_modmail_userinfo(self, conversation): # validate that the currently logged in user is a mod # of the subreddit associated with the conversation self._try_get_subreddit_access(conversation, admin_override=True) try: userinfo = self._get_modmail_userinfo(conversation) except (ValueError, NotFound): return self.send_error(404, errors.USER_DOESNT_EXIST) return simplejson.dumps(userinfo) @require_oauth2_scope('identity') @validate(conversation=VModConversation('conversation_id')) def POST_mute_participant(self, conversation): if conversation.is_internal or conversation.is_auto: return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) sr = Subreddit._by_fullname(conversation.owner_fullname) try: participant = conversation.get_participant_account() except NotFound: return self.send_error(404, errors.USER_DOESNT_EXIST) if not sr.can_mute(c.user, participant): return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) if not c.user_is_admin: if not sr.is_moderator_with_perms(c.user, 'access', 'mail'): return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) if sr.use_quotas: sr_ratelimit = SimpleRateLimit( name="sr_muted_%s" % sr._id36, seconds=g.sr_quota_time, limit=g.sr_muted_quota, ) if not sr_ratelimit.record_and_check(): return self.send_error(403, errors.SUBREDDIT_RATELIMIT) # Add the mute record but only if successful create the # appropriate notifications, this prevents duplicate # notifications from being sent added = sr.add_muted(participant) if not added: return simplejson.dumps( self._convo_to_serializable(conversation, all_messages=True)) MutedAccountsBySubreddit.mute(sr, participant, c.user) permalink = conversation.make_permalink() # Create the appropriate objects to be displayed on the # mute moderation log, use the permalink to the new modmail # system ModAction.create(sr, c.user, 'muteuser', target=participant, description=permalink) sr.add_rel_note('muted', participant, permalink) # Add the muted mod action to the conversation conversation.add_action(c.user, 'muted', commit=True) result = self._get_updated_convo(conversation.id, c.user) result['user'] = self._get_modmail_userinfo(conversation, sr=sr) return simplejson.dumps(result) @require_oauth2_scope('identity') @validate(conversation=VModConversation('conversation_id')) def POST_unmute_participant(self, conversation): if conversation.is_internal or conversation.is_auto: return self.send_error(400, errors.CANT_RESTRICT_MODERATOR) sr = Subreddit._by_fullname(conversation.owner_fullname) try: participant = conversation.get_participant_account() except NotFound: abort(404, errors.USER_DOESNT_EXIST) if not c.user_is_admin: if not sr.is_moderator_with_perms(c.user, 'access', 'mail'): return self.send_error(403, errors.INVALID_MOD_PERMISSIONS) removed = sr.remove_muted(participant) if not removed: return simplejson.dumps( self._convo_to_serializable(conversation, all_messages=True)) MutedAccountsBySubreddit.unmute(sr, participant) ModAction.create(sr, c.user, 'unmuteuser', target=participant) conversation.add_action(c.user, 'unmuted', commit=True) result = self._get_updated_convo(conversation.id, c.user) result['user'] = self._get_modmail_userinfo(conversation, sr=sr) return simplejson.dumps(result) def _get_modmail_userinfo(self, conversation, sr=None): if conversation.is_internal: raise ValueError('Cannot get userinfo for internal conversations') if not sr: sr = Subreddit._by_fullname(conversation.owner_fullname) # Retrieve the participant associated with the conversation try: account = conversation.get_participant_account() if not account: raise ValueError('No account associated with convo') permatimeout = (account.in_timeout and account.days_remaining_in_timeout == 0) if account._deleted or permatimeout: raise ValueError('User info is inaccessible') except NotFound: raise NotFound('Unable to retrieve conversation participant') # Fetch the mute and ban status of the participant as it relates # to the subreddit associated with the conversation. mute_status = sr.is_muted(account) ban_status = sr.is_banned(account) # Parse the ban status and retrieve the length of the ban, # then output the data into a serialiazable dict ban_result = { 'isBanned': bool(ban_status), 'reason': '', 'endDate': None, 'isPermanent': False } if ban_status: ban_result['reason'] = getattr(ban_status, 'note', '') ban_duration = sr.get_tempbans('banned', account.name) ban_duration = ban_duration.get(account.name) if ban_duration: ban_result['endDate'] = ban_duration.isoformat() else: ban_result['isPermanent'] = True ban_result['endDate'] = None # Parse the mute status and retrieve the length of the ban, # then output the data into the serialiazable dict mute_result = { 'isMuted': bool(mute_status), 'endDate': None, 'reason': '' } if mute_status: mute_result['reason'] = getattr(mute_status, 'note', '') muted_items = sr.get_muted_items(account.name) mute_duration = muted_items.get(account.name) if mute_duration: mute_result['endDate'] = mute_duration.isoformat() # Retrieve the participants post and comment fullnames from cache post_fullnames = [] comment_fullnames = [] if not account._spam: post_fullnames = list(queries.get_submitted(account, 'new', 'all'))[:100] comment_fullnames = list( queries.get_comments(account, 'new', 'all'))[:100] # Retrieve the associated link objects for posts and comments # using the retrieve fullnames, afer the link objects are retrieved # create a serializable dict with the the necessary information from # the endpoint. lookup_fullnames = list(set(post_fullnames) | set(comment_fullnames)) posts = Thing._by_fullname(lookup_fullnames) serializable_posts = {} for fullname in post_fullnames: if len(serializable_posts) == 3: break post = posts[fullname] if post.sr_id == sr._id and not post._deleted: serializable_posts[fullname] = { 'title': post.title, 'permalink': post.make_permalink(sr, force_domain=True), 'date': post._date.isoformat(), } # Extract the users most recent comments associated with the # subreddit sr_comments = [] for fullname in comment_fullnames: if len(sr_comments) == 3: break comment = posts[fullname] if comment.sr_id == sr._id and not comment._deleted: sr_comments.append(comment) # Retrieve all associated link objects (combines lookup) comment_links = Link._byID( [sr_comment.link_id for sr_comment in sr_comments]) # Serialize all of the user's sr comments serializable_comments = {} for sr_comment in sr_comments: comment_link = comment_links[sr_comment.link_id] comment_body = sr_comment.body if len(comment_body) > 140: comment_body = '{:.140}...'.format(comment_body) serializable_comments[sr_comment._fullname] = { 'title': comment_link.title, 'comment': comment_body, 'permalink': sr_comment.make_permalink(comment_link, sr, force_domain=True), 'date': sr_comment._date.isoformat(), } return { 'id': account._fullname, 'name': account.name, 'created': account._date.isoformat(), 'banStatus': ban_result, 'isShadowBanned': account._spam, 'muteStatus': mute_result, 'recentComments': serializable_comments, 'recentPosts': serializable_posts, } def _get_updated_convo(self, convo_id, user): # Retrieve updated conversation to be returned updated_convo = ModmailConversation._byID(convo_id, current_user=user) return self._convo_to_serializable(updated_convo, all_messages=True) def _convo_to_serializable(self, conversation, all_messages=False): serialized_convo = conversation.to_serializable( all_messages=all_messages, current_user=c.user) messages = serialized_convo.pop('messages') mod_actions = serialized_convo.pop('modActions') return { 'conversations': serialized_convo, 'messages': messages, 'modActions': mod_actions, } def _validate_vmodconversation(self): if (errors.CONVERSATION_NOT_FOUND, 'conversation_id') in c.errors: return self.send_error(404, errors.CONVERSATION_NOT_FOUND) def _get_conversation_access(self, ids): validated_convos = [] conversations = ModmailConversation._byID(ids) # fetch all srs that a user has modmail permissions to # transform sr to be a dict with a key being the sr fullname # and the value being the sr object itself modded_srs = c.user.moderated_subreddits('mail') sr_by_fullname = { sr._fullname: sr for sr in modded_srs if feature.is_enabled('new_modmail', subreddit=sr.name) } for conversation in tup(conversations): if sr_by_fullname.get(conversation.owner_fullname): validated_convos.append(conversation) else: raise ValueError('Invalid conversation id(s).') return validated_convos def _try_get_byID(self, ids, thing_class, return_dict=True, ignore_missing=False): """Helper method to lookup objects by id for a given model or return a 404 if not found""" try: return thing_class._byID(ids, return_dict=return_dict, ignore_missing=ignore_missing) except NotFound: return self.send_error(404, errors.THING_NOT_FOUND, explanation='{} not found'.format( thing_class.__name__)) except: return self.send_error(422, 'Invalid request') def _try_get_subreddit_access(self, conversation, admin_override=False): sr = Subreddit._by_fullname(conversation.owner_fullname) self._feature_enabled_check(sr) if (not sr.is_moderator_with_perms(c.user, 'mail') and not (admin_override and c.user_is_admin)): return self.send_error( 403, errors.SUBREDDIT_NO_ACCESS, ) return sr def _feature_enabled_check(self, sr): if not feature.is_enabled('new_modmail', subreddit=sr.name): return self.send_error(403, errors.SR_FEATURE_NOT_ENABLED) def send_error(self, code, error, fields=None, explanation=None): abort( reddit_http_error( code=code or error.code, error_name=error, explanation=explanation, fields=tup(fields), ))
class 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))