Skip to content

Commit

Permalink
Trylater: Enable temporary subreddit bans
Browse files Browse the repository at this point in the history
  • Loading branch information
Roger Ostrander committed Jun 5, 2014
1 parent 9599656 commit dc68b16
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 12 deletions.
26 changes: 23 additions & 3 deletions r2/r2/controllers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,9 @@ def POST_unfriend(self, nuser, iuser, container, type):
if type == "friend" and c.user.gold:
c.user.friend_rels_cache(_update=True)

if type in ('banned', 'wikibanned'):
container.unschedule_unban(victim, type)

@validatedForm(VSrModerator(), VModhash(),
target=VExistingUname('name'),
type_and_permissions=VPermissions('type', 'permissions'))
Expand Down Expand Up @@ -774,10 +777,12 @@ def POST_setpermissions(self, form, jquery, target, type_and_permissions):
container = nop('container'),
type = VOneOf('type', ('friend',) + _sr_friend_types),
type_and_permissions = VPermissions('type', 'permissions'),
note = VLength('note', 300))
note = VLength('note', 300),
duration = VInt('duration', min=1, max=999),
)
@api_doc(api_section.users)
def POST_friend(self, form, jquery, ip, friend,
container, type, type_and_permissions, note):
container, type, type_and_permissions, note, duration):
"""
Complement to POST_unfriend: handles friending as well as
privilege changes on subreddits.
Expand Down Expand Up @@ -885,9 +890,22 @@ def POST_friend(self, form, jquery, ip, friend,
# the right one and update its data.
c.user.friend_rels_cache(_update=True)
c.user.add_friend_note(friend, note or '')


tempinfo = None
if type in ('banned', 'wikibanned'):
container.add_rel_note(type, friend, note)
if duration:
container.unschedule_unban(friend, type)
tempinfo = container.schedule_unban(
type,
friend,
c.user,
duration,
)
elif not new:
# Preexisting ban and no duration specified means turn the
# temporary ban into a permanent one.
container.unschedule_unban(friend, type)

row_cls = dict(friend=FriendTableItem,
moderator=ModTableItem,
Expand All @@ -905,6 +923,8 @@ def POST_friend(self, form, jquery, ip, friend,
if new and row_cls:
new._thing2 = friend
user_row = row_cls(new)
if tempinfo:
BannedListing.populate_from_tempbans(user_row, tempinfo)
form.set_html(".status:first", user_row.executed_message)
rev_types = ["moderator", "moderator_invite", "friend"]
index = 0 if user_row.type not in rev_types else -1
Expand Down
3 changes: 3 additions & 0 deletions r2/r2/lib/db/tdb_cassandra.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
COUNTER_COLUMN_TYPE, TIME_UUID_TYPE,
ASCII_TYPE)
from pycassa.types import DateType, LongType, IntegerType
from pycassa.util import convert_uuid_to_time
from r2.lib.utils import tup, Storage
from r2.lib import cache
from uuid import uuid1, UUID
Expand Down Expand Up @@ -524,6 +525,8 @@ def _serialize_date(cls, date):
def _deserialize_date(cls, val):
if isinstance(val, datetime):
date = val
elif isinstance(val, UUID):
return convert_uuid_to_time(val)
elif len(val) == 8: # cassandra uses 8-byte integer format for this
date = date_serializer.unpack(val)
else: # it's probably the old-style stringified seconds since epoch
Expand Down
3 changes: 2 additions & 1 deletion r2/r2/lib/pages/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from r2.lib.db import tdb_cassandra, queries
from r2.config.extensions import is_api
from r2.lib.menus import CommentSortMenu

from pylons.i18n import _, ungettext
from pylons import c, request, g, config
from pylons.controllers.util import abort
Expand Down Expand Up @@ -3080,7 +3081,7 @@ def container_name(self):

class BannedTableItem(RelTableItem):
type = 'banned'
cells = ('user', 'age', 'sendmessage', 'remove', 'note')
cells = ('user', 'age', 'sendmessage', 'remove', 'note', 'temp')

@property
def executed_message(self):
Expand Down
20 changes: 20 additions & 0 deletions r2/r2/models/listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from vote import *
from report import *
from subreddit import DefaultSR, AllSR, Frontpage

from pylons import i18n, request, g
from pylons.i18n import _

Expand Down Expand Up @@ -170,6 +171,15 @@ def container_name(self):
class BannedListing(UserListing):
type = 'banned'

@classmethod
def populate_from_tempbans(cls, item, tempbans=None):
if not tempbans:
return
time = tempbans.get(item.user.name)
if time:
delay = time - datetime.now(g.tz)
item.tempban = max(delay.days, 0)

@property
def form_title(self):
return _("ban users")
Expand All @@ -179,6 +189,16 @@ def title(self):
return _("users banned from"
" /r/%(subreddit)s") % dict(subreddit=c.site.name)

def get_items(self, *a, **kw):
items = UserListing.get_items(self, *a, **kw)
wrapped_items = items[0]
names = [item.user.name for item in wrapped_items]
tempbans = c.site.get_tempbans(self.type, names)
for wrapped in wrapped_items:
BannedListing.populate_from_tempbans(wrapped, tempbans)
return items


class WikiBannedListing(BannedListing):
type = 'wikibanned'

Expand Down
105 changes: 103 additions & 2 deletions r2/r2/models/subreddit.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
import itertools
import json

from pylons import c, g
from pycassa.util import convert_uuid_to_time
from pylons import c, g, request
from pylons.i18n import _

from r2.lib.db.thing import Thing, Relation, NotFound
Expand All @@ -47,7 +48,7 @@
from r2.lib.filters import _force_unicode
from r2.lib.db import tdb_cassandra
from r2.models.wiki import WikiPage, ImagesByWikiPage

from r2.models.trylater import TryLater, TryLaterBySubject
from r2.lib.merge import ConflictException
from r2.lib.cache import CL_ONE
from r2.lib import hooks
Expand All @@ -59,6 +60,8 @@
import os.path
import random

trylater_hooks = hooks.HookRegistrar()


def get_links_sr_ids(sr_ids, sort, time):
from r2.lib.db import queries
Expand Down Expand Up @@ -922,6 +925,21 @@ def get_live_promos(self):
from r2.lib import promote
return promote.get_live_promotions([self.name])

def schedule_unban(self, kind, victim, banner, duration):
return SubredditTempBan.schedule(
self,
kind,
victim,
banner,
datetime.timedelta(days=duration),
)

def unschedule_unban(self, victim, type):
SubredditTempBan.unschedule(self.name, victim.name, type)

def get_tempbans(self, type=None, names=None):
return SubredditTempBan.search(self.name, type, names)


class FakeSubreddit(BaseSite):
_defaults = dict(Subreddit._defaults,
Expand Down Expand Up @@ -1747,3 +1765,86 @@ class SubredditPopularityByLanguage(tdb_cassandra.View):
_value_type = 'pickle'
_connection_pool = 'main'
_read_consistency_level = CL_ONE


class SubredditTempBan(object):
def __init__(self, sr, kind, victim, banner, duration):
self.sr = sr._id36
self._srname = sr.name
self.who = victim._id36
self._whoname = victim.name
self.type = kind
self.banner = banner._id36
self.duration = duration

@classmethod
def schedule(cls, sr, kind, victim, banner, duration):
info = {
'sr': sr._id36,
'who': victim._id36,
'type': kind,
'banner': banner._id36,
}
result = TryLaterBySubject.schedule(
cls.cancel_rowkey(sr.name, kind),
cls.cancel_colkey(victim.name),
json.dumps(info),
duration,
trylater_rowkey=cls.schedule_rowkey(),
)
return {victim.name: result.keys()[0]}

@classmethod
def cancel_colkey(cls, name):
return name

@classmethod
def cancel_rowkey(cls, name, type):
return "srunban:%s:%s" % (name, type)

@classmethod
def schedule_rowkey(cls):
return "srunban"

@classmethod
def search(cls, srname, bantype, subjects):
results = TryLaterBySubject.search(cls.cancel_rowkey(srname, bantype),
subjects)

def convert_uuid_to_datetime(uu):
return datetime.datetime.fromtimestamp(convert_uuid_to_time(uu),
g.tz)
return {
name: convert_uuid_to_datetime(uu)
for name, uu in results.iteritems()
}

@classmethod
def unschedule(cls, srname, victim_name, bantype):
TryLaterBySubject.unschedule(
cls.cancel_rowkey(srname, bantype),
cls.cancel_colkey(victim_name),
cls.schedule_rowkey(),
)


@trylater_hooks.on('trylater.srunban')
def on_subreddit_unban(mature_items):
from r2.models.modaction import ModAction
for blob in mature_items.itervalues():
baninfo = json.loads(blob)
container = Subreddit._byID36(baninfo['sr'], data=True)
victim = Account._byID36(baninfo['who'], data=True)
banner = Account._byID36(baninfo['banner'], data=True)
kind = baninfo['type']
remove_function = getattr(container, 'remove_' + kind)
new = remove_function(victim)
g.log.info("Unbanned %s from %s", victim.name, container.name)

if new:
action = dict(
banned='unbanuser',
wikibanned='wikiunbanned',
).get(kind, None)
ModAction.create(container, banner, action, target=victim,
description="was temporary")
81 changes: 78 additions & 3 deletions r2/r2/models/trylater.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@

import contextlib
import datetime
import json
import uuid

from pycassa.system_manager import TIME_UUID_TYPE
from pycassa.system_manager import TIME_UUID_TYPE, UTF8_TYPE
from pycassa.util import convert_time_to_uuid, convert_uuid_to_time
from pylons import g

from r2.lib.db import tdb_cassandra
from r2.lib.utils import tup


class TryLater(tdb_cassandra.View):
Expand All @@ -52,9 +57,79 @@ def multi_handle(cls, rowkeys, cutoff=None):
for system, items in ready.iteritems():
cls._remove(system, items.keys())

@classmethod
def search(cls, rowkey, when):
if isinstance(when, uuid.UUID):
when = convert_uuid_to_time(when)
try:
return cls._cf.get(rowkey, column_start=when, column_finish=when)
except tdb_cassandra.NotFoundException:
return {}

@classmethod
def schedule(cls, system, data, delay=None):
if delay is None:
delay = datetime.timedelta(minutes=60)
key = datetime.datetime.utcnow() + delay
cls._set_values(system, {key: data})
key = datetime.datetime.now(g.tz) + delay
scheduled = {key: data}
cls._set_values(system, scheduled)
return scheduled

@classmethod
def unschedule(cls, rowkey, column_keys):
column_keys = tup(column_keys)
return cls._cf.remove(rowkey, column_keys)


class TryLaterBySubject(tdb_cassandra.View):
_use_db = True
_read_consistency_level = tdb_cassandra.CL.QUORUM
_write_consistency_level = tdb_cassandra.CL.QUORUM
_compare_with = UTF8_TYPE
_extra_schema_creation_args = {
"key_validation_class": UTF8_TYPE,
"default_validation_class": TIME_UUID_TYPE,
}
_value_type = 'date'

@classmethod
def _serialize_column(cls, attr, val):
# Everything this class serializes in columns is a date, but without
# a consistent attr (column key) there's no way to signal that to the
# regular _serialize_column method. Thus, we override and perform no
# serialization, relying on pycassa to convert the dates to UUID1
return val

@classmethod
def schedule(cls, system, subject, data, delay, trylater_rowkey=None):
if trylater_rowkey is None:
trylater_rowkey = system
scheduled = TryLater.schedule(trylater_rowkey, data, delay)
when = scheduled.keys()[0]

# TTL 10 minutes after the TryLater runs just in case TryLater
# is running late.
ttl = (delay + datetime.timedelta(minutes=10)).seconds
coldict = {subject: when}
cls._set_values(system, coldict, ttl=ttl)
return scheduled

@classmethod
def search(cls, rowkey, subjects=None):
try:
if subjects:
subjects = tup(subjects)
return cls._cf.get(rowkey, subjects)
else:
return cls._cf.get(rowkey)
except tdb_cassandra.NotFoundException:
return {}

@classmethod
def unschedule(cls, rowkey, colkey, schedule_rowkey):
colkey = tup(colkey)
victims = cls.search(rowkey, colkey)
for uu in victims.itervalues():
keys = TryLater.search(schedule_rowkey, uu).keys()
TryLater.unschedule(schedule_rowkey, keys)
cls._cf.remove(rowkey, colkey)
4 changes: 2 additions & 2 deletions r2/r2/public/static/css/reddit.less
Original file line number Diff line number Diff line change
Expand Up @@ -6543,9 +6543,9 @@ body:not(.gold) .allminus-link {
.rel-note.edited button[type=submit] { display: inline-block; }
.rel-note.edited input[type=text] { width: 250px; margin-right: 0px; }

.friend-add.edited .ban-reason { display: block; }
.friend-add.edited .ban-details { display: block; }

.ban-reason {
.ban-details {
display: none;
}

Expand Down
7 changes: 6 additions & 1 deletion r2/r2/templates/userlisting.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ <h1>${title}</h1>
%if add_type in ("banned", "wikibanned"):
<label for="name">${_('who to ban?')} &nbsp;</label>
<input type="text" onfocus="$(this).parent('form').addClass('edited')" class="friend-name" name="name" id="name">
<span class="ban-reason">
<span class="ban-details">
<label for="note">${_('why the ban?')}</label>
<input type="text" maxlength="300" name="note" id="note">
<span>${_('(will not be visible to user)')}</span>
</span>
<span class="ban-details">
<label for="duration">${_('how long?')}</label>
<input type="number" min="1" max="999" name="duration" id="duration">
<span>${_('days (leave blank for permanent)')}</span>
</span>
%else:
<input type="text" name="name" id="name">
%endif
Expand Down
Loading

0 comments on commit dc68b16

Please sign in to comment.