Skip to content

Commit

Permalink
Gold Feature: "The Butler". Username monitoring in comments.
Browse files Browse the repository at this point in the history
This is a new feature available only to Gold members. It defaults to on,
but may be turned off in preferences. When a username is mentioned on
the site in the /u/username format, and some conditions apply, the user
is notified by an orangered and a new type of item in the inbox. There
are several cases where notifications won't occur to reduce noise, see
butler.py for details.
  • Loading branch information
spladug committed May 21, 2013
1 parent a66101f commit e057278
Show file tree
Hide file tree
Showing 15 changed files with 199 additions and 13 deletions.
3 changes: 3 additions & 0 deletions r2/r2/config/queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def declare_queues(g):
"log_q": MessageQueue(bind_to_self=True),
"cloudsearch_changes": MessageQueue(bind_to_self=True),
"update_promos_q": MessageQueue(bind_to_self=True),
"butler_q": MessageQueue(),
})

if g.shard_link_vote_queues:
Expand All @@ -98,4 +99,6 @@ def declare_queues(g):
)
queues.commentstree_q << "new_comment"
queues.commentstree_fastlane_q << "new_fastlane_comment"
queues.butler_q << ("new_comment",
"usertext_edited")
return queues
6 changes: 4 additions & 2 deletions r2/r2/controllers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,9 @@ def POST_del(self, thing):

if recipient:
inbox_class = Inbox.rel(Account, Comment)
d = inbox_class._fast_query(recipient, thing, ("inbox", "selfreply"))
d = inbox_class._fast_query(recipient, thing, ("inbox",
"selfreply",
"mention"))
rels = filter(None, d.values()) or None
queries.new_comment(thing, rels)

Expand Down Expand Up @@ -1111,7 +1113,7 @@ def POST_block(self, thing):
# or PM). Check that 'thing' is in the user's inbox somewhere
inbox_cls = Inbox.rel(Account, thing.__class__)
rels = inbox_cls._fast_query(c.user, thing,
("inbox", "selfreply"))
("inbox", "selfreply", "mention"))
if not filter(None, rels.values()):
return

Expand Down
18 changes: 15 additions & 3 deletions r2/r2/controllers/listingcontroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from r2.lib.utils import iters, check_cheating, timeago
from r2.lib import sup
from r2.lib.validator import *
from r2.lib.butler import extract_user_mentions
import socket

from api_docs import api_doc, api_section
Expand Down Expand Up @@ -735,12 +736,17 @@ def show_sidebar(self):
@property
def menus(self):
if c.default_sr and self.where in ('inbox', 'messages', 'comments',
'selfreply', 'unread'):
buttons = (NavButton(_("all"), "inbox"),
'selfreply', 'unread', 'mentions'):
buttons = [NavButton(_("all"), "inbox"),
NavButton(_("unread"), "unread"),
NavButton(plurals.messages, "messages"),
NavButton(_("comment replies"), 'comments'),
NavButton(_("post replies"), 'selfreply'))
NavButton(_("post replies"), 'selfreply')]

if c.user.gold:
buttons += [NavButton(_("username mentions"),
"mentions",
css_class="gold")]

return [NavMenu(buttons, base_path = '/message/',
default = 'inbox', type = "flatlist")]
Expand Down Expand Up @@ -769,6 +775,10 @@ def keep(item):
and (item.author_id == c.user._id or not item.new)):
return False

if (item.message_style == "mention" and
c.user.name.lower() not in extract_user_mentions(item.body)):
return False

return wouldkeep
return keep

Expand Down Expand Up @@ -842,6 +852,8 @@ def query(self):
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':
Expand Down
1 change: 1 addition & 0 deletions r2/r2/controllers/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def POST_unlogged_options(self, all_langs, pref_lang):
pref_show_sponsors = VBoolean("show_sponsors"),
pref_show_sponsorships = VBoolean("show_sponsorships"),
pref_highlight_new_comments = VBoolean("highlight_new_comments"),
pref_monitor_mentions=VBoolean("monitor_mentions"),
all_langs = VOneOf('all-langs', ('all', 'some'), default='all'))
def POST_options(self, all_langs, pref_lang, **kw):
#temporary. eventually we'll change pref_clickgadget to an
Expand Down
112 changes: 112 additions & 0 deletions r2/r2/lib/butler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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
# http://code.reddit.com/LICENSE. 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-2013 reddit
# Inc. All Rights Reserved.
###############################################################################

