From 51f2b060c91fe46a8b76f65b686a6b3026cf5a0c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 6 Jun 2020 19:34:39 +0100 Subject: [PATCH] Avoid race condition when repeating post --- announce.py | 136 +--------------------------------------------------- daemon.py | 26 ++++++++++ utils.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 134 deletions(-) diff --git a/announce.py b/announce.py index 83f02f6f..68529de2 100644 --- a/announce.py +++ b/announce.py @@ -6,18 +6,15 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" -import os -from pprint import pprint -from utils import removePostFromCache from utils import getStatusNumber from utils import createOutboxDir from utils import urlPermitted from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost -from utils import getCachedPostFilename -from utils import loadJson from utils import saveJson +from utils import undoAnnounceCollectionEntry +from utils import updateAnnounceCollection from posts import sendSignedJson from posts import getPersonBox from session import postJson @@ -74,135 +71,6 @@ def outboxAnnounce(recentPostsCache: {}, return False -def undoAnnounceCollectionEntry(recentPostsCache: {}, - baseDir: str, postFilename: str, - actor: str, domain: str, debug: bool) -> None: - """Undoes an announce for a particular actor by removing it from - the "shares" collection within a post. Note that the "shares" - collection has no relation to shared items in shares.py. It's - shares of posts, not shares of physical objects. - """ - postJsonObject = loadJson(postFilename) - if postJsonObject: - # remove any cached version of this announce so that the announce - # icon is changed - nickname = getNicknameFromActor(actor) - cachedPostFilename = getCachedPostFilename(baseDir, nickname, domain, - postJsonObject) - if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - removePostFromCache(postJsonObject, recentPostsCache) - - if not postJsonObject.get('type'): - return - if postJsonObject['type'] != 'Create': - return - if not postJsonObject.get('object'): - if debug: - pprint(postJsonObject) - print('DEBUG: post has no object') - return - if not isinstance(postJsonObject['object'], dict): - return - if not postJsonObject['object'].get('shares'): - return - if not postJsonObject['object']['shares'].get('items'): - return - totalItems = 0 - if postJsonObject['object']['shares'].get('totalItems'): - totalItems = postJsonObject['object']['shares']['totalItems'] - itemFound = False - for announceItem in postJsonObject['object']['shares']['items']: - if announceItem.get('actor'): - if announceItem['actor'] == actor: - if debug: - print('DEBUG: Announce was removed for ' + actor) - anIt = announceItem - postJsonObject['object']['shares']['items'].remove(anIt) - itemFound = True - break - if itemFound: - if totalItems == 1: - if debug: - print('DEBUG: shares (announcements) ' + - 'was removed from post') - del postJsonObject['object']['shares'] - else: - itlen = len(postJsonObject['object']['shares']['items']) - postJsonObject['object']['shares']['totalItems'] = itlen - - saveJson(postJsonObject, postFilename) - - -def updateAnnounceCollection(recentPostsCache: {}, - baseDir: str, postFilename: str, - actor: str, domain: str, debug: bool) -> None: - """Updates the announcements collection within a post - Confusingly this is known as "shares", but isn't the - same as shared items within shares.py - It's shares of posts, not shares of physical objects. - """ - postJsonObject = loadJson(postFilename) - if postJsonObject: - # remove any cached version of this announce so that the announce - # icon is changed - nickname = getNicknameFromActor(actor) - cachedPostFilename = getCachedPostFilename(baseDir, nickname, domain, - postJsonObject) - if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - removePostFromCache(postJsonObject, recentPostsCache) - - if not postJsonObject.get('object'): - if debug: - pprint(postJsonObject) - print('DEBUG: post ' + postFilename + ' has no object') - return - if not isinstance(postJsonObject['object'], dict): - return - postUrl = postJsonObject['id'].replace('/activity', '') + '/shares' - if not postJsonObject['object'].get('shares'): - if debug: - print('DEBUG: Adding initial shares (announcements) to ' + - postUrl) - announcementsJson = { - "@context": "https://www.w3.org/ns/activitystreams", - 'id': postUrl, - 'type': 'Collection', - "totalItems": 1, - 'items': [{ - 'type': 'Announce', - 'actor': actor - }] - } - postJsonObject['object']['shares'] = announcementsJson - else: - if postJsonObject['object']['shares'].get('items'): - sharesItems = postJsonObject['object']['shares']['items'] - for announceItem in sharesItems: - if announceItem.get('actor'): - if announceItem['actor'] == actor: - return - newAnnounce = { - 'type': 'Announce', - 'actor': actor - } - postJsonObject['object']['shares']['items'].append(newAnnounce) - itlen = len(postJsonObject['object']['shares']['items']) - postJsonObject['object']['shares']['totalItems'] = itlen - else: - if debug: - print('DEBUG: shares (announcements) section of post ' + - 'has no items list') - - if debug: - print('DEBUG: saving post with shares (announcements) added') - pprint(postJsonObject) - saveJson(postJsonObject, postFilename) - - def announcedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool: """Returns True if the given post is announced by the given person """ diff --git a/daemon.py b/daemon.py index 7b2eb96c..85e90238 100644 --- a/daemon.py +++ b/daemon.py @@ -143,6 +143,8 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import undoAnnounceCollectionEntry +from utils import updateAnnounceCollection from utils import updateLikesCollection from utils import undoLikesCollectionEntry from utils import deletePost @@ -2275,6 +2277,18 @@ class PubServer(BaseHTTPRequestHandler): self.server.debug, self.server.projectVersion) if announceJson: + # directly announce the post file + announcePostFilename = \ + locatePost(self.server.baseDir, + self.postToNickname, + self.server.domain, + repeatUrl) + if announcePostFilename: + updateAnnounceCollection(self.server.recentPostsCache, + self.server.baseDir, + announcePostFilename, + actor, self.server.domain, + self.server.debug) self._postToOutboxThread(announceJson) self.server.GETbusy = False actorAbsolute = self.server.httpPrefix + '://' + \ @@ -2360,6 +2374,18 @@ class PubServer(BaseHTTPRequestHandler): 'type': 'Announce' } } + # directly undo announce in post file + announcePostFilename = \ + locatePost(self.server.baseDir, + self.postToNickname, + self.server.domain, + repeatUrl) + if announcePostFilename: + undoAnnounceCollectionEntry(self.server.recentPostsCache, + self.server.baseDir, + announcePostFilename, + actor, self.server.domain, + self.server.debug) self._postToOutboxThread(newUndoAnnounce) self.server.GETbusy = False actorAbsolute = self.server.httpPrefix + '://' + \ diff --git a/utils.py b/utils.py index 67d179d3..c8c228b2 100644 --- a/utils.py +++ b/utils.py @@ -923,3 +923,132 @@ def updateLikesCollection(recentPostsCache: {}, print('DEBUG: saving post with likes added') pprint(postJsonObject) saveJson(postJsonObject, postFilename) + + +def undoAnnounceCollectionEntry(recentPostsCache: {}, + baseDir: str, postFilename: str, + actor: str, domain: str, debug: bool) -> None: + """Undoes an announce for a particular actor by removing it from + the "shares" collection within a post. Note that the "shares" + collection has no relation to shared items in shares.py. It's + shares of posts, not shares of physical objects. + """ + postJsonObject = loadJson(postFilename) + if postJsonObject: + # remove any cached version of this announce so that the announce + # icon is changed + nickname = getNicknameFromActor(actor) + cachedPostFilename = getCachedPostFilename(baseDir, nickname, domain, + postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) + removePostFromCache(postJsonObject, recentPostsCache) + + if not postJsonObject.get('type'): + return + if postJsonObject['type'] != 'Create': + return + if not postJsonObject.get('object'): + if debug: + pprint(postJsonObject) + print('DEBUG: post has no object') + return + if not isinstance(postJsonObject['object'], dict): + return + if not postJsonObject['object'].get('shares'): + return + if not postJsonObject['object']['shares'].get('items'): + return + totalItems = 0 + if postJsonObject['object']['shares'].get('totalItems'): + totalItems = postJsonObject['object']['shares']['totalItems'] + itemFound = False + for announceItem in postJsonObject['object']['shares']['items']: + if announceItem.get('actor'): + if announceItem['actor'] == actor: + if debug: + print('DEBUG: Announce was removed for ' + actor) + anIt = announceItem + postJsonObject['object']['shares']['items'].remove(anIt) + itemFound = True + break + if itemFound: + if totalItems == 1: + if debug: + print('DEBUG: shares (announcements) ' + + 'was removed from post') + del postJsonObject['object']['shares'] + else: + itlen = len(postJsonObject['object']['shares']['items']) + postJsonObject['object']['shares']['totalItems'] = itlen + + saveJson(postJsonObject, postFilename) + + +def updateAnnounceCollection(recentPostsCache: {}, + baseDir: str, postFilename: str, + actor: str, domain: str, debug: bool) -> None: + """Updates the announcements collection within a post + Confusingly this is known as "shares", but isn't the + same as shared items within shares.py + It's shares of posts, not shares of physical objects. + """ + postJsonObject = loadJson(postFilename) + if postJsonObject: + # remove any cached version of this announce so that the announce + # icon is changed + nickname = getNicknameFromActor(actor) + cachedPostFilename = getCachedPostFilename(baseDir, nickname, domain, + postJsonObject) + if cachedPostFilename: + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) + removePostFromCache(postJsonObject, recentPostsCache) + + if not postJsonObject.get('object'): + if debug: + pprint(postJsonObject) + print('DEBUG: post ' + postFilename + ' has no object') + return + if not isinstance(postJsonObject['object'], dict): + return + postUrl = postJsonObject['id'].replace('/activity', '') + '/shares' + if not postJsonObject['object'].get('shares'): + if debug: + print('DEBUG: Adding initial shares (announcements) to ' + + postUrl) + announcementsJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'id': postUrl, + 'type': 'Collection', + "totalItems": 1, + 'items': [{ + 'type': 'Announce', + 'actor': actor + }] + } + postJsonObject['object']['shares'] = announcementsJson + else: + if postJsonObject['object']['shares'].get('items'): + sharesItems = postJsonObject['object']['shares']['items'] + for announceItem in sharesItems: + if announceItem.get('actor'): + if announceItem['actor'] == actor: + return + newAnnounce = { + 'type': 'Announce', + 'actor': actor + } + postJsonObject['object']['shares']['items'].append(newAnnounce) + itlen = len(postJsonObject['object']['shares']['items']) + postJsonObject['object']['shares']['totalItems'] = itlen + else: + if debug: + print('DEBUG: shares (announcements) section of post ' + + 'has no items list') + + if debug: + print('DEBUG: saving post with shares (announcements) added') + pprint(postJsonObject) + saveJson(postJsonObject, postFilename)