Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ celeryconfig.py
.eggs/
build/
html/
web/
worker/
pip-log.txt
.DS_Store
*.swp
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: init start stop custom-build build shell test destroy migrate mysql
.PHONY: init start stop custom-build build staging shell test destroy migrate mysql

init:
cp settings.example.py settings.py
Expand All @@ -18,6 +18,9 @@ custom-build:
build:
docker build -t mltshp/mltshp-web:latest .

staging:
docker build --platform linux/amd64 -t mltshp/mltshp-web:staging .

shell:
docker compose exec mltshp bash

Expand Down
2 changes: 1 addition & 1 deletion celeryconfig.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## Celery configuration

# List of modules to import when celery starts.
imports = ("tasks.timeline", "tasks.counts", "tasks.migration", "tasks.transcode")
imports = ("tasks.timeline", "tasks.counts", "tasks.migration", "tasks.transcode", "tasks.admin")

task_routes = {
"tasks.transcode.*": { "queue": "transcode" },
Expand Down
108 changes: 93 additions & 15 deletions handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

from .base import BaseHandler
from models import Sharedfile, User, Shake, Invitation, Waitlist, ShakeCategory, \
DmcaTakedown, Comment
from lib.utilities import send_slack_notification
DmcaTakedown, Comment, Favorite, PaymentLog, Conversation
from lib.utilities import send_slack_notification, pretty_date
from tasks.admin import delete_account


class AdminBaseHandler(BaseHandler):
Expand Down Expand Up @@ -52,6 +53,62 @@ def get(self):
return self.render("admin/index.html")


class UserHandler(AdminBaseHandler):
@tornado.web.authenticated
def get(self, user_name):
plan_names = {
"mltshp-single-canceled": "Single Scoop - Canceled",
"mltshp-single": "Single Scoop",
"mltshp-double-canceled": "Double Scoop - Canceled",
"mltshp-double": "Double Scoop",
}
user = User.get('name=%s', user_name)
if not user:
return self.redirect('/admin?error=User%20not%20found')

post_count = "{:,d}".format(Sharedfile.where_count("user_id=%s and deleted=0", user.id))
shake_count = "{:,d}".format(Shake.where_count("user_id=%s and deleted=0", user.id))
comment_count = "{:,d}".format(Comment.where_count("user_id=%s and deleted=0", user.id))
like_count = "{:,d}".format(Favorite.where_count("user_id=%s and deleted=0", user.id))
last_activity_date = user.get_last_activity_date()
pretty_last_activity_date = last_activity_date and pretty_date(last_activity_date) or "None"
subscribed = bool(user.is_paid)
subscription = subscribed and user.active_paid_subscription()
subscription_level = plan_names.get(user.stripe_plan_id) or None
subscription_start = subscription and subscription['start_date']
subscription_end = subscription and subscription['end_date']
all_payments = PaymentLog.where("user_id=%s", user.id)
total_payments = 0.00
for payment in all_payments:
if payment.status == "payment":
total_payments = total_payments + float(payment.transaction_amount.split(" ")[1])
uploaded_all_time_mb = "{:,.2f}".format(user.uploaded_kilobytes() / 1024)
uploaded_this_month_mb = "{:,.2f}".format(user.uploaded_this_month() / 1024)
# select all _original_ posts from this user; we care less about reposts for this view
recent_posts = Sharedfile.where("user_id=%s and original_id=0 order by created_at desc limit 50", user.id)
recent_comments = Comment.where("user_id=%s order by created_at desc limit 100", user.id)

return self.render(
"admin/user.html",
user=user,
user_name=user_name,
post_count=post_count,
shake_count=shake_count,
comment_count=comment_count,
like_count=like_count,
uploaded_all_time_mb=uploaded_all_time_mb,
uploaded_this_month_mb=uploaded_this_month_mb,
total_payments=total_payments,
subscribed=subscribed,
subscription_level=subscription_level,
subscription_start=subscription_start,
subscription_end=subscription_end,
last_activity_date=last_activity_date,
pretty_last_activity_date=pretty_last_activity_date,
recent_posts=recent_posts,
recent_comments=recent_comments,)


class NSFWUserHandler(AdminBaseHandler):
@tornado.web.authenticated
def get(self):
Expand Down Expand Up @@ -80,11 +137,17 @@ def get(self):
if not self.admin_user.is_superuser():
return self.redirect('/admin')

share_key = self.get_argument("share_key", None)
if share_key:
sharedfile = Sharedfile.get("share_key=%s AND deleted=0", share_key)
else:
sharedfile = None

return self.render(
"admin/image-takedown.html",
share_key="",
share_key=share_key or "",
confirm_step=False,
sharedfile=None,
sharedfile=sharedfile,
comment="",
canceled=self.get_argument('canceled', "0") == "1",
deleted=self.get_argument('deleted', "0") == "1")
Expand Down Expand Up @@ -168,19 +231,30 @@ def post(self):


class DeleteUserHandler(AdminBaseHandler):
@tornado.web.authenticated
def get(self):
if not self.admin_user.is_superuser():
return self.redirect('/admin')
return self.render('admin/delete-user.html')

@tornado.web.authenticated
def post(self):
user_id = self.get_argument('user_id')
# Only a superuser can delete users
if not self.admin_user.is_superuser():
return self.write({'error': 'not allowed'})

user_name = self.get_argument('user_name')
user = User.get('name=%s and id=%s', user_name, user_id)
user.delete()
return self.redirect('/user/%s' % user_name)
user = None
if user_name:
user = User.get('name=%s', user_name)

if user:
# admin users cannot be deleted (moderator or superuser)
if user.is_admin():
return self.write({'error': 'cannot delete admin'})

# Flag as deleted; send full deletion work to the background
user.deleted = 1
user.save()

delete_account.delay_or_run(user_id=user.id)
return self.write({'response': 'ok' })
else:
return self.write({'error': 'user not found'})


class FlagNSFWHandler(AdminBaseHandler):
Expand All @@ -190,6 +264,7 @@ def post(self, user_name):
if not user:
return self.redirect('/')

json = int(self.get_argument("json", 0))
nsfw = int(self.get_argument("nsfw", 0))
if nsfw == 1:
user.flag_nsfw()
Expand All @@ -198,7 +273,10 @@ def post(self, user_name):
user.save()
send_slack_notification("%s flagged user '%s' as %s" % (self.admin_user.name, user.name, nsfw == 1 and "NSFW" or "SFW"),
channel="#moderation", icon_emoji=":ghost:", username="modbot")
return self.redirect("/user/%s" % user.name)
if json == 1:
return self.write({'response': 'ok' })
else:
return self.redirect("/user/%s" % user.name)


class RecommendedGroupShakeHandler(AdminBaseHandler):
Expand Down
1 change: 1 addition & 0 deletions handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def render_string(self, template_name, **kwargs):
kwargs['current_user_object'] = current_user_object
kwargs['site_is_readonly'] = options.readonly == 1
kwargs['disable_signups'] = options.disable_signups == 1
kwargs['xsrf_token'] = self.xsrf_token
# site merchandise promotions are shown to members
kwargs['show_promos'] = options.show_promos and (
current_user_object and current_user_object.is_paid == 1)
Expand Down
4 changes: 2 additions & 2 deletions lib/uimodules.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,10 @@ def render(self, sharedfile, current_user=None, list_view=False, show_attributio


class ImageMedium(UIModule):
def render(self, sharedfile):
def render(self, sharedfile, direct=False):
sharedfile_user = sharedfile.user()
return self.render_string("uimodules/image-medium.html", sharedfile=sharedfile, \
sharedfile_user=sharedfile_user)
sharedfile_user=sharedfile_user, direct=direct)

class ShakeFollow(UIModule):
def render(self, follow_user=None, follow_shake=None, current_user=None,
Expand Down
16 changes: 11 additions & 5 deletions models/sharedfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ def feed_date(self):
"""
return rfc822_date(self.created_at)

def thumbnail_url(self):
def thumbnail_url(self, direct=False):
# If we are running on Fastly, then we can use the Image Optimizer to
# resize a given image. Thumbnail size is 100x100. This size is used
# for the conversations page.
Expand All @@ -619,14 +619,17 @@ def thumbnail_url(self):
size = self.size
# Fastly I/O won't process images > 50mb, so condition for that
if sourcefile.type == 'image' and options.use_fastly and size > 0 and size < 50_000_000:
return f"https://{options.cdn_host}/r/{self.share_key}?width=100"
if direct:
return f"https://{options.cdn_host}/s3/originals/{sourcefile.file_key}?width=100"
else:
return f"https://{options.cdn_host}/r/{self.share_key}?width=100"
else:
return s3_url(options.aws_key, options.aws_secret, options.aws_bucket, \
file_path="thumbnails/%s" % (sourcefile.thumb_key), seconds=3600)

def small_thumbnail_url(self):
def small_thumbnail_url(self, direct=False):
# If we are running on Fastly, then we can use the Image Optimizer to
# resize a given image. Small thumbnails are 240x184 at most. This size is
# resize a given image. Small thumbnails are 270-wide at most. This size is
# currently only used within the admin UI.
sourcefile = self.sourcefile()
size = 0
Expand All @@ -638,7 +641,10 @@ def small_thumbnail_url(self):
size = self.size
# Fastly I/O won't process images > 50mb, so condition for that
if sourcefile.type == 'image' and options.use_fastly and size > 0 and size < 50_000_000:
return f"https://{options.cdn_host}/r/{self.share_key}?width=240&height=184&fit=bounds"
if direct:
return f"https://{options.cdn_host}/s3/originals/{sourcefile.file_key}?width=270"
else:
return f"https://{options.cdn_host}/r/{self.share_key}?width=270"
else:
return s3_url(options.aws_key, options.aws_secret, options.aws_bucket, \
file_path="smalls/%s" % (sourcefile.small_key), seconds=3600)
Expand Down
2 changes: 1 addition & 1 deletion models/sourcefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def get_from_file(file_path, sha1_value, type='image', skip_s3=None, content_typ
small = img.copy()

thumb.thumbnail((100,100), Image.Resampling.LANCZOS)
small.thumbnail((240,184), Image.Resampling.LANCZOS)
small.thumbnail((270,200), Image.Resampling.LANCZOS)

thumb.save(thumb_cstr, format="JPEG")
small.save(small_cstr, format="JPEG")
Expand Down
43 changes: 33 additions & 10 deletions models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import postmark
from tornado.options import define, options

from lib.utilities import email_re, transform_to_square_thumbnail, utcnow
from lib.utilities import email_re, transform_to_square_thumbnail, utcnow, pretty_date
from lib.flyingcow import Model, Property
from lib.flyingcow.cache import ModelQueryCache
from lib.flyingcow.db import IntegrityError
Expand Down Expand Up @@ -505,10 +505,11 @@ def delete(self):
service.save()

user_shake = self.shake()
subscriptions = subscription.Subscription.where("user_id=%s or shake_id=%s", self.id, user_shake.id)
for sub in subscriptions:
sub.deleted = 1
sub.save()
if user_shake:
subscriptions = subscription.Subscription.where("user_id=%s or shake_id=%s", self.id, user_shake.id)
for sub in subscriptions:
sub.deleted = 1
sub.save()

shakemanagers = shakemanager.ShakeManager.where("user_id=%s and deleted=0", self.id)
for sm in shakemanagers:
Expand Down Expand Up @@ -725,6 +726,25 @@ def update_email(self, email):
self.email = email
self._validate_email_uniqueness()

def get_last_activity_date(self):
sql = """SELECT max(created_at) last_activity_date FROM (
SELECT max(created_at) created_at FROM sharedfile WHERE user_id=%s AND deleted=0
UNION
SELECT max(created_at) created_at FROM comment WHERE user_id=%s AND deleted=0
UNION
SELECT max(created_at) created_at FROM favorite WHERE user_id=%s AND deleted=0
UNION
SELECT max(created_at) created_at FROM bookmark WHERE user_id=%s
) as activities
"""
response = self.query(sql, self.id, self.id, self.id, self.id)
if response and response[0]['last_activity_date']:
return response[0]['last_activity_date']
return None

def pretty_created_at(self):
return pretty_date(self.created_at)

def uploaded_kilobytes(self, start_time=None, end_time=None):
"""
Returns the total number of kilobytes uploaded for the time period specified
Expand All @@ -750,6 +770,13 @@ def uploaded_kilobytes(self, start_time=None, end_time=None):
def can_post(self):
return self.can_upload_this_month()

def uploaded_this_month(self):
month_days = calendar.monthrange(utcnow().year,utcnow().month)
start_time = utcnow().strftime("%Y-%m-01")
end_time = utcnow().strftime("%Y-%m-" + str(month_days[1]) )

return self.uploaded_kilobytes(start_time=start_time, end_time=end_time)

def can_upload_this_month(self):
"""
Returns if this user can upload this month.
Expand All @@ -761,11 +788,7 @@ def can_upload_this_month(self):
if self.is_plus():
return True

month_days = calendar.monthrange(utcnow().year,utcnow().month)
start_time = utcnow().strftime("%Y-%m-01")
end_time = utcnow().strftime("%Y-%m-" + str(month_days[1]) )

total_bytes = self.uploaded_kilobytes(start_time=start_time, end_time=end_time)
total_bytes = self.uploaded_this_month()

if total_bytes == 0:
return True
Expand Down
1 change: 1 addition & 0 deletions routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@
(r"/admin/interesting-stats", handlers.admin.InterestingStatsHandler),
(r"/admin/waitlist", handlers.admin.WaitlistHandler),
(r"/admin/nsfw-users", handlers.admin.NSFWUserHandler),
(r"/admin/user/([a-zA-Z0-9_\-]+)", handlers.admin.UserHandler),
(r"/admin/user/([a-zA-Z0-9_\-]+)/flag-nsfw",
handlers.admin.FlagNSFWHandler),
(r"/admin/delete-user", handlers.admin.DeleteUserHandler),
Expand Down
Loading