[1mdiff --git a/model.py b/model.py[m
[1mindex 306a97b..2ddf23b 100644[m
[1m--- a/model.py[m
[1m+++ b/model.py[m
[36m@@ -6,7 +6,7 @@[m [mimport re[m
import shutil[m
import time[m
from typing import Union[m
[31m-from utils import ago_text, clean_title, parse_at_names, shorten_text, \[m
[32m+[m[32mfrom utils import ago_text, clean_title, parse_at_names, shorten_text, strip_links, \[m
GeminiError, UTC, INNER_LINK_PREFIX[m
[m
[m
[36m@@ -149,8 +149,9 @@[m [mclass Notification:[m
event = f'removed you as moderator of s/{self.subname}'[m
[m
if with_title:[m
[31m- vis_title = self.post_title if self.post_title else \[m
[31m- shorten_text(clean_title(self.post_summary), 50) if self.post_summary else None[m
[32m+[m[32m vis_title = shorten_text(self.post_title, 50) if self.post_title \[m
[32m+[m[32m else shorten_text(strip_links(clean_title(self.post_summary)), 50) if self.post_summary \[m
[32m+[m[32m else None[m
if vis_title:[m
if self.type == Notification.MENTION:[m
event += ' in'[m
[36m@@ -455,7 +456,11 @@[m [mclass Database:[m
ts_edited TIMESTAMP DEFAULT CURRENT_TIMESTAMP,[m
ts_comment TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- time of latest comment[m
summary TEXT DEFAULT '',[m
[31m- UNIQUE KEY (subspace, issueid)[m
[32m+[m[32m UNIQUE KEY (subspace, issueid),[m
[32m+[m[32m INDEX (subspace),[m
[32m+[m[32m INDEX (parent),[m
[32m+[m[32m INDEX (user),[m
[32m+[m[32m INDEX (issueid)[m
)""")[m
[m
db.execute("""CREATE TABLE IF NOT EXISTS tags ([m
[36m@@ -864,6 +869,22 @@[m [mclass Database:[m
url, label, post))[m
return files[m
[m
[32m+[m[32m def get_time_files(self, ts_range):[m
[32m+[m[32m cur = self.conn.cursor()[m
[32m+[m[32m cur.execute("""[m
[32m+[m[32m SELECT[m
[32m+[m[32m f.id, f.segment, f.name, f.mimetype, f.data, s.url, s.content, s.post, p.user[m
[32m+[m[32m FROM files f[m
[32m+[m[32m JOIN segments s ON s.id=f.segment[m
[32m+[m[32m JOIN posts p ON s.post=p.id[m
[32m+[m[32m WHERE UNIX_TIMESTAMP(p.ts_edited)>=? AND UNIX_TIMESTAMP(p.ts_edited)<?[m
[32m+[m[32m """, ts_range)[m
[32m+[m[32m files = [][m
[32m+[m[32m for (file_id, segment, name, mimetype, data, url, label, post, post_user) in cur:[m
[32m+[m[32m files.append(File(file_id, segment, post_user, name, mimetype, data,[m
[32m+[m[32m url, label, post))[m
[32m+[m[32m return files[m
[32m+[m
def set_file_segment(self, file_id, segment_id):[m
cur = self.conn.cursor()[m
cur.execute("UPDATE files SET segment=? WHERE id=?", (segment_id, file_id))[m
[36m@@ -1092,8 +1113,6 @@[m [mclass Database:[m
segments = self.get_segments(post)[m
render = ''[m
[m
[31m- uri_pattern = re.compile(r'(gemini|finger|gopher|mailto|data|file|https?|fdroidrepos?:):(//)?[^ ]+')[m
[31m-[m
# Use only the first link/attachment.[m
for seg in filter(lambda s: s.type in [Segment.LINK,[m
Segment.ATTACHMENT], segments):[m
[36m@@ -1116,8 +1135,7 @@[m [mclass Database:[m
[m
first = True[m
for text in filter(lambda s: s.type == Segment.TEXT, segments):[m
[31m- str = clean_title(text.content)[m
[31m- str = uri_pattern.sub(r'[\1 link]', str)[m
[32m+[m[32m str = strip_links(clean_title(text.content))[m
if len(str) == 0: continue[m
if with_title and first:[m
# Separate title from the body text.[m
[36m@@ -1462,7 +1480,7 @@[m [mclass Database:[m
parent=None, sort_descending=True, sort_hotness=False,[m
filter_by_followed=None, filter_issue_status=None, filter_tag=None,[m
gemini_feed=False, notifs_for_user_id=0, muted_by_user_id=0,[m
[31m- limit=None, page=0):[m
[32m+[m[32m ts_range=None, limit=None, page=0):[m
cur = self.conn.cursor()[m
where_stm = [][m
values = [notifs_for_user_id][m
[36m@@ -1492,11 +1510,16 @@[m [mclass Database:[m
if subspace != None:[m
where_stm.append('p.subspace=?')[m
values.append(subspace.id)[m
[31m- PIN_ORDER = 'p.is_pinned DESC'[m
[32m+[m[32m PIN_ORDER = 'p.is_pinned DESC, '[m
[32m+[m[32m elif ts_range != None:[m
[32m+[m[32m where_stm.append('(UNIX_TIMESTAMP(p.ts_edited)>=? AND UNIX_TIMESTAMP(p.ts_edited)<?)')[m
[32m+[m[32m values.append(ts_range[0])[m
[32m+[m[32m values.append(ts_range[1])[m
[32m+[m[32m PIN_ORDER = ''[m
else:[m
if id is None and user is None and parent is None:[m
where_stm.append(f'((sub1.flags & {Subspace.OMIT_FROM_ALL_FLAG})=0 AND (p.flags & {Post.OMIT_FROM_ALL_FLAG})=0)')[m
[31m- PIN_ORDER = 'p.is_pinned=2 DESC'[m
[32m+[m[32m PIN_ORDER = 'p.is_pinned=2 DESC, '[m
[m
if draft != None:[m
where_stm.append('p.is_draft=?')[m
[36m@@ -1553,7 +1576,7 @@[m [mclass Database:[m
LEFT JOIN subspaces sub2 ON p.subspace=sub2.id AND p.user=sub2.owner[m
{filter}[m
WHERE {' AND '.join(where_stm)}[m
[31m- ORDER BY {PIN_ORDER}, {order_by}[m
[32m+[m[32m ORDER BY {PIN_ORDER}{order_by}[m
{limit_expr}[m
""", tuple(values))[m
[m
[36m@@ -1593,12 +1616,16 @@[m [mclass Database:[m
def count_posts(self,[m
user=None,[m
subspace=None,[m
[32m+[m[32m parent_id=None,[m
draft=False,[m
filter_by_followed=None,[m
filter_issue_status=None,[m
filter_tag=None,[m
muted_by_user_id=0):[m
[31m- cond = ['p.parent=0'] # no comments[m
[32m+[m[32m if not parent_id:[m
[32m+[m[32m cond = ['p.parent=0'] # no comments[m
[32m+[m[32m else:[m
[32m+[m[32m cond = [][m
values = [][m
filter = ''[m
if filter_by_followed:[m
[36m@@ -1615,6 +1642,9 @@[m [mclass Database:[m
if user != None:[m
cond.append('p.user=?')[m
values.append(user.id)[m
[32m+[m[32m if parent_id != None:[m
[32m+[m[32m cond.append('p.parent=?')[m
[32m+[m[32m values.append(parent_id)[m
if subspace != None:[m
cond.append('p.subspace=?')[m
values.append(subspace.id)[m
[36m@@ -1820,11 +1850,15 @@[m [mclass Database:[m
else:[m
cond = ''[m
values = tuple()[m
[32m+[m[32m subspace_cond = ''[m
[32m+[m[32m if not subspace:[m
[32m+[m[32m # Issue tracker tags should not be included in the All Posts tags.[m
[32m+[m[32m subspace_cond = f' AND (s.flags & {Subspace.ISSUE_TRACKER})=0'[m
cur.execute(f"""[m
SELECT t.tag, COUNT(t.tag)[m
FROM tags t[m
JOIN posts p ON p.id=t.post[m
[31m- JOIN subspaces s ON s.id=p.subspace[m
[32m+[m[32m JOIN subspaces s ON s.id=p.subspace {subspace_cond}[m
{cond}[m
GROUP BY t.tag[m
ORDER BY COUNT(t.tag) DESC[m
[1mdiff --git a/subspace.py b/subspace.py[m
[1mindex 9bfb631..9afc67a 100644[m
[1m--- a/subspace.py[m
[1m+++ b/subspace.py[m
[36m@@ -360,10 +360,16 @@[m [mclass GempubArchive:[m
self.ts = post.ts_created[m
self.dt = datetime.datetime.fromtimestamp(self.ts, UTC)[m
self.post_id = post.id[m
[32m+[m[32m self.issueid = post.issueid[m
[32m+[m[32m self.title = post.title[m
self.subspace_id = post.subspace[m
[32m+[m[32m self.user_id = post.user[m
self.label = label[m
self.page = page[m
self.file = file[m
[32m+[m[32m self.tags = post.tags[m
[32m+[m[32m self.num_cmts = post.num_cmts[m
[32m+[m[32m self.num_likes = post.num_likes[m
self.referenced_from_posts = [][m
[m
def ymd(self):[m
[36m@@ -373,43 +379,62 @@[m [mclass GempubArchive:[m
if self.file:[m
pos = self.file.segment_url.rfind('/') + 1[m
return f'file{self.file.id}_{self.file.segment_url[pos:]}'[m
[31m- fn = re.sub(r'[^\w\d-]', '', self.label.replace(' ', '-')).strip() # clean it up[m
[31m- if len(fn) == 0:[m
[31m- fn = f'{self.dt.day}_post{self.post_id}.gmi'[m
[31m- return f'{self.dt.year:04d}-{self.dt.month:02d}/{self.post_id}_{fn}.gmi'[m
[31m-[m
[31m- def init(self, session, user, subspace):[m
[32m+[m[32m fn = re.sub(r'[^\w\d-]', '', self.title.replace(' ', '-')).lower().strip() # clean it up[m
[32m+[m[32m if len(fn) > 0:[m
[32m+[m[32m fn = '_' + fn[m
[32m+[m[32m #if len(fn) == 0:[m
[32m+[m[32m # fn = f'{self.dt.day}_post{self.post_id}.gmi'[m
[32m+[m[32m return f'{self.dt.year:04d}-{self.dt.month:02d}/{self.post_id}{fn}.gmi'[m
[32m+[m
[32m+[m[32m def init(self, session, user=None, subspace=None, month_range=None):[m
self.session = session[m
self.db = session.db[m
[32m+[m[32m self.ts_range = None[m
[32m+[m[32m if month_range:[m
[32m+[m[32m year, month = month_range[m
[32m+[m[32m end_month = month + 1 if month < 12 else 1[m
[32m+[m[32m end_year = year if month < 12 else year + 1[m
[32m+[m[32m self.ts_range = ([m
[32m+[m[32m datetime.datetime(year, month, 1, 0, 0, 0, tzinfo=UTC).timestamp(),[m
[32m+[m[32m datetime.datetime(end_year, end_month, 1, 0, 0, 0, tzinfo=UTC).timestamp()[m
[32m+[m[32m )[m
self.user = user[m
self.subspace = subspace[m
[31m- self.is_user = subspace.owner != 0[m
[32m+[m[32m self.is_user = self.ts_range is None and subspace.owner != 0[m
[m
assert self.is_user and self.user or not self.is_user and not self.user[m
[31m- assert self.subspace is not None[m
[32m+[m[32m assert self.ts_range or self.subspace is not None[m
[m
# Modify settion so rendered pages appear to be not logged in.[m
session.user = None[m
[m
self.site_link = session.server_root()[m
[31m-[m
[31m- generator = f'Generated with 💬 Bubble v{session.bubble.version}.'[m
[32m+[m[32m if month_range:[m
[32m+[m[32m archive_title = f'{datetime.datetime(year, month, 1).strftime("%B %Y")}'[m
[32m+[m[32m archive_description = f'All posts and comments made on {session.bubble.site_name}. '[m
[32m+[m[32m else:[m
[32m+[m[32m archive_title = f'{"s/" if not self.is_user else ""}{subspace.name} on {session.bubble.site_name}'[m
[32m+[m[32m archive_description = \[m
[32m+[m[32m (f'All posts and comments made in the subspace {subspace.title()} on {session.bubble.site_name}. ' if not self.is_user else f'All posts and comments made by {user.name} on {session.bubble.site_name}. ')[m
self.metadata = {[m
'gpubVersion': '1.0.0',[m
[31m- 'title': f'{"s/" if not self.is_user else ""}{subspace.name} on {session.bubble.site_name}',[m
[31m- 'author': 'Bubble Archiver' if not self.is_user else user.name,[m
[32m+[m[32m 'title': archive_title,[m
[32m+[m[32m 'description': archive_description,[m
[32m+[m[32m 'author': f'Bubble v{session.bubble.version}',[m
'publishDate': time.strftime('%Y-%m-%d'),[m
[31m- 'index': 'index.gmi',[m
[31m- 'description': (f'All posts and comments made in the subspace {subspace.title()} on {session.bubble.site_name}. ' if not self.is_user else f'All posts and comments made by {user.name} on {session.bubble.site_name}. ') + generator[m
[32m+[m[32m 'index': 'index.gmi'[m
}[m
[m
self.local_entries = [] # posts in the archive's subspace[m
self.foreign_entries = [] # posts in other subspaces[m
[32m+[m[32m self.subspace_entries = {} # subspace name => list of entries[m
self.comment_entries = [] # posts where user has commented[m
self.file_entries = [] # files[m
self.entry_index = {} # indexed by post ID[m
self.file_index = {} # indexed by file ID[m
self.referenced_users = {} # info about posters[m
[32m+[m[32m self.total_count = [0, 0][m
[32m+[m[32m self.subspace_count = {} # [posts, comments][m
[m
self.subspaces = {}[m
self.users = {}[m
[36m@@ -452,15 +477,18 @@[m [mclass GempubArchive:[m
self.session.context = self.get_subspace(post.subspace)[m
self.session.is_context_tracker = (self.session.context.flags & Subspace.ISSUE_TRACKER) != 0[m
[m
[31m- is_local = post.subspace == self.subspace.id[m
[31m- where = self.session.context.title() if not is_local and ([m
[31m- not self.is_user or is_comment) else None[m
[31m- label_sub = ' · ' + where if where else ''[m
[32m+[m[32m is_local = (post.subspace == self.subspace.id) if self.subspace else False[m
[32m+[m[32m if not self.ts_range:[m
[32m+[m[32m where = self.session.context.title() if not is_local and ([m
[32m+[m[32m not self.is_user or is_comment) else None[m
[32m+[m[32m label_sub = ' · ' + where if where else ''[m
[m
page = make_post_page(self.session, post)[m
[31m- entry = GempubArchive.Entry(post,[m
[31m- (post.title if post.title else shorten_text(clean_title(post.summary), 100)) + label_sub,[m
[31m- page)[m
[32m+[m[32m if self.ts_range:[m
[32m+[m[32m label = shorten_text(clean_title(strip_links(post.summary)), 150)[m
[32m+[m[32m else:[m
[32m+[m[32m label = (post.title if post.title else shorten_text(clean_title(strip_links(post.summary)), 100)) + label_sub[m
[32m+[m[32m entry = GempubArchive.Entry(post, label, page)[m
[m
# Check for referenced users.[m
for username in re.findall(r'=> /u/([\w-]+)\s', page):[m
[36m@@ -475,16 +503,40 @@[m [mclass GempubArchive:[m
else:[m
self.foreign_entries.append(entry)[m
[m
[32m+[m[32m skey = self.session.context.name[m
[32m+[m[32m if skey in self.subspace_entries:[m
[32m+[m[32m self.subspace_entries[skey].append(entry)[m
[32m+[m[32m else:[m
[32m+[m[32m self.subspace_entries[skey] = [entry][m
[32m+[m
[32m+[m[32m if not post.id in self.entry_index:[m
[32m+[m[32m if not is_comment:[m
[32m+[m[32m self.add_count(post.subspace,[m
[32m+[m[32m (1, self.db.count_posts(parent_id=post.id, draft=False)))[m
[32m+[m
self.entry_index[post.id] = entry[m
[m
[32m+[m[32m def add_count(self, subspace_id, count):[m
[32m+[m[32m self.total_count[0] += count[0][m
[32m+[m[32m self.total_count[1] += count[1][m
[32m+[m[32m if not subspace_id in self.subspace_count:[m
[32m+[m[32m self.subspace_count[subspace_id] = [count[0], count[1]][m
[32m+[m[32m else:[m
[32m+[m[32m self.subspace_count[subspace_id][0] += count[0][m
[32m+[m[32m self.subspace_count[subspace_id][1] += count[1][m
[32m+[m
def render_post_entries(self):[m
db = self.db[m
[m
# Entries for the user/subspace posts.[m
if self.is_user:[m
posts = db.get_posts(user=self.user, comment=False, draft=False)[m
[32m+[m[32m elif self.ts_range:[m
[32m+[m[32m posts = db.get_posts(ts_range=self.ts_range, comment=False, draft=False,[m
[32m+[m[32m sort_descending=False)[m
else:[m
posts = db.get_posts(subspace=self.subspace, comment=False, draft=False)[m
[32m+[m
for post in posts:[m
self.add_post_entry(post)[m
[m
[36m@@ -492,7 +544,8 @@[m [mclass GempubArchive:[m
# Make entries for posts where user has commented in.[m
# TODO: Add a proper database query for this.[m
commented_in = set()[m
[31m- for cmt in db.get_posts(user=self.user, comment=True, draft=False):[m
[32m+[m[32m for cmt in db.get_posts(user=self.user, comment=True, draft=False,[m
[32m+[m[32m sort_descending=False):[m
commented_in.add(cmt.parent)[m
for post in [db.get_post(id=post_id) for post_id in commented_in]:[m
if post and post.user != self.user.id:[m
[36m@@ -501,7 +554,8 @@[m [mclass GempubArchive:[m
def render_file_entries(self):[m
db = self.db[m
for file in db.get_user_files(self.user) if self.user \[m
[31m- else db.get_subspace_files(self.subspace):[m
[32m+[m[32m else db.get_subspace_files(self.subspace) if self.subspace \[m
[32m+[m[32m else db.get_time_files(self.ts_range):[m
post = db.get_post(id=file.segment_post)[m
filesize = len(file.data)[m
entry = GempubArchive.Entry(post,[m
[36m@@ -516,7 +570,10 @@[m [mclass GempubArchive:[m
src_post_id = entry.post_id[m
[m
user_pattern = re.compile(r'^=>\s*/u/([\w%-]+)\s')[m
[31m- post_pattern = re.compile(r'^=>\s*/([us])/' + self.subspace.name + r'/(\d+)\s')[m
[32m+[m[32m if self.subspace:[m
[32m+[m[32m post_pattern = re.compile(r'^=>\s*/([us])/' + self.subspace.name + r'/(\d+)\s')[m
[32m+[m[32m else:[m
[32m+[m[32m post_pattern = re.compile(r'^=>\s*/([us])/[\w%-]+/(\d+)\s')[m
file_pattern = re.compile(r'^=>\s*/([us])/[\w%-]+/(image|file)/(\d+)[^ ]*\s')[m
root_pattern = re.compile(r'^=>\s*/([^ ]*)\s')[m
rewritten = [][m
[36m@@ -561,6 +618,14 @@[m [mclass GempubArchive:[m
buffer = io.BytesIO()[m
zip = zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9)[m
[m
[32m+[m[32m def counter_text(count):[m
[32m+[m[32m parts = [][m
[32m+[m[32m if count[0]:[m
[32m+[m[32m parts.append(f'{count[0]} post{plural_s(count[0])}')[m
[32m+[m[32m if count[1]:[m
[32m+[m[32m parts.append(f'{count[1]} comment{plural_s(count[1])}')[m
[32m+[m[32m return ' and '.join(parts)[m
[32m+[m
with zip.open('metadata.txt', 'w') as f:[m
for entry in self.metadata:[m
f.write(f"{entry}: {self.metadata[entry]}\n".encode('utf-8'))[m
[36m@@ -568,7 +633,7 @@[m [mclass GempubArchive:[m
with zip.open('title.gmi', 'w') as f:[m
f.write(f"""[m
[m
[31m-# {self.user.name if self.is_user else self.subspace.name}[m
[32m+[m[32m# {self.user.name if self.is_user else self.subspace.name if self.subspace else self.metadata['title']}[m
[m
[m
[36m@@ -583,7 +648,7 @@[m [mExported on {self.metadata['publishDate']}.[m
index_page += '\n=> title.gmi Title page\n'[m
profile_path = 'users/' + self.user.name + '.gmi'[m
index_page += f'=> {profile_path} {self.user.avatar} {self.user.name}\n'[m
[31m- else:[m
[32m+[m[32m elif self.subspace:[m
index_page = f'# s/{self.subspace.name}\n\nTable of Contents:\n'[m
index_page += '\n=> title.gmi Title page\n'[m
profile_path = self.subspace.name + '.gmi'[m
[36m@@ -597,20 +662,73 @@[m [mExported on {self.metadata['publishDate']}.[m
src += '\nThe subspace was created on ' + \[m
make_timestamp(self.subspace.ts_created, '%Y-%m-%d') + '.\n'[m
f.write(src.encode('utf-8'))[m
[32m+[m[32m else:[m
[32m+[m[32m index_page = '# ' + self.metadata['title'] + '\n\nTable of Contents:\n\n'[m
[32m+[m
[32m+[m[32m if self.local_entries:[m
[32m+[m[32m index_page += f'\n=> posts/index.gmi Posts in {self.subspace.title()}\n'[m
[32m+[m[32m local_index_page = f'# Posts in {self.subspace.title()}\n\n'[m
[32m+[m[32m for entry in self.local_entries:[m
[32m+[m[32m entry_path = 'posts/' + entry.path()[m
[32m+[m[32m local_index_page += f'=> {entry.path()} {entry.ymd()} {entry.label}\n'[m
[32m+[m[32m with zip.open(entry_path, 'w') as content:[m
[32m+[m[32m content.write(self.rewrite_internal_urls(entry).encode('utf-8'))[m
[32m+[m[32m with zip.open('posts/index.gmi', 'w') as content:[m
[32m+[m[32m content.write(local_index_page.encode('utf-8'))[m
[32m+[m
[32m+[m[32m if self.ts_range:[m
[32m+[m[32m sub_links = [][m
[32m+[m[32m for sub_name in sorted(self.subspace_entries.keys(), key=str.lower):[m
[32m+[m[32m first_entry = self.subspace_entries[sub_name][0][m
[32m+[m[32m sub = self.get_subspace(first_entry.subspace_id)[m
[32m+[m[32m entry_path = f'{sub.title()[0]}_{sub.name}.gmi'[m
[32m+[m[32m sub_links.append(f'=> {entry_path} {sub.title()}\n')[m
[32m+[m
[32m+[m[32m title_icon = ''[m
[32m+[m[32m if sub.owner:[m
[32m+[m[32m title_icon = f'{self.get_user(first_entry.user_id).avatar} '[m
[32m+[m[32m sub_page = f'# {title_icon}{sub.title()}\n'[m
[32m+[m[32m sub_page += f'{counter_text(self.subspace_count[sub.id])} in this subspace.\n'[m
[32m+[m
[32m+[m[32m for entry in self.subspace_entries[sub_name]:[m
[32m+[m[32m entry_user = self.get_user(entry.user_id)[m
[32m+[m[32m author = f'{entry_user.avatar} {entry_user.name}'[m
[32m+[m[32m meta = [][m
[32m+[m[32m top = None[m
[32m+[m[32m if entry.issueid:[m
[32m+[m[32m top = f'[#{entry.issueid}] {entry.title}'[m
[32m+[m[32m meta.append(author)[m
[32m+[m[32m if entry.tags:[m
[32m+[m[32m top += f' · {entry.tags}'[m
[32m+[m[32m elif not sub.owner:[m
[32m+[m[32m meta.append(author)[m
[32m+[m[32m meta.append(entry.dt.strftime('%Y-%m-%d %H:%M'))[m
[32m+[m[32m if entry.num_cmts > 0:[m
[32m+[m[32m meta.append(f'{entry.num_cmts} comment{plural_s(entry.num_cmts)}')[m
[32m+[m[32m if entry.num_likes > 0:[m
[32m+[m[32m meta.append(f'{entry.num_likes} like{plural_s(entry.num_likes)}')[m
[32m+[m[32m if entry.tags and not entry.issueid:[m
[32m+[m[32m meta.append(entry.tags)[m
[32m+[m[32m link = f'=> posts/{entry.path()}'[m
[32m+[m[32m if top:[m
[32m+[m[32m sub_page += f'\n{link} {top}\n{entry.label}\n{" · ".join(meta)}\n'[m
[32m+[m[32m else:[m
[32m+[m[32m sub_page += f'\n{entry.label}\n{link} {" · ".join(meta)}\n'[m
[32m+[m[32m # Write to the archive.[m
[32m+[m[32m with zip.open('posts/' + entry.path(), 'w') as content:[m
[32m+[m[32m content.write(self.rewrite_internal_urls(entry).encode('utf-8'))[m
[32m+[m[32m with zip.open(entry_path, 'w') as content:[m
[32m+[m[32m content.write(sub_page.encode('utf-8'))[m
[m
[31m- # TODO: Rewrite user/page/file/image links to point to locations inside the archive.[m
[31m-[m
[31m- index_page += f'\n=> posts/index.gmi Posts in {self.subspace.title()}\n'[m
[31m- local_index_page = f'# Posts in {self.subspace.title()}\n\n'[m
[31m- for entry in self.local_entries:[m
[31m- entry_path = 'posts/' + entry.path()[m
[31m- local_index_page += f'=> {entry.path()} {entry.ymd()} {entry.label}\n'[m
[31m- with zip.open(entry_path, 'w') as content:[m
[31m- content.write(self.rewrite_internal_urls(entry).encode('utf-8'))[m
[31m- with zip.open('posts/index.gmi', 'w') as content:[m
[31m- content.write(local_index_page.encode('utf-8'))[m
[32m+[m[32m prev_type = None[m
[32m+[m[32m for link in sorted(sub_links, key=str.lower):[m
[32m+[m[32m if prev_type and prev_type != link[3]:[m
[32m+[m[32m index_page += '\n'[m
[32m+[m[32m index_page += link[m
[32m+[m[32m prev_type = link[3] # u or s[m
[32m+[m[32m index_page += '\n'[m
[m
[31m- if self.foreign_entries:[m
[32m+[m[32m elif self.foreign_entries:[m
index_page += f'=> other/index.gmi Posts in Other Subspaces\n'[m
foreign_index_page = '# Posts in Other Subspaces\n'[m
last_sub = None[m
[36m@@ -627,7 +745,6 @@[m [mExported on {self.metadata['publishDate']}.[m
with zip.open('other/index.gmi', 'w') as content:[m
content.write(foreign_index_page.encode('utf-8'))[m
[m
[31m- # Comments.[m
if self.comment_entries:[m
index_page += f'=> comments/index.gmi Commented Posts\n'[m
comment_index_page = '# Commented Posts\n'[m
[36m@@ -639,7 +756,6 @@[m [mExported on {self.metadata['publishDate']}.[m
with zip.open('comments/index.gmi', 'w') as content:[m
content.write(comment_index_page.encode('utf-8'))[m
[m
[31m- # File attachments.[m
if self.file_entries:[m
index_page += '=> files/index.gmi File attachments\n'[m
file_index_page = '# File Attachments\n'[m
[36m@@ -688,24 +804,33 @@[m [mdef export_gempub_archive(session):[m
return 60, 'Login required'[m
[m
# Determine subspace to export.[m
[31m- m = re.search(r'/export/(s/)?([\w%-]+).gpub$', req.path)[m
[32m+[m[32m m = re.search(r'/export/(s/|month/)?([\w%-]+).gpub$', req.path)[m
if not m or not m[2]:[m
return 59, 'Bad request'[m
name = urlparse.unquote(m[2])[m
[31m- subspace = db.get_subspace(name=name)[m
[32m+[m[32m if m[1] == 'month/':[m
[32m+[m[32m month_range = map(int, m[2].split('-'))[m
[32m+[m[32m subspace = None[m
[32m+[m[32m else:[m
[32m+[m[32m month_range = None[m
[32m+[m[32m subspace = db.get_subspace(name=name)[m
is_user = m[1] is None[m
[m
# Check access rights. At the moment, exporting is only possible via user[m
# settings and subspace admin pages, so the user must have moderation[m
# rights in the exported subspace.[m
[31m- if is_user:[m
[32m+[m[32m if month_range:[m
[32m+[m[32m if not user:[m
[32m+[m[32m # Have to be logged in.[m
[32m+[m[32m return 61, 'Not authorized'[m
[32m+[m[32m elif is_user:[m
if subspace.owner != user.id:[m
return 61, 'Not authorized'[m
else:[m
if user.id not in map(lambda u: u.id, db.get_mods(subspace)):[m
return 61, 'Not authorized'[m
[m
[31m- archive = GempubArchive(session, user if is_user else None, subspace)[m
[32m+[m[32m archive = GempubArchive(session, user if is_user else None, subspace, month_range)[m
archive.render_post_entries()[m
archive.render_file_entries()[m
data = archive.compress()[m
[1mdiff --git a/utils.py b/utils.py[m
[1mindex 32549ed..8b53673 100644[m
[1m--- a/utils.py[m
[1m+++ b/utils.py[m
[36m@@ -6,6 +6,7 @@[m [mimport urllib.parse as urlparse[m
[m
UTC = datetime.timezone.utc[m
GEMTEXT_MARKUP = re.compile(r'^(\s*=>\s*|* |>\s*|##?#?)')[m
[32m+[m[32mURI_PATTERN = re.compile(r'(gemini|finger|gopher|mailto|data|file|https?|fdroidrepos?:):(//)?[^`" ]+')[m
INNER_LINK_PREFIX = '— '[m
[m
[m
[36m@@ -79,6 +80,10 @@[m [mdef clean_text(text):[m
return text.rstrip()[m
[m
[m
[32m+[m[32mdef strip_links(text):[m
[32m+[m[32m return URI_PATTERN.sub(r'[\1 link]', text)[m
[32m+[m
[32m+[m
def clean_title(title):[m
# Strip `=>` and other Gemini syntax.[m
cleaned = [][m
text/plain
This content has been proxied by September (ba2dc).