diff --git a/daemon.py b/daemon.py index 05795bdcf..87909d1f3 100644 --- a/daemon.py +++ b/daemon.py @@ -192,6 +192,7 @@ from webapp_suspended import htmlSuspended from webapp_tos import htmlTermsOfService from webapp_confirm import htmlConfirmFollow from webapp_confirm import htmlConfirmUnfollow +from webapp_post import htmlEmojiReactionPicker from webapp_post import htmlPostReplies from webapp_post import htmlIndividualPost from webapp_post import individualPostAsHtml @@ -238,6 +239,8 @@ from categories import updateHashtagCategories from languages import getActorLanguages from languages import setActorLanguages from like import updateLikesCollection +from reaction import updateReactionCollection +from utils import undoReactionCollectionEntry from utils import getNewPostEndpoints from utils import malformedCiphertext from utils import hasActor @@ -654,12 +657,9 @@ class PubServer(BaseHTTPRequestHandler): return False return True - def _secureMode(self) -> bool: - """http authentication of GET requests for json + def _signedGETkeyId(self) -> str: + """Returns the actor from the signed GET keyId """ - if not self.server.secureMode: - return True - signature = None if self.headers.get('signature'): signature = self.headers['signature'] @@ -669,9 +669,9 @@ class PubServer(BaseHTTPRequestHandler): # check that the headers are signed if not signature: if self.server.debug: - print('AUTH: secure mode, ' + + print('AUTH: secure mode actor, ' + 'GET has no signature in headers') - return False + return None # get the keyId, which is typically the instance actor keyId = None @@ -680,17 +680,25 @@ class PubServer(BaseHTTPRequestHandler): if signatureItem.startswith('keyId='): if '"' in signatureItem: keyId = signatureItem.split('"')[1] - break + # remove #main-key + if '#' in keyId: + keyId = keyId.split('#')[0] + return keyId + return None + + def _secureMode(self, force: bool = False) -> bool: + """http authentication of GET requests for json + """ + if not self.server.secureMode and not force: + return True + + keyId = self._signedGETkeyId() if not keyId: if self.server.debug: print('AUTH: secure mode, ' + 'failed to obtain keyId from signature') return False - # remove #main-key - if '#' in keyId: - keyId = keyId.split('#')[0] - # is the keyId (actor) valid? if not urlPermitted(keyId, self.server.federationList): if self.server.debug: @@ -1484,7 +1492,9 @@ class PubServer(BaseHTTPRequestHandler): originalMessageJson = messageJson.copy() # whether to add a 'to' field to the message - addToFieldTypes = ('Follow', 'Like', 'Add', 'Remove', 'Ignore') + addToFieldTypes = ( + 'Follow', 'Like', 'EmojiReact', 'Add', 'Remove', 'Ignore' + ) for addToType in addToFieldTypes: messageJson, toFieldExists = \ addToField(addToType, messageJson, self.server.debug) @@ -5701,6 +5711,32 @@ class PubServer(BaseHTTPRequestHandler): notifyLikesFilename) pass + notifyReactionsFilename = \ + acctDir(baseDir, nickname, domain) + \ + '/.notifyReactions' + if onFinalWelcomeScreen: + # default setting from welcome screen + with open(notifyReactionsFilename, 'w+') as rFile: + rFile.write('\n') + actorChanged = True + else: + notifyReactionsActive = False + if fields.get('notifyReactions'): + if fields['notifyReactions'] == 'on': + notifyReactionsActive = True + with open(notifyReactionsFilename, + 'w+') as rFile: + rFile.write('\n') + if not notifyReactionsActive: + if os.path.isfile(notifyReactionsFilename): + try: + os.remove(notifyReactionsFilename) + except BaseException: + print('EX: _profileUpdate ' + + 'unable to delete ' + + notifyReactionsFilename) + pass + # this account is a bot if fields.get('isBot'): if fields['isBot'] == 'on': @@ -7862,6 +7898,473 @@ class PubServer(BaseHTTPRequestHandler): '_GET', '_undoLikeButton', self.server.debug) + def _reactionButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, + proxyType: str, cookie: str, + debug: str): + """Press an emoji reaction button + Note that this is not the emoji reaction selection icon at the + bottom of the post + """ + pageNumber = 1 + reactionUrl = path.split('?react=')[1] + if '?' in reactionUrl: + reactionUrl = reactionUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + actor = path.split('?react=')[0] + if '?page=' in path: + pageNumberStr = path.split('?page=')[1] + if '?' in pageNumberStr: + pageNumberStr = pageNumberStr.split('?')[0] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + timelineStr = 'inbox' + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + emojiContentEncoded = None + if '?emojreact=' in path: + emojiContentEncoded = path.split('?emojreact=')[1] + if '?' in emojiContentEncoded: + emojiContentEncoded = emojiContentEncoded.split('?')[0] + if not emojiContentEncoded: + print('WARN: no emoji reaction ' + actor) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, + callingDomain) + return + emojiContent = urllib.parse.unquote_plus(emojiContentEncoded) + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during emoji reaction') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: ' + + 'GET failed to create session during emoji reaction') + self._404() + self.server.GETbusy = False + return + reactionActor = \ + localActorUrl(httpPrefix, self.postToNickname, domainFull) + actorReaction = path.split('?actor=')[1] + if '?' in actorReaction: + actorReaction = actorReaction.split('?')[0] + + # if this is an announce then send the emoji reaction + # to the original post + origActor, origPostUrl, origFilename = \ + getOriginalPostFromAnnounceUrl(reactionUrl, baseDir, + self.postToNickname, domain) + reactionUrl2 = reactionUrl + reactionPostFilename = origFilename + if origActor and origPostUrl: + actorReaction = origActor + reactionUrl2 = origPostUrl + reactionPostFilename = None + + reactionJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'EmojiReact', + 'actor': reactionActor, + 'to': [actorReaction], + 'object': reactionUrl2, + 'content': emojiContent + } + + # send out the emoji reaction to followers + self._postToOutbox(reactionJson, self.server.projectVersion, None) + + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', '_reactionButton postToOutbox', + self.server.debug) + + print('Locating emoji reaction post ' + reactionUrl) + # directly emoji reaction the post file + if not reactionPostFilename: + reactionPostFilename = \ + locatePost(baseDir, self.postToNickname, domain, reactionUrl) + if reactionPostFilename: + recentPostsCache = self.server.recentPostsCache + reactionPostJson = loadJson(reactionPostFilename, 0, 1) + if origFilename and origPostUrl: + updateReactionCollection(recentPostsCache, + baseDir, reactionPostFilename, + reactionUrl, + reactionActor, self.postToNickname, + domain, debug, reactionPostJson, + emojiContent) + reactionUrl = origPostUrl + reactionPostFilename = origFilename + if debug: + print('Updating emoji reaction for ' + reactionPostFilename) + updateReactionCollection(recentPostsCache, + baseDir, reactionPostFilename, + reactionUrl, + reactionActor, + self.postToNickname, domain, + debug, None, emojiContent) + if debug: + print('Regenerating html post for changed ' + + 'emoji reaction collection') + # clear the icon from the cache so that it gets updated + if reactionPostJson: + cachedPostFilename = \ + getCachedPostFilename(baseDir, self.postToNickname, + domain, reactionPostJson) + if debug: + print('Reaction post json: ' + str(reactionPostJson)) + print('Reaction post nickname: ' + + self.postToNickname + ' ' + domain) + print('Reaction post cache: ' + str(cachedPostFilename)) + showIndividualPostIcons = True + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, + self.postToNickname, domain) + showRepeats = not isDM(reactionPostJson) + individualPostAsHtml(self.server.signingPrivateKeyPem, False, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, baseDir, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.postToNickname, domain, + self.server.port, reactionPostJson, + None, True, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + timelineStr, + self.server.YTReplacementDomain, + self.server.twitterReplacementDomain, + self.server.showPublishedDateOnly, + self.server.peertubeInstances, + self.server.allowLocalNetworkAccess, + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount, + showRepeats, + showIndividualPostIcons, + manuallyApproveFollowers, + False, True, False, + self.server.CWlists, + self.server.listsEnabled) + else: + print('WARN: Emoji reaction post not found: ' + + reactionPostFilename) + else: + print('WARN: unable to locate file for emoji reaction post ' + + reactionUrl) + + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, + callingDomain) + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', '_reactionButton', + self.server.debug) + + def _undoReactionButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, + proxyType: str, cookie: str, + debug: str): + """A button is pressed to undo emoji reaction + """ + pageNumber = 1 + reactionUrl = path.split('?unreact=')[1] + if '?' in reactionUrl: + reactionUrl = reactionUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + if '?page=' in path: + pageNumberStr = path.split('?page=')[1] + if '?' in pageNumberStr: + pageNumberStr = pageNumberStr.split('?')[0] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + timelineStr = 'inbox' + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + actor = path.split('?unreact=')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + self._redirect_headers(actorPathStr, cookie, + callingDomain) + return + emojiContentEncoded = None + if '?emojreact=' in path: + emojiContentEncoded = path.split('?emojreact=')[1] + if '?' in emojiContentEncoded: + emojiContentEncoded = emojiContentEncoded.split('?')[0] + if not emojiContentEncoded: + print('WARN: no emoji reaction ' + actor) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, + callingDomain) + return + emojiContent = urllib.parse.unquote_plus(emojiContentEncoded) + if not self.server.session: + print('Starting new session during undo emoji reaction') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during undo emoji reaction') + self._404() + self.server.GETbusy = False + return + undoActor = \ + localActorUrl(httpPrefix, self.postToNickname, domainFull) + actorReaction = path.split('?actor=')[1] + if '?' in actorReaction: + actorReaction = actorReaction.split('?')[0] + + # if this is an announce then send the emoji reaction + # to the original post + origActor, origPostUrl, origFilename = \ + getOriginalPostFromAnnounceUrl(reactionUrl, baseDir, + self.postToNickname, domain) + reactionUrl2 = reactionUrl + reactionPostFilename = origFilename + if origActor and origPostUrl: + actorReaction = origActor + reactionUrl2 = origPostUrl + reactionPostFilename = None + + undoReactionJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Undo', + 'actor': undoActor, + 'to': [actorReaction], + 'object': { + 'type': 'EmojiReaction', + 'actor': undoActor, + 'to': [actorReaction], + 'object': reactionUrl2 + } + } + + # send out the undo emoji reaction to followers + self._postToOutbox(undoReactionJson, self.server.projectVersion, None) + + # directly undo the emoji reaction within the post file + if not reactionPostFilename: + reactionPostFilename = \ + locatePost(baseDir, self.postToNickname, domain, reactionUrl) + if reactionPostFilename: + recentPostsCache = self.server.recentPostsCache + reactionPostJson = loadJson(reactionPostFilename, 0, 1) + if origFilename and origPostUrl: + undoReactionCollectionEntry(recentPostsCache, + baseDir, reactionPostFilename, + reactionUrl, + undoActor, domain, debug, + reactionPostJson, + emojiContent) + reactionUrl = origPostUrl + reactionPostFilename = origFilename + if debug: + print('Removing emoji reaction for ' + reactionPostFilename) + undoReactionCollectionEntry(recentPostsCache, + baseDir, + reactionPostFilename, reactionUrl, + undoActor, domain, debug, None, + emojiContent) + if debug: + print('Regenerating html post for changed ' + + 'emoji reaction collection') + if reactionPostJson: + showIndividualPostIcons = True + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, + self.postToNickname, domain) + showRepeats = not isDM(reactionPostJson) + individualPostAsHtml(self.server.signingPrivateKeyPem, False, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, baseDir, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.postToNickname, domain, + self.server.port, reactionPostJson, + None, True, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, timelineStr, + self.server.YTReplacementDomain, + self.server.twitterReplacementDomain, + self.server.showPublishedDateOnly, + self.server.peertubeInstances, + self.server.allowLocalNetworkAccess, + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount, + showRepeats, + showIndividualPostIcons, + manuallyApproveFollowers, + False, True, False, + self.server.CWlists, + self.server.listsEnabled) + else: + print('WARN: Unreaction post not found: ' + + reactionPostFilename) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, callingDomain) + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', '_undoReactionButton', + self.server.debug) + + def _reactionPicker(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, + proxyType: str, cookie: str, + debug: str) -> None: + """Press the emoji reaction picker icon at the bottom of the post + """ + pageNumber = 1 + reactionUrl = path.split('?selreact=')[1] + if '?' in reactionUrl: + reactionUrl = reactionUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + actor = path.split('?selreact=')[0] + if '?page=' in path: + pageNumberStr = path.split('?page=')[1] + if '?' in pageNumberStr: + pageNumberStr = pageNumberStr.split('?')[0] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + timelineStr = 'inbox' + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, callingDomain) + return + + postJsonObject = None + reactionPostFilename = \ + locatePost(self.server.baseDir, + self.postToNickname, domain, reactionUrl) + if reactionPostFilename: + postJsonObject = loadJson(reactionPostFilename) + if not reactionPostFilename or not postJsonObject: + print('WARN: unable to locate reaction post ' + reactionUrl) + self.server.GETbusy = False + actorAbsolute = self._getInstanceUrl(callingDomain) + actor + actorPathStr = \ + actorAbsolute + '/' + timelineStr + \ + '?page=' + str(pageNumber) + timelineBookmark + self._redirect_headers(actorPathStr, cookie, callingDomain) + return + + msg = \ + htmlEmojiReactionPicker(self.server.cssCache, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.baseDir, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.postToNickname, + domain, port, postJsonObject, + self.server.httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain, + self.server.twitterReplacementDomain, + self.server.showPublishedDateOnly, + self.server.peertubeInstances, + self.server.allowLocalNetworkAccess, + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.signingPrivateKeyPem, + self.server.CWlists, + self.server.listsEnabled, + self.server.defaultTimeline) + msg = msg.encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) + self._write(msg) + fitnessPerformance(GETstartTime, + self.server.fitness, + '_GET', '_reactionPicker', + self.server.debug) + self.server.GETbusy = False + def _bookmarkButton(self, callingDomain: str, path: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, @@ -8953,6 +9456,18 @@ class PubServer(BaseHTTPRequestHandler): likedBy = likedBy.split('?')[0] path = path.split('?likedBy=')[0] + reactBy = None + reactEmoji = None + if '?reactBy=' in path: + reactBy = path.split('?reactBy=')[1].strip() + if ';' in reactBy: + reactBy = reactBy.split(';')[0] + if ';emoj=' in path: + reactEmoji = path.split(';emoj=')[1].strip() + if ';' in reactEmoji: + reactEmoji = reactEmoji.split(';')[0] + path = path.split('?reactBy=')[0] + namedStatus = path.split('/@')[1] if '/' not in namedStatus: # show actor @@ -8977,6 +9492,7 @@ class PubServer(BaseHTTPRequestHandler): includeCreateWrapper = True result = self._showPostFromFile(postFilename, likedBy, + reactBy, reactEmoji, authorized, callingDomain, path, baseDir, httpPrefix, nickname, domain, domainFull, port, @@ -8990,6 +9506,7 @@ class PubServer(BaseHTTPRequestHandler): return result def _showPostFromFile(self, postFilename: str, likedBy: str, + reactBy: str, reactEmoji: str, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, nickname: str, @@ -9036,7 +9553,7 @@ class PubServer(BaseHTTPRequestHandler): postJsonObject, httpPrefix, self.server.projectVersion, - likedBy, + likedBy, reactBy, reactEmoji, self.server.YTReplacementDomain, self.server.twitterReplacementDomain, self.server.showPublishedDateOnly, @@ -9099,6 +9616,19 @@ class PubServer(BaseHTTPRequestHandler): if '?' in likedBy: likedBy = likedBy.split('?')[0] path = path.split('?likedBy=')[0] + + reactBy = None + reactEmoji = None + if '?reactBy=' in path: + reactBy = path.split('?reactBy=')[1].strip() + if ';' in reactBy: + reactBy = reactBy.split(';')[0] + if ';emoj=' in path: + reactEmoji = path.split(';emoj=')[1].strip() + if ';' in reactEmoji: + reactEmoji = reactEmoji.split(';')[0] + path = path.split('?reactBy=')[0] + namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False @@ -9120,6 +9650,7 @@ class PubServer(BaseHTTPRequestHandler): includeCreateWrapper = True result = self._showPostFromFile(postFilename, likedBy, + reactBy, reactEmoji, authorized, callingDomain, path, baseDir, httpPrefix, nickname, domain, domainFull, port, @@ -9144,6 +9675,8 @@ class PubServer(BaseHTTPRequestHandler): and where you have the notify checkbox set on person options """ likedBy = None + reactBy = None + reactEmoji = None postId = path.split('?notifypost=')[1].strip() postId = postId.replace('-', '/') path = path.split('?notifypost=')[0] @@ -9161,6 +9694,7 @@ class PubServer(BaseHTTPRequestHandler): includeCreateWrapper = True result = self._showPostFromFile(postFilename, likedBy, + reactBy, reactEmoji, authorized, callingDomain, path, baseDir, httpPrefix, nickname, domain, domainFull, port, @@ -12980,6 +13514,7 @@ class PubServer(BaseHTTPRequestHandler): nickname = self.path.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] + # return the featured posts collection self._getFeaturedCollection(callingDomain, self.server.baseDir, self.path, @@ -14346,7 +14881,7 @@ class PubServer(BaseHTTPRequestHandler): return fitnessPerformance(GETstartTime, self.server.fitness, - '_GET', 'like shown done', + '_GET', 'like button done', self.server.debug) # undo a like from the web interface icon @@ -14364,7 +14899,44 @@ class PubServer(BaseHTTPRequestHandler): return fitnessPerformance(GETstartTime, self.server.fitness, - '_GET', 'unlike shown done', + '_GET', 'unlike button done', + self.server.debug) + + # emoji reaction from the web interface icon + if authorized and htmlGET and '?react=' in self.path: + self._reactionButton(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, + self.server.proxyType, + cookie, + self.server.debug) + return + + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', 'emoji reaction button done', + self.server.debug) + + # undo an emoji reaction from the web interface icon + if authorized and htmlGET and '?unreact=' in self.path: + self._undoReactionButton(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, + self.server.proxyType, + cookie, self.server.debug) + return + + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', 'unreaction button done', self.server.debug) # bookmark from the web interface icon @@ -14386,6 +14958,25 @@ class PubServer(BaseHTTPRequestHandler): '_GET', 'bookmark shown done', self.server.debug) + # emoji recation from the web interface bottom icon + if authorized and htmlGET and '?selreact=' in self.path: + self._reactionPicker(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, + self.server.proxyType, + cookie, self.server.debug) + return + + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', 'bookmark shown done', + self.server.debug) + # undo a bookmark from the web interface icon if authorized and htmlGET and '?unbookmark=' in self.path: self._undoBookmarkButton(callingDomain, self.path, @@ -15495,8 +16086,10 @@ class PubServer(BaseHTTPRequestHandler): contentStr = \ getBaseContentFromPost(messageJson, self.server.systemLanguage) + followersOnly = False pinPost(self.server.baseDir, - nickname, self.server.domain, contentStr) + nickname, self.server.domain, contentStr, + followersOnly) return 1 if self._postToOutbox(messageJson, self.server.projectVersion, diff --git a/emoji/default_reactions.json b/emoji/default_reactions.json new file mode 100644 index 000000000..758cdeb44 --- /dev/null +++ b/emoji/default_reactions.json @@ -0,0 +1,1293 @@ +{ + "SMILEYS_AND_EMOTION": [ + "๐Ÿ˜€", + "๐Ÿ˜ƒ", + "๐Ÿ˜„", + "๐Ÿ˜", + "๐Ÿ˜†", + "๐Ÿ˜…", + "๐Ÿคฃ", + "๐Ÿ˜‚", + "๐Ÿ™‚", + "๐Ÿ™ƒ", + "๐Ÿ˜‰", + "๐Ÿ˜Š", + "๐Ÿ˜‡", + "๐Ÿฅฐ", + "๐Ÿ˜", + "๐Ÿคฉ", + "๐Ÿ˜˜", + "๐Ÿ˜—", + "โ˜บ", + "๐Ÿ˜š", + "๐Ÿ˜™", + "๐Ÿ˜‹", + "๐Ÿ˜›", + "๐Ÿ˜œ", + "๐Ÿคช", + "๐Ÿ˜", + "๐Ÿค‘", + "๐Ÿค—", + "๐Ÿคญ", + "๐Ÿคซ", + "๐Ÿค”", + "๐Ÿค", + "๐Ÿคจ", + "๐Ÿ˜", + "๐Ÿ˜‘", + "๐Ÿ˜ถ", + "๐Ÿ˜", + "๐Ÿ˜’", + "๐Ÿ™„", + "๐Ÿ˜ฌ", + "๐Ÿคฅ", + "๐Ÿ˜Œ", + "๐Ÿ˜”", + "๐Ÿ˜ช", + "๐Ÿคค", + "๐Ÿ˜ด", + "๐Ÿ˜ท", + "๐Ÿค’", + "๐Ÿค•", + "๐Ÿคข", + "๐Ÿคฎ", + "๐Ÿคง", + "๐Ÿฅต", + "๐Ÿฅถ", + "๐Ÿฅด", + "๐Ÿ˜ต", + "๐Ÿคฏ", + "๐Ÿค ", + "๐Ÿฅณ", + "๐Ÿ˜Ž", + "๐Ÿค“", + "๐Ÿง", + "๐Ÿ˜•", + "๐Ÿ˜Ÿ", + "๐Ÿ™", + "โ˜น", + "๐Ÿ˜ฎ", + "๐Ÿ˜ฏ", + "๐Ÿ˜ฒ", + "๐Ÿ˜ณ", + "๐Ÿฅบ", + "๐Ÿ˜ฆ", + "๐Ÿ˜ง", + "๐Ÿ˜จ", + "๐Ÿ˜ฐ", + "๐Ÿ˜ฅ", + "๐Ÿ˜ข", + "๐Ÿ˜ญ", + "๐Ÿ˜ฑ", + "๐Ÿ˜–", + "๐Ÿ˜ฃ", + "๐Ÿ˜ž", + "๐Ÿ˜“", + "๐Ÿ˜ฉ", + "๐Ÿ˜ซ", + "๐Ÿฅฑ", + "๐Ÿ˜ค", + "๐Ÿ˜ก", + "๐Ÿ˜ ", + "๐Ÿคฌ", + "๐Ÿ˜ˆ", + "๐Ÿ‘ฟ", + "๐Ÿ’€", + "โ˜ ", + "๐Ÿ’ฉ", + "๐Ÿคก", + "๐Ÿ‘น", + "๐Ÿ‘บ", + "๐Ÿ‘ป", + "๐Ÿ‘ฝ", + "๐Ÿ‘พ", + "๐Ÿค–", + "๐Ÿ˜บ", + "๐Ÿ˜ธ", + "๐Ÿ˜น", + "๐Ÿ˜ป", + "๐Ÿ˜ผ", + "๐Ÿ˜ฝ", + "๐Ÿ™€", + "๐Ÿ˜ฟ", + "๐Ÿ˜พ", + "๐Ÿ™ˆ", + "๐Ÿ™‰", + "๐Ÿ™Š", + "๐Ÿ’‹", + "๐Ÿ’Œ", + "๐Ÿ’˜", + "๐Ÿ’", + "๐Ÿ’–", + "๐Ÿ’—", + "๐Ÿ’“", + "๐Ÿ’ž", + "๐Ÿ’•", + "๐Ÿ’Ÿ", + "โฃ", + "๐Ÿ’”", + "โค", + "๐Ÿงก", + "๐Ÿ’›", + "๐Ÿ’š", + "๐Ÿ’™", + "๐Ÿ’œ", + "๐ŸคŽ", + "๐Ÿ–ค", + "๐Ÿค", + "๐Ÿ’ฏ", + "๐Ÿ’ข", + "๐Ÿ’ฅ", + "๐Ÿ’ซ", + "๐Ÿ’ฆ", + "๐Ÿ’จ", + "๐Ÿ•ณ", + "๐Ÿ’ฃ", + "๐Ÿ’ฌ", + "๐Ÿ—จ", + "๐Ÿ—ฏ", + "๐Ÿ’ญ", + "๐Ÿ’ค" + ], + "PEOPLE_AND_BODY": [ + "๐Ÿ‘‹", + "๐Ÿคš", + "๐Ÿ–", + "โœ‹", + "๐Ÿ––", + "๐Ÿ‘Œ", + "๐Ÿค", + "โœŒ", + "๐Ÿคž", + "๐ŸคŸ", + "๐Ÿค˜", + "๐Ÿค™", + "๐Ÿ‘ˆ", + "๐Ÿ‘‰", + "๐Ÿ‘†", + "๐Ÿ–•", + "๐Ÿ‘‡", + "โ˜", + "๐Ÿ‘", + "๐Ÿ‘Ž", + "โœŠ", + "๐Ÿ‘Š", + "๐Ÿค›", + "๐Ÿคœ", + "๐Ÿ‘", + "๐Ÿ™Œ", + "๐Ÿ‘", + "๐Ÿคฒ", + "๐Ÿค", + "๐Ÿ™", + "โœ", + "๐Ÿ’…", + "๐Ÿคณ", + "๐Ÿ’ช", + "๐Ÿฆพ", + "๐Ÿฆฟ", + "๐Ÿฆต", + "๐Ÿฆถ", + "๐Ÿ‘‚", + "๐Ÿฆป", + "๐Ÿ‘ƒ", + "๐Ÿง ", + "๐Ÿฆท", + "๐Ÿฆด", + "๐Ÿ‘€", + "๐Ÿ‘", + "๐Ÿ‘…", + "๐Ÿ‘„", + "๐Ÿ‘ถ", + "๐Ÿง’", + "๐Ÿ‘ฆ", + "๐Ÿ‘ง", + "๐Ÿง‘", + "๐Ÿ‘ฑ", + "๐Ÿ‘จ", + "๐Ÿง”", + "๐Ÿ‘ฉ", + "๐Ÿง“", + "๐Ÿ‘ด", + "๐Ÿ‘ต", + "๐Ÿ™", + "๐Ÿ™Ž", + "๐Ÿ™…", + "๐Ÿ™†", + "๐Ÿ’", + "๐Ÿ™‹", + "๐Ÿง", + "๐Ÿ™‡", + "๐Ÿคฆ", + "๐Ÿคท", + "๐Ÿ‘ฎ", + "๐Ÿ•ต", + "๐Ÿ’‚", + "๐Ÿ‘ท", + "๐Ÿคด", + "๐Ÿ‘ธ", + "๐Ÿ‘ณ", + "๐Ÿ‘ฒ", + "๐Ÿง•", + "๐Ÿคต", + "๐Ÿ‘ฐ", + "๐Ÿคฐ", + "๐Ÿคฑ", + "๐Ÿ‘ผ", + "๐ŸŽ…", + "๐Ÿคถ", + "๐Ÿฆธ", + "๐Ÿฆน", + "๐Ÿง™", + "๐Ÿงš", + "๐Ÿง›", + "๐Ÿงœ", + "๐Ÿง", + "๐Ÿงž", + "๐ŸงŸ", + "๐Ÿ’†", + "๐Ÿ’‡", + "๐Ÿšถ", + "๐Ÿง", + "๐ŸงŽ", + "๐Ÿƒ", + "๐Ÿ’ƒ", + "๐Ÿ•บ", + "๐Ÿ•ด", + "๐Ÿ‘ฏ", + "๐Ÿง–", + "๐Ÿง—", + "๐Ÿคบ", + "๐Ÿ‡", + "โ›ท", + "๐Ÿ‚", + "๐ŸŒ", + "๐Ÿ„", + "๐Ÿšฃ", + "๐ŸŠ", + "โ›น", + "๐Ÿ‹", + "๐Ÿšด", + "๐Ÿšต", + "๐Ÿคธ", + "๐Ÿคผ", + "๐Ÿคฝ", + "๐Ÿคพ", + "๐Ÿคน", + "๐Ÿง˜", + "๐Ÿ›€", + "๐Ÿ›Œ", + "๐Ÿ‘ญ", + "๐Ÿ‘ซ", + "๐Ÿ‘ฌ", + "๐Ÿ’", + "๐Ÿ’‘", + "๐Ÿ‘ช", + "๐Ÿ—ฃ", + "๐Ÿ‘ค", + "๐Ÿ‘ฅ", + "๐Ÿ‘ฃ" + ], + "COMPONENT": [ + "๐Ÿป", + "๐Ÿผ", + "๐Ÿฝ", + "๐Ÿพ", + "๐Ÿฟ", + "๐Ÿฆฐ", + "๐Ÿฆฑ", + "๐Ÿฆณ", + "๐Ÿฆฒ" + ], + "ANIMALS_AND_NATURE": [ + "๐Ÿต", + "๐Ÿ’", + "๐Ÿฆ", + "๐Ÿฆง", + "๐Ÿถ", + "๐Ÿ•", + "๐Ÿฆฎ", + "๐Ÿฉ", + "๐Ÿบ", + "๐ŸฆŠ", + "๐Ÿฆ", + "๐Ÿฑ", + "๐Ÿˆ", + "๐Ÿฆ", + "๐Ÿฏ", + "๐Ÿ…", + "๐Ÿ†", + "๐Ÿด", + "๐ŸŽ", + "๐Ÿฆ„", + "๐Ÿฆ“", + "๐ŸฆŒ", + "๐Ÿฎ", + "๐Ÿ‚", + "๐Ÿƒ", + "๐Ÿ„", + "๐Ÿท", + "๐Ÿ–", + "๐Ÿ—", + "๐Ÿฝ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ", + "๐Ÿช", + "๐Ÿซ", + "๐Ÿฆ™", + "๐Ÿฆ’", + "๐Ÿ˜", + "๐Ÿฆ", + "๐Ÿฆ›", + "๐Ÿญ", + "๐Ÿ", + "๐Ÿ€", + "๐Ÿน", + "๐Ÿฐ", + "๐Ÿ‡", + "๐Ÿฟ", + "๐Ÿฆ”", + "๐Ÿฆ‡", + "๐Ÿป", + "๐Ÿจ", + "๐Ÿผ", + "๐Ÿฆฅ", + "๐Ÿฆฆ", + "๐Ÿฆจ", + "๐Ÿฆ˜", + "๐Ÿฆก", + "๐Ÿพ", + "๐Ÿฆƒ", + "๐Ÿ”", + "๐Ÿ“", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฆ", + "๐Ÿง", + "๐Ÿ•Š", + "๐Ÿฆ…", + "๐Ÿฆ†", + "๐Ÿฆข", + "๐Ÿฆ‰", + "๐Ÿฆฉ", + "๐Ÿฆš", + "๐Ÿฆœ", + "๐Ÿธ", + "๐ŸŠ", + "๐Ÿข", + "๐ŸฆŽ", + "๐Ÿ", + "๐Ÿฒ", + "๐Ÿ‰", + "๐Ÿฆ•", + "๐Ÿฆ–", + "๐Ÿณ", + "๐Ÿ‹", + "๐Ÿฌ", + "๐ŸŸ", + "๐Ÿ ", + "๐Ÿก", + "๐Ÿฆˆ", + "๐Ÿ™", + "๐Ÿš", + "๐ŸŒ", + "๐Ÿฆ‹", + "๐Ÿ›", + "๐Ÿœ", + "๐Ÿ", + "๐Ÿž", + "๐Ÿฆ—", + "๐Ÿ•ท", + "๐Ÿ•ธ", + "๐Ÿฆ‚", + "๐ŸฆŸ", + "๐Ÿฆ ", + "๐Ÿ’", + "๐ŸŒธ", + "๐Ÿ’ฎ", + "๐Ÿต", + "๐ŸŒน", + "๐Ÿฅ€", + "๐ŸŒบ", + "๐ŸŒป", + "๐ŸŒผ", + "๐ŸŒท", + "๐ŸŒฑ", + "๐ŸŒฒ", + "๐ŸŒณ", + "๐ŸŒด", + "๐ŸŒต", + "๐ŸŒพ", + "๐ŸŒฟ", + "โ˜˜", + "๐Ÿ€", + "๐Ÿ", + "๐Ÿ‚", + "๐Ÿƒ" + ], + "FOOD_AND_DRINK": [ + "๐Ÿ‡", + "๐Ÿˆ", + "๐Ÿ‰", + "๐ŸŠ", + "๐Ÿ‹", + "๐ŸŒ", + "๐Ÿ", + "๐Ÿฅญ", + "๐ŸŽ", + "๐Ÿ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ’", + "๐Ÿ“", + "๐Ÿฅ", + "๐Ÿ…", + "๐Ÿฅฅ", + "๐Ÿฅ‘", + "๐Ÿ†", + "๐Ÿฅ”", + "๐Ÿฅ•", + "๐ŸŒฝ", + "๐ŸŒถ", + "๐Ÿฅ’", + "๐Ÿฅฌ", + "๐Ÿฅฆ", + "๐Ÿง„", + "๐Ÿง…", + "๐Ÿ„", + "๐Ÿฅœ", + "๐ŸŒฐ", + "๐Ÿž", + "๐Ÿฅ", + "๐Ÿฅ–", + "๐Ÿฅจ", + "๐Ÿฅฏ", + "๐Ÿฅž", + "๐Ÿง‡", + "๐Ÿง€", + "๐Ÿ–", + "๐Ÿ—", + "๐Ÿฅฉ", + "๐Ÿฅ“", + "๐Ÿ”", + "๐ŸŸ", + "๐Ÿ•", + "๐ŸŒญ", + "๐Ÿฅช", + "๐ŸŒฎ", + "๐ŸŒฏ", + "๐Ÿฅ™", + "๐Ÿง†", + "๐Ÿฅš", + "๐Ÿณ", + "๐Ÿฅ˜", + "๐Ÿฒ", + "๐Ÿฅฃ", + "๐Ÿฅ—", + "๐Ÿฟ", + "๐Ÿงˆ", + "๐Ÿง‚", + "๐Ÿฅซ", + "๐Ÿฑ", + "๐Ÿ˜", + "๐Ÿ™", + "๐Ÿš", + "๐Ÿ›", + "๐Ÿœ", + "๐Ÿ", + "๐Ÿ ", + "๐Ÿข", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฅฎ", + "๐Ÿก", + "๐ŸฅŸ", + "๐Ÿฅ ", + "๐Ÿฅก", + "๐Ÿฆ€", + "๐Ÿฆž", + "๐Ÿฆ", + "๐Ÿฆ‘", + "๐Ÿฆช", + "๐Ÿฆ", + "๐Ÿง", + "๐Ÿจ", + "๐Ÿฉ", + "๐Ÿช", + "๐ŸŽ‚", + "๐Ÿฐ", + "๐Ÿง", + "๐Ÿฅง", + "๐Ÿซ", + "๐Ÿฌ", + "๐Ÿญ", + "๐Ÿฎ", + "๐Ÿฏ", + "๐Ÿผ", + "๐Ÿฅ›", + "โ˜•", + "๐Ÿต", + "๐Ÿถ", + "๐Ÿพ", + "๐Ÿท", + "๐Ÿธ", + "๐Ÿน", + "๐Ÿบ", + "๐Ÿป", + "๐Ÿฅ‚", + "๐Ÿฅƒ", + "๐Ÿฅค", + "๐Ÿงƒ", + "๐Ÿง‰", + "๐ŸงŠ", + "๐Ÿฅข", + "๐Ÿฝ", + "๐Ÿด", + "๐Ÿฅ„", + "๐Ÿ”ช", + "๐Ÿบ" + ], + "TRAVEL_AND_PLACES": [ + "๐ŸŒ", + "๐ŸŒŽ", + "๐ŸŒ", + "๐ŸŒ", + "๐Ÿ—บ", + "๐Ÿ—พ", + "๐Ÿงญ", + "๐Ÿ”", + "โ›ฐ", + "๐ŸŒ‹", + "๐Ÿ—ป", + "๐Ÿ•", + "๐Ÿ–", + "๐Ÿœ", + "๐Ÿ", + "๐Ÿž", + "๐ŸŸ", + "๐Ÿ›", + "๐Ÿ—", + "๐Ÿงฑ", + "๐Ÿ˜", + "๐Ÿš", + "๐Ÿ ", + "๐Ÿก", + "๐Ÿข", + "๐Ÿฃ", + "๐Ÿค", + "๐Ÿฅ", + "๐Ÿฆ", + "๐Ÿจ", + "๐Ÿฉ", + "๐Ÿช", + "๐Ÿซ", + "๐Ÿฌ", + "๐Ÿญ", + "๐Ÿฏ", + "๐Ÿฐ", + "๐Ÿ’’", + "๐Ÿ—ผ", + "๐Ÿ—ฝ", + "โ›ช", + "๐Ÿ•Œ", + "๐Ÿ›•", + "๐Ÿ•", + "โ›ฉ", + "๐Ÿ•‹", + "โ›ฒ", + "โ›บ", + "๐ŸŒ", + "๐ŸŒƒ", + "๐Ÿ™", + "๐ŸŒ„", + "๐ŸŒ…", + "๐ŸŒ†", + "๐ŸŒ‡", + "๐ŸŒ‰", + "โ™จ", + "๐ŸŽ ", + "๐ŸŽก", + "๐ŸŽข", + "๐Ÿ’ˆ", + "๐ŸŽช", + "๐Ÿš‚", + "๐Ÿšƒ", + "๐Ÿš„", + "๐Ÿš…", + "๐Ÿš†", + "๐Ÿš‡", + "๐Ÿšˆ", + "๐Ÿš‰", + "๐ŸšŠ", + "๐Ÿš", + "๐Ÿšž", + "๐Ÿš‹", + "๐ŸšŒ", + "๐Ÿš", + "๐ŸšŽ", + "๐Ÿš", + "๐Ÿš‘", + "๐Ÿš’", + "๐Ÿš“", + "๐Ÿš”", + "๐Ÿš•", + "๐Ÿš–", + "๐Ÿš—", + "๐Ÿš˜", + "๐Ÿš™", + "๐Ÿšš", + "๐Ÿš›", + "๐Ÿšœ", + "๐ŸŽ", + "๐Ÿ", + "๐Ÿ›ต", + "๐Ÿฆฝ", + "๐Ÿฆผ", + "๐Ÿ›บ", + "๐Ÿšฒ", + "๐Ÿ›ด", + "๐Ÿ›น", + "๐Ÿš", + "๐Ÿ›ฃ", + "๐Ÿ›ค", + "๐Ÿ›ข", + "โ›ฝ", + "๐Ÿšจ", + "๐Ÿšฅ", + "๐Ÿšฆ", + "๐Ÿ›‘", + "๐Ÿšง", + "โš“", + "โ›ต", + "๐Ÿ›ถ", + "๐Ÿšค", + "๐Ÿ›ณ", + "โ›ด", + "๐Ÿ›ฅ", + "๐Ÿšข", + "โœˆ", + "๐Ÿ›ฉ", + "๐Ÿ›ซ", + "๐Ÿ›ฌ", + "๐Ÿช‚", + "๐Ÿ’บ", + "๐Ÿš", + "๐ŸšŸ", + "๐Ÿš ", + "๐Ÿšก", + "๐Ÿ›ฐ", + "๐Ÿš€", + "๐Ÿ›ธ", + "๐Ÿ›Ž", + "๐Ÿงณ", + "โŒ›", + "โณ", + "โŒš", + "โฐ", + "โฑ", + "โฒ", + "๐Ÿ•ฐ", + "๐Ÿ•›", + "๐Ÿ•ง", + "๐Ÿ•", + "๐Ÿ•œ", + "๐Ÿ•‘", + "๐Ÿ•", + "๐Ÿ•’", + "๐Ÿ•ž", + "๐Ÿ•“", + "๐Ÿ•Ÿ", + "๐Ÿ•”", + "๐Ÿ• ", + "๐Ÿ••", + "๐Ÿ•ก", + "๐Ÿ•–", + "๐Ÿ•ข", + "๐Ÿ•—", + "๐Ÿ•ฃ", + "๐Ÿ•˜", + "๐Ÿ•ค", + "๐Ÿ•™", + "๐Ÿ•ฅ", + "๐Ÿ•š", + "๐Ÿ•ฆ", + "๐ŸŒ‘", + "๐ŸŒ’", + "๐ŸŒ“", + "๐ŸŒ”", + "๐ŸŒ•", + "๐ŸŒ–", + "๐ŸŒ—", + "๐ŸŒ˜", + "๐ŸŒ™", + "๐ŸŒš", + "๐ŸŒ›", + "๐ŸŒœ", + "๐ŸŒก", + "โ˜€", + "๐ŸŒ", + "๐ŸŒž", + "๐Ÿช", + "โญ", + "๐ŸŒŸ", + "๐ŸŒ ", + "๐ŸŒŒ", + "โ˜", + "โ›…", + "โ›ˆ", + "๐ŸŒค", + "๐ŸŒฅ", + "๐ŸŒฆ", + "๐ŸŒง", + "๐ŸŒจ", + "๐ŸŒฉ", + "๐ŸŒช", + "๐ŸŒซ", + "๐ŸŒฌ", + "๐ŸŒ€", + "๐ŸŒˆ", + "๐ŸŒ‚", + "โ˜‚", + "โ˜”", + "โ›ฑ", + "โšก", + "โ„", + "โ˜ƒ", + "โ›„", + "โ˜„", + "๐Ÿ”ฅ", + "๐Ÿ’ง", + "๐ŸŒŠ" + ], + "ACTIVITIES": [ + "๐ŸŽƒ", + "๐ŸŽ„", + "๐ŸŽ†", + "๐ŸŽ‡", + "๐Ÿงจ", + "โœจ", + "๐ŸŽˆ", + "๐ŸŽ‰", + "๐ŸŽŠ", + "๐ŸŽ‹", + "๐ŸŽ", + "๐ŸŽŽ", + "๐ŸŽ", + "๐ŸŽ", + "๐ŸŽ‘", + "๐Ÿงง", + "๐ŸŽ€", + "๐ŸŽ", + "๐ŸŽ—", + "๐ŸŽŸ", + "๐ŸŽซ", + "๐ŸŽ–", + "๐Ÿ†", + "๐Ÿ…", + "๐Ÿฅ‡", + "๐Ÿฅˆ", + "๐Ÿฅ‰", + "โšฝ", + "โšพ", + "๐ŸฅŽ", + "๐Ÿ€", + "๐Ÿ", + "๐Ÿˆ", + "๐Ÿ‰", + "๐ŸŽพ", + "๐Ÿฅ", + "๐ŸŽณ", + "๐Ÿ", + "๐Ÿ‘", + "๐Ÿ’", + "๐Ÿฅ", + "๐Ÿ“", + "๐Ÿธ", + "๐ŸฅŠ", + "๐Ÿฅ‹", + "๐Ÿฅ…", + "โ›ณ", + "โ›ธ", + "๐ŸŽฃ", + "๐Ÿคฟ", + "๐ŸŽฝ", + "๐ŸŽฟ", + "๐Ÿ›ท", + "๐ŸฅŒ", + "๐ŸŽฏ", + "๐Ÿช€", + "๐Ÿช", + "๐ŸŽฑ", + "๐Ÿ”ฎ", + "๐Ÿงฟ", + "๐ŸŽฎ", + "๐Ÿ•น", + "๐ŸŽฐ", + "๐ŸŽฒ", + "๐Ÿงฉ", + "๐Ÿงธ", + "โ™ ", + "โ™ฅ", + "โ™ฆ", + "โ™ฃ", + "โ™Ÿ", + "๐Ÿƒ", + "๐Ÿ€„", + "๐ŸŽด", + "๐ŸŽญ", + "๐Ÿ–ผ", + "๐ŸŽจ", + "๐Ÿงต", + "๐Ÿงถ" + ], + "OBJECTS": [ + "๐Ÿ‘“", + "๐Ÿ•ถ", + "๐Ÿฅฝ", + "๐Ÿฅผ", + "๐Ÿฆบ", + "๐Ÿ‘”", + "๐Ÿ‘•", + "๐Ÿ‘–", + "๐Ÿงฃ", + "๐Ÿงค", + "๐Ÿงฅ", + "๐Ÿงฆ", + "๐Ÿ‘—", + "๐Ÿ‘˜", + "๐Ÿฅป", + "๐Ÿฉฑ", + "๐Ÿฉฒ", + "๐Ÿฉณ", + "๐Ÿ‘™", + "๐Ÿ‘š", + "๐Ÿ‘›", + "๐Ÿ‘œ", + "๐Ÿ‘", + "๐Ÿ›", + "๐ŸŽ’", + "๐Ÿ‘ž", + "๐Ÿ‘Ÿ", + "๐Ÿฅพ", + "๐Ÿฅฟ", + "๐Ÿ‘ ", + "๐Ÿ‘’", + "๐Ÿฉฐ", + "๐Ÿ‘‘", + "๐ŸŽฉ", + "๐ŸŽ“", + "๐Ÿงข", + "โ›‘", + "๐Ÿ“ฟ", + "๐Ÿ’„", + "๐Ÿ’", + "๐Ÿ’Ž", + "๐Ÿ”‡", + "๐Ÿ”ˆ", + "๐Ÿ”‰", + "๐Ÿ”Š", + "๐Ÿ“ข", + "๐Ÿ“ฃ", + "๐Ÿ“ฏ", + "๐Ÿ””", + "๐Ÿ”•", + "๐ŸŽผ", + "๐ŸŽต", + "๐ŸŽถ", + "๐ŸŽ™", + "๐ŸŽš", + "๐ŸŽ›", + "๐ŸŽค", + "๐ŸŽง", + "๐Ÿ“ป", + "๐ŸŽท", + "๐ŸŽธ", + "๐ŸŽน", + "๐ŸŽบ", + "๐ŸŽป", + "๐Ÿช•", + "๐Ÿฅ", + "๐Ÿ“ฑ", + "๐Ÿ“ฒ", + "โ˜Ž", + "๐Ÿ“ž", + "๐Ÿ“Ÿ", + "๐Ÿ“ ", + "๐Ÿ”‹", + "๐Ÿ”Œ", + "๐Ÿ’ป", + "๐Ÿ–ฅ", + "๐Ÿ–จ", + "โŒจ", + "๐Ÿ–ฑ", + "๐Ÿ–ฒ", + "๐Ÿ’ฝ", + "๐Ÿ’พ", + "๐Ÿ’ฟ", + "๐Ÿ“€", + "๐Ÿงฎ", + "๐ŸŽฅ", + "๐ŸŽž", + "๐Ÿ“ฝ", + "๐ŸŽฌ", + "๐Ÿ“บ", + "๐Ÿ“ท", + "๐Ÿ“ธ", + "๐Ÿ“น", + "๐Ÿ“ผ", + "๐Ÿ”", + "๐Ÿ”Ž", + "๐Ÿ•ฏ", + "๐Ÿ’ก", + "๐Ÿ”ฆ", + "๐Ÿฎ", + "๐Ÿช”", + "๐Ÿ“”", + "๐Ÿ“•", + "๐Ÿ“–", + "๐Ÿ“—", + "๐Ÿ“˜", + "๐Ÿ“™", + "๐Ÿ“š", + "๐Ÿ““", + "๐Ÿ“’", + "๐Ÿ“ƒ", + "๐Ÿ“œ", + "๐Ÿ“„", + "๐Ÿ“ฐ", + "๐Ÿ—ž", + "๐Ÿ“‘", + "๐Ÿ”–", + "๐Ÿท", + "๐Ÿ’ฐ", + "๐Ÿ’ด", + "๐Ÿ’ต", + "๐Ÿ’ถ", + "๐Ÿ’ท", + "๐Ÿ’ธ", + "๐Ÿ’ณ", + "๐Ÿงพ", + "๐Ÿ’น", + "๐Ÿ’ฑ", + "๐Ÿ’ฒ", + "โœ‰", + "๐Ÿ“ง", + "๐Ÿ“จ", + "๐Ÿ“ฉ", + "๐Ÿ“ค", + "๐Ÿ“ฅ", + "๐Ÿ“ฆ", + "๐Ÿ“ซ", + "๐Ÿ“ช", + "๐Ÿ“ฌ", + "๐Ÿ“ญ", + "๐Ÿ“ฎ", + "๐Ÿ—ณ", + "โœ", + "โœ’", + "๐Ÿ–‹", + "๐Ÿ–Š", + "๐Ÿ–Œ", + "๐Ÿ–", + "๐Ÿ“", + "๐Ÿ’ผ", + "๐Ÿ“", + "๐Ÿ“‚", + "๐Ÿ—‚", + "๐Ÿ“…", + "๐Ÿ“†", + "๐Ÿ—’", + "๐Ÿ—“", + "๐Ÿ“‡", + "๐Ÿ“ˆ", + "๐Ÿ“‰", + "๐Ÿ“Š", + "๐Ÿ“‹", + "๐Ÿ“Œ", + "๐Ÿ“", + "๐Ÿ“Ž", + "๐Ÿ–‡", + "๐Ÿ“", + "๐Ÿ“", + "โœ‚", + "๐Ÿ—ƒ", + "๐Ÿ—„", + "๐Ÿ—‘", + "๐Ÿ”’", + "๐Ÿ”“", + "๐Ÿ”", + "๐Ÿ”", + "๐Ÿ”‘", + "๐Ÿ—", + "๐Ÿ”จ", + "๐Ÿช“", + "โ›", + "โš’", + "๐Ÿ› ", + "๐Ÿ—ก", + "โš”", + "๐Ÿ”ซ", + "๐Ÿน", + "๐Ÿ›ก", + "๐Ÿ”ง", + "๐Ÿ”ฉ", + "โš™", + "๐Ÿ—œ", + "โš–", + "๐Ÿฆฏ", + "๐Ÿ”—", + "โ›“", + "๐Ÿงฐ", + "๐Ÿงฒ", + "โš—", + "๐Ÿงช", + "๐Ÿงซ", + "๐Ÿงฌ", + "๐Ÿ”ฌ", + "๐Ÿ”ญ", + "๐Ÿ“ก", + "๐Ÿ’‰", + "๐Ÿฉธ", + "๐Ÿ’Š", + "๐Ÿฉน", + "๐Ÿฉบ", + "๐Ÿšช", + "๐Ÿ›", + "๐Ÿ›‹", + "๐Ÿช‘", + "๐Ÿšฝ", + "๐Ÿšฟ", + "๐Ÿ›", + "๐Ÿช’", + "๐Ÿงด", + "๐Ÿงท", + "๐Ÿงน", + "๐Ÿงบ", + "๐Ÿงป", + "๐Ÿงผ", + "๐Ÿงฝ", + "๐Ÿงฏ", + "๐Ÿ›’", + "๐Ÿšฌ", + "โšฐ", + "โšฑ", + "๐Ÿ—ฟ" + ], + "SYMBOLS": [ + "๐Ÿง", + "๐Ÿšฎ", + "๐Ÿšฐ", + "โ™ฟ", + "๐Ÿšน", + "๐Ÿšบ", + "๐Ÿšป", + "๐Ÿšผ", + "๐Ÿšพ", + "๐Ÿ›‚", + "๐Ÿ›ƒ", + "๐Ÿ›„", + "๐Ÿ›…", + "โš ", + "๐Ÿšธ", + "โ›”", + "๐Ÿšซ", + "๐Ÿšณ", + "๐Ÿšญ", + "๐Ÿšฏ", + "๐Ÿšฑ", + "๐Ÿšท", + "๐Ÿ“ต", + "๐Ÿ”ž", + "โ˜ข", + "โ˜ฃ", + "โฌ†", + "โ†—", + "โžก", + "โ†˜", + "โฌ‡", + "โ†™", + "โฌ…", + "โ†–", + "โ†•", + "โ†”", + "โ†ฉ", + "โ†ช", + "โคด", + "โคต", + "๐Ÿ”ƒ", + "๐Ÿ”„", + "๐Ÿ”™", + "๐Ÿ”š", + "๐Ÿ”›", + "๐Ÿ”œ", + "๐Ÿ”", + "๐Ÿ›", + "โš›", + "๐Ÿ•‰", + "โœก", + "โ˜ธ", + "โ˜ฏ", + "โœ", + "โ˜ฆ", + "โ˜ช", + "โ˜ฎ", + "๐Ÿ•Ž", + "๐Ÿ”ฏ", + "โ™ˆ", + "โ™‰", + "โ™Š", + "โ™‹", + "โ™Œ", + "โ™", + "โ™Ž", + "โ™", + "โ™", + "โ™‘", + "โ™’", + "โ™“", + "โ›Ž", + "๐Ÿ”€", + "๐Ÿ”", + "๐Ÿ”‚", + "โ–ถ", + "โฉ", + "โญ", + "โฏ", + "โ—€", + "โช", + "โฎ", + "๐Ÿ”ผ", + "โซ", + "๐Ÿ”ฝ", + "โฌ", + "โธ", + "โน", + "โบ", + "โ", + "๐ŸŽฆ", + "๐Ÿ”…", + "๐Ÿ”†", + "๐Ÿ“ถ", + "๐Ÿ“ณ", + "๐Ÿ“ด", + "โ™€", + "โ™‚", + "โš•", + "โ™พ", + "โ™ป", + "โšœ", + "๐Ÿ”ฑ", + "๐Ÿ“›", + "๐Ÿ”ฐ", + "โญ•", + "โœ…", + "โ˜‘", + "โœ”", + "โœ–", + "โŒ", + "โŽ", + "โž•", + "โž–", + "โž—", + "โžฐ", + "โžฟ", + "ใ€ฝ", + "โœณ", + "โœด", + "โ‡", + "โ€ผ", + "โ‰", + "โ“", + "โ”", + "โ•", + "โ—", + "ใ€ฐ", + "ยฉ", + "ยฎ", + "โ„ข", + "๐Ÿ”Ÿ", + "๐Ÿ” ", + "๐Ÿ”ก", + "๐Ÿ”ข", + "๐Ÿ”ฃ", + "๐Ÿ”ค", + "๐Ÿ…ฐ", + "๐Ÿ†Ž", + "๐Ÿ…ฑ", + "๐Ÿ†‘", + "๐Ÿ†’", + "๐Ÿ†“", + "โ„น", + "๐Ÿ†”", + "โ“‚", + "๐Ÿ†•", + "๐Ÿ†–", + "๐Ÿ…พ", + "๐Ÿ†—", + "๐Ÿ…ฟ", + "๐Ÿ†˜", + "๐Ÿ†™", + "๐Ÿ†š", + "๐Ÿˆ", + "๐Ÿˆ‚", + "๐Ÿˆท", + "๐Ÿˆถ", + "๐Ÿˆฏ", + "๐Ÿ‰", + "๐Ÿˆน", + "๐Ÿˆš", + "๐Ÿˆฒ", + "๐Ÿ‰‘", + "๐Ÿˆธ", + "๐Ÿˆด", + "๐Ÿˆณ", + "ใŠ—", + "ใŠ™", + "๐Ÿˆบ", + "๐Ÿˆต", + "๐Ÿ”ด", + "๐ŸŸ ", + "๐ŸŸก", + "๐ŸŸข", + "๐Ÿ”ต", + "๐ŸŸฃ", + "๐ŸŸค", + "โšซ", + "โšช", + "๐ŸŸฅ", + "๐ŸŸง", + "๐ŸŸจ", + "๐ŸŸฉ", + "๐ŸŸฆ", + "๐ŸŸช", + "๐ŸŸซ", + "โฌ›", + "โฌœ", + "โ—ผ", + "โ—ป", + "โ—พ", + "โ—ฝ", + "โ–ช", + "โ–ซ", + "๐Ÿ”ถ", + "๐Ÿ”ท", + "๐Ÿ”ธ", + "๐Ÿ”น", + "๐Ÿ”บ", + "๐Ÿ”ป", + "๐Ÿ’ ", + "๐Ÿ”˜", + "๐Ÿ”ณ", + "๐Ÿ”ฒ" + ], + "FLAGS": [ + "๐Ÿ", + "๐Ÿšฉ", + "๐ŸŽŒ", + "๐Ÿด", + "๐Ÿณ" + ] +} diff --git a/epicyon-profile.css b/epicyon-profile.css index 45b41ac4d..b6a6add5e 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -51,6 +51,10 @@ --font-size-pgp-key2: 18px; --font-size-tox: 16px; --font-size-tox2: 18px; + --font-size-emoji-reaction: 16px; + --font-size-emoji-reaction-mobile: 24px; + --follow-text-size1: 24px; + --follow-text-size2: 40px; --time-color: #aaa; --time-vertical-align: 0%; --time-vertical-align-mobile: 1.5%; @@ -1037,6 +1041,10 @@ div.container { font-size: var(--font-size); color: var(--title-color); } + .followText { + font-size: var(--follow-text-size1); + font-family: Arial, Helvetica, sans-serif; + } .accountsTable { width: 100%; border: 0; @@ -1731,14 +1739,39 @@ div.container { .pageslist { } .voteresult { - width: var(--voteresult-width); - height: var(--voteresult-height); + width: var(--voteresult-width); + height: var(--voteresult-height); } .voteresultbar { - height: var(--voteresult-height); - fill: var(--voteresult-color); - stroke-width: 10; - stroke: var(--voteresult-border-color) + height: var(--voteresult-height); + fill: var(--voteresult-color); + stroke-width: 10; + stroke: var(--voteresult-border-color) + } + .emojiReactionBar { + } + .emojiReactionButton { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + line-height: 2rem; + margin-right: 6px; + padding: 1px 6px; + border: 1px solid var(--border-color); + border-radius: 10px; + background-color: var(--main-bg-color); + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + font-size: var(--font-size-emoji-reaction); + } + .rlab { + font-size: var(--font-size); + margin: 15px 15px; + line-height: 200%; } } @@ -2446,13 +2479,42 @@ div.container { .pageslist { } .voteresult { - width: var(--voteresult-width-mobile); - height: var(--voteresult-height-mobile); + width: var(--voteresult-width-mobile); + height: var(--voteresult-height-mobile); } .voteresultbar { - height: var(--voteresult-height-mobile); - fill: var(--voteresult-color); - stroke-width: 10; - stroke: var(--voteresult-border-color) + height: var(--voteresult-height-mobile); + fill: var(--voteresult-color); + stroke-width: 10; + stroke: var(--voteresult-border-color) + } + .emojiReactionBar { + } + .emojiReactionButton { + display: -webkit-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; + line-height: 2rem; + margin-right: 6px; + padding: 1px 6px; + border: 1px solid var(--border-color); + border-radius: 10px; + background-color: var(--main-bg-color); + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + font-size: var(--font-size-emoji-reaction-mobile); + } + .followText { + font-size: var(--follow-text-size2); + font-family: Arial, Helvetica, sans-serif; + } + .rlab { + font-size: var(--font-size-mobile); + margin: 25px 25px; + line-height: 300%; } } diff --git a/epicyon.py b/epicyon.py index 9cf52179e..7cc492481 100644 --- a/epicyon.py +++ b/epicyon.py @@ -81,6 +81,9 @@ from media import getAttachmentMediaType from delete import sendDeleteViaServer from like import sendLikeViaServer from like import sendUndoLikeViaServer +from reaction import sendReactionViaServer +from reaction import sendUndoReactionViaServer +from reaction import validEmojiContent from skills import sendSkillViaServer from availability import setAvailability from availability import sendAvailabilityViaServer @@ -510,6 +513,13 @@ parser.add_argument('--favorite', '--like', dest='like', type=str, default=None, help='Like a url') parser.add_argument('--undolike', '--unlike', dest='undolike', type=str, default=None, help='Undo a like of a url') +parser.add_argument('--react', '--reaction', dest='react', type=str, + default=None, help='Reaction url') +parser.add_argument('--emoji', type=str, + default=None, help='Reaction emoji') +parser.add_argument('--undoreact', '--undoreaction', dest='undoreact', + type=str, + default=None, help='Reaction url') parser.add_argument('--bookmark', '--bm', dest='bookmark', type=str, default=None, help='Bookmark the url of a post') @@ -1612,6 +1622,45 @@ if args.like: time.sleep(1) sys.exit() +if args.react: + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + if not args.emoji: + print('Specify a reaction emoji with the --emoji option') + sys.exit() + if not validEmojiContent(args.emoji): + print('This is not a valid emoji') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + if not domain: + domain = getConfigParam(baseDir, 'domain') + signingPrivateKeyPem = None + if args.secureMode: + signingPrivateKeyPem = getInstanceActorKey(baseDir, domain) + print('Sending emoji reaction ' + args.emoji + ' to ' + args.react) + + sendReactionViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, args.react, args.emoji, + cachedWebfingers, personCache, + True, __version__, signingPrivateKeyPem) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + if args.undolike: if not args.nickname: print('Specify a nickname with the --nickname option') @@ -1646,6 +1695,46 @@ if args.undolike: time.sleep(1) sys.exit() +if args.undoreact: + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + if not args.emoji: + print('Specify a reaction emoji with the --emoji option') + sys.exit() + if not validEmojiContent(args.emoji): + print('This is not a valid emoji') + sys.exit() + + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + if not domain: + domain = getConfigParam(baseDir, 'domain') + signingPrivateKeyPem = None + if args.secureMode: + signingPrivateKeyPem = getInstanceActorKey(baseDir, domain) + print('Sending undo emoji reaction ' + args.emoji + ' to ' + args.react) + + sendUndoReactionViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, args.undoreact, args.emoji, + cachedWebfingers, personCache, + True, __version__, + signingPrivateKeyPem) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + if args.bookmark: if not args.nickname: print('Specify a nickname with the --nickname option') diff --git a/inbox.py b/inbox.py index fab4fca6e..a2bbdf81d 100644 --- a/inbox.py +++ b/inbox.py @@ -15,6 +15,9 @@ import random from linked_data_sig import verifyJsonSignature from languages import understoodPostLanguage from like import updateLikesCollection +from reaction import updateReactionCollection +from reaction import validEmojiContent +from utils import removeHtml from utils import fileLastModified from utils import hasObjectString from utils import hasObjectStringObject @@ -50,6 +53,7 @@ from utils import removeModerationPostFromIndex from utils import loadJson from utils import saveJson from utils import undoLikesCollectionEntry +from utils import undoReactionCollectionEntry from utils import hasGroupType from utils import localActorUrl from utils import hasObjectStringType @@ -393,7 +397,8 @@ def inboxMessageHasParams(messageJson: {}) -> bool: return False if not messageJson.get('to'): - allowedWithoutToParam = ['Like', 'Follow', 'Join', 'Request', + allowedWithoutToParam = ['Like', 'EmojiReact', + 'Follow', 'Join', 'Request', 'Accept', 'Capability', 'Undo'] if messageJson['type'] not in allowedWithoutToParam: return False @@ -415,7 +420,9 @@ def inboxPermittedMessage(domain: str, messageJson: {}, if not urlPermitted(actor, federationList): return False - alwaysAllowedTypes = ('Follow', 'Join', 'Like', 'Delete', 'Announce') + alwaysAllowedTypes = ( + 'Follow', 'Join', 'Like', 'EmojiReact', 'Delete', 'Announce' + ) if messageJson['type'] not in alwaysAllowedTypes: if not hasObjectDict(messageJson): return True @@ -1203,6 +1210,275 @@ def _receiveUndoLike(recentPostsCache: {}, return True +def _receiveReaction(recentPostsCache: {}, + session, handle: str, isGroup: bool, baseDir: str, + httpPrefix: str, domain: str, port: int, + onionDomain: str, + sendThreads: [], postLog: [], cachedWebfingers: {}, + personCache: {}, messageJson: {}, federationList: [], + debug: bool, + signingPrivateKeyPem: str, + maxRecentPosts: int, translate: {}, + allowDeletion: bool, + YTReplacementDomain: str, + twitterReplacementDomain: str, + peertubeInstances: [], + allowLocalNetworkAccess: bool, + themeName: str, systemLanguage: str, + maxLikeCount: int, CWlists: {}, + listsEnabled: str) -> bool: + """Receives an emoji reaction within the POST section of HTTPServer + """ + if messageJson['type'] != 'EmojiReact': + return False + if not hasActor(messageJson, debug): + return False + if not hasObjectString(messageJson, debug): + return False + if not messageJson.get('to'): + if debug: + print('DEBUG: ' + messageJson['type'] + ' has no "to" list') + return False + if not messageJson.get('content'): + if debug: + print('DEBUG: ' + messageJson['type'] + ' has no "content"') + return False + if not isinstance(messageJson['content'], str): + if debug: + print('DEBUG: ' + messageJson['type'] + ' content is not string') + return False + if not validEmojiContent(messageJson['content']): + print('_receiveReaction: Invalid emoji reaction: "' + + messageJson['content'] + '" from ' + messageJson['actor']) + return False + if not hasUsersPath(messageJson['actor']): + if debug: + print('DEBUG: "users" or "profile" missing from actor in ' + + messageJson['type']) + return False + if '/statuses/' not in messageJson['object']: + if debug: + print('DEBUG: "statuses" missing from object in ' + + messageJson['type']) + return False + if not os.path.isdir(baseDir + '/accounts/' + handle): + print('DEBUG: unknown recipient of emoji reaction - ' + handle) + # if this post in the outbox of the person? + handleName = handle.split('@')[0] + handleDom = handle.split('@')[1] + postReactionId = messageJson['object'] + emojiContent = removeHtml(messageJson['content']) + if not emojiContent: + if debug: + print('DEBUG: emoji reaction has no content') + return True + postFilename = locatePost(baseDir, handleName, handleDom, postReactionId) + if not postFilename: + if debug: + print('DEBUG: emoji reaction post not found in inbox or outbox') + print(postReactionId) + return True + if debug: + print('DEBUG: emoji reaction post found in inbox') + + reactionActor = messageJson['actor'] + handleName = handle.split('@')[0] + handleDom = handle.split('@')[1] + if not _alreadyReacted(baseDir, + handleName, handleDom, + postReactionId, + reactionActor, + emojiContent): + _reactionNotify(baseDir, domain, onionDomain, handle, + reactionActor, postReactionId, emojiContent) + updateReactionCollection(recentPostsCache, baseDir, postFilename, + postReactionId, reactionActor, + handleName, domain, debug, None, emojiContent) + # regenerate the html + reactionPostJson = loadJson(postFilename, 0, 1) + if reactionPostJson: + if reactionPostJson.get('type'): + if reactionPostJson['type'] == 'Announce' and \ + reactionPostJson.get('object'): + if isinstance(reactionPostJson['object'], str): + announceReactionUrl = reactionPostJson['object'] + announceReactionFilename = \ + locatePost(baseDir, handleName, + domain, announceReactionUrl) + if announceReactionFilename: + postReactionId = announceReactionUrl + postFilename = announceReactionFilename + updateReactionCollection(recentPostsCache, + baseDir, + postFilename, + postReactionId, + reactionActor, + handleName, + domain, debug, None, + emojiContent) + if reactionPostJson: + if debug: + cachedPostFilename = \ + getCachedPostFilename(baseDir, handleName, domain, + reactionPostJson) + print('Reaction post json: ' + str(reactionPostJson)) + print('Reaction post nickname: ' + handleName + ' ' + domain) + print('Reaction post cache: ' + str(cachedPostFilename)) + pageNumber = 1 + showPublishedDateOnly = False + showIndividualPostIcons = True + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, handleName, domain) + notDM = not isDM(reactionPostJson) + individualPostAsHtml(signingPrivateKeyPem, False, + recentPostsCache, maxRecentPosts, + translate, pageNumber, baseDir, + session, cachedWebfingers, personCache, + handleName, domain, port, reactionPostJson, + None, True, allowDeletion, + httpPrefix, __version__, + 'inbox', + YTReplacementDomain, + twitterReplacementDomain, + showPublishedDateOnly, + peertubeInstances, + allowLocalNetworkAccess, + themeName, systemLanguage, + maxLikeCount, notDM, + showIndividualPostIcons, + manuallyApproveFollowers, + False, True, False, CWlists, + listsEnabled) + return True + + +def _receiveUndoReaction(recentPostsCache: {}, + session, handle: str, isGroup: bool, baseDir: str, + httpPrefix: str, domain: str, port: int, + sendThreads: [], postLog: [], cachedWebfingers: {}, + personCache: {}, messageJson: {}, federationList: [], + debug: bool, + signingPrivateKeyPem: str, + maxRecentPosts: int, translate: {}, + allowDeletion: bool, + YTReplacementDomain: str, + twitterReplacementDomain: str, + peertubeInstances: [], + allowLocalNetworkAccess: bool, + themeName: str, systemLanguage: str, + maxLikeCount: int, CWlists: {}, + listsEnabled: str) -> bool: + """Receives an undo emoji reaction within the POST section of HTTPServer + """ + if messageJson['type'] != 'Undo': + return False + if not hasActor(messageJson, debug): + return False + if not hasObjectStringType(messageJson, debug): + return False + if messageJson['object']['type'] != 'EmojiReact': + return False + if not hasObjectStringObject(messageJson, debug): + return False + if not messageJson['object'].get('content'): + if debug: + print('DEBUG: ' + messageJson['type'] + ' has no "content"') + return False + if not isinstance(messageJson['object']['content'], str): + if debug: + print('DEBUG: ' + messageJson['type'] + ' content is not string') + return False + if not hasUsersPath(messageJson['actor']): + if debug: + print('DEBUG: "users" or "profile" missing from actor in ' + + messageJson['type'] + ' reaction') + return False + if '/statuses/' not in messageJson['object']['object']: + if debug: + print('DEBUG: "statuses" missing from reaction object in ' + + messageJson['type']) + return False + if not os.path.isdir(baseDir + '/accounts/' + handle): + print('DEBUG: unknown recipient of undo reaction - ' + handle) + # if this post in the outbox of the person? + handleName = handle.split('@')[0] + handleDom = handle.split('@')[1] + postFilename = \ + locatePost(baseDir, handleName, handleDom, + messageJson['object']['object']) + if not postFilename: + if debug: + print('DEBUG: unreaction post not found in inbox or outbox') + print(messageJson['object']['object']) + return True + if debug: + print('DEBUG: reaction post found in inbox. Now undoing.') + reactionActor = messageJson['actor'] + postReactionId = messageJson['object'] + emojiContent = removeHtml(messageJson['object']['content']) + if not emojiContent: + if debug: + print('DEBUG: unreaction has no content') + return True + undoReactionCollectionEntry(recentPostsCache, baseDir, postFilename, + postReactionId, reactionActor, domain, + debug, None, emojiContent) + # regenerate the html + reactionPostJson = loadJson(postFilename, 0, 1) + if reactionPostJson: + if reactionPostJson.get('type'): + if reactionPostJson['type'] == 'Announce' and \ + reactionPostJson.get('object'): + if isinstance(reactionPostJson['object'], str): + announceReactionUrl = reactionPostJson['object'] + announceReactionFilename = \ + locatePost(baseDir, handleName, + domain, announceReactionUrl) + if announceReactionFilename: + postReactionId = announceReactionUrl + postFilename = announceReactionFilename + undoReactionCollectionEntry(recentPostsCache, baseDir, + postFilename, + postReactionId, + reactionActor, domain, + debug, None, + emojiContent) + if reactionPostJson: + if debug: + cachedPostFilename = \ + getCachedPostFilename(baseDir, handleName, domain, + reactionPostJson) + print('Unreaction post json: ' + str(reactionPostJson)) + print('Unreaction post nickname: ' + handleName + ' ' + domain) + print('Unreaction post cache: ' + str(cachedPostFilename)) + pageNumber = 1 + showPublishedDateOnly = False + showIndividualPostIcons = True + manuallyApproveFollowers = \ + followerApprovalActive(baseDir, handleName, domain) + notDM = not isDM(reactionPostJson) + individualPostAsHtml(signingPrivateKeyPem, False, + recentPostsCache, maxRecentPosts, + translate, pageNumber, baseDir, + session, cachedWebfingers, personCache, + handleName, domain, port, reactionPostJson, + None, True, allowDeletion, + httpPrefix, __version__, + 'inbox', + YTReplacementDomain, + twitterReplacementDomain, + showPublishedDateOnly, + peertubeInstances, + allowLocalNetworkAccess, + themeName, systemLanguage, + maxLikeCount, notDM, + showIndividualPostIcons, + manuallyApproveFollowers, + False, True, False, CWlists, + listsEnabled) + return True + + def _receiveBookmark(recentPostsCache: {}, session, handle: str, isGroup: bool, baseDir: str, httpPrefix: str, domain: str, port: int, @@ -2068,6 +2344,40 @@ def _alreadyLiked(baseDir: str, nickname: str, domain: str, return False +def _alreadyReacted(baseDir: str, nickname: str, domain: str, + postUrl: str, reactionActor: str, + emojiContent: str) -> bool: + """Is the given post already emoji reacted by the given handle? + """ + postFilename = \ + locatePost(baseDir, nickname, domain, postUrl) + if not postFilename: + return False + postJsonObject = loadJson(postFilename, 1) + if not postJsonObject: + return False + if not hasObjectDict(postJsonObject): + return False + if not postJsonObject['object'].get('reactions'): + return False + if not postJsonObject['object']['reactions'].get('items'): + return False + for react in postJsonObject['object']['reactions']['items']: + if not react.get('type'): + continue + if not react.get('content'): + continue + if not react.get('actor'): + continue + if react['type'] != 'EmojiReact': + continue + if react['content'] != emojiContent: + continue + if react['actor'] == reactionActor: + return True + return False + + def _likeNotify(baseDir: str, domain: str, onionDomain: str, handle: str, actor: str, url: str) -> None: """Creates a notification that a like has arrived @@ -2130,6 +2440,71 @@ def _likeNotify(baseDir: str, domain: str, onionDomain: str, pass +def _reactionNotify(baseDir: str, domain: str, onionDomain: str, + handle: str, actor: str, + url: str, emojiContent: str) -> None: + """Creates a notification that an emoji reaction has arrived + """ + # This is not you reacting to your own post + if actor in url: + return + + # check that the reaction post was by this handle + nickname = handle.split('@')[0] + if '/' + domain + '/users/' + nickname not in url: + if not onionDomain: + return + if '/' + onionDomain + '/users/' + nickname not in url: + return + + accountDir = baseDir + '/accounts/' + handle + + # are reaction notifications enabled? + notifyReactionEnabledFilename = accountDir + '/.notifyReactions' + if not os.path.isfile(notifyReactionEnabledFilename): + return + + reactionFile = accountDir + '/.newReaction' + if os.path.isfile(reactionFile): + if '##sent##' not in open(reactionFile).read(): + return + + reactionNickname = getNicknameFromActor(actor) + reactionDomain, reactionPort = getDomainFromActor(actor) + if reactionNickname and reactionDomain: + reactionHandle = reactionNickname + '@' + reactionDomain + else: + print('_reactionNotify reactionHandle: ' + + str(reactionNickname) + '@' + str(reactionDomain)) + reactionHandle = actor + if reactionHandle != handle: + reactionStr = \ + reactionHandle + ' ' + url + '?reactBy=' + actor + \ + ';emoj=' + emojiContent + prevReactionFile = accountDir + '/.prevReaction' + # was there a previous reaction notification? + if os.path.isfile(prevReactionFile): + # is it the same as the current notification ? + with open(prevReactionFile, 'r') as fp: + prevReactionStr = fp.read() + if prevReactionStr == reactionStr: + return + try: + with open(prevReactionFile, 'w+') as fp: + fp.write(reactionStr) + except BaseException: + print('EX: ERROR: unable to save previous reaction notification ' + + prevReactionFile) + pass + try: + with open(reactionFile, 'w+') as fp: + fp.write(reactionStr) + except BaseException: + print('EX: ERROR: unable to write reaction notification file ' + + reactionFile) + pass + + def _notifyPostArrival(baseDir: str, handle: str, url: str) -> None: """Creates a notification that a new post has arrived. This is for followed accounts with the notify checkbox enabled @@ -2834,6 +3209,51 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, print('DEBUG: Undo like accepted from ' + actor) return False + if _receiveReaction(recentPostsCache, + session, handle, isGroup, + baseDir, httpPrefix, + domain, port, + onionDomain, + sendThreads, postLog, + cachedWebfingers, + personCache, + messageJson, + federationList, + debug, signingPrivateKeyPem, + maxRecentPosts, translate, + allowDeletion, + YTReplacementDomain, + twitterReplacementDomain, + peertubeInstances, + allowLocalNetworkAccess, + themeName, systemLanguage, + maxLikeCount, CWlists, listsEnabled): + if debug: + print('DEBUG: Reaction accepted from ' + actor) + return False + + if _receiveUndoReaction(recentPostsCache, + session, handle, isGroup, + baseDir, httpPrefix, + domain, port, + sendThreads, postLog, + cachedWebfingers, + personCache, + messageJson, + federationList, + debug, signingPrivateKeyPem, + maxRecentPosts, translate, + allowDeletion, + YTReplacementDomain, + twitterReplacementDomain, + peertubeInstances, + allowLocalNetworkAccess, + themeName, systemLanguage, + maxLikeCount, CWlists, listsEnabled): + if debug: + print('DEBUG: Undo reaction accepted from ' + actor) + return False + if _receiveBookmark(recentPostsCache, session, handle, isGroup, baseDir, httpPrefix, diff --git a/like.py b/like.py index e1df5a5de..ef1c57a5d 100644 --- a/like.py +++ b/like.py @@ -35,6 +35,22 @@ from auth import createBasicAuthHeader from posts import getPersonBox +def noOfLikes(postJsonObject: {}) -> int: + """Returns the number of likes ona given post + """ + obj = postJsonObject + if hasObjectDict(postJsonObject): + obj = postJsonObject['object'] + if not obj.get('likes'): + return 0 + if not isinstance(obj['likes'], dict): + return 0 + if not obj['likes'].get('items'): + obj['likes']['items'] = [] + obj['likes']['totalItems'] = 0 + return len(obj['likes']['items']) + + def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool: """Returns True if the given post is liked by the given person """ @@ -52,22 +68,6 @@ def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool: return False -def noOfLikes(postJsonObject: {}) -> int: - """Returns the number of likes ona given post - """ - obj = postJsonObject - if hasObjectDict(postJsonObject): - obj = postJsonObject['object'] - if not obj.get('likes'): - return 0 - if not isinstance(obj['likes'], dict): - return 0 - if not obj['likes'].get('items'): - obj['likes']['items'] = [] - obj['likes']['totalItems'] = 0 - return len(obj['likes']['items']) - - def _like(recentPostsCache: {}, session, baseDir: str, federationList: [], nickname: str, domain: str, port: int, diff --git a/media.py b/media.py index 184c8d2af..5b742a5e1 100644 --- a/media.py +++ b/media.py @@ -140,7 +140,7 @@ def _spoofMetaData(baseDir: str, nickname: str, domain: str, camMake, camModel, camSerialNumber) = \ spoofGeolocation(baseDir, spoofCity, currTimeAdjusted, decoySeed, None, None) - if os.system('exiftool -artist="' + nickname + '" ' + + if os.system('exiftool -artist=@"' + nickname + '@' + domain + '" ' + '-Make="' + camMake + '" ' + '-Model="' + camModel + '" ' + '-Comment="' + str(camSerialNumber) + '" ' + diff --git a/outbox.py b/outbox.py index 6b0bc0583..f9f42de9f 100644 --- a/outbox.py +++ b/outbox.py @@ -48,6 +48,8 @@ from skills import outboxSkills from availability import outboxAvailability from like import outboxLike from like import outboxUndoLike +from reaction import outboxReaction +from reaction import outboxUndoReaction from bookmarks import outboxBookmark from bookmarks import outboxUndoBookmark from delete import outboxDelete @@ -338,9 +340,10 @@ def postMessageToOutbox(session, translate: {}, '/system/' + 'media_attachments/files/') - permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo', - 'Update', 'Add', 'Remove', 'Block', 'Delete', - 'Skill', 'Ignore') + permittedOutboxTypes = ( + 'Create', 'Announce', 'Like', 'EmojiReact', 'Follow', 'Undo', + 'Update', 'Add', 'Remove', 'Block', 'Delete', 'Skill', 'Ignore' + ) if messageJson['type'] not in permittedOutboxTypes: if debug: print('DEBUG: POST to outbox - ' + messageJson['type'] + @@ -547,6 +550,20 @@ def postMessageToOutbox(session, translate: {}, baseDir, httpPrefix, postToNickname, domain, port, messageJson, debug) + + if debug: + print('DEBUG: handle any emoji reaction requests') + outboxReaction(recentPostsCache, + baseDir, httpPrefix, + postToNickname, domain, port, + messageJson, debug) + if debug: + print('DEBUG: handle any undo emoji reaction requests') + outboxUndoReaction(recentPostsCache, + baseDir, httpPrefix, + postToNickname, domain, port, + messageJson, debug) + if debug: print('DEBUG: handle any undo announce requests') outboxUndoAnnounce(recentPostsCache, diff --git a/person.py b/person.py index 3594dd849..ada52381e 100644 --- a/person.py +++ b/person.py @@ -635,6 +635,13 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, with open(notifyLikesFilename, 'w+') as nFile: nFile.write('\n') + # notify when posts have emoji reactions + if nickname != 'news': + notifyReactionsFilename = \ + acctDir(baseDir, nickname, domain) + '/.notifyReactions' + with open(notifyReactionsFilename, 'w+') as nFile: + nFile.write('\n') + theme = getConfigParam(baseDir, 'theme') if not theme: theme = 'default' diff --git a/posts.py b/posts.py index 38be89bd0..347821b10 100644 --- a/posts.py +++ b/posts.py @@ -1562,7 +1562,7 @@ def _postIsAddressedToFollowers(baseDir: str, def pinPost(baseDir: str, nickname: str, domain: str, - pinnedContent: str) -> None: + pinnedContent: str, followersOnly: bool) -> None: """Pins the given post Id to the profile of then given account """ accountDir = acctDir(baseDir, nickname, domain) @@ -1581,7 +1581,6 @@ def undoPinnedPost(baseDir: str, nickname: str, domain: str) -> None: os.remove(pinnedFilename) except BaseException: print('EX: undoPinnedPost unable to delete ' + pinnedFilename) - pass def getPinnedPostAsJson(baseDir: str, httpPrefix: str, @@ -3507,6 +3506,11 @@ def removePostInteractions(postJsonObject: {}, force: bool) -> bool: postObj['likes'] = { 'items': [] } + # clear the reactions + if postObj.get('reactions'): + postObj['reactions'] = { + 'items': [] + } # remove other collections removeCollections = ( 'replies', 'shares', 'bookmarks', 'ignores' diff --git a/reaction.py b/reaction.py new file mode 100644 index 000000000..536dbc06e --- /dev/null +++ b/reaction.py @@ -0,0 +1,564 @@ +__filename__ = "reaction.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.2.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@libreserver.org" +__status__ = "Production" +__module_group__ = "ActivityPub" + +import os +import re +import urllib.parse +from pprint import pprint +from utils import hasObjectString +from utils import hasObjectStringObject +from utils import hasObjectStringType +from utils import removeDomainPort +from utils import hasObjectDict +from utils import hasUsersPath +from utils import getFullDomain +from utils import removeIdEnding +from utils import urlPermitted +from utils import getNicknameFromActor +from utils import getDomainFromActor +from utils import locatePost +from utils import undoReactionCollectionEntry +from utils import hasGroupType +from utils import localActorUrl +from utils import loadJson +from utils import saveJson +from utils import removePostFromCache +from utils import getCachedPostFilename +from utils import containsInvalidChars +from posts import sendSignedJson +from session import postJson +from webfinger import webfingerHandle +from auth import createBasicAuthHeader +from posts import getPersonBox + +# the maximum number of reactions from individual actors which can be +# added to a post. Hence an adversary can't bombard you with sockpuppet +# generated reactions and make the post infeasibly large +maxActorReactionsPerPost = 64 + +# regex defining permissable emoji icon range +emojiRegex = re.compile(r'[\u263a-\U0001f645]') + + +def validEmojiContent(emojiContent: str) -> bool: + """Is the given emoji content valid? + """ + if not emojiContent: + return False + if len(emojiContent) > 1: + return False + if len(emojiRegex.findall(emojiContent)) == 0: + return False + if containsInvalidChars(emojiContent): + return False + return True + + +def _reaction(recentPostsCache: {}, + session, baseDir: str, federationList: [], + nickname: str, domain: str, port: int, + ccList: [], httpPrefix: str, + objectUrl: str, emojiContent: str, + actorReaction: str, + clientToServer: bool, + sendThreads: [], postLog: [], + personCache: {}, cachedWebfingers: {}, + debug: bool, projectVersion: str, + signingPrivateKeyPem: str) -> {}: + """Creates an emoji reaction + actor is the person doing the reacting + 'to' might be a specific person (actor) whose post was reaction + object is typically the url of the message which was reaction + """ + if not urlPermitted(objectUrl, federationList): + return None + if not validEmojiContent(emojiContent): + print('_reaction: Invalid emoji reaction: "' + emojiContent + '"') + return + + fullDomain = getFullDomain(domain, port) + + newReactionJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'EmojiReact', + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), + 'object': objectUrl, + 'content': emojiContent + } + if ccList: + if len(ccList) > 0: + newReactionJson['cc'] = ccList + + # Extract the domain and nickname from a statuses link + reactionPostNickname = None + reactionPostDomain = None + reactionPostPort = None + groupAccount = False + if actorReaction: + reactionPostNickname = getNicknameFromActor(actorReaction) + reactionPostDomain, reactionPostPort = \ + getDomainFromActor(actorReaction) + groupAccount = hasGroupType(baseDir, actorReaction, personCache) + else: + if hasUsersPath(objectUrl): + reactionPostNickname = getNicknameFromActor(objectUrl) + reactionPostDomain, reactionPostPort = \ + getDomainFromActor(objectUrl) + if '/' + str(reactionPostNickname) + '/' in objectUrl: + actorReaction = \ + objectUrl.split('/' + reactionPostNickname + '/')[0] + \ + '/' + reactionPostNickname + groupAccount = \ + hasGroupType(baseDir, actorReaction, personCache) + + if reactionPostNickname: + postFilename = locatePost(baseDir, nickname, domain, objectUrl) + if not postFilename: + print('DEBUG: reaction baseDir: ' + baseDir) + print('DEBUG: reaction nickname: ' + nickname) + print('DEBUG: reaction domain: ' + domain) + print('DEBUG: reaction objectUrl: ' + objectUrl) + return None + + updateReactionCollection(recentPostsCache, + baseDir, postFilename, objectUrl, + newReactionJson['actor'], + nickname, domain, debug, None, + emojiContent) + + sendSignedJson(newReactionJson, session, baseDir, + nickname, domain, port, + reactionPostNickname, + reactionPostDomain, reactionPostPort, + 'https://www.w3.org/ns/activitystreams#Public', + httpPrefix, True, clientToServer, federationList, + sendThreads, postLog, cachedWebfingers, personCache, + debug, projectVersion, None, groupAccount, + signingPrivateKeyPem, 7165392) + + return newReactionJson + + +def reactionPost(recentPostsCache: {}, + session, baseDir: str, federationList: [], + nickname: str, domain: str, port: int, httpPrefix: str, + reactionNickname: str, reactionDomain: str, reactionPort: int, + ccList: [], + reactionStatusNumber: int, emojiContent: str, + clientToServer: bool, + sendThreads: [], postLog: [], + personCache: {}, cachedWebfingers: {}, + debug: bool, projectVersion: str, + signingPrivateKeyPem: str) -> {}: + """Adds a reaction to a given status post. This is only used by unit tests + """ + reactionDomain = getFullDomain(reactionDomain, reactionPort) + + actorReaction = localActorUrl(httpPrefix, reactionNickname, reactionDomain) + objectUrl = actorReaction + '/statuses/' + str(reactionStatusNumber) + + return _reaction(recentPostsCache, + session, baseDir, federationList, + nickname, domain, port, + ccList, httpPrefix, objectUrl, emojiContent, + actorReaction, clientToServer, + sendThreads, postLog, personCache, cachedWebfingers, + debug, projectVersion, signingPrivateKeyPem) + + +def sendReactionViaServer(baseDir: str, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, reactionUrl: str, + emojiContent: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str, + signingPrivateKeyPem: str) -> {}: + """Creates a reaction via c2s + """ + if not session: + print('WARN: No session for sendReactionViaServer') + return 6 + if not validEmojiContent(emojiContent): + print('sendReactionViaServer: Invalid emoji reaction: "' + + emojiContent + '"') + return 7 + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + + newReactionJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'EmojiReact', + 'actor': actor, + 'object': reactionUrl, + 'content': emojiContent + } + + handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + fromDomain, projectVersion, debug, False, + signingPrivateKeyPem) + if not wfRequest: + if debug: + print('DEBUG: reaction webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: reaction webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + originDomain = fromDomain + (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl, + displayName, _) = getPersonBox(signingPrivateKeyPem, + originDomain, + baseDir, session, wfRequest, + personCache, + projectVersion, httpPrefix, + fromNickname, fromDomain, + postToBox, 72873) + + if not inboxUrl: + if debug: + print('DEBUG: reaction no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: reaction no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(httpPrefix, fromDomainFull, + session, newReactionJson, [], inboxUrl, + headers, 3, True) + if not postResult: + if debug: + print('WARN: POST reaction failed for c2s to ' + inboxUrl) + return 5 + + if debug: + print('DEBUG: c2s POST reaction success') + + return newReactionJson + + +def sendUndoReactionViaServer(baseDir: str, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, reactionUrl: str, + emojiContent: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str, + signingPrivateKeyPem: str) -> {}: + """Undo a reaction via c2s + """ + if not session: + print('WARN: No session for sendUndoReactionViaServer') + return 6 + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + + newUndoReactionJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Undo', + 'actor': actor, + 'object': { + 'type': 'EmojiReact', + 'actor': actor, + 'object': reactionUrl, + 'content': emojiContent + } + } + + handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + fromDomain, projectVersion, debug, False, + signingPrivateKeyPem) + if not wfRequest: + if debug: + print('DEBUG: unreaction webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + if debug: + print('WARN: unreaction webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + originDomain = fromDomain + (inboxUrl, pubKeyId, pubKey, fromPersonId, sharedInbox, avatarUrl, + displayName, _) = getPersonBox(signingPrivateKeyPem, + originDomain, + baseDir, session, wfRequest, + personCache, projectVersion, + httpPrefix, fromNickname, + fromDomain, postToBox, + 72625) + + if not inboxUrl: + if debug: + print('DEBUG: unreaction no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: unreaction no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = postJson(httpPrefix, fromDomainFull, + session, newUndoReactionJson, [], inboxUrl, + headers, 3, True) + if not postResult: + if debug: + print('WARN: POST unreaction failed for c2s to ' + inboxUrl) + return 5 + + if debug: + print('DEBUG: c2s POST unreaction success') + + return newUndoReactionJson + + +def outboxReaction(recentPostsCache: {}, + baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool) -> None: + """ When a reaction request is received by the outbox from c2s + """ + if not messageJson.get('type'): + if debug: + print('DEBUG: reaction - no type') + return + if not messageJson['type'] == 'EmojiReact': + if debug: + print('DEBUG: not a reaction') + return + if not hasObjectString(messageJson, debug): + return + if not messageJson.get('content'): + return + if not isinstance(messageJson['content'], str): + return + if not validEmojiContent(messageJson['content']): + print('outboxReaction: Invalid emoji reaction: "' + + messageJson['content'] + '"') + return + if debug: + print('DEBUG: c2s reaction request arrived in outbox') + + messageId = removeIdEnding(messageJson['object']) + domain = removeDomainPort(domain) + emojiContent = messageJson['content'] + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + if debug: + print('DEBUG: c2s reaction post not found in inbox or outbox') + print(messageId) + return True + updateReactionCollection(recentPostsCache, + baseDir, postFilename, messageId, + messageJson['actor'], + nickname, domain, debug, None, emojiContent) + if debug: + print('DEBUG: post reaction via c2s - ' + postFilename) + + +def outboxUndoReaction(recentPostsCache: {}, + baseDir: str, httpPrefix: str, + nickname: str, domain: str, port: int, + messageJson: {}, debug: bool) -> None: + """ When an undo reaction request is received by the outbox from c2s + """ + if not messageJson.get('type'): + return + if not messageJson['type'] == 'Undo': + return + if not hasObjectStringType(messageJson, debug): + return + if not messageJson['object']['type'] == 'EmojiReact': + if debug: + print('DEBUG: not a undo reaction') + return + if not messageJson['object'].get('content'): + return + if not isinstance(messageJson['object']['content'], str): + return + if not hasObjectStringObject(messageJson, debug): + return + if debug: + print('DEBUG: c2s undo reaction request arrived in outbox') + + messageId = removeIdEnding(messageJson['object']['object']) + emojiContent = messageJson['object']['content'] + domain = removeDomainPort(domain) + postFilename = locatePost(baseDir, nickname, domain, messageId) + if not postFilename: + if debug: + print('DEBUG: c2s undo reaction post not found in inbox or outbox') + print(messageId) + return True + undoReactionCollectionEntry(recentPostsCache, baseDir, postFilename, + messageId, messageJson['actor'], + domain, debug, None, emojiContent) + if debug: + print('DEBUG: post undo reaction via c2s - ' + postFilename) + + +def updateReactionCollection(recentPostsCache: {}, + baseDir: str, postFilename: str, + objectUrl: str, actor: str, + nickname: str, domain: str, debug: bool, + postJsonObject: {}, + emojiContent: str) -> None: + """Updates the reactions collection within a post + """ + if not postJsonObject: + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return + + # remove any cached version of this post so that the + # reaction icon is changed + removePostFromCache(postJsonObject, recentPostsCache) + cachedPostFilename = getCachedPostFilename(baseDir, nickname, + domain, postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + try: + os.remove(cachedPostFilename) + except BaseException: + print('EX: updateReactionCollection unable to delete ' + + cachedPostFilename) + pass + + obj = postJsonObject + if hasObjectDict(postJsonObject): + obj = postJsonObject['object'] + + if not objectUrl.endswith('/reactions'): + objectUrl = objectUrl + '/reactions' + if not obj.get('reactions'): + if debug: + print('DEBUG: Adding initial emoji reaction to ' + objectUrl) + reactionsJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': objectUrl, + 'type': 'Collection', + "totalItems": 1, + 'items': [{ + 'type': 'EmojiReact', + 'actor': actor, + 'content': emojiContent + }] + } + obj['reactions'] = reactionsJson + else: + if not obj['reactions'].get('items'): + obj['reactions']['items'] = [] + # upper limit for the number of reactions on a post + if len(obj['reactions']['items']) >= maxActorReactionsPerPost: + return + for reactionItem in obj['reactions']['items']: + if reactionItem.get('actor') and reactionItem.get('content'): + if reactionItem['actor'] == actor and \ + reactionItem['content'] == emojiContent: + # already reaction + return + newReaction = { + 'type': 'EmojiReact', + 'actor': actor, + 'content': emojiContent + } + obj['reactions']['items'].append(newReaction) + itlen = len(obj['reactions']['items']) + obj['reactions']['totalItems'] = itlen + + if debug: + print('DEBUG: saving post with emoji reaction added') + pprint(postJsonObject) + saveJson(postJsonObject, postFilename) + + +def htmlEmojiReactions(postJsonObject: {}, interactive: bool, + actor: str, maxReactionTypes: int) -> str: + """html containing row of emoji reactions + """ + if not hasObjectDict(postJsonObject): + return '' + if not postJsonObject['object'].get('reactions'): + return '' + if not postJsonObject['object']['reactions'].get('items'): + return '' + reactions = {} + reactedToByThisActor = [] + for item in postJsonObject['object']['reactions']['items']: + emojiContent = item['content'] + emojiActor = item['actor'] + if emojiActor == actor: + if emojiContent not in reactedToByThisActor: + reactedToByThisActor.append(emojiContent) + if not reactions.get(emojiContent): + if len(reactions.items()) < maxReactionTypes: + reactions[emojiContent] = 1 + else: + reactions[emojiContent] += 1 + if len(reactions.items()) == 0: + return '' + reactBy = removeIdEnding(postJsonObject['object']['id']) + htmlStr = '
\n' + for emojiContent, count in reactions.items(): + if emojiContent not in reactedToByThisActor: + baseUrl = actor + '?react=' + reactBy + '?emojreact=' + else: + baseUrl = actor + '?unreact=' + reactBy + '?emojreact=' + + htmlStr += '
\n' + if count < 100: + countStr = str(count) + else: + countStr = '99+' + emojiContentStr = emojiContent + countStr + if interactive: + # urlencode the emoji + emojiContentEncoded = urllib.parse.quote_plus(emojiContent) + emojiContentStr = \ + ' ' + \ + emojiContentStr + '\n' + htmlStr += emojiContentStr + htmlStr += '
\n' + htmlStr += '
\n' + return htmlStr diff --git a/scripts/epicyon-notification b/scripts/epicyon-notification index a92db2b01..d764e4771 100755 --- a/scripts/epicyon-notification +++ b/scripts/epicyon-notification @@ -215,6 +215,21 @@ function notifications { fi fi + # send notifications for emoji reactions to XMPP/email users + epicyonLikeFile="$epicyonDir/.newReaction" + if [ -f "$epicyonReactionFile" ]; then + if ! grep -q "##sent##" "$epicyonReactionFile"; then + epicyonReactionMessage=$(notification_translate_text 'Reaction by') + epicyonReactionFileContent=$(cat "$epicyonReactionFile" | awk -F ' ' '{print $1}')" "$(echo "$epicyonReactionMessage")" "$(cat "$epicyonReactionFile" | awk -F ' ' '{print $2}') + if [[ "$epicyonReactionFileContent" == *':'* ]]; then + epicyonReactionMessage="Epicyon: $epicyonReactionFileContent" + fi + sendNotification "$USERNAME" "Epicyon" "$epicyonReactionMessage" + echo "##sent##" > "$epicyonReactionFile" + chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonReactionFile" + fi + fi + # send notifications for posts arriving from a particular person epicyonNotifyFile="$epicyonDir/.newNotifiedPost" if [ -f "$epicyonNotifyFile" ]; then diff --git a/session.py b/session.py index 5f23da457..a1f51f017 100644 --- a/session.py +++ b/session.py @@ -196,8 +196,8 @@ def _getJsonSigned(session, url: str, domainFull: str, sessionHeaders: {}, sessionHeaders['Accept'] = signatureHeaderJson['accept'] sessionHeaders['Signature'] = signatureHeaderJson['signature'] sessionHeaders['Content-Length'] = '0' - # if debug: - print('Signed GET sessionHeaders ' + str(sessionHeaders)) + if debug: + print('Signed GET sessionHeaders ' + str(sessionHeaders)) return _getJsonRequest(session, url, domainFull, sessionHeaders, sessionParams, timeoutSec, None, quiet, debug) diff --git a/tests.py b/tests.py index 616389903..6e3641260 100644 --- a/tests.py +++ b/tests.py @@ -107,6 +107,9 @@ from auth import authorizeBasic from auth import storeBasicCredentials from like import likePost from like import sendLikeViaServer +from reaction import reactionPost +from reaction import sendReactionViaServer +from reaction import validEmojiContent from announce import announcePublic from announce import sendAnnounceViaServer from city import parseNogoString @@ -1370,6 +1373,28 @@ def testPostMessageBetweenServers(baseDir: str) -> None: assert 'likes' in open(outboxPostFilename).read() + print('\n\n*******************************************************') + print("Bob reacts to Alice's post") + + assert reactionPost({}, sessionBob, bobDir, federationList, + 'bob', bobDomain, bobPort, httpPrefix, + 'alice', aliceDomain, alicePort, [], + statusNumber, '๐Ÿ˜€', + False, bobSendThreads, bobPostLog, + bobPersonCache, bobCachedWebfingers, + True, __version__, signingPrivateKeyPem) + + for i in range(20): + if 'reactions' in open(outboxPostFilename).read(): + break + time.sleep(1) + + alicePostJson = loadJson(outboxPostFilename, 0) + if alicePostJson: + pprint(alicePostJson) + + assert 'reactions' in open(outboxPostFilename).read() + print('\n\n*******************************************************') print("Bob repeats Alice's post") objectUrl = \ @@ -3071,22 +3096,62 @@ def testClientToServer(baseDir: str): time.sleep(1) showTestBoxes('alice', aliceInboxPath, aliceOutboxPath) showTestBoxes('bob', bobInboxPath, bobOutboxPath) - assert len([name for name in os.listdir(bobOutboxPath) - if os.path.isfile(os.path.join(bobOutboxPath, name))]) == 2 - assert len([name for name in os.listdir(aliceInboxPath) - if os.path.isfile(os.path.join(aliceInboxPath, name))]) == 0 + bobOutboxPathCtr = \ + len([name for name in os.listdir(bobOutboxPath) + if os.path.isfile(os.path.join(bobOutboxPath, name))]) + print('bobOutboxPathCtr: ' + str(bobOutboxPathCtr)) + assert bobOutboxPathCtr == 2 + aliceInboxPathCtr = \ + len([name for name in os.listdir(aliceInboxPath) + if os.path.isfile(os.path.join(aliceInboxPath, name))]) + print('aliceInboxPathCtr: ' + str(aliceInboxPathCtr)) + assert aliceInboxPathCtr == 0 print('EVENT: Post liked') + print('\n\nEVENT: Bob reacts to the post') + sendReactionViaServer(bobDir, sessionBob, + 'bob', 'bobpass', + bobDomain, bobPort, + httpPrefix, outboxPostId, '๐Ÿ˜ƒ', + cachedWebfingers, personCache, + True, __version__, signingPrivateKeyPem) + for i in range(20): + if os.path.isdir(outboxPath) and os.path.isdir(inboxPath): + if len([name for name in os.listdir(outboxPath) + if os.path.isfile(os.path.join(outboxPath, name))]) == 3: + test = len([name for name in os.listdir(inboxPath) + if os.path.isfile(os.path.join(inboxPath, name))]) + if test == 1: + break + time.sleep(1) + showTestBoxes('alice', aliceInboxPath, aliceOutboxPath) + showTestBoxes('bob', bobInboxPath, bobOutboxPath) + bobOutboxPathCtr = \ + len([name for name in os.listdir(bobOutboxPath) + if os.path.isfile(os.path.join(bobOutboxPath, name))]) + print('bobOutboxPathCtr: ' + str(bobOutboxPathCtr)) + assert bobOutboxPathCtr == 3 + aliceInboxPathCtr = \ + len([name for name in os.listdir(aliceInboxPath) + if os.path.isfile(os.path.join(aliceInboxPath, name))]) + print('aliceInboxPathCtr: ' + str(aliceInboxPathCtr)) + assert aliceInboxPathCtr == 0 + print('EVENT: Post reacted to') + print(str(len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))]))) showTestBoxes('alice', aliceInboxPath, aliceOutboxPath) showTestBoxes('bob', bobInboxPath, bobOutboxPath) - assert len([name for name in os.listdir(outboxPath) - if os.path.isfile(os.path.join(outboxPath, name))]) == 2 - print(str(len([name for name in os.listdir(inboxPath) - if os.path.isfile(os.path.join(inboxPath, name))]))) - assert len([name for name in os.listdir(aliceInboxPath) - if os.path.isfile(os.path.join(aliceInboxPath, name))]) == 0 + outboxPathCtr = \ + len([name for name in os.listdir(outboxPath) + if os.path.isfile(os.path.join(outboxPath, name))]) + print('outboxPathCtr: ' + str(outboxPathCtr)) + assert outboxPathCtr == 3 + inboxPathCtr = \ + len([name for name in os.listdir(inboxPath) + if os.path.isfile(os.path.join(inboxPath, name))]) + print('inboxPathCtr: ' + str(inboxPathCtr)) + assert inboxPathCtr == 0 showTestBoxes('alice', aliceInboxPath, aliceOutboxPath) showTestBoxes('bob', bobInboxPath, bobOutboxPath) print('\n\nEVENT: Bob repeats the post') @@ -3100,7 +3165,7 @@ def testClientToServer(baseDir: str): for i in range(20): if os.path.isdir(outboxPath) and os.path.isdir(inboxPath): if len([name for name in os.listdir(outboxPath) - if os.path.isfile(os.path.join(outboxPath, name))]) == 3: + if os.path.isfile(os.path.join(outboxPath, name))]) == 4: if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))]) == 2: @@ -3109,10 +3174,16 @@ def testClientToServer(baseDir: str): showTestBoxes('alice', aliceInboxPath, aliceOutboxPath) showTestBoxes('bob', bobInboxPath, bobOutboxPath) - assert len([name for name in os.listdir(bobOutboxPath) - if os.path.isfile(os.path.join(bobOutboxPath, name))]) == 4 - assert len([name for name in os.listdir(aliceInboxPath) - if os.path.isfile(os.path.join(aliceInboxPath, name))]) == 1 + bobOutboxPathCtr = \ + len([name for name in os.listdir(bobOutboxPath) + if os.path.isfile(os.path.join(bobOutboxPath, name))]) + print('bobOutboxPathCtr: ' + str(bobOutboxPathCtr)) + assert bobOutboxPathCtr == 5 + aliceInboxPathCtr = \ + len([name for name in os.listdir(aliceInboxPath) + if os.path.isfile(os.path.join(aliceInboxPath, name))]) + print('aliceInboxPathCtr: ' + str(aliceInboxPathCtr)) + assert aliceInboxPathCtr == 1 print('EVENT: Post repeated') inboxPath = bobDir + '/accounts/bob@' + bobDomain + '/inbox' @@ -5838,6 +5909,16 @@ def _testAddCWfromLists(baseDir: str) -> None: assert postJsonObject['object']['summary'] == "Murdoch Press / Existing CW" +def _testValidEmojiContent() -> None: + print('testValidEmojiContent') + assert not validEmojiContent(None) + assert not validEmojiContent(' ') + assert not validEmojiContent('j') + assert not validEmojiContent('๐Ÿ˜€๐Ÿ˜€') + assert validEmojiContent('๐Ÿ˜€') + assert validEmojiContent('๐Ÿ˜„') + + def runAllTests(): baseDir = os.getcwd() print('Running tests...') @@ -5845,6 +5926,7 @@ def runAllTests(): _translateOntology(baseDir) _testGetPriceFromString() _testFunctions() + _testValidEmojiContent() _testAddCWfromLists(baseDir) _testWordsSimilarity() _testSecondsBetweenPublished() diff --git a/theme/blue/icons/reaction.png b/theme/blue/icons/reaction.png new file mode 100644 index 000000000..0279ca5df Binary files /dev/null and b/theme/blue/icons/reaction.png differ diff --git a/theme/debian/icons/reaction.png b/theme/debian/icons/reaction.png new file mode 100644 index 000000000..0279ca5df Binary files /dev/null and b/theme/debian/icons/reaction.png differ diff --git a/theme/default/icons/reaction.png b/theme/default/icons/reaction.png new file mode 100644 index 000000000..0279ca5df Binary files /dev/null and b/theme/default/icons/reaction.png differ diff --git a/theme/hacker/icons/reaction.png b/theme/hacker/icons/reaction.png new file mode 100644 index 000000000..1e27dcb24 Binary files /dev/null and b/theme/hacker/icons/reaction.png differ diff --git a/theme/indymediaclassic/icons/reaction.png b/theme/indymediaclassic/icons/reaction.png new file mode 100644 index 000000000..e048f9bf0 Binary files /dev/null and b/theme/indymediaclassic/icons/reaction.png differ diff --git a/theme/indymediamodern/icons/reaction.png b/theme/indymediamodern/icons/reaction.png new file mode 100644 index 000000000..2908ae393 Binary files /dev/null and b/theme/indymediamodern/icons/reaction.png differ diff --git a/theme/lcd/icons/reaction.png b/theme/lcd/icons/reaction.png new file mode 100644 index 000000000..a59bc187f Binary files /dev/null and b/theme/lcd/icons/reaction.png differ diff --git a/theme/light/icons/reaction.png b/theme/light/icons/reaction.png new file mode 100644 index 000000000..006cf9e7c Binary files /dev/null and b/theme/light/icons/reaction.png differ diff --git a/theme/night/icons/reaction.png b/theme/night/icons/reaction.png new file mode 100644 index 000000000..0279ca5df Binary files /dev/null and b/theme/night/icons/reaction.png differ diff --git a/theme/pixel/icons/reaction.png b/theme/pixel/icons/reaction.png new file mode 100644 index 000000000..937e4de2c Binary files /dev/null and b/theme/pixel/icons/reaction.png differ diff --git a/theme/purple/icons/reaction.png b/theme/purple/icons/reaction.png new file mode 100644 index 000000000..da610dd09 Binary files /dev/null and b/theme/purple/icons/reaction.png differ diff --git a/theme/rc3/icons/reaction.png b/theme/rc3/icons/reaction.png new file mode 100644 index 000000000..5613bada9 Binary files /dev/null and b/theme/rc3/icons/reaction.png differ diff --git a/theme/solidaric/icons/reaction.png b/theme/solidaric/icons/reaction.png new file mode 100644 index 000000000..a921e608d Binary files /dev/null and b/theme/solidaric/icons/reaction.png differ diff --git a/theme/starlight/icons/reaction.png b/theme/starlight/icons/reaction.png new file mode 100644 index 000000000..62a562860 Binary files /dev/null and b/theme/starlight/icons/reaction.png differ diff --git a/theme/zen/icons/reaction.png b/theme/zen/icons/reaction.png new file mode 100644 index 000000000..09a4d0f3b Binary files /dev/null and b/theme/zen/icons/reaction.png differ diff --git a/translations/ar.json b/translations/ar.json index 53e3d7da9..2aa67428f 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "ุฃุถู ุชุญุฐูŠุฑุงุช ุงู„ู…ุญุชูˆู‰ ู„ู„ู…ูˆุงู‚ุน ุงู„ุชุงู„ูŠุฉ", "Known Web Crawlers": "ุจุฑุงู…ุฌ ุฒุญู ุงู„ูˆูŠุจ ุงู„ู…ุนุฑูˆูุฉ", "Add to the calendar": "ุฃุถู ุฅู„ู‰ ุงู„ุชู‚ูˆูŠู…", - "Content License": "ุชุฑุฎูŠุต ุงู„ู…ุญุชูˆู‰" + "Content License": "ุชุฑุฎูŠุต ุงู„ู…ุญุชูˆู‰", + "Reaction by": "ุฑุฏ ูุนู„", + "Notify on emoji reactions": "ูŠุฎุทุฑ ุนู„ู‰ ุฑุฏูˆุฏ ุงู„ูุนู„ ุงู„ุฑู…ูˆุฒ ุงู„ุชุนุจูŠุฑูŠุฉ", + "Select reaction": "ุญุฏุฏ ุฑุฏ ุงู„ูุนู„" } diff --git a/translations/ca.json b/translations/ca.json index fb0b7340d..cd87323a8 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Afegiu advertiments de contingut per als llocs segรผents", "Known Web Crawlers": "Exploradors web coneguts", "Add to the calendar": "Afegeix al calendari", - "Content License": "Llicรจncia de contingut" + "Content License": "Llicรจncia de contingut", + "Reaction by": "Reacciรณ de", + "Notify on emoji reactions": "Notificar sobre les reaccions dels emojis", + "Select reaction": "Seleccioneu la reacciรณ" } diff --git a/translations/cy.json b/translations/cy.json index d1eaf3d76..643238496 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Ychwanegwch rybuddion cynnwys ar gyfer y gwefannau canlynol", "Known Web Crawlers": "Crawlers Gwe Hysbys", "Add to the calendar": "Ychwanegwch at y calendr", - "Content License": "Trwydded Cynnwys" + "Content License": "Trwydded Cynnwys", + "Reaction by": "Ymateb gan", + "Notify on emoji reactions": "Hysbysu ar ymatebion emoji", + "Select reaction": "Dewiswch adwaith" } diff --git a/translations/de.json b/translations/de.json index a94268597..0432f0d52 100644 --- a/translations/de.json +++ b/translations/de.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Inhaltswarnungen fรผr die folgenden Websites hinzufรผgen", "Known Web Crawlers": "Bekannte Web-Crawler", "Add to the calendar": "Zum Kalender hinzufรผgen", - "Content License": "Inhaltslizenz" + "Content License": "Inhaltslizenz", + "Reaction by": "Reaktion von", + "Notify on emoji reactions": "Bei Emoji-Reaktionen benachrichtigen", + "Select reaction": "Reaktion auswรคhlen" } diff --git a/translations/en.json b/translations/en.json index d96083f0b..a65a63695 100644 --- a/translations/en.json +++ b/translations/en.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Add content warnings for the following sites", "Known Web Crawlers": "Known Web Crawlers", "Add to the calendar": "Add to the calendar", - "Content License": "Content License" + "Content License": "Content License", + "Reaction by": "Reaction by", + "Notify on emoji reactions": "Notify on emoji reactions", + "Select reaction": "Select reaction" } diff --git a/translations/es.json b/translations/es.json index 00a945f28..5917dcfc2 100644 --- a/translations/es.json +++ b/translations/es.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Agregue advertencias de contenido para los siguientes sitios", "Known Web Crawlers": "Rastreadores web conocidos", "Add to the calendar": "Agregar al calendario", - "Content License": "Licencia de contenido" + "Content License": "Licencia de contenido", + "Reaction by": "Reacciรณn de", + "Notify on emoji reactions": "Notificar sobre reacciones emoji", + "Select reaction": "Seleccionar reacciรณn" } diff --git a/translations/fr.json b/translations/fr.json index 0d082c4a9..db3948815 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Ajouter des avertissements de contenu pour les sites suivants", "Known Web Crawlers": "Crawlers Web connus", "Add to the calendar": "Ajouter au calendrier", - "Content License": "Licence de contenu" + "Content License": "Licence de contenu", + "Reaction by": "Rรฉaction par", + "Notify on emoji reactions": "Avertir sur les rรฉactions emoji", + "Select reaction": "Sรฉlectionnez la rรฉaction" } diff --git a/translations/ga.json b/translations/ga.json index 1d91c0471..63b7d3add 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Cuir rabhaidh รกbhair leis na suรญomhanna seo a leanas", "Known Web Crawlers": "Crawlers Grรฉasรกin Aitheanta", "Add to the calendar": "Cuir leis an bhfรฉilire", - "Content License": "Ceadรบnas รbhar" + "Content License": "Ceadรบnas รbhar", + "Reaction by": "Imoibriรบ le", + "Notify on emoji reactions": "Fรณgra a thabhairt faoi imoibrithe emoji", + "Select reaction": "Roghnaigh imoibriรบ" } diff --git a/translations/hi.json b/translations/hi.json index 93a81c7e6..58099bcf2 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "เคจเคฟเคฎเฅเคจเคฒเคฟเค–เคฟเคค เคธเคพเค‡เคŸเฅ‹เค‚ เค•เฅ‡ เคฒเคฟเค เคธเคพเคฎเค—เฅเคฐเฅ€ เคšเฅ‡เคคเคพเคตเคจเคฟเคฏเคพเค เคœเฅ‹เคกเคผเฅ‡เค‚", "Known Web Crawlers": "เคœเฅเคžเคพเคค เคตเฅ‡เคฌ เค•เฅเคฐเฅ‰เคฒเคฐ", "Add to the calendar": "เค•เฅˆเคฒเฅ‡เค‚เคกเคฐ เคฎเฅ‡เค‚ เคœเฅ‹เคกเคผเฅ‡เค‚", - "Content License": "เคธเคพเคฎเค—เฅเคฐเฅ€ เคฒเคพเค‡เคธเฅ‡เค‚เคธ" + "Content License": "เคธเคพเคฎเค—เฅเคฐเฅ€ เคฒเคพเค‡เคธเฅ‡เค‚เคธ", + "Reaction by": "เคฆเฅเคตเคพเคฐเคพ เคชเฅเคฐเคคเคฟเค•เฅเคฐเคฟเคฏเคพ", + "Notify on emoji reactions": "เค‡เคฎเฅ‹เคœเฅ€ เคชเฅเคฐเคคเคฟเค•เฅเคฐเคฟเคฏเคพเค“เค‚ เคชเคฐ เคธเฅ‚เคšเคฟเคค เค•เคฐเฅ‡เค‚", + "Select reaction": "เคชเฅเคฐเคคเคฟเค•เฅเคฐเคฟเคฏเคพ เค•เคพ เคšเคฏเคจ เค•เคฐเฅ‡เค‚" } diff --git a/translations/it.json b/translations/it.json index 64319f34a..80c6d4376 100644 --- a/translations/it.json +++ b/translations/it.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Aggiungi avvisi sui contenuti per i seguenti siti", "Known Web Crawlers": "Crawler Web conosciuti", "Add to the calendar": "Aggiungi al calendario", - "Content License": "Licenza sui contenuti" + "Content License": "Licenza sui contenuti", + "Reaction by": "Reazione di", + "Notify on emoji reactions": "Notifica sulle reazioni emoji", + "Select reaction": "Seleziona reazione" } diff --git a/translations/ja.json b/translations/ja.json index 89b880f99..17db79a59 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "ๆฌกใฎใ‚ตใ‚คใƒˆใฎใ‚ณใƒณใƒ†ใƒณใƒ„่ญฆๅ‘Šใ‚’่ฟฝๅŠ ใ—ใพใ™", "Known Web Crawlers": "ๆ—ข็ŸฅใฎWebใ‚ฏใƒญใƒผใƒฉใƒผ", "Add to the calendar": "ใ‚ซใƒฌใƒณใƒ€ใƒผใซ่ฟฝๅŠ ", - "Content License": "ใ‚ณใƒณใƒ†ใƒณใƒ„ใƒฉใ‚คใ‚ปใƒณใ‚น" + "Content License": "ใ‚ณใƒณใƒ†ใƒณใƒ„ใƒฉใ‚คใ‚ปใƒณใ‚น", + "Reaction by": "ใซใ‚ˆใ‚‹ๅๅฟœ", + "Notify on emoji reactions": "็ตตๆ–‡ๅญ—ใฎๅๅฟœใ‚’้€š็Ÿฅใ™ใ‚‹", + "Select reaction": "ๅๅฟœใ‚’้ธๆŠž" } diff --git a/translations/ku.json b/translations/ku.json index 54edfdbb8..8d7130e7a 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Ji bo malperรชn jรชrรฎn hiลŸyariyรชn naverokรช zรชde bikin", "Known Web Crawlers": "Crawlerรชn Webรช yรชn naskirรฎ", "Add to the calendar": "Di salnameyรช de zรชde bike", - "Content License": "Naverok License de" + "Content License": "Naverok License de", + "Reaction by": "Reaction by", + "Notify on emoji reactions": "Li ser reaksiyonรชn emoji agahdar bikin", + "Select reaction": "Reaksiyonรช hilbijรชrin" } diff --git a/translations/oc.json b/translations/oc.json index 8240c4a8e..5db6f0855 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -488,5 +488,8 @@ "Add content warnings for the following sites": "Add content warnings for the following sites", "Known Web Crawlers": "Known Web Crawlers", "Add to the calendar": "Add to the calendar", - "Content License": "Content License" + "Content License": "Content License", + "Reaction by": "Reaction by", + "Notify on emoji reactions": "Notify on emoji reactions", + "Select reaction": "Select reaction" } diff --git a/translations/pt.json b/translations/pt.json index 958a40f6f..556520314 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Adicione avisos de conteรบdo para os seguintes sites", "Known Web Crawlers": "Rastreadores da Web conhecidos", "Add to the calendar": "Adicionar ao calendรกrio", - "Content License": "Licenรงa de Conteรบdo" + "Content License": "Licenรงa de Conteรบdo", + "Reaction by": "Reazione di", + "Notify on emoji reactions": "Notificar sobre reaรงรตes de emoji", + "Select reaction": "Selecione a reaรงรฃo" } diff --git a/translations/ru.json b/translations/ru.json index d59bf4869..7714015bd 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฟั€ะตะดัƒะฟั€ะตะถะดะตะฝะธั ะพ ัะพะดะตั€ะถะฐะฝะธะธ ะดะปั ัะปะตะดัƒัŽั‰ะธั… ัะฐะนั‚ะพะฒ", "Known Web Crawlers": "ะ˜ะทะฒะตัั‚ะฝั‹ะต ะฒะตะฑ-ัะบะฐะฝะตั€ั‹", "Add to the calendar": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฒ ะบะฐะปะตะฝะดะฐั€ัŒ", - "Content License": "ะ›ะธั†ะตะฝะทะธั ะฝะฐ ัะพะดะตั€ะถะฐะฝะธะต" + "Content License": "ะ›ะธั†ะตะฝะทะธั ะฝะฐ ัะพะดะตั€ะถะฐะฝะธะต", + "Reaction by": "ะ ะตะฐะบั†ะธั ัะพ ัั‚ะพั€ะพะฝั‹", + "Notify on emoji reactions": "ะฃะฒะตะดะพะผะปัั‚ัŒ ะพ ั€ะตะฐะบั†ะธะธ ะฝะฐ ัะผะฐะนะปะธะบะธ", + "Select reaction": "ะ’ั‹ะฑะตั€ะธั‚ะต ั€ะตะฐะบั†ะธัŽ" } diff --git a/translations/sw.json b/translations/sw.json index d4a5ca713..b153ee46b 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "Ongeza maonyo ya yaliyomo kwa wavuti zifuatazo", "Known Web Crawlers": "Watambaji Wavuti Wanaojulikana", "Add to the calendar": "Ongeza kwenye kalenda", - "Content License": "Leseni ya Maudhui" + "Content License": "Leseni ya Maudhui", + "Reaction by": "Majibu kwa", + "Notify on emoji reactions": "Arifu kuhusu maitikio ya emoji", + "Select reaction": "Chagua majibu" } diff --git a/translations/zh.json b/translations/zh.json index a698fb5ce..319389d06 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -492,5 +492,8 @@ "Add content warnings for the following sites": "ไธบไปฅไธ‹็ฝ‘็ซ™ๆทปๅŠ ๅ†…ๅฎน่ญฆๅ‘Š", "Known Web Crawlers": "ๅทฒ็Ÿฅ็š„็ฝ‘็ปœ็ˆฌ่™ซ", "Add to the calendar": "ๆทปๅŠ ๅˆฐๆ—ฅๅކ", - "Content License": "ๅ†…ๅฎน่ฎธๅฏ" + "Content License": "ๅ†…ๅฎน่ฎธๅฏ", + "Reaction by": "ๅๅบ”็”ฑ", + "Notify on emoji reactions": "้€š็Ÿฅ่กจๆƒ…็ฌฆๅทๅๅบ”", + "Select reaction": "้€‰ๆ‹ฉๅๅบ”" } diff --git a/utils.py b/utils.py index e67a22a59..09eb9003b 100644 --- a/utils.py +++ b/utils.py @@ -2258,6 +2258,72 @@ def undoLikesCollectionEntry(recentPostsCache: {}, saveJson(postJsonObject, postFilename) +def undoReactionCollectionEntry(recentPostsCache: {}, + baseDir: str, postFilename: str, + objectUrl: str, + actor: str, domain: str, debug: bool, + postJsonObject: {}, emojiContent: str) -> None: + """Undoes an emoji reaction for a particular actor + """ + if not postJsonObject: + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return + # remove any cached version of this post so that the + # like icon is changed + nickname = getNicknameFromActor(actor) + cachedPostFilename = getCachedPostFilename(baseDir, nickname, + domain, postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + try: + os.remove(cachedPostFilename) + except BaseException: + print('EX: undoReactionCollectionEntry ' + + 'unable to delete cached post ' + + str(cachedPostFilename)) + pass + removePostFromCache(postJsonObject, recentPostsCache) + + if not postJsonObject.get('type'): + return + if postJsonObject['type'] != 'Create': + return + obj = postJsonObject + if hasObjectDict(postJsonObject): + obj = postJsonObject['object'] + if not obj.get('reactions'): + return + if not isinstance(obj['reactions'], dict): + return + if not obj['reactions'].get('items'): + return + totalItems = 0 + if obj['reactions'].get('totalItems'): + totalItems = obj['reactions']['totalItems'] + itemFound = False + for likeItem in obj['reactions']['items']: + if likeItem.get('actor'): + if likeItem['actor'] == actor and \ + likeItem['content'] == emojiContent: + if debug: + print('DEBUG: emoji reaction was removed for ' + actor) + obj['reactions']['items'].remove(likeItem) + itemFound = True + break + if not itemFound: + return + if totalItems == 1: + if debug: + print('DEBUG: emoji reaction was removed from post') + del obj['reactions'] + else: + itlen = len(obj['reactions']['items']) + obj['reactions']['totalItems'] = itlen + + saveJson(postJsonObject, postFilename) + + def undoAnnounceCollectionEntry(recentPostsCache: {}, baseDir: str, postFilename: str, actor: str, domain: str, debug: bool) -> None: diff --git a/webapp_post.py b/webapp_post.py index e2c821748..5f8fcc212 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -9,6 +9,7 @@ __module_group__ = "Web Interface" import os import time +import urllib.parse from dateutil.parser import parse from auth import createPassword from git import isGitPatch @@ -61,6 +62,7 @@ from content import switchWords from person import isPersonSnoozed from person import getPersonAvatarUrl from announce import announcedByPerson +from webapp_utils import getBannerFile from webapp_utils import getAvatarImageUrl from webapp_utils import updateAvatarImageCache from webapp_utils import loadIndividualPostAsHtmlFromCache @@ -79,6 +81,7 @@ from speaker import updateSpeaker from languages import autoTranslatePost from blocking import isBlocked from blocking import addCWfromLists +from reaction import htmlEmojiReactions def _htmlPostMetadataOpenGraph(domain: str, postJsonObject: {}) -> str: @@ -698,6 +701,41 @@ def _getBookmarkIconHtml(nickname: str, domainFull: str, return bookmarkStr +def _getReactionIconHtml(nickname: str, domainFull: str, + postJsonObject: {}, + isModerationPost: bool, + translate: {}, + enableTimingLog: bool, + postStartTime, boxName: str, + pageNumberParam: str, + timelinePostReaction: str) -> str: + """Returns html for reaction icon/button + """ + reactionStr = '' + + if isModerationPost: + return reactionStr + + reactionIcon = 'reaction.png' + reactionTitle = 'Select reaction' + if translate.get(reactionTitle): + reactionTitle = translate[reactionTitle] + _logPostTiming(enableTimingLog, postStartTime, '12.65') + reactionPostId = removeIdEnding(postJsonObject['object']['id']) + reactionStr = \ + ' \n' + reactionStr += \ + ' ' + \ + '' + \
+        reactionTitle + ' |\n' + return reactionStr + + def _getMuteIconHtml(isMuted: bool, postActor: str, messageId: str, @@ -1246,7 +1284,8 @@ def _getPostTitleHtml(baseDir: str, def _getFooterWithIcons(showIcons: bool, containerClassIcons: str, replyStr: str, announceStr: str, - likeStr: str, bookmarkStr: str, + likeStr: str, reactionStr: str, + bookmarkStr: str, deleteStr: str, muteStr: str, editStr: str, postJsonObject: {}, publishedLink: str, timeClass: str, publishedStr: str) -> str: @@ -1257,7 +1296,7 @@ def _getFooterWithIcons(showIcons: bool, footerStr = '\n