Merge branch 'main' of gitlab.com:bashrc2/epicyon

merge-requests/30/head
Bob Mottram 2021-11-11 23:45:13 +00:00
commit cd66c7aede
49 changed files with 3535 additions and 108 deletions

627
daemon.py
View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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%;
}
}

View File

@ -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')

424
inbox.py
View File

@ -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,

32
like.py
View File

@ -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,

View File

@ -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) + '" ' +

View File

@ -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,

View File

@ -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'

View File

@ -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'

564
reaction.py 100644
View File

@ -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 = '<div class="emojiReactionBar">\n'
for emojiContent, count in reactions.items():
if emojiContent not in reactedToByThisActor:
baseUrl = actor + '?react=' + reactBy + '?emojreact='
else:
baseUrl = actor + '?unreact=' + reactBy + '?emojreact='
htmlStr += ' <div class="emojiReactionButton">\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 = \
' <a href="' + baseUrl + emojiContentEncoded + '">' + \
emojiContentStr + '</a>\n'
htmlStr += emojiContentStr
htmlStr += ' </div>\n'
htmlStr += '</div>\n'
return htmlStr

View File

@ -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

View File

@ -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)

112
tests.py
View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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": "حدد رد الفعل"
}

View File

@ -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ó"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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ú"
}

View File

@ -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": "प्रतिक्रिया का चयन करें"
}

View File

@ -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"
}

View File

@ -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": "反応を選択"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "Выберите реакцию"
}

View File

@ -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"
}

View File

@ -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": "选择反应"
}

View File

@ -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:

View File