from pylons import g, c

from r2.lib.db import queries
from r2.lib import amqp
from r2.lib.utils import extract_urls_from_markdown
from r2.lib.validator import chkuser
from r2.models import query_cache, Thing, Comment, Account, Inbox, NotFound


def extract_user_mentions(text):
for url in extract_urls_from_markdown(text):
if not url.startswith("/u/"):
continue

username = url[len("/u/"):]
if chkuser(username):
yield username.lower()


def notify_mention(user, thing):
inbox_rel = Inbox._add(user, thing, "mention")
with query_cache.CachedQueryMutator() as m:
m.insert(queries.get_inbox_comment_mentions(user), [inbox_rel])
queries.set_unread(thing, user, unread=True, mutator=m)


def monitor_mentions(comment):
if not isinstance(comment, Comment):
return

if comment._spam or comment._deleted:
return

sender = comment.author_slow
if getattr(sender, "butler_ignore", False):
# this is an account that generates false notifications, e.g.
# LinkFixer
return

subreddit = comment.subreddit_slow
usernames = list(extract_user_mentions(comment.body))
inbox_class = Inbox.rel(Account, Comment)

# Subreddit.can_view stupidly requires this.
c.user_is_loggedin = True

for username in usernames:
try:
account = Account._by_name(username)
except NotFound:
continue

# most people are aware of when they mention themselves.
if account == sender:
continue

# bail out if that user doesn't have gold or has the feature turned off
if not account.gold or not account.pref_monitor_mentions:
continue

# don't notify users of things they can't see
if not subreddit.can_view(account):
continue

# don't notify users when a person they've blocked mentions them
if account.is_enemy(sender):
continue

# ensure this comment isn't already in the user's inbox already
rels = inbox_class._fast_query(
account,
comment,
("inbox", "selfreply", "mention"),
)
if filter(None, rels.values()):
continue

notify_mention(account, comment)


def run():
@g.stats.amqp_processor("butler_q")
def process_message(msg):
fname = msg.body
item = Thing._by_fullname(fname, data=True)
monitor_mentions(item)

amqp.consume_items("butler_q",
process_message,
verbose=True)
18 changes: 18 additions & 0 deletions r2/r2/lib/db/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,22 @@ def get_unread_selfreply(user):
return rel_query(inbox_comment_rel, user, 'selfreply',
filters = [inbox_comment_rel.c.new == True])


@cached_userrel_query
def get_inbox_comment_mentions(user):
return rel_query(inbox_comment_rel, user, "mention")


@cached_userrel_query
def get_unread_comment_mentions(user):
return rel_query(inbox_comment_rel, user, "mention",
filters=[inbox_comment_rel.c.new == True])


def get_inbox(user):
return merge_results(get_inbox_comments(user),
get_inbox_messages(user),
get_inbox_comment_mentions(user),
get_inbox_selfreply(user))

@cached_query(UserQueryCache)
Expand All @@ -602,6 +615,7 @@ def get_sent(user_id):
def get_unread_inbox(user):
return merge_results(get_unread_comments(user),
get_unread_messages(user),
get_unread_comment_mentions(user),
get_unread_selfreply(user))

def _user_reported_query(user_id, thing_cls):
Expand Down Expand Up @@ -877,6 +891,8 @@ def new_comment(comment, inbox_rels):
else:
raise ValueError("wtf is " + inbox_rel._name)

# mentions happen in butler_q

if not comment._deleted:
m.insert(query, [inbox_rel])
else:
Expand Down Expand Up @@ -992,6 +1008,8 @@ def set_unread(messages, to, unread, mutator=None):
query = get_unread_comments(i._thing1_id)
elif i._name == "selfreply":
query = get_unread_selfreply(i._thing1_id)
elif i._name == "mention":
query = get_unread_comment_mentions(i._thing1_id)
elif isinstance(messages[0], Message):
query = get_unread_messages(i._thing1_id)
assert query is not None
Expand Down
1 change: 1 addition & 0 deletions r2/r2/models/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class Account(Thing):
pref_show_sponsors = True, # sponsored links
pref_show_sponsorships = True,
pref_highlight_new_comments = True,
pref_monitor_mentions=True,
mobile_compress = False,
mobile_thumbnail = True,
trusted_sponsor = False,
Expand Down
17 changes: 13 additions & 4 deletions r2/r2/models/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,14 +1375,23 @@ def add_props(cls, user, wrapped):
item.permalink = item.lookups[0].make_permalink(link, sr=sr)
item.link_permalink = link.make_permalink(sr)
if item.parent_id:
item.subject = _('comment reply')
item.message_style = "comment-reply"
parent = parents[item.parent_id]
item.parent = parent._fullname
item.parent_permalink = parent.make_permalink(link, sr)

