mirror of https://git.skyjake.fi/gemini/bubble.git
241 lines
9.4 KiB
Python
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 |