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 += \
'