From a21c73976238b12e896fa2d8b78d44bfeacd487b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 10:31:31 +0000 Subject: [PATCH 01/13] Dropdown colors --- theme/hacker/theme.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/theme/hacker/theme.json b/theme/hacker/theme.json index 7c2b120dd..bb121213c 100644 --- a/theme/hacker/theme.json +++ b/theme/hacker/theme.json @@ -1,4 +1,8 @@ { + "dropdown-fg-color": "#dddddd", + "dropdown-bg-color": "#111", + "dropdown-bg-color-hover": "#035103", + "dropdown-fg-color-hover": "#dddddd", "newswire-publish-icon": "True", "full-width-timeline-buttons": "False", "icons-as-buttons": "False", From 555a40a91bed1e2cb0eb787331ba8b793b8bc628 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 11:13:31 +0000 Subject: [PATCH 02/13] Terms of service in markdown format --- daemon.py | 2 +- default_tos.md | 48 +++++++++++++++++++++++++++++++++++++++++++ webapp_column_left.py | 2 +- webapp_tos.py | 13 ++++++------ 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 default_tos.md diff --git a/daemon.py b/daemon.py index fd7aef23d..e5e050d3b 100644 --- a/daemon.py +++ b/daemon.py @@ -3316,7 +3316,7 @@ class PubServer(BaseHTTPRequestHandler): linksFilename = baseDir + '/accounts/links.txt' aboutFilename = baseDir + '/accounts/about.txt' - TOSFilename = baseDir + '/accounts/tos.txt' + TOSFilename = baseDir + '/accounts/tos.md' # extract all of the text fields into a dict fields = \ diff --git a/default_tos.md b/default_tos.md new file mode 100644 index 000000000..218cc0b50 --- /dev/null +++ b/default_tos.md @@ -0,0 +1,48 @@ +# Terms of Service +### Data Collected +Your username and a hash of your password, any posts you make and a list of accounts which you follow. The admin of the site does not know your password and it is not stored in plaintext anywhere. + +There is a quota on the number of posts retained by this instance for each account. Older posts will be removed when the limit is reached. Anything you post here should be considered ephemeral and you should keep a separate personal copy of them if you wish to retain a permanent archive. + +No IP addresses are logged. + +Posts can be removed on request if there is sufficient justification, but the nature of ActivityPub means that deletion of data federated to other instances cannot be guaranteed. + +### Content Policy +This instance will not host content containing sexism, racism, casteism, homophobia, transphobia, misogyny, antisemitism or other forms of bigotry or discrimination on the basis of nationality or immigration status. Claims that transgressions of this type were intended to be "ironic" will be treated as a terms of service violation. + +Even if not conspicuously discriminatory, expressions of support for organizations with discrminatory agendas are not permitted on this instance. These include, but are not limited to, racial supremacist groups, the redpill/incel movement and anti-LGBT or anti-immigrant campaigns. + +Depictions of injury, death or medical procedures are not permitted. + +Violent or abusive content will be subject to moderation and is likely to be removed. + +Content of a sexual nature may be published providing that only consenting adults (aged 18 or over) are depicted and an appropriate content warning message is added. Posting sexual content without a content warning is a terms of service violation. Sexual content is defined both as photographs of real people and also artistic or fictional depictions, edited/generated photos or narratives. + +Moderators rely upon your reports. Don't assume that something of concern has already been reported. It's better for there to be duplicate reports than for something potentially damaging to go unreported. + +Content found to be non-compliant with this policy will be removed and any accounts on this instance producing, repeating or linking to such content will be deleted typically without prior notification. + +### Federation Policy + +In a proactive effort to avoid the classic fate of *"embrace, extend, extinguish"* this system will block any instance launched, acquired or funded by Alphabet, Facebook, Twitter, Microsoft, Apple, Amazon, Elsevier or other monopolistic Silicon Valley companies. + +This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible. + +### Use of User Generated Content for Research + +Data may not be "scraped" or otherwise obtained from this instance and used for academic research or cited within research publications without the prior written permission of the administrator. Financial remedy will be sought through the courts from any researcher publishing data obtained from this instance without consent. + +### Commercial Use + +Commercial use of original content on this instance is strictly forbidden without the prior written permission of individual account holders. The instance administrator does not hold copyright on any original content posted by account holders. Publication or federation of content does not imply permission for commercial use. + +Commercial use includes the harvesting of data to create products which are then sold, such as statistics, business reports or machine learning models. + +### Copyrights + +Epicyon is licensed under [GNU AGPL version 3](https://www.gnu.org/licenses/agpl-3.0-standalone.html) + +Emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0) + +Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/webapp_column_left.py b/webapp_column_left.py index c83a02c21..c2fc8da78 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -430,7 +430,7 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, editLinksForm += \ '' - TOSFilename = baseDir + '/accounts/tos.txt' + TOSFilename = baseDir + '/accounts/tos.md' TOSStr = '' if os.path.isfile(TOSFilename): with open(TOSFilename, 'r') as fp: diff --git a/webapp_tos.py b/webapp_tos.py index da59e0632..24d8badb0 100644 --- a/webapp_tos.py +++ b/webapp_tos.py @@ -11,6 +11,7 @@ from shutil import copyfile from utils import getConfigParam from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter +from webapp_utils import markdownToHtml def htmlTermsOfService(cssCache: {}, baseDir: str, @@ -18,9 +19,9 @@ def htmlTermsOfService(cssCache: {}, baseDir: str, """Show the terms of service screen """ adminNickname = getConfigParam(baseDir, 'admin') - if not os.path.isfile(baseDir + '/accounts/tos.txt'): - copyfile(baseDir + '/default_tos.txt', - baseDir + '/accounts/tos.txt') + if not os.path.isfile(baseDir + '/accounts/tos.md'): + copyfile(baseDir + '/default_tos.md', + baseDir + '/accounts/tos.md') if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): @@ -28,9 +29,9 @@ def htmlTermsOfService(cssCache: {}, baseDir: str, baseDir + '/accounts/login-background.jpg') TOSText = 'Terms of Service go here.' - if os.path.isfile(baseDir + '/accounts/tos.txt'): - with open(baseDir + '/accounts/tos.txt', 'r') as file: - TOSText = file.read() + if os.path.isfile(baseDir + '/accounts/tos.md'): + with open(baseDir + '/accounts/tos.md', 'r') as file: + TOSText = markdownToHtml(file.read()) TOSForm = '' cssFilename = baseDir + '/epicyon-profile.css' From 768f2d4fe9cd5cb0923e628627142060cb2bf1f8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 11:15:48 +0000 Subject: [PATCH 03/13] Remove lines --- default_tos.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/default_tos.md b/default_tos.md index 218cc0b50..eb28005c6 100644 --- a/default_tos.md +++ b/default_tos.md @@ -24,23 +24,19 @@ Moderators rely upon your reports. Don't assume that something of concern has al Content found to be non-compliant with this policy will be removed and any accounts on this instance producing, repeating or linking to such content will be deleted typically without prior notification. ### Federation Policy - In a proactive effort to avoid the classic fate of *"embrace, extend, extinguish"* this system will block any instance launched, acquired or funded by Alphabet, Facebook, Twitter, Microsoft, Apple, Amazon, Elsevier or other monopolistic Silicon Valley companies. This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible. ### Use of User Generated Content for Research - Data may not be "scraped" or otherwise obtained from this instance and used for academic research or cited within research publications without the prior written permission of the administrator. Financial remedy will be sought through the courts from any researcher publishing data obtained from this instance without consent. ### Commercial Use - Commercial use of original content on this instance is strictly forbidden without the prior written permission of individual account holders. The instance administrator does not hold copyright on any original content posted by account holders. Publication or federation of content does not imply permission for commercial use. Commercial use includes the harvesting of data to create products which are then sold, such as statistics, business reports or machine learning models. ### Copyrights - Epicyon is licensed under [GNU AGPL version 3](https://www.gnu.org/licenses/agpl-3.0-standalone.html) Emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0) From 3b227aa52050249bf58175c7f3f66b5d5016ba93 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 11:40:49 +0000 Subject: [PATCH 04/13] Instance about in markdown format --- daemon.py | 2 +- webapp_about.py | 10 +++++----- webapp_column_left.py | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/daemon.py b/daemon.py index e5e050d3b..f50ca9d85 100644 --- a/daemon.py +++ b/daemon.py @@ -3315,7 +3315,7 @@ class PubServer(BaseHTTPRequestHandler): return linksFilename = baseDir + '/accounts/links.txt' - aboutFilename = baseDir + '/accounts/about.txt' + aboutFilename = baseDir + '/accounts/about.md' TOSFilename = baseDir + '/accounts/tos.md' # extract all of the text fields into a dict diff --git a/webapp_about.py b/webapp_about.py index 9493c1b59..d7a9c35cb 100644 --- a/webapp_about.py +++ b/webapp_about.py @@ -18,9 +18,9 @@ def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, """Show the about screen """ adminNickname = getConfigParam(baseDir, 'admin') - if not os.path.isfile(baseDir + '/accounts/about.txt'): - copyfile(baseDir + '/default_about.txt', - baseDir + '/accounts/about.txt') + if not os.path.isfile(baseDir + '/accounts/about.md'): + copyfile(baseDir + '/default_about.md', + baseDir + '/accounts/about.md') if os.path.isfile(baseDir + '/accounts/login-background-custom.jpg'): if not os.path.isfile(baseDir + '/accounts/login-background.jpg'): @@ -28,8 +28,8 @@ def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, baseDir + '/accounts/login-background.jpg') aboutText = 'Information about this instance goes here.' - if os.path.isfile(baseDir + '/accounts/about.txt'): - with open(baseDir + '/accounts/about.txt', 'r') as aboutFile: + if os.path.isfile(baseDir + '/accounts/about.md'): + with open(baseDir + '/accounts/about.md', 'r') as aboutFile: aboutText = aboutFile.read() aboutForm = '' diff --git a/webapp_column_left.py b/webapp_column_left.py index c2fc8da78..a7d143102 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -17,6 +17,7 @@ from webapp_utils import headerButtonsFrontScreen from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getBannerFile +from webapp_utils import markdownToHtml def _linksExist(baseDir: str) -> bool: @@ -411,11 +412,11 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, adminNickname = getConfigParam(baseDir, 'admin') if adminNickname: if nickname == adminNickname: - aboutFilename = baseDir + '/accounts/about.txt' + aboutFilename = baseDir + '/accounts/about.md' aboutStr = '' if os.path.isfile(aboutFilename): with open(aboutFilename, 'r') as fp: - aboutStr = fp.read() + aboutStr = markdownToHtml(fp.read()) editLinksForm += \ '
' From d70895c0cf1a36a1d09d0451c62f6ec420ad8f73 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 11:42:52 +0000 Subject: [PATCH 05/13] Default about text --- default_about.md | 9 +++++++++ default_about.txt | 13 ------------ default_tos.txt | 51 ----------------------------------------------- 3 files changed, 9 insertions(+), 64 deletions(-) create mode 100644 default_about.md delete mode 100644 default_about.txt delete mode 100644 default_tos.txt diff --git a/default_about.md b/default_about.md new file mode 100644 index 000000000..4d03b1056 --- /dev/null +++ b/default_about.md @@ -0,0 +1,9 @@ +# About this Instance +### Origin Story +How your instance began. + +### Lore +Customs and rituals. + +### Epic Tales +Heroic deeds and dastardly foes. diff --git a/default_about.txt b/default_about.txt deleted file mode 100644 index a1e535820..000000000 --- a/default_about.txt +++ /dev/null @@ -1,13 +0,0 @@ -

About this Instance

- -

Origin Story

- -

How your instance began.

- -

Lore

- -

Customs and rituals.

- -

Epic Tales

- -

Heroic deeds and dastardly foes.

diff --git a/default_tos.txt b/default_tos.txt deleted file mode 100644 index b74391ab7..000000000 --- a/default_tos.txt +++ /dev/null @@ -1,51 +0,0 @@ -

Terms of Service

- -

Data Collected

- -

Your username and a hash of your password, any posts you make and a list of accounts which you follow. The admin of the site does not know your password and it is not stored in plaintext anywhere.

- -

There is a quota on the number of posts retained by this instance for each account. Older posts will be removed when the limit is reached. Anything you post here should be considered ephemeral and you should keep a separate personal copy of them if you wish to retain a permanent archive.

- -

No IP addresses are logged.

- -

Posts can be removed on request if there is sufficient justification, but the nature of ActivityPub means that deletion of data federated to other instances cannot be guaranteed.

- -

Content Policy

- -

This instance will not host content containing sexism, racism, casteism, homophobia, transphobia, misogyny, antisemitism or other forms of bigotry or discrimination on the basis of nationality or immigration status. Claims that transgressions of this type were intended to be "ironic" will be treated as a terms of service violation.

- -

Even if not conspicuously discriminatory, expressions of support for organizations with discrminatory agendas are not permitted on this instance. These include, but are not limited to, racial supremacist groups, the redpill/incel movement and anti-LGBT or anti-immigrant campaigns.

- -

Depictions of injury, death or medical procedures are not permitted.

- -

Violent or abusive content will be subject to moderation and is likely to be removed.

- -

Content of a sexual nature may be published providing that only consenting adults (aged 18 or over) are depicted and an appropriate content warning message is added. Posting sexual content without a content warning is a terms of service violation. Sexual content is defined both as photographs of real people and also artistic or fictional depictions, edited/generated photos or narratives.

- -

Moderators rely upon your reports. Don't assume that something of concern has already been reported. It's better for there to be duplicate reports than for something potentially damaging to go unreported.

- -

Content found to be non-compliant with this policy will be removed and any accounts on this instance producing, repeating or linking to such content will be deleted typically without prior notification.

- -

Federation Policy

- -

In a proactive effort to avoid the classic fate of "embrace, extend, extinguish" this system will block any instance launched, acquired or funded by Alphabet, Facebook, Twitter, Microsoft, Apple, Amazon, Elsevier or other monopolistic Silicon Valley companies.

- -

This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible.

- -

Use of User Generated Content for Research

- -

Data may not be "scraped" or otherwise obtained from this instance and used for academic research or cited within research publications without the prior written permission of the administrator. Financial remedy will be sought through the courts from any researcher publishing data obtained from this instance without consent.

- -

Commercial Use

- -

Commercial use of original content on this instance is strictly forbidden without the prior written permission of individual account holders. The instance administrator does not hold copyright on any original content posted by account holders. Publication or federation of content does not imply permission for commercial use.

- -

Commercial use includes the harvesting of data to create products which are then sold, such as statistics, business reports or machine learning models.

- -

Copyrights

- -

Epicyon is licensed under GNU AGPL version 3 - -

Emojis designed by OpenMoji – the open-source emoji and icon project. License: CC BY-SA 4.0

- -

Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under Apache 2.0.

From 778f980733db4920e78d1f7595a416ecab0ce487 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 11:45:51 +0000 Subject: [PATCH 06/13] About text rendering as html --- webapp_about.py | 3 ++- webapp_column_left.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp_about.py b/webapp_about.py index d7a9c35cb..4f9b5f8bb 100644 --- a/webapp_about.py +++ b/webapp_about.py @@ -11,6 +11,7 @@ from shutil import copyfile from utils import getConfigParam from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter +from webapp_utils import markdownToHtml def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, @@ -30,7 +31,7 @@ def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str, aboutText = 'Information about this instance goes here.' if os.path.isfile(baseDir + '/accounts/about.md'): with open(baseDir + '/accounts/about.md', 'r') as aboutFile: - aboutText = aboutFile.read() + aboutText = markdownToHtml(aboutFile.read()) aboutForm = '' cssFilename = baseDir + '/epicyon-profile.css' diff --git a/webapp_column_left.py b/webapp_column_left.py index a7d143102..70c9648c0 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -17,7 +17,6 @@ from webapp_utils import headerButtonsFrontScreen from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from webapp_utils import getBannerFile -from webapp_utils import markdownToHtml def _linksExist(baseDir: str) -> bool: @@ -416,7 +415,7 @@ def htmlEditLinks(cssCache: {}, translate: {}, baseDir: str, path: str, aboutStr = '' if os.path.isfile(aboutFilename): with open(aboutFilename, 'r') as fp: - aboutStr = markdownToHtml(fp.read()) + aboutStr = fp.read() editLinksForm += \ '
' From fdc9c0ab6261395a83de580bc05810cd5f1df9ad Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 12:15:06 +0000 Subject: [PATCH 07/13] Allow semicolons in some other fields --- content.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/content.py b/content.py index bf22d7886..f926033f2 100644 --- a/content.py +++ b/content.py @@ -995,6 +995,12 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool, messageFields = messageFields.split(boundary) fields = {} + fieldsWithSemicolonAllowed = ( + 'message', 'bio', 'autoCW', + 'password', 'passwordconfirm', + 'instanceDescription', + 'instanceDescriptionShort' + ) # examine each section of the POST, separated by the boundary for f in messageFields: if f == '--': @@ -1007,7 +1013,8 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool, postKey = postStr.split('"', 1)[0] postValueStr = postStr.split('"', 1)[1] if ';' in postValueStr: - if postKey != 'message': + if postKey not in fieldsWithSemicolonAllowed and \ + not postKey.startswith('edited'): continue if '\r\n' not in postValueStr: continue From 6b3feec7aa7434d7fa2cf340720630848d396d93 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 12:19:49 +0000 Subject: [PATCH 08/13] Allow semicolons in some other fields --- content.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/content.py b/content.py index f926033f2..c1b2ff418 100644 --- a/content.py +++ b/content.py @@ -996,10 +996,9 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool, messageFields = messageFields.split(boundary) fields = {} fieldsWithSemicolonAllowed = ( - 'message', 'bio', 'autoCW', - 'password', 'passwordconfirm', - 'instanceDescription', - 'instanceDescriptionShort' + 'message', 'bio', 'autoCW', 'password', 'passwordconfirm', + 'instanceDescription', 'instanceDescriptionShort', + 'subject', 'location', 'imageDescription' ) # examine each section of the POST, separated by the boundary for f in messageFields: From 58ab2b05d31b48f25d233fae60f6e461f4149924 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 15:24:12 +0000 Subject: [PATCH 09/13] Endpoint for TTS of posts arriving in the inbox --- daemon.py | 32 ++++++++++++++++++++++++++++++++ inbox.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/daemon.py b/daemon.py index f50ca9d85..a288e8c87 100644 --- a/daemon.py +++ b/daemon.py @@ -5242,6 +5242,28 @@ class PubServer(BaseHTTPRequestHandler): print('favicon not sent: ' + callingDomain) self._404() + def _getSpeaker(self, callingDomain: str, path: str, + baseDir: str, domain: str, debug: bool) -> None: + """Returns the speaker file used for TTS and + accessed via c2s + """ + nickname = path.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + speakerFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json' + if not os.path.isfile(speakerFilename): + self._404() + return + + speakerJson = loadJson(speakerFilename) + msg = json.dumps(speakerJson, + ensure_ascii=False).encode('utf-8') + msglen = len(msg) + self._set_headers('application/json', msglen, + None, callingDomain) + self._write(msg) + def _getFonts(self, callingDomain: str, path: str, baseDir: str, debug: bool, GETstartTime, GETtimings: {}) -> None: @@ -10454,6 +10476,16 @@ class PubServer(BaseHTTPRequestHandler): if '/users/' in self.path: usersInPath = True + # authorized endpoint used for TTS of posts + # arriving in your inbox + if authorized and usersInPath and \ + self.path.endswith('/speaker'): + self._getSpeaker(callingDomain, self.path, + self.server.baseDir, + self.server.domain, + self.server.debug) + return + # redirect to the welcome screen if htmlGET and authorized and usersInPath and \ '/welcome' not in self.path: diff --git a/inbox.py b/inbox.py index dd408122e..d44e6de75 100644 --- a/inbox.py +++ b/inbox.py @@ -11,6 +11,8 @@ import os import datetime import time from linked_data_sig import verifyJsonSignature +from utils import getDisplayName +from utils import removeHtml from utils import getConfigParam from utils import hasUsersPath from utils import validPostDate @@ -2134,6 +2136,36 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, return True +def _updateSpeaker(baseDir: str, nickname: str, domain: str, + postJsonObject: {}, personCache: {}) -> None: + """ Generates a json file which can be used for TTS announcement + of incoming inbox posts + """ + if not postJsonObject.get('object'): + return + if not isinstance(postJsonObject['object'], dict): + return + if not postJsonObject['object'].get('content'): + return + if not isinstance(postJsonObject['object']['content'], str): + return + speakerFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json' + content = removeHtml(postJsonObject['object']['content']) + summary = '' + if postJsonObject['object'].get('summary'): + if isinstance(postJsonObject['object']['summary'], str): + summary = postJsonObject['object']['summary'] + speakerName = \ + getDisplayName(baseDir, postJsonObject['actor'], personCache) + speakerJson = { + "name": speakerName, + "summary": summary, + "say": content + } + saveJson(speakerJson, speakerFilename) + + def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, session, keyId: str, handle: str, messageJson: {}, baseDir: str, httpPrefix: str, sendThreads: [], @@ -2468,6 +2500,9 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, destinationFilename, debug): print('ERROR: unable to update ' + boxname + ' index') else: + if boxname == 'inbox': + _updateSpeaker(baseDir, nickname, domain, + postJsonObject, personCache) if not unitTest: if debug: print('Saving inbox post as html to cache') From 47be751f47b5e2b3e7c0638761879fd779e4a3b9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 15:36:40 +0000 Subject: [PATCH 10/13] Remove encoding from speaker endpoint --- inbox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/inbox.py b/inbox.py index d44e6de75..83db0796f 100644 --- a/inbox.py +++ b/inbox.py @@ -10,6 +10,7 @@ import json import os import datetime import time +import urllib.parse from linked_data_sig import verifyJsonSignature from utils import getDisplayName from utils import removeHtml @@ -2151,11 +2152,13 @@ def _updateSpeaker(baseDir: str, nickname: str, domain: str, return speakerFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json' - content = removeHtml(postJsonObject['object']['content']) + content = urllib.parse.unquote_plus(postJsonObject['object']['content']) + content = removeHtml(content) summary = '' if postJsonObject['object'].get('summary'): if isinstance(postJsonObject['object']['summary'], str): - summary = postJsonObject['object']['summary'] + summary = \ + urllib.parse.unquote_plus(postJsonObject['object']['summary']) speakerName = \ getDisplayName(baseDir, postJsonObject['actor'], personCache) speakerJson = { From 8f8c70ed6aabc633a26f7b04b7bca51904bf371b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 16:04:19 +0000 Subject: [PATCH 11/13] Replace quotes --- inbox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 83db0796f..e3aed9cee 100644 --- a/inbox.py +++ b/inbox.py @@ -80,6 +80,7 @@ from happening import saveEventPost from delete import removeOldHashtags from categories import guessHashtagCategory from context import hasValidContext +from content import htmlReplaceQuoteMarks def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: @@ -2153,7 +2154,7 @@ def _updateSpeaker(baseDir: str, nickname: str, domain: str, speakerFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json' content = urllib.parse.unquote_plus(postJsonObject['object']['content']) - content = removeHtml(content) + content = removeHtml(htmlReplaceQuoteMarks(content)) summary = '' if postJsonObject['object'].get('summary'): if isinstance(postJsonObject['object']['summary'], str): From 6a33bce7c1fe3eb4285f331f702f884184f51e84 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 19:16:33 +0000 Subject: [PATCH 12/13] Speaker option --- epicyon.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ speaker.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 speaker.py diff --git a/epicyon.py b/epicyon.py index ea958dd75..4c6d2a354 100644 --- a/epicyon.py +++ b/epicyon.py @@ -75,6 +75,10 @@ from theme import setTheme from announce import sendAnnounceViaServer from socnet import instancesGraph from migrate import migrateAccounts +from speaker import getSpeakerFromServer +from speaker import getSpeakerPitch +from speaker import getSpeakerRate +from speaker import getSpeakerRange import argparse @@ -429,6 +433,10 @@ parser.add_argument('--level', dest='skillLevelPercent', type=int, parser.add_argument('--status', '--availability', dest='availability', type=str, default=None, help='Set an availability status') +parser.add_argument('--speaker', '--tts', dest='speaker', + type=str, default=None, + help='Announce posts as they arrive at your ' + + 'inbox using TTS. --speaker [handle]') parser.add_argument('--block', dest='block', type=str, default=None, help='Block a particular address') parser.add_argument('--unblock', dest='unblock', type=str, default=None, @@ -1887,6 +1895,65 @@ if args.availability: time.sleep(1) sys.exit() +if args.speaker: + # Announce posts as they arrive in your inbox using text-to-speech + if args.speaker.startswith('@'): + args.speaker = args.speaker[1:] + if '@' not in args.speaker: + print('Specify the handle of the speaker nickname@domain') + sys.exit() + nickname = args.speaker.split('@')[0] + domain = args.speaker.split('@')[1] + + if not nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.password: + print('Specify a password with the --password option') + sys.exit() + + proxyType = None + if args.tor or domain.endswith('.onion'): + proxyType = 'tor' + if domain.endswith('.onion'): + args.port = 80 + elif args.i2p or domain.endswith('.i2p'): + proxyType = 'i2p' + if domain.endswith('.i2p'): + args.port = 80 + elif args.gnunet: + proxyType = 'gnunet' + + print('Setting up espeak') + from espeak import espeak + + session = createSession(proxyType) + print('Running speaker for ' + nickname + '@' + domain) + + prevSay = '' + while (1): + speakerJson = \ + getSpeakerFromServer(baseDir, session, nickname, args.password, + domain, port, + httpPrefix, + True, __version__) + if speakerJson: + if speakerJson['say'] != prevSay: + print(speakerJson['name'] + ': ' + speakerJson['say'] + '\n') + pitch = getSpeakerPitch(speakerJson['name']) + espeak.set_parameter(espeak.Parameter.Pitch, pitch) + rate = getSpeakerRate(speakerJson['name']) + espeak.set_parameter(espeak.Parameter.Rate, 110) + srange = getSpeakerRange(speakerJson['name']) + espeak.set_parameter(espeak.Parameter.Range, srange) + espeak.synth(speakerJson['name']) + time.sleep(3) + espeak.synth(speakerJson['say']) + prevSay = speakerJson['say'] + time.sleep(20) + sys.exit() + if federationList: print('Federating with: ' + str(federationList)) diff --git a/speaker.py b/speaker.py new file mode 100644 index 000000000..ea3df1141 --- /dev/null +++ b/speaker.py @@ -0,0 +1,65 @@ +__filename__ = "speaker.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.2.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import random +from auth import createBasicAuthHeader +from session import getJson +from utils import getFullDomain + + +def getSpeakerPitch(displayName: str) -> int: + """Returns the speech synthesis pitch for the given name + """ + random.seed(displayName) + return random.randint(1, 100) + + +def getSpeakerRate(displayName: str) -> int: + """Returns the speech synthesis rate for the given name + """ + random.seed(displayName) + return random.randint(50, 120) + + +def getSpeakerRange(displayName: str) -> int: + """Returns the speech synthesis range for the given name + """ + random.seed(displayName) + return random.randint(300, 800) + + +def getSpeakerFromServer(baseDir: str, session, + nickname: str, password: str, + domain: str, port: int, + httpPrefix: str, + debug: bool, projectVersion: str) -> {}: + """Returns some json which contains the latest inbox + entry in a minimal format suitable for a text-to-speech reader + """ + if not session: + print('WARN: No session for getSpeakerFromServer') + return 6 + + domainFull = getFullDomain(domain, port) + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + + url = \ + httpPrefix + '://' + \ + domainFull + '/users/' + nickname + '/speaker' + + speakerJson = \ + getJson(session, url, headers, None, + __version__, httpPrefix, domain) + return speakerJson From 12f4efc47b12fed0ab547a9a637a90ff5de71118 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 1 Mar 2021 19:20:49 +0000 Subject: [PATCH 13/13] Note about speaker option --- README_commandline.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README_commandline.md b/README_commandline.md index a8bfc9c92..f260a6e68 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -390,3 +390,15 @@ To remove a shared item: ``` bash python3 epicyon.py --undoItemName "spanner" --nickname [yournick] --domain [yourdomain] --password [c2s password] ``` + +## Speaking your inbox + +It is possible to use text-to-speech to read your inbox as posts arrive. This can be useful if you are not looking at a screen but want to stay ambiently informed of what's happening. + +On Debian based systems you will need to have the **python3-espeak** package installed. + +``` bash +python3 epicyon.py --speaker yournickname@yourdomain --password [yourpassword] +``` + +This will then stay running and incoming posts will be announced as they arrive.