diff --git a/daemon.py b/daemon.py index 5ea50f7ff..d007ec902 100644 --- a/daemon.py +++ b/daemon.py @@ -2719,7 +2719,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) @@ -2772,7 +2773,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) if historyStr: msg = historyStr.encode('utf-8') msglen = len(msg) @@ -2862,7 +2864,8 @@ class PubServer(BaseHTTPRequestHandler): showPublishedDateOnly, self.server.defaultTimeline, self.server.peertubeInstances, - allowLocalNetworkAccess) + allowLocalNetworkAccess, + self.server.themeName) if profileStr: msg = profileStr.encode('utf-8') msglen = len(msg) @@ -4853,8 +4856,7 @@ class PubServer(BaseHTTPRequestHandler): # only receive DMs from accounts you follow followDMsFilename = \ baseDir + '/accounts/' + \ - nickname + '@' + domain + \ - '/.followDMs' + nickname + '@' + domain + '/.followDMs' if onFinalWelcomeScreen: # initial default setting created via # the welcome screen @@ -5988,7 +5990,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) @@ -6951,7 +6954,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) if deleteStr: deleteStrLen = len(deleteStr) self._set_headers('text/html', deleteStrLen, @@ -7156,7 +7160,8 @@ class PubServer(BaseHTTPRequestHandler): ytDomain, self.server.showPublishedDateOnly, peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -7243,7 +7248,8 @@ class PubServer(BaseHTTPRequestHandler): ytDomain, self.server.showPublishedDateOnly, peertubeInstances, - self.server.allowLocalNetworkAccess) + self.server.allowLocalNetworkAccess, + self.server.themeName) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -7533,6 +7539,8 @@ class PubServer(BaseHTTPRequestHandler): cssCache = self.server.cssCache allowLocalNetworkAccess = \ self.server.allowLocalNetworkAccess + themeName = \ + self.server.themeName msg = \ htmlIndividualPost(cssCache, recentPostsCache, @@ -7553,7 +7561,8 @@ class PubServer(BaseHTTPRequestHandler): ytDomain, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess) + allowLocalNetworkAccess, + themeName) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -7657,6 +7666,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances allowLocalNetworkAccess = \ self.server.allowLocalNetworkAccess + themeName = \ + self.server.themeName msg = \ htmlIndividualPost(self.server.cssCache, recentPostsCache, @@ -7677,7 +7688,8 @@ class PubServer(BaseHTTPRequestHandler): ytDomain, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess) + allowLocalNetworkAccess, + themeName) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, @@ -14668,7 +14680,8 @@ def runDaemon(brochMode: bool, httpd.maxFollowers, httpd.allowLocalNetworkAccess, httpd.peertubeInstances, - verifyAllSignatures), daemon=True) + verifyAllSignatures, + httpd.themeName), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ diff --git a/inbox.py b/inbox.py index 131647ef9..6203db0e4 100644 --- a/inbox.py +++ b/inbox.py @@ -58,12 +58,12 @@ from filters import isFiltered from utils import updateAnnounceCollection from utils import undoAnnounceCollectionEntry from utils import dangerousMarkup +from utils import isDM +from utils import isReply from httpsig import messageContentDigest from posts import createDirectMessagePost from posts import validContentWarning from posts import downloadAnnounce -from posts import isDM -from posts import isReply from posts import isMuted from posts import isImageMedia from posts import sendSignedJson @@ -158,7 +158,8 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, allowDeletion: bool, boxname: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> None: + allowLocalNetworkAccess: bool, + themeName: str) -> None: """Converts the json post into html and stores it in a cache This enables the post to be quickly displayed later """ @@ -176,6 +177,7 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, httpPrefix, __version__, boxname, None, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, not isDM(postJsonObject), True, True, False, True) @@ -1290,7 +1292,8 @@ def _receiveAnnounce(recentPostsCache: {}, personCache: {}, messageJson: {}, federationList: [], debug: bool, translate: {}, YTReplacementDomain: str, - allowLocalNetworkAccess: bool) -> bool: + allowLocalNetworkAccess: bool, + themeName: str) -> bool: """Receives an announce activity within the POST section of HTTPServer """ if messageJson['type'] != 'Announce': @@ -1408,9 +1411,12 @@ def _receiveAnnounce(recentPostsCache: {}, if isRecentPost(postJsonObject): if not os.path.isfile(postFilename + '.tts'): - updateSpeaker(baseDir, nickname, domain, + domainFull = getFullDomain(domain, port) + updateSpeaker(baseDir, httpPrefix, + nickname, domain, domainFull, postJsonObject, personCache, - translate, lookupActor) + translate, lookupActor, + themeName) ttsFile = open(postFilename + '.tts', "w+") if ttsFile: ttsFile.write('\n') @@ -2164,7 +2170,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly: bool, allowLocalNetworkAccess: bool, peertubeInstances: [], - lastBounceMessage: []) -> bool: + lastBounceMessage: [], + themeName: str) -> bool: """ Anything which needs to be done after initial checks have passed """ actor = keyId @@ -2243,7 +2250,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, federationList, debug, translate, YTReplacementDomain, - allowLocalNetworkAccess): + allowLocalNetworkAccess, + themeName): if debug: print('DEBUG: Announce accepted from ' + actor) @@ -2491,9 +2499,11 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, else: if boxname == 'inbox': if isRecentPost(postJsonObject): - updateSpeaker(baseDir, nickname, domain, + domainFull = getFullDomain(domain, port) + updateSpeaker(baseDir, httpPrefix, + nickname, domain, domainFull, postJsonObject, personCache, - translate, None) + translate, None, themeName) if not unitTest: if debug: print('Saving inbox post as html to cache') @@ -2513,7 +2523,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, boxname, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess) + allowLocalNetworkAccess, + themeName) if debug: timeDiff = \ str(int((time.time() - htmlCacheStartTime) * @@ -2614,7 +2625,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly: bool, maxFollowers: int, allowLocalNetworkAccess: bool, peertubeInstances: [], - verifyAllSignatures: bool) -> None: + verifyAllSignatures: bool, + themeName: str) -> None: """Processes received items and moves them to the appropriate directories """ @@ -3107,7 +3119,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly, allowLocalNetworkAccess, peertubeInstances, - lastBounceMessage) + lastBounceMessage, + themeName) if debug: pprint(queueJson['post']) diff --git a/notifications_client.py b/notifications_client.py index 67b37b72e..eb09f5389 100644 --- a/notifications_client.py +++ b/notifications_client.py @@ -66,6 +66,17 @@ def _speakerPicospeaker(pitch: int, rate: int, systemLanguage: str, os.system(speakerCmd) +def _playNotificationSound(soundFilename: str, player='ffplay') -> None: + """Plays a sound + """ + if not os.path.isfile(soundFilename): + return + + if player == 'ffplay': + os.system('ffplay ' + soundFilename + + ' -autoexit -hide_banner -nodisp') + + def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, nickname: str, domain: str, port: int, password: str, screenreader: str, @@ -73,64 +84,121 @@ def runNotificationsClient(baseDir: str, proxyType: str, httpPrefix: str, """Runs the notifications and screen reader client, which announces new inbox items """ - if screenreader == 'espeak': - print('Setting up espeak') - from espeak import espeak - elif screenreader != 'picospeaker': - print(screenreader + ' is not a supported TTS system') - return + if screenreader: + if screenreader == 'espeak': + print('Setting up espeak') + from espeak import espeak + elif screenreader != 'picospeaker': + print(screenreader + ' is not a supported TTS system') + return - print('Running ' + screenreader + ' for ' + nickname + '@' + domain) + print('Running ' + screenreader + ' for ' + nickname + '@' + domain) prevSay = '' + prevDM = False + prevReply = False + prevCalendar = False + prevFollow = False + prevLike = '' + prevShare = False + dmSoundFilename = 'dm.ogg' + replySoundFilename = 'reply.ogg' + calendarSoundFilename = 'calendar.ogg' + followSoundFilename = 'follow.ogg' + likeSoundFilename = 'like.ogg' + shareSoundFilename = 'share.ogg' + player = 'ffplay' while (1): session = createSession(proxyType) speakerJson = \ getSpeakerFromServer(baseDir, session, nickname, password, domain, port, httpPrefix, True, __version__) if speakerJson: - if speakerJson['say'] != prevSay: - if speakerJson.get('name'): - nameStr = speakerJson['name'] - gender = 'They/Them' - if speakerJson.get('gender'): - gender = speakerJson['gender'] + if speakerJson.get('notify'): + soundsDir = 'theme/default/sounds' + if speakerJson['notify'].get('theme'): + soundsDir = \ + 'theme/' + speakerJson['notify']['theme'] + '/sounds' + if not os.path.isdir(soundsDir): + soundsDir = 'theme/default/sounds' + if dmSoundFilename: + if speakerJson['notify']['dm'] != prevDM: + _playNotificationSound(soundsDir + '/' + + dmSoundFilename, player) + elif replySoundFilename: + if speakerJson['notify']['reply'] != prevReply: + _playNotificationSound(soundsDir + '/' + + replySoundFilename, player) + elif calendarSoundFilename: + if speakerJson['notify']['calendar'] != prevCalendar: + _playNotificationSound(soundsDir + '/' + + calendarSoundFilename, player) + elif followSoundFilename: + if speakerJson['notify']['followRequests'] != prevFollow: + _playNotificationSound(soundsDir + '/' + + followSoundFilename, player) + elif likeSoundFilename: + if speakerJson['notify']['like'] != prevLike: + _playNotificationSound(soundsDir + '/' + + likeSoundFilename, player) + elif shareSoundFilename: + if speakerJson['notify']['share'] != prevShare: + _playNotificationSound(soundsDir + '/' + + shareSoundFilename, player) - # get the speech parameters - pitch = getSpeakerPitch(nameStr, screenreader, gender) - rate = getSpeakerRate(nameStr, screenreader) - srange = getSpeakerRange(nameStr) + prevDM = speakerJson['notify']['dm'] + prevReply = speakerJson['notify']['reply'] + prevCalendar = speakerJson['notify']['calendar'] + prevFollow = speakerJson['notify']['followRequests'] + prevLike = speakerJson['notify']['like'] + prevShare = speakerJson['notify']['share'] - # say the speaker's name - if screenreader == 'espeak': - _speakerEspeak(espeak, pitch, rate, srange, nameStr) - elif screenreader == 'picospeaker': - _speakerPicospeaker(pitch, rate, - systemLanguage, nameStr) - time.sleep(2) + if speakerJson.get('say'): + if speakerJson['say'] != prevSay: + if speakerJson.get('name'): + nameStr = speakerJson['name'] + gender = 'They/Them' + if speakerJson.get('gender'): + gender = speakerJson['gender'] - # append image description if needed - if not speakerJson.get('imageDescription'): - sayStr = speakerJson['say'] - # echo spoken text to the screen - print(html.unescape(nameStr) + ': ' + - html.unescape(speakerJson['say']) + '\n') - else: - sayStr = speakerJson['say'] + '. ' + \ - speakerJson['imageDescription'] - # echo spoken text to the screen - print(html.unescape(nameStr) + ': ' + - html.unescape(speakerJson['say']) + '\n' + - html.unescape(speakerJson['imageDescription'])) + # get the speech parameters + pitch = getSpeakerPitch(nameStr, screenreader, gender) + rate = getSpeakerRate(nameStr, screenreader) + srange = getSpeakerRange(nameStr) - # speak the post content - if screenreader == 'espeak': - _speakerEspeak(espeak, pitch, rate, srange, sayStr) - elif screenreader == 'picospeaker': - _speakerPicospeaker(pitch, rate, - systemLanguage, sayStr) + # say the speaker's name + if screenreader == 'espeak': + _speakerEspeak(espeak, pitch, rate, srange, + nameStr) + elif screenreader == 'picospeaker': + _speakerPicospeaker(pitch, rate, + systemLanguage, nameStr) + time.sleep(2) - prevSay = speakerJson['say'] + # append image description if needed + if not speakerJson.get('imageDescription'): + sayStr = speakerJson['say'] + # echo spoken text to the screen + print(html.unescape(nameStr) + ': ' + + html.unescape(speakerJson['say']) + '\n') + else: + sayStr = speakerJson['say'] + '. ' + \ + speakerJson['imageDescription'] + # echo spoken text to the screen + imageDescription = \ + html.unescape(speakerJson['imageDescription']) + print(html.unescape(nameStr) + ': ' + + html.unescape(speakerJson['say']) + '\n' + + imageDescription) + + # speak the post content + if screenreader == 'espeak': + _speakerEspeak(espeak, pitch, rate, srange, sayStr) + elif screenreader == 'picospeaker': + _speakerPicospeaker(pitch, rate, + systemLanguage, sayStr) + + prevSay = speakerJson['say'] # wait for a while, or until a key is pressed keyPress = _waitForKeypress(30, debug) diff --git a/posts.py b/posts.py index 33959a7cc..fcfe3bce4 100644 --- a/posts.py +++ b/posts.py @@ -2921,34 +2921,6 @@ def createModeration(baseDir: str, nickname: str, domain: str, port: int, return boxItems -def isDM(postJsonObject: {}) -> bool: - """Returns true if the given post is a DM - """ - if postJsonObject['type'] != 'Create': - return False - if not postJsonObject.get('object'): - return False - if not isinstance(postJsonObject['object'], dict): - return False - if postJsonObject['object']['type'] != 'Note' and \ - postJsonObject['object']['type'] != 'Patch' and \ - postJsonObject['object']['type'] != 'EncryptedMessage' and \ - postJsonObject['object']['type'] != 'Article': - return False - if postJsonObject['object'].get('moderationStatus'): - return False - fields = ('to', 'cc') - for f in fields: - if not postJsonObject['object'].get(f): - continue - for toAddress in postJsonObject['object'][f]: - if toAddress.endswith('#Public'): - return False - if toAddress.endswith('followers'): - return False - return True - - def isImageMedia(session, baseDir: str, httpPrefix: str, nickname: str, domain: str, postJsonObject: {}, translate: {}, @@ -2992,40 +2964,6 @@ def isImageMedia(session, baseDir: str, httpPrefix: str, return False -def isReply(postJsonObject: {}, actor: str) -> bool: - """Returns true if the given post is a reply to the given actor - """ - if postJsonObject['type'] != 'Create': - return False - if not postJsonObject.get('object'): - return False - if not isinstance(postJsonObject['object'], dict): - return False - if postJsonObject['object'].get('moderationStatus'): - return False - if postJsonObject['object']['type'] != 'Note' and \ - postJsonObject['object']['type'] != 'EncryptedMessage' and \ - postJsonObject['object']['type'] != 'Article': - return False - if postJsonObject['object'].get('inReplyTo'): - if isinstance(postJsonObject['object']['inReplyTo'], str): - if postJsonObject['object']['inReplyTo'].startswith(actor): - return True - if not postJsonObject['object'].get('tag'): - return False - if not isinstance(postJsonObject['object']['tag'], list): - return False - for tag in postJsonObject['object']['tag']: - if not tag.get('type'): - continue - if tag['type'] == 'Mention': - if not tag.get('href'): - continue - if actor in tag['href']: - return True - return False - - def _addPostStringToTimeline(postStr: str, boxname: str, postsInBox: [], boxActor: str) -> bool: """ is this a valid timeline post? diff --git a/scripts/epicyon-notification b/scripts/epicyon-notification index cdeb716e6..5b4f4ec89 100755 --- a/scripts/epicyon-notification +++ b/scripts/epicyon-notification @@ -209,9 +209,9 @@ function notifications { if [[ "$epicyonLikeFileContent" == *':'* ]]; then epicyonLikeMessage="Epicyon: $epicyonLikeFileContent" fi - "${PROJECT_NAME}-notification" -u "$USERNAME" -s "Epicyon" -m "$epicyonLikeMessage" --sensitive yes + sendNotification "$USERNAME" "Epicyon" "$epicyonLikeMessage" echo "##sent##" > "$epicyonLikeFile" - chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonLkeFile" + chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonLikeFile" fi fi diff --git a/speaker.py b/speaker.py index 2b6ab90cc..fbd3f8952 100644 --- a/speaker.py +++ b/speaker.py @@ -12,6 +12,8 @@ import random import urllib.parse from auth import createBasicAuthHeader from session import getJson +from utils import isDM +from utils import isReply from utils import camelCaseSplit from utils import getDomainFromActor from utils import getNicknameFromActor @@ -263,7 +265,11 @@ def getSpeakerFromServer(baseDir: str, session, def _speakerEndpointJson(displayName: str, summary: str, content: str, imageDescription: str, - links: [], gender: str, postId: str) -> {}: + links: [], gender: str, postId: str, + postDM: bool, postReply: bool, + followRequestsExist: bool, + likedBy: str, postCal: bool, + postShare: bool, themeName: str) -> {}: """Returns a json endpoint for the TTS speaker """ speakerJson = { @@ -272,7 +278,16 @@ def _speakerEndpointJson(displayName: str, summary: str, "say": content, "imageDescription": imageDescription, "detectedLinks": links, - "id": postId + "id": postId, + "notify": { + "theme": themeName, + "dm": postDM, + "reply": postReply, + "followRequests": followRequestsExist, + "likedBy": likedBy, + "calendar": postCal, + "share": postShare + } } if gender: speakerJson['gender'] = gender @@ -360,9 +375,11 @@ def getSSMLbox(baseDir: str, path: str, instanceTitle, gender) -def _postToSpeakerJson(baseDir: str, nickname: str, domain: str, +def _postToSpeakerJson(baseDir: str, httpPrefix: str, + nickname: str, domain: str, domainFull: str, postJsonObject: {}, personCache: {}, - translate: {}, announcingActor: str) -> {}: + translate: {}, announcingActor: str, + themeName: str) -> {}: """Converts an ActivityPub post into some Json containing speech synthesis parameters. NOTE: There currently appears to be no standardized json @@ -429,21 +446,54 @@ def _postToSpeakerJson(baseDir: str, nickname: str, domain: str, postId = None if postJsonObject['object'].get('id'): postId = postJsonObject['object']['id'] + + actor = httpPrefix + '://' + domainFull + '/users/' + nickname + postDM = isDM(postJsonObject) + postReply = isReply(postJsonObject, actor) + + followRequestsExist = False + accountsDir = baseDir + '/accounts/' + nickname + '@' + domainFull + approveFollowsFilename = accountsDir + '/followrequests.txt' + if os.path.isfile(approveFollowsFilename): + with open(approveFollowsFilename, 'r') as fp: + follows = fp.readlines() + if len(follows) > 0: + followRequestsExist = True + likedBy = '' + likeFilename = accountsDir + '/.newLike' + if os.path.isfile(likeFilename): + with open(likeFilename, 'r') as fp: + likedBy = fp.read() + if '##sent##' in likedBy: + likedBy = '' + calendarFilename = accountsDir + '/.newCalendar' + postCal = os.path.isfile(calendarFilename) + shareFilename = accountsDir + '/.newShare' + postShare = os.path.isfile(shareFilename) + return _speakerEndpointJson(speakerName, summary, content, imageDescription, - detectedLinks, gender, postId) + detectedLinks, gender, postId, + postDM, postReply, + followRequestsExist, + likedBy, + postCal, postShare, themeName) -def updateSpeaker(baseDir: str, nickname: str, domain: str, +def updateSpeaker(baseDir: str, httpPrefix: str, + nickname: str, domain: str, domainFull: str, postJsonObject: {}, personCache: {}, - translate: {}, announcingActor: str) -> None: + translate: {}, announcingActor: str, + themeName: str) -> None: """ Generates a json file which can be used for TTS announcement of incoming inbox posts """ speakerJson = \ - _postToSpeakerJson(baseDir, nickname, domain, + _postToSpeakerJson(baseDir, httpPrefix, + nickname, domain, domainFull, postJsonObject, personCache, - translate, announcingActor) + translate, announcingActor, + themeName) speakerFilename = \ baseDir + '/accounts/' + nickname + '@' + domain + '/speaker.json' saveJson(speakerJson, speakerFilename) diff --git a/theme/default/sounds/calendar.ogg b/theme/default/sounds/calendar.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/calendar.ogg differ diff --git a/theme/default/sounds/dm.ogg b/theme/default/sounds/dm.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/dm.ogg differ diff --git a/theme/default/sounds/follow.ogg b/theme/default/sounds/follow.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/follow.ogg differ diff --git a/theme/default/sounds/like.ogg b/theme/default/sounds/like.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/like.ogg differ diff --git a/theme/default/sounds/reply.ogg b/theme/default/sounds/reply.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/reply.ogg differ diff --git a/theme/default/sounds/share.ogg b/theme/default/sounds/share.ogg new file mode 100644 index 000000000..05ba472e2 Binary files /dev/null and b/theme/default/sounds/share.ogg differ diff --git a/utils.py b/utils.py index 57d41662f..1a977d692 100644 --- a/utils.py +++ b/utils.py @@ -2068,3 +2068,65 @@ def rejectPostId(baseDir: str, nickname: str, domain: str, if rejectFile: rejectFile.write('\n') rejectFile.close() + + +def isDM(postJsonObject: {}) -> bool: + """Returns true if the given post is a DM + """ + if postJsonObject['type'] != 'Create': + return False + if not postJsonObject.get('object'): + return False + if not isinstance(postJsonObject['object'], dict): + return False + if postJsonObject['object']['type'] != 'Note' and \ + postJsonObject['object']['type'] != 'Patch' and \ + postJsonObject['object']['type'] != 'EncryptedMessage' and \ + postJsonObject['object']['type'] != 'Article': + return False + if postJsonObject['object'].get('moderationStatus'): + return False + fields = ('to', 'cc') + for f in fields: + if not postJsonObject['object'].get(f): + continue + for toAddress in postJsonObject['object'][f]: + if toAddress.endswith('#Public'): + return False + if toAddress.endswith('followers'): + return False + return True + + +def isReply(postJsonObject: {}, actor: str) -> bool: + """Returns true if the given post is a reply to the given actor + """ + if postJsonObject['type'] != 'Create': + return False + if not postJsonObject.get('object'): + return False + if not isinstance(postJsonObject['object'], dict): + return False + if postJsonObject['object'].get('moderationStatus'): + return False + if postJsonObject['object']['type'] != 'Note' and \ + postJsonObject['object']['type'] != 'EncryptedMessage' and \ + postJsonObject['object']['type'] != 'Article': + return False + if postJsonObject['object'].get('inReplyTo'): + if isinstance(postJsonObject['object']['inReplyTo'], str): + if postJsonObject['object']['inReplyTo'].startswith(actor): + return True + if not postJsonObject['object'].get('tag'): + return False + if not isinstance(postJsonObject['object']['tag'], list): + return False + for tag in postJsonObject['object']['tag']: + if not tag.get('type'): + continue + if tag['type'] == 'Mention': + if not tag.get('href'): + continue + if actor in tag['href']: + return True + return False diff --git a/webapp_confirm.py b/webapp_confirm.py index 26b8289b7..346d94893 100644 --- a/webapp_confirm.py +++ b/webapp_confirm.py @@ -31,7 +31,8 @@ def htmlConfirmDelete(cssCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Shows a screen asking to confirm the deletion of a post """ if '/statuses/' not in messageId: @@ -72,6 +73,7 @@ def htmlConfirmDelete(cssCache: {}, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, False, False, False, False) deletePostStr += '
' deletePostStr += \ diff --git a/webapp_frontscreen.py b/webapp_frontscreen.py index 4e5ad14fc..0d3d18486 100644 --- a/webapp_frontscreen.py +++ b/webapp_frontscreen.py @@ -30,7 +30,8 @@ def _htmlFrontScreenPosts(recentPostsCache: {}, maxRecentPosts: int, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> 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 @@ -71,6 +72,7 @@ def _htmlFrontScreenPosts(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, False, False, True, False) if postStr: profileStr += postStr + separatorStr @@ -159,7 +161,8 @@ def htmlFrontScreen(rssIconAtTop: bool, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess) + licenseStr + allowLocalNetworkAccess, + theme) + licenseStr # Footer which is only used for system accounts profileFooterStr = ' \n' diff --git a/webapp_post.py b/webapp_post.py index 532f535d2..36aec0653 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -19,9 +19,9 @@ from like import noOfLikes from follow import isFollowingActor from posts import postIsMuted from posts import getPersonBox -from posts import isDM from posts import downloadAnnounce from posts import populateRepliesJson +from utils import isDM from utils import rejectPostId from utils import isRecentPost from utils import getConfigParam @@ -1139,6 +1139,7 @@ def individualPostAsHtml(allowDownloads: bool, showPublishedDateOnly: bool, peertubeInstances: [], allowLocalNetworkAccess: bool, + themeName: str, showRepeats=True, showIcons=False, manuallyApprovesFollowers=False, @@ -1304,9 +1305,11 @@ def individualPostAsHtml(allowDownloads: bool, postJsonObject['id']) if announceFilename and postJsonObject.get('actor'): if not os.path.isfile(announceFilename + '.tts'): - updateSpeaker(baseDir, nickname, domain, + updateSpeaker(baseDir, httpPrefix, + nickname, domain, domainFull, postJsonObject, personCache, - translate, postJsonObject['actor']) + translate, postJsonObject['actor'], + themeName) ttsFile = open(announceFilename + '.tts', "w+") if ttsFile: ttsFile.write('\n') @@ -1683,7 +1686,8 @@ def htmlIndividualPost(cssCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Show an individual post as html """ postStr = '' @@ -1724,7 +1728,7 @@ def htmlIndividualPost(cssCache: {}, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess, + allowLocalNetworkAccess, themeName, False, authorized, False, False, False) messageId = removeIdEnding(postJsonObject['id']) @@ -1752,6 +1756,7 @@ def htmlIndividualPost(cssCache: {}, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, authorized, False, False, False) + postStr @@ -1782,6 +1787,7 @@ def htmlIndividualPost(cssCache: {}, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, authorized, False, False, False) cssFilename = baseDir + '/epicyon-profile.css' @@ -1803,7 +1809,8 @@ def htmlPostReplies(cssCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Show the replies to an individual post as html """ repliesStr = '' @@ -1822,6 +1829,7 @@ def htmlPostReplies(cssCache: {}, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, False, False, False, False) cssFilename = baseDir + '/epicyon-profile.css' diff --git a/webapp_profile.py b/webapp_profile.py index 0b5bb8db1..1a987244b 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -66,7 +66,8 @@ def htmlProfileAfterSearch(cssCache: {}, showPublishedDateOnly: bool, defaultTimeline: str, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Show a profile page after a search for a fediverse address """ if hasUsersPath(profileHandle) or '/@' in profileHandle: @@ -300,6 +301,7 @@ def htmlProfileAfterSearch(cssCache: {}, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, False, False, False, False) i += 1 if i >= 20: @@ -804,7 +806,8 @@ def htmlProfile(rssIconAtTop: bool, YTReplacementDomain, showPublishedDateOnly, peertubeInstances, - allowLocalNetworkAccess) + licenseStr + allowLocalNetworkAccess, + theme) + licenseStr elif selected == 'following': profileStr += \ _htmlProfileFollowing(translate, baseDir, httpPrefix, @@ -856,7 +859,8 @@ def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Shows posts on the profile screen These should only be public posts """ @@ -896,6 +900,7 @@ def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, False, False, False, True, False) if postStr: profileStr += postStr + separatorStr diff --git a/webapp_search.py b/webapp_search.py index 0912beb90..6d982c45d 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -527,7 +527,8 @@ def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Show a page containing search results for your post history """ if historysearch.startswith('!'): @@ -604,6 +605,7 @@ def htmlHistorySearch(cssCache: {}, translate: {}, baseDir: str, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, + themeName, showIndividualPostIcons, showIndividualPostIcons, False, False, False) @@ -626,7 +628,8 @@ def htmlHashtagSearch(cssCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, peertubeInstances: [], - allowLocalNetworkAccess: bool) -> str: + allowLocalNetworkAccess: bool, + themeName: str) -> str: """Show a page containing search results for a hashtag """ if hashtag.startswith('#'): @@ -778,6 +781,7 @@ def htmlHashtagSearch(cssCache: {}, allowLocalNetworkAccess, showRepeats, showIcons, manuallyApprovesFollowers, + themeName, showPublicOnly, storeToCache) if postStr: diff --git a/webapp_timeline.py b/webapp_timeline.py index 71e88d932..db256378f 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -723,6 +723,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, boxName != 'dm', showIndividualPostIcons, manuallyApproveFollowers, + theme, False, True) _logTimelineTiming(enableTimingLog, timelineStartTime, boxName, '12')