From f5d95940a36e7774f0c88f8dae86164711a0420f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 21 Nov 2022 10:12:38 +0000 Subject: [PATCH 1/6] Use different id for announce --- daemon.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/daemon.py b/daemon.py index 524040177..09241180a 100644 --- a/daemon.py +++ b/daemon.py @@ -8996,6 +8996,7 @@ class PubServer(BaseHTTPRequestHandler): '/followers' if not repeat_private: announce_to_str = 'https://www.w3.org/ns/activitystreams#Public' + announce_id = None announce_json = \ create_announce(curr_session, base_dir, @@ -9105,7 +9106,10 @@ class PubServer(BaseHTTPRequestHandler): actor_absolute = self._get_instance_url(calling_domain) + actor - first_post_id = repeat_url.replace('/', '--') + if announce_id: + first_post_id = announce_id.replace('/', '--') + else: + first_post_id = repeat_url.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') actor_path_str = \ @@ -9226,12 +9230,9 @@ class PubServer(BaseHTTPRequestHandler): actor_absolute = self._get_instance_url(calling_domain) + actor - first_post_id = repeat_url.replace('/', '--') - first_post_id = ';firstpost=' + first_post_id.replace('#', '--') - actor_path_str = \ actor_absolute + '/' + timeline_str + '?page=' + \ - str(page_number) + first_post_id + timeline_bookmark + str(page_number) + timeline_bookmark fitness_performance(getreq_start_time, self.server.fitness, '_GET', '_undo_announce_button', self.server.debug) From 4ffee425296e0da132034901713c400a5fd226c5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 21 Nov 2022 10:52:33 +0000 Subject: [PATCH 2/6] If first post is not founf in a timeline then generate it based on page number --- daemon.py | 21 +++--- posts.py | 188 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 133 insertions(+), 76 deletions(-) diff --git a/daemon.py b/daemon.py index 09241180a..6876bcfd9 100644 --- a/daemon.py +++ b/daemon.py @@ -11004,12 +11004,11 @@ class PubServer(BaseHTTPRequestHandler): first_post_id = mute_url.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') - self._redirect_headers(actor + '/' + - timeline_str + - '?page=' + str(page_number) + - first_post_id + - timeline_bookmark, - cookie, calling_domain) + page_number_str = str(page_number) + redirect_str = \ + actor + '/' + timeline_str + '?page=' + page_number_str + \ + first_post_id + timeline_bookmark + self._redirect_headers(redirect_str, cookie, calling_domain) def _undo_mute_button(self, calling_domain: str, path: str, base_dir: str, http_prefix: str, @@ -11142,11 +11141,11 @@ class PubServer(BaseHTTPRequestHandler): first_post_id = mute_url.replace('/', '--') first_post_id = ';firstpost=' + first_post_id.replace('#', '--') - self._redirect_headers(actor + '/' + timeline_str + - '?page=' + str(page_number) + - first_post_id + - timeline_bookmark, - cookie, calling_domain) + page_number_str = str(page_number) + redirect_str = \ + actor + '/' + timeline_str + '?page=' + page_number_str + \ + first_post_id + timeline_bookmark + self._redirect_headers(redirect_str, cookie, calling_domain) def _show_replies_to_post(self, authorized: bool, calling_domain: str, referer_domain: str, diff --git a/posts.py b/posts.py index 29ecd3261..bebd55cc4 100644 --- a/posts.py +++ b/posts.py @@ -3957,72 +3957,24 @@ def _passed_newswire_voting(newswire_votes_threshold: int, return True -def _create_box_indexed(recent_posts_cache: {}, - base_dir: str, boxname: str, - nickname: str, domain: str, port: int, - http_prefix: str, - items_per_page: int, header_only: bool, - authorized: bool, - newswire_votes_threshold: int, positive_voting: bool, - voting_time_mins: int, page_number: int, - first_post_id: str = '') -> {}: - """Constructs the box feed for a person with the given nickname +def _create_box_items(base_dir: str, + timeline_nickname: str, + original_domain: str, + nickname: str, domain: str, + index_box_name: str, + first_post_id: str, + page_number: int, + items_per_page: int, + newswire_votes_threshold: int, + positive_voting: bool, + voting_time_mins: int, + post_urls_in_box: [], + recent_posts_cache: {}, + boxname: str, + posts_in_box: [], + box_actor: str) -> (int, int): + """Creates the list of posts within a timeline """ - if not authorized or not page_number: - page_number = 1 - - if boxname not in ('inbox', 'dm', 'tlreplies', 'tlmedia', - 'tlblogs', 'tlnews', 'tlfeatures', 'outbox', - 'tlbookmarks', 'bookmarks'): - print('ERROR: invalid boxname ' + boxname) - return None - - # bookmarks and events timelines are like the inbox - # but have their own separate index - index_box_name = boxname - timeline_nickname = nickname - if boxname == "tlbookmarks": - boxname = "bookmarks" - index_box_name = boxname - elif boxname == "tlfeatures": - boxname = "tlblogs" - index_box_name = boxname - timeline_nickname = 'news' - - original_domain = domain - domain = get_full_domain(domain, port) - - box_actor = local_actor_url(http_prefix, nickname, domain) - - page_str = '?page=true' - if page_number: - page_number = max(page_number, 1) - try: - page_str = '?page=' + str(page_number) - except BaseException: - print('EX: _create_box_indexed ' + - 'unable to convert page number to string') - box_url = local_actor_url(http_prefix, nickname, domain) + '/' + boxname - box_header = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'first': box_url + '?page=true', - 'id': box_url, - 'last': box_url + '?page=true', - 'totalItems': 0, - 'type': 'OrderedCollection' - } - box_items = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': box_url + page_str, - 'orderedItems': [ - ], - 'partOf': box_url, - 'type': 'OrderedCollectionPage' - } - - posts_in_box = [] - post_urls_in_box = [] - index_filename = \ acct_dir(base_dir, timeline_nickname, original_domain) + \ '/' + index_box_name + '.index' @@ -4134,6 +4086,112 @@ def _create_box_indexed(recent_posts_cache: {}, else: print('WARN: Unable to locate post ' + post_url + ' nickname ' + nickname) + return total_posts_count, posts_added_to_timeline + + +def _create_box_indexed(recent_posts_cache: {}, + base_dir: str, boxname: str, + nickname: str, domain: str, port: int, + http_prefix: str, + items_per_page: int, header_only: bool, + authorized: bool, + newswire_votes_threshold: int, positive_voting: bool, + voting_time_mins: int, page_number: int, + first_post_id: str = '') -> {}: + """Constructs the box feed for a person with the given nickname + """ + if not authorized or not page_number: + page_number = 1 + + if boxname not in ('inbox', 'dm', 'tlreplies', 'tlmedia', + 'tlblogs', 'tlnews', 'tlfeatures', 'outbox', + 'tlbookmarks', 'bookmarks'): + print('ERROR: invalid boxname ' + boxname) + return None + + # bookmarks and events timelines are like the inbox + # but have their own separate index + index_box_name = boxname + timeline_nickname = nickname + if boxname == "tlbookmarks": + boxname = "bookmarks" + index_box_name = boxname + elif boxname == "tlfeatures": + boxname = "tlblogs" + index_box_name = boxname + timeline_nickname = 'news' + + original_domain = domain + domain = get_full_domain(domain, port) + + box_actor = local_actor_url(http_prefix, nickname, domain) + + page_str = '?page=true' + if page_number: + page_number = max(page_number, 1) + try: + page_str = '?page=' + str(page_number) + except BaseException: + print('EX: _create_box_indexed ' + + 'unable to convert page number to string') + box_url = local_actor_url(http_prefix, nickname, domain) + '/' + boxname + box_header = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'first': box_url + '?page=true', + 'id': box_url, + 'last': box_url + '?page=true', + 'totalItems': 0, + 'type': 'OrderedCollection' + } + box_items = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': box_url + page_str, + 'orderedItems': [ + ], + 'partOf': box_url, + 'type': 'OrderedCollectionPage' + } + + posts_in_box = [] + post_urls_in_box = [] + + total_posts_count, posts_added_to_timeline = \ + _create_box_items(base_dir, + timeline_nickname, + original_domain, + nickname, domain, + index_box_name, + first_post_id, + page_number, + items_per_page, + newswire_votes_threshold, + positive_voting, + voting_time_mins, + post_urls_in_box, + recent_posts_cache, + boxname, + posts_in_box, + box_actor) + if first_post_id and posts_added_to_timeline == 0: + # no first post was found within the index, so just use the page number + first_post_id = '' + total_posts_count, posts_added_to_timeline = \ + _create_box_items(base_dir, + timeline_nickname, + original_domain, + nickname, domain, + index_box_name, + first_post_id, + page_number, + items_per_page, + newswire_votes_threshold, + positive_voting, + voting_time_mins, + post_urls_in_box, + recent_posts_cache, + boxname, + posts_in_box, + box_actor) if total_posts_count < 3: print('Posts added to json timeline ' + boxname + ': ' + From 9d7e90a90840e8b740a6faca2f713b9500f13081 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 21 Nov 2022 11:05:12 +0000 Subject: [PATCH 3/6] Tidying --- posts.py | 189 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 96 insertions(+), 93 deletions(-) diff --git a/posts.py b/posts.py index bebd55cc4..8ad328394 100644 --- a/posts.py +++ b/posts.py @@ -3980,112 +3980,115 @@ def _create_box_items(base_dir: str, '/' + index_box_name + '.index' total_posts_count = 0 posts_added_to_timeline = 0 - if os.path.isfile(index_filename): - if first_post_id: - first_post_id = first_post_id.replace('--', '#') - first_post_id = first_post_id.replace('/', '#') - with open(index_filename, 'r', encoding='utf-8') as index_file: - posts_added_to_timeline = 0 - while posts_added_to_timeline < items_per_page: - post_filename = index_file.readline() + if not os.path.isfile(index_filename): + return total_posts_count, posts_added_to_timeline - if not post_filename: - break + if first_post_id: + first_post_id = first_post_id.replace('--', '#') + first_post_id = first_post_id.replace('/', '#') - if first_post_id and total_posts_count == 0: - if first_post_id not in post_filename: - continue - total_posts_count = \ - int((page_number - 1) * items_per_page) + with open(index_filename, 'r', encoding='utf-8') as index_file: + posts_added_to_timeline = 0 + while posts_added_to_timeline < items_per_page: + post_filename = index_file.readline() - # Has this post passed through the newswire voting stage? - if not _passed_newswire_voting(newswire_votes_threshold, - base_dir, domain, - post_filename, - positive_voting, - voting_time_mins): + if not post_filename: + break + + if first_post_id and total_posts_count == 0: + if first_post_id not in post_filename: + continue + total_posts_count = \ + int((page_number - 1) * items_per_page) + + # Has this post passed through the newswire voting stage? + if not _passed_newswire_voting(newswire_votes_threshold, + base_dir, domain, + post_filename, + positive_voting, + voting_time_mins): + continue + + # Skip through any posts previous to the current page + if not first_post_id: + if total_posts_count < \ + int((page_number - 1) * items_per_page): + total_posts_count += 1 continue - # Skip through any posts previous to the current page - if not first_post_id: - if total_posts_count < \ - int((page_number - 1) * items_per_page): - total_posts_count += 1 - continue + # if this is a full path then remove the directories + if '/' in post_filename: + post_filename = post_filename.split('/')[-1] - # if this is a full path then remove the directories - if '/' in post_filename: - post_filename = post_filename.split('/')[-1] + # filename of the post without any extension or path + # This should also correspond to any index entry in + # the posts cache + post_url = remove_eol(post_filename) + post_url = post_url.replace('.json', '').strip() - # filename of the post without any extension or path - # This should also correspond to any index entry in - # the posts cache - post_url = remove_eol(post_filename) - post_url = post_url.replace('.json', '').strip() + if post_url in post_urls_in_box: + continue - if post_url in post_urls_in_box: + # is the post cached in memory? + if recent_posts_cache.get('index'): + if post_url in recent_posts_cache['index']: + if recent_posts_cache['json'].get(post_url): + url = recent_posts_cache['json'][post_url] + if _add_post_string_to_timeline(url, + boxname, + posts_in_box, + box_actor): + total_posts_count += 1 + posts_added_to_timeline += 1 + post_urls_in_box.append(post_url) + continue + print('Post not added to timeline') + + # read the post from file + full_post_filename = \ + locate_post(base_dir, nickname, + original_domain, post_url, False) + if full_post_filename: + # has the post been rejected? + if os.path.isfile(full_post_filename + '.reject'): continue - # is the post cached in memory? - if recent_posts_cache.get('index'): - if post_url in recent_posts_cache['index']: - if recent_posts_cache['json'].get(post_url): - url = recent_posts_cache['json'][post_url] - if _add_post_string_to_timeline(url, - boxname, - posts_in_box, - box_actor): - total_posts_count += 1 - posts_added_to_timeline += 1 - post_urls_in_box.append(post_url) - continue - print('Post not added to timeline') - - # read the post from file - full_post_filename = \ - locate_post(base_dir, nickname, - original_domain, post_url, False) - if full_post_filename: - # has the post been rejected? - if os.path.isfile(full_post_filename + '.reject'): - continue - - if _add_post_to_timeline(full_post_filename, boxname, - posts_in_box, box_actor): - posts_added_to_timeline += 1 - total_posts_count += 1 - post_urls_in_box.append(post_url) - else: - print('WARN: Unable to add post ' + post_url + - ' nickname ' + nickname + - ' timeline ' + boxname) + if _add_post_to_timeline(full_post_filename, boxname, + posts_in_box, box_actor): + posts_added_to_timeline += 1 + total_posts_count += 1 + post_urls_in_box.append(post_url) else: - if timeline_nickname != nickname: - # if this is the features timeline - full_post_filename = \ - locate_post(base_dir, timeline_nickname, - original_domain, post_url, False) - if full_post_filename: - if _add_post_to_timeline(full_post_filename, - boxname, - posts_in_box, box_actor): - posts_added_to_timeline += 1 - total_posts_count += 1 - post_urls_in_box.append(post_url) - else: - print('WARN: Unable to add features post ' + - post_url + ' nickname ' + nickname + - ' timeline ' + boxname) + print('WARN: Unable to add post ' + post_url + + ' nickname ' + nickname + + ' timeline ' + boxname) + else: + if timeline_nickname != nickname: + # if this is the features timeline + full_post_filename = \ + locate_post(base_dir, timeline_nickname, + original_domain, post_url, False) + if full_post_filename: + if _add_post_to_timeline(full_post_filename, + boxname, + posts_in_box, box_actor): + posts_added_to_timeline += 1 + total_posts_count += 1 + post_urls_in_box.append(post_url) else: - print('WARN: features timeline. ' + - 'Unable to locate post ' + post_url) + print('WARN: Unable to add features post ' + + post_url + ' nickname ' + nickname + + ' timeline ' + boxname) else: - if timeline_nickname == 'news': - print('WARN: Unable to locate news post ' + - post_url + ' nickname ' + nickname) - else: - print('WARN: Unable to locate post ' + post_url + - ' nickname ' + nickname) + print('WARN: features timeline. ' + + 'Unable to locate post ' + post_url) + else: + if timeline_nickname == 'news': + print('WARN: Unable to locate news post ' + + post_url + ' nickname ' + nickname) + else: + print('WARN: Unable to locate post ' + post_url + + ' nickname ' + nickname) return total_posts_count, posts_added_to_timeline From d35d02579ce75da9cb0f566581d50783c5728551 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 21 Nov 2022 12:54:58 +0000 Subject: [PATCH 4/6] Over-zealous markup check --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index daf821a8a..6d75dbe1a 100644 --- a/utils.py +++ b/utils.py @@ -1112,7 +1112,7 @@ def dangerous_markup(content: str, allow_local_network_access: bool) -> bool: """ separators = [['<', '>'], ['<', '>']] invalid_strings = [ - 'analytics', 'ampproject', 'googleapis', '_exec(' + 'ampproject', 'googleapis', '_exec(' ] if _is_dangerous_string_simple(content, allow_local_network_access, separators, invalid_strings): From 1e221d8c795907edd7d1d18df75e85348cb5c5f7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 21 Nov 2022 13:56:27 +0000 Subject: [PATCH 5/6] Regenerate manual --- README.md | 2 +- gemini/EN/install.gmi | 2 +- manual/manual.epub | Bin 2034146 -> 2034150 bytes manual/manual.html | 2 +- manual/manual.md | 2 +- website/EN/index.html | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 98a0ffe8e..0e25d3a38 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ server { } server { - listen 443 ssl; + listen 443 ssl http2; server_name YOUR_DOMAIN; gzip on; diff --git a/gemini/EN/install.gmi b/gemini/EN/install.gmi index 857656d82..9239b8539 100644 --- a/gemini/EN/install.gmi +++ b/gemini/EN/install.gmi @@ -96,7 +96,7 @@ And paste the following: } server { - listen 443 ssl; + listen 443 ssl http2; server_name YOUR_DOMAIN; ssl_stapling off; diff --git a/manual/manual.epub b/manual/manual.epub index db1e3a1c62764f35eb53d5addcbb70b637862af5..88ba0873a39808056b3eb68faf17e1d5ec98c60e 100644 GIT binary patch delta 7444 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 delta 7647 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 f$ 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 tJJ@Rqy?