diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py index 96183de44..87f8d5259 100644 --- a/r2/r2/controllers/api.py +++ b/r2/r2/controllers/api.py @@ -31,7 +31,7 @@ from r2.models import * from r2.lib.utils import get_title, sanitize_url, timeuntil, set_last_modified from r2.lib.utils import query_string, timefromnow, randstr from r2.lib.utils import timeago, tup, filter_links, levenshtein -from r2.lib.pages import FriendList, ContributorList, ModList, \ +from r2.lib.pages import EnemyList, FriendList, ContributorList, ModList, \ BannedList, BoringPage, FormPage, CssError, UploadedImage, \ ClickGadget, UrlParser from r2.lib.utils.trial_utils import indict, end_trial, trial_info @@ -184,7 +184,8 @@ class ApiController(RedditController): handles message composition under /message/compose. """ if not (form.has_errors("to", errors.USER_DOESNT_EXIST, - errors.NO_USER, errors.SUBREDDIT_NOEXIST) or + errors.NO_USER, errors.SUBREDDIT_NOEXIST, + errors.USER_BLOCKED) or form.has_errors("subject", errors.NO_SUBJECT) or form.has_errors("text", errors.NO_TEXT, errors.TOO_LONG) or form.has_errors("captcha", errors.BAD_CAPTCHA)): @@ -492,7 +493,7 @@ class ApiController(RedditController): nuser = VExistingUname('name'), iuser = VByName('id'), container = VByName('container'), - type = VOneOf('type', ('friend', 'moderator', + type = VOneOf('type', ('friend', 'enemy', 'moderator', 'contributor', 'banned'))) def POST_unfriend(self, nuser, iuser, container, type): """ @@ -517,7 +518,7 @@ class ApiController(RedditController): abort(403, 'forbidden') # if we are (strictly) unfriending, the container had better # be the current user. - if type == "friend" and container != c.user: + if type in ("friend", "enemy") and container != c.user: abort(403, 'forbidden') fn = getattr(container, 'remove_' + type) fn(victim) @@ -528,8 +529,6 @@ class ApiController(RedditController): if type in ("moderator", "contributor"): Subreddit.special_reddits(victim, type, _update=True) - - @validatedForm(VUser(), VModhash(), ip = ValidIP(), @@ -553,8 +552,8 @@ class ApiController(RedditController): and not c.site.is_moderator(c.user))): abort(403,'forbidden') - # if we are (strictly) friending, the container had better - # be the current user. + # if we are (strictly) friending, the container + # had better be the current user. if type == "friend" and container != c.user: abort(403,'forbidden') @@ -765,6 +764,36 @@ class ApiController(RedditController): return Report.new(c.user, thing) + @noresponse(VUser(), VModhash(), + thing=VByName('id')) + def POST_block(self, thing): + '''for blocking via inbox''' + if not thing: + return + # Users may only block someone who has + # actively harassed them (i.e., comment/link reply + # or PM). Check that 'thing' would have showed up in the + # user's inbox at some point + if isinstance(thing, Message): + if thing.to_id != c.user._id: + return + elif isinstance(thing, Comment): + parent_id = getattr(thing, 'parent_id', None) + link_id = thing.link_id + if parent_id: + parent_comment = Comment._byID(parent_id) + parent_author_id = parent_comment.author_id + else: + parent_link = Link._byID(link_id) + parent_author_id = parent_link.author_id + if parent_author_id != c.user._id: + return + + block_acct = Account._byID(thing.author_id) + if block_acct.name in g.admins: + return + c.user.add_enemy(block_acct) + @noresponse(VAdmin(), VModhash(), thing = VByName('id')) def POST_indict(self, thing): @@ -863,7 +892,8 @@ class ApiController(RedditController): not commentform.has_errors("parent", errors.DELETED_COMMENT, errors.DELETED_LINK, - errors.TOO_OLD)): + errors.TOO_OLD, + errors.USER_BLOCKED)): if is_message: to = Account._byID(parent.author_id) diff --git a/r2/r2/controllers/errors.py b/r2/r2/controllers/errors.py index 84b0c4dd4..b718e25a2 100644 --- a/r2/r2/controllers/errors.py +++ b/r2/r2/controllers/errors.py @@ -32,6 +32,7 @@ error_list = dict(( ('BAD_USERNAME', _('invalid user name')), ('USERNAME_TAKEN', _('that username is already taken')), ('USERNAME_TAKEN_DEL', _('that username is taken by a deleted account')), + ('USER_BLOCKED', _("you can't send to a user that you have blocked")), ('NO_THING_ID', _('id not specified')), ('NOT_AUTHOR', _("you can't do that")), ('DELETED_LINK', _('the link you are commenting on has been deleted')), diff --git a/r2/r2/controllers/front.py b/r2/r2/controllers/front.py index 2924119b5..7aa0bf827 100644 --- a/r2/r2/controllers/front.py +++ b/r2/r2/controllers/front.py @@ -897,6 +897,7 @@ class FormsController(RedditController): content = PaneStack() infotext = strings.friends % Friends.path content.append(FriendList()) + content.append(EnemyList()) elif location == 'update': content = PrefUpdate() elif location == 'feeds' and c.user.pref_private_feeds: diff --git a/r2/r2/controllers/listingcontroller.py b/r2/r2/controllers/listingcontroller.py index 325895313..1419571c7 100644 --- a/r2/r2/controllers/listingcontroller.py +++ b/r2/r2/controllers/listingcontroller.py @@ -625,6 +625,7 @@ class MessageController(ListingController): def keep_fn(self): def keep(item): wouldkeep = item.keep_item(item) + # TODO: Consider a flag to disable this (and see above plus builder.py) if (item._deleted or item._spam) and not c.user_is_admin: return False diff --git a/r2/r2/controllers/validator/validator.py b/r2/r2/controllers/validator/validator.py index e17ee088a..1fa28b4b9 100644 --- a/r2/r2/controllers/validator/validator.py +++ b/r2/r2/controllers/validator/validator.py @@ -711,11 +711,14 @@ class VSubmitParent(VByName): fullname = fullname or fullname2 if fullname: parent = VByName.run(self, fullname) - if parent and parent._deleted: - if isinstance(parent, Link): - self.set_error(errors.DELETED_LINK) - else: - self.set_error(errors.DELETED_COMMENT) + if parent: + if c.user_is_loggedin and parent.author_id in c.user.enemies: + self.set_error(errors.USER_BLOCKED) + if parent._deleted: + if isinstance(parent, Link): + self.set_error(errors.DELETED_LINK) + else: + self.set_error(errors.DELETED_COMMENT) if isinstance(parent, Message): return parent else: @@ -907,7 +910,11 @@ class VMessageRecipent(VExistingUname): except NotFound: self.set_error(errors.SUBREDDIT_NOEXIST) else: - return VExistingUname.run(self, name) + account = VExistingUname.run(self, name) + if account._id in c.user.enemies: + self.set_error(errors.USER_BLOCKED) + else: + return account class VUserWithEmail(VExistingUname): def run(self, name): diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index c1cf37caf..64a14743e 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -2283,8 +2283,11 @@ class UserList(Templated): remove_action = "unfriend" editable_fn = None - def __init__(self, editable = True): + def __init__(self, editable=True, addable=None): self.editable = editable + if addable is None: + addable = editable + self.addable = addable Templated.__init__(self) def user_row(self, user): @@ -2351,10 +2354,27 @@ class FriendList(UserList): return UserTableItem(user, self.type, self.cells, self.container_name, True, self.remove_action, rel) + +class EnemyList(UserList): + """Blacklist on /pref/friends""" + type = 'enemy' + cells = ('user', 'remove') + + def __init__(self, editable=True, addable=False): + UserList.__init__(self, editable, addable) + + @property + def table_title(self): + return _('blocked users') + + def user_ids(self): + return c.user.enemies + @property def container_name(self): return c.user._fullname + class ContributorList(UserList): """Contributor list on a restricted/private reddit.""" type = 'contributor' diff --git a/r2/r2/models/account.py b/r2/r2/models/account.py index ecd8842c8..1aee87bed 100644 --- a/r2/r2/models/account.py +++ b/r2/r2/models/account.py @@ -251,6 +251,10 @@ class Account(Thing): def friends(self): return self.friend_ids() + @property + def enemies(self): + return self.enemy_ids() + # Used on the goldmember version of /prefs/friends @memoize('account.friend_rels') def friend_rels_cache(self): @@ -313,6 +317,12 @@ class Account(Thing): for f in q: f._thing1.remove_friend(f._thing2) + q = Friend._query(Friend.c._thing2_id == self._id, + Friend.c._name == 'enemy', + eager_load=True) + for f in q: + f._thing1.remove_enemy(f._thing2) + @property def subreddits(self): from subreddit import Subreddit @@ -622,7 +632,8 @@ def register(name, password): class Friend(Relation(Account, Account)): pass -Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn = True),) +Account.__bases__ += (UserRel('friend', Friend, disable_reverse_ids_fn=True), + UserRel('enemy', Friend, disable_reverse_ids_fn=True)) class DeletedUser(FakeAccount): @property @@ -644,3 +655,12 @@ class DeletedUser(FakeAccount): pass else: object.__setattr__(self, attr, val) + +class BlockedUser(DeletedUser): + @property + def name(self): + return '[blocked]' + + @property + def _deleted(self): + return False diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index aa855d59b..18ba4b4f6 100755 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -24,7 +24,7 @@ from r2.lib.db.thing import Thing, Relation, NotFound, MultiRelation, \ from r2.lib.db.operators import desc from r2.lib.utils import base_url, tup, domain, title_to_url, UrlParser from r2.lib.utils.trial_utils import trial_info -from account import Account, DeletedUser +from account import Account, DeletedUser, BlockedUser from subreddit import Subreddit from printable import Printable from r2.config import cache @@ -620,7 +620,10 @@ class Comment(Thing, Printable): inbox_rel = None # only global admins can be message spammed. - if to and (not c._spam or to.name in g.admins): + # Don't send the message if the recipient has blocked + # the author + if to and ((not c._spam and author._id not in to.enemies) + or to.name in g.admins): orangered = (to.name != author.name) inbox_rel = Inbox._add(to, c, name, orangered=orangered) @@ -756,10 +759,16 @@ class Comment(Thing, Printable): # don't collapse for admins, on profile pages, or if deleted - item.collapsed = ((item.score < min_score) and - not (profilepage or - item.deleted or - user_is_admin)) + item.collapsed = False + if ((item.score < min_score) and not (profilepage or + item.deleted or user_is_admin)): + item.collapsed = True + item.collapsed_reason = _("comment score below threshold") + if c.user_is_loggedin and item.author_id in c.user.enemies: + if "grayed" not in extra_css: + extra_css += " grayed" + item.collapsed = True + item.collapsed_reason = _("blocked user") item.editted = getattr(item, "editted", False) @@ -959,7 +968,9 @@ class Message(Thing, Printable): # if the current "to" is not a sr moderator, # they need to be notified if not sr_id or not sr.is_moderator(to): - orangered = (to.name != author.name) + # Don't notify on PMs from blocked users, either + orangered = (to.name != author.name and + author._id not in to.enemies) inbox_rel.append(Inbox._add(to, m, 'inbox', orangered=orangered)) # find the message originator @@ -1090,6 +1101,13 @@ class Message(Thing, Printable): item.is_collapsed = item.author_collapse if c.user.pref_collapse_read_messages: item.is_collapsed = (item.is_collapsed is not False) + if item.author_id in c.user.enemies: + item.is_collapsed = True + if not c.user_is_admin: + item.author = BlockedUser() + item.subject = _('[blocked]') + item.body = _('[blocked]') + # Run this last Printable.add_props(user, wrapped) diff --git a/r2/r2/templates/comment.html b/r2/r2/templates/comment.html index 4d47ec47a..5f0654b0d 100644 --- a/r2/r2/templates/comment.html +++ b/r2/r2/templates/comment.html @@ -91,7 +91,7 @@ ${parent.collapsed()} %endif %if collapse and thing.collapsed and show: - ${_("comment score below threshold")} + ${thing.collapsed_reason} %else: %if show: ${unsafe(self.score(thing, likes = thing.likes))} diff --git a/r2/r2/templates/messagecompose.compact b/r2/r2/templates/messagecompose.compact index 4e4601f2e..e0c8ac17a 100644 --- a/r2/r2/templates/messagecompose.compact +++ b/r2/r2/templates/messagecompose.compact @@ -39,6 +39,7 @@ ${error_field("NO_USER", "to")} ${error_field("USER_DOESNT_EXIST", "to")} ${error_field("SUBREDDIT_NOEXIST", "to")} + ${error_field("USER_BLOCKED", "to")} <%utils:round_field title="${_('subject')}"> diff --git a/r2/r2/templates/messagecompose.html b/r2/r2/templates/messagecompose.html index 980a516a8..16969037d 100644 --- a/r2/r2/templates/messagecompose.html +++ b/r2/r2/templates/messagecompose.html @@ -55,6 +55,7 @@ function admincheck(elem) { ${error_field("NO_USER", "to")} ${error_field("USER_DOESNT_EXIST", "to")} ${error_field("SUBREDDIT_NOEXIST", "to")} + ${error_field("USER_BLOCKED", "to")} diff --git a/r2/r2/templates/printablebuttons.html b/r2/r2/templates/printablebuttons.html index 76b0a577d..fb880bb7b 100644 --- a/r2/r2/templates/printablebuttons.html +++ b/r2/r2/templates/printablebuttons.html @@ -302,6 +302,11 @@ %endif %if thing.recipient: ${self.banbuttons()} + %if thing.thing.author_id != c.user._id and thing.thing.author_id not in c.user.enemies: +
  • + ${ynbutton(_("block user"), _("blocked"), "block", "hide_thing")} +
  • + %endif
  • ${self.state_button("unread", _("mark unread"), \ "return change_state(this, 'unread_message', unread_thing, true);", \ diff --git a/r2/r2/templates/userlist.html b/r2/r2/templates/userlist.html index e53837470..b3e83005d 100644 --- a/r2/r2/templates/userlist.html +++ b/r2/r2/templates/userlist.html @@ -23,7 +23,7 @@ <%namespace file="utils.html" import="error_field"/> <% from r2.lib.template_helpers import static %>
    - %if thing.editable: + %if thing.addable:
    help
    diff --git a/r2/r2/templates/usertext.html b/r2/r2/templates/usertext.html index aad28b467..0bf22bf3d 100644 --- a/r2/r2/templates/usertext.html +++ b/r2/r2/templates/usertext.html @@ -79,6 +79,7 @@ ${error_field("TOO_OLD", "parent", "span")} ${error_field("DELETED_COMMENT", "parent", "span")} ${error_field("DELETED_LINK", "parent", "span")} + ${error_field("USER_BLOCKED", "parent", "span")}
    ${action_button("save", "submit", "", thing.creating and thing.have_form)} diff --git a/r2/r2/templates/wrappeduser.html b/r2/r2/templates/wrappeduser.html index 2026df85a..618b3edf6 100644 --- a/r2/r2/templates/wrappeduser.html +++ b/r2/r2/templates/wrappeduser.html @@ -27,6 +27,8 @@ %else: %if thing.user_deleted: [deleted] + %elif thing.name == '[blocked]': + ${_(thing.name)} %else: ${plain_link(thing.name + thing.karma, "/user/%s" % thing.name, _class = thing.author_cls + (" id-%s" % thing.fullname),