if parent.author_id == c.user._id:
item.subject = _('comment reply')
item.message_style = "comment-reply"
else:
item.subject = _('username mention')
item.message_style = "mention"
else:
item.subject = _('post reply')
item.message_style = "post-reply"
if link.author_id == c.user._id:
item.subject = _('post reply')
item.message_style = "post-reply"
else:
item.subject = _('username mention')
item.message_style = "mention"
elif item.sr_id is not None:
item.subreddit = m_subreddits[item.sr_id]

Expand Down
4 changes: 3 additions & 1 deletion r2/r2/public/static/css/reddit.less
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ label.disabled { color: gray; }

.flat-list {list-style-type: none; display: inline;}
.flat-list li, .flat-list form {display: inline; white-space: nowrap; }
.flat-list .selected a { color: orangered; }

.flat-list li a.gold { color: #9a7d2e; font-weight: bold; }
.flat-list li.selected a { color: orangered; }

.link .flat-list { display: block; padding: 1px 0; }
.link.compressed .flat-list { display: inline-block; padding: 0 0 1px 0; }
Expand Down
Binary file added r2/r2/public/static/gold/sample-butler.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 8 additions & 2 deletions r2/r2/templates/goldinfopage.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ <h1>${_('Make reddit better.')}</h1>
_(
"# Filter specific subreddits from /r/all.\n"
"Are there subreddits you don't want to see? Customize /r/all by removing them with a url like /r/all-operative-badger (up to ~300 removals - limited by maximum url length)."
), 'new')}
))}
${feature_item(static('gold/sample-butler.png'),
_(
"# Notifications when you're mentioned in comments.\n"
"""Want to say "boo" when people talk about you? """
"""Get an orangered whenever someone "/u/" mentions your username in a comment."""
), "new")}
${feature_item(static('gold/sample-morecomments.png'),
_(
'# More subreddits and comments per page.\n'
Expand All @@ -77,7 +83,7 @@ <h1>${_('Make reddit better.')}</h1>
'# Save comments and view by subreddit.\n'
"Save the great comments you'd like to revisit later. \n"
'Filter your favorite posts and comments by subreddit.'
), 'new')}
))}
${feature_item(static('gold/sample-adsoptions.png'),
_(
'# Turn off ads.\n'
Expand Down
5 changes: 5 additions & 0 deletions r2/r2/templates/preffeeds.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ <h1>${_("Private RSS feeds")}</h1>
<br/>
<%self:feedbuttons path="/message/selfreply"></%self:feedbuttons>
${_("self-post replies only")}
% if c.user.gold:
<br>
<%self:feedbuttons path="/message/mentions"></%self:feedbuttons>
${_("mentions of your username only")}
% endif
</td>
</tr>
%if c.show_mod_mail:
Expand Down
1 change: 1 addition & 0 deletions r2/r2/templates/prefoptions.html
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@
(${_("we'll remember your visits for 48 hours and show you which comments you haven't seen yet")})
</span>
<br/>
${checkbox(_("notify me when people say my username"), "monitor_mentions")}
</td>
</tr>
%endif
Expand Down
2 changes: 1 addition & 1 deletion r2/r2/templates/printablebuttons.html
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@
%endif
%if thing.recipient:
${self.banbuttons()}
%if not thing.was_comment and thing.thing.author_id != c.user._id and thing.thing.author_id not in c.user.enemies:
%if (not thing.was_comment or thing.thing.message_style == "mention") and 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>
Expand Down
14 changes: 14 additions & 0 deletions upstart/reddit-consumer-butler_q.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description "notify users when their username is mentioned"

instance $x

stop on reddit-stop or runlevel [016]

respawn
respawn limit 10 5

nice 10
script
. /etc/default/reddit
wrap-job paster run --proctitle butler_q$x $REDDIT_INI -c 'from r2.lib.butler import run; run()'
end script

0 comments on commit e057278

Please sign in to comment.