2023-05-06 13:29:21 +00:00
|
|
|
|
import re
|
2023-05-11 18:44:22 +00:00
|
|
|
|
from model import User, Segment, Subspace, Commit, Crossref, \
|
2023-05-06 13:29:21 +00:00
|
|
|
|
FOLLOW_SUBSPACE, FOLLOW_USER, FOLLOW_POST
|
2023-05-10 10:01:08 +00:00
|
|
|
|
from subspace import subspace_admin_actions
|
2023-05-06 13:29:21 +00:00
|
|
|
|
from utils import *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_post_page_or_configure_feed(session):
|
|
|
|
|
# This may return None, in which case should make a feed page.
|
|
|
|
|
|
|
|
|
|
db = session.db
|
|
|
|
|
req = session.req
|
|
|
|
|
|
|
|
|
|
# User or subspace feed.
|
|
|
|
|
path = req.path[len(session.path):]
|
2023-05-12 03:30:50 +00:00
|
|
|
|
found = re.match(r'(u|s)/([\w-]+)(/(post|compose|image|file|issues|admin))?(/([\w\d-]+)(.*))?', path)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if not found:
|
|
|
|
|
return 59, 'Bad request'
|
|
|
|
|
|
|
|
|
|
# Set up the feed parameters.
|
|
|
|
|
session.feed_type = found.group(1)
|
|
|
|
|
url_name = found.group(2)
|
|
|
|
|
action = found.group(4)
|
|
|
|
|
arg = found.group(6)
|
2023-05-12 03:30:50 +00:00
|
|
|
|
arg2 = found.group(7)
|
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-05-06 13:29:21 +00:00
|
|
|
|
else:
|
|
|
|
|
session.c_user = None
|
|
|
|
|
session.context = db.get_subspace(name=url_name)
|
|
|
|
|
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':
|
|
|
|
|
post = db.get_post_for_issueid(session.context, int(arg))
|
|
|
|
|
if not post:
|
|
|
|
|
return 51, 'Not found'
|
|
|
|
|
return 30, post.page_url()
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
if action == 'compose':
|
|
|
|
|
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-05-06 13:29:21 +00:00
|
|
|
|
draft_id = db.create_post(session.user, session.context.id)
|
|
|
|
|
return 30, '/edit/%d' % draft_id
|
|
|
|
|
|
|
|
|
|
if action == 'post':
|
|
|
|
|
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-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-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)
|
|
|
|
|
|
|
|
|
|
if arg:
|
|
|
|
|
# Viewing a single post.
|
|
|
|
|
post = db.get_post(id=int(arg), draft=False)
|
|
|
|
|
if not post:
|
|
|
|
|
return 51, 'Not found'
|
|
|
|
|
if post.subspace != session.context.id:
|
|
|
|
|
return 51, 'Not found'
|
2023-05-08 19:39:56 +00:00
|
|
|
|
if post.parent != 0:
|
|
|
|
|
# Comments cannot be viewed individually.
|
|
|
|
|
# TODO: Make this possible.
|
|
|
|
|
return 30, db.get_post(id=post.parent).page_url()
|
|
|
|
|
|
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-05-12 16:32:50 +00:00
|
|
|
|
page = make_post_page(session, post)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
return page
|
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):
|
|
|
|
|
db = session.db
|
2023-05-21 18:23:30 +00:00
|
|
|
|
user = session.user
|
2023-05-07 21:04:14 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
page = session.render_post(post)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-23 04:48:14 +00:00
|
|
|
|
commits = []
|
|
|
|
|
incoming_xrefs = []
|
|
|
|
|
outgoing_xrefs = {}
|
|
|
|
|
if post.issueid:
|
|
|
|
|
repo = db.get_repository(subspace=session.context)
|
|
|
|
|
commits = db.get_commits(repo, issueid=post.issueid)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# Cross references outgoing from the main post itself.
|
|
|
|
|
if outgoing_xrefs and post.id in outgoing_xrefs:
|
|
|
|
|
page += '\n'
|
|
|
|
|
for xref in outgoing_xrefs[post.id]:
|
|
|
|
|
page += xref.outgoing_entry()
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
# Poll.
|
|
|
|
|
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
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
# Metadata.
|
|
|
|
|
if len(page):
|
|
|
|
|
page += '\n'
|
|
|
|
|
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
|
|
|
|
|
last_age = post.age()
|
|
|
|
|
page += f'{last_age}'
|
|
|
|
|
liked = []
|
|
|
|
|
if post.num_likes:
|
|
|
|
|
liked = db.get_likes(post)
|
|
|
|
|
page += ' · 👍 ' + ', '.join(liked)
|
|
|
|
|
page += '\n'
|
|
|
|
|
|
|
|
|
|
# Post actions.
|
|
|
|
|
kind = 'issue' if session.is_context_tracker else 'post'
|
|
|
|
|
if session.user and not session.is_context_locked:
|
|
|
|
|
page += '\n## Actions\n'
|
|
|
|
|
if session.user.id == post.user or session.is_user_mod:
|
|
|
|
|
if session.is_context_tracker:
|
|
|
|
|
if '✔︎' in post.tags:
|
|
|
|
|
page += f'=> /edit-tags/{post.id}/open 🐞 Reopen issue\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /edit-tags/{post.id}/close ✔︎ Mark as closed\n'
|
|
|
|
|
if session.is_editable(post):
|
|
|
|
|
page += f'=> /edit/{post.id} ✏️ Edit {kind}\n'
|
|
|
|
|
page += f'=> /comment/{post.id} 💬 Comment\n'
|
|
|
|
|
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.user.id == post.user or session.is_user_mod:
|
|
|
|
|
page += f'=> /edit-tags/{post.id} 🏷️ Add/remove tags\n'
|
|
|
|
|
|
|
|
|
|
if session.user.id != post.user:
|
|
|
|
|
if (FOLLOW_POST, post.id) in session.user_follows:
|
|
|
|
|
page += f'=> /unfollow/post/{post.id} ➖ Unfollow {kind}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /follow/post/{post.id} ➕ Follow {kind}\n'
|
|
|
|
|
|
2023-05-15 09:21:22 +00:00
|
|
|
|
mod_actions = []
|
|
|
|
|
if session.is_title_editable(post) and not session.is_editable(post):
|
|
|
|
|
mod_actions.append(f'=> /edit/{post.id}/mod-title ✏️ Edit {kind} title\n')
|
2023-05-14 20:33:49 +00:00
|
|
|
|
if session.is_deletable(post) and not session.is_editable(post):
|
2023-05-15 09:21:22 +00:00
|
|
|
|
mod_actions.append(f'=> /edit/{post.id}/delete/{session.get_token()} ❌ Delete {kind}\n')
|
|
|
|
|
if mod_actions:
|
|
|
|
|
page += '\n' + ''.join(mod_actions)
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
if session.user.id == post.user and post.sub_owner == post.user:
|
|
|
|
|
antenna_feed = f"gemini://{session.bubble.hostname}{session.path}u/{session.user.name}/{post.id}/antenna"
|
2023-05-14 20:33:49 +00:00
|
|
|
|
page += f'\n=> {session.bubble.antenna_url}?{urlparse.quote(antenna_feed)} Submit post to 📡 Antenna\n'
|
|
|
|
|
|
|
|
|
|
page += '\n' + session.dashboard_link()
|
2023-05-12 16:32:50 +00:00
|
|
|
|
|
2023-05-21 18:23:30 +00:00
|
|
|
|
notifs = db.get_notifications(user=user, post_id=post.id)
|
|
|
|
|
if notifs:
|
2023-05-21 20:37:27 +00:00
|
|
|
|
page += f'{len(notifs)} notification{plural_s(len(notifs))} on this page:\n'
|
2023-05-21 18:23:30 +00:00
|
|
|
|
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-12 16:32:50 +00:00
|
|
|
|
# Comments, repository commits, and issue cross-references.
|
|
|
|
|
display_order_desc = not session.user or \
|
|
|
|
|
session.user.sort_cmt == User.SORT_COMMENT_NEWEST
|
|
|
|
|
comments = db.get_posts(parent=post.id, draft=False, sort_descending=False, limit=None)
|
2023-05-23 04:48:14 +00:00
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
n = len(comments)
|
2023-05-14 20:55:35 +00:00
|
|
|
|
if n > 0 or commits or incoming_xrefs:
|
2023-05-15 09:21:22 +00:00
|
|
|
|
if n > 1:
|
|
|
|
|
dir_icon = ' ↑' if display_order_desc else ' ↓'
|
|
|
|
|
else:
|
|
|
|
|
dir_icon = ''
|
|
|
|
|
page += f'\n## {n} Comment{plural_s(n)}{dir_icon}'
|
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 = []
|
|
|
|
|
for cmt in comments:
|
|
|
|
|
|
|
|
|
|
# Commits are shown as links to the Git viewer.
|
|
|
|
|
if isinstance(cmt, Commit):
|
2023-05-23 04:48:14 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
src = f'=> /u/{cmt.poster_name} {cmt.poster_avatar} {cmt.poster_name}\n'
|
|
|
|
|
src += session.render_post(cmt)
|
|
|
|
|
|
|
|
|
|
# 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).
|
|
|
|
|
comment_age = cmt.age()
|
|
|
|
|
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.
|
2023-05-14 21:46:48 +00:00
|
|
|
|
age_suffix = f" · {comment_age}" if len(comment_age) else comment_age
|
|
|
|
|
if session.is_editable(cmt):
|
|
|
|
|
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'
|
2023-05-12 16:32:50 +00:00
|
|
|
|
elif len(comment_age):
|
|
|
|
|
src += comment_age + '\n'
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
if session.user and not session.is_context_locked and \
|
|
|
|
|
len(comments) > 1 and not display_order_desc:
|
|
|
|
|
page += f'\n=> /comment/{post.id} 💬 Comment\n'
|
|
|
|
|
|
2023-05-12 16:32:50 +00:00
|
|
|
|
return page
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_feed_page(session):
|
|
|
|
|
# NOTE: Some parameters were configured in the function above.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
page = ''
|
|
|
|
|
|
|
|
|
|
is_gemini_feed = req.query == 'feed'
|
|
|
|
|
is_tinylog = req.query == 'tinylog'
|
|
|
|
|
sort_hotness = False
|
|
|
|
|
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.
|
|
|
|
|
if 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-05-06 13:29:21 +00:00
|
|
|
|
if not context:
|
2023-05-09 19:48:35 +00:00
|
|
|
|
topinfo += f"{session.bubble.site_info}\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'
|
|
|
|
|
|
|
|
|
|
page += topinfo if not is_tinylog else clean_tinylog(topinfo)
|
|
|
|
|
page += '\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
|
|
|
|
# Navigation menu.
|
|
|
|
|
if not is_gemini_feed and not is_tinylog:
|
|
|
|
|
if not user:
|
|
|
|
|
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
|
|
|
|
|
page += session.FOOTER_MENU
|
|
|
|
|
else:
|
|
|
|
|
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:
|
|
|
|
|
page += f'=> /u/{user.name}/post 💬 New post\n'
|
|
|
|
|
page += f'=> /u/{user.name}/compose ✏️ Compose draft\n'
|
|
|
|
|
elif context and context.owner == 0:
|
2023-05-10 18:46:55 +00:00
|
|
|
|
if is_issue_tracker:
|
|
|
|
|
page += f'=> /{context.title()}/post 🐞 New issue in s/{context.name}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += f'=> /{context.title()}/post 💬 New post in s/{context.name}\n'
|
|
|
|
|
page += f'=> /{context.title()}/compose ✏️ Compose draft in s/{context.name}\n'
|
2023-05-07 21:04:14 +00:00
|
|
|
|
else:
|
|
|
|
|
page += f'=> /u/{user.name}/post 💬 New post in u/{user.name}\n'
|
|
|
|
|
page += f'=> /u/{user.name}/compose ✏️ Compose draft in u/{user.name}\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'
|
|
|
|
|
|
|
|
|
|
# The feed.
|
|
|
|
|
sort_hotness = (user and user.sort_post == User.SORT_POST_HOTNESS)
|
|
|
|
|
if not is_empty_query(req):
|
|
|
|
|
if req.query == 'sort=hot':
|
|
|
|
|
sort_hotness = True
|
|
|
|
|
elif req.query == 'sort=new':
|
|
|
|
|
sort_hotness = False
|
|
|
|
|
elif re.match(r'p\d+', req.query):
|
|
|
|
|
page_index = int(req.query[1:]) - 1
|
|
|
|
|
|
|
|
|
|
sort_mode = ' 🔥' if sort_hotness else ''
|
|
|
|
|
if session.feed_mode == 'all':
|
2023-05-10 12:20:32 +00:00
|
|
|
|
if is_issue_tracker:
|
|
|
|
|
page_title = 'Issues'
|
|
|
|
|
else:
|
|
|
|
|
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'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
else:
|
|
|
|
|
page_title = 'Followed'
|
|
|
|
|
page += f'\n## {page_title}{sort_mode}\n'
|
|
|
|
|
|
|
|
|
|
elif is_tinylog:
|
|
|
|
|
page += f'author: @{c_user.name}@{session.bubble.hostname}\n'
|
|
|
|
|
page += f'avatar: {c_user.avatar}\n\n'
|
|
|
|
|
|
|
|
|
|
filter_by_followed = user if session.feed_mode == 'followed' else None
|
2023-05-10 12:20:32 +00:00
|
|
|
|
filter_issue_status = True if session.feed_mode == 'open' else \
|
|
|
|
|
False if session.feed_mode == 'closed' else None
|
2023-05-11 18:44:22 +00:00
|
|
|
|
|
2023-05-06 13:29:21 +00:00
|
|
|
|
posts = db.get_posts(subspace=context,
|
2023-05-07 06:09:10 +00:00
|
|
|
|
comment=False,
|
|
|
|
|
draft=False,
|
|
|
|
|
sort_hotness=sort_hotness,
|
|
|
|
|
notifs_for_user=(user.id if user else 0),
|
|
|
|
|
filter_by_followed=filter_by_followed,
|
2023-05-10 12:20:32 +00:00
|
|
|
|
filter_issue_status=filter_issue_status,
|
2023-05-11 18:44:22 +00:00
|
|
|
|
gemini_feed=is_gemini_feed,
|
2023-05-07 06:09:10 +00:00
|
|
|
|
limit=page_size,
|
|
|
|
|
page=page_index)
|
2023-05-06 13:29:21 +00:00
|
|
|
|
|
2023-05-11 18:44:22 +00:00
|
|
|
|
if not is_gemini_feed and not is_tinylog:
|
|
|
|
|
num_total = db.count_posts(subspace=context,
|
|
|
|
|
draft=False,
|
|
|
|
|
filter_by_followed=filter_by_followed,
|
|
|
|
|
filter_issue_status=filter_issue_status)
|
|
|
|
|
num_pages = int((num_total + page_size - 1) / page_size)
|
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" + \
|
|
|
|
|
"> Emptiness is a boundless canvas, an unconstrained beginning: " + \
|
|
|
|
|
"an opportunity for the courageous to create and the curious to explore.\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)
|
|
|
|
|
elif is_tinylog:
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.tinylog_entry(post) + '\n'
|
|
|
|
|
else:
|
|
|
|
|
def page_range(n):
|
|
|
|
|
return f'{n + 1} / {num_pages}'
|
|
|
|
|
|
|
|
|
|
if page_index > 0:
|
|
|
|
|
page += f'=> ?p{page_index} Previous page\n\n'
|
|
|
|
|
|
|
|
|
|
for post in posts:
|
|
|
|
|
page += session.feed_entry(post, context) + '\n'
|
|
|
|
|
|
|
|
|
|
if len(posts) > 0 and page_index < num_pages - 1:
|
|
|
|
|
page += f'=> ?p{page_index + 2} Next page\n'
|
|
|
|
|
|
|
|
|
|
if num_pages > 1:
|
|
|
|
|
page += f'Page {page_index + 1} of {num_pages}\n\n'
|
|
|
|
|
|
|
|
|
|
# Footer.
|
|
|
|
|
if not is_tinylog:
|
|
|
|
|
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-10 12:20:32 +00:00
|
|
|
|
if is_issue_tracker:
|
2023-05-12 03:30:50 +00:00
|
|
|
|
if session.feed_mode in ('all', 'closed'):
|
|
|
|
|
page += f'=> ? 🐞 Open Issues\n'
|
|
|
|
|
if session.feed_mode in ('all', 'open'):
|
|
|
|
|
page += f'=> ?closed ✔︎ Closed Issues\n'
|
|
|
|
|
if session.feed_mode in ('open', 'closed'):
|
|
|
|
|
page += f'=> ?open&closed All Issues\n'
|
2023-05-06 13:29:21 +00:00
|
|
|
|
page += "=> ?feed Gemini feed\n"
|
|
|
|
|
if c_user:
|
|
|
|
|
page += "=> ?tinylog Tinylog\n"
|
|
|
|
|
if user:
|
2023-05-08 19:39:56 +00:00
|
|
|
|
# Search.
|
|
|
|
|
if context:
|
|
|
|
|
page += f'=> /{context.title()}/search 🔍 Search in {context.title()}\n'
|
|
|
|
|
else:
|
|
|
|
|
page += '=> /search 🔍 Search\n'
|
|
|
|
|
|
|
|
|
|
# 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-05-12 03:30:50 +00:00
|
|
|
|
if 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-05-12 03:30:50 +00:00
|
|
|
|
page += f'=> {session.bubble.antenna_url}?{urlparse.quote(antenna_feed)} Submit feed to 📡 Antenna\n'
|
|
|
|
|
|
2023-05-08 19:39:56 +00:00
|
|
|
|
# Following.
|
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'
|
|
|
|
|
else:
|
|
|
|
|
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-05-07 21:04:14 +00:00
|
|
|
|
if context and context.owner != user.id and not session.is_context_locked:
|
2023-05-06 13:29:21 +00:00
|
|
|
|
if (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'
|
|
|
|
|
|
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
|