mirror of https://gitlab.com/bashrc2/epicyon
Handle emoji reactions
parent
5b9af53cf9
commit
68badb0e36
|
@ -1489,7 +1489,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)
|
||||
|
|
82
epicyon.py
82
epicyon.py
|
@ -81,6 +81,8 @@ 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 skills import sendSkillViaServer
|
||||
from availability import setAvailability
|
||||
from availability import sendAvailabilityViaServer
|
||||
|
@ -510,6 +512,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 +1621,42 @@ 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 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 +1691,43 @@ 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 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')
|
||||
|
|
419
inbox.py
419
inbox.py
|
@ -15,6 +15,8 @@ import random
|
|||
from linked_data_sig import verifyJsonSignature
|
||||
from languages import understoodPostLanguage
|
||||
from like import updateLikesCollection
|
||||
from reaction import updateReactionCollection
|
||||
from utils import removeHtml
|
||||
from utils import fileLastModified
|
||||
from utils import hasObjectString
|
||||
from utils import hasObjectStringObject
|
||||
|
@ -50,6 +52,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 +396,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 +419,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 +1209,271 @@ 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 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 +2339,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 +2435,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 + '/.notifyReaction'
|
||||
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 + '?reactionBy=' + actor + \
|
||||
';emoji=' + 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 +3204,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
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,
|
||||
|
|
23
outbox.py
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,
|
||||
|
|
5
posts.py
5
posts.py
|
@ -3506,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,496 @@
|
|||
__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
|
||||
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 posts import sendSignedJson
|
||||
from session import postJson
|
||||
from webfinger import webfingerHandle
|
||||
from auth import createBasicAuthHeader
|
||||
from posts import getPersonBox
|
||||
|
||||
|
||||
def noOfReactions(postJsonObject: {}, emojiContent: str) -> int:
|
||||
"""Returns the number of emoji reactions of a given content type on a post
|
||||
"""
|
||||
obj = postJsonObject
|
||||
if hasObjectDict(postJsonObject):
|
||||
obj = postJsonObject['object']
|
||||
if not obj.get('reactions'):
|
||||
return 0
|
||||
if not isinstance(obj['reactions'], dict):
|
||||
return 0
|
||||
if not obj['reactions'].get('items'):
|
||||
obj['reactions']['items'] = []
|
||||
obj['reactions']['totalItems'] = 0
|
||||
ctr = 0
|
||||
for item in obj['reactions']['items']:
|
||||
if not item.get('content'):
|
||||
continue
|
||||
if item['content'] == emojiContent:
|
||||
ctr += 1
|
||||
return ctr
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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 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'] = []
|
||||
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)
|
103
tests.py
103
tests.py
|
@ -107,6 +107,8 @@ 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 announce import announcePublic
|
||||
from announce import sendAnnounceViaServer
|
||||
from city import parseNogoString
|
||||
|
@ -1370,6 +1372,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 +3095,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 +3164,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 +3173,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'
|
||||
|
@ -4581,7 +4651,8 @@ def _testFunctions():
|
|||
'E2EEremoveDevice',
|
||||
'setOrganizationScheme',
|
||||
'fill_headers',
|
||||
'_nothing'
|
||||
'_nothing',
|
||||
'noOfReactions'
|
||||
]
|
||||
excludeImports = [
|
||||
'link',
|
||||
|
|
66
utils.py
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:
|
||||
|
|
Loading…
Reference in New Issue