Skip to content


Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
1696 lines (1424 sloc) 63.247 kb
# -*- coding: utf-8 -*-
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
# The Original Code is reddit.
# The Original Developer is the Initial Developer. The Initial Developer of
# the Original Code is reddit Inc.
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
import urllib
from oauth2 import require_oauth2_scope
from reddit_base import RedditController, base_listing, paginated_listing
from r2.models import *
from r2.models.query_cache import CachedQuery, MergedCachedQuery
from r2.config.extensions import is_api
from r2.lib.filters import _force_unicode
from r2.lib.pages import *
from r2.lib.pages.things import wrap_links
from r2.lib.menus import TimeMenu, SortMenu, RecSortMenu, ProfileSortMenu
from r2.lib.menus import ControversyTimeMenu, menu, QueryButton
from r2.lib.rising import get_rising, normalized_rising
from r2.lib.wrapped import Wrapped
from r2.lib.normalized_hot import normalized_hot
from r2.lib.db.thing import Query, Merge, Relations
from r2.lib.db import queries
from r2.lib.strings import Score
import as search
from r2.lib.template_helpers import add_sr
from r2.lib.admin_utils import check_cheating
from r2.lib.csrf import csrf_exempt
from r2.lib.utils import (
from r2.lib import hooks, organic, sup, trending
from r2.lib.memoize import memoize
from r2.lib.validator import *
import socket
from api_docs import api_doc, api_section
from pylons.i18n import _
from datetime import timedelta
import random
from functools import partial
class ListingController(RedditController):
"""Generalized controller for pages with lists of links."""
# toggle skipping of links based on the users' save/hide/vote preferences
skip = True
# allow stylesheets on listings
allow_stylesheets = True
# toggle sending origin-only referrer headers on cross-domain navigation
private_referrer = True
# toggles showing numbers
show_nums = True
# any text that should be shown on the top of the page
infotext = None
infotext_class = None
# builder class to use to generate the listing. if none, we'll try
# to figure it out based on the query type
builder_cls = None
# page title
title_text = ''
# login box, subreddit box, submit box, etc, visible
show_sidebar = True
show_chooser = False
suppress_reply_buttons = False
# class (probably a subclass of Reddit) to use to render the page.
render_cls = Reddit
# class for suggestions next to "next/prev" buttons
next_suggestions_cls = None
#extra parameters to send to the render_cls constructor
render_params = {}
extra_page_classes = ['listing-page']
def menus(self):
"""list of menus underneat the header (e.g., sort, time, kind,
etc) to be displayed on this listing page"""
return []
def can_send_referrer(self):
"""Return whether links within this listing may have full referrers"""
if not self.private_referrer:
return False
def build_listing(self, num, after, reverse, count, **kwargs):
"""uses the query() method to define the contents of the
listing and renders the page self.render_cls(..).render() with
the listing as contents"""
self.num = num
self.count = count
self.after = after
self.reverse = reverse
self.query_obj = self.query()
self.builder_obj = self.builder()
# Don't leak info about things the user can't view
if after and not self.builder_obj.valid_after(after):
listing_name = self.__class__.__name__
g.stats.event_count("listing.invalid_after", listing_name)
self.listing_obj = self.listing()
content = self.content()
return self.render_cls(content=content,
robots=getattr(self, "robots", None),
def content(self):
"""Renderable object which will end up as content of the render_cls"""
return self.listing_obj
def query(self):
"""Query to execute to generate the listing"""
raise NotImplementedError
def builder(self):
#store the query itself so it can be used elsewhere
if self.builder_cls:
builder_cls = self.builder_cls
elif isinstance(self.query_obj, Query):
builder_cls = QueryBuilder
elif isinstance(self.query_obj, search.SearchQuery):
builder_cls = SearchBuilder
elif isinstance(self.query_obj, iters):
builder_cls = IDBuilder
elif isinstance(self.query_obj, (queries.CachedResults, queries.MergedCachedResults)):
builder_cls = IDBuilder
elif isinstance(self.query_obj, (CachedQuery, MergedCachedQuery)):
builder_cls = IDBuilder
b = builder_cls(self.query_obj,
num = self.num,
skip = self.skip,
after = self.after,
count = self.count,
reverse = self.reverse,
keep_fn = self.keep_fn(),
wrap = self.builder_wrapper)
return b
def keep_fn(self):
def keep(item):
wouldkeep = item.keep_item(item)
if isinstance(, AllSR):
if not item.subreddit.allow_top:
return False
if getattr(item, "promoted", None) is not None:
return False
if item._deleted and not c.user_is_admin:
return False
return wouldkeep
return keep
def listing(self):
"""Listing to generate from the builder"""
if (getattr(, "_id", -1) == Subreddit.get_promote_srid() and
not c.user_is_sponsor):
abort(403, 'forbidden')
model = LinkListing(self.builder_obj, show_nums=self.show_nums)
suggestions = None
if self.next_suggestions_cls:
suggestions = self.next_suggestions_cls()
pane = model.listing(next_suggestions=suggestions)
# Indicate that the comment tree wasn't built for comments
for i in pane:
if hasattr(i, 'full_comment_path'):
i.child = None
i.suppress_reply_buttons = self.suppress_reply_buttons
return pane
def title(self):
"""Page <title>"""
return _(self.title_text) + " : " +
def rightbox(self):
"""Contents of the right box when rendering"""
builder_wrapper = staticmethod(default_thing_wrapper())
def GET_listing(self, **env):
if self.can_send_referrer():
c.referrer_policy = "always"
return self.build_listing(**env)
listing_api_doc = partial(
extensions=["json", "xml"],
class SubredditListingController(ListingController):
private_referrer = False
def _build_og_title(self, max_length=256):
sr_fragment = "/r/" +
title =
if not title:
return trunc_string(sr_fragment, max_length)
if sr_fragment in title:
return _force_unicode(trunc_string(title, max_length))
# We'd like to always show the whole subreddit name, so let's
# truncate the title while still ensuring the entire thing is under
# the limit.
# This doesn't handle `max_length`s shorter than `sr_fragment`.
# Unknown what the behavior should be, but realistically it shouldn't
# happen, since this is scoped pretty small.
max_title_length = max_length - len(u"%s" % sr_fragment)
title = trunc_string(title, max_title_length)
return u"%s%s" % (_force_unicode(title), sr_fragment)
def _build_og_description(self):
description =
if not description:
description = _(g.short_description)
return _force_unicode(trunc_string(description, MAX_DESCRIPTION_LENGTH))
def render_params(self):
if isinstance(, DefaultSR):
return {'show_locationbar': True}
if not c.user_is_loggedin:
# This data is only for scrapers, which shouldn't be logged in.
twitter_card = {
"site": "reddit",
"card": "summary",
"title": self._build_og_title(max_length=70),
# Twitter will fall back to any defined OpenGraph
# attributes, so we don't need to define
# 'twitter:image' or 'twitter:description'.
hook = hooks.get_hook('subreddit_listing.twitter_card'),
return {
"og_data": {
"site_name": "reddit",
"title": self._build_og_title(),
"image": static('icon.png'),
"description": self._build_og_description(),
"twitter_card": twitter_card,
return {}
class ListingWithPromos(SubredditListingController):
show_organic = False
def make_requested_ad(self, requested_ad):
link = Link._by_fullname(requested_ad, data=True)
except NotFound:
if not promote.is_live_on_sr(link,
res = wrap_links([link._fullname], wrapper=self.builder_wrapper,
res.parent_name = "promoted"
if res.things:
return res
def make_single_ad(self):
srnames = promote.srnames_with_live_promos(c.user,
if srnames:
return SpotlightListing(show_promo=True, srnames=srnames,
def make_spotlight(self):
"""Build the Spotlight.
The frontpage gets a Spotlight box that contains promoted and organic
links from the user's subscribed subreddits and promoted links targeted
to the frontpage. If the user has disabled ads promoted links will not
be shown. Promoted links are requested from the adserver client-side.
organic_fullnames = organic.organic_links(c.user)
promoted_links = []
show_promo = False
srnames = []
can_show_promo = c.user.pref_show_sponsors or not
try_show_promo = ((c.user_is_loggedin and random.random() > 0.5) or
not c.user_is_loggedin)
if can_show_promo and try_show_promo:
srnames = promote.srnames_with_live_promos(c.user,
if srnames:
show_promo = True
def organic_keep_fn(item):
base_keep_fn = super(ListingWithPromos, self).keep_fn()
would_keep = base_keep_fn(item)
return would_keep and item.fresh
organic_fullnames = organic_fullnames[:10]
b = IDBuilder(organic_fullnames,
organic_links = b.get_items()[0]
has_subscribed = c.user.has_subscribed
interestbar_prob = g.live_config['spotlight_interest_sub_p'
if has_subscribed else
interestbar = InterestBar(has_subscribed)
s = SpotlightListing(organic_links=organic_links,
max_num = self.listing_obj.max_num,
max_score = self.listing_obj.max_score).listing()
return s
def content(self):
# only send a spotlight listing for HTML rendering
if c.render_style == "html":
spotlight = None
show_sponsors = not (not c.user.pref_show_sponsors and
show_organic = self.show_organic and c.user.pref_organic
on_frontpage = isinstance(, DefaultSR)
requested_ad = request.GET.get('ad')
if on_frontpage:
self.extra_page_classes = \
self.extra_page_classes + ['front-page']
if requested_ad:
spotlight = self.make_requested_ad(requested_ad)
elif on_frontpage and show_organic:
spotlight = self.make_spotlight()
elif show_sponsors:
spotlight = self.make_single_ad()
self.spotlight = spotlight
if spotlight:
return PaneStack([spotlight, self.listing_obj],
return self.listing_obj
class HotController(ListingWithPromos):
where = 'hot'
extra_page_classes = ListingController.extra_page_classes + ['hot-page']
show_chooser = True
next_suggestions_cls = ListingSuggestions
show_organic = True
def query(self):
if isinstance(, DefaultSR):
sr_ids = Subreddit.user_subreddits(c.user)
return normalized_hot(sr_ids)
elif isinstance(, MultiReddit):
return normalized_hot(, obey_age_limit=False,
link_list = []
wrapped = wrap_links(link_list,
# add all other items and decrement count if sticky is visible
if wrapped.things:
link_list += [l for l in'hot', 'all')
if l !=]
if not self.after:
self.count -= 1
self.num += 1
return link_list
# no sticky or sticky hidden
return'hot', 'all')
def trending_info(cls):
if not c.user.pref_show_trending:
return None
trending_data = trending.get_trending_subreddits()
if not trending_data:
return None
link = Link._byID(trending_data['link_id'], data=True, stale=True)
return {
'subreddit_names': trending_data['subreddit_names'],
'comment_url': trending_data['permalink'],
'comment_count': link.num_comments,
def content(self):
content = super(HotController, self).content()
if (c.render_style == "html" and isinstance(, DefaultSR) and
not self.listing_obj.prev):
trending_info = self.trending_info()
if trending_info:
return PaneStack(filter(None, [
]), css_class='spacer')
return content
def title(self):
@listing_api_doc(uri='/hot', uses_site=True)
def GET_listing(self, **env):
self.infotext = request.GET.get('deleted') and strings.user_deleted
return ListingController.GET_listing(self, **env)
class NewController(ListingWithPromos):
where = 'new'
title_text = _('newest submissions')
extra_page_classes = ListingController.extra_page_classes + ['new-page']
show_chooser = True
next_suggestions_cls = ListingSuggestions
def keep_fn(self):
def keep(item):
if item.promoted is not None:
return False
return item.keep_item(item)
return keep
def query(self):
return'new', 'all')
def POST_listing(self, **env):
# Redirect to GET mode in case of any legacy requests
return self.redirect(request.fullpath)
@listing_api_doc(uri='/new', uses_site=True)
def GET_listing(self, **env):
if request.params.get('sort') == 'rising':
return self.redirect(add_sr('/rising'))
return ListingController.GET_listing(self, **env)
class RisingController(NewController):
where = 'rising'
title_text = _('rising submissions')
extra_page_classes = ListingController.extra_page_classes + ['rising-page']
def query(self):
if isinstance(, DefaultSR):
sr_ids = Subreddit.user_subreddits(c.user)
return normalized_rising(sr_ids)
elif isinstance(, MultiReddit):
return normalized_rising(
return get_rising(
class BrowseController(ListingWithPromos):
where = 'browse'
show_chooser = True
next_suggestions_cls = ListingSuggestions
def keep_fn(self):
"""For merged time-listings, don't show items that are too old
(this can happen when mr_top hasn't run in a while)"""
if self.time != 'all' and c.default_sr:
oldest = timeago('1 %s' % (str(self.time),))
def keep(item):
if isinstance(, AllSR):
if not item.subreddit.allow_top:
return False
return item._date > oldest and item.keep_item(item)
return keep
return ListingController.keep_fn(self)
def menus(self):
return [ControversyTimeMenu(default = self.time)]
def query(self):
return, self.time)
@validate(t = VMenu('sort', ControversyTimeMenu))
def POST_listing(self, sort, t, **env):
# VMenu validator will save the value of time before we reach this
# point. Now just redirect to GET mode.
return self.redirect(
request.fullpath + query_string(dict(sort=sort, t=t)))
@validate(t = VMenu('sort', ControversyTimeMenu))
@listing_api_doc(uri='/{sort}', uri_variants=['/top', '/controversial'],
def GET_listing(self, sort, t, **env):
self.sort = sort
if sort == 'top':
self.title_text = _('top scoring links')
self.extra_page_classes = \
self.extra_page_classes + ['top-page']
elif sort == 'controversial':
self.title_text = _('most controversial links')
self.extra_page_classes = \
self.extra_page_classes + ['controversial-page']
# 'sort' is forced to top/controversial by,
# but in case something has gone wrong...
self.time = t
return ListingController.GET_listing(self, **env)
class AdsController(SubredditListingController):
builder_cls = CampaignBuilder
title_text = _('promoted links')
def infotext(self):
infotext = _("want to advertise? [click here!](%(link)s)")
if c.user.pref_show_promote or c.user_is_sponsor:
return infotext % {'link': '/promoted'}
return infotext % {'link': '/advertising'}
def keep_fn(self):
def keep(item):
if item._fullname in self.promos:
return False
if item.promoted and not item._deleted:
return True
return False
return keep
def query(self):
except NotImplementedError:
def listing(self):
listing = ListingController.listing(self)
return listing
def GET_listing(self, *a, **kw):
self.promos = set()
return SubredditListingController.GET_listing(self, *a, **kw)
class RandomrisingController(ListingWithPromos):
where = 'randomrising'
title_text = _('you\'re really bored now, eh?')
next_suggestions_cls = ListingSuggestions
def query(self):
links = get_rising(
if not links:
# just pull from the new page if the rising page isn't
# populated for some reason
links ='new', 'all')
if isinstance(links, Query):
links._limit = 200
links = [x._fullname for x in links]
links = list(links)
return links
class ByIDController(ListingController):
title_text = _('API')
skip = False
def query(self):
return self.names
@validate(links=VByName("names", thing_cls=Link,
ignore_missing=True, multiple=True))
@api_doc(api_section.listings, uri='/by_id/{names}')
def GET_listing(self, links, **env):
"""Get a listing of links by fullname.
`names` is a list of fullnames for links separated by commas or spaces.
if not links:
return self.abort404()
self.names = [l._fullname for l in links]
return ListingController.GET_listing(self, **env)
class UserController(ListingController):
render_cls = ProfilePage
show_nums = False
skip = True
def menus(self):
res = []
if (self.where in ('overview', 'submitted', 'comments')):
res.append(ProfileSortMenu(default = self.sort))
if self.sort not in ("hot", "new"):
res.append(TimeMenu(default = self.time))
if self.where == 'saved' and
srnames = LinkSavesBySubreddit.get_saved_subreddits(self.vuser)
srnames += CommentSavesBySubreddit.get_saved_subreddits(self.vuser)
srs = Subreddit._by_name(srnames)
srnames = [name for name, sr in srs.iteritems()
if sr.can_view(c.user)]
srnames = sorted(set(srnames), key=lambda name: name.lower())
if len(srnames) > 1:
sr_buttons = [QueryButton(_('all'), None, query_param='sr',
for srname in srnames:
sr_buttons.append(QueryButton(srname, srname, query_param='sr'))
base_path = '/user/%s/saved' %
if self.savedcategory:
base_path += '/%s' % urllib.quote(self.savedcategory)
sr_menu = NavMenu(sr_buttons, base_path=base_path,
title=_('filter by subreddit'),
categories = LinkSavesByCategory.get_saved_categories(self.vuser)
categories += CommentSavesByCategory.get_saved_categories(self.vuser)
categories = sorted(set(categories))
if len(categories) >= 1:
cat_buttons = [NavButton(_('all'), '/', css_class='primary')]
for cat in categories:
base_path = '/user/%s/saved/' %
cat_menu = NavMenu(cat_buttons, base_path=base_path,
title=_('filter by category'),
elif (self.where == 'gilded' and
(c.user == self.vuser or c.user_is_admin)):
path = '/user/%s/gilded/' %
buttons = [NavButton(_("gildings received"), dest='/'),
NavButton(_("gildings given"), dest='/given')]
res.append(NavMenu(buttons, base_path=path, type='flatlist'))
return res
def title(self):
titles = {'overview': _("overview for %(user)s"),
'comments': _("comments by %(user)s"),
'submitted': _("submitted by %(user)s"),
'gilded': _("gilded by %(user)s"),
'liked': _("liked by %(user)s"),
'disliked': _("disliked by %(user)s"),
'saved': _("saved by %(user)s"),
'hidden': _("hidden by %(user)s"),
'promoted': _("promoted by %(user)s")}
if self.where == 'gilded' and == 'given':
return _("gildings given by %(user)s") % {'user':}
title = titles.get(self.where, _('profile for %(user)s')) \
% dict(user =, site =
return title
def keep_fn(self):
def keep(item):
if self.where == 'promoted':
return bool(getattr(item, "promoted", None))
if item._deleted and not c.user_is_admin:
return False
if c.user == self.vuser:
if not item.likes and self.where == 'liked':
return False
if item.likes is not False and self.where == 'disliked':
return False
if self.where == 'saved' and not item.saved:
return False
if (self.time != 'all' and
item._date <= utils.timeago('1 %s' % str(self.time))):
return False
if self.where == 'gilded' and item.gildings <= 0:
return False
if self.where == 'deleted' and not item._deleted:
return False
is_promoted = getattr(item, "promoted", None) is not None
if self.where != 'saved' and is_promoted:
return False
return True
return keep
def query(self):
q = None
if self.where == 'overview':
self.check_modified(self.vuser, 'overview')
q = queries.get_overview(self.vuser, self.sort, self.time)
elif self.where == 'comments':
sup.set_sup_header(self.vuser, 'commented')
self.check_modified(self.vuser, 'commented')
q = queries.get_comments(self.vuser, self.sort, self.time)
elif self.where == 'submitted':
sup.set_sup_header(self.vuser, 'submitted')
self.check_modified(self.vuser, 'submitted')
q = queries.get_submitted(self.vuser, self.sort, self.time)
elif self.where == 'gilded':
sup.set_sup_header(self.vuser, 'gilded')
self.check_modified(self.vuser, 'gilded')
if == 'given':
q = queries.get_user_gildings(self.vuser)
q = queries.get_gilded_user(self.vuser)
elif self.where in ('liked', 'disliked'):
sup.set_sup_header(self.vuser, self.where)
self.check_modified(self.vuser, self.where)
if self.where == 'liked':
q = queries.get_liked(self.vuser)
q = queries.get_disliked(self.vuser)
elif self.where == 'hidden':
q = queries.get_hidden(self.vuser)
elif self.where == 'saved':
if not self.savedcategory and
self.builder_cls = SavedBuilder
sr_id = self.savedsr._id if self.savedsr else None
q = queries.get_saved(self.vuser, sr_id,
elif c.user_is_sponsor and self.where == 'promoted':
q = queries.get_promoted_links(self.vuser._id)
if q is None:
return self.abort404()
return q
@validate(vuser = VExistingUname('username', allow_deleted=True),
sort = VMenu('sort', ProfileSortMenu, remember = False),
time = VMenu('t', TimeMenu, remember = False),
show=VOneOf('show', ('given',)))
@listing_api_doc(section=api_section.users, uri='/user/{username}/{where}',
uri_variants=['/user/{username}/' + where for where in [
'overview', 'submitted', 'comments',
'liked', 'disliked', 'hidden', 'saved',
def GET_listing(self, where, vuser, sort, time, show, **env):
self.where = where
self.sort = sort
self.time = time = show
# the validator will ensure that vuser is a valid account
if not vuser:
return self.abort404()
# only allow admins to view deleted users
if vuser._deleted and not c.user_is_admin:
return self.abort404()
if c.user_is_admin:
c.referrer_policy = "always"
if self.sort in ('hot', 'new'):
self.time = 'all'
# hide spammers profile pages
if vuser._spam and not vuser.banned_profile_visible:
if (not c.user_is_loggedin or
not (c.user._id == vuser._id or
c.user_is_admin or
c.user_is_sponsor and where == "promoted")):
return self.abort404()
if where in ('liked', 'disliked') and not votes_visible(vuser):
return self.abort403()
if ((where in ('saved', 'hidden') or
(where == 'gilded' and show == 'given')) and
not (c.user_is_loggedin and c.user._id == vuser._id) and
not c.user_is_admin):
return self.abort403()
if where == 'saved':
self.show_chooser = True
category = VSavedCategory('category').run(env.get('category'))
srname = request.GET.get('sr')
if srname and
sr = Subreddit._by_name(srname)
except NotFound:
sr = None
sr = None
if category and not
category = None
self.savedsr = sr
self.savedcategory = category
self.vuser = vuser
self.render_params = {'user' : vuser}
c.profilepage = True
self.suppress_reply_buttons = True
if vuser.pref_hide_from_robots:
self.robots = 'noindex,nofollow'
return ListingController.GET_listing(self, **env)
@validate(vuser = VExistingUname('username'))
@api_doc(section=api_section.users, uri='/user/{username}/about',
def GET_about(self, vuser):
"""Return information about the user, including karma and gold status."""
if (not is_api() or
not vuser or
(vuser._spam and vuser != c.user)):
return self.abort404()
return Reddit(content = Wrapped(vuser)).render()
def GET_saved_redirect(self):
if not c.user_is_loggedin:
dest = "/".join(("/user",, "saved"))
extension = request.environ.get('extension')
if extension:
dest = ".".join((dest, extension))
query_string = request.environ.get('QUERY_STRING')
if query_string:
dest += "?" + query_string
return self.redirect(dest)
def GET_rel_user_redirect(self, rest=""):
url = "/user/%s/%s" % (, rest)
if request.query_string:
url += "?" + request.query_string
return self.redirect(url, code=302)
class MessageController(ListingController):
show_nums = False
render_cls = MessagePage
allow_stylesheets = False
# note: this intentionally replaces the listing-page class which doesn't
# conceptually fit for styling these pages.
extra_page_classes = ['messages-page']
def show_sidebar(self):
if c.default_sr and not isinstance(, (ModSR, MultiReddit)):
return False
return self.where in ("moderator", "multi")
def menus(self):
if c.default_sr and self.where in ('inbox', 'messages', 'comments',
'selfreply', 'unread', 'mentions'):
buttons = [NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"),
NavButton(plurals.messages, "messages"),
NavButton(_("comment replies"), 'comments'),
NavButton(_("post replies"), 'selfreply'),
NavButton(_("username mentions"), "mentions"),
return [NavMenu(buttons, base_path = '/message/',
default = 'inbox', type = "flatlist")]
elif not c.default_sr or self.where in ('moderator', 'multi'):
buttons = (NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"))
return [NavMenu(buttons, base_path = '/message/moderator/',
default = 'inbox', type = "flatlist")]
return []
def title(self):
return _('messages') + ': ' + _(self.where)
def keep_fn(self):
def keep(item):
wouldkeep = item.keep_item(item)
# TODO: Consider a flag to disable this (and see above plus
if item._deleted and not c.user_is_admin:
return False
if (item._spam and
item.author_id != c.user._id and
not c.user_is_admin):
return False
if item.author_id in c.user.enemies:
return False
# don't show user their own unread stuff
if ((self.where == 'unread' or self.subwhere == 'unread')
and (item.author_id == c.user._id or not
return False
if (item.is_mention and not in extract_user_mentions(item.body)):
return False
return wouldkeep
return keep
def builder_wrapper(thing):
if isinstance(thing, Comment):
f = thing._fullname
w = Wrapped(thing)
w.render_class = Message
w.to_id = c.user._id
w.was_comment = True
w._fullname = f
w = ListingController.builder_wrapper(thing)
return w
def builder(self):
if (self.where == 'messages' or
(self.where in ("moderator", "multi") and self.subwhere != "unread")):
root = c.user
message_cls = UserMessageBuilder
if self.where == "multi":
root =
message_cls = MultiredditMessageBuilder
elif not c.default_sr:
root =
message_cls = SrMessageBuilder
elif self.where == 'moderator' and self.subwhere != 'unread':
message_cls = ModeratorMessageBuilder
elif self.message and self.message.sr_id:
sr = self.message.subreddit_slow
if sr.is_moderator_with_perms(c.user, 'mail'):
# this is a moderator message and the user is a moderator.
# use the ModeratorMessageBuilder because not all messages
# will be in the user's mailbox
message_cls = ModeratorMessageBuilder
parent = None
skip = False
if self.message:
if self.message.first_message:
parent = Message._byID(self.message.first_message,
parent = self.message
elif c.user.pref_threaded_messages:
skip = (c.render_style == "html")
if (message_cls is UserMessageBuilder and parent and parent.sr_id
and not parent.from_sr):
# Make sure we use the subreddit message builder for modmail,
# because the per-user cache will be wrong if more than two
# parties are involved in the thread.
root = Subreddit._byID(parent.sr_id)
message_cls = SrMessageBuilder
return message_cls(root,
wrap = self.builder_wrapper,
parent = parent,
skip = skip,
num = self.num,
after = self.after,
keep_fn = self.keep_fn(),
reverse = self.reverse)
return ListingController.builder(self)
def _verify_inbox_count(self, kept_msgs):
"""If a user has experienced drift in their inbox counts, correct it.
A small percentage (~0.2%) of users are seeing drift in their inbox
counts (presumably because _incr is experiencing rare failures). If the
user has no unread messages in their inbox currently, this will repair
that drift and log it. Yes, this is a hack.
if g.disallow_db_writes:
if not len(kept_msgs) and c.user.inbox_count != 0:
"Fixing inbox drift for %r. Kept msgs: %d. Inbox_count: %d.",
c.user._incr('inbox_count', -c.user.inbox_count)
def listing(self):
if (self.where == 'messages' and
(c.user.pref_threaded_messages or self.message)):
return Listing(self.builder_obj).listing()
pane = ListingController.listing(self)
# Indicate that the comment tree wasn't built for comments
for i in pane.things:
if i.was_comment:
i.child = None
if self.where == 'unread':
return pane
def query(self):
if self.where == 'messages':
q = queries.get_inbox_messages(c.user)
elif self.where == 'comments':
q = queries.get_inbox_comments(c.user)
elif self.where == 'selfreply':
q = queries.get_inbox_selfreply(c.user)
elif self.where == 'mentions':
q = queries.get_inbox_comment_mentions(c.user)
elif self.where == 'inbox':
q = queries.get_inbox(c.user)
elif self.where == 'unread':
q = queries.get_unread_inbox(c.user)
elif self.where == 'sent':
q = queries.get_sent(c.user)
elif self.where == 'multi' and self.subwhere == 'unread':
q = queries.get_unread_subreddit_messages_multi(
elif self.where == 'moderator' and self.subwhere == 'unread':
if c.default_sr:
srids = Subreddit.reverse_moderator_ids(c.user)
srs = [sr for sr in Subreddit._byID(srids, data=False,
if sr.is_moderator_with_perms(c.user, 'mail')]
q = queries.get_unread_subreddit_messages_multi(srs)
q = queries.get_unread_subreddit_messages(
elif self.where in ('moderator', 'multi'):
if c.have_mod_messages and self.mark != 'false':
c.have_mod_messages = False
c.user.modmsgtime = False
# the query is handled by the builder on the moderator page
return self.abort404()
if self.where != 'sent':
#reset the inbox
if c.have_messages and c.user.pref_mark_messages_read and self.mark != 'false':
c.have_messages = False
return q
message = VMessageID('mid'),
mark = VOneOf('mark',('true','false')))
uri_variants=['/message/inbox', '/message/unread', '/message/sent'])
def GET_listing(self, where, mark, message, subwhere = None, **env):
if not (c.default_sr
or, 'mail')
or c.user_is_admin):
abort(403, "forbidden")
if isinstance(, MultiReddit):
if not (c.user_is_admin or
self.where = "multi"
elif isinstance(, ModSR) or not c.default_sr:
self.where = "moderator"
self.where = where
self.subwhere = subwhere
self.message = message
if mark is not None:
self.mark = mark
elif self.message:
self.mark = "false"
elif is_api():
self.mark = 'false'
elif c.render_style and c.render_style == "xml":
self.mark = 'false'
self.mark = 'true'
if c.user_is_admin:
c.referrer_policy = "always"
if self.where == 'unread':
self.next_suggestions_cls = UnreadMessagesSuggestions
return ListingController.GET_listing(self, **env)
def GET_compose(self, to, subject, message):
mod_srs = []
subreddit_message = False
from_user = True
self.where = "compose"
if isinstance(, MultiReddit):
mod_srs =, "mail")
if not mod_srs:
subreddit_message = True
elif not isinstance(, FakeSubreddit):
if not, "mail"):
mod_srs = []
subreddit_message = True
from_user = False
elif c.user.is_moderator_somewhere:
mod_srs = Mod.srs_with_perms(c.user, "mail")
subreddit_message = bool(mod_srs)
captcha = Captcha() if c.user.needs_captcha() else None
if subreddit_message:
content = ModeratorMessageCompose(mod_srs, from_user=from_user,
to=to, subject=subject,
captcha=captcha, message=message)
content = MessageCompose(to=to, subject=subject, captcha=captcha,
return MessagePage(content=content, title=self.title()).render()
class RedditsController(ListingController):
render_cls = SubredditsPage
def title(self):
return _('subreddits')
def keep_fn(self):
base_keep_fn = ListingController.keep_fn(self)
def keep(item):
return base_keep_fn(item) and (c.over18 or not item.over_18)
return keep
def query(self):
if self.where == 'banned' and c.user_is_admin:
reddits = Subreddit._query(Subreddit.c._spam == True,
sort = desc('_date'),
write_cache = True,
read_cache = True,
cache_time = 5 * 60)
reddits = None
if self.where == 'new':
reddits = Subreddit._query( write_cache = True,
read_cache = True,
cache_time = 5 * 60)
reddits._sort = desc('_date')
elif self.where == 'employee':
reddits = Subreddit._query(
cache_time=5 * 60,
reddits._sort = desc('_downs')
elif self.where == 'gold':
reddits = Subreddit._query(
cache_time=5 * 60,
reddits._sort = desc('_downs')
reddits = Subreddit._query( write_cache = True,
read_cache = True,
cache_time = 60 * 60)
reddits._sort = desc('_downs')
if g.domain != '':
# don't try to render special subreddits (like promos)
reddits._filter(Subreddit.c.author_id != -1)
if self.where == 'popular':
self.render_params = {"show_interestbar": True}
return reddits
def GET_listing(self, where, **env):
"""Get all subreddits.
The `where` parameter chooses the order in which the subreddits are
displayed. `popular` sorts on the activity of the subreddit and the
position of the subreddits can shift around. `new` sorts the subreddits
based on their creation date, newest first.
self.where = where
return ListingController.GET_listing(self, **env)
class MyredditsController(ListingController):
render_cls = MySubredditsPage
def menus(self):
buttons = (NavButton(plurals.subscriber, 'subscriber'),
NavButton(getattr(plurals, "approved submitter"), 'contributor'),
NavButton(plurals.moderator, 'moderator'))
return [NavMenu(buttons, base_path = '/subreddits/mine/',
default = 'subscriber', type = "flatlist")]
def title(self):
return _('subreddits: ') + self.where
def builder_wrapper(self, thing):
w = ListingController.builder_wrapper(thing)
if self.where == 'moderator':
is_moderator = thing.is_moderator(c.user)
if is_moderator:
w.mod_permissions = is_moderator.get_permissions()
return w
def query(self):
if self.where == 'moderator' and not c.user.is_moderator_somewhere:
return []
reddits = SRMember._query(SRMember.c._name == self.where,
SRMember.c._thing2_id == c.user._id,
#hack to prevent the query from
#adding it's own date
sort = (desc('_t1_ups'), desc('_t1_date')),
eager_load = True,
thing_data = True)
reddits.prewrap_fn = lambda x: x._thing1
return reddits
def content(self):
user = c.user if c.user_is_loggedin else None
num_subscriptions = len(Subreddit.reverse_subscriber_ids(user))
if self.where == 'subscriber' and num_subscriptions == 0:
message = strings.sr_messages['empty']
message = strings.sr_messages.get(self.where)
stack = PaneStack()
if message:
return stack
def build_listing(self, after=None, **kwargs):
if after and isinstance(after, Subreddit):
after = SRMember._fast_query(after, c.user, self.where,
if after and not isinstance(after, SRMember):
abort(400, 'gimme a srmember')
return ListingController.build_listing(self, after=after, **kwargs)
uri_variants=['/subreddits/mine/subscriber', '/subreddits/mine/contributor', '/subreddits/mine/moderator'])
def GET_listing(self, where='subscriber', **env):
"""Get subreddits the user has a relationship with.
The `where` parameter chooses which subreddits are returned as follows:
* `subscriber` - subreddits the user is subscribed to
* `contributor` - subreddits the user is an approved submitter in
* `moderator` - subreddits the user is a moderator of
See also: [/api/subscribe](#POST_api_subscribe),
[/api/friend](#POST_api_friend), and
self.where = where
return ListingController.GET_listing(self, **env)
class CommentsController(SubredditListingController):
title_text = _('comments')
def keep_fn(self):
def keep(item):
can_see_spam = (c.user_is_loggedin and
(item.author_id == c.user._id or
c.user_is_admin or
can_see_deleted = c.user_is_loggedin and c.user_is_admin
return ((not item._spam or can_see_spam) and
(not item._deleted or can_see_deleted))
return keep
def query(self):
def GET_listing(self, **env):
c.profilepage = True
self.suppress_reply_buttons = True
return ListingController.GET_listing(self, **env)
class UserListListingController(ListingController):
builder_cls = UserListBuilder
allow_stylesheets = False
skip = False
friends_compat = True
def infotext(self):
if self.where == 'friends':
return strings.friends % Friends.path
elif self.where == 'blocked':
return _("To block a user click 'block user' below a message"
" from a user you wish to block from messaging you.")
def render_params(self):
params = {}
is_wiki_action = self.where in ["wikibanned", "wikicontributors"]
params["show_wiki_actions"] = is_wiki_action
return params
def render_cls(self):
if self.where in ["friends", "blocked"]:
return PrefsPage
return Reddit
def moderator_wrap(self, rel, invited=False):
rel._permission_class = ModeratorPermissionSet
cls = ModTableItem if not invited else InvitedModTableItem
return cls(rel, editable=self.editable)
def builder_wrapper(self):
if self.where == 'banned':
cls = BannedTableItem
elif self.where == 'moderators':
return self.moderator_wrap
elif self.where == 'wikibanned':
cls = WikiBannedTableItem
elif self.where == 'contributors':
cls = ContributorTableItem
elif self.where == 'wikicontributors':
cls = WikiMayContributeTableItem
elif self.where == 'friends':
cls = FriendTableItem
elif self.where == 'blocked':
cls = EnemyTableItem
return lambda rel : cls(rel, editable=self.editable)
def title(self):
section_title = menu[self.where]
# We'll probably want to slowly start opting more and more things into
# having this suffix, to make similar tabs on different subreddits
# distinct.
if self.where == 'moderators':
return '%(section)s - /r/%(subreddit)s' % {
'section': section_title,
return section_title
def rel(self):
if self.where in ['friends', 'blocked']:
return Friend
return SRMember
def name(self):
return self._names.get(self.where)
_names = {
'friends': 'friend',
'blocked': 'enemy',
'moderators': 'moderator',
'contributors': 'contributor',
'banned': 'banned',
'wikibanned': 'wikibanned',
'wikicontributors': 'wikicontributor',
def query(self):
rel = self.rel()
if self.where in ["friends", "blocked"]:
thing1_id = c.user._id
thing1_id =
reversed_types = ["friends", "moderators", "blocked"]
sort = desc if self.where not in reversed_types else asc
q = rel._query(rel.c._thing1_id == thing1_id,
rel.c._name ==,
if self.jump_to_val:
thing2_id = self.user._id if self.user else None
q._filter(rel.c._thing2_id == thing2_id)
return q
def listing(self):
listing = self.listing_cls(self.builder_obj,
return listing.listing()
def invited_mod_listing(self):
query = SRMember._query(SRMember.c._name == 'moderator_invite',
SRMember.c._thing1_id ==,
sort=asc('_date'), data=True)
wrapper = lambda rel: self.moderator_wrap(rel, invited=True)
b = self.builder_cls(query,
return InvitedModListing(b, nextprev=False).listing()
def content(self):
is_api = c.render_style in extensions.API_TYPES
if self.where == 'moderators' and self.editable and not is_api:
# Do not stack the invited mod list in api mode
# to allow for api compatibility with older api users.
content = PaneStack()
elif self.where == 'friends' and is_api and self.friends_compat:
content = PaneStack()
empty_builder = IDBuilder([])
# Append an empty UserList on the api for backwards
# compatibility with the old blocked list.
content.append(UserListing(empty_builder, nextprev=False).listing())
content = self.listing_obj
return content
uri_variants=['/prefs/friends', '/prefs/blocked',
'/api/v1/me/friends', '/api/v1/me/blocked'])
def GET_user_prefs(self, where, **kw):
self.where = where
self.listing_cls = None
self.editable = True
self.paginated = False
self.jump_to_val = None
self.show_not_found = False
self.show_jump_to = False
# The /prefs/friends version of this endpoint used to contain
# two lists of users: friends AND blocked users. For backwards
# compatibility with the old JSON structure, an empty list
# of "blocked" users is sent.
# The /api/v1/me/friends version of the friends list does not
# have this requirement, so it will send just the "friends"
# data structure.
self.friends_compat = not request.path.startswith('/api/v1/me/')
if where == 'friends':
self.listing_cls = FriendListing
elif where == 'blocked':
self.listing_cls = EnemyListing
self.show_not_found = True
kw['num'] = 0
return self.build_listing(**kw)
uri_variants=['/about/' + where for where in [
'banned', 'wikibanned', 'contributors',
'wikicontributors', 'moderators']])
def GET_listing(self, where, user=None, **kw):
if isinstance(, FakeSubreddit):
return self.abort404()
self.where = where
has_mod_access = ((c.user_is_loggedin and, 'access'))
or c.user_is_admin)
if not c.user_is_loggedin and where not in ['contributors', 'moderators']:
self.listing_cls = None
self.editable = True
self.paginated = True
self.jump_to_val = request.GET.get('user')
self.show_not_found = bool(self.jump_to_val)
if where == 'contributors':
# On public reddits, only moderators may see the whitelist.
if == 'public' and not has_mod_access:
# Used for subreddits like /r/lounge
# used for subreddits that don't allow access to approved submitters
self.listing_cls = ContributorListing
self.editable = has_mod_access
elif where == 'banned':
if not has_mod_access:
self.listing_cls = BannedListing
elif where == 'wikibanned':
if not, 'wiki'):
self.listing_cls = WikiBannedListing
elif where == 'wikicontributors':
if not, 'wiki'):
self.listing_cls = WikiMayContributeListing
elif where == 'moderators':
self.editable = ((c.user_is_loggedin and or
self.listing_cls = ModListing
self.paginated = False
if not self.listing_cls:
self.user = user
self.show_jump_to = self.paginated
if not self.paginated:
kw['num'] = 0
return self.build_listing(**kw)
class GildedController(SubredditListingController):
title_text = _("gilded")
def infotext(self):
if isinstance(, FakeSubreddit):
return ''
seconds =
if not seconds:
return ''
delta = timedelta(seconds=seconds)
server_time = precise_format_timedelta(
delta, threshold=5, locale=c.locale)
message = _("gildings in this subreddit have paid for %(time)s of "
"server time")
return message % {'time': server_time}
def infotext_class(self):
return "rounded gold-accent"
def keep_fn(self):
def keep(item):
return item.gildings > 0 and not item._deleted and not item._spam
return keep
def query(self):
except NotImplementedError:
def GET_listing(self, **env):
c.profilepage = True
self.suppress_reply_buttons = True
return ListingController.GET_listing(self, **env)
Jump to Line
Something went wrong with that request. Please try again.