From 2f9a0c77921fa38e429f00027df46b46428cd9e7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 15:04:48 +0000 Subject: [PATCH 01/12] Buy links --- daemon.py | 165 ++++++++++++++++++--------- inbox.py | 104 +++++++++++------ outbox.py | 6 +- schedule.py | 3 +- theme/blue/icons/buy.png | Bin 0 -> 7238 bytes theme/debian/icons/buy.png | Bin 0 -> 7238 bytes theme/default/icons/buy.png | Bin 0 -> 7238 bytes theme/hacker/icons/buy.png | Bin 0 -> 7467 bytes theme/henge/icons/buy.png | Bin 0 -> 7494 bytes theme/indymediaclassic/icons/buy.png | Bin 0 -> 8983 bytes theme/indymediamodern/icons/buy.png | Bin 0 -> 6916 bytes theme/lcd/icons/buy.png | Bin 0 -> 7349 bytes theme/light/icons/buy.png | Bin 0 -> 6917 bytes theme/night/icons/buy.png | Bin 0 -> 7238 bytes theme/pixel/icons/buy.png | Bin 0 -> 5453 bytes theme/purple/icons/buy.png | Bin 0 -> 7720 bytes theme/rc3/icons/buy.png | Bin 0 -> 8152 bytes theme/solidaric/icons/buy.png | Bin 0 -> 5804 bytes theme/starlight/icons/buy.png | Bin 0 -> 7496 bytes theme/zen/icons/buy.png | Bin 0 -> 7296 bytes translations/ar.json | 4 +- translations/bn.json | 4 +- translations/ca.json | 4 +- translations/cy.json | 4 +- translations/de.json | 4 +- translations/el.json | 4 +- translations/en.json | 4 +- translations/es.json | 4 +- translations/fa.json | 4 +- translations/fr.json | 4 +- translations/ga.json | 4 +- translations/hi.json | 4 +- translations/it.json | 4 +- translations/ja.json | 4 +- translations/ko.json | 4 +- translations/ku.json | 4 +- translations/nl.json | 4 +- translations/oc.json | 4 +- translations/pl.json | 4 +- translations/pt.json | 4 +- translations/ru.json | 4 +- translations/sw.json | 4 +- translations/tr.json | 4 +- translations/uk.json | 4 +- translations/yi.json | 4 +- translations/zh.json | 4 +- utils.py | 2 +- webapp_confirm.py | 5 +- webapp_conversation.py | 5 +- webapp_create_post.py | 6 +- webapp_frontscreen.py | 12 +- webapp_likers.py | 4 +- webapp_moderation.py | 6 +- webapp_post.py | 69 ++++++++--- webapp_profile.py | 20 ++-- webapp_search.py | 17 ++- webapp_timeline.py | 62 ++++++---- webapp_utils.py | 56 +++++++++ 58 files changed, 460 insertions(+), 186 deletions(-) create mode 100644 theme/blue/icons/buy.png create mode 100644 theme/debian/icons/buy.png create mode 100644 theme/default/icons/buy.png create mode 100644 theme/hacker/icons/buy.png create mode 100644 theme/henge/icons/buy.png create mode 100644 theme/indymediaclassic/icons/buy.png create mode 100644 theme/indymediamodern/icons/buy.png create mode 100644 theme/lcd/icons/buy.png create mode 100644 theme/light/icons/buy.png create mode 100644 theme/night/icons/buy.png create mode 100644 theme/pixel/icons/buy.png create mode 100644 theme/purple/icons/buy.png create mode 100644 theme/rc3/icons/buy.png create mode 100644 theme/solidaric/icons/buy.png create mode 100644 theme/starlight/icons/buy.png create mode 100644 theme/zen/icons/buy.png diff --git a/daemon.py b/daemon.py index 36f7f44f9..4d23b3aee 100644 --- a/daemon.py +++ b/daemon.py @@ -483,7 +483,8 @@ class PubServer(BaseHTTPRequestHandler): theme_name: str, max_like_count: int, cw_lists: {}, dogwhistles: {}, min_images_for_accounts: [], - max_hashtags: int) -> None: + max_hashtags: int, + buy_sites: {}) -> None: """When an edited post is created this assigns a published and updated date to it, and uses the previous id @@ -531,7 +532,7 @@ class PubServer(BaseHTTPRequestHandler): theme_name, max_like_count, cw_lists, dogwhistles, min_images_for_accounts, - max_hashtags) + max_hashtags, buy_sites) # update the index id_str = edited_postid.split('/')[-1] @@ -1876,7 +1877,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, self.server.content_license_url, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) def _get_outbox_thread_index(self, nickname: str, max_outbox_threads_per_account: int) -> int: @@ -3240,7 +3242,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2p_domain, bold_reading, self.server.dogwhistles, - min_images_for_accounts) + min_images_for_accounts, + self.server.buy_sites) if profile_str: msg = profile_str.encode('utf-8') msglen = len(msg) @@ -3707,7 +3710,8 @@ class PubServer(BaseHTTPRequestHandler): bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, - None, None, default_post_language) + None, None, default_post_language, + self.server.buy_sites) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -3866,7 +3870,8 @@ class PubServer(BaseHTTPRequestHandler): bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, - None, None, default_post_language) + None, None, default_post_language, + self.server.buy_sites) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -4444,7 +4449,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.map_format, self.server.access_keys, 'search', - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) @@ -4555,7 +4561,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, self.server.access_keys, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if history_str: msg = history_str.encode('utf-8') msglen = len(msg) @@ -4638,7 +4645,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, self.server.access_keys, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if bookmarks_str: msg = bookmarks_str.encode('utf-8') msglen = len(msg) @@ -4817,7 +4825,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2p_domain, bold_reading, self.server.dogwhistles, - min_images_for_accounts) + min_images_for_accounts, + self.server.buy_sites) if profile_str: msg = profile_str.encode('utf-8') msglen = len(msg) @@ -9201,7 +9210,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.map_format, self.server.access_keys, 'search', - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) @@ -9522,7 +9532,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) actor_absolute = self._get_instance_url(calling_domain) + actor @@ -10088,7 +10099,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Liked post not found: ' + liked_post_filename) # clear the icon from the cache so that it gets updated @@ -10292,7 +10304,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Unliked post not found: ' + liked_post_filename) # clear the icon from the cache so that it gets updated @@ -10525,7 +10538,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Emoji reaction post not found: ' + reaction_post_filename) @@ -10748,7 +10762,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Unreaction post not found: ' + reaction_post_filename) @@ -10857,7 +10872,8 @@ class PubServer(BaseHTTPRequestHandler): timeline_str, page_number, timezone, bold_reading, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -11016,7 +11032,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Bookmarked post not found: ' + bookmark_filename) # self._post_to_outbox(bookmark_json, @@ -11184,7 +11201,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Unbookmarked post not found: ' + bookmark_filename) @@ -11298,7 +11316,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if delete_str: delete_str_len = len(delete_str) self._set_headers('text/html', delete_str_len, @@ -11439,7 +11458,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Muted post not found: ' + mute_filename) @@ -11583,7 +11603,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + self.server.buy_sites) else: print('WARN: Unmuted post not found: ' + mute_filename) if calling_domain.endswith('.onion') and onion_domain: @@ -11714,7 +11735,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -11828,7 +11850,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, bold_reading, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -11948,7 +11971,8 @@ class PubServer(BaseHTTPRequestHandler): None, None, self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url, - timezone, bold_reading) + timezone, bold_reading, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12074,7 +12098,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, content_license_url, - timezone, bold_reading) + timezone, bold_reading, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12173,7 +12198,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, self.server.access_keys, self.server.min_images_for_accounts, - self.server.debug) + self.server.debug, + self.server.buy_sites) if conv_str: msg = conv_str.encode('utf-8') msglen = len(msg) @@ -12336,7 +12362,8 @@ class PubServer(BaseHTTPRequestHandler): 'inbox', self.server.default_timeline, bold_reading, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) if not msg: self._404() return True @@ -12402,6 +12429,7 @@ class PubServer(BaseHTTPRequestHandler): 'inbox', self.server.default_timeline, bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, + self.server.buy_sites, 'shares') if not msg: self._404() @@ -12491,7 +12519,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, timezone, mitm, bold_reading, self.server.dogwhistles, - self.server.min_images_for_accounts) + self.server.min_images_for_accounts, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12817,7 +12846,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + self.server.buy_sites) if getreq_start_time: fitness_performance(getreq_start_time, self.server.fitness, @@ -13000,7 +13030,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13174,7 +13205,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13344,7 +13376,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13514,7 +13547,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13687,7 +13721,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13864,7 +13899,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13992,7 +14028,8 @@ class PubServer(BaseHTTPRequestHandler): bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14093,7 +14130,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14235,7 +14273,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14395,7 +14434,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, bold_reading, self.server.dogwhistles, ua_str, self.server.min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14552,7 +14592,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence) + reverse_sequence, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14696,7 +14737,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url, - timezone, bold_reading) + timezone, bold_reading, + self.server.buy_sites) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14838,7 +14880,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, content_license_url, - timezone, bold_reading).encode('utf-8') + timezone, bold_reading, + self.server.buy_sites).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -14974,7 +15017,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, content_license_url, - timezone, bold_reading).encode('utf-8') + timezone, bold_reading, + self.server.buy_sites).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -15114,7 +15158,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, content_license_url, - timezone, bold_reading).encode('utf-8') + timezone, bold_reading, + self.server.buy_sites).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -15255,7 +15300,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, content_license_url, - timezone, bold_reading).encode('utf-8') + timezone, bold_reading, + self.server.buy_sites).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -15422,7 +15468,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, self.server.content_license_url, - timezone, bold_reading).encode('utf-8') + timezone, bold_reading, + self.server.buy_sites).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -16264,7 +16311,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, self.server.min_images_for_accounts, new_post_month, new_post_year, - default_post_language) + default_post_language, + self.server.buy_sites) if not msg: print('Error replying to ' + in_reply_to_url) self._404() @@ -18634,7 +18682,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.bold_reading, self.server.dogwhistles, self.server.min_images_for_accounts, - self.server.debug) + self.server.debug, + self.server.buy_sites) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -20722,7 +20771,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, - self.server.max_hashtags) + self.server.max_hashtags, + self.server.buy_sites) print('DEBUG: sending edited public post ' + str(message_json)) if fields['schedulePost']: @@ -21026,7 +21076,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, - self.server.max_hashtags) + self.server.max_hashtags, + self.server.buy_sites) print('DEBUG: sending edited unlisted post ' + str(message_json)) @@ -21136,7 +21187,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, - self.server.max_hashtags) + self.server.max_hashtags, + self.server.buy_sites) print('DEBUG: sending edited followers post ' + str(message_json)) @@ -21260,7 +21312,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, - self.server.max_hashtags) + self.server.max_hashtags, + self.server.buy_sites) print('DEBUG: sending edited dm post ' + str(message_json)) @@ -21378,7 +21431,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.dogwhistles, min_images_for_accounts, - self.server.max_hashtags) + self.server.max_hashtags, + self.server.buy_sites) print('DEBUG: sending edited reminder post ' + str(message_json)) if self._post_to_outbox(message_json, @@ -22928,6 +22982,9 @@ def run_daemon(max_hashtags: int, # scan the theme directory for any svg files containing scripts assert not scan_themes_for_scripts(base_dir) + # permitted sites from which the buy button may be displayed + httpd.buy_sites = {} + # which accounts should minimize all attached images by default httpd.min_images_for_accounts = load_min_images_for_accounts(base_dir) diff --git a/inbox.py b/inbox.py index a6295f20a..b97373cce 100644 --- a/inbox.py +++ b/inbox.py @@ -452,7 +452,8 @@ def _inbox_store_post_to_html_cache(recent_posts_cache: {}, mitm: bool, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> None: + min_images_for_accounts: [], + buy_sites: {}) -> None: """Converts the json post into html and stores it in a cache This enables the post to be quickly displayed later """ @@ -482,7 +483,7 @@ def _inbox_store_post_to_html_cache(recent_posts_cache: {}, not_dm, True, True, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, minimize_all_images, - None) + None, buy_sites) def valid_inbox(base_dir: str, nickname: str, domain: str) -> bool: @@ -1257,7 +1258,8 @@ def receive_edit_to_post(recent_posts_cache: {}, message_json: {}, theme_name: str, max_like_count: int, cw_lists: {}, dogwhistles: {}, min_images_for_accounts: [], - max_hashtags: int) -> bool: + max_hashtags: int, + buy_sites: {}) -> bool: """A post was edited """ if not has_object_dict(message_json): @@ -1392,7 +1394,8 @@ def receive_edit_to_post(recent_posts_cache: {}, message_json: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) return True @@ -1414,7 +1417,8 @@ def _receive_update_activity(recent_posts_cache: {}, session, base_dir: str, theme_name: str, max_like_count: int, cw_lists: {}, dogwhistles: {}, min_images_for_accounts: [], - max_hashtags: int) -> bool: + max_hashtags: int, + buy_sites: {}) -> bool: """Receives an Update activity within the POST section of HTTPServer """ @@ -1456,7 +1460,7 @@ def _receive_update_activity(recent_posts_cache: {}, session, base_dir: str, theme_name, max_like_count, cw_lists, dogwhistles, min_images_for_accounts, - max_hashtags): + max_hashtags, buy_sites): print('EDITPOST: received ' + message_json['object']['id']) return True else: @@ -1507,7 +1511,8 @@ def _receive_like(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives a Like activity within the POST section of HTTPServer """ if message_json['type'] != 'Like': @@ -1621,7 +1626,7 @@ def _receive_like(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, buy_sites) return True @@ -1642,7 +1647,8 @@ def _receive_undo_like(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an undo like activity within the POST section of HTTPServer """ if message_json['type'] != 'Undo': @@ -1746,7 +1752,8 @@ def _receive_undo_like(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) return True @@ -1768,7 +1775,8 @@ def _receive_reaction(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an emoji reaction within the POST section of HTTPServer """ if message_json['type'] != 'EmojiReact': @@ -1903,7 +1911,7 @@ def _receive_reaction(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, buy_sites) return True @@ -1925,7 +1933,8 @@ def _receive_zot_reaction(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an zot-style emoji reaction within the POST section of HTTPServer A zot style emoji reaction is an ordinary reply Note whose content is exactly one emoji @@ -2084,7 +2093,8 @@ def _receive_zot_reaction(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) return True @@ -2107,7 +2117,8 @@ def _receive_undo_reaction(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an undo emoji reaction within the POST section of HTTPServer """ if message_json['type'] != 'Undo': @@ -2229,7 +2240,8 @@ def _receive_undo_reaction(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) return True @@ -2249,7 +2261,8 @@ def _receive_bookmark(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: {}, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives a bookmark activity within the POST section of HTTPServer """ if not message_json.get('type'): @@ -2350,7 +2363,8 @@ def _receive_bookmark(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) return True @@ -2372,7 +2386,8 @@ def _receive_undo_bookmark(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an undo bookmark activity within the POST section of HTTPServer """ if not message_json.get('type'): @@ -2473,7 +2488,8 @@ def _receive_undo_bookmark(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, - dogwhistles, minimize_all_images, None) + dogwhistles, minimize_all_images, None, + buy_sites) return True @@ -2572,7 +2588,8 @@ def _receive_announce(recent_posts_cache: {}, max_like_count: int, cw_lists: {}, lists_enabled: str, bold_reading: bool, dogwhistles: {}, mitm: bool, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """Receives an announce activity within the POST section of HTTPServer """ if message_json['type'] != 'Announce': @@ -2714,7 +2731,8 @@ def _receive_announce(recent_posts_cache: {}, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if not announce_html: print('WARN: Unable to generate html for announce ' + str(message_json)) @@ -3903,7 +3921,8 @@ def _receive_question_vote(server, base_dir: str, nickname: str, domain: str, max_like_count: int, cw_lists: {}, lists_enabled: bool, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> None: + min_images_for_accounts: [], + buy_sites: {}) -> None: """Updates the votes on a Question/poll """ # if this is a reply to a question then update the votes @@ -3961,7 +3980,8 @@ def _receive_question_vote(server, base_dir: str, nickname: str, domain: str, False, True, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) # add id to inbox index inbox_update_index('inbox', base_dir, handle, @@ -4136,7 +4156,7 @@ def _inbox_after_initial(server, inbox_start_time, languages_understood: [], mitm: bool, bold_reading: bool, dogwhistles: {}, - max_hashtags: int) -> bool: + max_hashtags: int, buy_sites: {}) -> bool: """ Anything which needs to be done after initial checks have passed """ # if this is a clearnet instance then replace any onion/i2p @@ -4185,7 +4205,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + buy_sites): if debug: print('DEBUG: Like accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4211,7 +4232,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + buy_sites): if debug: print('DEBUG: Undo like accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4238,7 +4260,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + buy_sites): if debug: print('DEBUG: Reaction accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4265,7 +4288,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + buy_sites): if debug: print('DEBUG: Zot reaction accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4293,7 +4317,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + buy_sites): if debug: print('DEBUG: Undo reaction accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4321,7 +4346,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + server.buy_sites): if debug: print('DEBUG: Bookmark accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4349,7 +4375,8 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts): + server.min_images_for_accounts, + server.buy_sites): if debug: print('DEBUG: Undo bookmark accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4385,7 +4412,8 @@ def _inbox_after_initial(server, inbox_start_time, peertube_instances, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, mitm, - server.min_images_for_accounts): + server.min_images_for_accounts, + server.buy_sites): if debug: print('DEBUG: Announce accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4462,7 +4490,8 @@ def _inbox_after_initial(server, inbox_start_time, max_like_count, cw_lists, lists_enabled, bold_reading, dogwhistles, - server.min_images_for_accounts) + server.min_images_for_accounts, + server.buy_sites) fitness_performance(inbox_start_time, server.fitness, 'INBOX', '_receive_question_vote', debug) @@ -4779,7 +4808,8 @@ def _inbox_after_initial(server, inbox_start_time, timezone, mitm, bold_reading, dogwhistles, - min_img_for_accounts) + min_img_for_accounts, + buy_sites) fitness_performance(inbox_start_time, server.fitness, 'INBOX', @@ -5840,7 +5870,7 @@ def run_inbox_queue(server, theme_name, max_like_count, cw_lists, dogwhistles, server.min_images_for_accounts, - max_hashtags): + max_hashtags, server.buy_sites): if debug: print('Queue: Update accepted from ' + key_id) if os.path.isfile(queue_filename): @@ -5969,7 +5999,7 @@ def run_inbox_queue(server, content_license_url, languages_understood, mitm, bold_reading, dogwhistles, - max_hashtags) + max_hashtags, server.buy_sites) fitness_performance(inbox_start_time, server.fitness, 'INBOX', 'handle_after_initial', debug) diff --git a/outbox.py b/outbox.py index 8870096ea..a5b56ed36 100644 --- a/outbox.py +++ b/outbox.py @@ -238,7 +238,8 @@ def post_message_to_outbox(session, translate: {}, lists_enabled: str, content_license_url: str, dogwhistles: {}, - min_images_for_accounts: []) -> bool: + min_images_for_accounts: [], + buy_sites: {}) -> bool: """post is received by the outbox Client to server message post https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery @@ -593,7 +594,8 @@ def post_message_to_outbox(session, translate: {}, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if is_edited_post: message_json['type'] = 'Update' diff --git a/schedule.py b/schedule.py index 04960e032..5ac876958 100644 --- a/schedule.py +++ b/schedule.py @@ -147,7 +147,8 @@ def _update_post_schedule(base_dir: str, handle: str, httpd, httpd.lists_enabled, httpd.content_license_url, httpd.dogwhistles, - httpd.min_images_for_accounts): + httpd.min_images_for_accounts, + httpd.buy_sites): index_lines.remove(line) try: os.remove(post_filename) diff --git a/theme/blue/icons/buy.png b/theme/blue/icons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..ab86f115d4428f8b9aaa1dfbca2408902642104c GIT binary patch literal 7238 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 literal 0 HcmV?d00001 diff --git a/theme/lcd/icons/buy.png b/theme/lcd/icons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..4968fa5e15654dc018ed34c4879e1bb8e705efd5 GIT binary patch literal 7349 zcmeHMc{G&!{~zmUYzZah8q*>&X2#4IGZ{Nk_O(cinP(UbV`l8iQna9CySOBwC`DH) zA|xuiNFqx~6xpMj==)H&Tj%#Z=XdV;{qBEv&Y5|h=e@i>@AvEdet*vAIdjU=e6J`> z76ySpL`{qhtih|!>Q_h*{Jp~P8-PHB2qCryxYm?FIE(E|XLti}ZZHdg13?Tr1QOKy z{s1L$PFd`8o6>@ipaTCx9)7zi!O+jl$Sf1J)LZ_Vx9%eFmd?w2cDX`OJb$Nvj^hXQFPuk6a-wI3Op!|J{hmfVTUlMEoK( zaj`lV9?~e2w_Y%q#M%MtgZwT}yfPMasH(pfe^I`0sF7RG&r=@TIc($Fu~kuOP(DO= zq+@Pzcv9|~OzYk;9pqI6qoRRUzfDb zTTzR9k3N6}M7!3;Y>vrvo{?EUGbf^43~?D;Jo|E=P6D#=mLZI{Z$T=4d6Q&2Z^kq{ zx4*AG_PiwA;6Mf?P7x02(!61@lCb_kD8Cxt1z0CveS1WNyBKVn{`P$}JE2ap#V1|P z3W~`D$UxJ1)?G5-^)6|G*vG|vK9_b*KjqXmtm4MjLw9!W!}#qpNj&E!oj7)U$0_4f z1m5OMI;Sy_qgtM7DU|nkhqO|8vi0unqO|f??@vQLnK@Rm@y`3r9DVYio|%7pgJa}9-p51P-l)=>OGJzPf*S~u}XtHJfrX8K;9WeKRv)DhVrR9{W+KHV?OD5Pw5H2)9pEON1H8L?s#LVyTXES zv~MN+qP#^{f>=cFZDLPAlR~NI73BwoC7ygwOV3UaNBd9NKjD3XxPotai@Cs^5R2`9 zU{r8qo@k36_A_QA3zi*sw4UL`XCSw~er#-m7jGdS)Z6&_Ubdxdg~P;i&lf{#WtNE; z6TvrTWQhW`vXsZxiOpR%o4dEuv|E9yvw$;$( znxpT9jyR?T)Xff%fb}{%q~u!m7IU~kr+E)1kEX3l{>twBRK zCT~El^W?UJa zQ>mzJfX9aS`{Ug*O>r|_$LCA_e(` zlF@Bf;n{r~@Q;QM`FHc2NXTN{{x#urQ(LG|rIrL3@PmzG z(jsBI?pEj;HHv$LMO6M0rPw|`^o7w(po>D5 z`U}1ExA53&FLQtszXXhYh>N~gQ8`uQ2QS+xa6Wja5lhVSaog4ai(i&b)Dc$BU+)*J z{p{~6R=uZzp)#bZn$RLpcL%aXG$L{?E-F(Yh65>14mOkNVrV$u>f9TlaOk)l;*8@= zui_sQcczwxJG4Bw7xOl4`<>{7Ma+>lomB`+hNYYMbiVqCMIJ2?J)vRh8Fu&>pL>Mb z$}Lme=2(J5B5L3e&Y29n-hSRgn?QW^E_Jwf4oZ{}7G11M5 zRi-S9x`qDvmn+TAsaC8FFY>j{cp}8O-k;BYaPjEL()^5rapKQogkJPvROmg5(g_(B zRt`o>_ZInD`MqRyDPk1L_Fk}z-D+U z&sI;~I=G4W{r7g)Yn!IZr;g?p%I1{|hKmrJLY8`+Cm&v!xH7UZ6|tq!f79q>MrO{S z-v)?`HcDte4CinqyXu#088t13)5UtVUgKN=|GC3YmlJ+vKG$UFr3bvJQWBiB7!jA= z4m)$V^uaJkq1XS>k=+Kn56r)rx$8CPb)m0}x0u75gnVnv5#Ia!KE^H+ujm_HZjBwt z%UjC4+V=i2y#Y{6I{4{?;;EO1aWSX5eLi^-bNN(K9^z6xE$ihP<&AzR?D%1kVq+>x;V05fFTehkIc2^} zYM3e~dJh-3gm0Da{Ze*H=ZWW+sW0L)xR~txwB4By2v0OaU*FP1U;ob|1bA@BJaUd~ zTxTg2ZDCV}xF!~`-tUoRmWq_LUzf5}j$VPZqdz+Mtvrrl?09Iaz_X}h+Oc-fP43Qe zhXiz|p%0$cn{*iNt3)Z?JUMfJ_(*18+smpYNRAafnVu4KEGVSZlQp0yR(i2$L$>7E z-?+N3_FmtuTJ5K*p)%cEl5XB2dE!*P9(1={^XW@Qnuc4g@L99baZSpj$X3Vl2FJzM zD3LewDZuqkSzX@P15x}EFwb2%amp8Oj^`aP?g)NSr#FX~%Zyul<`K)zsE`L~x-1krOQAywB5&qPErhh9}& zQwpsa5kt?@9$lI+KI*8aC0?1`K>#9aoi6ycWI2s*y)3SiK~JgV5fh8N!3z|JR^GkM z@%gfF^UWfkRuMaKq2XfHy;%vGf5ChH(fRun94@rG|J>i8O?^7ECjU1OhMtE(IRM z^!DM9g2;$9ToQP{Dn=pTYbIPTGU9-lC0yT^4Zv}#I8`*#Fo+SLhR}k+HQ6*e$=bl^ z8wB`7MtE?!ED{P87#OG;h*kAvyQ45fA`yjFL#e4DK?@`&*oR9ALi%vFtwMaoFaS7I zHiN}w`1-(CF)41o{#-Hw0qWs@#>ZrtnSF=%;e2BON{zE`$CTSwF?Ls##0t$3Q^$@3=o%|C#%mF=%CGMl$fF`mcs( zVn9Z$u1})*QW-ST+N~QJg9UKvbR>a_1CThnn;X(iU0n@{CeX3!Y8V`Vq2qsmGV$SX zDLzzS6$%7bWq>#|Djv|Fq1BK?907~OQD|7Ch6VwLB&ZWLuxNEWUY$z&0m6dK0K1an z{bN+CP&5#VPF1JS@nBEj@n{grjRqh!Xl@v!8WxYmVhC6bf}6$~6pczU@?|qAU^y8~ ziaUT}`M9qsRtYERTAGj%YO3hJB$nP3E**3rBTN}S{y~2!Y#B_zhD%vx6QfR8O&Lo> zqt!74HQZlD_5hm$cH$}~2Ca%!ThpwTg#;!85=&X_QxITH4(3AAX9E52Zn+n zfZeV^M7mLEZb&SKssT2CK4;$27&NHO$>Bx!G9MrtO}&;WFCf>E|Lz~C~Vqv%XZhX zO7q(Frw?`rSRsCon@}pyNeYRpAV;6f-)?aKgRW4>jPA6ZR2ToTg!PDmT<+^jh@ArC z?^?XFLYrnL7fMF&qbDY_o@a$FB#%Oc_h9!7Hvf@56*jtO@E}WLtJsOUm*6FE+u^&y z6shEh$hd%a)&=P*+NRH$#yOMoHW9UA&kjS3B0~hXxbS7WhrQv{1R#$h)v!7Z-44b7;6+$(A7d>xy52mdZx$Z{{`9&iLKj}25HE%ynR~g!iWsYAx!*$pk_u# zt3{b-*d4QB0>iggMDs~)gJjJ`cD)O%lCN@I5o8kD?a4DfaytB$ef(uJ{@T&V#R#K! z*6R*HHIp?K<)#5)?2aLQxboN$s_Q0}NuT-c5kZ!kh3kjed%7|L$7=jsk42aEo~=14 z^{%*xkX#Ygj;hIv=EcnpBsf9dAI_~g^+g|n>uE8wE~u{CbmS-KLo*ZgoK^3~hd8HH!NTZ>H%zgUP1edzD1`) zPJESVb@q2N^bWnYb-D==CHcm0?!&(uwZ3KBK36H`*Hmg3p)@qAPIl&-ef|DfL=#eT zLQ`OckTW(itudXdYO7)%_f(gRoNS_upWMh+>9De?**FCCnuBEB5y`J?;#*19E6lt# r$+IzEb4XmtHg0VM!Ab0p{K89=5Ya)GWrTve1jNM9+~B&NYt(-L*fzlz literal 0 HcmV?d00001 diff --git a/theme/light/icons/buy.png b/theme/light/icons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..8f37cc095d1656aab7a9ff2aba810e5858939ba3 GIT binary patch literal 6917 zcmeHLc{r5)*PlU1OsT9{#t@~(Y#1}yw~UYwiei>~n3%=PV35aFB1;cN3dycgDJ`;O zlv3Hsk_Qo`vOUtTMaz@kd#Ijz-`{n;*K@t^@4x1{?(KZf`Fzj$oO8bS_nu@|m(6lA zsxlA=M2_TO>jqveMTfK`cs|Vt8HPY4Z$)}|^WDhda1NJEX9NRqegp@A0|Eve0uc=S z>E)BTK~MH`pZ>*t&$T34M|WhY(>@Ty25_t>y6N?pQ-3@wlatm_57DXk@_O!M<7~(~ zlRMSTzgwHkzPhWI`)cK(w|8GNU={_hpU$nAfBd$aDJ*I&44q=jM$PxLS<5PQpQc$L zS5IlIn-gce^nVHzHe|S``FA~dXU#y3O{=1#;_BOo)(8L9zpu+{&}GY$j~V89ZI{_8 zzF(r^=2VNjcMRr-NS)~DFOBUk&_4Q#GHXbTFK?*}k1r1`*X7*7SqzykRZY8ID9Ga< zxc6tf>C5sBJBMf1&pa991QonHJo=)-sV^qIpyu&(w8dZBvh9^#PO(!JTP~@1(lkdV ztI~+h$JjXs?CM$SOHc2&nWb*jdYwC)mNY5TGqCLfwJ8d#$dP;MjO&-}4)Af_e{eYL zV!Tq&nb2w+@xoottFt%zWH%UGdya6eTov|m+F1#A)p>L2HJ|2=cK_2x-ffm+eSRO$ z3Y8;QrxKpDd);r{8EV|)I`$@+VcDGer^gAHd1K$9CzG-8Ge_Sh>6l!pTq~2iT$i&d z@IZ2En7WcS69rKs2PI-}L!l?>YPtTf3!bTQ+S-tNuIhTtig5oNW#T98snoB=nD29?!GRTgmmDU9Q6kRwgTbSw8Ke@7DdO4l!vJZNr`BD z_5s2L?Yb4)$L6K+D`O7iMk^8A#(ztGq|NGd4LY7UX&anx9XdJ6`N(}8Y#PLk=<4zk zZ)AS)8^5)`W;={-n*sHB1#y(qigO&hWBj~M@x_%R741Xop2@FRgE*VIbMuroDTMBE zbdOTsX11@JqLb>Va);b{vwhnDwRL(FbzrrJu2VE>awT3Z$^2g2iWc}ji2=L3QdQrZ z8U$ncn8c;~nzyDk~1=+q9K3c2lJgQgk&`+14`~2PWl}mqzR?_AR&jFvg5rqBw)z-+qahmRa7*YA*7; zS$FoRQ^ZHVvy;Zd4L;G;&s^ndG0Z&($D(a-8c3|rS|xX4y9!Uueq1)S?$y~5s>8`r zJ#phObjQIYuKm72c{1Hr&>uDAakBL0fpov)qvwjkGn02GYoNVTmypHPKQ$FM@jfr< zX6bgEJVb4AJglx?Z@t8rN6n@NR@_(rL)D}=vN`1?E-W(yE$R4@WqTObHgoq_4W?xM z=9eU z3gS*Gk3DFl@#lDQ$n_AKUZut5*n+IHr-N?DYFJA=79`=9AFC2L+$)JM_i%_=*2-dV)X0zOfg5c`rQQ{vpOEh_Mrl}D;p`=hIOkw5)W_uh*pk1IBR!JHC}h7(h_AaP zr*)Sfwy$jWGyloz%2xyiKldOM_rK}Cu;N3 z#MED4n-s*u8%r-L?wFEI+j`T)ml&6^&o=FNRBi%S_z^=4r9eKdutuSL^%;74*ZA(L?!fmQz#QGtj z>0EvUZFie*HN5vx^`+qQ?2DJwO7>_x?qpmWtM6Lk-;^JcJ>uDLc?)Oll^qEw4@W2| zo=))btaqL(838LZvTPG)QXjp~>DztkbwBNFj)lwg)98fjH5_4eV_m=l6eC#ANTRXq zGVW9yej*^XQ`KfB%1iEa@TREb_SUS{!|$GETl?*DAKB2mBG)DQ-KTwgKla#>?pon} z{>5RoZc*pC6CYaiUDy%)2BmhV#&iVsGclP_&}_2xNxho+G$|&>YCd;aT>9oMZ%{kf zzzJ&0@mUB2x}RZV<4Urz`SwBsUt@XE8DQjn13kzEja>q5lak5UPq3DPSO+$u*o3GlEzW4--l&v@K&*#L8(A?U))$iLO5SR}G++lb- zI}xdDrU9A8rT_*4CI{SsKp>_T0uGrP1n}V$Ab`O#L%ghOM8Fv|GlZ9sGuoMB0|YW0 zBDsKjq>BeNGKfl`AuP;gOa(*`fC=!)Z~-%z#Ul#L5DU0O@LnWFA>a!t{2()gx3eqU zhRp@wh6aWPXr!Hh5r##W%fL;!G&<4E*8VF5_+*9%JC+#nohz~%;^Fa!bt zg~p<=SR|-{ zi}#fUkPnoA%t2uc&?qJo^`ixkZx;rFe0AvGTJSu;jT*`g;ITuwRKP9_VDYtogrHHs z+jBy>!3*Kgs3;&9V1lYVa8}GuQ*I_XyMDJ2Q4qjjau%#WvVYR#Gw6Sl^^tCamV(CM07GLs5>GV*kcM;$ z1xYbB#v;*pI?fo2F$6GlqaUD1EFPcCq5>i)5Zr(P;s8c?JdHplAPorwI?@oplaUlW z8H>aMSR-Sq34kS=;C_JE%4L96Ne=$eD-je8grZZ8$#f&ICX9^GIHVzk1|Usn6burJ zGs598c$^8IVzK~5qY~}eTqYSzCxb~208kuOz`}rtaH5qf$qa!tK>saq4JPyHpn(~} ziNOjL{5{~oU;^%ZvWQKLF^o0+qA^4a&I4me zG{h6Jh8xgmBKik?HjP1#_}{ce%>y@GOt}Mt2hJa{AX@AwcOYc(Y4I_bvCv9z_(D?< z$<)OVc;qmEw%{j-wKzl#B(nklaP|0FuHWR0e^Uy06N;e;nm|EPaRe}bbQ}(8LNG=n z0VhGk!WjRI&STU0;bbmg6#()Gas`&>0$1>L3yE6)vvznOAj$$r7!r*^E)s?{ zCK?gG5{CNEhe#oy@mMSkjik{<)lMZ-ktP&06^SPR6g-+trlT>7b?}|Q|9gl(*2LEk zO;Ms{@mo?&QU4?DiwZxiqaZblGH`tcmwVLr_5Leopuhjg-`8^cPfh`c|C;0<@%xLe zUv&K=2L6%quj=|m*FR$5A1VK;uKzc>Wd8nQ1X$okFC6?HyefEg4g8jrq-@!23)u_V z2T2l|!@hu$r5uMHJP2gjD$xOj9M4k*g;IQyvz^ons2mKXEFSXtHUuJmmtau3L5pF_RQbZfWK9*njMy#s8P4|f5r>WS!J(c4X8=)|0`Kp%AnoD86q{CMw z1m+=+#HYpQ*V{oze&TxW<(ft-Q*t@+Bm;<)j@ia;srO?xMfQ1-oOtt+;xE^ns$4L= z&^C1S{K|{MWOHfGW5RXvrcS7yazmP@guC14q1eBAouT;3idzJ z;V~-aP)nwJnF+F@*6!M~9eMwr19ydqtnaHrW!2}pRfAc#A1K4j#eU0ta|YErf{qqm zFRHtGJ+W!$c_D3NSKpLos{+Tj>lPw=TlMz-HA21BeIME#@x9UUTkJY^YiEpX7)epA z99|x~{kmXw&|}lBqg!0|Q#JQOJn);h)|TBl>XDm8b4EhlSwzP+IZk>Mx;y@!iM=kOmXZ1&ikC#Ip=lG`#kT@9Cx(06cttw z27y4LR(s8Gz^~ETM@RtpE}?}Cfj~m#k$87Djt~xEFzHlUAO*sXU{D|w4vh)|aRz4G zy({0Si$(d#RPiZo2X}z^T&j+EtX7wwIyJWPa&VSbqI|h8#qOwMttXVQIx(!aA_BjZ zX_D?$U9y!^n5nAmmGLU7=FmyYfqP%AdzGRN*Kd0#E>tV;XV4^jvu*g@y@uF@r;MHA z@$EOZewYyam>JuCA~ygUmMSC{wyEUo_Dc)KjT@?29tqFWx1D`75pn&6b^GVvqPdMh z!^w@&4x1jMZrdqmDt>NG`@>$a&rtEk2y|5b>u)K)_1;a&XWY~?HoBG6^YvH=;h{)U z_mLdOy8W$Xvk7k1Qr@PYcHWOPJRD(Y_F3WdmM@m88E=wuW=SqzKTPiYvg*;%J`m8R z>WMq*S!Vw7Ol28D%p%>vo5!n1>6H>9ZK+|@yLlS?^sW54idXd(JYH&-{3}{zCJa*1 zTw>m)-LK;OC1$gZp zkP%8ixXZ8pZs1w)Q6XoM?Bg5wVm!&m6;f?8NM;Zb&;iDIw38r@)6os~IHIes^ZlYW z2j-xul~673JJZgXqmU-e^2>uDS8JZtF?@5bvbvQSzEb+~lML<4#RpvFTF+pLe%~3; zRX3KoZzMfQ8djHqv$=xbl&9hnaINt9fWqs*^1-<`@!-hP43tDS zVc+l0Poa_nABj76xL)8u+5LfCP_OE`ZdB7sDBNk|=0kb*>|L>Ud30n`tY@QR zesYF*XZTZw?nKt{NhUO96Da?8s6@9ISjaA=--&2Fb{|8vJhoCHku80%Gfzo?ujk|r zwV`qmHgx*_SEWG{dxsm3hoN$c^}1=xCttV~|8X|ALUK>lqa%@sO7U(#8a`95!C_+V z<&p62Z@M3f;z2J>LX2G=M4R=WbG+z5dF@g@WRR8`Wg-(0mgwy#Tf7l#5=H{!Jdn#F1ea`x7s$X@oJ#)9M z9DLqHeAB7o3AdEE;fsSBHi`XA?bv;)#D~yyHIs$}*G?SveU(A7Sa(YyafcOL=YYA9 ztgvQd#tjlu_*yBkUA;%%z|kf&CBIP875`~U&E~ONm|{=h$FHXQz0IPbuWw!N3We0U z9ca!?jyKp2?TXoOoBx0)Pp#bK&7t0j!HtdlxCy-D9e$PSwhrRffRNsD@3tF0@g}=m zSvQ-L-lwE1%Imb>iccKln?HUrZMpzyccO}4etsa{(}|m@!(~lUt*v(L_cU`17`zl2 z=*wGSTs}Y(JUZn=cb1@gQ_4olx+Mn`uJbS=kd z;Ys6Y5ufBz$6SkE)wKj}SExv^@oZsjpJ!{7E7L#kwO96#)bq+$q?B9F;;RFz=nR+b71_vV zm?s3iIcd@_Qt}Y@II;3nVI9QQ_`_?$?IZj|vV)+bo_s!~;{1?7?{Jh@)1I$> z1_>uJ_ia)@B#C}0rn&gscj#*Qz2y}?e^BYEzmuu?yTI|sBXj#Q@~#+8y04DAi!B8tXsbFtiI28z56IdaKPQS{WM$M%GmqZSP;a5f@xQ)O=g* zy(_&zthHNQpaa*Yo&($Ex4mJj?H-vo!k6ydhx9w^`9;q9!RFHp3eRTauC7SBwJ5(y zE_VA3azCM?fOYJ-?1L+OPhqF7sPu2Ys$UgpCS7bf+V=d(^mWz)02D*8*iaHp<{2Ap~>g7O!6B0P#$ z#d@jf!pfE%xw>1_dh#yiROdnW8Qh#Q8}D`ztHykmdZ*tqUUxnxd&`k!(P*b2 ze_yOBEz3^~#8k5uR}`YBwJneZn|nlNOdT{Z(_DVb*{dZDWdq(a2($gi?7>K`RZ`)w zRAE%@yE$i2bmzH=C=c+4c`eJ)zU*z*sP^+hk78$S(6e`%R_GT}?=*F+)=K0j=kh$1 zm;G&i(J!^sf-|YD(~`PHshV)|c;XU@QY#|~SNag?HO`U|Gdw-A{|ck7gu%1DFvl)t zhsi;R20y2<@!60nV!LERe;cmCe1ZBxCS3Q$&8TvHy|y?H*pxbhrzS^M9XfJm^L&PV zTusV>ePiXxEzxP)TiuoUXlDZhXY2D0>g3vZ<08U0OKv?TJ8`n|_x7N=Vh+4uD3h&J z6OitcDSfM7Bg*oPO`DI4dH{M!t}nnov^+FBMoC|?J6=>^H&y{V+hbWs4^tainrVL% z%%7fovt?M<^Q9EyNY>s!gRd4?$FPD&ibwC=RT?y_@SSImplm+V0v2}TEkfCEd(ZJH zY~n;~y`(P=3S>LI4)}0sk6zd;U12n+h+K_++Ol^GQ?%L(`f%XZPgzO--$Cvk^{o zO~!ASIkB|sYd86AE8bJ#Vs@YYr)`vT*`ekZ%L&Tc&QNl#T7z9L!u{R7z20^{ex2vy zu;s;_aXqXby2hqTf-P5AiVSmJpft@pAC_3{zcqQd@Ap>}?G?F@(Hty6IruaP1U^PH zHFdNyHU0DK0-RW~52fkvZE=)7=CH32S}H6n8FJt8yn131`hZN57M++vQJBQ9nd*0=Y%o}tyOw0`(ifQtpmh>aV ziZdw%QX$(4k$Rc_i@Bvi{aQO zbLN9F)z~x=Sf==QMZ-_qR&q-JxRA{_WfF36q4m0T5GhFxu^Cu#(rCj z)Z99Y=!<6Dr+Z)NGI7iy!Ns$30 z3>j)@Agss10sui2HUYv33JhjpIr`9bTr6H>niL3eW<&gBgB->q(HPZv@{Sf za}F&`6KWs~(PNUSSe%)~4+!8+AL`F$Gq7-YczC!*I7)-g^n)WY7z`Yt3D?ww0U9t? zL@=Acfd#Xa)*!xPm{C|HCXK63kZm6@pCqY0n5{2Ck<= zCc!CzlpsKr1$agN&84N4o#Ri7H46M_L5y`Pfb73%vT4-6$ogAsYb)#N{2B;g{uB3a z+JEN0t_*0|*n%%b&}3z;+@N*aR~In*x9$G*MUt5{pFPky=Wi@ecw^el#uUt-){qH>stu|S>F^` z0_l4SEJ7HCydEcj^?ixtPYCv-0Mp|~yZ$Ms{kK*?6SdF?3=u{`VSxHkQ79N1ql17^ zNV)_hMi=NNq|V>bS#&BpoWP_Q`vE)xTmkL5&J|?$dZASQt{v`AS*rp-7z}}g{U>4Y z9|^%sqn6TNkXUxo}|_gw}IFJKnJe-6VRoUM(W|Hs#lTKqqH0HFVM@{joa zm#%;5`bP}>Bjta)>tDM55d;57`QPsPKch?dug57$FmMVA2OgHT9BEhv9<&6AHkM|f zD9}+*VzYtRH$d_mW3LAb1QOY}_5p)3v*iJyAlu5$TyO#`DyFc9cb7d62*k%@WoC>A z{`1e=pNMx+yk{65_EH^1p3lYQO0$=F?I!ySy%-sZw)}^Lzp$!yzjGd6loqV0-(lAF z1!6flE)iSi>L`nrfW~e>%R?_gcN!GXW}erN*Q4}1J#Kit>m1s{TI^Ek78H0rv}Y-N z_H)kg=kvfDoX|4DKr4zRw1lXQ*3@$a3E)W^TG6$FbM^}@=U&FAk5b|MlXTV$&6lsZ zsC*ta=hW_Jz%K+_l`#|=#^Y9~r$NyY!2)660NlW5ZOUw!pb6LN(6j(zSo{vsH#L1U z@aYxt!XA2 z3YCHLt&>7d@$5*MCtiLF?uc1-9=QChf-OK(xdn?=WaHEc=-~sKq*|>B*4eaKuc^gS$mX2yvNr z_~dyPqhtLY{>t(vqsrcxT}`V6EweQY@=_#|PA+s)|^+?*!5}{BFK-Yvk8ST(A#x_0fgxz7ZAp)e6^Auj|(!qkLzQ{w`%wMHC@BRp*mQlR!U_F1guR~5ip^b zNg+soqbV}_NE`dJ-u4Mn};+~#J zPTD)G;I&h-;$G{U-QfqZX4-tWqMDHn=5Uavx@tUyD#-~ef|4UlHE&_ChtNm_FK+_ zMW-r{5Xr?Pr|~^IQ-4uj5J-t*Z!TV%ty|sDS4N#Bt`snsJ~{akV~o z0=nRwBOcGX-2PMgd0%0^?o3(cL(^*yXIErbg^snDUo?4E@YQ|w6`>~n7wc8&)$^jH zdxKjp2p!Xo?$hLW&axkyC#rJrK6LpOTXDATw*8dI(-FI3YR>h(k>8nrxhH7Yt}54) zrj+~l9?u+BeM^szcHVw+N|Si_-KUROdI#jes=UA~e=eKq(0rvM7~T|D0Hu@PP(4@`TUf}zb};Ueff0Ba@LLLw%V)t z&bHSse|1_sY^}Q0A{NeG>w&mh46}@LEtCged%jSisu1S-Ecz@*p_(m2EG*>iNkxiV zUoObDz3<&xZ=TY+wm+B0C`s8=nR3VV@a*%nh;jHNn~ZRd?<@4U3=_8M_ z{xv%kjdtAYtNhei+%G6)xewcWPd5Ke+}L+fF|kEm-Y|OQKOZZ%Vpi))-LJc>yETDS zM=eWctK#hA>jxN2?rTnAZM-N*jS}yZ}%PBLmC}FLsqV2+LC)bX)<0-Zsw}*E>BK8z~-Il&T zI$+{RCwlWNw{Gh(rytaKT6jnwYIfwWh)um6&S{#T9h>||Uo`3wORpYp*Fe90D3g=9 zmqRY<@?NyU!DD52p}uaL%_txB{G``wH8rSDL$>qpSG^Xki1Pg5@U3&DaV2NM&&(7y zdEPBv6n*db*=TZPbl2A2mD>v|j?^X2@e5iwa+;mn)Bp#MT+}U~Dj;jMebISAui3ho zv3b+WM>+ExLzi0Db}sL)>1Zx^U2DaeU_YE6lhA`ZTRt^iTOdyxb2F*U+|tV|a?Zt= zeP)c}UDxN#d13kBwgshpZo%>4^PWO|A;%;^>+HAv*3iE655~XtOunqM(H^YS#BXhT zVG`6>x{*^|{3h+IRseN*oINDb)nbq%bK6S*VvK z(MjIcu$NjQ<%jtP3{rqQ0Vz(aRq?4*olZy5F(^uPES1LN@u&!$N~ea1*3~=}p8eApql1g1_@uC8`sQ=}2%YkwC}+R0DRU zeXwPqNF4gk!yrMdOs+C|0kJ(hR_%UlPx84a3(=!N->O#vDtJo!jUrAbQ+VONm)avL<)@- zRp5ky3XoG|fCodE2!=CpvV@M&$V^mfbL!$mB%07B#3zV{;5C)7W$#oectb51LJ= zHJ}p>oHT^Opc_{VW#NNlfUu~cPXU3^4szl9sR>l8R7WV42?CNKB-r5jZdnYDlLXbG z{-~A!qzIkCM`(N+BZ9`{GueEEJ_$ki$Pm0zB9kWnH?-mKz+M9-50YuX{>etuz=;Yc z77g4D+$PA3M+t_Fhk}pd11V_GBtl}06L1YI;c=)UmH^XZuwCEUWq;BN94^M>B0P+Y zGkBnWQU-&}<*^YmfpbtAj{~}i#{Q69qm*iOsG9JN1v&z)Kzkasf<29en(!f77e^SX z00bi=H1dF8bT*&G8x)NC*Qbc!IGZ4(Y%-6{KtPKk1eqt{vB(?_oy)**9Fs5xy8WGi z|9grLrTP-eBaZ9lMBse+<45M=X(;WB{=3Y~#*xK<<<4sSOZVM{S>w`BDJ(+-ipZv^)u zzR=mVy0&ud6>yD=o{YqG_E&E^+kd6`LHfw~FcNpzJomLrunmGB-%iCCr7=!DBlm0_ zcH1LpG}>b3$i9V*3qG+Pt3G*Rmh+o=rznn!o9EZB|K4ZGw{_W#kc&xOwxiT@^C3HW zdU4FF&mGxMk?!tJS|h+j=X6i@&oBw&_6D~0pN)kow9krje#$Z1yjohj<(FO4X%N)? Xe5yy#>||~` literal 0 HcmV?d00001 diff --git a/theme/purple/icons/buy.png b/theme/purple/icons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..8e41a7b08888bff66fbe3b6a0ca6d90f44c3ce8e GIT binary patch literal 7720 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 P5d%2h0qnh=G1>nH4V<9S literal 0 HcmV?d00001 diff --git a/theme/starlight/icons/buy.png b/theme/starlight/icons/buy.png new file mode 100644 index 0000000000000000000000000000000000000000..791c08ea412d2a92f2950c77c616336082859d7e GIT binary patch literal 7496 zcmeHMc{tST+aF8zHPi^1#;(S!mYIxw-?Nl0%*tREjJ50%C2kK-Y?ZT^}fIBdarZ6@A>b{bR)b0X{F%f}Vpwd@E7Tu53p_ID{3x?>s`)|I=>#A*5j35j*OiM6}+YI8NMu&WSy4X)01C4$sB0g-$GN5dS~Y_ zH*joe!K{&5v;?B8b(eH zXHpR1eZSJVe6dy3+_ls*g2mGj-M0PITvHMrmOZ%?D2+ZIXRc=8+LGZdKUdGyk9w9o zwm7=C_h?6&u%-}p_e&&Xb3es-i07u>TF$GJtj4azwy!JAee<+c{9@7iPXTRB(V2xRhJAbdt@I*FE#JkIAGHO!;F3`HGt_~ZX+1~uVuWr&SN)8hN ziAlq-U+pm~AS76!?M~6A?_as!WKh@6Xjj?^+aGz-WxjlWO$;t3-o3%0C9Cb>Bg4Lo z1N_?O7q}Sj>pQTCb2%N+cW0&j+a5c-NRfkMN}`gNVdtzG6K2!WE;sWf)c51PR-P?+ z8{QJfd=4~J2)5aysL*X4mW}hDl^6^TuzQ{3|72k}U1Ze8vZi^}fb!KAL+nA~fmI~ZHZCnQglmX+>NFp{yBSujC#x*pipcBsF$Bw?s-*74oqHaa*bY!-r4auB{pa8 zhsRdwd)po;B?@Dc&Saa*OMsxt=N=iRc&fawdMqBA-SVJuLD}ZRm5;-O8zPj7Exzi@ zm8=7rcOBA8HWy>20?yVr__YYua@vb!``^#|sJBYLy1PwK)kx@QqL8oJohcP9CvkAF zabNas@JYdv>&T12yN*=`UemHJ@y@|;d)}GP`pmtX6UEWw8#z~!;Qn#ceb;6BRr0l@ za)FDIY> z?y_P_myPvpFT-{vt=@_>bA14oOtqx_4X0=1Mr^4CZ3|P zWG$}i@ed@aH-mC3rv^o-vYj*aFy4!Gbi>>B7o<584!E??um86XG{+We+ zjCn-(n}dGhW3U*EL-)L?*3-7m$AdbwrfsBTpYz48k)gGS6xXq6WyO$1beUY`LVaT0 zYi^0K%L#|x>~Mz7=bIL2HU{kR391;m7h?WND=x2u9F(HsX5ZmWn#~YhuFJS2)ieY} z_dCgi;e#-`w&9~MZ1C9!(3+0?CRGyW?^>wXJmPnv9aiIb?|9qB*rC&qp&M1)#nx8| zcWr}a+2r?1PmO)f7%8AEJ387cB<#k{Hypd1Cf(Mym7V+A=^a~i=!Bi;Vdxf}@WJ=n z%_Ai$Ppyg@tWA>^l6&n3h5?xR1r6o^4 z2{oyXF#2lO5IXai`=QRe>@lycL5d9nrn3BVq2lHVpC!b-`{usxUtOr~6RU0AvQ6M} z@DYdLo^2VN8QwXb(x1ObP%&Aea8E@umIH-3i@08!!_!qra!d-yn^U=xdc%Z*`r?JsnWSYxuj#m{}vM zCUt6`=SWraRu!@IkzSdldvXQ13_*=#s5(8Jk&HSP_;gHQMUX`)iTwPUYF@K^5Pz-DCov#(ko#sgw54iZ zTfpo_X~^&afkC`tiO^trDcK{^C@9M+|0H@|HZh9#b`5r^ttfYo!gOnaJ_D}i(0C?X za`t+OA0+Pvy`#>(ZqxF;?8|a>RzXT@hbU*R&be@`85kC=)~v~Wksp_Vr*};m+krqF zNi<_)2TNn)A7>!o3%0-d_nJF^W@v-(Q&=ys0Y*A zxGAv!YBB;@1v+nYQir*HJxW%Z07EHH~;y{S^rj|fq`NnA(yX3=M$AK0hOWlUvFC&dV5Xas&G37-dDo` zc&7D^eVfu`7YE~bhCU>&wayQ@x2Fcd)qdk{@|K+>f%#@RA2hu@D8Jiok(*DCYhTo} zTaQ5J^er#d*R6>6)(d-j)@xEk8%8*I3WbKU_N!S~t8R&3Mt?e|+Z!vGwC_e^-NT?y zATn+5l+4(e7+7u3QeIE<0o>#g(>@?ZU%fK;i1PWwZ6J_{2Msury4u>{NKCp0fy^XQ zG$QCM;LHjF>FP(Y2&4cC8$zV`(inK?eDf_Rghs|g_i5T9Y+1$>Kbl2UFvTg#&Y2Vy zK*Ext`g#Jo5jX&VPGJ)u5%fSt2rdE--N3~G*Xw3D6tdyM4!}cQZ5<%S%wP%xqk+*t zz)T}(VJN7c07N&KOvO2xn00>B46 zg1~|!H4tz*9sYAf2-`Fa0QsKKe~bum29EY{M@k4YG?+v&4WlsF3O_@TNq@$(LW2W0 z${~~Blt2m{a18-wMgB6SxuvbcpAqX6_|oXCjVOTZUp(0~>R)92lH2;oMmawx0)+pG z`-}IF+Bcj5FI!uj36m7Mo}Q%%9=bk1j?5&{$heJ5A_9q~V6><(Z4!n8!%&Gt7*R_L z1w&|4(OM`ZhJvJO{sd*o2w@W#B+5Dz0Iop;a4=*Y9UUqL4Z|W3L>PvEq{4JiIs_OA zO`?#=C~Y(vP5lYNK9~lmk`VZFR_jn?0E$Y|B2YB}O=xN&&@c> zwb44-M4b&NG6`qK45kx+cGBnsUkaSX@ZA_#Cmd(!V2OvKG!TDT90Cb!DiDB&+RzxG z5q}Lh)94f@HesDjq?R^NGC~W5!fIo6u-bn)?WF{V07_iPL?SfMsEv{Jw%~wb0AdO2 zIt2hW>_9Cz<6sJb%?x&CG6V6@^^_p%k$(=`0?Ua^U=vITYzhF1K%sF6Bo2voMq+Si zZ5#r%6M?`Xe)4CMY1GL7&3k?EKy<&g+=3PY%pbX7`nIB+C_&%uzTF1WHkJ|uvau*| z1k$$>LI`0L@ku(I2rLokJr?L66^(}JV6_l1 z3Q3!Q#A*X-LTder9>S!u!wJC@LtlVLfGa?r8(cwDHyWk(t9Q5`WxWdkVK4*|_Kh%< z7ETlUoiO-6Um~D(ZL+484h)SU18N5}57Qx_wP92Wk&Gn~kvb>@<=3SD?OOA5GPT|BtqRbNFc=1*rLE1NLWNyNCa|-+$+9eP{g#U*F~S56%EU|2xUw;`d*= z{-x`0G4QvX|5ew&bp0&`{+9E<>iU19OW?03MhXKs=!FB%gBS0q!~)NfJVa}A6VO3W z5-6obPh<_S@Utx3LO>uviS;)JC@&uj7oZPE3S|AYDu%(HiGw|<7 zo|~VfCHP+KXzGHI_(JQViz&Cb>4Jc&`qD)uRaZzujrv@P`4YwD`X0$FRlWqc9-s9s zT4a_+r7AQ*_4Z|`S4wlunAEXtmyoHVPVwhX*);j$@O1%S3ePp4zg75Y--qz0ac$!) z9a)jXZR0E37n+*Icy9AfZyuh?aIaJONW9ND-!~lq+f)t@q3`JBW-HbN@$2NiJ#{6H zZnnCW0MW9j@^V))0H>6-g!%Xz?e5w9@T;c}9PuRo6X$zDu15khIqvwh=vY(@cu=Ko zx+7$hjV&@FZfQHuP0m~nn^d9gMI5dSJy9X9S||U|9=X``!qWVK-|sFw{amWO zBBl2SN3VY|sDQ?_fW^6a*|xN=8AF}DJ8c~+>)hK&)D<(+Z8V;f(?933 za<#X{*}m^R)#VAg@U$Btw&wm;z22g#u>azga*85$Q$Ekv+G6nZgcPeI>#i4EWF8B8%T&JbLLY3$nU!Qw&!~&t@+O&&RUD+JX9o37cv#3WtbK76Z zzhMP!l^$aW@0~aEIRx9jD%@_j@B(}O_Dx|u(2>(612#^t6x*p3rjzgGTf?8?a`pX+ z8c13D7`@Os|2ng{TyRXz7yoT#1jsZ|PvyzEiUI3HdGhu5;Kej!$BLs;gQrAh(kjc3 zx4<1M%=-nzc!;VjkgJrM8E21FL0V~p_%OJcEwbX`{~;$-I1?dO5;xj%yf!LY zpk_4iqzdI=YO8V3n}b=J1jV~7pDJ3~ol`u!tOym&g+@EN9ZuJQ3bI~o>!uHsXO~r; zd9-*PweaGKOPpxSD1-WGQ-(4#Rzz2=_V!%nc&x-_jl=U?6}BZr*H`hJM-qud-uPSnVsyt%I{n2x7PQ~+LNPB4pw3!8%01M zkeH3Nxij!Kc|6DO79Cg1))@j0O5q5`DV4VB zmUL76+qn+)+?_}7ej5LH<4Do$$GKY329?KM51#j$YcRehlhTYvX0G!!G5ZeObSkec z(GM%Fi#_HjI+J3p#=dZ^&grpehPQGUGJWW3YWkTcDyQGO-I(7RaxkUddh+oQtf!&Q zcz;Ttb$4hLchr772UO~d$=Z39=RMOnQsOY*C3|zLQ@fVIo+5#xmd~6HD8G-o=T?1X zqzmiybVl`k={PGJIg&ClbiwYyvk}K9I*X5dCNyl%sL#9(PnN%{k9Ej67**D6I2e)T z*^Jj73w`Ym^}-AIwn<}(;?$eUw44I0T2t44pem1Ud^kd{R=&|w^;krz?seNb&2BfV zYV`42%V?n)0q**Ra54R8P~{Q4`a{zUDG)93O?xdwK)X6XvOnSsDDccc1=?a->D8mt!^O%} z!L50ab(J<*;GD(vpQThtW+#psV1?vU3-CfBhAd%|nB!!5X=a>SqIK3*g7fUHVqf}9 zGv&Sa)r^R{ysGC&?1`MdI0bguL`_i9Z%UW4+W65lsQJ^xN03hKPL9krWsME{{N)I_ zl{?hS^S~&-J8Q?zNb-wMk;BYi*SU<#R&R;^BT+ey+FI^uv0nI7u(HH{vfmR^*No3{ z!Iodv1iv}ot{DHuY`lJKLFD>5(glZ>oDF5p5;2Ptlfy97`J{P&``Avm{HcVZoMqpa zvGL~8R7ljd zoa>0c@+JAI66dl#7t)^Ix~3GVS#q|o`K6hrrDe+@?=`~$w#SvE-JeNM+vNreB|p~j zJ+P@R_4&Q@elvv&D%Ly5*%*5 z)z*%%V=i7v>1`KUG##RJS?B6%CatFys&1;>uAA?*vDGmVtKi_d+Gj2)RPeNM(vJtF zic6Cul_b$_we2N{nM3aRyjx&TPK*-<%I%1?CZ>2mEz@^-A#1kbrFI1N>4|!3y9*pP zlJ>uQDfOI}Xnz#sTCLj>mU{tW)jn)426An(_C>8zzRE!2!Rw9<33`}R&-+(17m9a& z?v)W-uTDW%PQ8z$YTb9h4Se<5SV2A6=q)OeBWB^;43Z9kMVn8iXz#a@al5kjkjA0p zi=kcc4`N~Os?%u_ z%{;K~Jng}30VC&RNu%H>qEw?!+3dtIl2u*I?La3}C2O$4*6!f@ZMPD%C4@Bx!V!Cy zDk`@Hh960BTsC@|`y?6jHmHI9#%jM&-&c_;b=C&yjp3g_<`!?AwH%uUQqP={oZOfF z=!1g#$@f*mEpXj+nf40&hhw?gZ`y87!ljzk(mISh#Ftd7JA#@M)<6w3d*k9Son3!i zMR3@s{>&rQP{k9k@7?GvNbtSITc)&Qc0Ntt7%L)r-0)?S7L2L!X54T-TaMHE6=CWp z{;={zv~&`^Hzd`bG(d&J{?rt z*cpzdcWgi z=6jWzPa;qB!=?v^CkpKM-{^cK^`US|s-T1rQO=vHJ#x5bWMfs2J~O}r9NMqI4DsG% zjqNdyF}*w;Mx0GfF{(dk=ybX!L^ZkOa{ENv`l(wsycSFK)|?MhCcO>Y!`|r6;`ZL5 zo_C_F%dpZ;Ag+|tIeYCLHw@j~|2j5ma@G+eW?k|1w!*u51uu=>7RK91v;pC|c?|BB zOGsDb;zG-t&gJ%JtXz)V`2 z0*qa&3kU>`XPB8e*_fIAIWqv0Lczfd1M6ld*?33qv#<*BFv*Zar&HQVN%(zoNlvPR zA{E5EJMSg(aa*?`Qey%d8;e@YVvki{7QY+=U0O5tYOM4nOzml|tXB)~`J7>Q?bO?+ z2csH%=w-ub@eBF1Qt1%Y%sp~3SKg{7Z(dKbBcC%=PqN`X95M)X`By71C;VBF+%EQS zYYsr>v};Lc4qt_P46XaS&+8-#$X^wZf>2#|nkveZuLYFm zS@%UeX*L>NvS4?NYer{Kz;YMTm5i2Dz7$rRC@NrOnuZk5x85*Hh#p1sUKn0jyc(qE z5XHx`&R+kLO+?MxL{^-&NQ-(JG&*+sY{!27gm~|AscpizHQ4Kq1Dkn9CCqz}gEx&A z^S9reBsi!<;kq;<)+i}KwyeG9v&ai~|NP{S8AkW4&|zbCIjT923j&FIGk_t^!`_Za zVFn>cR3@2*;03XOArA!7H{!8KlmHqRN~ZZSf(>A^wGA*RgK7YC*Re<0v&?Az4C_cX z%_Y*ol@b|1Ay8pPh9derA^;FXa(eIqO-Z>cL?Cg0Orr-vWQ4zcz8G>9E)JG{g7w^fq+C| zkQfXcuz+(Sg1ICfJead(1>zfqIgLYMGgw>(GZ?ypNg^{txdt#8pojh$Ul7aQ{s(+8 z=Q|4kA4ndFg+wDz$e-Up)29pm>a-W)+bV#6b6;JdP_#3u{4}69j-^g(cm~b znG7fE>SEw1JvvqwgT~R&be*4|Y=SvlQZR+K0tJ907yu4QSC6bqrt86}7&--xqmnUj zGF}G{r;>3Ps*VmGkD}=P1mVbL09{E6{5h%>C@KI&r|6RCIzUh8=%BE09GOakQS?5Xp~*WCi=JDpm+5nmE}Qz%U5ZUlON45|<7* z7{Kfp!J)jr6t0XQnhTe-!X{c*4@en>Bj9!Q&{#dpUq-uVY!1+gE0|~$0{cy~QWheR z3_vVtrB4BXRSzH+q8Xb;;xgH;OlF_~Y$YVV`t}&eSlvoc z=<22*k|^I&;E=*-)YUivtZyocKPlLc2Am$>+x1U5<3F^59-fTDqX=X;1xo1h@j)bCoNo#%iIq{c0WVPg|)1Ko}f_ zhJParqf68wd?yU~-%pW-!s$_Hcp4l-!;#=PT{IO=MiF%2cwG#ZO4p@eXe9ElLI2-V z{JAH-r>KuyITrscsy_06l>M8*Pv?W&BTf{lC#A^4A+9Ef^T|!h!d};&&3~fp%{ZZ_S0DOS`#K(OLea}kZQ-d_p~j(s!!lX<7I9~V&v*c1qlrdnK*fiir(Xw}4Mdw+i*GvXsyU7khK}L% zH(w?TX9@G4!x23F&1rXE%<=RKg);VD7R~5W?ak`%zLvgEvulG)m|vW0W{PZa&Asaz zqBrj)#-}i&(p(HVTY3Xp8>AHLY^_5Fwt-QzKpR@ zB@7cO`*MTxzYwNT%xJ&psew;Xnz4r!A4y#6ro3<~mQ^$m2wdZnxMyVTlQm84FDa&F zeXxR24u6DiH|8uH2-$q>)OHD_D|>$vSb#WAz29%kKf;#_*iOjt=AY0`)oK4E3M)Ao zeI>d}-152#She(%`ivMUMdG$VGC_lH_iz(NbZ@S^1nbjsy0Fd=J2U;250!hd>2pNw z=&+B7{_a~K4ocw8TG{Mjd+_I|V0QrpA;dZRqx1Yp!BDApQ7&nq4pWA5?@S!%7`VOp}CKYzOzJUl|47RlN4Ig#eEa$u@$Q9n(Zq1fP@pU#N zo%Rd{AH0DRmuI~Nzde=yO5MGz(X%Zo`cCc5%L$D;uJE_;Pu>`6NY@%IPv5sSjpoX9 zTM&6`_%Za*K)+)|(dfRyYIobMOR`I);7j3py%R}7%g5N~mu|+mJnvnr0-EURGZ~R3 znACXI^x}r6uUs{RK9}&)m{pL=-jpnIv8_M~s%nW3XehK~P`NHmQ{#`hV)FiP@0aKL zP>TB0ONo18PkrDwK1#YRdS^@N?dft*M&JGDw5TDn+G`2^e$z4}_K{SUo~gxH{|0`o zX<5;!fJAf4D}sg~AIHJhL40tnTVc+5&Gnn_c~4zBwQkG0U6AcTl&v;Gm1?mSHIX7` z_058wLOPue29#IsDY49{B02PkmbEPN-N0`^4V~x#y(sO7&nkTDcsX6W)MfkjTBUm7 z>B^KOxvSyVhZ9;$6ax>^ []: def is_quote_toot(post_json_object: str, content: str) -> bool: - """Returns true if the given post is a quote toot + """Returns true if the given post is a quote toot / quote tweet """ # Pleroma/Misskey implementations if post_json_object['object'].get('quoteUri') or \ diff --git a/webapp_confirm.py b/webapp_confirm.py index 7905272f6..b56f552ef 100644 --- a/webapp_confirm.py +++ b/webapp_confirm.py @@ -40,7 +40,8 @@ def html_confirm_delete(server, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Shows a screen asking to confirm the deletion of a post """ if '/statuses/' not in message_id: @@ -96,7 +97,7 @@ def html_confirm_delete(server, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, buy_sites) delete_post_str += '
' delete_post_str += \ '

' + \ diff --git a/webapp_conversation.py b/webapp_conversation.py index 9753e59f3..c15f26c64 100644 --- a/webapp_conversation.py +++ b/webapp_conversation.py @@ -42,7 +42,7 @@ def html_conversation_view(post_id: str, timezone: str, bold_reading: bool, dogwhistles: {}, access_keys: {}, min_images_for_accounts: [], - debug: bool) -> str: + debug: bool, buy_sites: {}) -> str: """Show a page containing a conversation thread """ conv_posts = \ @@ -96,7 +96,8 @@ def html_conversation_view(post_id: str, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str: # check for "HTTP/1.1 303 See Other Server" if 'X-AP-Instance-ID' not in post_str: diff --git a/webapp_create_post.py b/webapp_create_post.py index 8155b6c96..0652a2ef7 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -239,7 +239,8 @@ def html_new_post(edit_post_params: {}, dogwhistles: {}, min_images_for_accounts: [], default_month: int, default_year: int, - default_post_language: str) -> str: + default_post_language: str, + buy_sites: {}) -> str: """New post screen """ # get the json if this is an edited post @@ -390,7 +391,8 @@ def html_new_post(edit_post_params: {}, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) reply_str = '\n' diff --git a/webapp_frontscreen.py b/webapp_frontscreen.py index 6c829bcdf..4c3158130 100644 --- a/webapp_frontscreen.py +++ b/webapp_frontscreen.py @@ -40,7 +40,8 @@ def _html_front_screen_posts(recent_posts_cache: {}, max_recent_posts: int, lists_enabled: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Shows posts on the front screen of a news instance These should only be public blog posts from the features timeline which is the blog timeline of the news actor @@ -95,7 +96,8 @@ def _html_front_screen_posts(recent_posts_cache: {}, max_recent_posts: int, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str: profile_str += post_str + separator_str ctr += 1 @@ -128,7 +130,8 @@ def html_front_screen(signing_priv_key_pem: str, max_items_per_page: int, cw_lists: {}, lists_enabled: str, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show the news instance front screen """ bold_reading = False @@ -204,7 +207,8 @@ def html_front_screen(signing_priv_key_pem: str, signing_priv_key_pem, cw_lists, lists_enabled, bold_reading, dogwhistles, - min_images_for_accounts) + license_str + min_images_for_accounts, + buy_sites) + license_str # Footer which is only used for system accounts profile_footer_str = ' \n' diff --git a/webapp_likers.py b/webapp_likers.py index d5221b8da..57d755f81 100644 --- a/webapp_likers.py +++ b/webapp_likers.py @@ -43,6 +43,7 @@ def html_likers_of_post(base_dir: str, nickname: str, box_name: str, default_timeline: str, bold_reading: bool, dogwhistles: {}, min_images_for_accounts: [], + buy_sites: {}, dict_name: str = 'likes') -> str: """Returns html for a screen showing who liked a post """ @@ -113,7 +114,8 @@ def html_likers_of_post(base_dir: str, nickname: str, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) # show likers beneath the post obj = post_json_object diff --git a/webapp_moderation.py b/webapp_moderation.py index 5ebb799a5..3ff6b93f6 100644 --- a/webapp_moderation.py +++ b/webapp_moderation.py @@ -61,7 +61,8 @@ def html_moderation(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the moderation feed as html This is what you see when selecting the "mod" timeline """ @@ -88,7 +89,8 @@ def html_moderation(default_timeline: str, max_like_count, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, - min_images_for_accounts, reverse_sequence, None) + min_images_for_accounts, reverse_sequence, None, + buy_sites) def html_account_info(translate: {}, diff --git a/webapp_post.py b/webapp_post.py index 31c8ac4da..161bf2d55 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -81,6 +81,7 @@ from content import get_mentions_from_html from content import switch_words from person import is_person_snoozed from person import get_person_avatar_url +from webapp_utils import get_buy_links from webapp_utils import language_right_to_left from webapp_utils import get_banner_file from webapp_utils import get_avatar_image_url @@ -1648,6 +1649,7 @@ def _get_footer_with_icons(show_icons: bool, like_str: str, reaction_str: str, bookmark_str: str, delete_str: str, mute_str: str, edit_str: str, + buy_str: str, post_json_object: {}, published_link: str, time_class: str, published_str: str, nickname: str, content_license_url: str, @@ -1661,7 +1663,7 @@ def _get_footer_with_icons(show_icons: bool, footer_str += '

\n' footer_str += \ reply_str + announce_str + like_str + bookmark_str + reaction_str - footer_str += delete_str + mute_str + edit_str + footer_str += delete_str + mute_str + edit_str + buy_str if not is_news_post(post_json_object): footer_str += ' ' if content_license_url: @@ -1836,6 +1838,28 @@ def _get_copyright_footer(content_license_url: str, return copyright_str +def _get_buy_footer(buy_links: {}, translate: {}) -> str: + """Returns the footer buy link + """ + if not buy_links: + return '' + icon_filename = 'buy.png' + buy_title, buy_url = buy_links.items()[0] + if buy_title: + description = buy_title + else: + description = translate['Buy'] + buy_str = \ + ' ' + \ + '' + \ + '' + description + \
+        ' |\n' + + return buy_str + + def individual_post_as_html(signing_priv_key_pem: str, allow_downloads: bool, recent_posts_cache: {}, max_recent_posts: int, @@ -1867,7 +1891,8 @@ def individual_post_as_html(signing_priv_key_pem: str, mitm: bool, bold_reading: bool, dogwhistles: {}, minimize_all_images: bool, - first_post_id: str) -> str: + first_post_id: str, + buy_sites: {}) -> str: """ Shows a single post as html """ if not post_json_object: @@ -2475,12 +2500,23 @@ def individual_post_as_html(signing_priv_key_pem: str, if disallow_reply(content_all_str): reply_str = '' + is_patch = is_git_patch(base_dir, nickname, domain, + post_json_object['object']['type'], + summary_str, content_str) + + # html for the buy icon + buy_str = '' + if post_json_object['object'].get('tag'): + if not is_patch: + buy_links = get_buy_links(post_json_object, translate, buy_sites) + buy_str = _get_buy_footer(buy_links, translate) + new_footer_str = \ _get_footer_with_icons(show_icons, container_class_icons, reply_str, announce_str, like_str, reaction_str, bookmark_str, - delete_str, mute_str, edit_str, + delete_str, mute_str, edit_str, buy_str, post_json_object, published_link, time_class, published_str, nickname, content_license_url, translate) @@ -2495,9 +2531,6 @@ def individual_post_as_html(signing_priv_key_pem: str, if not summary_str: summary_str = get_summary_from_post(post_json_object, system_language, languages_understood) - is_patch = is_git_patch(base_dir, nickname, domain, - post_json_object['object']['type'], - summary_str, content_str) _log_post_timing(enable_timing_log, post_start_time, '16') @@ -2582,13 +2615,14 @@ def individual_post_as_html(signing_priv_key_pem: str, _log_post_timing(enable_timing_log, post_start_time, '17') map_str = '' + buy_links = {} if post_json_object['object'].get('tag'): if not is_patch: content_str = \ replace_emoji_from_tags(session, base_dir, content_str, post_json_object['object']['tag'], 'content', False, True) - + buy_links = get_buy_links(post_json_object, translate, buy_sites) # show embedded map if the location contains a map url location_str = \ get_location_from_tags(post_json_object['object']['tag']) @@ -2707,7 +2741,8 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, cw_lists: {}, lists_enabled: str, timezone: str, mitm: bool, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show an individual post as html """ original_post_json = post_json_object @@ -2791,7 +2826,7 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, False, authorized, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, buy_sites) message_id = remove_id_ending(post_json_object['id']) # show the previous posts @@ -2835,7 +2870,7 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, bold_reading, dogwhistles, minimize_all_images, - None) + post_str + None, buy_sites) + post_str # show the following posts post_filename = locate_post(base_dir, nickname, domain, message_id) @@ -2875,7 +2910,8 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' @@ -2906,7 +2942,8 @@ def html_post_replies(recent_posts_cache: {}, max_recent_posts: int, lists_enabled: str, timezone: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show the replies to an individual post as html """ replies_str = '' @@ -2937,7 +2974,8 @@ def html_post_replies(recent_posts_cache: {}, max_recent_posts: int, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): @@ -2968,7 +3006,8 @@ def html_emoji_reaction_picker(recent_posts_cache: {}, max_recent_posts: int, box_name: str, page_number: int, timezone: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Returns the emoji picker screen """ minimize_all_images = False @@ -2996,7 +3035,7 @@ def html_emoji_reaction_picker(recent_posts_cache: {}, max_recent_posts: int, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, buy_sites) reactions_filename = base_dir + '/emoji/reactions.json' if not os.path.isfile(reactions_filename): diff --git a/webapp_profile.py b/webapp_profile.py index e310f4198..fd36d423a 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -164,7 +164,8 @@ def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int, timezone: str, onion_domain: str, i2p_domain: str, bold_reading: bool, dogwhistles: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show a profile page after a search for a fediverse address """ http = False @@ -416,7 +417,8 @@ def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) i += 1 if i >= 8: break @@ -646,7 +648,8 @@ def html_profile(signing_priv_key_pem: str, max_items_per_page: int, cw_lists: {}, lists_enabled: str, content_license_url: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + buy_sites: {}) -> str: """Show the profile page as html """ show_moved_accounts = False @@ -678,7 +681,7 @@ def html_profile(signing_priv_key_pem: str, shared_items_federated_domains, None, page_number, max_items_per_page, cw_lists, lists_enabled, {}, - min_images_for_accounts) + min_images_for_accounts, buy_sites) domain, port = get_domain_from_actor(profile_json['id']) if not domain: @@ -1169,7 +1172,8 @@ def html_profile(signing_priv_key_pem: str, cw_lists, lists_enabled, timezone, bold_reading, {}, min_images_for_accounts, - max_profile_posts) + license_str + max_profile_posts, + buy_sites) + license_str if not is_group: if selected == 'following': profile_str += \ @@ -1269,7 +1273,8 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, timezone: str, bold_reading: bool, dogwhistles: {}, min_images_for_accounts: [], - max_profile_posts: int) -> str: + max_profile_posts: int, + buy_sites: {}) -> str: """Shows posts on the profile screen These should only be public posts """ @@ -1327,7 +1332,8 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str and item_id not in shown_items: profile_str += post_str + separator_str shown_items.append(item_id) diff --git a/webapp_search.py b/webapp_search.py index 70cb8e0a0..b9204371d 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -702,7 +702,8 @@ def html_history_search(translate: {}, base_dir: str, lists_enabled: str, timezone: str, bold_reading: bool, dogwhistles: {}, access_keys: {}, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show a page containing search results for your post history """ if historysearch.startswith("'"): @@ -812,7 +813,8 @@ def html_history_search(translate: {}, base_dir: str, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str: history_search_form += separator_str + post_str index += 1 @@ -840,7 +842,8 @@ def html_hashtag_search(nickname: str, domain: str, port: int, timezone: str, bold_reading: bool, dogwhistles: {}, map_format: str, access_keys: {}, box_name: str, - min_images_for_accounts: []) -> str: + min_images_for_accounts: [], + buy_sites: {}) -> str: """Show a page containing search results for a hashtag or after selecting a hashtag from the swarm """ @@ -1032,7 +1035,8 @@ def html_hashtag_search(nickname: str, domain: str, port: int, store_to_sache, False, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str: hashtag_search_form += \ text_mode_separator + separator_str + post_str @@ -1075,7 +1079,7 @@ def html_hashtag_search_remote(nickname: str, domain: str, port: int, timezone: str, bold_reading: bool, dogwhistles: {}, min_images_for_accounts: [], - debug: bool) -> str: + debug: bool, buy_sites: {}) -> str: """Show a page containing search results for a remote hashtag """ hashtag = hashtag_url.split('/')[-1] @@ -1235,7 +1239,8 @@ def html_hashtag_search_remote(nickname: str, domain: str, port: int, store_to_sache, False, cw_lists, lists_enabled, timezone, False, bold_reading, dogwhistles, - minimize_all_images, None) + minimize_all_images, None, + buy_sites) if post_str: hashtag_search_form += \ text_mode_separator + separator_str + post_str diff --git a/webapp_timeline.py b/webapp_timeline.py index e1ce92329..ee9d21483 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -501,7 +501,8 @@ def html_timeline(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the timeline as html """ enable_timing_log = False @@ -1037,7 +1038,7 @@ def html_timeline(default_timeline: str, timezone, mitm, bold_reading, dogwhistles, minimize_all_images, - first_post_id) + first_post_id, buy_sites) _log_timeline_timing(enable_timing_log, timeline_start_time, box_name, '12') @@ -1306,7 +1307,8 @@ def html_shares(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the shares timeline as html """ manually_approve_followers = \ @@ -1339,7 +1341,7 @@ def html_shares(default_timeline: str, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) def html_wanted(default_timeline: str, @@ -1371,7 +1373,8 @@ def html_wanted(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the wanted timeline as html """ manually_approve_followers = \ @@ -1404,7 +1407,7 @@ def html_wanted(default_timeline: str, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) def html_inbox(default_timeline: str, @@ -1438,7 +1441,8 @@ def html_inbox(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the inbox as html """ manually_approve_followers = \ @@ -1471,7 +1475,8 @@ def html_inbox(default_timeline: str, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + buy_sites) def html_bookmarks(default_timeline: str, @@ -1504,7 +1509,8 @@ def html_bookmarks(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the bookmarks as html """ manually_approve_followers = \ @@ -1536,7 +1542,7 @@ def html_bookmarks(default_timeline: str, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) def html_inbox_dms(default_timeline: str, @@ -1570,7 +1576,8 @@ def html_inbox_dms(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the DM timeline as html """ artist = is_artist(base_dir, nickname) @@ -1598,7 +1605,8 @@ def html_inbox_dms(default_timeline: str, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, + buy_sites) def html_inbox_replies(default_timeline: str, @@ -1632,7 +1640,8 @@ def html_inbox_replies(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the replies timeline as html """ artist = is_artist(base_dir, nickname) @@ -1658,7 +1667,7 @@ def html_inbox_replies(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, buy_sites) def html_inbox_media(default_timeline: str, @@ -1692,7 +1701,8 @@ def html_inbox_media(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the media timeline as html """ artist = is_artist(base_dir, nickname) @@ -1718,7 +1728,7 @@ def html_inbox_media(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, buy_sites) def html_inbox_blogs(default_timeline: str, @@ -1752,7 +1762,8 @@ def html_inbox_blogs(default_timeline: str, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], reverse_sequence: bool, - last_post_id: str) -> str: + last_post_id: str, + buy_sites: {}) -> str: """Show the blogs timeline as html """ artist = is_artist(base_dir, nickname) @@ -1778,7 +1789,7 @@ def html_inbox_blogs(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, last_post_id) + reverse_sequence, last_post_id, buy_sites) def html_inbox_features(default_timeline: str, @@ -1812,7 +1823,8 @@ def html_inbox_features(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the features timeline as html """ return html_timeline(default_timeline, @@ -1837,7 +1849,7 @@ def html_inbox_features(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) def html_inbox_news(default_timeline: str, @@ -1870,7 +1882,8 @@ def html_inbox_news(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the news timeline as html """ return html_timeline(default_timeline, @@ -1895,7 +1908,7 @@ def html_inbox_news(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) def html_outbox(default_timeline: str, @@ -1928,7 +1941,8 @@ def html_outbox(default_timeline: str, timezone: str, bold_reading: bool, dogwhistles: {}, ua_str: str, min_images_for_accounts: [], - reverse_sequence: bool) -> str: + reverse_sequence: bool, + buy_sites: {}) -> str: """Show the Outbox as html """ manually_approve_followers = \ @@ -1956,4 +1970,4 @@ def html_outbox(default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, bold_reading, dogwhistles, ua_str, min_images_for_accounts, - reverse_sequence, None) + reverse_sequence, None, buy_sites) diff --git a/webapp_utils.py b/webapp_utils.py index 984e8c78a..7d8697990 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2080,3 +2080,59 @@ def html_following_dropdown(base_dir: str, nickname: str, following_address + '\n' list_str += '\n' return list_str + + +def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: + """Returns any links to buy something from an external site + """ + if not post_json_object['object'].get('tag'): + return {} + if not isinstance(post_json_object['object']['tag'], list): + return {} + links = {} + buy_strings = [] + buy_strings += translate['Buy'].lower() + buy_strings += translate['Purchase'].lower() + buy_strings += translate['Subscribe'].lower() + for item in post_json_object['object']['tag']: + if not isinstance(item, dict): + continue + if not item.get('name'): + continue + if not isinstance(item['name'], str): + continue + if not item.get('type'): + continue + if not item.get('href'): + continue + if not isinstance(item['type'], str): + continue + if not isinstance(item['href'], str): + continue + if item['type'] != 'Link': + continue + if not item.get('mediaType'): + continue + if not isinstance(item['mediaType'], str): + continue + if 'html' not in item['mediaType']: + continue + item_name = item['name'] + # there should be no html in the name + if remove_html(item_name) != item_name: + continue + # there should be no html in the link + if '<' in item['href'] or \ + '://' not in item['href']: + continue + # does the name indicate buying? + for buy_str in buy_strings: + if buy_str in item_name.lower(): + links[item_name] = item['href'] + continue + # is the link on an allowlist of sites? + for site, keyword in buy_sites.items(): + if keyword in item['href']: + links[site.title()] = item['href'] + continue + return links From 9d844e641b90b2d448a8e7ee9580286184c4f8b7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 15:16:08 +0000 Subject: [PATCH 02/12] Load buy sites from file --- daemon.py | 3 ++- webapp_utils.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 4d23b3aee..86cdfd30b 100644 --- a/daemon.py +++ b/daemon.py @@ -178,6 +178,7 @@ from webapp_podcast import html_podcast_episode from webapp_theme_designer import html_theme_designer from webapp_minimalbutton import set_minimal from webapp_minimalbutton import is_minimal +from webapp_utils import load_buy_sites from webapp_utils import get_default_path from webapp_utils import get_avatar_image_url from webapp_utils import html_hashtag_blocked @@ -22983,7 +22984,7 @@ def run_daemon(max_hashtags: int, assert not scan_themes_for_scripts(base_dir) # permitted sites from which the buy button may be displayed - httpd.buy_sites = {} + httpd.buy_sites = load_buy_sites(base_dir) # which accounts should minimize all attached images by default httpd.min_images_for_accounts = load_min_images_for_accounts(base_dir) diff --git a/webapp_utils.py b/webapp_utils.py index 7d8697990..2c9cf2b48 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2136,3 +2136,14 @@ def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: links[site.title()] = item['href'] continue return links + + +def load_buy_sites(base_dir: str) -> {}: + """Loads domains from which buying is permitted + """ + buy_sites_filename = base_dir + '/accounts/buy_sites.txt' + if os.path.isfile(buy_sites_filename): + buy_sites_json = load_json(buy_sites_filename) + if buy_sites_json: + return buy_sites_json + return {} From 78bc0937e222cd13572fafe612c4cffcf2d6b9b8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 15:30:15 +0000 Subject: [PATCH 03/12] Buy button logic --- webapp_utils.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/webapp_utils.py b/webapp_utils.py index 2c9cf2b48..9c48949c4 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2118,23 +2118,29 @@ def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: if 'html' not in item['mediaType']: continue item_name = item['name'] + # The name should not be excessively long + if len(item_name) > 32: + continue # there should be no html in the name if remove_html(item_name) != item_name: continue # there should be no html in the link if '<' in item['href'] or \ - '://' not in item['href']: + '://' not in item['href'] or \ + ' ' in item['href']: continue - # does the name indicate buying? - for buy_str in buy_strings: - if buy_str in item_name.lower(): - links[item_name] = item['href'] - continue - # is the link on an allowlist of sites? - for site, keyword in buy_sites.items(): - if keyword in item['href']: - links[site.title()] = item['href'] - continue + if buy_sites: + # limited to an allowlist of buying sites + for site, buy_domain in buy_sites.items(): + if buy_domain in item['href']: + links[site.title()] = item['href'] + continue + else: + # The name only needs to indicate that this is a buy link + for buy_str in buy_strings: + if buy_str in item_name.lower(): + links[item_name] = item['href'] + continue return links From 6928de6465fd24d3952974603050d9dbc472c2b3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 17:41:48 +0000 Subject: [PATCH 04/12] Buy link on edit new post --- daemon.py | 13 +++++++++---- translations/ar.json | 3 ++- translations/bn.json | 3 ++- translations/ca.json | 3 ++- translations/cy.json | 3 ++- translations/de.json | 3 ++- translations/el.json | 3 ++- translations/en.json | 3 ++- translations/es.json | 3 ++- translations/fa.json | 3 ++- translations/fr.json | 3 ++- translations/ga.json | 3 ++- translations/hi.json | 3 ++- translations/it.json | 3 ++- translations/ja.json | 3 ++- translations/ko.json | 3 ++- translations/ku.json | 3 ++- translations/nl.json | 3 ++- translations/oc.json | 3 ++- translations/pl.json | 3 ++- translations/pt.json | 3 ++- translations/ru.json | 3 ++- translations/sw.json | 3 ++- translations/tr.json | 3 ++- translations/uk.json | 3 ++- translations/yi.json | 3 ++- translations/zh.json | 3 ++- webapp_create_post.py | 14 ++++++++++++-- webapp_post.py | 28 +++++++++++++++------------- 29 files changed, 88 insertions(+), 45 deletions(-) diff --git a/daemon.py b/daemon.py index 86cdfd30b..8672b118f 100644 --- a/daemon.py +++ b/daemon.py @@ -3671,6 +3671,7 @@ class PubServer(BaseHTTPRequestHandler): if self.server.default_post_language.get(nickname): default_post_language = \ self.server.default_post_language[nickname] + default_buy_site = '' msg = \ html_new_post({}, False, self.server.translate, base_dir, @@ -3712,7 +3713,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, self.server.min_images_for_accounts, None, None, default_post_language, - self.server.buy_sites) + self.server.buy_sites, + default_buy_site) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -3832,6 +3834,7 @@ class PubServer(BaseHTTPRequestHandler): if self.server.default_post_language.get(nickname): default_post_language = \ self.server.default_post_language[nickname] + default_buy_site = '' msg = \ html_new_post({}, False, self.server.translate, base_dir, @@ -3872,7 +3875,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.dogwhistles, self.server.min_images_for_accounts, None, None, default_post_language, - self.server.buy_sites) + self.server.buy_sites, + default_buy_site) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -16268,7 +16272,7 @@ class PubServer(BaseHTTPRequestHandler): nickname, self.server.domain_full, self.server.person_cache) - + default_buy_site = '' msg = \ html_new_post(edit_post_params, media_instance, translate, @@ -16313,7 +16317,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.min_images_for_accounts, new_post_month, new_post_year, default_post_language, - self.server.buy_sites) + self.server.buy_sites, + default_buy_site) if not msg: print('Error replying to ' + in_reply_to_url) self._404() diff --git a/translations/ar.json b/translations/ar.json index f95a25c0f..a905add84 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -610,5 +610,6 @@ "Registrations remaining": "التسجيلات المتبقية", "Edit reminder": "تحرير التذكير", "Purchase": "شراء", - "Subscribe": "الإشتراك" + "Subscribe": "الإشتراك", + "Buy link": "رابط شراء" } diff --git a/translations/bn.json b/translations/bn.json index 9f2285128..6f8c9a2fa 100644 --- a/translations/bn.json +++ b/translations/bn.json @@ -610,5 +610,6 @@ "Registrations remaining": "রেজিস্ট্রেশন বাকি", "Edit reminder": "অনুস্মারক সম্পাদনা করুন", "Purchase": "ক্রয়", - "Subscribe": "সাবস্ক্রাইব" + "Subscribe": "সাবস্ক্রাইব", + "Buy link": "সংযোগ কেনা" } diff --git a/translations/ca.json b/translations/ca.json index ec85663ca..7d2a1a673 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -610,5 +610,6 @@ "Registrations remaining": "Inscripcions restants", "Edit reminder": "Edita el recordatori", "Purchase": "Compra", - "Subscribe": "Subscriu-te" + "Subscribe": "Subscriu-te", + "Buy link": "Enllaç de compra" } diff --git a/translations/cy.json b/translations/cy.json index 0f611cf52..9b1c34612 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -610,5 +610,6 @@ "Registrations remaining": "Cofrestriadau ar ôl", "Edit reminder": "Golygu nodyn atgoffa", "Purchase": "Prynu", - "Subscribe": "Tanysgrifio" + "Subscribe": "Tanysgrifio", + "Buy link": "Prynu dolen" } diff --git a/translations/de.json b/translations/de.json index 77e57c285..e9381fe8f 100644 --- a/translations/de.json +++ b/translations/de.json @@ -610,5 +610,6 @@ "Registrations remaining": "Anmeldungen verbleiben", "Edit reminder": "Erinnerung bearbeiten", "Purchase": "Kaufen", - "Subscribe": "Abonnieren" + "Subscribe": "Abonnieren", + "Buy link": "Link kaufen" } diff --git a/translations/el.json b/translations/el.json index 6e37693d0..f52794045 100644 --- a/translations/el.json +++ b/translations/el.json @@ -610,5 +610,6 @@ "Registrations remaining": "Απομένουν οι εγγραφές", "Edit reminder": "Επεξεργασία υπενθύμισης", "Purchase": "Αγορά", - "Subscribe": "Εγγραφείτε" + "Subscribe": "Εγγραφείτε", + "Buy link": "Σύνδεσμος αγοράς" } diff --git a/translations/en.json b/translations/en.json index 406b6eac4..b609436cc 100644 --- a/translations/en.json +++ b/translations/en.json @@ -610,5 +610,6 @@ "Registrations remaining": "Registrations remaining", "Edit reminder": "Edit reminder", "Purchase": "Purchase", - "Subscribe": "Subscribe" + "Subscribe": "Subscribe", + "Buy link": "Buy link" } diff --git a/translations/es.json b/translations/es.json index 784058bac..ce6b671ed 100644 --- a/translations/es.json +++ b/translations/es.json @@ -610,5 +610,6 @@ "Registrations remaining": "Registros restantes", "Edit reminder": "Editar recordatorio", "Purchase": "Compra", - "Subscribe": "Suscribir" + "Subscribe": "Suscribir", + "Buy link": "Enlace de compra" } diff --git a/translations/fa.json b/translations/fa.json index c49d28e7e..36b442958 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -610,5 +610,6 @@ "Registrations remaining": "ثبت نام باقی مانده است", "Edit reminder": "ویرایش یادآوری", "Purchase": "خرید", - "Subscribe": "اشتراک در" + "Subscribe": "اشتراک در", + "Buy link": "لینک خرید" } diff --git a/translations/fr.json b/translations/fr.json index a6db94b3f..dff468ab6 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -610,5 +610,6 @@ "Registrations remaining": "Inscriptions restantes", "Edit reminder": "Modifier le rappel", "Purchase": "Acheter", - "Subscribe": "S'abonner" + "Subscribe": "S'abonner", + "Buy link": "Acheter un lien" } diff --git a/translations/ga.json b/translations/ga.json index 2daed5a2b..adde1a810 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -610,5 +610,6 @@ "Registrations remaining": "Clárúcháin fágtha", "Edit reminder": "Cuir meabhrúchán in eagar", "Purchase": "Ceannach", - "Subscribe": "Liostáil" + "Subscribe": "Liostáil", + "Buy link": "Ceannaigh nasc" } diff --git a/translations/hi.json b/translations/hi.json index c7b576c6c..4f0b54782 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -610,5 +610,6 @@ "Registrations remaining": "रजिस्ट्रेशन बाकी हैं", "Edit reminder": "रिमाइंडर संपादित करें", "Purchase": "खरीदना", - "Subscribe": "सदस्यता लेने के" + "Subscribe": "सदस्यता लेने के", + "Buy link": "लिंक खरीदें" } diff --git a/translations/it.json b/translations/it.json index a46ba260a..94b851b07 100644 --- a/translations/it.json +++ b/translations/it.json @@ -610,5 +610,6 @@ "Registrations remaining": "Iscrizioni rimanenti", "Edit reminder": "Modifica promemoria", "Purchase": "Acquistare", - "Subscribe": "Sottoscrivi" + "Subscribe": "Sottoscrivi", + "Buy link": "Link per l'acquisto" } diff --git a/translations/ja.json b/translations/ja.json index cf4698433..9dfa8a408 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -610,5 +610,6 @@ "Registrations remaining": "残りの登録数", "Edit reminder": "リマインダーを編集", "Purchase": "購入", - "Subscribe": "申し込む" + "Subscribe": "申し込む", + "Buy link": "購入リンク" } diff --git a/translations/ko.json b/translations/ko.json index 9533b6bd9..77ba773c2 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -610,5 +610,6 @@ "Registrations remaining": "남은 등록", "Edit reminder": "알림 수정", "Purchase": "구입", - "Subscribe": "구독하다" + "Subscribe": "구독하다", + "Buy link": "구매 링크" } diff --git a/translations/ku.json b/translations/ku.json index b01d66621..ac8e07cd7 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -610,5 +610,6 @@ "Registrations remaining": "Registrations maye", "Edit reminder": "Bîranîna biguherîne", "Purchase": "Kirrîn", - "Subscribe": "Subscribe" + "Subscribe": "Subscribe", + "Buy link": "Girêdanê bikirin" } diff --git a/translations/nl.json b/translations/nl.json index 790f303f6..61f2141a2 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -610,5 +610,6 @@ "Registrations remaining": "Resterende inschrijvingen", "Edit reminder": "Herinnering bewerken", "Purchase": "Aankoop", - "Subscribe": "Abonneren" + "Subscribe": "Abonneren", + "Buy link": "koop link" } diff --git a/translations/oc.json b/translations/oc.json index b1608f1a0..3b7eeb875 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -606,5 +606,6 @@ "Registrations remaining": "Registrations remaining", "Edit reminder": "Edit reminder", "Purchase": "Purchase", - "Subscribe": "Subscribe" + "Subscribe": "Subscribe", + "Buy link": "Buy link" } diff --git a/translations/pl.json b/translations/pl.json index b625c3cc4..aac582b80 100644 --- a/translations/pl.json +++ b/translations/pl.json @@ -610,5 +610,6 @@ "Registrations remaining": "Pozostały zapisy", "Edit reminder": "Edytuj przypomnienie", "Purchase": "Zakup", - "Subscribe": "Subskrybuj" + "Subscribe": "Subskrybuj", + "Buy link": "Kup Link" } diff --git a/translations/pt.json b/translations/pt.json index d467b80c6..c597c1e21 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -610,5 +610,6 @@ "Registrations remaining": "Inscrições restantes", "Edit reminder": "Editar lembrete", "Purchase": "Comprar", - "Subscribe": "Se inscrever" + "Subscribe": "Se inscrever", + "Buy link": "Link de compra" } diff --git a/translations/ru.json b/translations/ru.json index 721447c70..8254236b3 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -610,5 +610,6 @@ "Registrations remaining": "Осталось регистраций", "Edit reminder": "Изменить напоминание", "Purchase": "Покупка", - "Subscribe": "Подписаться" + "Subscribe": "Подписаться", + "Buy link": "Купить ссылку" } diff --git a/translations/sw.json b/translations/sw.json index 2f4f02c10..40f4e4097 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -610,5 +610,6 @@ "Registrations remaining": "Usajili uliosalia", "Edit reminder": "Badilisha kikumbusho", "Purchase": "Nunua", - "Subscribe": "Jisajili" + "Subscribe": "Jisajili", + "Buy link": "Nunua kiungo" } diff --git a/translations/tr.json b/translations/tr.json index db2e448d0..aba9288a9 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -610,5 +610,6 @@ "Registrations remaining": "kalan kayıtlar", "Edit reminder": "Hatırlatıcıyı düzenle", "Purchase": "Satın alma", - "Subscribe": "Abone" + "Subscribe": "Abone", + "Buy link": "Bağlantı satın al" } diff --git a/translations/uk.json b/translations/uk.json index d0d848bb8..030935be9 100644 --- a/translations/uk.json +++ b/translations/uk.json @@ -610,5 +610,6 @@ "Registrations remaining": "Залишилось реєстрацій", "Edit reminder": "Редагувати нагадування", "Purchase": "Купівля", - "Subscribe": "Підпишіться" + "Subscribe": "Підпишіться", + "Buy link": "Купити посилання" } diff --git a/translations/yi.json b/translations/yi.json index b9393777b..343c45346 100644 --- a/translations/yi.json +++ b/translations/yi.json @@ -610,5 +610,6 @@ "Registrations remaining": "רעדזשיסטריישאַנז רוען", "Edit reminder": "רעדאַגירן דערמאָנונג", "Purchase": "קויפן", - "Subscribe": "אַבאָנירן" + "Subscribe": "אַבאָנירן", + "Buy link": "קויפן לינק" } diff --git a/translations/zh.json b/translations/zh.json index 997c4c53f..742429670 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -610,5 +610,6 @@ "Registrations remaining": "剩余名额", "Edit reminder": "编辑提醒", "Purchase": "购买", - "Subscribe": "订阅" + "Subscribe": "订阅", + "Buy link": "购买链接" } diff --git a/webapp_create_post.py b/webapp_create_post.py index 0652a2ef7..8a7d602e9 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -24,6 +24,7 @@ from utils import get_currencies from utils import get_category_types from utils import get_account_timezone from utils import get_supported_languages +from webapp_utils import get_buy_links from webapp_utils import html_following_data_list from webapp_utils import html_common_emoji from webapp_utils import begin_edit_section @@ -240,7 +241,8 @@ def html_new_post(edit_post_params: {}, min_images_for_accounts: [], default_month: int, default_year: int, default_post_language: str, - buy_sites: {}) -> str: + buy_sites: {}, + default_buy_site: str) -> str: """New post screen """ # get the json if this is an edited post @@ -257,6 +259,12 @@ def html_new_post(edit_post_params: {}, return '' if not edited_post_json: return '' + buy_links = \ + get_buy_links(edited_post_json, translate, buy_sites) + if buy_links: + for _, buy_url in buy_links.items(): + default_buy_site = buy_url + break if edited_post_json['object'].get('conversation'): conversation_id = edited_post_json['object']['conversation'] elif edited_post_json['object'].get('context'): @@ -481,7 +489,6 @@ def html_new_post(edit_post_params: {}, ' \n' - new_post_image_section += end_edit_section() new_post_emoji_section = '' @@ -768,6 +775,9 @@ def html_new_post(edit_post_params: {}, ' \n' replies_section += languages_dropdown + buy_link_str = '🛒 ' + translate['Buy link'] + replies_section += edit_text_field(buy_link_str + ':', 'buySite', + default_buy_site, 'https://...') replies_section += '
\n' date_and_location = \ diff --git a/webapp_post.py b/webapp_post.py index 161bf2d55..98d172b7b 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -1844,19 +1844,21 @@ def _get_buy_footer(buy_links: {}, translate: {}) -> str: if not buy_links: return '' icon_filename = 'buy.png' - buy_title, buy_url = buy_links.items()[0] - if buy_title: - description = buy_title - else: - description = translate['Buy'] - buy_str = \ - ' ' + \ - '' + \ - '' + description + \
-        ' |\n' - + description = '' + buy_str = '' + for buy_title, buy_url in buy_links.items(): + if buy_title: + description = buy_title + else: + description = translate['Buy'] + buy_str = \ + ' ' + \ + '' + \ + '' + description + \
+            ' |\n' + break return buy_str From b9c749b4b9e659f48429d954e0ef26711a9ddd16 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 17:45:06 +0000 Subject: [PATCH 05/12] No colon --- webapp_create_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp_create_post.py b/webapp_create_post.py index 8a7d602e9..e6a747ecd 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -776,7 +776,7 @@ def html_new_post(edit_post_params: {}, translate['Language used'] + '\n' replies_section += languages_dropdown buy_link_str = '🛒 ' + translate['Buy link'] - replies_section += edit_text_field(buy_link_str + ':', 'buySite', + replies_section += edit_text_field(buy_link_str, 'buySite', default_buy_site, 'https://...') replies_section += '\n' From 1b5594bcbd8fc5c846d306b0e851b337047ccc03 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 19:19:57 +0000 Subject: [PATCH 06/12] Add buy link to posts --- daemon.py | 22 ++++++++---- desktop_client.py | 9 +++-- epicyon.py | 22 +++++++----- inbox.py | 3 +- newsdaemon.py | 4 ++- posts.py | 80 ++++++++++++++++++++++++++++++------------- tests.py | 47 ++++++++++++++++--------- webapp_create_post.py | 2 +- webapp_utils.py | 7 ++-- 9 files changed, 130 insertions(+), 66 deletions(-) diff --git a/daemon.py b/daemon.py index 8672b118f..535861cdf 100644 --- a/daemon.py +++ b/daemon.py @@ -667,6 +667,7 @@ class PubServer(BaseHTTPRequestHandler): event_end_time = None location = None conversation_id = None + buy_url = '' city = get_spoofed_city(self.server.city, self.server.base_dir, nickname, self.server.domain) @@ -700,7 +701,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, False, - self.server.translate) + self.server.translate, buy_url) if message_json: # NOTE: content and contentMap are not required, but we will keep # them in there so that the post does not get filtered out by @@ -20680,6 +20681,10 @@ class PubServer(BaseHTTPRequestHandler): else: comments_enabled = True + buy_url = '' + if fields.get('buyUrl'): + buy_url = fields['buyUrl'] + if post_type == 'newpost': if not fields.get('pinToProfile'): pin_to_profile = False @@ -20730,7 +20735,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, - self.server.translate) + self.server.translate, buy_url) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache @@ -20870,7 +20875,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, - self.server.translate) + self.server.translate, buy_url) if message_json: if fields['schedulePost']: return 1 @@ -21035,7 +21040,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, - self.server.translate) + self.server.translate, buy_url) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache @@ -21146,7 +21151,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, - self.server.translate) + self.server.translate, + buy_url) if message_json: if edited_postid: recent_posts_cache = self.server.recent_posts_cache @@ -21269,7 +21275,8 @@ class PubServer(BaseHTTPRequestHandler): content_license_url, languages_understood, reply_is_chat, - self.server.translate) + self.server.translate, + buy_url) if message_json: print('DEBUG: posting DM edited_postid ' + str(edited_postid)) @@ -21385,7 +21392,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.low_bandwidth, self.server.content_license_url, languages_understood, - False, self.server.translate) + False, self.server.translate, + buy_url) if message_json: if fields['schedulePost']: return 1 diff --git a/desktop_client.py b/desktop_client.py index e7d4f6de6..5c3c7e5b2 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -528,6 +528,7 @@ def _desktop_reply_to_post(session, post_id: str, event_time = None event_end_time = None location = None + buy_url = '' _say_command(say_str, say_str, screenreader, system_language, espeak) if send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, nickname, password, @@ -540,7 +541,7 @@ def _desktop_reply_to_post(session, post_id: str, system_language, languages_understood, low_bandwidth, content_license_url, event_date, event_time, event_end_time, location, - translate, debug, post_id, post_id, + translate, buy_url, debug, post_id, post_id, conversation_id, subject) == 0: say_str = 'Reply sent' else: @@ -602,6 +603,7 @@ def _desktop_new_post(session, event_time = None event_end_time = None location = None + buy_url = '' _say_command(say_str, say_str, screenreader, system_language, espeak) if send_post_via_server(signing_priv_key_pem, __version__, base_dir, session, nickname, password, @@ -614,7 +616,7 @@ def _desktop_new_post(session, system_language, languages_understood, low_bandwidth, content_license_url, event_date, event_time, event_end_time, location, - translate, debug, None, None, + translate, buy_url, debug, None, None, conversation_id, subject) == 0: say_str = 'Post sent' else: @@ -1345,6 +1347,7 @@ def _desktop_new_dm_base(session, to_handle: str, event_time = None event_end_time = None location = None + buy_url = '' say_str = 'Sending' _say_command(say_str, say_str, screenreader, system_language, espeak) @@ -1359,7 +1362,7 @@ def _desktop_new_dm_base(session, to_handle: str, system_language, languages_understood, low_bandwidth, content_license_url, event_date, event_time, event_end_time, location, - translate, debug, None, None, + translate, buy_url, debug, None, None, conversation_id, subject) == 0: say_str = 'Direct message sent' else: diff --git a/epicyon.py b/epicyon.py index 9bb1c7126..0bbacdceb 100644 --- a/epicyon.py +++ b/epicyon.py @@ -149,6 +149,9 @@ def _command_options() -> None: parser.add_argument('--eventLocation', type=str, default=None, help='Location for an event when sending a c2s post') + parser.add_argument('--buyUrl', type=str, + default=None, + help='Link for buying something') parser.add_argument('--content_license_url', type=str, default='https://creativecommons.org/' + 'licenses/by-nc/4.0', @@ -1702,7 +1705,7 @@ def _command_options() -> None: argb.low_bandwidth, argb.content_license_url, argb.eventDate, argb.eventTime, argb.eventEndTime, - argb.eventLocation, translate, + argb.eventLocation, translate, argb.buyUrl, argb.debug, reply_to, reply_to, argb.conversationId, subject) for _ in range(10): @@ -3375,6 +3378,7 @@ def _command_options() -> None: low_bandwidth = False languages_understood = [argb.language] translate = {} + buy_url = '' create_public_post(base_dir, nickname, domain, port, http_prefix, "like this is totally just a #test man", @@ -3389,7 +3393,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "Zoiks!!!", test_save_to_file, @@ -3403,7 +3407,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "Hey scoob we need like a hundred more #milkshakes", test_save_to_file, @@ -3417,7 +3421,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "Getting kinda spooky around here", test_save_to_file, @@ -3431,7 +3435,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "And they would have gotten away with it too" + "if it wasn't for those pesky hackers", @@ -3446,7 +3450,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "man these centralized sites are like the worst!", test_save_to_file, @@ -3460,7 +3464,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "another mystery solved #test", test_save_to_file, @@ -3474,7 +3478,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(base_dir, nickname, domain, port, http_prefix, "let's go bowling", test_save_to_file, @@ -3488,7 +3492,7 @@ def _command_options() -> None: test_event_end_time, test_location, test_is_article, argb.language, conversation_id, low_bandwidth, argb.content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) domain_full = domain + ':' + str(port) clear_follows(base_dir, nickname, domain) follow_person(base_dir, nickname, domain, 'maxboardroom', domain_full, diff --git a/inbox.py b/inbox.py index b97373cce..66c1f36f0 100644 --- a/inbox.py +++ b/inbox.py @@ -3745,6 +3745,7 @@ def _bounce_dm(sender_post_id: str, session, http_prefix: str, location = None conversation_id = None low_bandwidth = False + buy_url = '' post_json_object = \ create_direct_message_post(base_dir, nickname, domain, port, http_prefix, content, @@ -3759,7 +3760,7 @@ def _bounce_dm(sender_post_id: str, session, http_prefix: str, low_bandwidth, content_license_url, languages_understood, bounce_is_chat, - translate) + translate, buy_url) if not post_json_object: print('WARN: unable to create bounce message to ' + sending_handle) return False diff --git a/newsdaemon.py b/newsdaemon.py index dedcddfba..f8c7b633d 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -636,6 +636,7 @@ def _convert_rss_to_activitypub(base_dir: str, http_prefix: str, city = 'London, England' conversation_id = None languages_understood = [system_language] + buy_url = '' blog = create_news_post(base_dir, domain, port, http_prefix, rss_description, @@ -645,7 +646,8 @@ def _convert_rss_to_activitypub(base_dir: str, http_prefix: str, rss_title, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, + buy_url) if not blog: continue diff --git a/posts.py b/posts.py index 21a8c0574..0ce3847dd 100644 --- a/posts.py +++ b/posts.py @@ -1092,6 +1092,27 @@ def _attach_post_license(post_json_object: {}, }) +def _attach_buy_link(post_json_object: {}, + buy_url: str, translate: {}) -> None: + """Attaches a link for buying something + """ + if not buy_url: + return + if '://' not in buy_url: + return + if ' ' in buy_url or '<' in buy_url: + return + buy_str = 'Buy' + if translate.get(buy_str): + buy_str = translate[buy_str] + post_json_object['attachment'].append({ + "type": "Link", + "name": buy_str, + "href": buy_url, + "mediaType": "text/html" + }) + + def _create_post_s2s(base_dir: str, nickname: str, domain: str, port: int, http_prefix: str, content: str, status_number: str, published: str, new_post_id: str, post_context: {}, @@ -1102,7 +1123,8 @@ def _create_post_s2s(base_dir: str, nickname: str, domain: str, port: int, post_object_type: str, summary: str, in_reply_to_atom_uri: str, system_language: str, conversation_id: str, low_bandwidth: bool, - content_license_url: str) -> {}: + content_license_url: str, buy_url: str, + translate: {}) -> {}: """Creates a new server-to-server post """ actor_url = local_actor_url(http_prefix, nickname, domain) @@ -1166,6 +1188,7 @@ def _create_post_s2s(base_dir: str, nickname: str, domain: str, port: int, media_type, image_description, city, low_bandwidth, content_license_url) _attach_post_license(new_post['object'], content_license_url) + _attach_buy_link(new_post['object'], buy_url, translate) return new_post @@ -1179,7 +1202,8 @@ def _create_post_c2s(base_dir: str, nickname: str, domain: str, port: int, post_object_type: str, summary: str, in_reply_to_atom_uri: str, system_language: str, conversation_id: str, low_bandwidth: str, - content_license_url: str) -> {}: + content_license_url: str, buy_url: str, + translate: {}) -> {}: """Creates a new client-to-server post """ domain_full = get_full_domain(domain, port) @@ -1233,6 +1257,7 @@ def _create_post_c2s(base_dir: str, nickname: str, domain: str, port: int, media_type, image_description, city, low_bandwidth, content_license_url) _attach_post_license(new_post, content_license_url) + _attach_buy_link(new_post, buy_url, translate) return new_post @@ -1433,7 +1458,8 @@ def _create_post_base(base_dir: str, system_language: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, - languages_understood: [], translate: {}) -> {}: + languages_understood: [], translate: {}, + buy_url: str) -> {}: """Creates a message """ content = remove_invalid_chars(content) @@ -1591,7 +1617,7 @@ def _create_post_base(base_dir: str, post_object_type, summary, in_reply_to_atom_uri, system_language, conversation_id, low_bandwidth, - content_license_url) + content_license_url, buy_url, translate) else: new_post = \ _create_post_c2s(base_dir, nickname, domain, port, @@ -1604,7 +1630,7 @@ def _create_post_base(base_dir: str, post_object_type, summary, in_reply_to_atom_uri, system_language, conversation_id, low_bandwidth, - content_license_url) + content_license_url, buy_url, translate) _create_post_mentions(cc_url, new_post, to_recipients, tags) @@ -1845,7 +1871,8 @@ def create_public_post(base_dir: str, location: str, is_article: bool, system_language: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, - languages_understood: [], translate: {}) -> {}: + languages_understood: [], translate: {}, + buy_url: str) -> {}: """Public post """ domain_full = get_full_domain(domain, port) @@ -1879,7 +1906,7 @@ def create_public_post(base_dir: str, event_status, ticket_url, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) def _append_citations_to_blog_post(base_dir: str, @@ -1924,7 +1951,8 @@ def create_blog_post(base_dir: str, location: str, system_language: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, - languages_understood: [], translate: {}) -> {}: + languages_understood: [], translate: {}, + buy_url: str) -> {}: blog_json = \ create_public_post(base_dir, nickname, domain, port, http_prefix, @@ -1937,7 +1965,7 @@ def create_blog_post(base_dir: str, event_date, event_time, event_end_time, location, True, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) blog_json['object']['url'] = \ blog_json['object']['url'].replace('/@', '/users/') _append_citations_to_blog_post(base_dir, nickname, domain, blog_json) @@ -1953,7 +1981,8 @@ def create_news_post(base_dir: str, subject: str, system_language: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, - languages_understood: [], translate: {}) -> {}: + languages_understood: [], translate: {}, + buy_url: str) -> {}: client_to_server = False in_reply_to = None in_reply_to_atom_uri = None @@ -1974,7 +2003,7 @@ def create_news_post(base_dir: str, event_date, event_time, event_end_time, location, True, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) blog['object']['type'] = 'Article' return blog @@ -1995,6 +2024,7 @@ def create_question_post(base_dir: str, """ domain_full = get_full_domain(domain, port) local_actor = local_actor_url(http_prefix, nickname, domain_full) + buy_url = '' message_json = \ _create_post_base(base_dir, nickname, domain, port, 'https://www.w3.org/ns/activitystreams#Public', @@ -2008,7 +2038,7 @@ def create_question_post(base_dir: str, None, None, None, None, None, None, None, None, system_language, None, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) message_json['object']['type'] = 'Question' message_json['object']['oneOf'] = [] message_json['object']['votersCount'] = 0 @@ -2043,7 +2073,8 @@ def create_unlisted_post(base_dir: str, location: str, system_language: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, - languages_understood: [], translate: {}) -> {}: + languages_understood: [], translate: {}, + buy_url: str) -> {}: """Unlisted post. This has the #Public and followers links inverted. """ domain_full = get_full_domain(domain, port) @@ -2063,7 +2094,7 @@ def create_unlisted_post(base_dir: str, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood, - translate) + translate, buy_url) def create_followers_only_post(base_dir: str, @@ -2082,7 +2113,7 @@ def create_followers_only_post(base_dir: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, languages_understood: [], - translate: {}) -> {}: + translate: {}, buy_url: str) -> {}: """Followers only post """ domain_full = get_full_domain(domain, port) @@ -2100,7 +2131,7 @@ def create_followers_only_post(base_dir: str, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood, - translate) + translate, buy_url) def get_mentioned_people(base_dir: str, http_prefix: str, @@ -2157,7 +2188,8 @@ def create_direct_message_post(base_dir: str, conversation_id: str, low_bandwidth: bool, content_license_url: str, languages_understood: [], - dm_is_chat: bool, translate: {}) -> {}: + dm_is_chat: bool, translate: {}, + buy_url: str) -> {}: """Direct Message post """ content = resolve_petnames(base_dir, nickname, domain, content) @@ -2183,7 +2215,7 @@ def create_direct_message_post(base_dir: str, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood, - translate) + translate, buy_url) # mentioned recipients go into To rather than Cc message_json['to'] = message_json['object']['cc'] message_json['object']['to'] = message_json['to'] @@ -2269,11 +2301,11 @@ def create_report_post(base_dir: str, post_to = moderators_list post_cc = None post_json_object = None + buy_url = '' for to_url in post_to: # who is this report going to? to_nickname = to_url.split('/users/')[1] handle = to_nickname + '@' + domain - post_json_object = \ _create_post_base(base_dir, nickname, domain, port, to_url, post_cc, @@ -2286,7 +2318,7 @@ def create_report_post(base_dir: str, None, None, None, None, None, None, None, None, system_language, None, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) if not post_json_object: continue @@ -2411,7 +2443,7 @@ def send_post(signing_priv_key_pem: str, project_version: str, shared_items_federated_domains: [], shared_item_federation_tokens: {}, low_bandwidth: bool, content_license_url: str, - translate: {}, + translate: {}, buy_url: str, debug: bool = False, in_reply_to: str = None, in_reply_to_atom_uri: str = None, subject: str = None) -> int: """Post to another inbox. Used by unit tests. @@ -2479,7 +2511,7 @@ def send_post(signing_priv_key_pem: str, project_version: str, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood, - translate) + translate, buy_url) # get the senders private key private_key_pem = get_person_key(nickname, domain, base_dir, 'private') @@ -2576,7 +2608,7 @@ def send_post_via_server(signing_priv_key_pem: str, project_version: str, low_bandwidth: bool, content_license_url: str, event_date: str, event_time: str, event_end_time: str, - location: str, translate: {}, + location: str, translate: {}, buy_url: str, debug: bool = False, in_reply_to: str = None, in_reply_to_atom_uri: str = None, @@ -2668,7 +2700,7 @@ def send_post_via_server(signing_priv_key_pem: str, project_version: str, None, None, None, None, None, system_language, conversation_id, low_bandwidth, content_license_url, languages_understood, - translate) + translate, buy_url) auth_header = create_basic_auth_header(from_nickname, password) diff --git a/tests.py b/tests.py index 994cfa108..40b2ef2c8 100644 --- a/tests.py +++ b/tests.py @@ -773,6 +773,7 @@ def create_server_alice(path: str, domain: str, port: int, conversation_id = None translate = {} content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0' + buy_url = '' create_public_post(path, nickname, domain, port, http_prefix, "No wise fish would go anywhere without a porpoise", test_save_to_file, @@ -787,7 +788,7 @@ def create_server_alice(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(path, nickname, domain, port, http_prefix, "Curiouser and curiouser!", test_save_to_file, @@ -802,7 +803,7 @@ def create_server_alice(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(path, nickname, domain, port, http_prefix, "In the gardens of memory, in the palace " + "of dreams, that is where you and I shall meet", @@ -818,7 +819,7 @@ def create_server_alice(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) regenerate_index_for_box(path, nickname, domain, 'outbox') global TEST_SERVER_ALICE_RUNNING TEST_SERVER_ALICE_RUNNING = True @@ -937,6 +938,7 @@ def create_server_bob(path: str, domain: str, port: int, conversation_id = None content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0' translate = {} + buy_url = '' create_public_post(path, nickname, domain, port, http_prefix, "It's your life, live it your way.", test_save_to_file, @@ -951,7 +953,7 @@ def create_server_bob(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(path, nickname, domain, port, http_prefix, "One of the things I've realised is that " + "I am very simple", @@ -967,7 +969,7 @@ def create_server_bob(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) create_public_post(path, nickname, domain, port, http_prefix, "Quantum physics is a bit of a passion of mine", test_save_to_file, @@ -982,7 +984,7 @@ def create_server_bob(path: str, domain: str, port: int, test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) regenerate_index_for_box(path, nickname, domain, 'outbox') global TEST_SERVER_BOB_RUNNING TEST_SERVER_BOB_RUNNING = True @@ -1321,6 +1323,7 @@ def test_post_message_between_servers(base_dir: str) -> None: low_bandwidth = False signing_priv_key_pem = None translate = {} + buy_url = '' send_result = \ send_post(signing_priv_key_pem, __version__, session_alice, alice_dir, 'alice', alice_domain, alice_port, @@ -1335,7 +1338,7 @@ def test_post_message_between_servers(base_dir: str) -> None: languages_understood, alice_shared_items_federated_domains, alice_shared_item_federation_tokens, low_bandwidth, - content_license_url, translate, + content_license_url, translate, buy_url, in_reply_to, in_reply_to_atom_uri, subject) print('send_result: ' + str(send_result)) @@ -1402,6 +1405,8 @@ def test_post_message_between_servers(base_dir: str) -> None: assert 'यह एक परीक्षण है' in received_json['object']['content'] print('Check that message received from Alice contains an attachment') assert received_json['object']['attachment'] + if len(received_json['object']['attachment']) != 2: + pprint(received_json['object']['attachment']) assert len(received_json['object']['attachment']) == 2 attached = received_json['object']['attachment'][0] pprint(attached) @@ -1693,6 +1698,7 @@ def test_follow_between_servers(base_dir: str) -> None: low_bandwidth = False signing_priv_key_pem = None translate = {} + buy_url = '' send_result = \ send_post(signing_priv_key_pem, __version__, session_alice, alice_dir, 'alice', alice_domain, alice_port, @@ -1705,7 +1711,7 @@ def test_follow_between_servers(base_dir: str) -> None: languages_understood, alice_shared_items_federated_domains, alice_shared_item_federation_tokens, low_bandwidth, - content_license_url, translate, + content_license_url, translate, buy_url, in_reply_to, in_reply_to_atom_uri, subject) print('send_result: ' + str(send_result)) @@ -2060,6 +2066,7 @@ def test_shared_items_federation(base_dir: str) -> None: low_bandwidth = False signing_priv_key_pem = None translate = {} + buy_url = '' send_result = \ send_post(signing_priv_key_pem, __version__, session_alice, alice_dir, 'alice', alice_domain, alice_port, @@ -2072,7 +2079,7 @@ def test_shared_items_federation(base_dir: str) -> None: languages_understood, alice_shared_items_federated_domains, alice_shared_item_federation_tokens, low_bandwidth, - content_license_url, translate, True, + content_license_url, translate, buy_url, True, in_reply_to, in_reply_to_atom_uri, subject) print('send_result: ' + str(send_result)) @@ -2490,6 +2497,7 @@ def test_group_follow(base_dir: str) -> None: if os.path.isfile(os.path.join(outbox_path, name))]) translate = {} + buy_url = '' send_result = \ send_post(signing_priv_key_pem, __version__, session_alice, alice_dir, 'alice', alice_domain, alice_port, @@ -2502,7 +2510,7 @@ def test_group_follow(base_dir: str) -> None: languages_understood, alice_shared_items_federated_domains, alice_shared_item_federation_tokens, low_bandwidth, - content_license_url, translate, + content_license_url, translate, buy_url, in_reply_to, in_reply_to_atom_uri, subject) print('send_result: ' + str(send_result)) @@ -2877,6 +2885,7 @@ def _test_create_person_account(base_dir: str): "(yawn)\n\n...then it's not really independent.\n\n" + \ "Politicians will threaten to withdraw funding if you do " + \ "anything which challenges middle class sensibilities or incomes." + buy_url = '' test_post_json = \ create_public_post(base_dir, nickname, domain, port, http_prefix, content, save_to_file, @@ -2889,7 +2898,7 @@ def _test_create_person_account(base_dir: str): test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) assert test_post_json assert test_post_json.get('object') assert test_post_json['object']['content'] @@ -2915,7 +2924,7 @@ def _test_create_person_account(base_dir: str): test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) assert test_post_json assert test_post_json.get('object') assert test_post_json['object']['content'] @@ -3116,6 +3125,7 @@ def test_client_to_server(base_dir: str): event_end_time = '12:30' location = "Kinshasa" translate = {} + buy_url = '' send_result = \ send_post_via_server(signing_priv_key_pem, __version__, alice_dir, session_alice, 'alice', password, @@ -3128,7 +3138,7 @@ def test_client_to_server(base_dir: str): system_language, languages_understood, low_bandwidth, content_license_url, event_date, event_time, event_end_time, location, - translate, True, None, None, + translate, buy_url, True, None, None, conversation_id, None) print('send_result: ' + str(send_result)) @@ -4728,6 +4738,7 @@ def _test_reply_to_public_post(base_dir: str) -> None: low_bandwidth = True content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0' translate = {} + buy_url = '' reply = \ create_public_post(base_dir, nickname, domain, port, http_prefix, content, save_to_file, @@ -4740,7 +4751,7 @@ def _test_reply_to_public_post(base_dir: str) -> None: test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) # print(str(reply)) expected_str = \ '

' + \ @@ -5678,6 +5689,7 @@ def _test_links_within_post(base_dir: str) -> None: low_bandwidth = True content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0' translate = {} + buy_url = '' post_json_object = \ create_public_post(base_dir, nickname, domain, port, http_prefix, @@ -5691,7 +5703,7 @@ def _test_links_within_post(base_dir: str) -> None: test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) expected_str = \ '

This is a test post with links.

' + \ @@ -5735,7 +5747,7 @@ def _test_links_within_post(base_dir: str) -> None: test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) assert post_json_object['object']['content'] == content assert post_json_object['object']['contentMap'][system_language] == content @@ -6787,6 +6799,7 @@ def _test_can_replyto(base_dir: str) -> None: low_bandwidth = True content_license_url = 'https://creativecommons.org/licenses/by-nc/4.0' translate = {} + buy_url = '' post_json_object = \ create_public_post(base_dir, nickname, domain, port, http_prefix, @@ -6800,7 +6813,7 @@ def _test_can_replyto(base_dir: str) -> None: test_event_end_time, test_location, test_is_article, system_language, conversation_id, low_bandwidth, content_license_url, - languages_understood, translate) + languages_understood, translate, buy_url) # set the date on the post curr_date_str = "2021-09-08T20:45:00Z" post_json_object['published'] = curr_date_str diff --git a/webapp_create_post.py b/webapp_create_post.py index e6a747ecd..bcb559003 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -776,7 +776,7 @@ def html_new_post(edit_post_params: {}, translate['Language used'] + '\n' replies_section += languages_dropdown buy_link_str = '🛒 ' + translate['Buy link'] - replies_section += edit_text_field(buy_link_str, 'buySite', + replies_section += edit_text_field(buy_link_str, 'buyUrl', default_buy_site, 'https://...') replies_section += '\n' diff --git a/webapp_utils.py b/webapp_utils.py index 9c48949c4..17c000fc1 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2091,9 +2091,10 @@ def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: return {} links = {} buy_strings = [] - buy_strings += translate['Buy'].lower() - buy_strings += translate['Purchase'].lower() - buy_strings += translate['Subscribe'].lower() + for buy_str in ('Buy', 'Purchase', 'Subscribe'): + if translate.get(buy_str): + buy_str = translate[buy_str] + buy_strings += buy_str.lower() for item in post_json_object['object']['tag']: if not isinstance(item, dict): continue From 8126f138c59e0ae6939fca5d50776ce0dc855047 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 21:15:02 +0000 Subject: [PATCH 07/12] Specify permitted sites to buy from --- daemon.py | 37 ++++++++++++++++++++++++++++++++++++- translations/ar.json | 3 ++- translations/bn.json | 3 ++- translations/ca.json | 3 ++- translations/cy.json | 3 ++- translations/de.json | 3 ++- translations/el.json | 3 ++- translations/en.json | 3 ++- translations/es.json | 3 ++- translations/fa.json | 3 ++- translations/fr.json | 3 ++- translations/ga.json | 3 ++- translations/hi.json | 3 ++- translations/it.json | 3 ++- translations/ja.json | 3 ++- translations/ko.json | 3 ++- translations/ku.json | 3 ++- translations/nl.json | 3 ++- translations/oc.json | 3 ++- translations/pl.json | 3 ++- translations/pt.json | 3 ++- translations/ru.json | 3 ++- translations/sw.json | 3 ++- translations/tr.json | 3 ++- translations/uk.json | 3 ++- translations/yi.json | 3 ++- translations/zh.json | 3 ++- webapp_profile.py | 22 +++++++++++++++++++--- webapp_utils.py | 2 +- 29 files changed, 108 insertions(+), 31 deletions(-) diff --git a/daemon.py b/daemon.py index 535861cdf..7a9ea8ab6 100644 --- a/daemon.py +++ b/daemon.py @@ -7882,6 +7882,40 @@ class PubServer(BaseHTTPRequestHandler): set_config_param(base_dir, 'crawlersAllowed', crawlers_allowed_str) + # save allowed buy domains + buy_sites = {} + if fields.get('buySitesStr'): + buy_sites_str = \ + fields['buySitesStr'] + buy_sites_list = \ + buy_sites_str.split('\n') + for site_url in buy_sites_list: + if ' ' in site_url: + site_url = site_url.split(' ')[-1] + buy_icon_text = \ + site_url.replace(site_url, '').strip() + if not buy_icon_text: + buy_icon_text = site_url + else: + buy_icon_text = site_url + if buy_sites.get(buy_icon_text): + continue + buy_sites[buy_icon_text] = site_url.strip() + if str(self.server.buy_sites) != \ + str(buy_sites): + self.server.buy_sites = buy_sites + buy_sites_filename = \ + base_dir + '/accounts/buy_sites.json' + if buy_sites: + save_json(buy_sites, buy_sites_filename) + else: + if os.path.isfile(buy_sites_filename): + try: + os.remove(buy_sites_filename) + except OSError: + print('EX: unable to delete ' + + buy_sites_filename) + # save peertube instances list peertube_instances_file = \ base_dir + '/accounts/peertube.txt' @@ -16398,7 +16432,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.system_language, self.server.min_images_for_accounts, self.server.max_recent_posts, - self.server.reverse_sequence) + self.server.reverse_sequence, + self.server.buy_sites) if msg: msg = msg.encode('utf-8') msglen = len(msg) diff --git a/translations/ar.json b/translations/ar.json index a905add84..d0e871237 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -611,5 +611,6 @@ "Edit reminder": "تحرير التذكير", "Purchase": "شراء", "Subscribe": "الإشتراك", - "Buy link": "رابط شراء" + "Buy link": "رابط شراء", + "Buy links are allowed from the following domains": "روابط الشراء مسموح بها من المجالات التالية" } diff --git a/translations/bn.json b/translations/bn.json index 6f8c9a2fa..75629536b 100644 --- a/translations/bn.json +++ b/translations/bn.json @@ -611,5 +611,6 @@ "Edit reminder": "অনুস্মারক সম্পাদনা করুন", "Purchase": "ক্রয়", "Subscribe": "সাবস্ক্রাইব", - "Buy link": "সংযোগ কেনা" + "Buy link": "সংযোগ কেনা", + "Buy links are allowed from the following domains": "নিম্নলিখিত ডোমেনগুলি থেকে লিঙ্কগুলি কেনার অনুমতি দেওয়া হয়েছে" } diff --git a/translations/ca.json b/translations/ca.json index 7d2a1a673..85b5bb3f1 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -611,5 +611,6 @@ "Edit reminder": "Edita el recordatori", "Purchase": "Compra", "Subscribe": "Subscriu-te", - "Buy link": "Enllaç de compra" + "Buy link": "Enllaç de compra", + "Buy links are allowed from the following domains": "Els enllaços de compra es permeten des dels dominis següents" } diff --git a/translations/cy.json b/translations/cy.json index 9b1c34612..56dcee7d0 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -611,5 +611,6 @@ "Edit reminder": "Golygu nodyn atgoffa", "Purchase": "Prynu", "Subscribe": "Tanysgrifio", - "Buy link": "Prynu dolen" + "Buy link": "Prynu dolen", + "Buy links are allowed from the following domains": "Caniateir dolenni prynu o'r parthau canlynol" } diff --git a/translations/de.json b/translations/de.json index e9381fe8f..8126e5514 100644 --- a/translations/de.json +++ b/translations/de.json @@ -611,5 +611,6 @@ "Edit reminder": "Erinnerung bearbeiten", "Purchase": "Kaufen", "Subscribe": "Abonnieren", - "Buy link": "Link kaufen" + "Buy link": "Link kaufen", + "Buy links are allowed from the following domains": "Kauflinks sind von den folgenden Domains erlaubt" } diff --git a/translations/el.json b/translations/el.json index f52794045..700da97fa 100644 --- a/translations/el.json +++ b/translations/el.json @@ -611,5 +611,6 @@ "Edit reminder": "Επεξεργασία υπενθύμισης", "Purchase": "Αγορά", "Subscribe": "Εγγραφείτε", - "Buy link": "Σύνδεσμος αγοράς" + "Buy link": "Σύνδεσμος αγοράς", + "Buy links are allowed from the following domains": "Οι σύνδεσμοι αγοράς επιτρέπονται από τους παρακάτω τομείς" } diff --git a/translations/en.json b/translations/en.json index b609436cc..4cfd16b63 100644 --- a/translations/en.json +++ b/translations/en.json @@ -611,5 +611,6 @@ "Edit reminder": "Edit reminder", "Purchase": "Purchase", "Subscribe": "Subscribe", - "Buy link": "Buy link" + "Buy link": "Buy link", + "Buy links are allowed from the following domains": "Buy links are allowed from the following domains" } diff --git a/translations/es.json b/translations/es.json index ce6b671ed..484fc52a8 100644 --- a/translations/es.json +++ b/translations/es.json @@ -611,5 +611,6 @@ "Edit reminder": "Editar recordatorio", "Purchase": "Compra", "Subscribe": "Suscribir", - "Buy link": "Enlace de compra" + "Buy link": "Enlace de compra", + "Buy links are allowed from the following domains": "Se permiten enlaces de compra de los siguientes dominios" } diff --git a/translations/fa.json b/translations/fa.json index 36b442958..61755dfbe 100644 --- a/translations/fa.json +++ b/translations/fa.json @@ -611,5 +611,6 @@ "Edit reminder": "ویرایش یادآوری", "Purchase": "خرید", "Subscribe": "اشتراک در", - "Buy link": "لینک خرید" + "Buy link": "لینک خرید", + "Buy links are allowed from the following domains": "لینک خرید از دامنه های زیر مجاز است" } diff --git a/translations/fr.json b/translations/fr.json index dff468ab6..cb8020e82 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -611,5 +611,6 @@ "Edit reminder": "Modifier le rappel", "Purchase": "Acheter", "Subscribe": "S'abonner", - "Buy link": "Acheter un lien" + "Buy link": "Acheter un lien", + "Buy links are allowed from the following domains": "Les liens d'achat sont autorisés à partir des domaines suivants" } diff --git a/translations/ga.json b/translations/ga.json index adde1a810..9416cc13b 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -611,5 +611,6 @@ "Edit reminder": "Cuir meabhrúchán in eagar", "Purchase": "Ceannach", "Subscribe": "Liostáil", - "Buy link": "Ceannaigh nasc" + "Buy link": "Ceannaigh nasc", + "Buy links are allowed from the following domains": "Ceadaítear naisc cheannaigh ó na fearainn seo a leanas" } diff --git a/translations/hi.json b/translations/hi.json index 4f0b54782..3f1c5fd6e 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -611,5 +611,6 @@ "Edit reminder": "रिमाइंडर संपादित करें", "Purchase": "खरीदना", "Subscribe": "सदस्यता लेने के", - "Buy link": "लिंक खरीदें" + "Buy link": "लिंक खरीदें", + "Buy links are allowed from the following domains": "निम्नलिखित डोमेन से खरीदें लिंक की अनुमति है" } diff --git a/translations/it.json b/translations/it.json index 94b851b07..f9bd2df67 100644 --- a/translations/it.json +++ b/translations/it.json @@ -611,5 +611,6 @@ "Edit reminder": "Modifica promemoria", "Purchase": "Acquistare", "Subscribe": "Sottoscrivi", - "Buy link": "Link per l'acquisto" + "Buy link": "Link per l'acquisto", + "Buy links are allowed from the following domains": "I link di acquisto sono consentiti dai seguenti domini" } diff --git a/translations/ja.json b/translations/ja.json index 9dfa8a408..f274f6bcf 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -611,5 +611,6 @@ "Edit reminder": "リマインダーを編集", "Purchase": "購入", "Subscribe": "申し込む", - "Buy link": "購入リンク" + "Buy link": "購入リンク", + "Buy links are allowed from the following domains": "次のドメインからの購入リンクが許可されています" } diff --git a/translations/ko.json b/translations/ko.json index 77ba773c2..71e437519 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -611,5 +611,6 @@ "Edit reminder": "알림 수정", "Purchase": "구입", "Subscribe": "구독하다", - "Buy link": "구매 링크" + "Buy link": "구매 링크", + "Buy links are allowed from the following domains": "다음 도메인에서 구매 링크가 허용됩니다." } diff --git a/translations/ku.json b/translations/ku.json index ac8e07cd7..29a310e21 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -611,5 +611,6 @@ "Edit reminder": "Bîranîna biguherîne", "Purchase": "Kirrîn", "Subscribe": "Subscribe", - "Buy link": "Girêdanê bikirin" + "Buy link": "Girêdanê bikirin", + "Buy links are allowed from the following domains": "Zencîreyên kirînê ji domên jêrîn têne destûr kirin" } diff --git a/translations/nl.json b/translations/nl.json index 61f2141a2..d8eb2d268 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -611,5 +611,6 @@ "Edit reminder": "Herinnering bewerken", "Purchase": "Aankoop", "Subscribe": "Abonneren", - "Buy link": "koop link" + "Buy link": "koop link", + "Buy links are allowed from the following domains": "Kooplinks zijn toegestaan vanaf de volgende domeinen" } diff --git a/translations/oc.json b/translations/oc.json index 3b7eeb875..bad9e02dd 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -607,5 +607,6 @@ "Edit reminder": "Edit reminder", "Purchase": "Purchase", "Subscribe": "Subscribe", - "Buy link": "Buy link" + "Buy link": "Buy link", + "Buy links are allowed from the following domains": "Buy links are allowed from the following domains" } diff --git a/translations/pl.json b/translations/pl.json index aac582b80..b85fe67e9 100644 --- a/translations/pl.json +++ b/translations/pl.json @@ -611,5 +611,6 @@ "Edit reminder": "Edytuj przypomnienie", "Purchase": "Zakup", "Subscribe": "Subskrybuj", - "Buy link": "Kup Link" + "Buy link": "Kup Link", + "Buy links are allowed from the following domains": "Kupuj linki są dozwolone z następujących domen" } diff --git a/translations/pt.json b/translations/pt.json index c597c1e21..9df0cdff5 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -611,5 +611,6 @@ "Edit reminder": "Editar lembrete", "Purchase": "Comprar", "Subscribe": "Se inscrever", - "Buy link": "Link de compra" + "Buy link": "Link de compra", + "Buy links are allowed from the following domains": "Links de compra são permitidos nos seguintes domínios" } diff --git a/translations/ru.json b/translations/ru.json index 8254236b3..bd3f0ce13 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -611,5 +611,6 @@ "Edit reminder": "Изменить напоминание", "Purchase": "Покупка", "Subscribe": "Подписаться", - "Buy link": "Купить ссылку" + "Buy link": "Купить ссылку", + "Buy links are allowed from the following domains": "Ссылки на покупку разрешены со следующих доменов" } diff --git a/translations/sw.json b/translations/sw.json index 40f4e4097..7bbf34f89 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -611,5 +611,6 @@ "Edit reminder": "Badilisha kikumbusho", "Purchase": "Nunua", "Subscribe": "Jisajili", - "Buy link": "Nunua kiungo" + "Buy link": "Nunua kiungo", + "Buy links are allowed from the following domains": "Viungo vya kununua vinaruhusiwa kutoka kwa vikoa vifuatavyo" } diff --git a/translations/tr.json b/translations/tr.json index aba9288a9..1606176b8 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -611,5 +611,6 @@ "Edit reminder": "Hatırlatıcıyı düzenle", "Purchase": "Satın alma", "Subscribe": "Abone", - "Buy link": "Bağlantı satın al" + "Buy link": "Bağlantı satın al", + "Buy links are allowed from the following domains": "Aşağıdaki alanlardan satın alma bağlantılarına izin verilir" } diff --git a/translations/uk.json b/translations/uk.json index 030935be9..590a799a1 100644 --- a/translations/uk.json +++ b/translations/uk.json @@ -611,5 +611,6 @@ "Edit reminder": "Редагувати нагадування", "Purchase": "Купівля", "Subscribe": "Підпишіться", - "Buy link": "Купити посилання" + "Buy link": "Купити посилання", + "Buy links are allowed from the following domains": "Посилання на купівлю дозволено з таких доменів" } diff --git a/translations/yi.json b/translations/yi.json index 343c45346..c3d62bf14 100644 --- a/translations/yi.json +++ b/translations/yi.json @@ -611,5 +611,6 @@ "Edit reminder": "רעדאַגירן דערמאָנונג", "Purchase": "קויפן", "Subscribe": "אַבאָנירן", - "Buy link": "קויפן לינק" + "Buy link": "קויפן לינק", + "Buy links are allowed from the following domains": "קויפן פֿאַרבינדונגען זענען ערלויבט פֿון די פאלגענדע דאָומיינז" } diff --git a/translations/zh.json b/translations/zh.json index 742429670..d7c5fb673 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -611,5 +611,6 @@ "Edit reminder": "编辑提醒", "Purchase": "购买", "Subscribe": "订阅", - "Buy link": "购买链接" + "Buy link": "购买链接", + "Buy links are allowed from the following domains": "允许来自以下域的购买链接" } diff --git a/webapp_profile.py b/webapp_profile.py index fd36d423a..3d201f4e0 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -1884,7 +1884,8 @@ def _html_edit_profile_filtering(base_dir: str, nickname: str, domain: str, user_agents_blocked: str, crawlers_allowed: str, translate: {}, reply_interval_hours: int, - cw_lists: {}, lists_enabled: str) -> str: + cw_lists: {}, lists_enabled: str, + buy_sites: {}) -> str: """Filtering and blocking section of edit profile screen """ filter_str = '' @@ -2071,6 +2072,20 @@ def _html_edit_profile_filtering(base_dir: str, nickname: str, domain: str, 'crawlersAllowedStr', crawlers_allowed_str, 200, '', False) + buy_domains_list_str = '' + for buy_icon_text, buy_url in buy_sites.items(): + if buy_icon_text != buy_url: + buy_domains_list_str += \ + buy_icon_text + ' ' + buy_url.strip() + '\n' + else: + buy_domains_list_str += buy_url.strip() + '\n' + buy_domains_str = \ + "Buy links are allowed from the following domains" + edit_profile_form += \ + edit_text_area(translate[buy_domains_str], None, + 'buySitesStr', buy_domains_list_str, + 200, '', False) + cw_lists_str = '' for name, _ in cw_lists.items(): variablename = get_cw_list_variable(name) @@ -2513,7 +2528,8 @@ def html_edit_profile(server, translate: {}, system_language: str, min_images_for_accounts: [], max_recent_posts: int, - reverse_sequence: []) -> str: + reverse_sequence: [], + buy_sites: {}) -> str: """Shows the edit profile screen """ path = path.replace('/inbox', '').replace('/outbox', '') @@ -2754,7 +2770,7 @@ def html_edit_profile(server, translate: {}, _html_edit_profile_filtering(base_dir, nickname, domain, user_agents_blocked, crawlers_allowed, translate, reply_interval_hours, - cw_lists, lists_enabled) + cw_lists, lists_enabled, buy_sites) # git projects section edit_profile_form += \ diff --git a/webapp_utils.py b/webapp_utils.py index 17c000fc1..db99b2321 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2148,7 +2148,7 @@ def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: def load_buy_sites(base_dir: str) -> {}: """Loads domains from which buying is permitted """ - buy_sites_filename = base_dir + '/accounts/buy_sites.txt' + buy_sites_filename = base_dir + '/accounts/buy_sites.json' if os.path.isfile(buy_sites_filename): buy_sites_json = load_json(buy_sites_filename) if buy_sites_json: From 79a73f9c70c704b961866564a9a7bdb3295209ff Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 21:17:12 +0000 Subject: [PATCH 08/12] Minimal checks on site urls --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index 7a9ea8ab6..4356a52ab 100644 --- a/daemon.py +++ b/daemon.py @@ -7900,6 +7900,8 @@ class PubServer(BaseHTTPRequestHandler): buy_icon_text = site_url if buy_sites.get(buy_icon_text): continue + if '<' in site_url: + continue buy_sites[buy_icon_text] = site_url.strip() if str(self.server.buy_sites) != \ str(buy_sites): From a9491491e707d80156481bc1a3d35e67c4f81cfa Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 21:22:38 +0000 Subject: [PATCH 09/12] Check for no value --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index 4356a52ab..8994e1d35 100644 --- a/daemon.py +++ b/daemon.py @@ -7902,6 +7902,8 @@ class PubServer(BaseHTTPRequestHandler): continue if '<' in site_url: continue + if not site_url.strip(): + continue buy_sites[buy_icon_text] = site_url.strip() if str(self.server.buy_sites) != \ str(buy_sites): From dd299c636e8b1bcd77964deed08f71debaf6ae01 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 21:43:14 +0000 Subject: [PATCH 10/12] Attachment rather than tag --- posts.py | 2 ++ webapp_post.py | 9 +++++---- webapp_utils.py | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/posts.py b/posts.py index 0ce3847dd..aa4052406 100644 --- a/posts.py +++ b/posts.py @@ -1105,6 +1105,8 @@ def _attach_buy_link(post_json_object: {}, buy_str = 'Buy' if translate.get(buy_str): buy_str = translate[buy_str] + if 'attachment' not in post_json_object: + post_json_object['attachment'] = [] post_json_object['attachment'].append({ "type": "Link", "name": buy_str, diff --git a/webapp_post.py b/webapp_post.py index 98d172b7b..04d243a08 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -2508,10 +2508,11 @@ def individual_post_as_html(signing_priv_key_pem: str, # html for the buy icon buy_str = '' - if post_json_object['object'].get('tag'): - if not is_patch: - buy_links = get_buy_links(post_json_object, translate, buy_sites) - buy_str = _get_buy_footer(buy_links, translate) + if 'attachment' not in post_json_object['object']: + post_json_object['object']['attachment'] = [] + if not is_patch: + buy_links = get_buy_links(post_json_object, translate, buy_sites) + buy_str = _get_buy_footer(buy_links, translate) new_footer_str = \ _get_footer_with_icons(show_icons, diff --git a/webapp_utils.py b/webapp_utils.py index db99b2321..53d635d9b 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -2085,9 +2085,9 @@ def html_following_dropdown(base_dir: str, nickname: str, def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: """Returns any links to buy something from an external site """ - if not post_json_object['object'].get('tag'): + if not post_json_object['object'].get('attachment'): return {} - if not isinstance(post_json_object['object']['tag'], list): + if not isinstance(post_json_object['object']['attachment'], list): return {} links = {} buy_strings = [] @@ -2095,7 +2095,7 @@ def get_buy_links(post_json_object: str, translate: {}, buy_sites: {}) -> {}: if translate.get(buy_str): buy_str = translate[buy_str] buy_strings += buy_str.lower() - for item in post_json_object['object']['tag']: + for item in post_json_object['object']['attachment']: if not isinstance(item, dict): continue if not item.get('name'): From 43cc11806b19d4ff0e4ca5a6f47e098a2ee081f4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 13 Jan 2023 21:52:22 +0000 Subject: [PATCH 11/12] Icon title is buy --- webapp_post.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webapp_post.py b/webapp_post.py index 04d243a08..998aa05ac 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -1844,13 +1844,9 @@ def _get_buy_footer(buy_links: {}, translate: {}) -> str: if not buy_links: return '' icon_filename = 'buy.png' - description = '' + description = translate['Buy'] buy_str = '' for buy_title, buy_url in buy_links.items(): - if buy_title: - description = buy_title - else: - description = translate['Buy'] buy_str = \ ' ' + \ 'Media timeline

  • Moderation
  • Themes
  • +
  • Buying and selling
  • Sharing economy
  • Search
  • Browsing in a command @@ -65,7 +66,11 @@ hardware. Think many small communicating nodes rather than a small number of large servers. Also, in spite of the prevailing great obsession with scale, not everything needs to. You can federate with a small number of servers for a particular purpose - such as running a -club or hackspace - and that’s ok.

    +club or hackspace - and that’s ok. It supports both the server-to-server +(S2S) and client-to-server (C2S) versions of the ActivityPub protocol, +with
    basic +auth for C2S authentication.

    Anti-virality is a common design approach in the fediverse, and Epicyon also follows @@ -429,7 +434,7 @@ preferable, so that it matches your typical pattern of daily posting activity without giving away your real location.

    Verifying your website or blog

    -

    It is possible to indicate that a website of blog belongs to you by +

    It is possible to indicate that a website or blog belongs to you by linking it to your profile screen. Within the head html section of your website or blog index page include a line similar to:

     

    Selecting the location header will open the last known geolocation, so if your current location is near this makes it quicker to find.

    +

    Scientific references

    +

    It is possible to have references to scientific papers linked +automatically, such that they are readable with one click/press. +Supported references are arXiv and Digital +object identifier (DOI). For example:

    +
    This is a reference to a paper: arxiv:2203.15752

    The Timeline

    Layout

    @@ -902,6 +914,18 @@ you to change colors and values for user interface components.

    Theme designer screen
    +

    Buying and selling

    +

    When creating a new post you have the option of specifying a buy +link This is a link to a web page where you can buy some particular +item. When someone receives the post if they have a compatible instance +then a small shopping cart icon will appear at the bottom of the post +along with the other icons. Clicking or pressing the shopping cart will +then take you to the buying site. It’s a predictable and machine +parsable way indicating that something is for sale, separate from the +post content.

    +

    To avoid spam, it is possible for the shopping icon to only appear if +it links to one of an allowed list of seller domains. In this way you +can be confident that you are only navigating to approved sites.

    Sharing economy

    This is intended to add Freecycle diff --git a/manual/manual.md b/manual/manual.md index 2a37d6a89..ac6b594ec 100644 --- a/manual/manual.md +++ b/manual/manual.md @@ -15,10 +15,11 @@ 14. [Media timeline](#media-timeline) 15. [Moderation](#moderation) 16. [Themes](#themes) -17. [Sharing economy](#sharing-economy) -18. [Search](#search) -19. [Browsing in a command shell](#browsing-in-a-command-shell) -20. [Building fediverse communities](#building-fediverse-communities) +17. [Buying and selling](#buying-and-selling) +18. [Sharing economy](#sharing-economy) +19. [Search](#search) +20. [Browsing in a command shell](#browsing-in-a-command-shell) +21. [Building fediverse communities](#building-fediverse-communities) # Introduction *"Every new beginning comes from some other beginning’s end."* @@ -655,6 +656,11 @@ If you have the *artist* role then from the top of the left column of the main t ![Theme designer screen](manual-theme-designer.png) +# Buying and selling +When creating a new post you have the option of specifying a *buy link* This is a link to a web page where you can buy some particular item. When someone receives the post if they have a compatible instance then a small shopping cart icon will appear at the bottom of the post along with the other icons. Clicking or pressing the shopping cart will then take you to the buying site. It's a predictable and machine parsable way indicating that something is for sale, separate from the post content. + +To avoid spam, it is possible for the shopping icon to only appear if it links to one of an allowed list of seller domains. In this way you can be confident that you are only navigating to approved sites. + # Sharing economy This is intended to add [Freecycle](https://en.wikipedia.org/wiki/The_Freecycle_Network) type functionality within a social network context, leveraging your social connections on the instance, or between participating instances, to facilitate sharing and reduce wasteful consumerism.