=> 26d23ed0861ead92b35bb6faf9608d1b45117335
[1mdiff --git a/50_bubble.py b/50_bubble.py[m [1mindex 43a44ff..fff7e33 100644[m [1m--- a/50_bubble.py[m [1m+++ b/50_bubble.py[m [36m@@ -87,6 +87,7 @@[m [mBubble is a Gemini bulletin board system with many influences from station.marti[m Bubble is open source:[m => gemini://git.skyjake.fi/bubble/main/ Bubble Git Repository[m """[m [32m+[m[32m self.EMPTY_FEED_PLACEHOLDER = "Emptiness is a boundless canvas, an unconstrained beginning: an opportunity for the courageous to create and the curious to explore."[m [m self.bubble = bubble[m self.path = bubble.path[m [36m@@ -287,6 +288,28 @@[m [mBubble is open source:[m src = f'=> {self.server_root()}{post.page_url()} {post.ymd_date()} {author}{vis_title}{sub}\n'[m return src[m [m [32m+[m[32m def atom_feed_entry(self, post, context=None):[m [32m+[m[32m vis_title = post.title[m [32m+[m[32m if len(vis_title) == 0:[m [32m+[m[32m vis_title = shorten_text(clean_title(post.summary), 100)[m [32m+[m[32m if self.is_context_tracker:[m [32m+[m[32m vis_title = f'[#{post.issueid if post.issueid else 0}] ' + vis_title[m [32m+[m[32m author = post.poster_name[m [32m+[m[32m category = f'\n'[m [32m+[m[32m else:[m [32m+[m[32m rend = atom_escaped(line)[m [32m+[m[32m else:[m [32m+[m[32m if line.startswith('###'):[m [32m+[m[32m rend = f'' if context else ''[m [32m+[m[32m page_url = self.server_root() + urlparse.quote(post.page_url())[m [32m+[m [32m+[m[32m return f"""[m [32m+[m[32m [m [32m+[m[32m [m [32m+[m[32m"""[m [32m+[m def tinylog_entry(self, post):[m src = f'## {post.ymd_hm_tz()}\n\n'[m [m [36m@@ -502,7 +525,7 @@[m [mPosts and comments are composed of "segments". There can be any number of segmen[m * File Attachment — Link to a (small) file stored in the Bubble database.[m * Poll Option[m [m [31m-When viewing the post page, each of these segments are visible in their entirety. In various out places, a shortened "feed preview" is shown instead. The composer shows a preview of both the shortened version and the full page contents before you can publish the post.[m [32m+[m[32mWhen viewing the post page, each of these segments are visible in their entirety. In various other places, a shortened "feed preview" is shown instead. The composer shows a preview of both the shortened version and the full page contents before you can publish the post.[m [m The formatting of the post is altered for Tinylogs: all headings are converted to level 3.[m [m [1mdiff --git a/feeds.py b/feeds.py[m [1mindex c8198f8..103a224 100644[m [1m--- a/feeds.py[m [1m+++ b/feeds.py[m [36m@@ -530,10 +530,24 @@[m [mdef make_feed_page(session):[m user = session.user[m user_follows = session.user_follows[m user_mutes = session.user_mutes[m [32m+[m[32m query_params = req.query.split('&') if req.query else [][m page = ''[m [m [31m- is_gemini_feed = req.query == 'feed'[m [31m- is_tinylog = req.query == 'tinylog'[m [32m+[m[32m # Determine format of feed.[m [32m+[m[32m is_atom_feed = False[m [32m+[m[32m is_gemini_feed = False[m [32m+[m[32m is_tinylog = False[m [32m+[m[32m is_bubble_feed = False[m [32m+[m [32m+[m[32m if 'feed' in query_params:[m [32m+[m[32m is_gemini_feed = True[m [32m+[m[32m elif 'atom' in query_params:[m [32m+[m[32m is_atom_feed = True[m [32m+[m[32m elif 'tinylog' in query_params:[m [32m+[m[32m is_tinylog = True[m [32m+[m[32m else:[m [32m+[m[32m is_bubble_feed = True[m [32m+[m sort_hotness = False[m page_size = 50 if is_gemini_feed else 100 if is_tinylog else 25[m page_index = 0[m [36m@@ -542,7 +556,10 @@[m [mdef make_feed_page(session):[m return 51, "Tinylogs are only for user feeds"[m [m # Page title.[m [31m- if is_gemini_feed or is_tinylog:[m [32m+[m[32m if is_atom_feed:[m [32m+[m[32m # Just print the entries, and add the header/footer in the end.[m [32m+[m[32m ts_last_updated = 0[m [32m+[m[32m elif is_gemini_feed or is_tinylog:[m page += f'# {session.feed_title()}\n'[m elif c_user:[m page += f'# {c_user.avatar} {context.title()}\n'[m [36m@@ -553,7 +570,9 @@[m [mdef make_feed_page(session):[m [m # Subspace description.[m topinfo = ''[m [31m- if not context:[m [32m+[m[32m if is_atom_feed:[m [32m+[m[32m pass[m [32m+[m[32m elif not context:[m topinfo += f"{session.bubble.site_info if session.user else session.bubble.site_info_nouser}\n"[m else:[m if c_user and (c_user.info or c_user.url):[m [36m@@ -572,14 +591,15 @@[m [mdef make_feed_page(session):[m if session.is_context_locked:[m topinfo += '=> /help/locked 🔒 Locked\n'[m [m [31m- page += topinfo if not is_tinylog else clean_tinylog(topinfo)[m [31m- page += '\n'[m [32m+[m[32m if topinfo:[m [32m+[m[32m page += topinfo if not is_tinylog else clean_tinylog(topinfo)[m [32m+[m[32m page += '\n'[m [m filter_by_followed = user if session.feed_mode == 'followed' else None[m filter_issue_status = True if session.feed_mode == 'open' else \[m False if session.feed_mode == 'closed' else None[m [m [31m- if not is_gemini_feed and not is_tinylog:[m [32m+[m[32m if is_bubble_feed:[m num_total = db.count_posts(subspace=context,[m draft=False,[m filter_by_followed=filter_by_followed,[m [36m@@ -588,13 +608,12 @@[m [mdef make_feed_page(session):[m muted_by_user_id=(user.id if user else 0))[m num_pages = int((num_total + page_size - 1) / page_size)[m [m [31m- # Navigation menu.[m [31m- if not is_gemini_feed and not is_tinylog:[m # Filter status.[m filter_mode = ''[m if session.feed_tag_filter:[m filter_mode = f' [#{session.feed_tag_filter}]'[m [m [32m+[m[32m # Navigation menu.[m if not user:[m page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'[m page += session.FOOTER_MENU[m [36m@@ -625,33 +644,35 @@[m [mdef make_feed_page(session):[m if session.feed_mode in ('open', 'closed'):[m page += f'=> ?open&closed Show all\n'[m [m [31m- # The feed.[m [32m+[m[32m if is_bubble_feed:[m [32m+[m[32m # Sorting mode and current page.[m sort_hotness = (user and user.sort_post == User.SORT_POST_HOTNESS)[m if not is_empty_query(req):[m [31m- for param in req.query.split('&'):[m [32m+[m[32m for param in query_params:[m if param == 'sort=hot':[m sort_hotness = True[m elif param == 'sort=new':[m sort_hotness = False[m elif re.match(r'p\d+', param):[m page_index = int(param[1:]) - 1[m [31m-[m sort_mode = ' 🔥' if sort_hotness else ''[m [31m- if session.feed_mode == 'all':[m [31m- if is_issue_tracker:[m [31m- page_title = 'Issues'[m [31m- else:[m [31m- page_title = 'All Posts' if not context else 'Posts'[m [31m- elif session.feed_mode in ('open', 'closed'):[m [31m- page_title = 'Open Issues' if session.feed_mode == 'open' else 'Closed Issues'[m [32m+[m [32m+[m[32m # Page title.[m [32m+[m[32m if session.feed_mode == 'all':[m [32m+[m[32m if is_issue_tracker:[m [32m+[m[32m page_title = 'Issues'[m else:[m [31m- page_title = 'Followed'[m [32m+[m[32m page_title = 'All Posts' if not context else 'Posts'[m [32m+[m[32m elif session.feed_mode in ('open', 'closed'):[m [32m+[m[32m page_title = 'Open Issues' if session.feed_mode == 'open' else 'Closed Issues'[m [32m+[m[32m else:[m [32m+[m[32m page_title = 'Followed'[m [m [32m+[m[32m if is_bubble_feed:[m if is_issue_tracker:[m title_count = f'{num_total} '[m else:[m title_count = ''[m [31m-[m page += f'\n## {title_count}{page_title}{sort_mode}{filter_mode}\n\n'[m [m elif is_tinylog:[m [36m@@ -678,16 +699,20 @@[m [mdef make_feed_page(session):[m page += "There are no issues.\n\n"[m else:[m page += "There are no posts.\n\n\n" + \[m [31m- "> Emptiness is a boundless canvas, an unconstrained beginning: " + \[m [31m- "an opportunity for the courageous to create and the curious to explore.\n\n\n"[m [32m+[m[32m f"> {session.EMPTY_FEED_PLACEHOLDER}\n\n\n"[m elif is_gemini_feed:[m for post in posts:[m page += session.gemini_feed_entry(post, context)[m [32m+[m[32m elif is_atom_feed:[m [32m+[m[32m for post in posts:[m [32m+[m[32m page += session.atom_feed_entry(post, context)[m [32m+[m[32m ts_last_updated = max(ts_last_updated, post.ts_created)[m elif is_tinylog:[m for post in posts:[m page += session.tinylog_entry(post) + '\n'[m else:[m pager_feed_mode = f'&{session.feed_mode}' if session.feed_mode != 'all' else ''[m [32m+[m def page_range(n):[m return f'{n + 1} / {num_pages}'[m [m [36m@@ -704,7 +729,21 @@[m [mdef make_feed_page(session):[m page += f'Page {page_index + 1} of {num_pages}\n\n'[m [m # Footer.[m [31m- if not is_tinylog:[m [32m+[m[32m if is_atom_feed:[m [32m+[m[32m origin_url = f"{session.server_root()}{context.title() if context else ''}"[m [32m+[m[32m atom_header = f"""[m [32m+[m[32m{vis_title} [m [32m+[m[32m[m [32m+[m[32m [m [32m+[m[32m {author} {self.server_root()}/u/{author} {page_url} {category}[m [32m+[m[32m{atom_timestamp(post.ts_created)} [m [32m+[m[32m{atom_timestamp(post.ts_edited)} [m [32m+[m[32m{atom_escaped(gemtext_to_html(self.render_post(post)))} [m [32m+[m[32m[m [32m+[m[32m "[m [32m+[m[32m return 20, 'application/atom+xml', atom_header + page + atom_footer[m [32m+[m [32m+[m[32m elif not is_tinylog:[m if not is_gemini_feed:[m page += "## Options\n"[m if sort_hotness:[m [1mdiff --git a/utils.py b/utils.py[m [1mindex 2669dff..0ad70c3 100644[m [1m--- a/utils.py[m [1m+++ b/utils.py[m [36m@@ -207,6 +207,95 @@[m [mdef ago_text(ts, suffix='ago', now='Now', tz=None):[m return time_delta_text(sec, ts, suffix, now, tz=tz)[m [m [m [32m+[m[32mdef atom_timestamp(ts):[m [32m+[m[32m return datetime.datetime.fromtimestamp(ts, UTC).strftime("%Y-%m-%dT%H:%M:%SZ")[m [32m+[m [32m+[m [32m+[m[32mdef atom_escaped(text):[m [32m+[m[32m return text.replace('&', '&').replace('<', '<').replace('>', '>').\[m [32m+[m[32m replace("'", ''').replace('"', '"')[m [32m+[m [32m+[m [32m+[m[32mdef gemtext_to_html(src):[m [32m+[m[32m out = [][m [32m+[m [32m+[m[32m in_list = False[m [32m+[m[32m in_quote = False[m [32m+[m[32m in_pre = False[m [32m+[m [32m+[m[32m for line in src.split('\n'):[m [32m+[m[32m rend = None[m [32m+[m[32m is_bullet = False[m [32m+[m[32m is_angle = False[m [32m+[m[32m if in_pre:[m [32m+[m[32m if line.startswith('```'):[m [32m+[m[32m in_pre = False[m [32m+[m[32m rend = '{session.feed_title()} [m [32m+[m[32m[m [32m+[m[32m[m [32m+[m[32m{session.req.url()}/ [m [32m+[m[32m{atom_timestamp(ts_last_updated if ts_last_updated else time.time())} [m [32m+[m[32mBubble [m [32m+[m[32m"""[m [32m+[m[32m atom_footer = "
')[m [32m+[m[32m rend = f'{atom_escaped(line[1:])}'[m [32m+[m[32m elif line.startswith('*'):[m [32m+[m[32m is_bullet = True[m [32m+[m[32m #if not in_list:[m [32m+[m[32m # in_list = True[m [32m+[m[32m # out.append('')[m [32m+[m[32m in_quote = False[m [32m+[m[32m if is_angle and not in_quote:[m [32m+[m[32m out.append('')[m [32m+[m[32m rend = f'
- {atom_escaped(line[1:].strip())}
'[m [32m+[m[32m elif line.startswith('=>'):[m [32m+[m[32m link = re.match(r'=>\s*([^\s]+)(\s+.*)?', line)[m [32m+[m[32m if not link:[m [32m+[m[32m continue[m [32m+[m[32m url = link.group(1)[m [32m+[m[32m label = link.group(2)[m [32m+[m[32m if label is None:[m [32m+[m[32m label = url[m [32m+[m[32m label = label.strip()[m [32m+[m[32m parts = urlparse.urlparse(url)[m [32m+[m[32m scheme = parts.scheme if parts.scheme else 'gemini'[m [32m+[m[32m # if not parts.netloc:[m [32m+[m[32m # # Do something about a relative URL?[m [32m+[m[32m link_attr = ''[m [32m+[m[32m #if parts.path.endswith('.png') or parts.path.endswith('.jpg') or \[m [32m+[m[32m # parts.path.endswith('.webp'):[m [32m+[m[32m # # Render as an image.[m [32m+[m[32m # rend = f''[m [32m+[m[32m #else:[m [32m+[m[32m rend = f''[m [32m+[m[32m elif line.startswith('```'):[m [32m+[m[32m in_pre = True[m [32m+[m[32m rend = ''[m [32m+[m[32m elif line:[m [32m+[m[32m rend = f'{atom_escaped(line)}
'[m [32m+[m [32m+[m[32m if rend is not None:[m [32m+[m[32m if not is_bullet and in_list:[m [32m+[m[32m out.append('')[m [32m+[m[32m in_list = False[m [32m+[m[32m if not is_angle and in_quote:[m [32m+[m[32m out.append('
')[m [32m+[m[32m in_quote = True[m [32m+[m[32m if is_bullet and not in_list:[m [32m+[m[32m out.append('')[m [32m+[m[32m in_list = True[m [32m+[m[32m out.append(rend)[m [32m+[m [32m+[m[32m return '\n'.join(out)[m [32m+[m [32m+[m def is_empty_query(req):[m return req.query == None or len(req.query) == 0[m [m
Proxy Information
- Original URL
- gemini://git.skyjake.fi/bubble/main/cdiff/26d23ed0861ead92b35bb6faf9608d1b45117335
- Status Code
- Success (20)
- Meta
text/gemini; charset=utf-8
- Capsule Response Time
- 34.197942 milliseconds
- Gemini-to-HTML Time
- 0.785123 milliseconds
This content has been proxied by September (3851b).