diff --git a/content.py b/content.py index e5b1af646..5d0a54a6b 100644 --- a/content.py +++ b/content.py @@ -1008,6 +1008,147 @@ def _auto_tag(base_dir: str, nickname: str, domain: str, append_tags.append('#' + tag_name) +def _get_simplified_content(content: str) -> str: + """Returns a simplified version of the content suitable for + splitting up into individual words + """ + content_simplified = \ + content.replace(',', ' ').replace(';', ' ').replace('- ', ' ') + content_simplified = content_simplified.replace('. ', ' ').strip() + if content_simplified.endswith('.'): + content_simplified = content_simplified[:len(content_simplified)-1] + return content_simplified + + +def detect_dogwhistles(content: str, dogwhistles: {}) -> {}: + """Returns a dict containing any detected dogwhistle words + """ + content = remove_html(content).lower() + result = {} + words = _get_simplified_content(content).split(' ') + for whistle, category in dogwhistles.items(): + if not category: + continue + ending = False + starting = False + whistle = whistle.lower() + + if whistle.startswith('x-'): + whistle = whistle[2:] + ending = True + elif (whistle.startswith('*') or + whistle.startswith('~') or + whistle.startswith('-')): + whistle = whistle[1:] + ending = True + + if ending: + prev_wrd = '' + for wrd in words: + wrd2 = (prev_wrd + ' ' + wrd).strip() + if wrd.endswith(whistle) or wrd2.endswith(whistle): + if not result.get(whistle): + result[whistle] = { + "count": 1, + "category": category + } + else: + result[whistle]['count'] += 1 + prev_wrd = wrd + continue + + if whistle.lower().endswith('-x'): + whistle = whistle[:len(whistle)-2] + starting = True + elif (whistle.endswith('*') or + whistle.endswith('~') or + whistle.endswith('-')): + whistle = whistle[:len(whistle)-1] + starting = True + + if starting: + prev_wrd = '' + for wrd in words: + wrd2 = (prev_wrd + ' ' + wrd).strip() + if wrd.startswith(whistle) or wrd2.startswith(whistle): + if not result.get(whistle): + result[whistle] = { + "count": 1, + "category": category + } + else: + result[whistle]['count'] += 1 + prev_wrd = wrd + continue + + if '*' in whistle: + whistle_start = whistle.split('*', 1)[0] + whistle_end = whistle.split('*', 1)[1] + prev_wrd = '' + for wrd in words: + wrd2 = (prev_wrd + ' ' + wrd).strip() + if ((wrd.startswith(whistle_start) and + wrd.endswith(whistle_end)) or + (wrd2.startswith(whistle_start) and + wrd2.endswith(whistle_end))): + if not result.get(whistle): + result[whistle] = { + "count": 1, + "category": category + } + else: + result[whistle]['count'] += 1 + prev_wrd = wrd + continue + + prev_wrd = '' + for wrd in words: + wrd2 = (prev_wrd + ' ' + wrd).strip() + if whistle in (wrd, wrd2): + if not result.get(whistle): + result[whistle] = { + "count": 1, + "category": category + } + else: + result[whistle]['count'] += 1 + prev_wrd = wrd + return result + + +def load_dogwhistles(filename: str) -> {}: + """Loads a list of dogwhistles from file + """ + if not os.path.isfile(filename): + return {} + dogwhistle_lines = [] + try: + with open(filename, 'r', encoding='utf-8') as fp_dogwhistles: + dogwhistle_lines = fp_dogwhistles.readlines() + except OSError: + print('EX: unable to load dogwhistles from ' + filename) + return {} + separators = ('->', '=>', ',', ';', '|', '=') + dogwhistles = {} + for line in dogwhistle_lines: + line = remove_eol(line).strip() + if not line: + continue + if line.startswith('#'): + continue + whistle = None + category = None + for sep in separators: + if sep in line: + whistle = line.split(sep, 1)[0].strip() + category = line.split(sep, 1)[1].strip() + break + if not whistle: + whistle = line + dogwhistles[whistle] = category + return dogwhistles + + def add_html_tags(base_dir: str, http_prefix: str, nickname: str, domain: str, content: str, recipients: [], hashtags: {}, @@ -1022,12 +1163,7 @@ def add_html_tags(base_dir: str, http_prefix: str, content = content.replace('\r', '') content = content.replace('\n', ' --linebreak-- ') content = _add_music_tag(content, 'nowplaying') - content_simplified = \ - content.replace(',', ' ').replace(';', ' ').replace('- ', ' ') - content_simplified = content_simplified.replace('. ', ' ').strip() - if content_simplified.endswith('.'): - content_simplified = content_simplified[:len(content_simplified)-1] - words = content_simplified.split(' ') + words = _get_simplified_content(content).split(' ') # remove . for words which are not mentions new_words = [] diff --git a/daemon.py b/daemon.py index df2daa8ef..7f60035aa 100644 --- a/daemon.py +++ b/daemon.py @@ -326,6 +326,7 @@ from utils import has_group_type from manualapprove import manual_deny_follow_request_thread from manualapprove import manual_approve_follow_request_thread from announce import create_announce +from content import load_dogwhistles from content import valid_url_lengths from content import contains_invalid_local_links from content import get_price_from_string @@ -1595,7 +1596,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.max_recent_posts, self.server.cw_lists, self.server.lists_enabled, - self.server.content_license_url) + self.server.content_license_url, + self.server.dogwhistles) def _get_outbox_thread_index(self, nickname: str, max_outbox_threads_per_account: int) -> int: @@ -3182,7 +3184,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, self.server.default_timeline, reply_is_chat, - bold_reading).encode('utf-8') + bold_reading, + self.server.dogwhistles).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -3326,7 +3329,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, self.server.default_timeline, reply_is_chat, - bold_reading).encode('utf-8') + bold_reading, + self.server.dogwhistles).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, cookie, calling_domain, False) @@ -3888,7 +3892,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) @@ -3993,7 +3998,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) if history_str: msg = history_str.encode('utf-8') msglen = len(msg) @@ -4073,7 +4079,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) if bookmarks_str: msg = bookmarks_str.encode('utf-8') msglen = len(msg) @@ -4243,7 +4250,8 @@ class PubServer(BaseHTTPRequestHandler): timezone, self.server.onion_domain, self.server.i2p_domain, - bold_reading) + bold_reading, + self.server.dogwhistles) if profile_str: msg = profile_str.encode('utf-8') msglen = len(msg) @@ -5098,6 +5106,27 @@ class PubServer(BaseHTTPRequestHandler): print('EX: _newswire_update unable to delete ' + filter_newswire_filename) + # save dogwhistle words list + dogwhistles_filename = base_dir + '/accounts/dogwhistles.txt' + if fields.get('dogwhistleWords'): + try: + with open(dogwhistles_filename, 'w+', + encoding='utf-8') as fp_dogwhistles: + fp_dogwhistles.write(fields['dogwhistleWords']) + except OSError: + print('EX: unable to write ' + dogwhistles_filename) + self.server.dogwhistles = \ + load_dogwhistles(dogwhistles_filename) + else: + # save an empty file + try: + with open(dogwhistles_filename, 'w+', + encoding='utf-8') as fp_dogwhistles: + fp_dogwhistles.write('') + except OSError: + print('EX: unable to write ' + dogwhistles_filename) + self.server.dogwhistles = {} + # save news tagging rules hashtag_rules_filename = \ base_dir + '/accounts/hashtagrules.txt' @@ -8471,7 +8500,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) if hashtag_str: msg = hashtag_str.encode('utf-8') msglen = len(msg) @@ -8719,7 +8749,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) actor_absolute = self._get_instance_url(calling_domain) + actor actor_path_str = \ @@ -9265,7 +9296,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Liked post not found: ' + liked_post_filename) # clear the icon from the cache so that it gets updated @@ -9450,7 +9482,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Unliked post not found: ' + liked_post_filename) # clear the icon from the cache so that it gets updated @@ -9664,7 +9697,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Emoji reaction post not found: ' + reaction_post_filename) @@ -9868,7 +9902,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Unreaction post not found: ' + reaction_post_filename) @@ -9974,7 +10009,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, timeline_str, page_number, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -10116,7 +10152,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Bookmarked post not found: ' + bookmark_filename) # self._post_to_outbox(bookmark_json, @@ -10265,7 +10302,8 @@ class PubServer(BaseHTTPRequestHandler): False, True, False, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Unbookmarked post not found: ' + bookmark_filename) @@ -10375,7 +10413,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.max_like_count, self.server.signing_priv_key_pem, self.server.cw_lists, - self.server.lists_enabled) + self.server.lists_enabled, + self.server.dogwhistles) if delete_str: delete_str_len = len(delete_str) self._set_headers('text/html', delete_str_len, @@ -10502,7 +10541,8 @@ class PubServer(BaseHTTPRequestHandler): use_cache_only, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Muted post not found: ' + mute_filename) @@ -10629,7 +10669,8 @@ class PubServer(BaseHTTPRequestHandler): use_cache_only, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) else: print('WARN: Unmuted post not found: ' + mute_filename) if calling_domain.endswith('.onion') and onion_domain: @@ -10758,7 +10799,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -10862,7 +10904,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -11316,7 +11359,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, 'inbox', self.server.default_timeline, - bold_reading) + bold_reading, self.server.dogwhistles) if not msg: self._404() return True @@ -11383,7 +11426,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.cw_lists, self.server.lists_enabled, 'inbox', self.server.default_timeline, - bold_reading, 'shares') + bold_reading, self.server.dogwhistles, + 'shares') if not msg: self._404() return True @@ -11472,7 +11516,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -11788,7 +11833,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) if getreq_start_time: fitness_performance(getreq_start_time, self.server.fitness, @@ -11959,7 +12005,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12119,7 +12166,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.replies) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12277,7 +12325,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12435,7 +12484,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12602,7 +12652,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12772,7 +12823,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12892,7 +12944,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, timezone, - bold_reading) + bold_reading, self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -12986,7 +13038,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13123,7 +13176,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13274,7 +13328,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -13422,7 +13477,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.signing_priv_key_pem, self.server.cw_lists, self.server.lists_enabled, - timezone, bold_reading) + timezone, bold_reading, + self.server.dogwhistles) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14799,7 +14855,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.lists_enabled, self.server.default_timeline, reply_is_chat, - bold_reading).encode('utf-8') + bold_reading, + self.server.dogwhistles).encode('utf-8') if not msg: print('Error replying to ' + in_reply_to_url) self._404() @@ -14942,7 +14999,8 @@ class PubServer(BaseHTTPRequestHandler): http_prefix, self.server.default_timeline, self.server.theme_name, - access_keys) + access_keys, + self.server.dogwhistles) if msg: msg = msg.encode('utf-8') msglen = len(msg) @@ -20967,6 +21025,12 @@ def run_daemon(preferred_podcast_formats: [], # scan the theme directory for any svg files containing scripts assert not scan_themes_for_scripts(base_dir) + # load a list of dogwhistle words + dogwhistles_filename = base_dir + '/accounts/dogwhistles.txt' + if not os.path.isfile(dogwhistles_filename): + dogwhistles_filename = base_dir + '/default_dogwhistles.txt' + httpd.dogwhistles = load_dogwhistles(dogwhistles_filename) + # list of preferred podcast formats # eg ['audio/opus', 'audio/mp3'] httpd.preferred_podcast_formats = preferred_podcast_formats diff --git a/default_dogwhistles.txt b/default_dogwhistles.txt new file mode 100644 index 000000000..4860e5081 --- /dev/null +++ b/default_dogwhistles.txt @@ -0,0 +1,15 @@ +X-pilled, alt-right terminology +soy boy, alt-right terminology +soyboy, alt-right terminology +soyboi, alt-right terminology +kek, alt-right terminology +groyper, alt-right meme +chad, alt-right meme +globalist*, antisemitism +globalism, antisemitism +fren, alt-right terminology +cuck*, alt-right terminology +*1488, nazism +nazbol, nazism +*1290, antisemitism + diff --git a/inbox.py b/inbox.py index 8b64d4a1b..091c6bbd4 100644 --- a/inbox.py +++ b/inbox.py @@ -131,6 +131,7 @@ from conversation import update_conversation from webapp_hashtagswarm import html_hash_tag_swarm from person import valid_sending_actor from fitnessFunctions import fitness_performance +from content import load_dogwhistles from content import valid_url_lengths from content import remove_script @@ -408,7 +409,8 @@ def _inbox_store_post_to_html_cache(recent_posts_cache: {}, lists_enabled: str, timezone: str, mitm: bool, - bold_reading: bool) -> None: + bold_reading: bool, + dogwhistles: {}) -> None: """Converts the json post into html and stores it in a cache This enables the post to be quickly displayed later """ @@ -434,7 +436,7 @@ def _inbox_store_post_to_html_cache(recent_posts_cache: {}, theme_name, system_language, max_like_count, not_dm, True, True, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) def valid_inbox(base_dir: str, nickname: str, domain: str) -> bool: @@ -1106,7 +1108,7 @@ def _receive_edit_to_post(recent_posts_cache: {}, message_json: {}, show_published_date_only: bool, peertube_instances: [], theme_name: str, max_like_count: int, - cw_lists: {}) -> bool: + cw_lists: {}, dogwhistles: {}) -> bool: """A post was edited """ if not has_object_dict(message_json): @@ -1230,7 +1232,7 @@ def _receive_edit_to_post(recent_posts_cache: {}, message_json: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1250,7 +1252,7 @@ def _receive_update_activity(recent_posts_cache: {}, session, base_dir: str, show_published_date_only: bool, peertube_instances: [], theme_name: str, max_like_count: int, - cw_lists: {}) -> bool: + cw_lists: {}, dogwhistles: {}) -> bool: """Receives an Update activity within the POST section of HTTPServer """ @@ -1290,7 +1292,7 @@ def _receive_update_activity(recent_posts_cache: {}, session, base_dir: str, show_published_date_only, peertube_instances, theme_name, max_like_count, - cw_lists): + cw_lists, dogwhistles): print('EDITPOST: received ' + message_json['object']['id']) return True else: @@ -1340,7 +1342,7 @@ def _receive_like(recent_posts_cache: {}, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, lists_enabled: str, - bold_reading: bool) -> bool: + bold_reading: bool, dogwhistles: {}) -> bool: """Receives a Like activity within the POST section of HTTPServer """ if message_json['type'] != 'Like': @@ -1449,7 +1451,7 @@ def _receive_like(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1469,7 +1471,7 @@ def _receive_undo_like(recent_posts_cache: {}, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, lists_enabled: str, - bold_reading: bool) -> bool: + bold_reading: bool, dogwhistles: {}) -> bool: """Receives an undo like activity within the POST section of HTTPServer """ if message_json['type'] != 'Undo': @@ -1568,7 +1570,7 @@ def _receive_undo_like(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1588,7 +1590,8 @@ def _receive_reaction(recent_posts_cache: {}, allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, - lists_enabled: str, bold_reading: bool) -> bool: + lists_enabled: str, bold_reading: bool, + dogwhistles: {}) -> bool: """Receives an emoji reaction within the POST section of HTTPServer """ if message_json['type'] != 'EmojiReact': @@ -1719,7 +1722,7 @@ def _receive_reaction(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1741,7 +1744,7 @@ def _receive_undo_reaction(recent_posts_cache: {}, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, lists_enabled: str, - bold_reading: bool) -> bool: + bold_reading: bool, dogwhistles: {}) -> bool: """Receives an undo emoji reaction within the POST section of HTTPServer """ if message_json['type'] != 'Undo': @@ -1858,7 +1861,7 @@ def _receive_undo_reaction(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1876,7 +1879,8 @@ def _receive_bookmark(recent_posts_cache: {}, allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, - lists_enabled: {}, bold_reading: bool) -> bool: + lists_enabled: {}, bold_reading: bool, + dogwhistles: {}) -> bool: """Receives a bookmark activity within the POST section of HTTPServer """ if not message_json.get('type'): @@ -1973,7 +1977,7 @@ def _receive_bookmark(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) return True @@ -1993,7 +1997,8 @@ def _receive_undo_bookmark(recent_posts_cache: {}, allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, - lists_enabled: str, bold_reading: bool) -> bool: + lists_enabled: str, bold_reading: bool, + dogwhistles: {}) -> bool: """Receives an undo bookmark activity within the POST section of HTTPServer """ if not message_json.get('type'): @@ -2090,7 +2095,8 @@ def _receive_undo_bookmark(recent_posts_cache: {}, show_individual_post_icons, manually_approve_followers, False, True, False, cw_lists, lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + dogwhistles) return True @@ -2186,7 +2192,8 @@ def _receive_announce(recent_posts_cache: {}, allow_deletion: bool, peertube_instances: [], max_like_count: int, cw_lists: {}, - lists_enabled: str, bold_reading: bool) -> bool: + lists_enabled: str, bold_reading: bool, + dogwhistles: {}) -> bool: """Receives an announce activity within the POST section of HTTPServer """ if message_json['type'] != 'Announce': @@ -2309,7 +2316,7 @@ def _receive_announce(recent_posts_cache: {}, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) if not announce_html: print('WARN: Unable to generate html for announce ' + str(message_json)) @@ -3452,7 +3459,7 @@ def _receive_question_vote(server, base_dir: str, nickname: str, domain: str, theme_name: str, system_language: str, max_like_count: int, cw_lists: {}, lists_enabled: bool, - bold_reading: bool) -> None: + bold_reading: bool, dogwhistles: {}) -> None: """Updates the votes on a Question/poll """ # if this is a reply to a question then update the votes @@ -3505,7 +3512,7 @@ def _receive_question_vote(server, base_dir: str, nickname: str, domain: str, manually_approve_followers, False, True, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) # add id to inbox index inbox_update_index('inbox', base_dir, handle, @@ -3676,7 +3683,8 @@ def _inbox_after_initial(server, inbox_start_time, cw_lists: {}, lists_enabled: str, content_license_url: str, languages_understood: [], - mitm: bool, bold_reading: bool) -> bool: + mitm: bool, bold_reading: bool, + dogwhistles: {}) -> bool: """ Anything which needs to be done after initial checks have passed """ # if this is a clearnet instance then replace any onion/i2p @@ -3724,7 +3732,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Like accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3749,7 +3757,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Undo like accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3775,7 +3783,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Reaction accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3802,7 +3810,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Undo reaction accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3829,7 +3837,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Bookmark accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3856,7 +3864,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_local_network_access, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Undo bookmark accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -3891,7 +3899,7 @@ def _inbox_after_initial(server, inbox_start_time, allow_deletion, peertube_instances, max_like_count, cw_lists, lists_enabled, - bold_reading): + bold_reading, dogwhistles): if debug: print('DEBUG: Announce accepted from ' + actor) fitness_performance(inbox_start_time, server.fitness, @@ -4033,7 +4041,7 @@ def _inbox_after_initial(server, inbox_start_time, theme_name, system_language, max_like_count, cw_lists, lists_enabled, - bold_reading) + bold_reading, dogwhistles) fitness_performance(inbox_start_time, server.fitness, 'INBOX', '_receive_question_vote', debug) @@ -4277,7 +4285,8 @@ def _inbox_after_initial(server, inbox_start_time, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, + dogwhistles) fitness_performance(inbox_start_time, server.fitness, 'INBOX', @@ -5237,6 +5246,11 @@ def run_inbox_queue(server, debug) inbox_start_time = time.time() + dogwhistles_filename = base_dir + '/accounts/dogwhistles.txt' + if not os.path.isfile(dogwhistles_filename): + dogwhistles_filename = base_dir + '/default_dogwhistles.txt' + dogwhistles = load_dogwhistles(dogwhistles_filename) + # set the id to the same as the post filename # This makes the filename and the id consistent # if queue_json['post'].get('id'): @@ -5328,7 +5342,7 @@ def run_inbox_queue(server, show_published_date_only, peertube_instances, theme_name, max_like_count, - cw_lists): + cw_lists, dogwhistles): if debug: print('Queue: Update accepted from ' + key_id) if os.path.isfile(queue_filename): @@ -5456,7 +5470,7 @@ def run_inbox_queue(server, cw_lists, lists_enabled, content_license_url, languages_understood, mitm, - bold_reading) + bold_reading, dogwhistles) fitness_performance(inbox_start_time, server.fitness, 'INBOX', 'handle_after_initial', debug) diff --git a/outbox.py b/outbox.py index 878897a0c..9698f1a11 100644 --- a/outbox.py +++ b/outbox.py @@ -213,7 +213,8 @@ def post_message_to_outbox(session, translate: {}, max_like_count: int, max_recent_posts: int, cw_lists: {}, lists_enabled: str, - content_license_url: str) -> bool: + content_license_url: str, + dogwhistles: {}) -> bool: """post is received by the outbox Client to server message post https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery @@ -513,7 +514,7 @@ def post_message_to_outbox(session, translate: {}, False, True, use_cache_only, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) if outbox_announce(recent_posts_cache, base_dir, message_json, debug): diff --git a/schedule.py b/schedule.py index 52460847b..e93a056bb 100644 --- a/schedule.py +++ b/schedule.py @@ -142,7 +142,8 @@ def _update_post_schedule(base_dir: str, handle: str, httpd, httpd.max_recent_posts, httpd.cw_lists, httpd.lists_enabled, - httpd.content_license_url): + httpd.content_license_url, + httpd.dogwhistles): index_lines.remove(line) try: os.remove(post_filename) diff --git a/tests.py b/tests.py index e6ed196a1..990299132 100644 --- a/tests.py +++ b/tests.py @@ -132,6 +132,7 @@ from inbox import valid_inbox from inbox import valid_inbox_filenames from inbox import cache_svg_images from categories import guess_hashtag_category +from content import detect_dogwhistles from content import remove_script from content import create_edits_html from content import content_diff @@ -7326,6 +7327,30 @@ def _test_remove_end_of_line(): assert remove_eol(text) == expected +def _test_dogwhistles(): + print('dogwhistles') + dogwhistles = { + "X-hamstered": "hamsterism", + "gerbil": "rodent", + "*snake": "slither", + "start*end": "something" + } + content = 'This text does not contain any dogwhistles' + assert not detect_dogwhistles(content, dogwhistles) + content = 'A gerbil named joe' + assert detect_dogwhistles(content, dogwhistles) + content = 'A rattlesnake.' + assert detect_dogwhistles(content, dogwhistles) + content = 'A startthingend.' + assert detect_dogwhistles(content, dogwhistles) + content = 'This content is unhamstered and yhamstered.' + result = detect_dogwhistles(content, dogwhistles) + assert result + assert result.get('hamstered') + assert result['hamstered']['count'] == 2 + assert result['hamstered']['category'] == "hamsterism" + + def run_all_tests(): base_dir = os.getcwd() print('Running tests...') @@ -7343,6 +7368,7 @@ def run_all_tests(): _test_checkbox_names() _test_thread_functions() _test_functions() + _test_dogwhistles() _test_remove_end_of_line() _test_translation_labels() _test_color_contrast_value(base_dir) diff --git a/translations/ar.json b/translations/ar.json index 5340cc17b..cc1c0383c 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -565,5 +565,7 @@ "Switch to moderation view": "قم بالتبديل إلى عرض الاعتدال", "Minimize attached images": "تصغير الصور المرفقة", "SHOW MEDIA": "عرض الوسائط", - "ActivityPub Specification": "مواصفات ActivityPub" + "ActivityPub Specification": "مواصفات ActivityPub", + "Dogwhistle words": "كلمات Dogwhistle", + "Content warnings will be added for the following": "ستتم إضافة تحذيرات المحتوى لما يلي" } diff --git a/translations/bn.json b/translations/bn.json index 553d29482..7fb7c941d 100644 --- a/translations/bn.json +++ b/translations/bn.json @@ -565,5 +565,7 @@ "Switch to moderation view": "সংযম দৃশ্যে স্যুইচ করুন", "Minimize attached images": "সংযুক্ত ছবি ছোট করুন", "SHOW MEDIA": "মিডিয়া দেখান", - "ActivityPub Specification": "ActivityPub স্পেসিফিকেশন" + "ActivityPub Specification": "ActivityPub স্পেসিফিকেশন", + "Dogwhistle words": "কুকুরের হুইসেল শব্দ", + "Content warnings will be added for the following": "নিম্নলিখিত জন্য বিষয়বস্তু সতর্কতা যোগ করা হবে" } diff --git a/translations/ca.json b/translations/ca.json index 67933223f..bfd994db7 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Canvia a la visualització de moderació", "Minimize attached images": "Minimitzar les imatges adjuntes", "SHOW MEDIA": "MOSTRA ELS MITJANS", - "ActivityPub Specification": "Especificació d'ActivityPub" + "ActivityPub Specification": "Especificació d'ActivityPub", + "Dogwhistle words": "Paraules de xiulet", + "Content warnings will be added for the following": "S'afegiran advertències de contingut per al següent" } diff --git a/translations/cy.json b/translations/cy.json index 7247eee33..ff2ab6fc4 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Newid i wedd safoni", "Minimize attached images": "Lleihau delweddau sydd ynghlwm", "SHOW MEDIA": "DANGOS CYFRYNGAU", - "ActivityPub Specification": "Manyleb GweithgareddPub" + "ActivityPub Specification": "Manyleb GweithgareddPub", + "Dogwhistle words": "Geiriau chwibanogl", + "Content warnings will be added for the following": "Bydd rhybuddion cynnwys yn cael eu hychwanegu ar gyfer y canlynol" } diff --git a/translations/de.json b/translations/de.json index f19a7411d..962cfcbb9 100644 --- a/translations/de.json +++ b/translations/de.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Wechseln Sie zur Moderationsansicht", "Minimize attached images": "Angehängte Bilder minimieren", "SHOW MEDIA": "MEDIEN ZEIGEN", - "ActivityPub Specification": "ActivityPub-Spezifikation" + "ActivityPub Specification": "ActivityPub-Spezifikation", + "Dogwhistle words": "Hundepfeife Worte", + "Content warnings will be added for the following": "Inhaltswarnungen werden für Folgendes hinzugefügt" } diff --git a/translations/el.json b/translations/el.json index ea496a1fc..8a3cee2df 100644 --- a/translations/el.json +++ b/translations/el.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Μετάβαση σε προβολή εποπτείας", "Minimize attached images": "Ελαχιστοποιήστε τις συνημμένες εικόνες", "SHOW MEDIA": "ΔΕΙΤΕ ΜΕΣΑ", - "ActivityPub Specification": "Προδιαγραφές ActivityPub" + "ActivityPub Specification": "Προδιαγραφές ActivityPub", + "Dogwhistle words": "Σφυρίχτρα λέξεις", + "Content warnings will be added for the following": "Θα προστεθούν προειδοποιήσεις περιεχομένου για τα ακόλουθα" } diff --git a/translations/en.json b/translations/en.json index d283394d6..6b19ef3b2 100644 --- a/translations/en.json +++ b/translations/en.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Switch to moderation view", "Minimize attached images": "Minimize attached images", "SHOW MEDIA": "SHOW MEDIA", - "ActivityPub Specification": "ActivityPub Specification" + "ActivityPub Specification": "ActivityPub Specification", + "Dogwhistle words": "Dogwhistle words", + "Content warnings will be added for the following": "Content warnings will be added for the following" } diff --git a/translations/es.json b/translations/es.json index c021c76a8..2a0f58af5 100644 --- a/translations/es.json +++ b/translations/es.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Cambiar a la vista de moderación", "Minimize attached images": "Minimizar imágenes adjuntas", "SHOW MEDIA": "MOSTRAR MEDIOS", - "ActivityPub Specification": "Especificación de ActivityPub" + "ActivityPub Specification": "Especificación de ActivityPub", + "Dogwhistle words": "Palabras de silbato para perros", + "Content warnings will be added for the following": "Se agregarán advertencias de contenido para lo siguiente" } diff --git a/translations/fr.json b/translations/fr.json index 02adc075e..493cc8346 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Passer en mode modération", "Minimize attached images": "Réduire les images jointes", "SHOW MEDIA": "AFFICHER LES MÉDIAS", - "ActivityPub Specification": "Spécification ActivityPub" + "ActivityPub Specification": "Spécification ActivityPub", + "Dogwhistle words": "Mots de sifflet de chien", + "Content warnings will be added for the following": "Des avertissements de contenu seront ajoutés pour les éléments suivants" } diff --git a/translations/ga.json b/translations/ga.json index 89e4839e1..d980c9432 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Athraigh go dtí an t-amharc modhnóireachta", "Minimize attached images": "Íoslaghdaigh íomhánna ceangailte", "SHOW MEDIA": "Taispeáin MEÁIN", - "ActivityPub Specification": "Sonraíocht ActivityPub" + "ActivityPub Specification": "Sonraíocht ActivityPub", + "Dogwhistle words": "Focail feadóg mhadra", + "Content warnings will be added for the following": "Cuirfear rabhaidh ábhair leis maidir leis na nithe seo a leanas" } diff --git a/translations/hi.json b/translations/hi.json index 25da4a044..95f4c8a66 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -565,5 +565,7 @@ "Switch to moderation view": "मॉडरेशन दृश्य पर स्विच करें", "Minimize attached images": "संलग्न छवियों को छोटा करें", "SHOW MEDIA": "मीडिया दिखाएं", - "ActivityPub Specification": "गतिविधिपब विशिष्टता" + "ActivityPub Specification": "गतिविधिपब विशिष्टता", + "Dogwhistle words": "कुत्ते की सीटी शब्द", + "Content warnings will be added for the following": "निम्नलिखित के लिए सामग्री चेतावनियां जोड़ दी जाएंगी" } diff --git a/translations/it.json b/translations/it.json index b120d93ea..f5b7cb4e6 100644 --- a/translations/it.json +++ b/translations/it.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Passa alla visualizzazione moderazione", "Minimize attached images": "Riduci al minimo le immagini allegate", "SHOW MEDIA": "MOSTRA MEDIA", - "ActivityPub Specification": "Specifica ActivityPub" + "ActivityPub Specification": "Specifica ActivityPub", + "Dogwhistle words": "Parole da fischietto", + "Content warnings will be added for the following": "Verranno aggiunti avvisi sui contenuti per quanto segue" } diff --git a/translations/ja.json b/translations/ja.json index 2ecea1bcb..5210d836c 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -565,5 +565,7 @@ "Switch to moderation view": "モデレートビューに切り替えます", "Minimize attached images": "添付画像を最小限に抑える", "SHOW MEDIA": "メディアを表示", - "ActivityPub Specification": "ActivityPubの仕様" + "ActivityPub Specification": "ActivityPubの仕様", + "Dogwhistle words": "犬笛の言葉", + "Content warnings will be added for the following": "以下のコンテンツ警告が追加されます" } diff --git a/translations/ko.json b/translations/ko.json index 8dbfbb256..b0a9e2b55 100644 --- a/translations/ko.json +++ b/translations/ko.json @@ -565,5 +565,7 @@ "Switch to moderation view": "검토 보기로 전환", "Minimize attached images": "첨부된 이미지 최소화", "SHOW MEDIA": "미디어 표시", - "ActivityPub Specification": "ActivityPub 사양" + "ActivityPub Specification": "ActivityPub 사양", + "Dogwhistle words": "개 휘파람 단어", + "Content warnings will be added for the following": "다음에 대한 콘텐츠 경고가 추가됩니다." } diff --git a/translations/ku.json b/translations/ku.json index fbbe26b22..7df2289a8 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Biguherîne bo dîtina moderatoriyê", "Minimize attached images": "Wêneyên pêvekirî kêm bikin", "SHOW MEDIA": "MEDYA NÎŞAN DE", - "ActivityPub Specification": "Specification ActivityPub" + "ActivityPub Specification": "Specification ActivityPub", + "Dogwhistle words": "Peyvên kûçikê", + "Content warnings will be added for the following": "Hişyariyên naverokê dê ji bo jêrîn werin zêdekirin" } diff --git a/translations/nl.json b/translations/nl.json index 775c8d284..8a94c779d 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Overschakelen naar moderatieweergave", "Minimize attached images": "Bijgevoegde afbeeldingen minimaliseren", "SHOW MEDIA": "TOON MEDIA", - "ActivityPub Specification": "ActivityPub-specificatie" + "ActivityPub Specification": "ActivityPub-specificatie", + "Dogwhistle words": "Hondenfluitwoorden", + "Content warnings will be added for the following": "Er worden inhoudswaarschuwingen toegevoegd voor het volgende:" } diff --git a/translations/oc.json b/translations/oc.json index 98d73eedc..42e21d93b 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -561,5 +561,7 @@ "Switch to moderation view": "Switch to moderation view", "Minimize attached images": "Minimize attached images", "SHOW MEDIA": "SHOW MEDIA", - "ActivityPub Specification": "ActivityPub Specification" + "ActivityPub Specification": "ActivityPub Specification", + "Dogwhistle words": "Dogwhistle words", + "Content warnings will be added for the following": "Content warnings will be added for the following" } diff --git a/translations/pl.json b/translations/pl.json index feb0e953e..75da3fdba 100644 --- a/translations/pl.json +++ b/translations/pl.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Przełącz na widok moderacji", "Minimize attached images": "Zminimalizuj załączone obrazy", "SHOW MEDIA": "POKAŻ MEDIA", - "ActivityPub Specification": "Specyfikacja ActivityPub" + "ActivityPub Specification": "Specyfikacja ActivityPub", + "Dogwhistle words": "Słowa gwizdka na psa", + "Content warnings will be added for the following": "Ostrzeżenia dotyczące treści zostaną dodane do następujących" } diff --git a/translations/pt.json b/translations/pt.json index b2725ce50..2adeebaa0 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Mudar para a visualização de moderação", "Minimize attached images": "Minimizar imagens anexadas", "SHOW MEDIA": "MOSTRAR MÍDIA", - "ActivityPub Specification": "Especificação do ActivityPub" + "ActivityPub Specification": "Especificação do ActivityPub", + "Dogwhistle words": "Palavras de apito", + "Content warnings will be added for the following": "Avisos de conteúdo serão adicionados para os seguintes" } diff --git a/translations/ru.json b/translations/ru.json index bf21f25f3..e12bb1799 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Перейти в режим модерации", "Minimize attached images": "Свернуть прикрепленные изображения", "SHOW MEDIA": "ПОКАЗАТЬ МЕДИА", - "ActivityPub Specification": "Спецификация ActivityPub" + "ActivityPub Specification": "Спецификация ActivityPub", + "Dogwhistle words": "Собачий свисток", + "Content warnings will be added for the following": "Предупреждения о содержании будут добавлены для следующих" } diff --git a/translations/sw.json b/translations/sw.json index 21d05b2b8..02682a5b5 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Badili hadi mwonekano wa udhibiti", "Minimize attached images": "Punguza picha zilizoambatishwa", "SHOW MEDIA": "ONESHA VYOMBO VYA HABARI", - "ActivityPub Specification": "Vipimo vya ActivityPub" + "ActivityPub Specification": "Vipimo vya ActivityPub", + "Dogwhistle words": "Maneno ya mbwa", + "Content warnings will be added for the following": "Maonyo ya maudhui yataongezwa kwa yafuatayo" } diff --git a/translations/tr.json b/translations/tr.json index 95b1bba51..2eadf668a 100644 --- a/translations/tr.json +++ b/translations/tr.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Denetleme görünümüne geç", "Minimize attached images": "Ekli resimleri simge durumuna küçült", "SHOW MEDIA": "MEDYA GÖSTER", - "ActivityPub Specification": "ActivityPub Spesifikasyonu" + "ActivityPub Specification": "ActivityPub Spesifikasyonu", + "Dogwhistle words": "İtiraf sözleri", + "Content warnings will be added for the following": "Aşağıdakiler için içerik uyarıları eklenecek" } diff --git a/translations/uk.json b/translations/uk.json index 86e92abaa..58723e75a 100644 --- a/translations/uk.json +++ b/translations/uk.json @@ -565,5 +565,7 @@ "Switch to moderation view": "Перейти до режиму модерації", "Minimize attached images": "Мінімізуйте вкладені зображення", "SHOW MEDIA": "ПОКАЗАТИ ЗМІ", - "ActivityPub Specification": "Специфікація ActivityPub" + "ActivityPub Specification": "Специфікація ActivityPub", + "Dogwhistle words": "Собачі слова", + "Content warnings will be added for the following": "Попередження про вміст буде додано для наступних" } diff --git a/translations/yi.json b/translations/yi.json index 1ab354557..1a3f2fcf8 100644 --- a/translations/yi.json +++ b/translations/yi.json @@ -565,5 +565,7 @@ "Switch to moderation view": "באַשטימען צו מאַדעריישאַן מיינונג", "Minimize attached images": "מינאַמייז אַטאַטשט בילדער", "SHOW MEDIA": "ווייַז מעדיע", - "ActivityPub Specification": "ActivityPub באַשרייַבונג" + "ActivityPub Specification": "ActivityPub באַשרייַבונג", + "Dogwhistle words": "דאָגווהיסטלע ווערטער", + "Content warnings will be added for the following": "אינהאַלט וואָרנינגז וועט זיין מוסיף פֿאַר די פאלגענדע" } diff --git a/translations/zh.json b/translations/zh.json index 0f1f7d9ae..e953072ec 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -565,5 +565,7 @@ "Switch to moderation view": "切换到审核视图", "Minimize attached images": "最小化附加图像", "SHOW MEDIA": "展示媒体", - "ActivityPub Specification": "ActivityPub 规范" + "ActivityPub Specification": "ActivityPub 规范", + "Dogwhistle words": "狗哨的话", + "Content warnings will be added for the following": "将为以下内容添加内容警告" } diff --git a/webapp_column_right.py b/webapp_column_right.py index 5e41fe570..5339f63f9 100644 --- a/webapp_column_right.py +++ b/webapp_column_right.py @@ -549,7 +549,7 @@ def html_newswire_mobile(css_cache: {}, base_dir: str, nickname: str, def html_edit_newswire(css_cache: {}, translate: {}, base_dir: str, path: str, domain: str, port: int, http_prefix: str, default_timeline: str, theme: str, - access_keys: {}) -> str: + access_keys: {}, dogwhistles: {}) -> str: """Shows the edit newswire screen """ if '/users/' not in path: @@ -645,6 +645,22 @@ def html_edit_newswire(css_cache: {}, translate: {}, base_dir: str, path: str, 'name="filteredWordsNewswire" style="height:50vh" ' + \ 'spellcheck="true">' + filter_str + '\n' + dogwhistle_str = '' + for whistle, category in dogwhistles.items(): + if not category: + continue + dogwhistle_str += whistle + ' -> ' + category + '\n' + + edit_newswire_form += \ + '
\n' + edit_newswire_form += '
' + edit_newswire_form += ' \n' + hashtag_rules_str = '' hashtag_rules_filename = \ base_dir + '/accounts/hashtagrules.txt' diff --git a/webapp_confirm.py b/webapp_confirm.py index 63dce3c4f..5b8a13106 100644 --- a/webapp_confirm.py +++ b/webapp_confirm.py @@ -38,7 +38,8 @@ def html_confirm_delete(server, css_cache: {}, allow_local_network_access: bool, theme_name: str, system_language: str, max_like_count: int, signing_priv_key_pem: str, - cw_lists: {}, lists_enabled: str) -> str: + cw_lists: {}, lists_enabled: str, + dogwhistles: {}) -> str: """Shows a screen asking to confirm the deletion of a post """ if '/statuses/' not in message_id: @@ -90,7 +91,7 @@ def html_confirm_delete(server, css_cache: {}, theme_name, system_language, max_like_count, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) delete_post_str += '
' delete_post_str += \ '

' + \ diff --git a/webapp_create_post.py b/webapp_create_post.py index 51dfcc54e..f8653a685 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -224,7 +224,8 @@ def html_new_post(css_cache: {}, media_instance: bool, translate: {}, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, box_name: str, - reply_is_chat: bool, bold_reading: bool) -> str: + reply_is_chat: bool, bold_reading: bool, + dogwhistles: {}) -> str: """New post screen """ reply_str = '' @@ -303,7 +304,7 @@ def html_new_post(css_cache: {}, media_instance: bool, translate: {}, False, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) reply_str = '\n' diff --git a/webapp_frontscreen.py b/webapp_frontscreen.py index f0996dc50..1e961d698 100644 --- a/webapp_frontscreen.py +++ b/webapp_frontscreen.py @@ -38,7 +38,8 @@ def _html_front_screen_posts(recent_posts_cache: {}, max_recent_posts: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - bold_reading: bool) -> str: + bold_reading: bool, + dogwhistles: {}) -> 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 @@ -89,7 +90,7 @@ def _html_front_screen_posts(recent_posts_cache: {}, max_recent_posts: int, True, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) if post_str: profile_str += post_str + separator_str ctr += 1 @@ -120,7 +121,8 @@ def html_front_screen(signing_priv_key_pem: str, extra_json: {}, page_number: int, max_items_per_page: int, - cw_lists: {}, lists_enabled: str) -> str: + cw_lists: {}, lists_enabled: str, + dogwhistles: {}) -> str: """Show the news instance front screen """ bold_reading = False @@ -195,7 +197,7 @@ def html_front_screen(signing_priv_key_pem: str, max_like_count, signing_priv_key_pem, cw_lists, lists_enabled, - bold_reading) + license_str + bold_reading, dogwhistles) + 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 680c78b7d..6ff5970f0 100644 --- a/webapp_likers.py +++ b/webapp_likers.py @@ -41,7 +41,7 @@ def html_likers_of_post(base_dir: str, nickname: str, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, box_name: str, default_timeline: str, - bold_reading: bool, + bold_reading: bool, dogwhistles: {}, dict_name: str = 'likes') -> str: """Returns html for a screen showing who liked a post """ @@ -107,7 +107,8 @@ def html_likers_of_post(base_dir: str, nickname: str, False, False, False, False, False, False, cw_lists, lists_enabled, - timezone, mitm, bold_reading) + timezone, mitm, bold_reading, + dogwhistles) # show likers beneath the post obj = post_json_object diff --git a/webapp_moderation.py b/webapp_moderation.py index a3914ce7b..50db5883e 100644 --- a/webapp_moderation.py +++ b/webapp_moderation.py @@ -57,7 +57,8 @@ def html_moderation(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the moderation feed as html This is what you see when selecting the "mod" timeline """ @@ -83,7 +84,7 @@ def html_moderation(css_cache: {}, default_timeline: str, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, - timezone, bold_reading) + timezone, bold_reading, dogwhistles) def html_account_info(css_cache: {}, translate: {}, diff --git a/webapp_post.py b/webapp_post.py index f42207fb6..cc34a0fb3 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -62,6 +62,7 @@ from utils import get_domain_from_actor from utils import acct_dir from utils import local_actor_url from utils import is_unlisted_post +from content import detect_dogwhistles from content import create_edits_html from content import bold_reading_string from content import limit_repeated_words @@ -1555,6 +1556,30 @@ def _substitute_onion_domains(base_dir: str, content: str) -> str: return content +def _add_dogwhistle_warnings(summary: str, content: str, + dogwhistles: {}, translate: {}) -> {}: + """Adds dogwhistle warnings for the given content + """ + if not dogwhistles: + return summary + content_str = str(summary) + ' ' + content + detected = detect_dogwhistles(content_str, dogwhistles) + if not detected: + return summary + + for _, item in detected.items(): + if not item.get('category'): + continue + whistle_str = item['category'] + if translate.get(whistle_str): + whistle_str = translate[whistle_str] + if summary: + summary += ', ' + whistle_str + else: + summary = whistle_str + return summary + + def individual_post_as_html(signing_priv_key_pem: str, allow_downloads: bool, recent_posts_cache: {}, max_recent_posts: int, @@ -1583,7 +1608,8 @@ def individual_post_as_html(signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, - mitm: bool, bold_reading: bool) -> str: + mitm: bool, bold_reading: bool, + dogwhistles: {}) -> str: """ Shows a single post as html """ if not post_json_object: @@ -2142,6 +2168,10 @@ def individual_post_as_html(signing_priv_key_pem: str, if content_str: summary_str = get_summary_from_post(post_json_object, system_language, languages_understood) + # add dogwhistle warnings to summary + summary_str = _add_dogwhistle_warnings(summary_str, content_str, + dogwhistles, translate) + content_all_str = str(summary_str) + ' ' + content_str # does an emoji indicate a no boost preference? # if so then don't show the repeat/announce icon @@ -2378,7 +2408,7 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, timezone: str, mitm: bool, - bold_reading: bool) -> str: + bold_reading: bool, dogwhistles: {}) -> str: """Show an individual post as html """ original_post_json = post_json_object @@ -2447,7 +2477,7 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, system_language, max_like_count, False, authorized, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) message_id = remove_id_ending(post_json_object['id']) # show the previous posts @@ -2488,7 +2518,8 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, False, False, False, False, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + post_str + bold_reading, + dogwhistles) + post_str # show the following posts post_filename = locate_post(base_dir, nickname, domain, message_id) @@ -2527,7 +2558,7 @@ def html_individual_post(recent_posts_cache: {}, max_recent_posts: int, False, False, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): css_filename = base_dir + '/epicyon.css' @@ -2555,7 +2586,8 @@ def html_post_replies(recent_posts_cache: {}, max_recent_posts: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the replies to an individual post as html """ replies_str = '' @@ -2582,7 +2614,7 @@ def html_post_replies(recent_posts_cache: {}, max_recent_posts: int, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) css_filename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): @@ -2611,7 +2643,8 @@ def html_emoji_reaction_picker(recent_posts_cache: {}, max_recent_posts: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, box_name: str, page_number: int, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Returns the emoji picker screen """ reacted_to_post_str = \ @@ -2635,7 +2668,7 @@ def html_emoji_reaction_picker(recent_posts_cache: {}, max_recent_posts: int, max_like_count, False, False, False, False, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) 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 e024cb659..9da1602bb 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -148,7 +148,7 @@ def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int, cw_lists: {}, lists_enabled: str, timezone: str, onion_domain: str, i2p_domain: str, - bold_reading: bool) -> str: + bold_reading: bool, dogwhistles: {}) -> str: """Show a profile page after a search for a fediverse address """ http = False @@ -388,7 +388,7 @@ def html_profile_after_search(recent_posts_cache: {}, max_recent_posts: int, False, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) i += 1 if i >= 8: break @@ -636,7 +636,7 @@ def html_profile(signing_priv_key_pem: str, system_language, max_like_count, shared_items_federated_domains, None, page_number, max_items_per_page, cw_lists, - lists_enabled) + lists_enabled, {}) domain, port = get_domain_from_actor(profile_json['id']) if not domain: @@ -1031,7 +1031,7 @@ def html_profile(signing_priv_key_pem: str, max_like_count, signing_priv_key_pem, cw_lists, lists_enabled, - timezone, bold_reading) + license_str + timezone, bold_reading, {}) + license_str if not is_group: if selected == 'following': profile_str += \ @@ -1104,7 +1104,8 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Shows posts on the profile screen These should only be public posts """ @@ -1154,7 +1155,7 @@ def _html_profile_posts(recent_posts_cache: {}, max_recent_posts: int, True, False, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) if post_str: profile_str += post_str + separator_str ctr += 1 diff --git a/webapp_search.py b/webapp_search.py index 72cc0058a..c30b78e97 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -621,7 +621,8 @@ def html_history_search(translate: {}, base_dir: str, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show a page containing search results for your post history """ if historysearch.startswith("'"): @@ -709,7 +710,8 @@ def html_history_search(translate: {}, base_dir: str, show_individual_post_icons, False, False, False, False, cw_lists, lists_enabled, - timezone, False, bold_reading) + timezone, False, bold_reading, + dogwhistles) if post_str: history_search_form += separator_str + post_str index += 1 @@ -734,7 +736,8 @@ def html_hashtag_search(nickname: str, domain: str, port: int, max_like_count: int, signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show a page containing search results for a hashtag or after selecting a hashtag from the swarm """ @@ -897,7 +900,7 @@ def html_hashtag_search(nickname: str, domain: str, port: int, show_public_only, store_to_sache, False, cw_lists, lists_enabled, timezone, False, - bold_reading) + bold_reading, dogwhistles) if post_str: hashtag_search_form += separator_str + post_str index += 1 diff --git a/webapp_timeline.py b/webapp_timeline.py index c6826b9c7..651a290f2 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -493,7 +493,8 @@ def html_timeline(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the timeline as html """ enable_timing_log = False @@ -1000,7 +1001,7 @@ def html_timeline(css_cache: {}, default_timeline: str, False, True, use_cache_only, cw_lists, lists_enabled, timezone, mitm, - bold_reading) + bold_reading, dogwhistles) _log_timeline_timing(enable_timing_log, timeline_start_time, box_name, '12') @@ -1238,7 +1239,8 @@ def html_shares(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the shares timeline as html """ manually_approve_followers = \ @@ -1269,7 +1271,7 @@ def html_shares(css_cache: {}, default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, - bold_reading) + bold_reading, dogwhistles) def html_wanted(css_cache: {}, default_timeline: str, @@ -1298,7 +1300,8 @@ def html_wanted(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the wanted timeline as html """ manually_approve_followers = \ @@ -1329,7 +1332,7 @@ def html_wanted(css_cache: {}, default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, - bold_reading) + bold_reading, dogwhistles) def html_inbox(css_cache: {}, default_timeline: str, @@ -1359,7 +1362,8 @@ def html_inbox(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the inbox as html """ manually_approve_followers = \ @@ -1390,7 +1394,7 @@ def html_inbox(css_cache: {}, default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, - bold_reading) + bold_reading, dogwhistles) def html_bookmarks(css_cache: {}, default_timeline: str, @@ -1420,7 +1424,8 @@ def html_bookmarks(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the bookmarks as html """ manually_approve_followers = \ @@ -1450,7 +1455,7 @@ def html_bookmarks(css_cache: {}, default_timeline: str, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, - bold_reading) + bold_reading, dogwhistles) def html_inbox_dms(css_cache: {}, default_timeline: str, @@ -1480,7 +1485,8 @@ def html_inbox_dms(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the DM timeline as html """ artist = is_artist(base_dir, nickname) @@ -1506,7 +1512,7 @@ def html_inbox_dms(css_cache: {}, default_timeline: str, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled, timezone, - bold_reading) + bold_reading, dogwhistles) def html_inbox_replies(css_cache: {}, default_timeline: str, @@ -1536,7 +1542,8 @@ def html_inbox_replies(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the replies timeline as html """ artist = is_artist(base_dir, nickname) @@ -1560,7 +1567,8 @@ def html_inbox_replies(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles) def html_inbox_media(css_cache: {}, default_timeline: str, @@ -1590,7 +1598,8 @@ def html_inbox_media(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the media timeline as html """ artist = is_artist(base_dir, nickname) @@ -1614,7 +1623,8 @@ def html_inbox_media(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles) def html_inbox_blogs(css_cache: {}, default_timeline: str, @@ -1644,7 +1654,8 @@ def html_inbox_blogs(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the blogs timeline as html """ artist = is_artist(base_dir, nickname) @@ -1668,7 +1679,8 @@ def html_inbox_blogs(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles) def html_inbox_features(css_cache: {}, default_timeline: str, @@ -1699,7 +1711,8 @@ def html_inbox_features(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the features timeline as html """ return html_timeline(css_cache, default_timeline, @@ -1722,7 +1735,8 @@ def html_inbox_features(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles) def html_inbox_news(css_cache: {}, default_timeline: str, @@ -1752,7 +1766,8 @@ def html_inbox_news(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the news timeline as html """ return html_timeline(css_cache, default_timeline, @@ -1775,7 +1790,8 @@ def html_inbox_news(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles) def html_outbox(css_cache: {}, default_timeline: str, @@ -1805,7 +1821,8 @@ def html_outbox(css_cache: {}, default_timeline: str, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str, - timezone: str, bold_reading: bool) -> str: + timezone: str, bold_reading: bool, + dogwhistles: {}) -> str: """Show the Outbox as html """ manually_approve_followers = \ @@ -1831,4 +1848,5 @@ def html_outbox(css_cache: {}, default_timeline: str, allow_local_network_access, text_mode_banner, access_keys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, - cw_lists, lists_enabled, timezone, bold_reading) + cw_lists, lists_enabled, timezone, bold_reading, + dogwhistles)