2023-05-06 13:29:21 +00:00
|
|
|
|
import re
|
2023-06-03 14:11:16 +00:00
|
|
|
|
import urllib.parse as urlparse
|
2023-10-27 08:09:57 +00:00
|
|
|
|
from model import User, Post, Segment, Subspace, LogEntry, Commit, Crossref, \
|
2023-06-02 15:16:41 +00:00
|
|
|
|
FOLLOW_SUBSPACE, FOLLOW_USER, FOLLOW_POST, \
|
|
|
|
|
MUTE_SUBSPACE, MUTE_USER, MUTE_POST
|
2023-05-10 10:01:08 +00:00
|
|
|
|
from subspace import subspace_admin_actions
|
2023-06-20 12:02:07 +00:00
|
|
|
|
from user import make_user_index_page
|
2023-05-06 13:29:21 +00:00
|
|
|
|
from utils import *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_post_page_or_configure_feed(session):
|
2023-05-29 19:40:32 +00:00
|
|
|
|
# This function may return None, in which case should make a feed page.
|
2023-11-05 16:43:16 +00:00
|
|
|
|
# TODO: Should refactor this to make the session configuration a separate step.
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
db = session.db
|
|
|
|
|
req = session.req
|
|
|
|
|
path = req.path[len(session.path):]
|
2023-06-20 12:02:07 +00:00
|
|
|
|
found = re.match(r'(u|s)/([\w%-]+)(/(post|compose|image|file|issues|admin|tag|index))?(/([\w\d-]+)(.*))?', path)
|
2023-06-05 16:20:50 +00:00
|
|
|
|
if not found and not path.startswith('tag'):
|
2023-05-06 13:29:21 +00:00
|
|
|
|
return 59, 'Bad request'
|
|
|
|
|
|
|
|
|
|
# Set up the feed parameters.
|
2023-06-05 16:20:50 +00:00
|
|
|
|
if found:
|
|
|
|
|
session.feed_type = found.group(1)
|
|
|
|
|
url_name = urlparse.unquote(found.group(2))
|
|
|
|
|
action = found.group(4)
|
|
|
|
|
arg = found.group(6)
|
|
|
|
|
arg2 = found.group(7)
|
|
|
|
|
else:
|
|
|
|
|
# Tag filtering All Posts.
|
|
|
|
|
found = re.match(r'(tag)(/([\w\d-]+)(.*))?', path)
|
|
|
|
|
action = found[1]
|
|
|
|
|
arg = found[3]
|
|
|
|
|
arg2 = found[4]
|
2023-05-10 12:20:32 +00:00
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if session.feed_type == 'u':
|
|
|
|
|
session.c_user = db.get_user(name=url_name)
|
|
|
|
|
if not session.c_user:
|
|
|
|
|
return 51, 'Not found: u/' + url_name
|
|
|
|
|
session.context = db.get_subspace(owner=session.c_user.id)
|
2023-05-07 21:04:14 +00:00
|
|
|
|
session.is_context_locked = False
|
|
|
|
|
session.is_user_mod = (session.user.id == session.c_user.id or \
|
|
|
|
|
session.user.role == User.ADMIN) if session.user else False
|
2023-06-05 16:20:50 +00:00
|
|
|
|
elif session.feed_type == 's':
|
2023-05-06 13:29:21 +00:00
|
|
|
|
session.c_user = None
|
|
|
|
|
session.context = db.get_subspace(name=url_name)
|
2023-05-27 16:11:30 +00:00
|
|
|
|
if session.context.owner:
|
|
|
|
|
return 30, '/u' + req.path[2:]
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not session.context:
|
|
|
|
|
return 51, 'Not found: s/' + url_name
|
|
|
|
|
session.context_mods = db.get_mods(session.context)
|
|
|
|
|
session.is_user_mod = \
|
|
|
|
|
(session.user.role == User.ADMIN or \
|
|
|
|
|
session.user.id in map(lambda m: m.id, session.context_mods)) \
|
|
|
|
|
if session.user else False
|
2023-05-10 12:20:32 +00:00
|
|
|
|
session.is_context_locked = len(session.context_mods) == 0
|
|
|
|
|
session.is_context_tracker = (session.context.flags & Subspace.ISSUE_TRACKER) != 0
|
|
|
|
|
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
session.feed_mode = 'open'
|
|
|
|
|
if req.query != None:
|
|
|
|
|
params = req.query.split('&')
|
|
|
|
|
if 'open' in params and 'closed' in params:
|
|
|
|
|
session.feed_mode = 'all'
|
|
|
|
|
elif 'open' in params:
|
|
|
|
|
session.feed_mode = 'open'
|
|
|
|
|
elif 'closed' in params:
|
|
|
|
|
session.feed_mode = 'closed'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
if session.feed_type == 's' and action == 'admin':
|
2023-05-10 10:01:08 +00:00
|
|
|
|
return subspace_admin_actions(session, arg)
|
|
|
|
|
|
2023-05-11 20:00:51 +00:00
|
|
|
|
if session.is_context_tracker and action == 'issues':
|
2023-05-26 16:07:34 +00:00
|
|
|
|
return 30, f"/s/{session.context.name}/{arg}"
|
2023-05-11 20:00:51 +00:00
|
|
|
|
|
2023-05-10 10:01:08 +00:00
|
|
|
|
if session.feed_type == 'u' and action in ('image', 'file'):
|
|
|
|
|
file = db.get_file(int(arg))
|
|
|
|
|
return 20, file.mimetype, file.data
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-06-20 12:02:07 +00:00
|
|
|
|
if session.feed_type == 'u' and action == 'index':
|
|
|
|
|
return make_user_index_page(session, found[6])
|
|
|
|
|
|
2023-05-29 19:40:32 +00:00
|
|
|
|
if action == 'tag':
|
|
|
|
|
if not arg:
|
2023-06-05 16:20:50 +00:00
|
|
|
|
page = f'Choose a tag for filtering {session.context.title() if session.context else "All Posts"}:\n'
|
|
|
|
|
page += f'=> {session.path}{session.context.title() if session.context else ""} ❌ None\n\n'
|
2023-05-29 19:40:32 +00:00
|
|
|
|
for tag in sorted(db.get_popular_tags(session.context), key=str.lower):
|
2023-06-05 16:20:50 +00:00
|
|
|
|
if tag == Post.TAG_CLOSED:
|
2023-05-29 19:40:32 +00:00
|
|
|
|
continue
|
2023-06-05 16:20:50 +00:00
|
|
|
|
page += f'=> {session.path}{session.context.title() + "/" if session.context else ""}tag/{tag} 🏷️ #{tag}\n'
|
2023-05-29 19:40:32 +00:00
|
|
|
|
return page
|
|
|
|
|
session.feed_tag_filter = arg
|
|
|
|
|
arg = None # don't show a post page
|
|
|
|
|
|
|
|
|
|
elif action == 'compose':
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not session.user:
|
|
|
|
|
return 60, 'Login required'
|
|
|
|
|
if session.c_user and session.user.id != session.c_user.id:
|
|
|
|
|
return 61, "Cannot post to another user's subspace"
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if session.is_context_locked:
|
|
|
|
|
return 61, "Subspace is locked"
|
2023-10-26 12:59:30 +00:00
|
|
|
|
if session.user.role == User.LIMITED and (not session.c_user or
|
2023-10-27 08:09:57 +00:00
|
|
|
|
session.c_user.id != session.user.id):
|
2023-10-26 12:59:30 +00:00
|
|
|
|
return 61, "Not authorized"
|
2023-10-27 08:09:57 +00:00
|
|
|
|
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):
|
2023-10-27 03:42:42 +00:00
|
|
|
|
return 61, "Expired"
|
2023-10-27 08:09:57 +00:00
|
|
|
|
if session.user.role == User.LIMITED and \
|
|
|
|
|
db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:
|
2023-10-29 04:52:44 +00:00
|
|
|
|
return 44, "Rate limit exceeded"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
draft_id = db.create_post(session.user, session.context.id)
|
2023-10-27 08:09:57 +00:00
|
|
|
|
db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
return 30, '/edit/%d' % draft_id
|
|
|
|
|
|
2023-05-29 19:40:32 +00:00
|
|
|
|
elif action == 'post':
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not session.user:
|
|
|
|
|
return 60, 'Login required'
|
|
|
|
|
if session.c_user and session.user.id != session.c_user.id:
|
|
|
|
|
return 61, "Cannot post to another user's subspace"
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if session.is_context_locked:
|
|
|
|
|
return 61, "Subspace is locked"
|
2023-10-26 12:59:30 +00:00
|
|
|
|
if session.user.role == User.LIMITED and not session.c_user:
|
|
|
|
|
return 61, "Not authorized"
|
2023-10-27 08:09:57 +00:00
|
|
|
|
if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):
|
2023-10-27 03:42:42 +00:00
|
|
|
|
return 61, "Expired"
|
2023-10-27 08:09:57 +00:00
|
|
|
|
if session.user.role == User.LIMITED and \
|
|
|
|
|
db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:
|
2023-10-29 04:52:44 +00:00
|
|
|
|
return 44, "Rate limit exceeded"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-21 18:23:30 +00:00
|
|
|
|
if session.is_gemini:
|
|
|
|
|
if is_empty_query(req):
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
return 10, f'Title for new issue in {session.context.title()}:'
|
|
|
|
|
return 10, f'New post in {session.context.title()}: (see Help for special commands)'
|
|
|
|
|
seg_text = clean_query(req)
|
|
|
|
|
else:
|
|
|
|
|
if not req.content_mime.startswith('text/'):
|
|
|
|
|
return 50, 'Content must be text'
|
|
|
|
|
seg_text = req.content.decode('utf-8')
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-21 18:23:30 +00:00
|
|
|
|
# Check special commands.
|
2023-05-23 12:09:21 +00:00
|
|
|
|
title = None
|
|
|
|
|
body = seg_text
|
|
|
|
|
url = None
|
2023-05-21 18:23:30 +00:00
|
|
|
|
special = None
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if len(seg_text) == 0:
|
2023-05-21 18:23:30 +00:00
|
|
|
|
special = 'draft'
|
2023-05-24 05:26:51 +00:00
|
|
|
|
elif seg_text == '.' or seg_text == '/':
|
2023-05-21 18:23:30 +00:00
|
|
|
|
special = 'draft'
|
2023-05-23 12:09:21 +00:00
|
|
|
|
body = ''
|
2023-05-21 18:23:30 +00:00
|
|
|
|
elif seg_text == ':':
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
return 50, 'Not supported when posting issues'
|
|
|
|
|
return 30, session.server_root('titan') + req.path
|
|
|
|
|
elif seg_text.endswith('\\'):
|
2023-05-23 12:09:21 +00:00
|
|
|
|
body = seg_text[:-1].strip()
|
2023-05-21 18:23:30 +00:00
|
|
|
|
special = 'draft'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-23 12:09:21 +00:00
|
|
|
|
# Detect a solitary link, and a headline on the first line.
|
|
|
|
|
lines = body.split('\n')
|
|
|
|
|
|
|
|
|
|
if len(lines) == 1:
|
|
|
|
|
link = re.match(r'^\s*(=>\s*)?((gemini|gopher|finger|https?)://[^ ]+)(\s+(.*))?', lines[0])
|
|
|
|
|
if link:
|
|
|
|
|
url = link[2]
|
|
|
|
|
body = link[5] if link[5] else ''
|
|
|
|
|
title = ''
|
|
|
|
|
|
|
|
|
|
if not url:
|
|
|
|
|
found = re.match(r'^\s*#\s*(.+)$', lines[0])
|
|
|
|
|
if found:
|
|
|
|
|
title = found[1]
|
|
|
|
|
body = '\n'.join(lines[1:]).strip()
|
|
|
|
|
elif session.is_context_tracker:
|
|
|
|
|
title = lines[0]
|
|
|
|
|
body = '\n'.join(lines[1:]).strip()
|
|
|
|
|
else:
|
|
|
|
|
title = ''
|
|
|
|
|
|
|
|
|
|
post_id = db.create_post(session.user, session.context.id, title=title)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
post = db.get_post(post_id, draft=True)
|
2023-05-23 12:09:21 +00:00
|
|
|
|
if url:
|
|
|
|
|
db.create_segment(post, Segment.LINK, url=url, content=body)
|
|
|
|
|
elif body:
|
|
|
|
|
db.create_segment(post, Segment.TEXT, content=body)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
db.update_post_summary(post)
|
2023-05-21 18:23:30 +00:00
|
|
|
|
|
|
|
|
|
# Further content is required for issues.
|
2023-05-23 12:09:21 +00:00
|
|
|
|
if (session.is_context_tracker and not body) or special == 'draft':
|
2023-05-21 18:23:30 +00:00
|
|
|
|
return 30, f'{session.server_root()}/edit/{post.id}'
|
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
db.publish_post(post)
|
2023-10-27 08:09:57 +00:00
|
|
|
|
db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)
|
2023-05-21 18:23:30 +00:00
|
|
|
|
return 30, session.server_root() + post.page_url()
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
if session.user:
|
|
|
|
|
session.user_follows = db.get_follows(session.user)
|
2023-06-02 15:16:41 +00:00
|
|
|
|
session.user_mutes = db.get_mutes(session.user)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
if arg:
|
|
|
|
|
# Viewing a single post.
|
2023-05-26 16:07:34 +00:00
|
|
|
|
post = None
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
# In issue trackers, posts are identified by the issue numbers.
|
|
|
|
|
post = db.get_post_for_issueid(session.context, int(arg))
|
|
|
|
|
if not post:
|
|
|
|
|
post = db.get_post(id=int(arg), draft=False)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not post:
|
2023-11-06 18:13:09 +00:00
|
|
|
|
# Maybe it is a draft?
|
|
|
|
|
if session.user:
|
|
|
|
|
post = db.get_post(id=int(arg), draft=True)
|
|
|
|
|
if post and post.user == session.user.id:
|
|
|
|
|
return 30, f'/edit/{post.id}'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
return 51, 'Not found'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
if post.parent != 0:
|
|
|
|
|
return make_post_page(session, post)
|
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if post.subspace != session.context.id:
|
2023-05-26 16:07:34 +00:00
|
|
|
|
# Redirect to the correct subspace.
|
|
|
|
|
post_sub = db.get_subspace(id=post.subspace)
|
|
|
|
|
if post_sub.flags & Subspace.ISSUE_TRACKER:
|
|
|
|
|
return 31, f'{session.path}{post_sub.title()}/{post.issueid}'
|
|
|
|
|
else:
|
|
|
|
|
return 31, f'{session.path}{post_sub.title()}/{post.id}'
|
|
|
|
|
|
2023-11-06 13:45:05 +00:00
|
|
|
|
if arg2 == '/more':
|
2023-11-06 16:19:51 +00:00
|
|
|
|
if not session.user:
|
|
|
|
|
return 60, "Login required"
|
|
|
|
|
|
2023-11-06 13:45:05 +00:00
|
|
|
|
# Additional post actions.
|
|
|
|
|
page = session.gemini_feed_entry(post)
|
|
|
|
|
page += "\n## More Actions\n"
|
|
|
|
|
|
|
|
|
|
actions = []
|
|
|
|
|
kind = 'issue' if session.is_context_tracker else 'post'
|
|
|
|
|
|
|
|
|
|
if not session.is_context_tracker and (session.user.id == post.user or session.is_user_mod):
|
|
|
|
|
actions.append(f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n')
|
|
|
|
|
actions.append(f'=> /remind/{post.id} 🔔 Remind me\n')
|
|
|
|
|
if session.is_movable(post):
|
|
|
|
|
actions.append(f'=> /edit/{post.id}/move/{session.get_token()} Move to subspace\n')
|
2023-11-06 16:19:51 +00:00
|
|
|
|
if post.user != session.user.id and not session.is_user_mod and session.user.role != User.ADMIN:
|
|
|
|
|
actions.append(f'=> /report/{post.id} ⚠️ Report\n')
|
2023-11-06 13:45:05 +00:00
|
|
|
|
|
|
|
|
|
if actions:
|
|
|
|
|
actions.append('\n')
|
|
|
|
|
|
|
|
|
|
if session.user.id != post.user:
|
|
|
|
|
if (FOLLOW_POST, post.id) in session.user_follows:
|
|
|
|
|
actions.append(f'=> /unfollow/post/{post.id} ➖ Unfollow {kind}\n')
|
|
|
|
|
else:
|
|
|
|
|
if (MUTE_POST, post.id) in session.user_mutes:
|
|
|
|
|
actions.append(f'=> /unmute/post/{post.id} 🔈 Unmute {kind}\n')
|
|
|
|
|
else:
|
|
|
|
|
actions.append(f'=> /follow/post/{post.id} ➕ Follow {kind}\n')
|
|
|
|
|
actions.append(f'=> /mute/post/{post.id} 🔇 Mute {kind}\n')
|
|
|
|
|
else:
|
|
|
|
|
# Own posts can be muted.
|
|
|
|
|
if (MUTE_POST, post.id) in session.user_mutes:
|
|
|
|
|
actions.append(f'=> /unmute/post/{post.id} 🔈 Unmute {kind}\n')
|
|
|
|
|
else:
|
|
|
|
|
actions.append(f'=> /mute/post/{post.id} 🔇 Mute {kind}\n')
|
|
|
|
|
|
|
|
|
|
if session.is_antenna_enabled() and session.user.id == post.user and post.sub_owner == post.user:
|
2023-11-06 16:19:51 +00:00
|
|
|
|
actions.append('\n')
|
2023-11-06 17:21:51 +00:00
|
|
|
|
antenna_feed = f"gemini://{session.bubble.hostname}{session.path}u/{session.user.name}/{post.id}/antenna"
|
|
|
|
|
actions += session.bubble.antenna_links('post', antenna_feed)
|
2023-11-06 13:45:05 +00:00
|
|
|
|
|
|
|
|
|
if session.is_deletable(post) and not session.is_editable(post):
|
2023-11-06 16:19:51 +00:00
|
|
|
|
actions.append('\n')
|
|
|
|
|
actions.append(f'=> /edit/{post.id}/delete/{session.get_token()} ❌ Delete {kind}\n')
|
2023-11-06 13:45:05 +00:00
|
|
|
|
|
|
|
|
|
if actions:
|
|
|
|
|
page += ''.join(actions)
|
|
|
|
|
return page
|
|
|
|
|
|
2023-05-12 03:30:50 +00:00
|
|
|
|
if arg2 == '/antenna':
|
|
|
|
|
# Special viewing mode for Antenna submissions, with the bare minimum.
|
|
|
|
|
page = f'# {session.feed_title()}\n'
|
|
|
|
|
page += session.gemini_feed_entry(post, session.context)
|
|
|
|
|
return page
|
|
|
|
|
|
2023-05-21 18:23:30 +00:00
|
|
|
|
if arg2.startswith('/clear-notif/'):
|
|
|
|
|
token = arg2[arg2.rindex('/') + 1:]
|
|
|
|
|
if not db.verify_token(session.user, token):
|
|
|
|
|
return 61, "Not authorized"
|
|
|
|
|
db.get_notifications(session.user, post_id=post.id, clear=True)
|
|
|
|
|
return 30, post.page_url()
|
|
|
|
|
|
2023-06-20 05:49:35 +00:00
|
|
|
|
return make_post_page(session, post)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
return None
|
2023-05-07 21:04:14 +00:00
|
|
|
|
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
def make_post_page(session, post):
|
2023-06-23 15:01:56 +00:00
|
|
|
|
"""Page containing a post and its discussion thread. If `post` is a comment, a partial
|
|
|
|
|
discussion thread is shown."""
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
db = session.db
|
2023-05-21 18:23:30 +00:00
|
|
|
|
user = session.user
|
2023-06-20 12:02:07 +00:00
|
|
|
|
post_id = post.id
|
2023-06-20 05:49:35 +00:00
|
|
|
|
is_comment_page = post.parent != 0
|
|
|
|
|
display_order_desc = session.user and \
|
|
|
|
|
session.user.sort_cmt == User.SORT_COMMENT_NEWEST
|
|
|
|
|
page = ''
|
2023-06-23 15:11:36 +00:00
|
|
|
|
focused_cmt = None
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
if is_comment_page:
|
|
|
|
|
# Switch to the parent post, but display it in preview mode.
|
|
|
|
|
focused_cmt = post
|
2023-06-20 12:02:07 +00:00
|
|
|
|
post_id = post.parent
|
|
|
|
|
post = db.get_post(id=post_id)
|
2023-10-29 04:54:18 +00:00
|
|
|
|
page += f'# Comment by {focused_cmt.poster_avatar} {focused_cmt.poster_name}\n\n'
|
2023-06-20 12:02:07 +00:00
|
|
|
|
if post:
|
2023-06-23 15:01:56 +00:00
|
|
|
|
page += f'=> {post.page_url()} Re: "{post.title if post.title else shorten_text(strip_links(clean_title(post.summary)), 60)}"\n'
|
|
|
|
|
sub_name = ("u/" if post.sub_owner else "s/") + post.sub_name
|
2023-10-29 04:54:18 +00:00
|
|
|
|
page += f'=> /{sub_name} In: {sub_name}\n\n'
|
2023-06-20 12:02:07 +00:00
|
|
|
|
else:
|
2023-06-20 16:56:28 +00:00
|
|
|
|
if not session.user or session.user.id != focused_cmt.user:
|
|
|
|
|
# Can't view others' comments on deleted posts, just your own.
|
|
|
|
|
return 51, 'Not found'
|
2023-06-23 15:01:56 +00:00
|
|
|
|
page += f'=> /help/deleted-post 🔒 Comment on a deleted post (ID:{post_id})\n\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
page += session.render_post(focused_cmt)
|
2023-06-23 15:01:56 +00:00
|
|
|
|
page += f'\n=> /u/{focused_cmt.poster_name} {focused_cmt.poster_avatar} {focused_cmt.poster_name}\n'
|
2023-11-05 18:20:00 +00:00
|
|
|
|
page += f'{focused_cmt.age()}\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
# Comment actions.
|
2023-07-18 05:41:26 +00:00
|
|
|
|
if user:
|
|
|
|
|
actions = []
|
|
|
|
|
if post:
|
|
|
|
|
if session.is_editable(focused_cmt):
|
2023-11-06 16:19:51 +00:00
|
|
|
|
actions.append(f'=> /edit/{focused_cmt.id} ✏️ Edit\n')
|
2023-10-29 04:54:18 +00:00
|
|
|
|
if post and session.user and not session.is_context_locked and \
|
2023-10-26 12:59:30 +00:00
|
|
|
|
session.user.role != User.LIMITED:
|
2023-11-06 16:19:51 +00:00
|
|
|
|
actions.append(f'=> /comment/{post.id} 💬 Comment\n')
|
2023-07-18 05:41:26 +00:00
|
|
|
|
if session.is_thanks_enabled() and focused_cmt.user != user.id:
|
|
|
|
|
actions.append(f'=> /thanks/{focused_cmt.id} 🙏 Give thanks\n')
|
2023-11-06 16:19:51 +00:00
|
|
|
|
actions.append(f'=> /remind/{focused_cmt.id} 🔔 Remind me\n')
|
|
|
|
|
|
|
|
|
|
actions.append(f'=> /report/{focused_cmt.id} ⚠️ Report\n')
|
2023-11-06 13:45:05 +00:00
|
|
|
|
if not session.is_editable(focused_cmt) and session.is_deletable(focused_cmt):
|
|
|
|
|
actions.append(f'=> /edit/{focused_cmt.id}/delete/{session.get_token()} ❌ Delete comment\n')
|
2023-10-29 04:54:18 +00:00
|
|
|
|
else:
|
2023-11-06 13:45:05 +00:00
|
|
|
|
if session.is_deletable(focused_cmt):
|
2023-10-29 04:54:18 +00:00
|
|
|
|
actions.append(f'=> /edit/{focused_cmt.id}/delete/{session.get_token()} ❌ Delete comment\n')
|
2023-07-18 05:41:26 +00:00
|
|
|
|
if actions:
|
|
|
|
|
page += '\n## Actions\n' + ''.join(actions)
|
|
|
|
|
page += '\n' + session.dashboard_link()
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
2023-06-20 12:02:07 +00:00
|
|
|
|
if post:
|
2023-10-29 04:54:18 +00:00
|
|
|
|
op_section = '\n# Original Post\n\n' + session.feed_entry(post)
|
2023-06-20 12:02:07 +00:00
|
|
|
|
else:
|
|
|
|
|
op_section = ''
|
|
|
|
|
|
2023-06-20 05:49:35 +00:00
|
|
|
|
else:
|
|
|
|
|
page += session.render_post(post)
|
|
|
|
|
|
2023-05-23 04:48:14 +00:00
|
|
|
|
commits = []
|
|
|
|
|
incoming_xrefs = []
|
|
|
|
|
outgoing_xrefs = {}
|
2023-06-20 12:02:07 +00:00
|
|
|
|
repo = None
|
|
|
|
|
|
|
|
|
|
# Poll options/results.
|
|
|
|
|
if post:
|
|
|
|
|
poll = session.render_poll(post, show_results=not session.user)
|
|
|
|
|
if poll:
|
|
|
|
|
# Ensure separation.
|
|
|
|
|
if len(page) and not page.endswith('\n\n'):
|
|
|
|
|
page += '\n'
|
|
|
|
|
page += poll
|
|
|
|
|
|
|
|
|
|
if post.issueid:
|
|
|
|
|
repo = db.get_repository(subspace=session.context)
|
|
|
|
|
commits = db.get_commits(repo, issueid=post.issueid)
|
2023-06-20 18:05:21 +00:00
|
|
|
|
if not is_comment_page:
|
|
|
|
|
incoming_xrefs = db.get_issue_crossrefs(session.context,
|
|
|
|
|
incoming_to_issueid=post.issueid)
|
|
|
|
|
outgoing_xrefs = db.get_issue_crossrefs(session.context,
|
|
|
|
|
outgoing_from_issueid=post.issueid)
|
2023-06-20 12:02:07 +00:00
|
|
|
|
|
|
|
|
|
# Issue and commit cross references outgoing from the post body.
|
2023-06-20 18:05:21 +00:00
|
|
|
|
if repo and repo.view_url:
|
2023-06-20 12:02:07 +00:00
|
|
|
|
first = True
|
|
|
|
|
for commit in db.find_commits_by_hash(repo, parse_likely_commit_hashes(page)):
|
|
|
|
|
if first:
|
|
|
|
|
page += '\n'
|
|
|
|
|
first = False
|
|
|
|
|
page += commit.entry(repo.view_url, outgoing=True)
|
|
|
|
|
if outgoing_xrefs and post_id in outgoing_xrefs:
|
|
|
|
|
page += '\n'
|
|
|
|
|
for xref in outgoing_xrefs[post_id]:
|
|
|
|
|
page += xref.outgoing_entry()
|
|
|
|
|
else:
|
|
|
|
|
repo = None
|
2023-05-23 04:48:14 +00:00
|
|
|
|
|
2023-06-20 05:49:35 +00:00
|
|
|
|
if not is_comment_page:
|
|
|
|
|
# Post metadata.
|
|
|
|
|
if len(page):
|
2023-05-12 16:32:50 +00:00
|
|
|
|
page += '\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
if post.tags:
|
|
|
|
|
page += '### ' + post.tags + '\n'
|
|
|
|
|
poster_link = f'=> /u/{post.poster_name} {post.poster_avatar} {post.poster_name}\n'
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
page += f'=> /{session.context.title()} 🐞 Issue #{post.issueid} in {session.context.title()}\n'
|
|
|
|
|
elif not session.c_user:
|
|
|
|
|
page += f'=> /{session.context.title()} Posted in: {session.context.title()}\n'
|
|
|
|
|
page += poster_link
|
2023-11-05 18:20:00 +00:00
|
|
|
|
post_age = post.age() if not session.is_archive else post.ymd_hm()
|
|
|
|
|
page += f'{post_age}'
|
2023-11-06 13:45:05 +00:00
|
|
|
|
if (MUTE_POST, post.id) in session.user_mutes:
|
|
|
|
|
page += ' 🔇'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
if session.is_likes_enabled():
|
|
|
|
|
liked = []
|
|
|
|
|
if post.num_likes:
|
2023-08-17 18:40:57 +00:00
|
|
|
|
liked = db.get_likes(post, session.user_mutes)
|
2023-06-20 05:49:35 +00:00
|
|
|
|
page += ' · 👍 ' + ', '.join(liked)
|
|
|
|
|
if session.is_reactions_enabled():
|
2023-08-17 18:40:57 +00:00
|
|
|
|
reactions = db.get_reactions(post, session.user_mutes)
|
2023-06-20 05:49:35 +00:00
|
|
|
|
listed = []
|
|
|
|
|
for r in reactions:
|
2023-11-06 13:45:05 +00:00
|
|
|
|
listed.append(f'{r} {reactions[r]}')
|
2023-06-20 05:49:35 +00:00
|
|
|
|
if listed:
|
|
|
|
|
page += ' · ' + ' '.join(listed)
|
2023-05-12 16:32:50 +00:00
|
|
|
|
page += '\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
# Post actions.
|
|
|
|
|
kind = 'issue' if session.is_context_tracker else 'post'
|
|
|
|
|
if session.user and not session.is_context_locked:
|
|
|
|
|
page += '\n## Actions\n'
|
2023-05-12 16:32:50 +00:00
|
|
|
|
if session.is_editable(post):
|
|
|
|
|
page += f'=> /edit/{post.id} ✏️ Edit {kind}\n'
|
2023-11-06 16:19:51 +00:00
|
|
|
|
elif session.is_title_editable(post):
|
|
|
|
|
page += f'=> /edit/{post.id}/mod-title ✏️ Edit {kind} title\n'
|
2023-10-26 12:59:30 +00:00
|
|
|
|
if session.user.role != User.LIMITED:
|
|
|
|
|
page += f'=> /comment/{post.id} 💬 Comment\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
# Reactions.
|
|
|
|
|
if session.user.id != post.user:
|
|
|
|
|
if session.is_likes_enabled():
|
|
|
|
|
if session.user.name not in liked:
|
|
|
|
|
page += f'=> /like/{post.id} 👍 Like\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /unlike/{post.id} 👎 Undo like\n'
|
|
|
|
|
if session.is_reactions_enabled():
|
|
|
|
|
reaction = db.get_user_reaction(post, session.user.id)
|
|
|
|
|
if reaction:
|
|
|
|
|
page += f'=> /react/{post.id} Change reaction: {reaction}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /react/{post.id} {session.bubble.user_reactions[0]} React\n'
|
|
|
|
|
if session.is_thanks_enabled():
|
|
|
|
|
page += f'=> /thanks/{post.id} 🙏 Give thanks\n'
|
|
|
|
|
|
|
|
|
|
# Moderator actions on a post.
|
|
|
|
|
mod_actions = []
|
|
|
|
|
if session.user.id == post.user or session.is_user_mod:
|
|
|
|
|
if session.is_context_tracker:
|
2023-11-06 16:19:51 +00:00
|
|
|
|
mod_actions.append(f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n')
|
2023-06-20 05:49:35 +00:00
|
|
|
|
if '✔︎' in post.tags:
|
|
|
|
|
mod_actions.append(f'=> /edit-tags/{post.id}/open 🐞 Reopen issue\n')
|
|
|
|
|
else:
|
|
|
|
|
mod_actions.append(f'=> /edit-tags/{post.id}/close ✔︎ Mark as closed\n')
|
|
|
|
|
|
|
|
|
|
if mod_actions:
|
2023-11-06 13:45:05 +00:00
|
|
|
|
page += ''.join(mod_actions)
|
|
|
|
|
|
|
|
|
|
page += f'=> {post.id}/more More...\n'
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
page += '\n' + session.dashboard_link()
|
|
|
|
|
|
2023-06-23 15:01:56 +00:00
|
|
|
|
# Notification on this page.
|
2023-06-24 04:36:49 +00:00
|
|
|
|
if post and user:
|
2023-06-23 15:01:56 +00:00
|
|
|
|
notifs = db.get_notifications(user=user, post_id=post.id, sort_desc=True)
|
|
|
|
|
if notifs:
|
|
|
|
|
page += f'{len(notifs)} notification{plural_s(len(notifs))} about this post:\n'
|
|
|
|
|
for notif in notifs:
|
|
|
|
|
link, label = notif.entry(with_title=False)
|
|
|
|
|
page += f'=> {link} {label}\n'
|
|
|
|
|
if len(notifs) > 1:
|
|
|
|
|
page += f'=> {post.page_url()}/clear-notif/{session.get_token()} 🧹 Clear\n'
|
2023-05-21 18:23:30 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
# Comments, repository commits, and issue cross-references.
|
2023-06-20 12:02:07 +00:00
|
|
|
|
comments = db.get_posts(parent=post_id,
|
2023-06-02 15:16:41 +00:00
|
|
|
|
draft=False,
|
|
|
|
|
sort_descending=False,
|
|
|
|
|
muted_by_user_id=(user.id if user else 0),
|
|
|
|
|
limit=None)
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
|
|
|
|
if is_comment_page:
|
2023-06-20 12:02:07 +00:00
|
|
|
|
if post:
|
|
|
|
|
# Omit comments older than the focused one.
|
|
|
|
|
comments = list(filter(lambda p: p.ts_created > focused_cmt.ts_created, comments))
|
|
|
|
|
else:
|
2023-06-20 16:56:28 +00:00
|
|
|
|
# Deleted post; only see your own comments.
|
|
|
|
|
comments = list(filter(lambda p: p.id != focused_cmt.id and p.user == focused_cmt.user,
|
|
|
|
|
comments))
|
2023-06-20 05:49:35 +00:00
|
|
|
|
|
2023-10-29 04:54:18 +00:00
|
|
|
|
have_other_comments = False
|
2023-05-12 16:32:50 +00:00
|
|
|
|
n = len(comments)
|
2023-10-29 04:54:18 +00:00
|
|
|
|
|
2023-05-14 20:55:35 +00:00
|
|
|
|
if n > 0 or commits or incoming_xrefs:
|
2023-10-29 04:54:18 +00:00
|
|
|
|
have_other_comments = True
|
2023-05-15 09:21:22 +00:00
|
|
|
|
if n > 1:
|
|
|
|
|
dir_icon = ' ↑' if display_order_desc else ' ↓'
|
|
|
|
|
else:
|
|
|
|
|
dir_icon = ''
|
2023-06-20 12:02:07 +00:00
|
|
|
|
page += f'\n## {n} {"Later " if is_comment_page and post else "Other " if is_comment_page else ""}Comment{plural_s(n)}{dir_icon}\n'
|
2023-05-12 16:32:50 +00:00
|
|
|
|
|
|
|
|
|
if commits or incoming_xrefs:
|
|
|
|
|
# Combine commits and commits into one list.
|
|
|
|
|
comments += commits
|
|
|
|
|
comments += incoming_xrefs
|
|
|
|
|
comments.sort(key=lambda c: c.ts if isinstance(c, Commit) else c.ts_created)
|
|
|
|
|
|
|
|
|
|
# TODO: This may need paging when there is a long thread.
|
|
|
|
|
rendered_comments = []
|
2023-11-05 18:20:00 +00:00
|
|
|
|
ts_now = time.time()
|
2023-05-12 16:32:50 +00:00
|
|
|
|
for cmt in comments:
|
|
|
|
|
|
|
|
|
|
# Commits are shown as links to the Git viewer.
|
|
|
|
|
if isinstance(cmt, Commit):
|
2023-06-23 15:01:56 +00:00
|
|
|
|
if not focused_cmt or cmt.ts > focused_cmt.ts_created:
|
|
|
|
|
rendered_comments.append(cmt.entry(repo.view_url))
|
2023-05-12 16:32:50 +00:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Cross-references incoming from other issues.
|
|
|
|
|
if isinstance(cmt, Crossref):
|
2023-05-23 04:48:14 +00:00
|
|
|
|
rendered_comments.append(cmt.incoming_entry())
|
2023-05-12 16:32:50 +00:00
|
|
|
|
continue
|
|
|
|
|
|
2023-11-05 18:20:00 +00:00
|
|
|
|
comment_age = cmt.age() if not session.is_archive and ts_now - cmt.ts_created < 24 * 3600 else \
|
|
|
|
|
cmt.ymd_hm(tz=session.tz, time_prefix='at ')
|
|
|
|
|
|
2023-06-20 12:22:38 +00:00
|
|
|
|
if not session.is_archive:
|
2023-11-05 18:20:00 +00:00
|
|
|
|
src = f'=> /u/{cmt.poster_name}/{cmt.id} {cmt.poster_avatar} {cmt.poster_name} · {comment_age}:\n'
|
2023-06-20 12:22:38 +00:00
|
|
|
|
else:
|
2023-11-05 18:20:00 +00:00
|
|
|
|
src = f'=> /u/{cmt.poster_name} {cmt.poster_avatar} {cmt.poster_name} · {comment_age}:\n'
|
2023-05-26 10:21:57 +00:00
|
|
|
|
comment_body = session.render_post(cmt)
|
|
|
|
|
src += comment_body
|
|
|
|
|
|
|
|
|
|
# Commit references.
|
|
|
|
|
if repo and repo.view_url:
|
|
|
|
|
for commit in db.find_commits_by_hash(repo, parse_likely_commit_hashes(comment_body)):
|
|
|
|
|
src += commit.entry(repo.view_url, outgoing=True)
|
2023-05-12 16:32:50 +00:00
|
|
|
|
|
|
|
|
|
# Cross-references to other issues.
|
|
|
|
|
if outgoing_xrefs and cmt.id in outgoing_xrefs:
|
|
|
|
|
for xref in outgoing_xrefs[cmt.id]:
|
2023-05-23 04:48:14 +00:00
|
|
|
|
src += xref.outgoing_entry()
|
2023-05-12 16:32:50 +00:00
|
|
|
|
|
|
|
|
|
# Hide the `age` if it's the same as the previous entry (in reading order).
|
2023-11-05 18:20:00 +00:00
|
|
|
|
# comment_age = cmt.age() if not session.is_archive else cmt.ymd_hm()
|
|
|
|
|
# if comment_age != last_age:
|
|
|
|
|
# last_age = comment_age
|
|
|
|
|
# else:
|
|
|
|
|
# comment_age = ''
|
|
|
|
|
|
|
|
|
|
# if session.user and (cmt.user == session.user.id or session.is_user_mod) and \
|
|
|
|
|
# not session.is_context_locked:
|
|
|
|
|
# # Actions on your own comments.
|
|
|
|
|
# age_suffix = f" · {comment_age}" if len(comment_age) else comment_age
|
|
|
|
|
# if session.is_editable(cmt) and post:
|
|
|
|
|
# src += f'=> /edit/{cmt.id} ✏️ Edit{age_suffix}\n'
|
|
|
|
|
# elif session.is_deletable(cmt):
|
|
|
|
|
# src += f'=> /edit/{cmt.id}/delete/{session.get_token()} ❌ Delete{age_suffix}\n'
|
|
|
|
|
# elif len(comment_age):
|
|
|
|
|
# src += comment_age + '\n'
|
2023-05-12 16:32:50 +00:00
|
|
|
|
|
|
|
|
|
rendered_comments.append(src)
|
|
|
|
|
|
|
|
|
|
# Print in the appropriate order.
|
|
|
|
|
if display_order_desc:
|
|
|
|
|
rendered_comments.reverse()
|
|
|
|
|
for rendered in rendered_comments:
|
|
|
|
|
page += '\n' + rendered
|
2023-05-14 20:55:35 +00:00
|
|
|
|
|
|
|
|
|
# Show the Comment action at the appropriate place wrt reading direction.
|
2023-10-29 04:54:18 +00:00
|
|
|
|
if (have_other_comments
|
|
|
|
|
and session.user
|
2023-10-26 12:59:30 +00:00
|
|
|
|
and session.user.role != User.LIMITED
|
2023-07-12 05:01:31 +00:00
|
|
|
|
and post
|
|
|
|
|
and not session.is_context_locked
|
|
|
|
|
and not display_order_desc
|
|
|
|
|
and (is_comment_page or len(comments) >= 1)):
|
2023-06-20 05:49:35 +00:00
|
|
|
|
page += f'\n=> /comment/{post.id} 💬 Add comment\n'
|
2023-05-14 20:55:35 +00:00
|
|
|
|
|
2023-07-12 05:01:31 +00:00
|
|
|
|
if is_comment_page:
|
|
|
|
|
page += op_section
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
return page
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_feed_page(session):
|
2023-11-05 11:36:53 +00:00
|
|
|
|
# NOTE: Some parameters were configured in `make_post_page_or_configure_feed()`.
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
req = session.req
|
|
|
|
|
db = session.db
|
|
|
|
|
c_user = session.c_user
|
|
|
|
|
context = session.context
|
|
|
|
|
context_mods = session.context_mods
|
2023-05-10 12:20:32 +00:00
|
|
|
|
is_issue_tracker = session.is_context_tracker
|
2023-05-06 13:29:21 +00:00
|
|
|
|
user = session.user
|
|
|
|
|
user_follows = session.user_follows
|
2023-06-02 15:16:41 +00:00
|
|
|
|
user_mutes = session.user_mutes
|
2023-07-17 12:31:47 +00:00
|
|
|
|
query_params = req.query.split('&') if req.query else []
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page = ''
|
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
# Determine format of feed.
|
|
|
|
|
is_atom_feed = False
|
|
|
|
|
is_gemini_feed = False
|
|
|
|
|
is_tinylog = False
|
|
|
|
|
is_bubble_feed = False
|
|
|
|
|
|
|
|
|
|
if 'feed' in query_params:
|
|
|
|
|
is_gemini_feed = True
|
|
|
|
|
elif 'atom' in query_params:
|
|
|
|
|
is_atom_feed = True
|
|
|
|
|
elif 'tinylog' in query_params:
|
|
|
|
|
is_tinylog = True
|
|
|
|
|
else:
|
|
|
|
|
is_bubble_feed = True
|
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
sort_hotness = False
|
2023-11-03 15:50:50 +00:00
|
|
|
|
rotate_per_day = False
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page_size = 50 if is_gemini_feed else 100 if is_tinylog else 25
|
|
|
|
|
page_index = 0
|
|
|
|
|
|
|
|
|
|
if is_tinylog and not c_user:
|
|
|
|
|
return 51, "Tinylogs are only for user feeds"
|
|
|
|
|
|
|
|
|
|
# Page title.
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if is_atom_feed:
|
|
|
|
|
# Just print the entries, and add the header/footer in the end.
|
|
|
|
|
ts_last_updated = 0
|
|
|
|
|
elif is_gemini_feed or is_tinylog:
|
2023-05-11 19:51:36 +00:00
|
|
|
|
page += f'# {session.feed_title()}\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
elif c_user:
|
|
|
|
|
page += f'# {c_user.avatar} {context.title()}\n'
|
|
|
|
|
elif context:
|
|
|
|
|
page += f'# {context.title()}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += session.BANNER
|
|
|
|
|
|
|
|
|
|
# Subspace description.
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo = ''
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if is_atom_feed:
|
|
|
|
|
pass
|
|
|
|
|
elif not context:
|
2023-05-28 06:58:52 +00:00
|
|
|
|
topinfo += f"{session.bubble.site_info if session.user else session.bubble.site_info_nouser}\n"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
else:
|
|
|
|
|
if c_user and (c_user.info or c_user.url):
|
|
|
|
|
if c_user.info:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += c_user.info + '\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if c_user.url:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += f'=> {c_user.url}\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
elif context:
|
|
|
|
|
if context.info:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += context.info + '\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if context.url:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += f'=> {context.url}\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
# Users moderating this subspace.
|
|
|
|
|
for mod in context_mods:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += f'=> /u/{mod.name} {mod.avatar} Moderated by: {mod.name}\n'
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if session.is_context_locked:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += '=> /help/locked 🔒 Locked\n'
|
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if topinfo:
|
|
|
|
|
page += topinfo if not is_tinylog else clean_tinylog(topinfo)
|
|
|
|
|
page += '\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-06-15 09:24:20 +00:00
|
|
|
|
filter_by_followed = user if session.feed_mode == 'followed' else None
|
|
|
|
|
filter_issue_status = True if session.feed_mode == 'open' else \
|
|
|
|
|
False if session.feed_mode == 'closed' else None
|
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if is_bubble_feed:
|
2023-11-03 15:50:50 +00:00
|
|
|
|
# Sorting mode and current page.
|
|
|
|
|
sort_hotness = (user and user.sort_post == User.SORT_POST_HOTNESS)
|
|
|
|
|
if not is_empty_query(req):
|
|
|
|
|
for param in query_params:
|
|
|
|
|
if param == 'sort=hot':
|
|
|
|
|
sort_hotness = True
|
|
|
|
|
elif param == 'sort=new':
|
|
|
|
|
sort_hotness = False
|
|
|
|
|
elif re.match(r'p\d+', param):
|
|
|
|
|
page_index = int(param[1:]) - 1
|
|
|
|
|
sort_mode = ' 🔥' if sort_hotness else ''
|
|
|
|
|
|
|
|
|
|
rotate_per_day = (session.is_rotation_enabled()
|
|
|
|
|
and not context
|
|
|
|
|
and not sort_hotness
|
|
|
|
|
and not filter_by_followed)
|
|
|
|
|
|
|
|
|
|
# Pagination.
|
2023-06-15 09:24:20 +00:00
|
|
|
|
num_total = db.count_posts(subspace=context,
|
|
|
|
|
draft=False,
|
|
|
|
|
filter_by_followed=filter_by_followed,
|
|
|
|
|
filter_issue_status=filter_issue_status,
|
|
|
|
|
filter_tag=session.feed_tag_filter,
|
2023-11-03 15:50:50 +00:00
|
|
|
|
muted_by_user_id=(user.id if user else 0),
|
|
|
|
|
rotate_per_day=rotate_per_day)
|
2023-06-15 09:24:20 +00:00
|
|
|
|
num_pages = int((num_total + page_size - 1) / page_size)
|
|
|
|
|
|
2023-05-29 19:40:32 +00:00
|
|
|
|
# Filter status.
|
|
|
|
|
filter_mode = ''
|
|
|
|
|
if session.feed_tag_filter:
|
|
|
|
|
filter_mode = f' [#{session.feed_tag_filter}]'
|
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
# Navigation menu.
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not user:
|
|
|
|
|
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
|
|
|
|
|
page += session.FOOTER_MENU
|
|
|
|
|
else:
|
2023-10-27 08:09:57 +00:00
|
|
|
|
if session.user.role == User.LIMITED:
|
|
|
|
|
link_suffix = '/' + session.get_token()
|
|
|
|
|
else:
|
|
|
|
|
link_suffix = ''
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += session.dashboard_link()
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if not session.is_context_locked:
|
|
|
|
|
if c_user and c_user.id == user.id:
|
2023-10-27 08:09:57 +00:00
|
|
|
|
page += f'=> /u/{user.name}/post{link_suffix} 💬 New post\n'
|
|
|
|
|
page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft\n'
|
2023-05-07 21:04:14 +00:00
|
|
|
|
elif context and context.owner == 0:
|
2023-05-10 18:46:55 +00:00
|
|
|
|
if is_issue_tracker:
|
2023-10-26 12:59:30 +00:00
|
|
|
|
if session.user.role != User.LIMITED:
|
|
|
|
|
page += f'=> /{context.title()}/post 🐞 New issue in s/{context.name}\n'
|
2023-05-10 18:46:55 +00:00
|
|
|
|
else:
|
2023-10-26 12:59:30 +00:00
|
|
|
|
if session.user.role != User.LIMITED:
|
|
|
|
|
page += f'=> /{context.title()}/post 💬 New post in s/{context.name}\n'
|
2023-10-27 08:09:57 +00:00
|
|
|
|
page += f'=> /{context.title()}/compose{link_suffix} ✏️ Compose draft in s/{context.name}\n'
|
2023-05-07 21:04:14 +00:00
|
|
|
|
else:
|
2023-10-27 08:09:57 +00:00
|
|
|
|
page += f'=> /u/{user.name}/post{link_suffix} 💬 New post in u/{user.name}\n'
|
|
|
|
|
page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft in u/{user.name}\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
|
|
|
|
|
|
2023-05-27 06:11:58 +00:00
|
|
|
|
if is_issue_tracker:
|
|
|
|
|
page += f'\n=> /{context.title()}/search 🔍 Search\n'
|
2023-05-29 19:40:32 +00:00
|
|
|
|
page += f'=> /{context.title()}/tag 🏷️ Tags\n'
|
2023-05-27 06:11:58 +00:00
|
|
|
|
if session.feed_mode in ('all', 'closed'):
|
|
|
|
|
page += f'=> ? 🐞 Show open\n'
|
|
|
|
|
if session.feed_mode in ('all', 'open'):
|
|
|
|
|
page += f'=> ?closed ✔︎ Show closed\n'
|
|
|
|
|
if session.feed_mode in ('open', 'closed'):
|
|
|
|
|
page += f'=> ?open&closed Show all\n'
|
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
# Page title.
|
|
|
|
|
if session.feed_mode == 'all':
|
|
|
|
|
if is_issue_tracker:
|
|
|
|
|
page_title = 'Issues'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
else:
|
2023-07-17 12:31:47 +00:00
|
|
|
|
page_title = 'All Posts' if not context else 'Posts'
|
|
|
|
|
elif session.feed_mode in ('open', 'closed'):
|
|
|
|
|
page_title = 'Open Issues' if session.feed_mode == 'open' else 'Closed Issues'
|
|
|
|
|
else:
|
|
|
|
|
page_title = 'Followed'
|
2023-05-29 19:40:32 +00:00
|
|
|
|
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if is_bubble_feed:
|
2023-06-15 09:24:20 +00:00
|
|
|
|
if is_issue_tracker:
|
|
|
|
|
title_count = f'{num_total} '
|
|
|
|
|
else:
|
|
|
|
|
title_count = ''
|
|
|
|
|
page += f'\n## {title_count}{page_title}{sort_mode}{filter_mode}\n\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
elif is_tinylog:
|
|
|
|
|
page += f'author: @{c_user.name}@{session.bubble.hostname}\n'
|
|
|
|
|
page += f'avatar: {c_user.avatar}\n\n'
|
|
|
|
|
|
|
|
|
|
posts = db.get_posts(subspace=context,
|
2023-05-07 06:09:10 +00:00
|
|
|
|
comment=False,
|
|
|
|
|
draft=False,
|
|
|
|
|
sort_hotness=sort_hotness,
|
2023-06-02 15:16:41 +00:00
|
|
|
|
notifs_for_user_id=(user.id if user else 0),
|
2023-05-07 06:09:10 +00:00
|
|
|
|
filter_by_followed=filter_by_followed,
|
2023-05-10 12:20:32 +00:00
|
|
|
|
filter_issue_status=filter_issue_status,
|
2023-05-29 19:40:32 +00:00
|
|
|
|
filter_tag=session.feed_tag_filter,
|
2023-06-02 15:16:41 +00:00
|
|
|
|
muted_by_user_id=(user.id if user else 0),
|
2023-07-18 14:31:13 +00:00
|
|
|
|
gemini_feed=is_gemini_feed or is_atom_feed,
|
2023-11-03 15:50:50 +00:00
|
|
|
|
rotate_per_day=rotate_per_day,
|
2023-05-07 06:09:10 +00:00
|
|
|
|
limit=page_size,
|
|
|
|
|
page=page_index)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
if len(posts) == 0 and page_index == 0:
|
2023-05-10 12:20:32 +00:00
|
|
|
|
if is_issue_tracker:
|
2023-05-11 18:44:22 +00:00
|
|
|
|
if session.feed_mode == 'open':
|
|
|
|
|
page += "All clear! "
|
2023-05-21 12:30:47 +00:00
|
|
|
|
page += "There are no issues.\n\n"
|
2023-05-10 12:20:32 +00:00
|
|
|
|
else:
|
|
|
|
|
page += "There are no posts.\n\n\n" + \
|
2023-07-17 12:31:47 +00:00
|
|
|
|
f"> {session.EMPTY_FEED_PLACEHOLDER}\n\n\n"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
elif is_gemini_feed:
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.gemini_feed_entry(post, context)
|
2023-07-17 12:31:47 +00:00
|
|
|
|
elif is_atom_feed:
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.atom_feed_entry(post, context)
|
|
|
|
|
ts_last_updated = max(ts_last_updated, post.ts_created)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
elif is_tinylog:
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.tinylog_entry(post) + '\n'
|
|
|
|
|
else:
|
2023-06-15 16:25:48 +00:00
|
|
|
|
pager_feed_mode = f'&{session.feed_mode}' if session.feed_mode != 'all' else ''
|
2023-07-17 12:31:47 +00:00
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
def page_range(n):
|
|
|
|
|
return f'{n + 1} / {num_pages}'
|
|
|
|
|
|
|
|
|
|
if page_index > 0:
|
2023-06-15 16:25:48 +00:00
|
|
|
|
page += f'=> ?p{page_index}{pager_feed_mode} Previous page\n\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.feed_entry(post, context) + '\n'
|
|
|
|
|
|
|
|
|
|
if len(posts) > 0 and page_index < num_pages - 1:
|
2023-06-15 16:25:48 +00:00
|
|
|
|
page += f'=> ?p{page_index + 2}{pager_feed_mode} Next page\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
if num_pages > 1:
|
2023-06-17 10:50:09 +00:00
|
|
|
|
page += f'Page {page_index + 1} of {num_pages}\n\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
# Footer.
|
2023-07-17 12:31:47 +00:00
|
|
|
|
if is_atom_feed:
|
2023-07-18 05:41:47 +00:00
|
|
|
|
origin_url = f"{session.server_root()}/{urlparse.quote(context.title()) if context else ''}"
|
2023-07-17 12:31:47 +00:00
|
|
|
|
atom_header = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
|
|
|
<title>{session.feed_title()}</title>
|
|
|
|
|
<link href="{origin_url}" />
|
|
|
|
|
<link rel="self" type="application/atom+xml" href="{session.req.url()}" />
|
2023-07-18 05:41:47 +00:00
|
|
|
|
<id>{session.req.url()}</id>
|
2023-07-17 12:31:47 +00:00
|
|
|
|
<updated>{atom_timestamp(ts_last_updated if ts_last_updated else time.time())}</updated>
|
|
|
|
|
<generator uri="https://gmi.skyjake.fi/bubble/" version="{session.bubble.version}">Bubble</generator>
|
|
|
|
|
"""
|
2023-07-18 05:41:47 +00:00
|
|
|
|
atom_footer = "\n</feed>"
|
2023-07-17 12:31:47 +00:00
|
|
|
|
return 20, 'application/atom+xml', atom_header + page + atom_footer
|
|
|
|
|
|
|
|
|
|
elif not is_tinylog:
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not is_gemini_feed:
|
|
|
|
|
page += "## Options\n"
|
|
|
|
|
if sort_hotness:
|
|
|
|
|
page += "=> ?sort=new 🕑 Sort by most recent\n"
|
|
|
|
|
else:
|
|
|
|
|
page += "=> ?sort=hot 🔥 Sort by hotness\n"
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if not context:
|
|
|
|
|
if session.feed_mode == 'followed':
|
|
|
|
|
page += '=> /all All Posts\n'
|
|
|
|
|
else:
|
|
|
|
|
page += '=> /followed Followed\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += "=> ?feed Gemini feed\n"
|
2023-07-17 14:35:29 +00:00
|
|
|
|
page += "=> ?atom Atom feed\n"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if c_user:
|
|
|
|
|
page += "=> ?tinylog Tinylog\n"
|
|
|
|
|
if user:
|
2023-05-08 19:39:56 +00:00
|
|
|
|
# Search.
|
2023-05-27 06:11:58 +00:00
|
|
|
|
if not is_issue_tracker:
|
|
|
|
|
if context:
|
|
|
|
|
page += f'=> /{context.title()}/search 🔍 Search in {context.title()}\n'
|
2023-06-05 07:00:56 +00:00
|
|
|
|
page += f'=> /{context.title()}/tag 🏷️ Tags\n'
|
2023-05-27 06:11:58 +00:00
|
|
|
|
else:
|
|
|
|
|
page += '=> /search 🔍 Search\n'
|
2023-06-05 07:00:56 +00:00
|
|
|
|
page += '=> /tag 🏷️ Tags\n'
|
2023-05-08 19:39:56 +00:00
|
|
|
|
|
|
|
|
|
# Settings.
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if context and not c_user and session.is_user_mod:
|
|
|
|
|
page += f'=> /{context.title()}/admin 🌒 Subspace admin\n'
|
|
|
|
|
page += "=> /settings ⚙️ Settings\n\n"
|
2023-05-08 19:39:56 +00:00
|
|
|
|
|
2023-10-25 09:16:14 +00:00
|
|
|
|
if session.is_antenna_enabled() and c_user and user.id == c_user.id:
|
2023-05-13 11:55:30 +00:00
|
|
|
|
antenna_feed = f"{session.server_root()}{session.path}u/{user.name}/antenna"
|
2023-11-06 17:21:51 +00:00
|
|
|
|
#page += f'=> {session.bubble.antenna_url}?{urlparse.quote(antenna_feed)} Submit feed to 📡 Antenna\n'
|
|
|
|
|
for link in session.bubble.antenna_links('feed', antenna_feed):
|
|
|
|
|
page += link
|
2023-05-12 03:30:50 +00:00
|
|
|
|
|
2023-06-02 15:16:41 +00:00
|
|
|
|
# Following and muting.
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if c_user and user.id != c_user.id:
|
|
|
|
|
if (FOLLOW_USER, c_user.id) in user_follows:
|
|
|
|
|
page += f'=> /unfollow/{c_user.name} ➖ Unfollow {c_user.name}\n'
|
2023-06-02 15:16:41 +00:00
|
|
|
|
elif not (MUTE_USER, c_user.id) in user_mutes:
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += f'=> /follow/{c_user.name} ➕ Follow {c_user.name}\n'
|
|
|
|
|
page += f'You will be notified when {c_user.name} posts anywhere on {session.bubble.site_name}.\n'
|
2023-06-02 15:16:41 +00:00
|
|
|
|
if (MUTE_USER, c_user.id) in user_mutes:
|
|
|
|
|
page += f'=> /unmute/{c_user.name} 🔈 Unmute {c_user.name}\n'
|
|
|
|
|
elif not (FOLLOW_USER, c_user.id) in user_follows:
|
|
|
|
|
page += f'=> /mute/{c_user.name} 🔇 Mute {c_user.name}\n'
|
|
|
|
|
page += f'You will not see posts or comments by {c_user.name} anywhere on {session.bubble.site_name}.\n'
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if context and context.owner != user.id and not session.is_context_locked:
|
2023-06-02 15:16:41 +00:00
|
|
|
|
if not c_user or not (MUTE_USER, c_user.id) in user_mutes:
|
2023-11-06 18:13:09 +00:00
|
|
|
|
page += '\n'
|
2023-06-02 15:16:41 +00:00
|
|
|
|
if (MUTE_SUBSPACE, context.id) in user_mutes:
|
|
|
|
|
page += f'=> /unmute/{context.title()} 🔈 Unmute subspace {context.title()}\n'
|
|
|
|
|
elif (FOLLOW_SUBSPACE, context.id) in user_follows:
|
|
|
|
|
page += f'=> /unfollow/{context.title()} ➖ Unfollow subspace {context.title()}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /follow/{context.title()} ➕ Follow subspace {context.title()}\n'
|
|
|
|
|
if context.id not in user.moderated_subspace_ids:
|
|
|
|
|
page += f'=> /mute/{context.title()} 🔇 Mute subspace {context.title()}\n'
|
2023-11-06 18:13:09 +00:00
|
|
|
|
page += "Posts from the subspace will not appear in All Posts.\n"
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-07 21:04:14 +00:00
|
|
|
|
if not page.endswith('\n\n'):
|
|
|
|
|
page += '\n'
|
2023-05-07 06:09:10 +00:00
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += session.FOOTER_MENU
|
|
|
|
|
else:
|
|
|
|
|
page += '\n'
|
|
|
|
|
if c_user:
|
|
|
|
|
page += c_user.subspace_link()
|
|
|
|
|
elif context:
|
|
|
|
|
page += context.subspace_link()
|
|
|
|
|
page += session.FOOTER_MENU
|
|
|
|
|
return page
|