Merge branch 'main' of gitlab.com:bashrc2/epicyon
							
								
								
									
										85
									
								
								daemon.py
								
								
								
								
							
							
						
						|  | @ -617,6 +617,17 @@ class PubServer(BaseHTTPRequestHandler): | |||
|             return True | ||||
|         return False | ||||
| 
 | ||||
|     def _request_ssml(self) -> bool: | ||||
|         """Should a ssml response be given? | ||||
|         """ | ||||
|         if not self.headers.get('Accept'): | ||||
|             return False | ||||
|         accept_str = self.headers['Accept'] | ||||
|         if 'application/ssml' in accept_str: | ||||
|             if 'text/html' not in accept_str: | ||||
|                 return True | ||||
|         return False | ||||
| 
 | ||||
|     def _request_http(self) -> bool: | ||||
|         """Should a http response be given? | ||||
|         """ | ||||
|  | @ -11025,7 +11036,7 @@ class PubServer(BaseHTTPRequestHandler): | |||
|         self._redirect_headers(actor_absolute, cookie, calling_domain) | ||||
|         return True | ||||
| 
 | ||||
|     def _show_individual_at_post(self, authorized: bool, | ||||
|     def _show_individual_at_post(self, ssml_getreq: bool, authorized: bool, | ||||
|                                  calling_domain: str, referer_domain: str, | ||||
|                                  path: str, | ||||
|                                  base_dir: str, http_prefix: str, | ||||
|  | @ -11073,6 +11084,35 @@ class PubServer(BaseHTTPRequestHandler): | |||
|         if len(status_number) <= 10 or not status_number.isdigit(): | ||||
|             return False | ||||
| 
 | ||||
|         if ssml_getreq: | ||||
|             ssml_filename = \ | ||||
|                 acct_dir(base_dir, nickname, domain) + '/outbox/' + \ | ||||
|                 http_prefix + ':##' + domain_full + '#users#' + nickname + \ | ||||
|                 '#statuses#' + status_number + '.ssml' | ||||
|             if not os.path.isfile(ssml_filename): | ||||
|                 ssml_filename = \ | ||||
|                     acct_dir(base_dir, nickname, domain) + '/postcache/' + \ | ||||
|                     http_prefix + ':##' + domain_full + '#users#' + \ | ||||
|                     nickname + '#statuses#' + status_number + '.ssml' | ||||
|             if not os.path.isfile(ssml_filename): | ||||
|                 self._404() | ||||
|                 return True | ||||
|             ssml_str = None | ||||
|             try: | ||||
|                 with open(ssml_filename, 'r') as fp_ssml: | ||||
|                     ssml_str = fp_ssml.read() | ||||
|             except OSError: | ||||
|                 pass | ||||
|             if ssml_str: | ||||
|                 msg = ssml_str.encode('utf-8') | ||||
|                 msglen = len(msg) | ||||
|                 self._set_headers('application/ssml+xml', msglen, | ||||
|                                   cookie, calling_domain, False) | ||||
|                 self._write(msg) | ||||
|                 return True | ||||
|             self._404() | ||||
|             return True | ||||
| 
 | ||||
|         post_filename = \ | ||||
|             acct_dir(base_dir, nickname, domain) + '/outbox/' + \ | ||||
|             http_prefix + ':##' + domain_full + '#users#' + nickname + \ | ||||
|  | @ -11346,7 +11386,7 @@ class PubServer(BaseHTTPRequestHandler): | |||
|         self.server.getreq_busy = False | ||||
|         return True | ||||
| 
 | ||||
|     def _show_individual_post(self, authorized: bool, | ||||
|     def _show_individual_post(self, ssml_getreq: bool, authorized: bool, | ||||
|                               calling_domain: str, referer_domain: str, | ||||
|                               path: str, | ||||
|                               base_dir: str, http_prefix: str, | ||||
|  | @ -11388,6 +11428,35 @@ class PubServer(BaseHTTPRequestHandler): | |||
|         if len(status_number) <= 10 or (not status_number.isdigit()): | ||||
|             return False | ||||
| 
 | ||||
|         if ssml_getreq: | ||||
|             ssml_filename = \ | ||||
|                 acct_dir(base_dir, nickname, domain) + '/outbox/' + \ | ||||
|                 http_prefix + ':##' + domain_full + '#users#' + nickname + \ | ||||
|                 '#statuses#' + status_number + '.ssml' | ||||
|             if not os.path.isfile(ssml_filename): | ||||
|                 ssml_filename = \ | ||||
|                     acct_dir(base_dir, nickname, domain) + '/postcache/' + \ | ||||
|                     http_prefix + ':##' + domain_full + '#users#' + \ | ||||
|                     nickname + '#statuses#' + status_number + '.ssml' | ||||
|             if not os.path.isfile(ssml_filename): | ||||
|                 self._404() | ||||
|                 return True | ||||
|             ssml_str = None | ||||
|             try: | ||||
|                 with open(ssml_filename, 'r') as fp_ssml: | ||||
|                     ssml_str = fp_ssml.read() | ||||
|             except OSError: | ||||
|                 pass | ||||
|             if ssml_str: | ||||
|                 msg = ssml_str.encode('utf-8') | ||||
|                 msglen = len(msg) | ||||
|                 self._set_headers('application/ssml+xml', msglen, | ||||
|                                   cookie, calling_domain, False) | ||||
|                 self._write(msg) | ||||
|                 return True | ||||
|             self._404() | ||||
|             return True | ||||
| 
 | ||||
|         post_filename = \ | ||||
|             acct_dir(base_dir, nickname, domain) + '/outbox/' + \ | ||||
|             http_prefix + ':##' + domain_full + '#users#' + nickname + \ | ||||
|  | @ -15387,12 +15456,15 @@ class PubServer(BaseHTTPRequestHandler): | |||
|                             '_GET', 'create session', | ||||
|                             self.server.debug) | ||||
| 
 | ||||
|         # is this a html request? | ||||
|         # is this a html/ssml/icalendar request? | ||||
|         html_getreq = False | ||||
|         ssml_getreq = False | ||||
|         icalendar_getreq = False | ||||
|         if self._has_accept(calling_domain): | ||||
|             if self._request_http(): | ||||
|                 html_getreq = True | ||||
|             elif self._request_ssml(): | ||||
|                 ssml_getreq = True | ||||
|             elif self._request_icalendar(): | ||||
|                 icalendar_getreq = True | ||||
|         else: | ||||
|  | @ -16184,7 +16256,8 @@ class PubServer(BaseHTTPRequestHandler): | |||
|                                  self.server.translate, | ||||
|                                  access_keys, | ||||
|                                  self.server.access_keys, | ||||
|                                  self.server.default_timeline) | ||||
|                                  self.server.default_timeline, | ||||
|                                  self.server.theme_name) | ||||
|             msg = msg.encode('utf-8') | ||||
|             msglen = len(msg) | ||||
|             self._login_headers('text/html', msglen, calling_domain) | ||||
|  | @ -17628,7 +17701,7 @@ class PubServer(BaseHTTPRequestHandler): | |||
|                             self.server.debug) | ||||
| 
 | ||||
