2023-05-06 13:29:21 +00:00
import re
2023-06-03 14:11:16 +00:00
import urllib . parse as urlparse
2023-05-29 19:40:32 +00:00
from model import User , Post , Segment , Subspace , 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-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-05-06 13:29:21 +00:00
draft_id = db . create_post ( session . user , session . context . id )
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-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 )
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 :
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-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 ) :
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 = ' '
if is_comment_page :
# Switch to the parent post, but display it in preview mode.
focused_cmt = post
last_age = focused_cmt . age ( )
2023-06-20 12:02:07 +00:00
post_id = post . parent
post = db . get_post ( id = post_id )
if post :
page + = f ' => { post . page_url ( ) } Comment on: " { post . title if post . title else shorten_text ( strip_links ( clean_title ( post . summary ) ) , 60 ) } " in { ( " u/ " if post . sub_owner else " s/ " ) + post . sub_name } \n '
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-20 12:02:07 +00:00
page + = f ' => /help/deleted-post 🔒 Comment on a deleted post (ID: { post_id } ) \n '
2023-06-20 05:49:35 +00:00
page + = f ' => /u/ { focused_cmt . poster_name } { focused_cmt . poster_avatar } { focused_cmt . poster_name } \n '
page + = f ' { last_age } \n \n '
page + = session . render_post ( focused_cmt )
# Comment actions.
actions = [ ]
2023-06-20 12:02:07 +00:00
if post :
if session . is_editable ( focused_cmt ) :
actions . append ( f ' => /edit/ { focused_cmt . id } ✏️ Edit comment \n ' )
if post and session . user and not session . is_context_locked and display_order_desc :
actions . append ( f ' => /comment/ { post . id } 💬 Comment \n ' )
if session . is_thanks_enabled ( ) and focused_cmt . user != user . id :
actions . append ( f ' => /thanks/ { focused_cmt . id } 🙏 Give thanks \n ' )
if session . is_deletable ( focused_cmt ) and ( not session . is_editable ( focused_cmt ) or not post ) :
2023-06-20 05:49:35 +00:00
actions . append ( f ' => /edit/ { focused_cmt . id } /delete/ { session . get_token ( ) } ❌ Delete comment \n ' )
actions . append ( session . dashboard_link ( ) )
if actions :
page + = ' \n ' + ' ' . join ( actions )
2023-06-20 12:02:07 +00:00
if post :
op_section = ' \n ## Original Post \n \n ' + session . feed_entry ( post )
else :
op_section = ' '
2023-06-20 05:49:35 +00:00
if not display_order_desc :
page + = op_section
2023-05-07 21:04:14 +00:00
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-06-20 12:22:38 +00:00
last_age = post . age ( ) if not session . is_archive else post . ymd_hm ( )
2023-06-20 05:49:35 +00:00
page + = f ' { last_age } '
if session . is_likes_enabled ( ) :
liked = [ ]
if post . num_likes :
liked = db . get_likes ( post )
page + = ' · 👍 ' + ' , ' . join ( liked )
if session . is_reactions_enabled ( ) :
reactions = db . get_reactions ( post )
listed = [ ]
for r in reactions :
listed . append ( f ' { reactions [ r ] } { r } ' )
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-06-20 05:49:35 +00:00
page + = f ' => /comment/ { post . id } 💬 Comment \n '
# 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 '
if session . user . id != post . user :
if ( FOLLOW_POST , post . id ) in session . user_follows :
page + = f ' => /unfollow/post/ { post . id } ➖ Unfollow { kind } \n '
2023-05-29 16:43:25 +00:00
else :
2023-06-20 05:49:35 +00:00
if ( MUTE_POST , post . id ) in session . user_mutes :
page + = f ' => /unmute/post/ { post . id } 🔈 Unmute { kind } \n '
else :
page + = f ' => /follow/post/ { post . id } ➕ Follow { kind } \n '
page + = f ' => /mute/post/ { post . id } 🔇 Mute { kind } \n '
2023-05-12 16:32:50 +00:00
else :
2023-06-20 05:49:35 +00:00
# Own posts can be muted.
2023-06-02 15:16:41 +00:00
if ( MUTE_POST , post . id ) in session . user_mutes :
page + = f ' => /unmute/post/ { post . id } 🔈 Unmute { kind } \n '
else :
page + = f ' => /mute/post/ { post . id } 🔇 Mute { kind } \n '
2023-05-12 16:32:50 +00:00
2023-06-20 05:49:35 +00:00
# Moderator actions on a post.
mod_actions = [ ]
if session . user . id == post . user or session . is_user_mod :
if session . is_context_tracker :
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 ' )
mod_actions . append ( f ' => /edit-tags/ { post . id } 🏷️ Add/remove tags \n ' )
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 ' )
if session . is_movable ( post ) :
mod_actions . append ( f ' => /edit/ { post . id } /move/ { session . get_token ( ) } Move to subspace \n ' )
if session . is_deletable ( post ) and not session . is_editable ( post ) :
mod_actions . append ( f ' => /edit/ { post . id } /delete/ { session . get_token ( ) } ❌ Delete { kind } \n ' )
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 "
mod_actions . append ( f ' => { session . bubble . antenna_url } ? { urlparse . quote ( antenna_feed ) } Submit post to 📡 Antenna \n ' )
if mod_actions :
page + = ' \n ' + ' ' . join ( mod_actions )
page + = ' \n ' + session . dashboard_link ( )
2023-06-20 12:02:07 +00:00
notifs = db . get_notifications ( user = user , post_id = post . id , sort_desc = True )
2023-06-20 05:49:35 +00:00
if notifs :
page + = f ' { len ( notifs ) } notification { plural_s ( len ( notifs ) ) } on this page: \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-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 = ' '
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 = [ ]
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
2023-06-20 12:22:38 +00:00
if not session . is_archive :
src = f ' => /u/ { cmt . poster_name } / { cmt . id } { cmt . poster_avatar } { cmt . poster_name } \n '
else :
src = f ' => /u/ { cmt . poster_name } { cmt . poster_avatar } { cmt . poster_name } \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-06-20 12:22:38 +00:00
comment_age = cmt . age ( ) if not session . is_archive else cmt . ymd_hm ( )
2023-05-12 16:32:50 +00:00
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
2023-06-20 12:02:07 +00:00
if session . is_editable ( cmt ) and post :
2023-05-14 21:46:48 +00:00
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
2023-06-20 05:49:35 +00:00
if is_comment_page and display_order_desc :
page + = op_section
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 \
2023-06-20 12:02:07 +00:00
len ( comments ) > = 1 and not display_order_desc and post :
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-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
2023-06-02 15:16:41 +00:00
user_mutes = session . user_mutes
2023-05-06 13:29:21 +00:00
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-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 '
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
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 ,
filter_tag = session . feed_tag_filter ,
muted_by_user_id = ( user . id if user else 0 ) )
num_pages = int ( ( num_total + page_size - 1 ) / page_size )
2023-05-06 13:29:21 +00:00
# Navigation menu.
if not is_gemini_feed and not is_tinylog :
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-05-06 13:29:21 +00:00
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 '
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-05-06 13:29:21 +00:00
# The feed.
sort_hotness = ( user and user . sort_post == User . SORT_POST_HOTNESS )
if not is_empty_query ( req ) :
2023-06-15 16:25:48 +00:00
for param in req . query . split ( ' & ' ) :
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
2023-05-06 13:29:21 +00:00
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 '
2023-05-29 19:40:32 +00:00
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-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
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 :
2023-06-15 16:25:48 +00:00
pager_feed_mode = f ' & { session . feed_mode } ' if session . feed_mode != ' all ' else ' '
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.
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-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.
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-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-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 :
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-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