mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-25 06:48:01 -05:00
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:
@@ -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)
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))} 
|
||||
|
||||
@@ -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')}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);", \
|
||||
|
||||
@@ -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}');"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user