|         # get an individual post from the path /@nickname/statusnumber | ||||
|         if self._show_individual_at_post(authorized, | ||||
|         if self._show_individual_at_post(ssml_getreq, authorized, | ||||
|                                          calling_domain, referer_domain, | ||||
|                                          self.path, | ||||
|                                          self.server.base_dir, | ||||
|  | @ -17773,7 +17846,7 @@ class PubServer(BaseHTTPRequestHandler): | |||
|         # get an individual post from the path | ||||
|         # /users/nickname/statuses/number | ||||
|         if '/statuses/' in self.path and users_in_path: | ||||
|             if self._show_individual_post(authorized, | ||||
|             if self._show_individual_post(ssml_getreq, authorized, | ||||
|                                           calling_domain, referer_domain, | ||||
|                                           self.path, | ||||
|                                           self.server.base_dir, | ||||
|  |  | |||
							
								
								
									
										38
									
								
								epicyon.py
								
								
								
								
							
							
						
						|  | @ -46,6 +46,7 @@ from session import create_session | |||
| from session import get_json | ||||
| from session import get_vcard | ||||
| from session import download_html | ||||
| from session import download_ssml | ||||
| from newswire import get_rss | ||||
| from filters import add_filter | ||||
| from filters import remove_filter | ||||
|  | @ -341,6 +342,8 @@ parser.add_argument('--xmlvcard', dest='xmlvcard', type=str, default=None, | |||
|                     'activitypub actor url') | ||||
| parser.add_argument('--json', dest='json', type=str, default=None, | ||||
|                     help='Show the json for a given activitypub url') | ||||
| parser.add_argument('--ssml', dest='ssml', type=str, default=None, | ||||
|                     help='Show the SSML for a given activitypub url') | ||||
| parser.add_argument('--htmlpost', dest='htmlpost', type=str, default=None, | ||||
|                     help='Show the html for a given activitypub url') | ||||
| parser.add_argument('--rss', dest='rss', type=str, default=None, | ||||
|  | @ -1040,6 +1043,31 @@ if args.json: | |||
|         pprint(test_json) | ||||
|     sys.exit() | ||||
| 
 | ||||
| if args.ssml: | ||||
|     session = create_session(None) | ||||
|     profile_str = 'https://www.w3.org/ns/activitystreams' | ||||
|     as_header = { | ||||
|         'Accept': 'application/ssml+xml; profile="' + profile_str + '"' | ||||
|     } | ||||
|     if not args.domain: | ||||
|         args.domain = get_config_param(base_dir, 'domain') | ||||
|     domain = '' | ||||
|     if args.domain: | ||||
|         domain = args.domain | ||||
|     signing_priv_key_pem = get_instance_actor_key(base_dir, domain) | ||||
|     if debug: | ||||
|         print('base_dir: ' + str(base_dir)) | ||||
|         if signing_priv_key_pem: | ||||
|             print('Obtained instance actor signing key') | ||||
|         else: | ||||
|             print('Did not obtain instance actor key for ' + domain) | ||||
|     test_ssml = download_ssml(signing_priv_key_pem, session, args.ssml, | ||||
|                               as_header, None, debug, __version__, | ||||
|                               http_prefix, domain) | ||||
|     if test_ssml: | ||||
|         print(str(test_ssml)) | ||||
|     sys.exit() | ||||
| 
 | ||||
| if args.vcard: | ||||
|     session = create_session(None) | ||||
|     if not args.domain: | ||||
|  | @ -1084,11 +1112,11 @@ if args.htmlpost: | |||
|             print('Obtained instance actor signing key') | ||||
|         else: | ||||
|             print('Did not obtain instance actor key for ' + domain) | ||||
|     testHtml = download_html(signing_priv_key_pem, session, args.htmlpost, | ||||
|                              as_header, None, debug, __version__, | ||||
|                              http_prefix, domain) | ||||
|     if testHtml: | ||||
|         print(testHtml) | ||||
|     test_html = download_html(signing_priv_key_pem, session, args.htmlpost, | ||||
|                               as_header, None, debug, __version__, | ||||
|                               http_prefix, domain) | ||||
|     if test_html: | ||||
|         print(test_html) | ||||
|     sys.exit() | ||||
| 
 | ||||
| # create cache for actors | ||||
|  |  | |||
							
								
								
									
										6
									
								
								inbox.py
								
								
								
								
							
							
						
						|  | @ -2282,7 +2282,8 @@ def _receive_announce(recent_posts_cache: {}, | |||
|                                        nickname, domain, domain_full, | ||||
|                                        post_json_object, person_cache, | ||||
|                                        translate, lookup_actor, | ||||
|                                        theme_name) | ||||
|                                        theme_name, system_language, | ||||
|                                        'inbox') | ||||
|                         try: | ||||
|                             with open(post_filename + '.tts', 'w+') as ttsfile: | ||||
|                                 ttsfile.write('\n') | ||||
|  | @ -4120,7 +4121,8 @@ def _inbox_after_initial(server, inbox_start_time, | |||
|                             update_speaker(base_dir, http_prefix, | ||||
|                                            nickname, domain, domain_full, | ||||
|                                            post_json_object, person_cache, | ||||
|                                            translate, None, theme_name) | ||||
|                                            translate, None, theme_name, | ||||
|                                            system_language, boxname) | ||||
|                             fitness_performance(inbox_start_time, | ||||
|                                                 server.fitness, | ||||
|                                                 'INBOX', 'update_speaker', | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ from delete import outbox_delete | |||
| from shares import outbox_share_upload | ||||
| from shares import outbox_undo_share_upload | ||||
| from webapp_post import individual_post_as_html | ||||
| from speaker import update_speaker | ||||
| 
 | ||||
| 
 | ||||
| def _person_receive_update_outbox(recent_posts_cache: {}, | ||||
|  | @ -400,6 +401,13 @@ def post_message_to_outbox(session, translate: {}, | |||
|             print('WARN: post not saved to outbox ' + outbox_name) | ||||
|             return False | ||||
| 
 | ||||
|         update_speaker(base_dir, http_prefix, | ||||
|                        post_to_nickname, domain, domain_full, | ||||
|                        message_json, person_cache, | ||||
|                        translate, message_json['actor'], | ||||
|                        theme, system_language, | ||||
|                        outbox_name) | ||||
| 
 | ||||
|         # save all instance blogs to the news actor | ||||
|         if post_to_nickname != 'news' and outbox_name == 'tlblogs': | ||||
|             if '/' in saved_filename: | ||||
|  |  | |||
							
								
								
									
										39
									
								
								session.py
								
								
								
								
							
							
						
						|  | @ -373,6 +373,45 @@ def download_html(signing_priv_key_pem: str, | |||
|                              None, quiet, debug, False) | ||||
| 
 | ||||
| 
 | ||||
| def download_ssml(signing_priv_key_pem: str, | ||||
|                   session, url: str, headers: {}, params: {}, debug: bool, | ||||
|                   version: str = __version__, http_prefix: str = 'https', | ||||
|                   domain: str = 'testdomain', | ||||
|                   timeout_sec: int = 20, quiet: bool = False) -> {}: | ||||
|     if not isinstance(url, str): | ||||
|         if debug and not quiet: | ||||
|             print('url: ' + str(url)) | ||||
|             print('ERROR: download_ssml failed, url should be a string') | ||||
|         return None | ||||
|     session_params = {} | ||||
|     session_headers = {} | ||||
|     if headers: | ||||
|         session_headers = headers | ||||
|     if params: | ||||
|         session_params = params | ||||
|     session_headers['Accept'] = 'application/ssml+xml' | ||||
|     session_headers['User-Agent'] = 'Epicyon/' + version | ||||
|     if domain: | ||||
|         session_headers['User-Agent'] += \ | ||||
|             '; +' + http_prefix + '://' + domain + '/' | ||||
|     if not session: | ||||
|         if not quiet: | ||||
|             print('WARN: download_ssml failed, no session specified') | ||||
|         return None | ||||
| 
 | ||||
|     if debug: | ||||
|         HTTPConnection.debuglevel = 1 | ||||
| 
 | ||||
|     if signing_priv_key_pem: | ||||
|         return _get_json_signed(session, url, domain, | ||||
|                                 session_headers, session_params, | ||||
|                                 timeout_sec, signing_priv_key_pem, | ||||
|                                 quiet, debug) | ||||
|     return _get_json_request(session, url, domain, session_headers, | ||||
|                              session_params, timeout_sec, | ||||
|                              None, quiet, debug, False) | ||||
| 
 | ||||
| 
 | ||||
| def _set_user_agent(session, http_prefix: str, domain_full: str) -> None: | ||||
|     """Sets the user agent | ||||
|     """ | ||||
|  |  | |||
							
								
								
									
										52
									
								
								speaker.py
								
								
								
								
							
							
						
						|  | @ -11,6 +11,7 @@ import os | |||
| import html | ||||
| import random | ||||
| import urllib.parse | ||||
| from utils import get_cached_post_filename | ||||
| from utils import remove_id_ending | ||||
| from utils import is_dm | ||||
| from utils import is_reply | ||||
|  | @ -301,9 +302,11 @@ def _speaker_endpoint_json(display_name: str, summary: str, | |||
|     return speaker_json | ||||
| 
 | ||||
| 
 | ||||
| def _ssm_lheader(system_language: str, instance_title: str) -> str: | ||||
| def _ssml_header(system_language: str, box_name: str, summary: str) -> str: | ||||
|     """Returns a header for an SSML document | ||||
|     """ | ||||
|     if summary: | ||||
|         summary = ': ' + summary | ||||
|     return '<?xml version="1.0"?>\n' + \ | ||||
|         '<speak xmlns="http://www.w3.org/2001/10/synthesis"\n' + \ | ||||
|         '       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' + \ | ||||
|  | @ -312,15 +315,14 @@ def _ssm_lheader(system_language: str, instance_title: str) -> str: | |||
|         '       version="1.1">\n' + \ | ||||
|         '  <metadata>\n' + \ | ||||
|         '    <dc:title xml:lang="' + system_language + '">' + \ | ||||
|         instance_title + ' inbox</dc:title>\n' + \ | ||||
|         box_name + summary + '</dc:title>\n' + \ | ||||
|         '  </metadata>\n' | ||||
| 
 | ||||
| 
 | ||||
| def _speaker_endpoint_ssml(display_name: str, summary: str, | ||||
|                            content: str, image_description: str, | ||||
|                            links: [], language: str, | ||||
|                            instance_title: str, | ||||
|                            gender: str) -> str: | ||||
|                            gender: str, box_name: str) -> str: | ||||
|     """Returns an SSML endpoint for the TTS speaker | ||||
|     https://en.wikipedia.org/wiki/Speech_Synthesis_Markup_Language | ||||
|     https://www.w3.org/TR/speech-synthesis/ | ||||
|  | @ -342,7 +344,9 @@ def _speaker_endpoint_ssml(display_name: str, summary: str, | |||
| 
 | ||||
|     content = _add_ssm_lemphasis(content) | ||||
|     voice_params = 'name="' + display_name + '" gender="' + gender + '"' | ||||
|     return _ssm_lheader(lang_short, instance_title) + \ | ||||
|     if summary is None: | ||||
|         summary = '' | ||||
|     return _ssml_header(lang_short, box_name, summary) + \ | ||||
|         '  <p>\n' + \ | ||||
|         '    <s xml:lang="' + language + '">\n' + \ | ||||
|         '      <voice ' + voice_params + '>\n' + \ | ||||
|  | @ -356,7 +360,6 @@ def _speaker_endpoint_ssml(display_name: str, summary: str, | |||
| def get_ssml_box(base_dir: str, path: str, | ||||
|                  domain: str, | ||||
|                  system_language: str, | ||||
|                  instance_title: str, | ||||
|                  box_name: str) -> str: | ||||
|     """Returns SSML for the given timeline | ||||
|     """ | ||||
|  | @ -379,7 +382,7 @@ def get_ssml_box(base_dir: str, path: str, | |||
|                                   speaker_json['imageDescription'], | ||||
|                                   speaker_json['detectedLinks'], | ||||
|                                   system_language, | ||||
|                                   instance_title, gender) | ||||
|                                   gender, box_name) | ||||
| 
 | ||||
| 
 | ||||
| def speakable_text(base_dir: str, content: str, translate: {}) -> (str, []): | ||||
|  | @ -544,7 +547,8 @@ def update_speaker(base_dir: str, http_prefix: str, | |||
|                    nickname: str, domain: str, domain_full: str, | ||||
|                    post_json_object: {}, person_cache: {}, | ||||
|                    translate: {}, announcing_actor: str, | ||||
|                    theme_name: str) -> None: | ||||
|                    theme_name: str, | ||||
|                    system_language: str, box_name: str) -> None: | ||||
|     """ Generates a json file which can be used for TTS announcement | ||||
|     of incoming inbox posts | ||||
|     """ | ||||
|  | @ -554,5 +558,35 @@ def update_speaker(base_dir: str, http_prefix: str, | |||
|                               post_json_object, person_cache, | ||||
|                               translate, announcing_actor, | ||||
|                               theme_name) | ||||
|     speaker_filename = acct_dir(base_dir, nickname, domain) + '/speaker.json' | ||||
|     if not speaker_json: | ||||
|         return | ||||
|     account_dir = acct_dir(base_dir, nickname, domain) | ||||
|     speaker_filename = account_dir + '/speaker.json' | ||||
|     save_json(speaker_json, speaker_filename) | ||||
| 
 | ||||
|     # save the ssml | ||||
|     cached_ssml_filename = \ | ||||
|         get_cached_post_filename(base_dir, nickname, | ||||
|                                  domain, post_json_object) | ||||
|     if not cached_ssml_filename: | ||||
|         return | ||||
|     cached_ssml_filename = cached_ssml_filename.replace('.html', '.ssml') | ||||
|     if box_name == 'outbox': | ||||
|         cached_ssml_filename = \ | ||||
|             cached_ssml_filename.replace('/postcache/', '/outbox/') | ||||
|     gender = None | ||||
|     if speaker_json.get('gender'): | ||||
|         gender = speaker_json['gender'] | ||||
|     ssml_str = \ | ||||
|         _speaker_endpoint_ssml(speaker_json['name'], | ||||
|                                speaker_json['summary'], | ||||
|                                speaker_json['say'], | ||||
|                                speaker_json['imageDescription'], | ||||
|                                speaker_json['detectedLinks'], | ||||
|                                system_language, | ||||
|                                gender, box_name) | ||||
|     try: | ||||
|         with open(cached_ssml_filename, 'w+') as fp_ssml: | ||||
|             fp_ssml.write(ssml_str) | ||||
|     except OSError: | ||||
|         print('EX: unable to write ssml ' + cached_ssml_filename) | ||||
|  |  | |||
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.7 KiB | 
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.1 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.1 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.0 KiB | 
| Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.0 KiB | 
| Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 6.5 KiB | 
| Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.9 KiB | 
| Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.0 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.7 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.6 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.9 KiB | 
| After Width: | Height: | Size: 5.2 KiB | 
| Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 8.1 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.6 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 8.0 KiB | 
| Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.8 KiB | 
| Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 7.2 KiB | 
| Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.1 KiB | 
| Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 8.2 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.1 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.7 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB | 
| Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.0 KiB | 
| Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 6.3 KiB | 
| Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 6.2 KiB | 
| Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 5.5 KiB | 
| Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 7.2 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.7 KiB | 
| Before Width: | Height: | Size: 1019 B After Width: | Height: | Size: 7.3 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.0 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 5.8 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.2 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.2 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.7 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.4 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 6.8 KiB | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 8.1 KiB | 
							
								
								
									
										19
									
								
								utils.py
								
								
								
								
							
							
						
						|  | @ -1745,6 +1745,25 @@ def delete_cached_html(base_dir: str, nickname: str, domain: str, | |||
|                       'unable to delete cached post file ' + | ||||
|                       str(cached_post_filename)) | ||||
| 
 | ||||
|         cached_post_filename = cached_post_filename.replace('.html', '.ssml') | ||||
|         if os.path.isfile(cached_post_filename): | ||||
|             try: | ||||
|                 os.remove(cached_post_filename) | ||||
|             except OSError: | ||||
|                 print('EX: delete_cached_html ' + | ||||
|                       'unable to delete cached ssml post file ' + | ||||
|                       str(cached_post_filename)) | ||||
| 
 | ||||
|         cached_post_filename = \ | ||||
|             cached_post_filename.replace('/postcache/', '/outbox/') | ||||
|         if os.path.isfile(cached_post_filename): | ||||
|             try: | ||||
|                 os.remove(cached_post_filename) | ||||
|             except OSError: | ||||
|                 print('EX: delete_cached_html ' + | ||||
|                       'unable to delete cached outbox ssml post file ' + | ||||
|                       str(cached_post_filename)) | ||||
| 
 | ||||
| 
 | ||||
| def _delete_hashtags_on_post(base_dir: str, post_json_object: {}) -> None: | ||||
|     """Removes hashtags when a post is deleted | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ from utils import get_config_param | |||
| from utils import acct_dir | ||||
| from webapp_utils import html_header_with_external_style | ||||
| from webapp_utils import html_footer | ||||
| from webapp_utils import get_banner_file | ||||
| 
 | ||||
| 
 | ||||
| def load_access_keys_for_accounts(base_dir: str, key_shortcuts: {}, | ||||
|  | @ -43,7 +44,7 @@ def html_access_keys(css_cache: {}, base_dir: str, | |||
|                      nickname: str, domain: str, | ||||
|                      translate: {}, access_keys: {}, | ||||
|                      default_access_keys: {}, | ||||
|                      default_timeline: str) -> str: | ||||
|                      default_timeline: str, theme: str) -> str: | ||||
|     """Show and edit key shortcuts | ||||
|     """ | ||||
|     access_keys_filename = \ | ||||
|  | @ -53,6 +54,9 @@ def html_access_keys(css_cache: {}, base_dir: str, | |||
|         if access_keys_from_file: | ||||
|             access_keys = access_keys_from_file | ||||
| 
 | ||||
|     timeline_key = access_keys['menuTimeline'] | ||||
|     submit_key = access_keys['submitButton'] | ||||
| 
 | ||||
|     access_keys_form = '' | ||||
|     css_filename = base_dir + '/epicyon-profile.css' | ||||
|     if os.path.isfile(base_dir + '/epicyon.css'): | ||||
|  | @ -62,6 +66,20 @@ def html_access_keys(css_cache: {}, base_dir: str, | |||
|         get_config_param(base_dir, 'instanceTitle') | ||||
|     access_keys_form = \ | ||||
|         html_header_with_external_style(css_filename, instance_title, None) | ||||
| 
 | ||||
|     access_keys_form += \ | ||||
|         '<header>\n' + \ | ||||
|         '<a href="/users/' + nickname + '/' + \ | ||||
|         default_timeline + '" title="' + \ | ||||
|         translate['Switch to timeline view'] + '" alt="' + \ | ||||
|         translate['Switch to timeline view'] + '">\n' | ||||
|     banner_file, _ = \ | ||||
|         get_banner_file(base_dir, nickname, domain, theme) | ||||
|     access_keys_form += '<img loading="lazy" decoding="async" ' + \ | ||||
|         'class="timeline-banner" alt="" ' + \ | ||||
|         'src="/users/' + nickname + '/' + banner_file + '" /></a>\n' + \ | ||||
|         '</header>\n' | ||||
| 
 | ||||
|     access_keys_form += '<div class="container">\n' | ||||
| 
 | ||||
|     access_keys_form += \ | ||||
|  | @ -73,8 +91,6 @@ def html_access_keys(css_cache: {}, base_dir: str, | |||
|     access_keys_form += '  <form method="POST" action="' + \ | ||||
|         '/users/' + nickname + '/changeAccessKeys">\n' | ||||
| 
 | ||||
|     timeline_key = access_keys['menuTimeline'] | ||||
|     submit_key = access_keys['submitButton'] | ||||
|     access_keys_form += \ | ||||
|         '    <center>\n' + \ | ||||
|         '    <button type="submit" class="button" ' + \ | ||||
|  |  | |||
|  | @ -57,28 +57,42 @@ def header_buttons_timeline(default_timeline: str, | |||
|     if default_timeline == 'tlmedia': | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + '/tlmedia" tabindex="-1" ' + \ | ||||
|             'accesskey="' + access_keys['menuMedia'] + '"' + \ | ||||
|             'accesskey="' + access_keys['menuMedia'] + '"' | ||||
|         if box_name == 'tlmedia': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="' + \ | ||||
|             mediaButton + '"><span>' + translate['Media'] + \ | ||||
|             '</span></button></a>' | ||||
|     elif default_timeline == 'tlblogs': | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + \ | ||||
|             '/tlblogs" tabindex="-1"><button class="' + \ | ||||
|             '/tlblogs" tabindex="-1"' | ||||
|         if box_name == 'tlblogs': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="' + \ | ||||
|             blogs_button + '"><span>' + translate['Blogs'] + \ | ||||
|             '</span></button></a>' | ||||
|     elif default_timeline == 'tlfeatures': | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + \ | ||||
|             '/tlfeatures" tabindex="-1"><button class="' + \ | ||||
|             '/tlfeatures" tabindex="-1"' | ||||
|         if box_name == 'tlfeatures': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="' + \ | ||||
|             features_button + '"><span>' + translate['Features'] + \ | ||||
|             '</span></button></a>' | ||||
|     else: | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + \ | ||||
|             '/inbox" tabindex="-1"><button class="' + \ | ||||
|             inbox_button + '"><span>' + \ | ||||
|             translate['Inbox'] + '</span></button></a>' | ||||
|             inbox_button + '"' | ||||
|         if box_name == 'inbox': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><span>' + translate['Inbox'] + '</span></button></a>' | ||||
| 
 | ||||
|     # if this is a news instance and we are viewing the news timeline | ||||
|     features_header = False | ||||
|  | @ -87,8 +101,11 @@ def header_buttons_timeline(default_timeline: str, | |||
| 
 | ||||
|     if not features_header: | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + \ | ||||
|             '/dm" tabindex="-1"><button class="' + dm_button + \ | ||||
|             '<a href="' + users_path + '/dm" tabindex="-1"' | ||||
|         if box_name == 'dm': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="' + dm_button + \ | ||||
|             '"><span>' + html_highlight_label(translate['DM'], new_dm) + \ | ||||
|             '</span></button></a>' | ||||
| 
 | ||||
|  | @ -96,8 +113,11 @@ def header_buttons_timeline(default_timeline: str, | |||
|             acct_dir(base_dir, nickname, domain) + '/tlreplies.index' | ||||
|         if os.path.isfile(replies_index_filename): | ||||
|             tl_str += \ | ||||
|                 '<a href="' + users_path + '/tlreplies" tabindex="-1">' + \ | ||||
|                 '<button class="' + replies_button + '"><span>' + \ | ||||
|                 '<a href="' + users_path + '/tlreplies" tabindex="-1"' | ||||
|             if box_name == 'tlreplies': | ||||
|                 tl_str += ' aria-current="location"' | ||||
|             tl_str += \ | ||||
|                 '><button class="' + replies_button + '"><span>' + \ | ||||
|                 html_highlight_label(translate['Replies'], new_reply) + \ | ||||
|                 '</span></button></a>' | ||||
| 
 | ||||
|  | @ -106,15 +126,22 @@ def header_buttons_timeline(default_timeline: str, | |||
|         if not minimal and not features_header: | ||||
|             tl_str += \ | ||||
|                 '<a href="' + users_path + '/tlmedia" tabindex="-1" ' + \ | ||||
|                 'accesskey="' + access_keys['menuMedia'] + '">' + \ | ||||
|                 '<button class="' + \ | ||||
|                 'accesskey="' + access_keys['menuMedia'] + '"' | ||||
|             if box_name == 'tlmedia': | ||||
|                 tl_str += ' aria-current="location"' | ||||
|             tl_str += \ | ||||
|                 '><button class="' + \ | ||||
|                 mediaButton + '"><span>' + translate['Media'] + \ | ||||
|                 '</span></button></a>' | ||||
|     else: | ||||
|         if not minimal: | ||||
|             tl_str += \ | ||||
|                 '<a href="' + users_path + \ | ||||
|                 '/inbox" tabindex="-1"><button class="' + \ | ||||
|                 '/inbox" tabindex="-1"' | ||||
|             if box_name == 'inbox': | ||||
|                 tl_str += ' aria-current="location"' | ||||
|             tl_str += \ | ||||
|                 '><button class="' + \ | ||||
|                 inbox_button + '"><span>' + translate['Inbox'] + \ | ||||
|                 '</span></button></a>' | ||||
| 
 | ||||
|  | @ -128,14 +155,22 @@ def header_buttons_timeline(default_timeline: str, | |||
|                     title_str = translate['Article'] | ||||
|                 tl_str += \ | ||||
|                     '<a href="' + users_path + \ | ||||
|                     '/tlblogs" tabindex="-1"><button class="' + \ | ||||
|                     '/tlblogs" tabindex="-1"' | ||||
|                 if box_name == 'tlblogs': | ||||
|                     tl_str += ' aria-current="location"' | ||||
|                 tl_str += \ | ||||
|                     '><button class="' + \ | ||||
|                     blogs_button + '"><span>' + title_str + \ | ||||
|                     '</span></button></a>' | ||||
|         else: | ||||
|             if not minimal: | ||||
|                 tl_str += \ | ||||
|                     '<a href="' + users_path + \ | ||||
|                     '/inbox" tabindex="-1"><button class="' + \ | ||||
|                     '/inbox" tabindex="-1"' | ||||
|                 if box_name == 'inbox': | ||||
|                     tl_str += ' aria-current="location"' | ||||
|                 tl_str += \ | ||||
|                     '><button class="' + \ | ||||
|                     inbox_button + '"><span>' + translate['Inbox'] + \ | ||||
|                     '</span></button></a>' | ||||
| 
 | ||||
|  | @ -145,7 +180,11 @@ def header_buttons_timeline(default_timeline: str, | |||
|         if not features_header: | ||||
|             tl_str += \ | ||||
|                 '<a href="' + users_path + \ | ||||
|                 '/inbox" tabindex="-1"><button class="' + \ | ||||
|                 '/inbox" tabindex="-1"' | ||||
|             if box_name == 'inbox': | ||||
|                 tl_str += ' aria-current="location"' | ||||
|             tl_str += \ | ||||
|                 '><button class="' + \ | ||||
|                 inbox_button + '"><span>' + translate['Inbox'] + \ | ||||
|                 '</span></button></a>' | ||||
| 
 | ||||
|  | @ -205,7 +244,11 @@ def header_buttons_timeline(default_timeline: str, | |||
|     if not features_header: | ||||
|         # button for the outbox | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + '/outbox"><button class="' + \ | ||||
|             '<a href="' + users_path + '/outbox"' | ||||
|         if box_name == 'outbox': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="' + \ | ||||
|             sent_button + '" tabindex="-1"><span>' + translate['Sent'] + \ | ||||
|             '</span></button></a>' | ||||
| 
 | ||||
|  | @ -278,8 +321,11 @@ def header_buttons_timeline(default_timeline: str, | |||
| 
 | ||||
|     if features_header: | ||||
|         tl_str += \ | ||||
|             '<a href="' + users_path + '/inbox" tabindex="-1">' + \ | ||||
|             '<button class="button">' + \ | ||||
|             '<a href="' + users_path + '/inbox" tabindex="-1"' | ||||
|         if box_name == 'inbox': | ||||
|             tl_str += ' aria-current="location"' | ||||
|         tl_str += \ | ||||
|             '><button class="button">' + \ | ||||
|             '<span>' + translate['User'] + '</span></button></a>' | ||||
| 
 | ||||
|     # the newswire button to show right column links | ||||
|  |  | |||
|  | @ -1698,7 +1698,8 @@ def individual_post_as_html(signing_priv_key_pem: str, | |||
|                                        nickname, domain, domain_full, | ||||
|                                        post_json_object, person_cache, | ||||
|                                        translate, post_json_object['actor'], | ||||
|                                        theme_name) | ||||
|                                        theme_name, system_language, | ||||
|                                        box_name) | ||||
|                         with open(announce_filename + '.tts', 'w+') as ttsfile: | ||||
|                             ttsfile.write('\n') | ||||
| 
 | ||||
|  |  | |||
|  | @ -419,13 +419,21 @@ def _page_number_buttons(users_path: str, box_name: str, | |||
|     for page in range(min_page_number, max_page_number): | ||||
|         if num_str: | ||||
|             num_str += ' ⸻ ' | ||||
|         aria_page_str = '' | ||||
|         page_str = str(page) | ||||
|         if page == page_number: | ||||
|             page_str = '<mark>' + str(page) + '</mark>' | ||||
|             aria_page_str = ' aria-current="true"' | ||||
|         num_str += \ | ||||
|             '<a href="' + users_path + '/' + box_name + '?page=' + \ | ||||
|             str(page) + '" class="pageslist">' + page_str + '</a>' | ||||
|     return '<center>' + num_str + '</center>' | ||||
|             str(page) + '" class="pageslist" ' + \ | ||||
|             'aria-label="Current Page, Page ' + str(page) + \ | ||||
|             '"' + aria_page_str + '>' + page_str + '</a>' | ||||
|     return '<center>\n' + \ | ||||
|         '  <nav role="navigation" aria-label="Pagination Navigation">\n' + \ | ||||
|         '    ' + num_str + '\n' + \ | ||||
|         '  </nav>\n' + \ | ||||
|         '</center>\n' | ||||
| 
 | ||||
| 
 | ||||
| def html_timeline(css_cache: {}, default_timeline: str, | ||||
|  | @ -656,8 +664,11 @@ def html_timeline(css_cache: {}, default_timeline: str, | |||
|     moderation_button_str = '' | ||||
|     if moderator and not minimal: | ||||
|         moderation_button_str = \ | ||||
|             '<a href="' + users_path + \ | ||||
|             '/moderation"><button class="' + \ | ||||
|             '<a href="' + users_path + '/moderation"' | ||||
|         if box_name == 'moderation': | ||||
|             moderation_button_str += ' aria-current="location"' | ||||
|         moderation_button_str += \ | ||||
|             '><button class="' + \ | ||||
|             moderation_button + '"><span>' + \ | ||||
|             html_highlight_label(translate['Mod'], new_report) + \ | ||||
|             ' </span></button></a>' | ||||
|  | @ -669,19 +680,30 @@ def html_timeline(css_cache: {}, default_timeline: str, | |||
|     events_button_str = '' | ||||
|     if not minimal: | ||||
|         shares_button_str = \ | ||||
|             '<a href="' + users_path + '/tlshares"><button class="' + \ | ||||
|             shares_button + '"><span>' + \ | ||||
|             '<a href="' + users_path + '/tlshares"' | ||||
|         if box_name == 'tlshares': | ||||
|             shares_button_str += ' aria-current="location"' | ||||
|         shares_button_str += \ | ||||
|             '><button class="' + shares_button + '"><span>' + \ | ||||
|             html_highlight_label(translate['Shares'], new_share) + \ | ||||
|             '</span></button></a>' | ||||
| 
 | ||||
|         wanted_button_str = \ | ||||
|             '<a href="' + users_path + '/tlwanted"><button class="' + \ | ||||
|             wanted_button + '"><span>' + \ | ||||
|             wanted_button + '"' | ||||
|         if box_name == 'tlwanted': | ||||
|             wanted_button_str += ' aria-current="location"' | ||||
|         wanted_button_str += \ | ||||
|             '><span>' + \ | ||||
|             html_highlight_label(translate['Wanted'], new_wanted) + \ | ||||
|             '</span></button></a>' | ||||
| 
 | ||||
|         bookmarks_button_str = \ | ||||
|             '<a href="' + users_path + '/tlbookmarks"><button class="' + \ | ||||
|             '<a href="' + users_path + '/tlbookmarks"' | ||||
|         if box_name == 'tlbookmarks': | ||||
|             bookmarks_button_str += ' aria-current="location"' | ||||
|         bookmarks_button_str += \ | ||||
|             '><button class="' + \ | ||||
|             bookmarks_button + '"><span>' + translate['Bookmarks'] + \ | ||||
|             '</span></button></a>' | ||||
| 
 | ||||
|  |  | |||