@ -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 = \
' <a class="imageAnchor" href="/users/' + nickname + \
'?selreact=' + reactionPostId + pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
'?bm=' + timelinePostReaction + \
'?tl=' + boxName + '" title="' + reactionTitle + '">\n'
reactionStr += \
' ' + \
'<img loading="lazy" title="' + reactionTitle + '" alt="' + \
reactionTitle + ' |" src="/icons' + \
'/' + reactionIcon + '"/></a>\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 <nav>\n'
footerStr += ' <div class="' + containerClassIcons + '">\n'
footerStr += replyStr + announceStr + likeStr + bookmarkStr
footerStr += replyStr + announceStr + likeStr + bookmarkStr + reactionStr
footerStr += deleteStr + muteStr + editStr
if not isNewsPost(postJsonObject):
footerStr += ' <a href="' + publishedLink + '" class="' + \
@ -1303,6 +1342,9 @@ def individualPostAsHtml(signingPrivateKeyPem: str,
if not postJsonObject:
return ''
# maximum number of different emoji reactions which can be added to a post
maxReactionTypes = 5
# benchmark
postStartTime = time.time()
@ -1645,6 +1687,18 @@ def individualPostAsHtml(signingPrivateKeyPem: str,
_logPostTiming(enableTimingLog, postStartTime, '12.9')
reactionStr = \
_getReactionIconHtml(nickname, domainFull,
postJsonObject,
isModerationPost,
translate,
enableTimingLog,
postStartTime, boxName,
pageNumberParam,
timelinePostBookmark)
_logPostTiming(enableTimingLog, postStartTime, '12.10')
isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
_logPostTiming(enableTimingLog, postStartTime, '13')
@ -1736,7 +1790,7 @@ def individualPostAsHtml(signingPrivateKeyPem: str,
newFooterStr = _getFooterWithIcons(showIcons,
containerClassIcons,
replyStr, announceStr,
likeStr, bookmarkStr,
likeStr, reactionStr, bookmarkStr,
deleteStr, muteStr, editStr,
postJsonObject, publishedLink,
timeClass, publishedStr)
@ -1879,13 +1933,20 @@ def individualPostAsHtml(signingPrivateKeyPem: str,
postHtml = ''
if boxName != 'tlmedia':
reactionStr = ''
if showIcons:
reactionStr = \
htmlEmojiReactions(postJsonObject, True, personUrl,
maxReactionTypes)
if postIsSensitive and reactionStr:
reactionStr = '<br>' + reactionStr
postHtml = ' <div id="' + timelinePostBookmark + \
'" class="' + containerClass + '">\n'
postHtml += avatarImageInPost
postHtml += ' <div class="post-title">\n' + \
' ' + titleStr + \
replyAvatarImageInPost + ' </div>\n'
postHtml += contentStr + citationsStr + footerStr + '\n'
postHtml += contentStr + citationsStr + reactionStr + footerStr + '\n'
postHtml += ' </div>\n'
else:
postHtml = galleryStr
@ -1914,6 +1975,7 @@ def htmlIndividualPost(cssCache: {},
nickname: str, domain: str, port: int, authorized: bool,
postJsonObject: {}, httpPrefix: str,
projectVersion: str, likedBy: str,
reactBy: str, reactEmoji: str,
YTReplacementDomain: str,
twitterReplacementDomain: str,
showPublishedDateOnly: bool,
@ -1926,17 +1988,27 @@ def htmlIndividualPost(cssCache: {},
"""
originalPostJson = postJsonObject
postStr = ''
byStr = ''
byText = ''
byTextExtra = ''
if likedBy:
likedByNickname = getNicknameFromActor(likedBy)
likedByDomain, likedByPort = getDomainFromActor(likedBy)
likedByDomain = getFullDomain(likedByDomain, likedByPort)
likedByHandle = likedByNickname + '@' + likedByDomain
likedByStr = 'Liked by'
if translate.get(likedByStr):
likedByStr = translate[likedByStr]
byStr = likedBy
byText = 'Liked by'
elif reactBy and reactEmoji:
byStr = reactBy
byText = 'Reaction by'
byTextExtra = ' ' + reactEmoji
if byStr:
byStrNickname = getNicknameFromActor(byStr)
byStrDomain, byStrPort = getDomainFromActor(byStr)
byStrDomain = getFullDomain(byStrDomain, byStrPort)
byStrHandle = byStrNickname + '@' + byStrDomain
if translate.get(byText):
byText = translate[byText]
postStr += \
'<p>' + likedByStr + ' <a href="' + likedBy + '">@' + \
likedByHandle + '</a>\n'
'<p>' + byText + ' <a href="' + byStr + '">@' + \
byStrHandle + '</a>' + byTextExtra + '\n'
domainFull = getFullDomain(domain, port)
actor = '/users/' + nickname
@ -1946,8 +2018,8 @@ def htmlIndividualPost(cssCache: {},
' <input type="hidden" name="actor" value="' + actor + '">\n'
followStr += \
' <input type="hidden" name="searchtext" value="' + \
likedByHandle + '">\n'
if not isFollowingActor(baseDir, nickname, domainFull, likedBy):
byStrHandle + '">\n'
if not isFollowingActor(baseDir, nickname, domainFull, byStr):
translateFollowStr = 'Follow'
if translate.get(translateFollowStr):
translateFollowStr = translate[translateFollowStr]
@ -2101,10 +2173,95 @@ def htmlPostReplies(cssCache: {},
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
# TODO
instanceTitle = getConfigParam(baseDir, 'instanceTitle')
metadata = ''
headerStr = \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle, metadata)
return headerStr + repliesStr + htmlFooter()
def htmlEmojiReactionPicker(cssCache: {},
recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, session, cachedWebfingers: {},
personCache: {},
nickname: str, domain: str, port: int,
postJsonObject: {}, httpPrefix: str,
projectVersion: str,
YTReplacementDomain: str,
twitterReplacementDomain: str,
showPublishedDateOnly: bool,
peertubeInstances: [],
allowLocalNetworkAccess: bool,
themeName: str, systemLanguage: str,
maxLikeCount: int, signingPrivateKeyPem: str,
CWlists: {}, listsEnabled: str,
defaultTimeline: str) -> str:
"""Returns the emoji picker screen
"""
reactedToPostStr = \
'<br><center><label class="followText">' + \
translate['Select reaction'].title() + '</label></center>\n' + \
individualPostAsHtml(signingPrivateKeyPem,
True, recentPostsCache,
maxRecentPosts,
translate, None,
baseDir, session, cachedWebfingers,
personCache,
nickname, domain, port, postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
twitterReplacementDomain,
showPublishedDateOnly,
peertubeInstances,
allowLocalNetworkAccess,
themeName, systemLanguage,
maxLikeCount,
False, False, False, False, False, False,
CWlists, listsEnabled)
reactionsFilename = baseDir + '/emoji/reactions.json'
if not os.path.isfile(reactionsFilename):
reactionsFilename = baseDir + '/emoji/default_reactions.json'
reactionsJson = loadJson(reactionsFilename)
emojiPicksStr = ''
baseUrl = '/users/' + nickname
postId = removeIdEnding(postJsonObject['id'])
for category, item in reactionsJson.items():
emojiPicksStr += '<div class="container">\n'
for emojiContent in item:
emojiContentEncoded = urllib.parse.quote_plus(emojiContent)
emojiUrl = \
baseUrl + '?react=' + postId + \
'?emojreact=' + emojiContentEncoded
emojiLabel = '<label class="rlab">' + emojiContent + '</label>'
emojiPicksStr += \
' <a href="' + emojiUrl + '">' + emojiLabel + '</a>\n'
emojiPicksStr += '</div>\n'
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
# filename of the banner shown at the top
bannerFile, _ = \
getBannerFile(baseDir, nickname, domain, themeName)
instanceTitle = getConfigParam(baseDir, 'instanceTitle')
metadata = ''
headerStr = \
htmlHeaderWithExternalStyle(cssFilename, instanceTitle, metadata)
# banner
headerStr += \
'<header>\n' + \
'<a href="/users/' + nickname + '/' + defaultTimeline + '" title="' + \
translate['Switch to timeline view'] + '" alt="' + \
translate['Switch to timeline view'] + '">\n'
headerStr += '<img loading="lazy" class="timeline-banner" ' + \
'alt="" ' + \
'src="/users/' + nickname + '/' + bannerFile + '" /></a>\n' + \
'</header>\n'
return headerStr + reactedToPostStr + emojiPicksStr + htmlFooter()

View File

@ -1867,7 +1867,8 @@ def _htmlEditProfileOptions(isAdmin: bool,
manuallyApprovesFollowers: str,
isBot: str, isGroup: str,
followDMs: str, removeTwitter: str,
notifyLikes: str, hideLikeButton: str,
notifyLikes: str, notifyReactions: str,
hideLikeButton: str,
translate: {}) -> str:
"""option checkboxes section of edit profile screen
"""
@ -1891,6 +1892,9 @@ def _htmlEditProfileOptions(isAdmin: bool,
editProfileForm += \
editCheckBox(translate['Notify when posts are liked'],
'notifyLikes', notifyLikes)
editProfileForm += \
editCheckBox(translate['Notify on emoji reactions'],
'notifyReactions', notifyReactions)
editProfileForm += \
editCheckBox(translate["Don't show the Like button"],
'hideLikeButton', hideLikeButton)
@ -2048,7 +2052,7 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
displayNickname = nickname
isBot = isGroup = followDMs = removeTwitter = ''
notifyLikes = hideLikeButton = mediaInstanceStr = ''
notifyLikes = notifyReactions = hideLikeButton = mediaInstanceStr = ''
blogsInstanceStr = newsInstanceStr = movedTo = twitterStr = ''
bioStr = donateUrl = websiteUrl = emailAddress = PGPpubKey = ''
PGPfingerprint = xmppAddress = matrixAddress = ''
@ -2099,6 +2103,8 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
removeTwitter = 'checked'
if os.path.isfile(accountDir + '/.notifyLikes'):
notifyLikes = 'checked'
if os.path.isfile(accountDir + '/.notifyReactions'):
notifyReactions = 'checked'
if os.path.isfile(accountDir + '/.hideLikeButton'):
hideLikeButton = 'checked'
@ -2196,7 +2202,8 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
editProfileForm += \
_htmlEditProfileOptions(isAdmin, manuallyApprovesFollowers,
isBot, isGroup, followDMs, removeTwitter,
notifyLikes, hideLikeButton, translate)
notifyLikes, notifyReactions,
hideLikeButton, translate)
# Contact information
editProfileForm += \