Merge branch 'main' of gitlab.com:bashrc2/epicyon
627
daemon.py
|
@ -192,6 +192,7 @@ from webapp_suspended import htmlSuspended
|
|||
from webapp_tos import htmlTermsOfService
|
||||
from webapp_confirm import htmlConfirmFollow
|
||||
from webapp_confirm import htmlConfirmUnfollow
|
||||
from webapp_post import htmlEmojiReactionPicker
|
||||
from webapp_post import htmlPostReplies
|
||||
from webapp_post import htmlIndividualPost
|
||||
from webapp_post import individualPostAsHtml
|
||||
|
@ -238,6 +239,8 @@ from categories import updateHashtagCategories
|
|||
from languages import getActorLanguages
|
||||
from languages import setActorLanguages
|
||||
from like import updateLikesCollection
|
||||
from reaction import updateReactionCollection
|
||||
from utils import undoReactionCollectionEntry
|
||||
from utils import getNewPostEndpoints
|
||||
from utils import malformedCiphertext
|
||||
from utils import hasActor
|
||||
|
@ -654,12 +657,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return False
|
||||
return True
|
||||
|
||||
def _secureMode(self) -> bool:
|
||||
"""http authentication of GET requests for json
|
||||
def _signedGETkeyId(self) -> str:
|
||||
"""Returns the actor from the signed GET keyId
|
||||
"""
|
||||
if not self.server.secureMode:
|
||||
return True
|
||||
|
||||
signature = None
|
||||
if self.headers.get('signature'):
|
||||
signature = self.headers['signature']
|
||||
|
@ -669,9 +669,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
# check that the headers are signed
|
||||
if not signature:
|
||||
if self.server.debug:
|
||||
print('AUTH: secure mode, ' +
|
||||
print('AUTH: secure mode actor, ' +
|
||||
'GET has no signature in headers')
|
||||
return False
|
||||
return None
|
||||
|
||||
# get the keyId, which is typically the instance actor
|
||||
keyId = None
|
||||
|
@ -680,17 +680,25 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if signatureItem.startswith('keyId='):
|
||||
if '"' in signatureItem:
|
||||
keyId = signatureItem.split('"')[1]
|
||||
break
|
||||
# remove #main-key
|
||||
if '#' in keyId:
|
||||
keyId = keyId.split('#')[0]
|
||||
return keyId
|
||||
return None
|
||||
|
||||
def _secureMode(self, force: bool = False) -> bool:
|
||||
"""http authentication of GET requests for json
|
||||
"""
|
||||
if not self.server.secureMode and not force:
|
||||
return True
|
||||
|
||||
keyId = self._signedGETkeyId()
|
||||
if not keyId:
|
||||
if self.server.debug:
|
||||
print('AUTH: secure mode, ' +
|
||||
'failed to obtain keyId from signature')
|
||||
return False
|
||||
|
||||
# remove #main-key
|
||||
if '#' in keyId:
|
||||
keyId = keyId.split('#')[0]
|
||||
|
||||
# is the keyId (actor) valid?
|
||||
if not urlPermitted(keyId, self.server.federationList):
|
||||
if self.server.debug:
|
||||
|
@ -1484,7 +1492,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
originalMessageJson = messageJson.copy()
|
||||
|
||||
# whether to add a 'to' field to the message
|
||||
addToFieldTypes = ('Follow', 'Like', 'Add', 'Remove', 'Ignore')
|
||||
addToFieldTypes = (
|
||||
'Follow', 'Like', 'EmojiReact', 'Add', 'Remove', 'Ignore'
|
||||
)
|
||||
for addToType in addToFieldTypes:
|
||||
messageJson, toFieldExists = \
|
||||
addToField(addToType, messageJson, self.server.debug)
|
||||
|
@ -5701,6 +5711,32 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
notifyLikesFilename)
|
||||
pass
|
||||
|
||||
notifyReactionsFilename = \
|
||||
acctDir(baseDir, nickname, domain) + \
|
||||
'/.notifyReactions'
|
||||
if onFinalWelcomeScreen:
|
||||
# default setting from welcome screen
|
||||
with open(notifyReactionsFilename, 'w+') as rFile:
|
||||
rFile.write('\n')
|
||||
actorChanged = True
|
||||
else:
|
||||
notifyReactionsActive = False
|
||||
if fields.get('notifyReactions'):
|
||||
if fields['notifyReactions'] == 'on':
|
||||
notifyReactionsActive = True
|
||||
with open(notifyReactionsFilename,
|
||||
'w+') as rFile:
|
||||
rFile.write('\n')
|
||||
if not notifyReactionsActive:
|
||||
if os.path.isfile(notifyReactionsFilename):
|
||||
try:
|
||||
os.remove(notifyReactionsFilename)
|
||||
except BaseException:
|
||||
print('EX: _profileUpdate ' +
|
||||
'unable to delete ' +
|
||||
notifyReactionsFilename)
|
||||
pass
|
||||
|
||||
# this account is a bot
|
||||
if fields.get('isBot'):
|
||||
if fields['isBot'] == 'on':
|
||||
|
@ -7862,6 +7898,473 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'_GET', '_undoLikeButton',
|
||||
self.server.debug)
|
||||
|
||||
def _reactionButton(self, callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str,
|
||||
GETstartTime,
|
||||
proxyType: str, cookie: str,
|
||||
debug: str):
|
||||
"""Press an emoji reaction button
|
||||
Note that this is not the emoji reaction selection icon at the
|
||||
bottom of the post
|
||||
"""
|
||||
pageNumber = 1
|
||||
reactionUrl = path.split('?react=')[1]
|
||||
if '?' in reactionUrl:
|
||||
reactionUrl = reactionUrl.split('?')[0]
|
||||
timelineBookmark = ''
|
||||
if '?bm=' in path:
|
||||
timelineBookmark = path.split('?bm=')[1]
|
||||
if '?' in timelineBookmark:
|
||||
timelineBookmark = timelineBookmark.split('?')[0]
|
||||
timelineBookmark = '#' + timelineBookmark
|
||||
actor = path.split('?react=')[0]
|
||||
if '?page=' in path:
|
||||
pageNumberStr = path.split('?page=')[1]
|
||||
if '?' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('?')[0]
|
||||
if '#' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('#')[0]
|
||||
if pageNumberStr.isdigit():
|
||||
pageNumber = int(pageNumberStr)
|
||||
timelineStr = 'inbox'
|
||||
if '?tl=' in path:
|
||||
timelineStr = path.split('?tl=')[1]
|
||||
if '?' in timelineStr:
|
||||
timelineStr = timelineStr.split('?')[0]
|
||||
emojiContentEncoded = None
|
||||
if '?emojreact=' in path:
|
||||
emojiContentEncoded = path.split('?emojreact=')[1]
|
||||
if '?' in emojiContentEncoded:
|
||||
emojiContentEncoded = emojiContentEncoded.split('?')[0]
|
||||
if not emojiContentEncoded:
|
||||
print('WARN: no emoji reaction ' + actor)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie,
|
||||
callingDomain)
|
||||
return
|
||||
emojiContent = urllib.parse.unquote_plus(emojiContentEncoded)
|
||||
self.postToNickname = getNicknameFromActor(actor)
|
||||
if not self.postToNickname:
|
||||
print('WARN: unable to find nickname in ' + actor)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie,
|
||||
callingDomain)
|
||||
return
|
||||
if not self.server.session:
|
||||
print('Starting new session during emoji reaction')
|
||||
self.server.session = createSession(proxyType)
|
||||
if not self.server.session:
|
||||
print('ERROR: ' +
|
||||
'GET failed to create session during emoji reaction')
|
||||
self._404()
|
||||
self.server.GETbusy = False
|
||||
return
|
||||
reactionActor = \
|
||||
localActorUrl(httpPrefix, self.postToNickname, domainFull)
|
||||
actorReaction = path.split('?actor=')[1]
|
||||
if '?' in actorReaction:
|
||||
actorReaction = actorReaction.split('?')[0]
|
||||
|
||||
# if this is an announce then send the emoji reaction
|
||||
# to the original post
|
||||
origActor, origPostUrl, origFilename = \
|
||||
getOriginalPostFromAnnounceUrl(reactionUrl, baseDir,
|
||||
self.postToNickname, domain)
|
||||
reactionUrl2 = reactionUrl
|
||||
reactionPostFilename = origFilename
|
||||
if origActor and origPostUrl:
|
||||
actorReaction = origActor
|
||||
reactionUrl2 = origPostUrl
|
||||
reactionPostFilename = None
|
||||
|
||||
reactionJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'type': 'EmojiReact',
|
||||
'actor': reactionActor,
|
||||
'to': [actorReaction],
|
||||
'object': reactionUrl2,
|
||||
'content': emojiContent
|
||||
}
|
||||
|
||||
# send out the emoji reaction to followers
|
||||
self._postToOutbox(reactionJson, self.server.projectVersion, None)
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', '_reactionButton postToOutbox',
|
||||
self.server.debug)
|
||||
|
||||
print('Locating emoji reaction post ' + reactionUrl)
|
||||
# directly emoji reaction the post file
|
||||
if not reactionPostFilename:
|
||||
reactionPostFilename = \
|
||||
locatePost(baseDir, self.postToNickname, domain, reactionUrl)
|
||||
if reactionPostFilename:
|
||||
recentPostsCache = self.server.recentPostsCache
|
||||
reactionPostJson = loadJson(reactionPostFilename, 0, 1)
|
||||
if origFilename and origPostUrl:
|
||||
updateReactionCollection(recentPostsCache,
|
||||
baseDir, reactionPostFilename,
|
||||
reactionUrl,
|
||||
reactionActor, self.postToNickname,
|
||||
domain, debug, reactionPostJson,
|
||||
emojiContent)
|
||||
reactionUrl = origPostUrl
|
||||
reactionPostFilename = origFilename
|
||||
if debug:
|
||||
print('Updating emoji reaction for ' + reactionPostFilename)
|
||||
updateReactionCollection(recentPostsCache,
|
||||
baseDir, reactionPostFilename,
|
||||
reactionUrl,
|
||||
reactionActor,
|
||||
self.postToNickname, domain,
|
||||
debug, None, emojiContent)
|
||||
if debug:
|
||||
print('Regenerating html post for changed ' +
|
||||
'emoji reaction collection')
|
||||
# clear the icon from the cache so that it gets updated
|
||||
if reactionPostJson:
|
||||
cachedPostFilename = \
|
||||
getCachedPostFilename(baseDir, self.postToNickname,
|
||||
domain, reactionPostJson)
|
||||
if debug:
|
||||
print('Reaction post json: ' + str(reactionPostJson))
|
||||
print('Reaction post nickname: ' +
|
||||
self.postToNickname + ' ' + domain)
|
||||
print('Reaction post cache: ' + str(cachedPostFilename))
|
||||
showIndividualPostIcons = True
|
||||
manuallyApproveFollowers = \
|
||||
followerApprovalActive(baseDir,
|
||||
self.postToNickname, domain)
|
||||
showRepeats = not isDM(reactionPostJson)
|
||||
individualPostAsHtml(self.server.signingPrivateKeyPem, False,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate,
|
||||
pageNumber, baseDir,
|
||||
self.server.session,
|
||||
self.server.cachedWebfingers,
|
||||
self.server.personCache,
|
||||
self.postToNickname, domain,
|
||||
self.server.port, reactionPostJson,
|
||||
None, True,
|
||||
self.server.allowDeletion,
|
||||
httpPrefix,
|
||||
self.server.projectVersion,
|
||||
timelineStr,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.twitterReplacementDomain,
|
||||
self.server.showPublishedDateOnly,
|
||||
self.server.peertubeInstances,
|
||||
self.server.allowLocalNetworkAccess,
|
||||
self.server.themeName,
|
||||
self.server.systemLanguage,
|
||||
self.server.maxLikeCount,
|
||||
showRepeats,
|
||||
showIndividualPostIcons,
|
||||
manuallyApproveFollowers,
|
||||
False, True, False,
|
||||
self.server.CWlists,
|
||||
self.server.listsEnabled)
|
||||
else:
|
||||
print('WARN: Emoji reaction post not found: ' +
|
||||
reactionPostFilename)
|
||||
else:
|
||||
print('WARN: unable to locate file for emoji reaction post ' +
|
||||
reactionUrl)
|
||||
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie,
|
||||
callingDomain)
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', '_reactionButton',
|
||||
self.server.debug)
|
||||
|
||||
def _undoReactionButton(self, callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str,
|
||||
GETstartTime,
|
||||
proxyType: str, cookie: str,
|
||||
debug: str):
|
||||
"""A button is pressed to undo emoji reaction
|
||||
"""
|
||||
pageNumber = 1
|
||||
reactionUrl = path.split('?unreact=')[1]
|
||||
if '?' in reactionUrl:
|
||||
reactionUrl = reactionUrl.split('?')[0]
|
||||
timelineBookmark = ''
|
||||
if '?bm=' in path:
|
||||
timelineBookmark = path.split('?bm=')[1]
|
||||
if '?' in timelineBookmark:
|
||||
timelineBookmark = timelineBookmark.split('?')[0]
|
||||
timelineBookmark = '#' + timelineBookmark
|
||||
if '?page=' in path:
|
||||
pageNumberStr = path.split('?page=')[1]
|
||||
if '?' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('?')[0]
|
||||
if '#' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('#')[0]
|
||||
if pageNumberStr.isdigit():
|
||||
pageNumber = int(pageNumberStr)
|
||||
timelineStr = 'inbox'
|
||||
if '?tl=' in path:
|
||||
timelineStr = path.split('?tl=')[1]
|
||||
if '?' in timelineStr:
|
||||
timelineStr = timelineStr.split('?')[0]
|
||||
actor = path.split('?unreact=')[0]
|
||||
self.postToNickname = getNicknameFromActor(actor)
|
||||
if not self.postToNickname:
|
||||
print('WARN: unable to find nickname in ' + actor)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber)
|
||||
self._redirect_headers(actorPathStr, cookie,
|
||||
callingDomain)
|
||||
return
|
||||
emojiContentEncoded = None
|
||||
if '?emojreact=' in path:
|
||||
emojiContentEncoded = path.split('?emojreact=')[1]
|
||||
if '?' in emojiContentEncoded:
|
||||
emojiContentEncoded = emojiContentEncoded.split('?')[0]
|
||||
if not emojiContentEncoded:
|
||||
print('WARN: no emoji reaction ' + actor)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie,
|
||||
callingDomain)
|
||||
return
|
||||
emojiContent = urllib.parse.unquote_plus(emojiContentEncoded)
|
||||
if not self.server.session:
|
||||
print('Starting new session during undo emoji reaction')
|
||||
self.server.session = createSession(proxyType)
|
||||
if not self.server.session:
|
||||
print('ERROR: GET failed to create session ' +
|
||||
'during undo emoji reaction')
|
||||
self._404()
|
||||
self.server.GETbusy = False
|
||||
return
|
||||
undoActor = \
|
||||
localActorUrl(httpPrefix, self.postToNickname, domainFull)
|
||||
actorReaction = path.split('?actor=')[1]
|
||||
if '?' in actorReaction:
|
||||
actorReaction = actorReaction.split('?')[0]
|
||||
|
||||
# if this is an announce then send the emoji reaction
|
||||
# to the original post
|
||||
origActor, origPostUrl, origFilename = \
|
||||
getOriginalPostFromAnnounceUrl(reactionUrl, baseDir,
|
||||
self.postToNickname, domain)
|
||||
reactionUrl2 = reactionUrl
|
||||
reactionPostFilename = origFilename
|
||||
if origActor and origPostUrl:
|
||||
actorReaction = origActor
|
||||
reactionUrl2 = origPostUrl
|
||||
reactionPostFilename = None
|
||||
|
||||
undoReactionJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'type': 'Undo',
|
||||
'actor': undoActor,
|
||||
'to': [actorReaction],
|
||||
'object': {
|
||||
'type': 'EmojiReaction',
|
||||
'actor': undoActor,
|
||||
'to': [actorReaction],
|
||||
'object': reactionUrl2
|
||||
}
|
||||
}
|
||||
|
||||
# send out the undo emoji reaction to followers
|
||||
self._postToOutbox(undoReactionJson, self.server.projectVersion, None)
|
||||
|
||||
# directly undo the emoji reaction within the post file
|
||||
if not reactionPostFilename:
|
||||
reactionPostFilename = \
|
||||
locatePost(baseDir, self.postToNickname, domain, reactionUrl)
|
||||
if reactionPostFilename:
|
||||
recentPostsCache = self.server.recentPostsCache
|
||||
reactionPostJson = loadJson(reactionPostFilename, 0, 1)
|
||||
if origFilename and origPostUrl:
|
||||
undoReactionCollectionEntry(recentPostsCache,
|
||||
baseDir, reactionPostFilename,
|
||||
reactionUrl,
|
||||
undoActor, domain, debug,
|
||||
reactionPostJson,
|
||||
emojiContent)
|
||||
reactionUrl = origPostUrl
|
||||
reactionPostFilename = origFilename
|
||||
if debug:
|
||||
print('Removing emoji reaction for ' + reactionPostFilename)
|
||||
undoReactionCollectionEntry(recentPostsCache,
|
||||
baseDir,
|
||||
reactionPostFilename, reactionUrl,
|
||||
undoActor, domain, debug, None,
|
||||
emojiContent)
|
||||
if debug:
|
||||
print('Regenerating html post for changed ' +
|
||||
'emoji reaction collection')
|
||||
if reactionPostJson:
|
||||
showIndividualPostIcons = True
|
||||
manuallyApproveFollowers = \
|
||||
followerApprovalActive(baseDir,
|
||||
self.postToNickname, domain)
|
||||
showRepeats = not isDM(reactionPostJson)
|
||||
individualPostAsHtml(self.server.signingPrivateKeyPem, False,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate,
|
||||
pageNumber, baseDir,
|
||||
self.server.session,
|
||||
self.server.cachedWebfingers,
|
||||
self.server.personCache,
|
||||
self.postToNickname, domain,
|
||||
self.server.port, reactionPostJson,
|
||||
None, True,
|
||||
self.server.allowDeletion,
|
||||
httpPrefix,
|
||||
self.server.projectVersion, timelineStr,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.twitterReplacementDomain,
|
||||
self.server.showPublishedDateOnly,
|
||||
self.server.peertubeInstances,
|
||||
self.server.allowLocalNetworkAccess,
|
||||
self.server.themeName,
|
||||
self.server.systemLanguage,
|
||||
self.server.maxLikeCount,
|
||||
showRepeats,
|
||||
showIndividualPostIcons,
|
||||
manuallyApproveFollowers,
|
||||
False, True, False,
|
||||
self.server.CWlists,
|
||||
self.server.listsEnabled)
|
||||
else:
|
||||
print('WARN: Unreaction post not found: ' +
|
||||
reactionPostFilename)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie, callingDomain)
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', '_undoReactionButton',
|
||||
self.server.debug)
|
||||
|
||||
def _reactionPicker(self, callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str, port: int,
|
||||
onionDomain: str, i2pDomain: str,
|
||||
GETstartTime,
|
||||
proxyType: str, cookie: str,
|
||||
debug: str) -> None:
|
||||
"""Press the emoji reaction picker icon at the bottom of the post
|
||||
"""
|
||||
pageNumber = 1
|
||||
reactionUrl = path.split('?selreact=')[1]
|
||||
if '?' in reactionUrl:
|
||||
reactionUrl = reactionUrl.split('?')[0]
|
||||
timelineBookmark = ''
|
||||
if '?bm=' in path:
|
||||
timelineBookmark = path.split('?bm=')[1]
|
||||
if '?' in timelineBookmark:
|
||||
timelineBookmark = timelineBookmark.split('?')[0]
|
||||
timelineBookmark = '#' + timelineBookmark
|
||||
actor = path.split('?selreact=')[0]
|
||||
if '?page=' in path:
|
||||
pageNumberStr = path.split('?page=')[1]
|
||||
if '?' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('?')[0]
|
||||
if '#' in pageNumberStr:
|
||||
pageNumberStr = pageNumberStr.split('#')[0]
|
||||
if pageNumberStr.isdigit():
|
||||
pageNumber = int(pageNumberStr)
|
||||
timelineStr = 'inbox'
|
||||
if '?tl=' in path:
|
||||
timelineStr = path.split('?tl=')[1]
|
||||
if '?' in timelineStr:
|
||||
timelineStr = timelineStr.split('?')[0]
|
||||
self.postToNickname = getNicknameFromActor(actor)
|
||||
if not self.postToNickname:
|
||||
print('WARN: unable to find nickname in ' + actor)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie, callingDomain)
|
||||
return
|
||||
|
||||
postJsonObject = None
|
||||
reactionPostFilename = \
|
||||
locatePost(self.server.baseDir,
|
||||
self.postToNickname, domain, reactionUrl)
|
||||
if reactionPostFilename:
|
||||
postJsonObject = loadJson(reactionPostFilename)
|
||||
if not reactionPostFilename or not postJsonObject:
|
||||
print('WARN: unable to locate reaction post ' + reactionUrl)
|
||||
self.server.GETbusy = False
|
||||
actorAbsolute = self._getInstanceUrl(callingDomain) + actor
|
||||
actorPathStr = \
|
||||
actorAbsolute + '/' + timelineStr + \
|
||||
'?page=' + str(pageNumber) + timelineBookmark
|
||||
self._redirect_headers(actorPathStr, cookie, callingDomain)
|
||||
return
|
||||
|
||||
msg = \
|
||||
htmlEmojiReactionPicker(self.server.cssCache,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate,
|
||||
self.server.baseDir,
|
||||
self.server.session,
|
||||
self.server.cachedWebfingers,
|
||||
self.server.personCache,
|
||||
self.postToNickname,
|
||||
domain, port, postJsonObject,
|
||||
self.server.httpPrefix,
|
||||
self.server.projectVersion,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.twitterReplacementDomain,
|
||||
self.server.showPublishedDateOnly,
|
||||
self.server.peertubeInstances,
|
||||
self.server.allowLocalNetworkAccess,
|
||||
self.server.themeName,
|
||||
self.server.systemLanguage,
|
||||
self.server.maxLikeCount,
|
||||
self.server.signingPrivateKeyPem,
|
||||
self.server.CWlists,
|
||||
self.server.listsEnabled,
|
||||
self.server.defaultTimeline)
|
||||
msg = msg.encode('utf-8')
|
||||
msglen = len(msg)
|
||||
self._set_headers('text/html', msglen,
|
||||
cookie, callingDomain, False)
|
||||
self._write(msg)
|
||||
fitnessPerformance(GETstartTime,
|
||||
self.server.fitness,
|
||||
'_GET', '_reactionPicker',
|
||||
self.server.debug)
|
||||
self.server.GETbusy = False
|
||||
|
||||
def _bookmarkButton(self, callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str, port: int,
|
||||
|
@ -8953,6 +9456,18 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
likedBy = likedBy.split('?')[0]
|
||||
path = path.split('?likedBy=')[0]
|
||||
|
||||
reactBy = None
|
||||
reactEmoji = None
|
||||
if '?reactBy=' in path:
|
||||
reactBy = path.split('?reactBy=')[1].strip()
|
||||
if ';' in reactBy:
|
||||
reactBy = reactBy.split(';')[0]
|
||||
if ';emoj=' in path:
|
||||
reactEmoji = path.split(';emoj=')[1].strip()
|
||||
if ';' in reactEmoji:
|
||||
reactEmoji = reactEmoji.split(';')[0]
|
||||
path = path.split('?reactBy=')[0]
|
||||
|
||||
namedStatus = path.split('/@')[1]
|
||||
if '/' not in namedStatus:
|
||||
# show actor
|
||||
|
@ -8977,6 +9492,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
includeCreateWrapper = True
|
||||
|
||||
result = self._showPostFromFile(postFilename, likedBy,
|
||||
reactBy, reactEmoji,
|
||||
authorized, callingDomain, path,
|
||||
baseDir, httpPrefix, nickname,
|
||||
domain, domainFull, port,
|
||||
|
@ -8990,6 +9506,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return result
|
||||
|
||||
def _showPostFromFile(self, postFilename: str, likedBy: str,
|
||||
reactBy: str, reactEmoji: str,
|
||||
authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str, nickname: str,
|
||||
|
@ -9036,7 +9553,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
postJsonObject,
|
||||
httpPrefix,
|
||||
self.server.projectVersion,
|
||||
likedBy,
|
||||
likedBy, reactBy, reactEmoji,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.twitterReplacementDomain,
|
||||
self.server.showPublishedDateOnly,
|
||||
|
@ -9099,6 +9616,19 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if '?' in likedBy:
|
||||
likedBy = likedBy.split('?')[0]
|
||||
path = path.split('?likedBy=')[0]
|
||||
|
||||
reactBy = None
|
||||
reactEmoji = None
|
||||
if '?reactBy=' in path:
|
||||
reactBy = path.split('?reactBy=')[1].strip()
|
||||
if ';' in reactBy:
|
||||
reactBy = reactBy.split(';')[0]
|
||||
if ';emoj=' in path:
|
||||
reactEmoji = path.split(';emoj=')[1].strip()
|
||||
if ';' in reactEmoji:
|
||||
reactEmoji = reactEmoji.split(';')[0]
|
||||
path = path.split('?reactBy=')[0]
|
||||
|
||||
namedStatus = path.split('/users/')[1]
|
||||
if '/' not in namedStatus:
|
||||
return False
|
||||
|
@ -9120,6 +9650,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
includeCreateWrapper = True
|
||||
|
||||
result = self._showPostFromFile(postFilename, likedBy,
|
||||
reactBy, reactEmoji,
|
||||
authorized, callingDomain, path,
|
||||
baseDir, httpPrefix, nickname,
|
||||
domain, domainFull, port,
|
||||
|
@ -9144,6 +9675,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
and where you have the notify checkbox set on person options
|
||||
"""
|
||||
likedBy = None
|
||||
reactBy = None
|
||||
reactEmoji = None
|
||||
postId = path.split('?notifypost=')[1].strip()
|
||||
postId = postId.replace('-', '/')
|
||||
path = path.split('?notifypost=')[0]
|
||||
|
@ -9161,6 +9694,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
includeCreateWrapper = True
|
||||
|
||||
result = self._showPostFromFile(postFilename, likedBy,
|
||||
reactBy, reactEmoji,
|
||||
authorized, callingDomain, path,
|
||||
baseDir, httpPrefix, nickname,
|
||||
domain, domainFull, port,
|
||||
|
@ -12980,6 +13514,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
nickname = self.path.split('/users/')[1]
|
||||
if '/' in nickname:
|
||||
nickname = nickname.split('/')[0]
|
||||
# return the featured posts collection
|
||||
self._getFeaturedCollection(callingDomain,
|
||||
self.server.baseDir,
|
||||
self.path,
|
||||
|
@ -14346,7 +14881,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', 'like shown done',
|
||||
'_GET', 'like button done',
|
||||
self.server.debug)
|
||||
|
||||
# undo a like from the web interface icon
|
||||
|
@ -14364,7 +14899,44 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', 'unlike shown done',
|
||||
'_GET', 'unlike button done',
|
||||
self.server.debug)
|
||||
|
||||
# emoji reaction from the web interface icon
|
||||
if authorized and htmlGET and '?react=' in self.path:
|
||||
self._reactionButton(callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain,
|
||||
GETstartTime,
|
||||
self.server.proxyType,
|
||||
cookie,
|
||||
self.server.debug)
|
||||
return
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', 'emoji reaction button done',
|
||||
self.server.debug)
|
||||
|
||||
# undo an emoji reaction from the web interface icon
|
||||
if authorized and htmlGET and '?unreact=' in self.path:
|
||||
self._undoReactionButton(callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain,
|
||||
GETstartTime,
|
||||
self.server.proxyType,
|
||||
cookie, self.server.debug)
|
||||
return
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', 'unreaction button done',
|
||||
self.server.debug)
|
||||
|
||||
# bookmark from the web interface icon
|
||||
|
@ -14386,6 +14958,25 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'_GET', 'bookmark shown done',
|
||||
self.server.debug)
|
||||
|
||||
# emoji recation from the web interface bottom icon
|
||||
if authorized and htmlGET and '?selreact=' in self.path:
|
||||
self._reactionPicker(callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.port,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain,
|
||||
GETstartTime,
|
||||
self.server.proxyType,
|
||||
cookie, self.server.debug)
|
||||
return
|
||||
|
||||
fitnessPerformance(GETstartTime, self.server.fitness,
|
||||
'_GET', 'bookmark shown done',
|
||||
self.server.debug)
|
||||
|
||||
# undo a bookmark from the web interface icon
|
||||
if authorized and htmlGET and '?unbookmark=' in self.path:
|
||||
self._undoBookmarkButton(callingDomain, self.path,
|
||||
|
@ -15495,8 +16086,10 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
contentStr = \
|
||||
getBaseContentFromPost(messageJson,
|
||||
self.server.systemLanguage)
|
||||
followersOnly = False
|
||||
pinPost(self.server.baseDir,
|
||||
nickname, self.server.domain, contentStr)
|
||||
nickname, self.server.domain, contentStr,
|
||||
followersOnly)
|
||||
return 1
|
||||
if self._postToOutbox(messageJson,
|
||||
self.server.projectVersion,
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
89
epicyon.py
|
@ -81,6 +81,9 @@ from media import getAttachmentMediaType
|
|||
from delete import sendDeleteViaServer
|
||||
from like import sendLikeViaServer
|
||||
from like import sendUndoLikeViaServer
|
||||
from reaction import sendReactionViaServer
|
||||
from reaction import sendUndoReactionViaServer
|
||||
from reaction import validEmojiContent
|
||||
from skills import sendSkillViaServer
|
||||
from availability import setAvailability
|
||||
from availability import sendAvailabilityViaServer
|
||||
|
@ -510,6 +513,13 @@ parser.add_argument('--favorite', '--like', dest='like', type=str,
|
|||
default=None, help='Like a url')
|
||||
parser.add_argument('--undolike', '--unlike', dest='undolike', type=str,
|
||||
default=None, help='Undo a like of a url')
|
||||
parser.add_argument('--react', '--reaction', dest='react', type=str,
|
||||
default=None, help='Reaction url')
|
||||
parser.add_argument('--emoji', type=str,
|
||||
default=None, help='Reaction emoji')
|
||||
parser.add_argument('--undoreact', '--undoreaction', dest='undoreact',
|
||||
type=str,
|
||||
default=None, help='Reaction url')
|
||||
parser.add_argument('--bookmark', '--bm', dest='bookmark', type=str,
|
||||
default=None,
|
||||
help='Bookmark the url of a post')
|
||||
|
@ -1612,6 +1622,45 @@ if args.like:
|
|||
time.sleep(1)
|
||||
sys.exit()
|
||||
|
||||
if args.react:
|
||||
if not args.nickname:
|
||||
print('Specify a nickname with the --nickname option')
|
||||
sys.exit()
|
||||
if not args.emoji:
|
||||
print('Specify a reaction emoji with the --emoji option')
|
||||
sys.exit()
|
||||
if not validEmojiContent(args.emoji):
|
||||
print('This is not a valid emoji')
|
||||
sys.exit()
|
||||
|
||||
if not args.password:
|
||||
args.password = getpass.getpass('Password: ')
|
||||
if not args.password:
|
||||
print('Specify a password with the --password option')
|
||||
sys.exit()
|
||||
args.password = args.password.replace('\n', '')
|
||||
|
||||
session = createSession(proxyType)
|
||||
personCache = {}
|
||||
cachedWebfingers = {}
|
||||
if not domain:
|
||||
domain = getConfigParam(baseDir, 'domain')
|
||||
signingPrivateKeyPem = None
|
||||
if args.secureMode:
|
||||
signingPrivateKeyPem = getInstanceActorKey(baseDir, domain)
|
||||
print('Sending emoji reaction ' + args.emoji + ' to ' + args.react)
|
||||
|
||||
sendReactionViaServer(baseDir, session,
|
||||
args.nickname, args.password,
|
||||
domain, port,
|
||||
httpPrefix, args.react, args.emoji,
|
||||
cachedWebfingers, personCache,
|
||||
True, __version__, signingPrivateKeyPem)
|
||||
for i in range(10):
|
||||
# TODO detect send success/fail
|
||||
time.sleep(1)
|
||||
sys.exit()
|
||||
|
||||
if args.undolike:
|
||||
if not args.nickname:
|
||||
print('Specify a nickname with the --nickname option')
|
||||
|
@ -1646,6 +1695,46 @@ if args.undolike:
|
|||
time.sleep(1)
|
||||
sys.exit()
|
||||
|
||||
if args.undoreact:
|
||||
if not args.nickname:
|
||||
print('Specify a nickname with the --nickname option')
|
||||
sys.exit()
|
||||
if not args.emoji:
|
||||
print('Specify a reaction emoji with the --emoji option')
|
||||
sys.exit()
|
||||
if not validEmojiContent(args.emoji):
|
||||
print('This is not a valid emoji')
|
||||
sys.exit()
|
||||
|
||||
if not args.password:
|
||||
args.password = getpass.getpass('Password: ')
|
||||
if not args.password:
|
||||
print('Specify a password with the --password option')
|
||||
sys.exit()
|
||||
args.password = args.password.replace('\n', '')
|
||||
|
||||
session = createSession(proxyType)
|
||||
personCache = {}
|
||||
cachedWebfingers = {}
|
||||
if not domain:
|
||||
domain = getConfigParam(baseDir, 'domain')
|
||||
signingPrivateKeyPem = None
|
||||
if args.secureMode:
|
||||
signingPrivateKeyPem = getInstanceActorKey(baseDir, domain)
|
||||
print('Sending undo emoji reaction ' + args.emoji + ' to ' + args.react)
|
||||
|
||||
sendUndoReactionViaServer(baseDir, session,
|
||||
args.nickname, args.password,
|
||||
domain, port,
|
||||
httpPrefix, args.undoreact, args.emoji,
|
||||
cachedWebfingers, personCache,
|
||||
True, __version__,
|
||||
signingPrivateKeyPem)
|
||||
for i in range(10):
|
||||
# TODO detect send success/fail
|
||||
time.sleep(1)
|
||||
sys.exit()
|
||||
|
||||
if args.bookmark:
|
||||
if not args.nickname:
|
||||
print('Specify a nickname with the --nickname option')
|
||||
|
|
424
inbox.py
|
@ -15,6 +15,9 @@ import random
|
|||
from linked_data_sig import verifyJsonSignature
|
||||
from languages import understoodPostLanguage
|
||||
from like import updateLikesCollection
|
||||
from reaction import updateReactionCollection
|
||||
from reaction import validEmojiContent
|
||||
from utils import removeHtml
|
||||
from utils import fileLastModified
|
||||
from utils import hasObjectString
|
||||
from utils import hasObjectStringObject
|
||||
|
@ -50,6 +53,7 @@ from utils import removeModerationPostFromIndex
|
|||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
from utils import undoLikesCollectionEntry
|
||||
from utils import undoReactionCollectionEntry
|
||||
from utils import hasGroupType
|
||||
from utils import localActorUrl
|
||||
from utils import hasObjectStringType
|
||||
|
@ -393,7 +397,8 @@ def inboxMessageHasParams(messageJson: {}) -> bool:
|
|||
return False
|
||||
|
||||
if not messageJson.get('to'):
|
||||
allowedWithoutToParam = ['Like', 'Follow', 'Join', 'Request',
|
||||
allowedWithoutToParam = ['Like', 'EmojiReact',
|
||||
'Follow', 'Join', 'Request',
|
||||
'Accept', 'Capability', 'Undo']
|
||||
if messageJson['type'] not in allowedWithoutToParam:
|
||||
return False
|
||||
|
@ -415,7 +420,9 @@ def inboxPermittedMessage(domain: str, messageJson: {},
|
|||
if not urlPermitted(actor, federationList):
|
||||
return False
|
||||
|
||||
alwaysAllowedTypes = ('Follow', 'Join', 'Like', 'Delete', 'Announce')
|
||||
alwaysAllowedTypes = (
|
||||
'Follow', 'Join', 'Like', 'EmojiReact', 'Delete', 'Announce'
|
||||
)
|
||||
if messageJson['type'] not in alwaysAllowedTypes:
|
||||
if not hasObjectDict(messageJson):
|
||||
return True
|
||||
|
@ -1203,6 +1210,275 @@ def _receiveUndoLike(recentPostsCache: {},
|
|||
return True
|
||||
|
||||
|
||||
def _receiveReaction(recentPostsCache: {},
|
||||
session, handle: str, isGroup: bool, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
onionDomain: str,
|
||||
sendThreads: [], postLog: [], cachedWebfingers: {},
|
||||
personCache: {}, messageJson: {}, federationList: [],
|
||||
debug: bool,
|
||||
signingPrivateKeyPem: str,
|
||||
maxRecentPosts: int, translate: {},
|
||||
allowDeletion: bool,
|
||||
YTReplacementDomain: str,
|
||||
twitterReplacementDomain: str,
|
||||
peertubeInstances: [],
|
||||
allowLocalNetworkAccess: bool,
|
||||
themeName: str, systemLanguage: str,
|
||||
maxLikeCount: int, CWlists: {},
|
||||
listsEnabled: str) -> bool:
|
||||
"""Receives an emoji reaction within the POST section of HTTPServer
|
||||
"""
|
||||
if messageJson['type'] != 'EmojiReact':
|
||||
return False
|
||||
if not hasActor(messageJson, debug):
|
||||
return False
|
||||
if not hasObjectString(messageJson, debug):
|
||||
return False
|
||||
if not messageJson.get('to'):
|
||||
if debug:
|
||||
print('DEBUG: ' + messageJson['type'] + ' has no "to" list')
|
||||
return False
|
||||
if not messageJson.get('content'):
|
||||
if debug:
|
||||
print('DEBUG: ' + messageJson['type'] + ' has no "content"')
|
||||
return False
|
||||
if not isinstance(messageJson['content'], str):
|
||||
if debug:
|
||||
print('DEBUG: ' + messageJson['type'] + ' content is not string')
|
||||
return False
|
||||
if not validEmojiContent(messageJson['content']):
|
||||
print('_receiveReaction: Invalid emoji reaction: "' +
|
||||
messageJson['content'] + '" from ' + messageJson['actor'])
|
||||
return False
|
||||
if not hasUsersPath(messageJson['actor']):
|
||||
if debug:
|
||||
print('DEBUG: "users" or "profile" missing from actor in ' +
|
||||
messageJson['type'])
|
||||
return False
|
||||
if '/statuses/' not in messageJson['object']:
|
||||
if debug:
|
||||
print('DEBUG: "statuses" missing from object in ' +
|
||||
messageJson['type'])
|
||||
return False
|
||||
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
||||
print('DEBUG: unknown recipient of emoji reaction - ' + handle)
|
||||
# if this post in the outbox of the person?
|
||||
handleName = handle.split('@')[0]
|
||||
handleDom = handle.split('@')[1]
|
||||
postReactionId = messageJson['object']
|
||||
emojiContent = removeHtml(messageJson['content'])
|
||||
if not emojiContent:
|
||||
if debug:
|
||||
print('DEBUG: emoji reaction has no content')
|
||||
return True
|
||||
postFilename = locatePost(baseDir, handleName, handleDom, postReactionId)
|
||||
if not postFilename:
|
||||
if debug:
|
||||
print('DEBUG: emoji reaction post not found in inbox or outbox')
|
||||
print(postReactionId)
|
||||
return True
|
||||
if debug:
|
||||
print('DEBUG: emoji reaction post found in inbox')
|
||||
|
||||
reactionActor = messageJson['actor']
|
||||
handleName = handle.split('@')[0]
|
||||
handleDom = handle.split('@')[1]
|
||||
if not _alreadyReacted(baseDir,
|
||||
handleName, handleDom,
|
||||
postReactionId,
|
||||
reactionActor,
|
||||
emojiContent):
|
||||
_reactionNotify(baseDir, domain, onionDomain, handle,
|
||||
reactionActor, postReactionId, emojiContent)
|
||||
updateReactionCollection(recentPostsCache, baseDir, postFilename,
|
||||
postReactionId, reactionActor,
|
||||
handleName, domain, debug, None, emojiContent)
|
||||
# regenerate the html
|
||||
reactionPostJson = loadJson(postFilename, 0, 1)
|
||||
if reactionPostJson:
|
||||
if reactionPostJson.get('type'):
|
||||
if reactionPostJson['type'] == 'Announce' and \
|
||||
reactionPostJson.get('object'):
|
||||
if isinstance(reactionPostJson['object'], str):
|
||||
announceReactionUrl = reactionPostJson['object']
|
||||
announceReactionFilename = \
|
||||
locatePost(baseDir, handleName,
|
||||
domain, announceReactionUrl)
|
||||
if announceReactionFilename:
|
||||
postReactionId = announceReactionUrl
|
||||
postFilename = announceReactionFilename
|
||||
updateReactionCollection(recentPostsCache,
|
||||
baseDir,
|
||||
postFilename,
|
||||
postReactionId,
|
||||
reactionActor,
|
||||
handleName,
|
||||
domain, debug, None,
|
||||
emojiContent)
|
||||
if reactionPostJson:
|
||||
if debug:
|
||||
cachedPostFilename = \
|
||||
getCachedPostFilename(baseDir, handleName, domain,
|
||||
reactionPostJson)
|
||||
print('Reaction post json: ' + str(reactionPostJson))
|
||||
print('Reaction post nickname: ' + handleName + ' ' + domain)
|
||||
print('Reaction post cache: ' + str(cachedPostFilename))
|
||||
pageNumber = 1
|
||||
showPublishedDateOnly = False
|
||||
showIndividualPostIcons = True
|
||||
manuallyApproveFollowers = \
|
||||
followerApprovalActive(baseDir, handleName, domain)
|
||||
notDM = not isDM(reactionPostJson)
|
||||
individualPostAsHtml(signingPrivateKeyPem, False,
|
||||
recentPostsCache, maxRecentPosts,
|
||||
translate, pageNumber, baseDir,
|
||||
session, cachedWebfingers, personCache,
|
||||
handleName, domain, port, reactionPostJson,
|
||||
None, True, allowDeletion,
|
||||
httpPrefix, __version__,
|
||||
'inbox',
|
||||
YTReplacementDomain,
|
||||
twitterReplacementDomain,
|
||||
showPublishedDateOnly,
|
||||
peertubeInstances,
|
||||
allowLocalNetworkAccess,
|
||||
themeName, systemLanguage,
|
||||
maxLikeCount, notDM,
|
||||
showIndividualPostIcons,
|
||||
manuallyApproveFollowers,
|
||||
False, True, False, CWlists,
|
||||
listsEnabled)
|
||||
return True
|
||||
|
||||
|
||||
def _receiveUndoReaction(recentPostsCache: {},
|
||||
session, handle: str, isGroup: bool, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
sendThreads: [], postLog: [], cachedWebfingers: {},
|
||||
personCache: {}, messageJson: {}, federationList: [],
|
||||
debug: bool,
|
||||
signingPrivateKeyPem: str,
|
||||
maxRecentPosts: int, translate: {},
|
||||
allowDeletion: bool,
|
||||
YTReplacementDomain: str,
|
||||
twitterReplacementDomain: str,
|
||||
peertubeInstances: [],
|
||||
allowLocalNetworkAccess: bool,
|
||||
themeName: str, systemLanguage: str,
|
||||
maxLikeCount: int, CWlists: {},
|
||||
listsEnabled: str) -> bool:
|
||||
"""Receives an undo emoji reaction within the POST section of HTTPServer
|
||||
"""
|
||||
if messageJson['type'] != 'Undo':
|
||||
return False
|
||||
if not hasActor(messageJson, debug):
|
||||
return False
|
||||
if not hasObjectStringType(messageJson, debug):
|
||||
return False
|
||||
if messageJson['object']['type'] != 'EmojiReact':
|
||||
return False
|
||||
if not hasObjectStringObject(messageJson, debug):
|
||||
return False
|
||||
if not messageJson['object'].get('content'):
|
||||
if debug:
|
||||
print('DEBUG: ' + messageJson['type'] + ' has no "content"')
|
||||
return False
|
||||
if not isinstance(messageJson['object']['content'], str):
|
||||
if debug:
|
||||
print('DEBUG: ' + messageJson['type'] + ' content is not string')
|
||||
return False
|
||||
if not hasUsersPath(messageJson['actor']):
|
||||
if debug:
|
||||
print('DEBUG: "users" or "profile" missing from actor in ' +
|
||||
messageJson['type'] + ' reaction')
|
||||
return False
|
||||
if '/statuses/' not in messageJson['object']['object']:
|
||||
if debug:
|
||||
print('DEBUG: "statuses" missing from reaction object in ' +
|
||||
messageJson['type'])
|
||||
return False
|
||||
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
||||
print('DEBUG: unknown recipient of undo reaction - ' + handle)
|
||||
# if this post in the outbox of the person?
|
||||
handleName = handle.split('@')[0]
|
||||
handleDom = handle.split('@')[1]
|
||||
postFilename = \
|
||||
locatePost(baseDir, handleName, handleDom,
|
||||
messageJson['object']['object'])
|
||||
if not postFilename:
|
||||
if debug:
|
||||
print('DEBUG: unreaction post not found in inbox or outbox')
|
||||
print(messageJson['object']['object'])
|
||||
return True
|
||||
if debug:
|
||||
print('DEBUG: reaction post found in inbox. Now undoing.')
|
||||
reactionActor = messageJson['actor']
|
||||
postReactionId = messageJson['object']
|
||||
emojiContent = removeHtml(messageJson['object']['content'])
|
||||
if not emojiContent:
|
||||
if debug:
|
||||
print('DEBUG: unreaction has no content')
|
||||
return True
|
||||
undoReactionCollectionEntry(recentPostsCache, baseDir, postFilename,
|
||||
postReactionId, reactionActor, domain,
|
||||
debug, None, emojiContent)
|
||||
# regenerate the html
|
||||
reactionPostJson = loadJson(postFilename, 0, 1)
|
||||
if reactionPostJson:
|
||||
if reactionPostJson.get('type'):
|
||||
if reactionPostJson['type'] == 'Announce' and \
|
||||
reactionPostJson.get('object'):
|
||||
if isinstance(reactionPostJson['object'], str):
|
||||
announceReactionUrl = reactionPostJson['object']
|
||||
announceReactionFilename = \
|
||||
locatePost(baseDir, handleName,
|
||||
domain, announceReactionUrl)
|
||||
if announceReactionFilename:
|
||||
postReactionId = announceReactionUrl
|
||||
postFilename = announceReactionFilename
|
||||
undoReactionCollectionEntry(recentPostsCache, baseDir,
|
||||
postFilename,
|
||||
postReactionId,
|
||||
reactionActor, domain,
|
||||
debug, None,
|
||||
emojiContent)
|
||||
if reactionPostJson:
|
||||
if debug:
|
||||
cachedPostFilename = \
|
||||
getCachedPostFilename(baseDir, handleName, domain,
|
||||
reactionPostJson)
|
||||
print('Unreaction post json: ' + str(reactionPostJson))
|
||||
print('Unreaction post nickname: ' + handleName + ' ' + domain)
|
||||
print('Unreaction post cache: ' + str(cachedPostFilename))
|
||||
pageNumber = 1
|
||||
showPublishedDateOnly = False
|
||||
showIndividualPostIcons = True
|
||||
manuallyApproveFollowers = \
|
||||
followerApprovalActive(baseDir, handleName, domain)
|
||||
notDM = not isDM(reactionPostJson)
|
||||
individualPostAsHtml(signingPrivateKeyPem, False,
|
||||
recentPostsCache, maxRecentPosts,
|
||||
translate, pageNumber, baseDir,
|
||||
session, cachedWebfingers, personCache,
|
||||
handleName, domain, port, reactionPostJson,
|
||||
None, True, allowDeletion,
|
||||
httpPrefix, __version__,
|
||||
'inbox',
|
||||
YTReplacementDomain,
|
||||
twitterReplacementDomain,
|
||||
showPublishedDateOnly,
|
||||
peertubeInstances,
|
||||
allowLocalNetworkAccess,
|
||||
themeName, systemLanguage,
|
||||
maxLikeCount, notDM,
|
||||
showIndividualPostIcons,
|
||||
manuallyApproveFollowers,
|
||||
False, True, False, CWlists,
|
||||
listsEnabled)
|
||||
return True
|
||||
|
||||
|
||||
def _receiveBookmark(recentPostsCache: {},
|
||||
session, handle: str, isGroup: bool, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
|
@ -2068,6 +2344,40 @@ def _alreadyLiked(baseDir: str, nickname: str, domain: str,
|
|||
return False
|
||||
|
||||
|
||||
def _alreadyReacted(baseDir: str, nickname: str, domain: str,
|
||||
postUrl: str, reactionActor: str,
|
||||
emojiContent: str) -> bool:
|
||||
"""Is the given post already emoji reacted by the given handle?
|
||||
"""
|
||||
postFilename = \
|
||||
locatePost(baseDir, nickname, domain, postUrl)
|
||||
if not postFilename:
|
||||
return False
|
||||
postJsonObject = loadJson(postFilename, 1)
|
||||
if not postJsonObject:
|
||||
return False
|
||||
if not hasObjectDict(postJsonObject):
|
||||
return False
|
||||
if not postJsonObject['object'].get('reactions'):
|
||||
return False
|
||||
if not postJsonObject['object']['reactions'].get('items'):
|
||||
return False
|
||||
for react in postJsonObject['object']['reactions']['items']:
|
||||
if not react.get('type'):
|
||||
continue
|
||||
if not react.get('content'):
|
||||
continue
|
||||
if not react.get('actor'):
|
||||
continue
|
||||
if react['type'] != 'EmojiReact':
|
||||
continue
|
||||
if react['content'] != emojiContent:
|
||||
continue
|
||||
if react['actor'] == reactionActor:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _likeNotify(baseDir: str, domain: str, onionDomain: str,
|
||||
handle: str, actor: str, url: str) -> None:
|
||||
"""Creates a notification that a like has arrived
|
||||
|
@ -2130,6 +2440,71 @@ def _likeNotify(baseDir: str, domain: str, onionDomain: str,
|
|||
pass
|
||||
|
||||
|
||||
def _reactionNotify(baseDir: str, domain: str, onionDomain: str,
|
||||
handle: str, actor: str,
|
||||
url: str, emojiContent: str) -> None:
|
||||
"""Creates a notification that an emoji reaction has arrived
|
||||
"""
|
||||
# This is not you reacting to your own post
|
||||
if actor in url:
|
||||
return
|
||||
|
||||
# check that the reaction post was by this handle
|
||||
nickname = handle.split('@')[0]
|
||||
if '/' + domain + '/users/' + nickname not in url:
|
||||
if not onionDomain:
|
||||
return
|
||||
if '/' + onionDomain + '/users/' + nickname not in url:
|
||||
return
|
||||
|
||||
accountDir = baseDir + '/accounts/' + handle
|
||||
|
||||
# are reaction notifications enabled?
|
||||
notifyReactionEnabledFilename = accountDir + '/.notifyReactions'
|
||||
if not os.path.isfile(notifyReactionEnabledFilename):
|
||||
return
|
||||
|
||||
reactionFile = accountDir + '/.newReaction'
|
||||
if os.path.isfile(reactionFile):
|
||||
if '##sent##' not in open(reactionFile).read():
|
||||
return
|
||||
|
||||
reactionNickname = getNicknameFromActor(actor)
|
||||
reactionDomain, reactionPort = getDomainFromActor(actor)
|
||||
if reactionNickname and reactionDomain:
|
||||
reactionHandle = reactionNickname + '@' + reactionDomain
|
||||
else:
|
||||
print('_reactionNotify reactionHandle: ' +
|
||||
str(reactionNickname) + '@' + str(reactionDomain))
|
||||
reactionHandle = actor
|
||||
if reactionHandle != handle:
|
||||
reactionStr = \
|
||||
reactionHandle + ' ' + url + '?reactBy=' + actor + \
|
||||
';emoj=' + emojiContent
|
||||
prevReactionFile = accountDir + '/.prevReaction'
|
||||
# was there a previous reaction notification?
|
||||
if os.path.isfile(prevReactionFile):
|
||||
# is it the same as the current notification ?
|
||||
with open(prevReactionFile, 'r') as fp:
|
||||
prevReactionStr = fp.read()
|
||||
if prevReactionStr == reactionStr:
|
||||
return
|
||||
try:
|
||||
with open(prevReactionFile, 'w+') as fp:
|
||||
fp.write(reactionStr)
|
||||
except BaseException:
|
||||
print('EX: ERROR: unable to save previous reaction notification ' +
|
||||
prevReactionFile)
|
||||
pass
|
||||
try:
|
||||
with open(reactionFile, 'w+') as fp:
|
||||
fp.write(reactionStr)
|
||||
except BaseException:
|
||||
print('EX: ERROR: unable to write reaction notification file ' +
|
||||
reactionFile)
|
||||
pass
|
||||
|
||||
|
||||
def _notifyPostArrival(baseDir: str, handle: str, url: str) -> None:
|
||||
"""Creates a notification that a new post has arrived.
|
||||
This is for followed accounts with the notify checkbox enabled
|
||||
|
@ -2834,6 +3209,51 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
|
|||
print('DEBUG: Undo like accepted from ' + actor)
|
||||
return False
|
||||
|
||||
if _receiveReaction(recentPostsCache,
|
||||
session, handle, isGroup,
|
||||
baseDir, httpPrefix,
|
||||
domain, port,
|
||||
onionDomain,
|
||||
sendThreads, postLog,
|
||||
cachedWebfingers,
|
||||
personCache,
|
||||
messageJson,
|
||||
federationList,
|
||||
debug, signingPrivateKeyPem,
|
||||
maxRecentPosts, translate,
|
||||
allowDeletion,
|
||||
YTReplacementDomain,
|
||||
twitterReplacementDomain,
|
||||
peertubeInstances,
|
||||
allowLocalNetworkAccess,
|
||||
themeName, systemLanguage,
|
||||
maxLikeCount, CWlists, listsEnabled):
|
||||
if debug:
|
||||
print('DEBUG: Reaction accepted from ' + actor)
|
||||
return False
|
||||
|
||||
if _receiveUndoReaction(recentPostsCache,
|
||||
session, handle, isGroup,
|
||||
baseDir, httpPrefix,
|
||||
domain, port,
|
||||
sendThreads, postLog,
|
||||
cachedWebfingers,
|
||||
personCache,
|
||||
messageJson,
|
||||
federationList,
|
||||
debug, signingPrivateKeyPem,
|
||||
maxRecentPosts, translate,
|
||||
allowDeletion,
|
||||
YTReplacementDomain,
|
||||
twitterReplacementDomain,
|
||||
peertubeInstances,
|
||||
allowLocalNetworkAccess,
|
||||
themeName, systemLanguage,
|
||||
maxLikeCount, CWlists, listsEnabled):
|
||||
if debug:
|
||||
print('DEBUG: Undo reaction accepted from ' + actor)
|
||||
return False
|
||||
|
||||
if _receiveBookmark(recentPostsCache,
|
||||
session, handle, isGroup,
|
||||
baseDir, httpPrefix,
|
||||
|
|
32
like.py
|
@ -35,6 +35,22 @@ from auth import createBasicAuthHeader
|
|||
from posts import getPersonBox
|
||||
|
||||
|
||||
def noOfLikes(postJsonObject: {}) -> int:
|
||||
"""Returns the number of likes ona given post
|
||||
"""
|
||||
obj = postJsonObject
|
||||
if hasObjectDict(postJsonObject):
|
||||
obj = postJsonObject['object']
|
||||
if not obj.get('likes'):
|
||||
return 0
|
||||
if not isinstance(obj['likes'], dict):
|
||||
return 0
|
||||
if not obj['likes'].get('items'):
|
||||
obj['likes']['items'] = []
|
||||
obj['likes']['totalItems'] = 0
|
||||
return len(obj['likes']['items'])
|
||||
|
||||
|
||||
def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool:
|
||||
"""Returns True if the given post is liked by the given person
|
||||
"""
|
||||
|
@ -52,22 +68,6 @@ def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def noOfLikes(postJsonObject: {}) -> int:
|
||||
"""Returns the number of likes ona given post
|
||||
"""
|
||||
obj = postJsonObject
|
||||
if hasObjectDict(postJsonObject):
|
||||
obj = postJsonObject['object']
|
||||
if not obj.get('likes'):
|
||||
return 0
|
||||
if not isinstance(obj['likes'], dict):
|
||||
return 0
|
||||
if not obj['likes'].get('items'):
|
||||
obj['likes']['items'] = []
|
||||
obj['likes']['totalItems'] = 0
|
||||
return len(obj['likes']['items'])
|
||||
|
||||
|
||||
def _like(recentPostsCache: {},
|
||||
session, baseDir: str, federationList: [],
|
||||
nickname: str, domain: str, port: int,
|
||||
|
|
2
media.py
|
@ -140,7 +140,7 @@ def _spoofMetaData(baseDir: str, nickname: str, domain: str,
|
|||
camMake, camModel, camSerialNumber) = \
|
||||
spoofGeolocation(baseDir, spoofCity, currTimeAdjusted,
|
||||
decoySeed, None, None)
|
||||
if os.system('exiftool -artist="' + nickname + '" ' +
|
||||
if os.system('exiftool -artist=@"' + nickname + '@' + domain + '" ' +
|
||||
'-Make="' + camMake + '" ' +
|
||||
'-Model="' + camModel + '" ' +
|
||||
'-Comment="' + str(camSerialNumber) + '" ' +
|
||||
|
|
23
outbox.py
|
@ -48,6 +48,8 @@ from skills import outboxSkills
|
|||
from availability import outboxAvailability
|
||||
from like import outboxLike
|
||||
from like import outboxUndoLike
|
||||
from reaction import outboxReaction
|
||||
from reaction import outboxUndoReaction
|
||||
from bookmarks import outboxBookmark
|
||||
from bookmarks import outboxUndoBookmark
|
||||
from delete import outboxDelete
|
||||
|
@ -338,9 +340,10 @@ def postMessageToOutbox(session, translate: {},
|
|||
'/system/' +
|
||||
'media_attachments/files/')
|
||||
|
||||
permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo',
|
||||
'Update', 'Add', 'Remove', 'Block', 'Delete',
|
||||
'Skill', 'Ignore')
|
||||
permittedOutboxTypes = (
|
||||
'Create', 'Announce', 'Like', 'EmojiReact', 'Follow', 'Undo',
|
||||
'Update', 'Add', 'Remove', 'Block', 'Delete', 'Skill', 'Ignore'
|
||||
)
|
||||
if messageJson['type'] not in permittedOutboxTypes:
|
||||
if debug:
|
||||
print('DEBUG: POST to outbox - ' + messageJson['type'] +
|
||||
|
@ -547,6 +550,20 @@ def postMessageToOutbox(session, translate: {},
|
|||
baseDir, httpPrefix,
|
||||
postToNickname, domain, port,
|
||||
messageJson, debug)
|
||||
|
||||
if debug:
|
||||
print('DEBUG: handle any emoji reaction requests')
|
||||
outboxReaction(recentPostsCache,
|
||||
baseDir, httpPrefix,
|
||||
postToNickname, domain, port,
|
||||
messageJson, debug)
|
||||
if debug:
|
||||
print('DEBUG: handle any undo emoji reaction requests')
|
||||
outboxUndoReaction(recentPostsCache,
|
||||
baseDir, httpPrefix,
|
||||
postToNickname, domain, port,
|
||||
messageJson, debug)
|
||||
|
||||
if debug:
|
||||
print('DEBUG: handle any undo announce requests')
|
||||
outboxUndoAnnounce(recentPostsCache,
|
||||
|
|
|
@ -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'
|
||||
|
|
8
posts.py
|
@ -1562,7 +1562,7 @@ def _postIsAddressedToFollowers(baseDir: str,
|
|||
|
||||
|
||||
def pinPost(baseDir: str, nickname: str, domain: str,
|
||||
pinnedContent: str) -> None:
|
||||
pinnedContent: str, followersOnly: bool) -> None:
|
||||
"""Pins the given post Id to the profile of then given account
|
||||
"""
|
||||
accountDir = acctDir(baseDir, nickname, domain)
|
||||
|
@ -1581,7 +1581,6 @@ def undoPinnedPost(baseDir: str, nickname: str, domain: str) -> None:
|
|||
os.remove(pinnedFilename)
|
||||
except BaseException:
|
||||
print('EX: undoPinnedPost unable to delete ' + pinnedFilename)
|
||||
pass
|
||||
|
||||
|
||||
def getPinnedPostAsJson(baseDir: str, httpPrefix: str,
|
||||
|
@ -3507,6 +3506,11 @@ def removePostInteractions(postJsonObject: {}, force: bool) -> bool:
|
|||
postObj['likes'] = {
|
||||
'items': []
|
||||
}
|
||||
# clear the reactions
|
||||
if postObj.get('reactions'):
|
||||
postObj['reactions'] = {
|
||||
'items': []
|
||||
}
|
||||
# remove other collections
|
||||
removeCollections = (
|
||||
'replies', 'shares', 'bookmarks', 'ignores'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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()
|
||||
|
|
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
|
@ -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": "حدد رد الفعل"
|
||||
}
|
||||
|
|
|
@ -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ó"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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ú"
|
||||
}
|
||||
|
|
|
@ -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": "प्रतिक्रिया का चयन करें"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "反応を選択"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "Выберите реакцию"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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": "选择反应"
|
||||
}
|
||||
|
|
66
utils.py
|
@ -2258,6 +2258,72 @@ def undoLikesCollectionEntry(recentPostsCache: {},
|
|||
saveJson(postJsonObject, postFilename)
|
||||
|
||||
|
||||
def undoReactionCollectionEntry(recentPostsCache: {},
|
||||
baseDir: str, postFilename: str,
|
||||
objectUrl: str,
|
||||
actor: str, domain: str, debug: bool,
|
||||
postJsonObject: {}, emojiContent: str) -> None:
|
||||
"""Undoes an emoji reaction for a particular actor
|
||||
"""
|
||||
if not postJsonObject:
|
||||
postJsonObject = loadJson(postFilename)
|
||||
if not postJsonObject:
|
||||
return
|
||||
# remove any cached version of this post so that the
|
||||
# like icon is changed
|
||||
nickname = getNicknameFromActor(actor)
|
||||
cachedPostFilename = getCachedPostFilename(baseDir, nickname,
|
||||
domain, postJsonObject)
|
||||
if cachedPostFilename:
|
||||
if os.path.isfile(cachedPostFilename):
|
||||
try:
|
||||
os.remove(cachedPostFilename)
|
||||
except BaseException:
|
||||
print('EX: undoReactionCollectionEntry ' +
|
||||
'unable to delete cached post ' +
|
||||
str(cachedPostFilename))
|
||||
pass
|
||||
removePostFromCache(postJsonObject, recentPostsCache)
|
||||
|
||||
if not postJsonObject.get('type'):
|
||||
return
|
||||
if postJsonObject['type'] != 'Create':
|
||||
return
|
||||
obj = postJsonObject
|
||||
if hasObjectDict(postJsonObject):
|
||||
obj = postJsonObject['object']
|
||||
if not obj.get('reactions'):
|
||||
return
|
||||
if not isinstance(obj['reactions'], dict):
|
||||
return
|
||||
if not obj['reactions'].get('items'):
|
||||
return
|
||||
totalItems = 0
|
||||
if obj['reactions'].get('totalItems'):
|
||||
totalItems = obj['reactions']['totalItems']
|
||||
itemFound = False
|
||||
for likeItem in obj['reactions']['items']:
|
||||
if likeItem.get('actor'):
|
||||
if likeItem['actor'] == actor and \
|
||||
likeItem['content'] == emojiContent:
|
||||
if debug:
|
||||
print('DEBUG: emoji reaction was removed for ' + actor)
|
||||
obj['reactions']['items'].remove(likeItem)
|
||||
itemFound = True
|
||||
break
|
||||
if not itemFound:
|
||||
return
|
||||
if totalItems == 1:
|
||||
if debug:
|
||||
print('DEBUG: emoji reaction was removed from post')
|
||||
del obj['reactions']
|
||||
else:
|
||||
itlen = len(obj['reactions']['items'])
|
||||
obj['reactions']['totalItems'] = itlen
|
||||
|
||||
saveJson(postJsonObject, postFilename)
|
||||
|
||||
|
||||
def undoAnnounceCollectionEntry(recentPostsCache: {},
|
||||
baseDir: str, postFilename: str,
|
||||
actor: str, domain: str, debug: bool) -> None:
|
||||
|
|
193
webapp_post.py
|
@ -9,6 +9,7 @@ __module_group__ = "Web Interface"
|
|||
|
||||
import os
|
||||
import time
|
||||
import urllib.parse
|
||||
from dateutil.parser import parse
|
||||
from auth import createPassword
|
||||
from git import isGitPatch
|
||||
|
@ -61,6 +62,7 @@ from content import switchWords
|
|||
from person import isPersonSnoozed
|
||||
from person import getPersonAvatarUrl
|
||||
from announce import announcedByPerson
|
||||
from webapp_utils import getBannerFile
|
||||
from webapp_utils import getAvatarImageUrl
|
||||
from webapp_utils import updateAvatarImageCache
|
||||
from webapp_utils import loadIndividualPostAsHtmlFromCache
|
||||
|
@ -79,6 +81,7 @@ from speaker import updateSpeaker
|
|||
from languages import autoTranslatePost
|
||||
from blocking import isBlocked
|
||||
from blocking import addCWfromLists
|
||||
from reaction import htmlEmojiReactions
|
||||
|
||||
|
||||
def _htmlPostMetadataOpenGraph(domain: str, postJsonObject: {}) -> str:
|
||||
|
@ -698,6 +701,41 @@ def _getBookmarkIconHtml(nickname: str, domainFull: str,
|
|||
return bookmarkStr
|
||||
|
||||
|
||||
def _getReactionIconHtml(nickname: str, domainFull: str,
|
||||
postJsonObject: {},
|
||||
isModerationPost: bool,
|
||||
translate: {},
|
||||
enableTimingLog: bool,
|
||||
postStartTime, boxName: str,
|
||||
pageNumberParam: str,
|
||||
timelinePostReaction: str) -> str:
|
||||
"""Returns html for reaction icon/button
|
||||
"""
|
||||
reactionStr = ''
|
||||
|
||||
if isModerationPost:
|
||||
return reactionStr
|
||||
|
||||
reactionIcon = 'reaction.png'
|
||||
reactionTitle = 'Select reaction'
|
||||
if translate.get(reactionTitle):
|
||||
reactionTitle = translate[reactionTitle]
|
||||
_logPostTiming(enableTimingLog, postStartTime, '12.65')
|
||||
reactionPostId = removeIdEnding(postJsonObject['object']['id'])
|
||||
reactionStr = \
|
||||
' <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()
|
||||
|
|
|
@ -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 += \
|
||||
|
|