diff --git a/posts.py b/posts.py index eefee952c..21179a06e 100644 --- a/posts.py +++ b/posts.py @@ -32,6 +32,7 @@ from webfinger import webfingerHandle from httpsig import createSignedHeader from siteactive import siteIsActive from languages import understoodPostLanguage +from utils import getUserPaths from utils import invalidCiphertext from utils import hasObjectStringType from utils import removeIdEnding @@ -1259,6 +1260,29 @@ def _createPostPlaceAndTime(eventDate: str, endDate: str, return eventDateStr +def _consolidateActorsList(actorsList: []) -> None: + """ consolidate duplicated actors + https://domain/@nick gets merged with https://domain/users/nick + """ + possibleDuplicateActors = [] + for ccActor in actorsList: + if '/@' in ccActor: + if ccActor not in possibleDuplicateActors: + possibleDuplicateActors.append(ccActor) + if possibleDuplicateActors: + uPaths = getUserPaths() + removeActors = [] + for ccActor in possibleDuplicateActors: + for usrPath in uPaths: + ccActorFull = ccActor.replace('/@', usrPath) + if ccActorFull in actorsList: + if ccActor not in removeActors: + removeActors.append(ccActor) + break + for ccActor in removeActors: + actorsList.remove(ccActor) + + def _createPostMentions(ccUrl: str, newPost: {}, toRecipients: [], tags: []) -> None: """Updates mentions for a new post @@ -1267,9 +1291,10 @@ def _createPostMentions(ccUrl: str, newPost: {}, return if len(ccUrl) == 0: return - newPost['cc'] = [ccUrl] + if newPost.get('object'): - newPost['object']['cc'] = [ccUrl] + if ccUrl not in newPost['object']['cc']: + newPost['object']['cc'] = [ccUrl] + newPost['object']['cc'] # if this is a public post then include any mentions in cc toCC = newPost['object']['cc'] @@ -1283,6 +1308,13 @@ def _createPostMentions(ccUrl: str, newPost: {}, if tag['href'] not in toCC: newPost['object']['cc'].append(tag['href']) + _consolidateActorsList(newPost['object']['cc']) + newPost['cc'] = newPost['object']['cc'] + else: + if ccUrl not in newPost['cc']: + newPost['cc'] = [ccUrl] + newPost['cc'] + _consolidateActorsList(['cc']) + def _createPostModReport(baseDir: str, isModerationReport: bool, newPost: {}, @@ -1302,6 +1334,30 @@ def _createPostModReport(baseDir: str, modFile.write(newPostId + '\n') +def getActorFromInReplyTo(inReplyTo: str) -> str: + """Tries to get the replied to actor from the inReplyTo post id + Note: this will not always be successful for some instance types + """ + replyNickname = getNicknameFromActor(inReplyTo) + if not replyNickname: + return None + replyActor = None + if '/' + replyNickname + '/' in inReplyTo: + replyActor = \ + inReplyTo.split('/' + replyNickname + '/')[0] + \ + '/' + replyNickname + elif '#' + replyNickname + '#' in inReplyTo: + replyActor = \ + inReplyTo.split('#' + replyNickname + '#')[0] + \ + '#' + replyNickname + replyActor = replyActor.replace('#', '/') + if not replyActor: + return None + if '://' not in replyActor: + return None + return replyActor + + def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, toUrl: str, ccUrl: str, httpPrefix: str, content: str, @@ -1394,14 +1450,15 @@ def _createPostBase(baseDir: str, if mention not in toCC: toCC.append(mention) + isPublic = False + for recipient in toRecipients: + if recipient.endswith('#Public'): + isPublic = True + break + # create a list of hashtags # Only posts which are #Public are searchable by hashtag if hashtagsDict: - isPublic = False - for recipient in toRecipients: - if recipient.endswith('#Public'): - isPublic = True - break for tagName, tag in hashtagsDict.items(): if not tagExists(tag['type'], tag['name'], tags): tags.append(tag) @@ -1421,18 +1478,27 @@ def _createPostBase(baseDir: str, postContext = getIndividualPostContext() - # make sure that CC doesn't also contain a To address - # eg. To: [ "https://mydomain/users/foo/followers" ] - # CC: [ "X", "Y", "https://mydomain/users/foo", "Z" ] - removeFromCC = [] - for ccRecipient in toCC: - for sendToActor in toRecipients: - if ccRecipient in sendToActor and \ - ccRecipient not in removeFromCC: - removeFromCC.append(ccRecipient) - break - for ccRemoval in removeFromCC: - toCC.remove(ccRemoval) + if not isPublic: + # make sure that CC doesn't also contain a To address + # eg. To: [ "https://mydomain/users/foo/followers" ] + # CC: [ "X", "Y", "https://mydomain/users/foo", "Z" ] + removeFromCC = [] + for ccRecipient in toCC: + for sendToActor in toRecipients: + if ccRecipient in sendToActor and \ + ccRecipient not in removeFromCC: + removeFromCC.append(ccRecipient) + break + for ccRemoval in removeFromCC: + toCC.remove(ccRemoval) + else: + if inReplyTo: + # If this is a public post then get the actor being + # replied to end ensure that it is within the CC list + replyActor = getActorFromInReplyTo(inReplyTo) + if replyActor: + if replyActor not in toCC: + toCC.append(replyActor) # the type of post to be made postObjectType = 'Note' diff --git a/tests.py b/tests.py index 07f6524a6..34b3122cf 100644 --- a/tests.py +++ b/tests.py @@ -36,6 +36,7 @@ from threads import threadWithTrace from daemon import runDaemon from session import createSession from session import getJson +from posts import getActorFromInReplyTo from posts import regenerateIndexForBox from posts import removePostInteractions from posts import getMentionedPeople @@ -4239,6 +4240,7 @@ def _testReplyToPublicPost(baseDir: str) -> None: mediaType = None imageDescription = 'Some description' city = 'London, England' + testInReplyTo = postId testInReplyToAtomUri = None testSubject = None testSchedulePost = False @@ -4254,7 +4256,7 @@ def _testReplyToPublicPost(baseDir: str) -> None: content, followersOnly, saveToFile, clientToServer, commentsEnabled, attachImageFilename, mediaType, - imageDescription, city, postId, + imageDescription, city, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, @@ -4275,8 +4277,20 @@ def _testReplyToPublicPost(baseDir: str) -> None: assert len(reply['object']['cc']) >= 1 assert reply['object']['cc'][0].endswith(nickname + '/followers') assert len(reply['object']['tag']) == 1 + if len(reply['object']['cc']) != 2: + print('reply["object"]["cc"]: ' + str(reply['object']['cc'])) assert len(reply['object']['cc']) == 2 - assert reply['object']['cc'][1] == httpPrefix + '://rat.site/@ninjarodent' + assert reply['object']['cc'][1] == \ + httpPrefix + '://rat.site/users/ninjarodent' + + assert len(reply['to']) == 1 + assert reply['to'][0].endswith('#Public') + assert len(reply['cc']) >= 1 + assert reply['cc'][0].endswith(nickname + '/followers') + if len(reply['cc']) != 2: + print('reply["cc"]: ' + str(reply['cc'])) + assert len(reply['cc']) == 2 + assert reply['cc'][1] == httpPrefix + '://rat.site/users/ninjarodent' def _getFunctionCallArgs(name: str, lines: [], startLineCtr: int) -> []: @@ -6013,6 +6027,18 @@ def _testHttpsigBaseNew(withDigest: bool, baseDir: str, shutil.rmtree(path, ignore_errors=False, onerror=None) +def _testGetActorFromInReplyTo() -> None: + print('testGetActorFromInReplyTo') + inReplyTo = \ + 'https://fosstodon.org/users/bashrc/statuses/107400700612621140' + replyActor = getActorFromInReplyTo(inReplyTo) + assert replyActor == 'https://fosstodon.org/users/bashrc' + + inReplyTo = 'https://fosstodon.org/activity/107400700612621140' + replyActor = getActorFromInReplyTo(inReplyTo) + assert replyActor is None + + def runAllTests(): baseDir = os.getcwd() print('Running tests...') @@ -6020,6 +6046,7 @@ def runAllTests(): _translateOntology(baseDir) _testGetPriceFromString() _testFunctions() + _testGetActorFromInReplyTo() _testValidEmojiContent() _testAddCWfromLists(baseDir) _testWordsSimilarity() diff --git a/utils.py b/utils.py index 98814262d..127b57a21 100644 --- a/utils.py +++ b/utils.py @@ -600,6 +600,14 @@ def removeIdEnding(idStr: str) -> str: return idStr +def removeHashFromPostId(postId: str) -> str: + """Removes any has from a post id + """ + if '#' not in postId: + return postId + return postId.split('#')[0] + + def getProtocolPrefixes() -> []: """Returns a list of valid prefixes """ @@ -3138,6 +3146,8 @@ def hasActor(postJsonObject: {}, debug: bool) -> bool: """Does the given post have an actor? """ if postJsonObject.get('actor'): + if '#' in postJsonObject['actor']: + return False return True if debug: if postJsonObject.get('type'): diff --git a/webapp_post.py b/webapp_post.py index 856b7effb..1f04fdf3c 100644 --- a/webapp_post.py +++ b/webapp_post.py @@ -23,6 +23,7 @@ from posts import postIsMuted from posts import getPersonBox from posts import downloadAnnounce from posts import populateRepliesJson +from utils import removeHashFromPostId from utils import removeHtml from utils import getActorLanguagesList from utils import getBaseContentFromPost @@ -390,11 +391,8 @@ def _getReplyIconHtml(baseDir: str, nickname: str, domain: str, return replyStr # reply is permitted - create reply icon - if '#' not in postJsonObject['object']['id']: - replyToLink = removeIdEnding(postJsonObject['object']['id']) - else: - replyToLink = \ - removeIdEnding(postJsonObject['object']['id'].split('#')[0]) + replyToLink = removeHashFromPostId(postJsonObject['object']['id']) + replyToLink = removeIdEnding(replyToLink) # see Mike MacGirvin's replyTo suggestion if postJsonObject['object'].get('replyTo'): @@ -575,11 +573,8 @@ def _getAnnounceIconHtml(isAnnounced: bool, unannounceLinkStr = '?unannounce=' + \ removeIdEnding(announceJsonObject['id']) - if '#' not in postJsonObject['object']['id']: - announcePostId = removeIdEnding(postJsonObject['object']['id']) - else: - announcePostId = \ - removeIdEnding(postJsonObject['object']['id'].split('#')[0]) + announcePostId = removeHashFromPostId(postJsonObject['object']['id']) + announcePostId = removeIdEnding(announcePostId) announceLinkStr = '?' + \ announceLink + '=' + announcePostId + pageNumberParam announceStr = \ @@ -647,10 +642,8 @@ def _getLikeIconHtml(nickname: str, domainFull: str, likeStr += '\n' - if '#' not in postJsonObject['id']: - likePostId = removeIdEnding(postJsonObject['id']) - else: - likePostId = removeIdEnding(postJsonObject['id'].split('#')[0]) + likePostId = removeHashFromPostId(postJsonObject['id']) + likePostId = removeIdEnding(likePostId) likeStr += \ '