Allow users to block users that harass them

When you block a user:
* If they reply to a comment/post, you do NOT receive an orangered, and
* see NOTHING in your inbox.
* If they PM you, you do NOT receive an orangered, but the PM WILL show
* up in your inbox, with all fields replaced with the text "[blocked]".
* It's not readily possible to keep it out of the inbox while leaving it
* in the sender's sent box; and it needs to be in the sent box so that
* they can't tell that you've blocked them.
* You may not PM or make a comment reply to a user that you've blocked.
* This is to prevent abuse of the system by pre-emptively blocking a
* user and then sending them a series of harassment messages.

At present, the only way to block a user is from your inbox; if they PM
you or make a comment reply. There is a new "block user" button.
Unblocking a user can be done via /prefs/friends. This is to keep
'blocks' from being used commonly; in general, we prefer to encourage
the use of the downvote arrow for bad comments, and leave user-blocking
for true harassment scenarios.
This commit is contained in:
Keith Mitchell
2011-06-24 13:34:10 -07:00
parent 2088628965
commit 4d7a2fa4e0
16 changed files with 135 additions and 26 deletions

View File

@@ -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)

View File

@@ -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')),

View File

@@ -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:

View File

@@ -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

View File

@@ -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):

View File

@@ -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'

View File

@@ -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

View File

@@ -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)

View File

@@ -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))}&#32;

View File

@@ -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>
<%utils:round_field title="${_('subject')}">

View File

@@ -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")}
</%utils:round_field>
</div>

View File

@@ -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:
<li>
${ynbutton(_("block user"), _("blocked"), "block", "hide_thing")}
</li>
%endif
<li class="unread">
${self.state_button("unread", _("mark unread"), \
"return change_state(this, 'unread_message', unread_thing, true);", \

View File

@@ -23,7 +23,7 @@
<%namespace file="utils.html" import="error_field"/>
<% from r2.lib.template_helpers import static %>
<div class="${thing._class} usertable">
%if thing.editable:
%if thing.addable:
<form action="/post/${thing.destination}"
method="post" class="pretty-form medium-text"
onsubmit="return post_form(this, '${thing.destination}');"

View File

@@ -83,6 +83,7 @@
${error_field("NO_TEXT", thing.name, "span")}
${error_field("DELETED_COMMENT", "parent", "span")}
${error_field("DELETED_LINK", "parent", "span")}
${error_field("USER_BLOCKED", "parent", "span")}
<a href="#" class="help-toggle button secondary_button">help</a>
</div>
<div class="markhelp-parent">

View File

@@ -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")}
<div class="usertext-buttons">
${action_button("save", "submit", "",
thing.creating and thing.have_form)}

View File

@@ -27,6 +27,8 @@
%else:
%if thing.user_deleted:
<span>[deleted]</span>
%elif thing.name == '[blocked]':
<span>${_(thing.name)}</span>
%else:
${plain_link(thing.name + thing.karma, "/user/%s" % thing.name,
_class = thing.author_cls + (" id-%s" % thing.fullname),