bubble/admin.py

241 lines
9.4 KiB
Python

import datetime
import random
import re
from model import User, Database, Subspace
from utils import clean_query, is_valid_name, is_empty_query
def admin_actions(session):
user = session.user
db = session.db
req = session.req
page = f'# Administration\n'
page += session.dashboard_link() + '\n'
if not user:
return 60, "Login required"
if user.role != User.ADMIN:
return 61, "Not authorized"
if req.path.startswith(session.path + 'admin/review-users/'):
token = session.get_token()
if req.path == session.path + f'admin/review-users/set-basic/{token}':
name = clean_query(req)
user = db.get_user(name=name)
if not user:
return 51, 'Not found'
db.update_user(user, role=User.BASIC)
db.unset_post_omit_flags(user)
db.notify_role(user)
page += f'User "{name}" (ID: {user.id}) has been given a Basic role.\n\n'
page += f"=> /admin/review-users/ Continue Review\n"
return page
if req.path == session.path + f'admin/review-users/batch-delete/{token}':
if is_empty_query(req):
return 10, 'Max age (hours) and max post count for batch deletion?'
args = clean_query(req).split()
seconds = int(args[0]) * 60 * 60
max_posts = int(args[1])
db.batch_delete_limited_users(seconds, max_posts)
return 31, '/admin/review-users/'
page += f'=> /admin/review-users/batch-delete/{token} 💥 Batch delete\n\n'
limited_users = db.get_users(role=User.LIMITED)
page += '## Promote Limited Users\n'
for u in limited_users:
page += f"=> /u/{u.name} {u.avatar} {u.name}\n"
page += f"=> set-basic/{token}?{u.name} Promote {u.name} to Basic role\n\n"
page += '## Delete Limited Users\n'
for u in limited_users:
page += f"=> /u/{u.name} {u.avatar} {u.name}\n"
page += f"=> /admin/delete-user/{token}?{u.name} ⚠️ DELETE {u.avatar} {u.name}\n\n"
return page
if req.path == session.path + 'admin/flair':
if is_empty_query(req):
return 10, "Edit flairs of user:"
return 30, session.path + 'settings/flair/' + clean_query(req) + '/'
if req.path == session.path + 'admin/':
token = session.get_token()
page += "## Users\n\n"
page += f'=> review-users/ ✔️ Review limited users\n'
page += f'=> flair 📛 Edit user flairs\n'
page += f'=> password/{token} 🔑 Generate a random password for user\n'
page += f'=> revoke/{token} 🛂 Revoke certificates of user\n'
page += f'=> create-user/{token} 👤 Create new user\n'
page += f'=> delete-user/{token} ❌ Delete user\n'
page += "\n## Subspaces\n\n"
page += f"=> omit-subspace/{token} Omit a subspace from All Posts\n"
page += f"=> include-subspace/{token} Include a subspace in All Posts\n"
page += f'=> delete-subspace/{token} Delete a subspace\n'
page += '\n### Locked\n'
locked_subs = db.get_subspaces(locked=True)
if not locked_subs:
page += 'No locked subspaces.\n'
for locked in locked_subs:
page += f'=> /{locked.title()} 🔒 {locked.title()}\n'
return page
found = re.search(r'^admin/(create-user|password|revoke|flair|delete-user|omit-subspace|include-subspace|delete-subspace)/([0-9a-zA-Z]{10})$',
req.path[len(session.path):])
if not found:
return 59, "Bad request"
name = clean_query(req)
action = found[1]
req_token = found[2]
if not db.verify_token(user, req_token):
return 61, "Not authorized"
if action == 'create-user':
if not is_valid_name(name):
return 10, "Enter name of user to create: " + session.NAME_HINT
user_id = db.create_user(name, None)
page += f'User "{name}" (ID: {user_id}) has been created, with no registered certificates.\n'
elif action == 'password':
prompt = 'Generate a certificate password for which user? ' + \
'(Optionally, followed by how many days the password is valid: "username 3". ' + \
'Default is 7 days.)'
if is_empty_query(req):
return 10, prompt
try:
parts = clean_query(req).split()
if len(parts) == 2:
name, days = parts
days = int(days)
else:
name = parts[0]
days = 7
user = db.get_user(name=name)
if not user:
return 10, prompt
# Generate a reasonably secure password.
pass_chars = [i for i in Database.TOKEN_CHARS]
pass_chars.remove('O')
pass_chars.remove('0')
pass_chars.remove('l')
pass_chars.remove('I')
pass_chars.remove('1')
pass_chars += ['@', '%', '&']
passwd = ''.join([pass_chars[random.randint(0, len(pass_chars) - 1)]
for _ in range(15)])
passwd = passwd[:5] + '-' + passwd[5:10] + '-' + passwd[10:]
expire_off = max(0, (days * 24 - 1) * 60) # normal expiration is one hour
db.update_user(user,
password=passwd,
password_expiration_offset_minutes=expire_off)
page += f'User "{name}" (ID: {user.id}) password has been set to:\n'
page += f'```\n{passwd}\n```\n'
dt = datetime.datetime.now() + datetime.timedelta(minutes=expire_off + 60)
page += f'It expires on {dt.strftime("%Y-%m-%d %H:%M:%S")}.\n'
except Exception as x:
import traceback
traceback.print_tb(x.__traceback__)
print(x)
return 10, prompt
elif action == 'revoke':
if not is_valid_name(name):
return 10, 'Enter name of user whose certificates to revoke:'
user = db.get_user(name=name)
if not user:
return 51, 'Not found'
if user.role == User.ADMIN:
return 50, 'Admin certificates cannot be revoked'
db.remove_certificate(user, None)
page += f'Certificates of user "{name}" (ID: {user.id}) have been unregistered.\n'
# elif action == 'flair':
# try:
# parts = clean_query(req).split()
# name = parts[0]
# if not is_valid_name(name):
# return 10, 'Enter user name followed by flair (e.g., "john Friendly"):'
# except:
# return 10, 'Enter user name followed by flair (e.g., "john Friendly"):'
# flair = clean_query(req)[len(name):].strip()
# user = db.get_user(name=name)
# if not user:
# return 51, 'Not found'
# db.update_user(user, flair=flair)
# page += f'Flair of user "{name}" (ID: {user.id}) has been set to: [{flair}]\n'
elif action == 'delete-user':
if not is_valid_name(name):
return 10, "Enter user to delete: (NOTE: All of their posts and comments will be deleted.)"
deleting = db.get_user(name=name)
if not deleting:
return 51, 'Not found'
db.destroy_user(deleting)
page += f'User "{deleting.name}" (ID: {deleting.id}) has been deleted.\n'
elif action == 'omit-subspace':
if not is_valid_name(name):
return 10, "Enter subspace to omit from All Posts:"
sub = db.get_subspace(name=name)
if not sub:
return 51, 'Not found'
db.update_subspace(sub,
flags=sub.flags |
Subspace.OMIT_FROM_ALL_FLAG |
Subspace.HIDE_OMIT_SETTING_FLAG)
page += f'Subspace "{sub.name}" (ID: {sub.id}) is now omitted from All Posts.\n'
elif action == 'include-subspace':
if not is_valid_name(name):
return 10, "Enter subspace to include in All Posts:"
sub = db.get_subspace(name=name)
if not sub:
return 51, 'Not found'
db.update_subspace(sub,
flags=sub.flags & ~(Subspace.OMIT_FROM_ALL_FLAG |
Subspace.HIDE_OMIT_SETTING_FLAG))
page += f'Subspace "{sub.name}" (ID: {sub.id}) is now included in All Posts.\n'
elif action == 'delete-subspace':
if not is_valid_name(name):
return 10, "Enter subspace to delete: (NOTE: All posts and comments will be deleted.)"
deleting = db.get_subspace(name=name)
if not deleting:
return 51, 'Not found'
if deleting.owner:
return 59, 'User subspaces cannot be separately deleted'
db.destroy_subspace(deleting)
page += f'Subspace "{deleting.name}" (ID: {deleting.id}) has been deleted.\n'
else:
return 59, "Invalid admin action"
page += f'\n=> {session.path}admin/ Back to Administration\n'
return page
def make_stats_page(session):
db = session.db
page = '# Statistics\n\n'
stats = db.get_statistics()
page += f"""```Table: Accounts and activity
Total accounts │ {stats['total']:4d}
Total posters │ {stats['posters']:4d}
Total commenters │ {stats['commenters']:4d}
Visited <= 30 days │ {stats['m_visited']:4d}
Post/comment <= 30 days │ {stats['m_post_cmt']:4d}
```
=> {session.path} Back to front page\n
"""
return page