From 3179a37975062d59d6943ae3c1dcd6d59b6b0082 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 29 Aug 2020 20:54:30 +0100 Subject: [PATCH 001/108] Tidying to reduce file reads --- cache.py | 5 +++-- daemon.py | 3 ++- utils.py | 18 ++++++++---------- webinterface.py | 20 ++++++++------------ 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/cache.py b/cache.py index 0ac180f80..08d7386c1 100644 --- a/cache.py +++ b/cache.py @@ -45,8 +45,9 @@ def getPersonFromCache(baseDir: str, personUrl: str, personCache: {}, # does the person exist as a cached file? cacheFilename = baseDir + '/cache/actors/' + \ personUrl.replace('/', '#')+'.json' - if os.path.isfile(getFileCaseInsensitive(cacheFilename)): - personJson = loadJson(getFileCaseInsensitive(cacheFilename)) + actorFilename = getFileCaseInsensitive(cacheFilename) + if actorFilename: + personJson = loadJson(actorFilename) if personJson: storePersonInCache(baseDir, personUrl, personJson, personCache, False) diff --git a/daemon.py b/daemon.py index 3b322a57e..bebb1ff27 100644 --- a/daemon.py +++ b/daemon.py @@ -7516,7 +7516,8 @@ class PubServer(BaseHTTPRequestHandler): if fields.get('removeTwitter'): if fields['removeTwitter'] == 'on': removeTwitterActive = True - with open(removeTwitterFilename, 'w+') as rFile: + with open(removeTwitterFilename, + 'w+') as rFile: rFile.write('\n') if not removeTwitterActive: if os.path.isfile(removeTwitterFilename): diff --git a/utils.py b/utils.py index f143bebbd..1a5e1b708 100644 --- a/utils.py +++ b/utils.py @@ -907,21 +907,19 @@ def searchBoxPosts(baseDir: str, nickname: str, domain: str, def getFileCaseInsensitive(path: str) -> str: """Returns a case specific filename given a case insensitive version of it """ - # does the given file exist? If so then we don't need - # to do a directory search if os.path.isfile(path): return path if path != path.lower(): if os.path.isfile(path.lower()): return path.lower() - directory, filename = os.path.split(path) - directory, filename = (directory or '.'), filename.lower() - for f in os.listdir(directory): - if f.lower() == filename: - newpath = os.path.join(directory, f) - if os.path.isfile(newpath): - return newpath - return path + # directory, filename = os.path.split(path) + # directory, filename = (directory or '.'), filename.lower() + # for f in os.listdir(directory): + # if f.lower() == filename: + # newpath = os.path.join(directory, f) + # if os.path.isfile(newpath): + # return newpath + return None def undoLikesCollectionEntry(recentPostsCache: {}, diff --git a/webinterface.py b/webinterface.py index 778565009..ad6691c2e 100644 --- a/webinterface.py +++ b/webinterface.py @@ -27,7 +27,6 @@ from matrix import getMatrixAddress from donate import getDonationUrl from utils import removeIdEnding from utils import getProtocolPrefixes -from utils import getFileCaseInsensitive from utils import searchBoxPosts from utils import isEventPost from utils import isBlogPost @@ -304,16 +303,13 @@ def getPersonAvatarUrl(baseDir: str, personUrl: str, personCache: {}, # get from locally stored image actorStr = personJson['id'].replace('/', '-') avatarImagePath = baseDir + '/cache/avatars/' + actorStr - if os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.png')): - return '/avatars/' + actorStr + '.png' - elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.jpg')): - return '/avatars/' + actorStr + '.jpg' - elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.gif')): - return '/avatars/' + actorStr + '.gif' - elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.webp')): - return '/avatars/' + actorStr + '.webp' - elif os.path.isfile(getFileCaseInsensitive(avatarImagePath)): - return '/avatars/' + actorStr + + imageExtension = ('png', 'jpg', 'jpeg', 'gif', 'webp') + for ext in imageExtension: + if os.path.isfile(avatarImagePath + '.' + ext): + return '/avatars/' + actorStr + '.' + ext + elif os.path.isfile(avatarImagePath.lower() + '.' + ext): + return '/avatars/' + actorStr.lower() + '.' + ext if personJson.get('icon'): if personJson['icon'].get('url'): @@ -6649,7 +6645,7 @@ def htmlCalendar(translate: {}, for weekOfMonth in range(1, 7): if dayOfMonth == daysInMonth: continue - calendarStr += ' \n' + calendarStr += ' \n' for dayNumber in range(1, 8): if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \ (weekOfMonth == 1 and dayNumber >= dow): From c86c1925a4d81faa4e0ad4484a8e3b761e1ca3fe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 29 Aug 2020 21:11:19 +0100 Subject: [PATCH 002/108] Check for none --- webinterface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/webinterface.py b/webinterface.py index ad6691c2e..47912e3e0 100644 --- a/webinterface.py +++ b/webinterface.py @@ -3808,6 +3808,9 @@ def individualPostAsHtml(allowDownloads: bool, storeToCache=True) -> str: """ Shows a single post as html """ + if not postJsonObject: + return '' + # benchmark postStartTime = time.time() From 2dc5bcfdd22b187b2265e753d0e536699c1ba78d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sat, 29 Aug 2020 21:14:44 +0100 Subject: [PATCH 003/108] Storing posts --- inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 107865bda..f5ae165a7 100644 --- a/inbox.py +++ b/inbox.py @@ -134,7 +134,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, avatarUrl = None if boxname != 'tlevents' and boxname != 'outbox': boxName = 'inbox' - individualPostAsHtml(recentPostsCache, maxRecentPosts, + individualPostAsHtml(True, recentPostsCache, maxRecentPosts, getIconsDir(baseDir), translate, pageNumber, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, postJsonObject, From 33987303a22dcb55f3704a72f20d8dc4607cadb7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 11:42:44 +0100 Subject: [PATCH 004/108] Refactoring profile edit --- daemon.py | 1435 +++++++++++++++++++++++++++-------------------------- 1 file changed, 721 insertions(+), 714 deletions(-) diff --git a/daemon.py b/daemon.py index bebb1ff27..ff263967c 100644 --- a/daemon.py +++ b/daemon.py @@ -1181,6 +1181,719 @@ class PubServer(BaseHTTPRequestHandler): '/users/' + nickname + '/statuses/' + userEnding2[1] return locatePost(baseDir, nickname, domain, messageId), nickname + def _profileUpdate(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool): + """Updates your user profile after editing via the Edit button + on the profile screen + """ + usersPath = path.replace('/profiledata', '') + usersPath = usersPath.replace('/editprofile', '') + actorStr = httpPrefix + '://' + domainFull + usersPath + if ' boundary=' in self.headers['Content-type']: + boundary = self.headers['Content-type'].split('boundary=')[1] + if ';' in boundary: + boundary = boundary.split(';')[0] + + nickname = getNicknameFromActor(actorStr) + if not nickname: + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + print('WARN: nickname not found in ' + actorStr) + self._redirect_headers(actorStr, cookie, callingDomain) + self.server.POSTbusy = False + return + length = int(self.headers['Content-length']) + if length > self.server.maxPostLength: + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + print('Maximum profile data length exceeded ' + + str(length)) + self._redirect_headers(actorStr, cookie, callingDomain) + self.server.POSTbusy = False + return + + try: + # read the bytes of the http form POST + postBytes = self.rfile.read(length) + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: connection was reset while ' + + 'reading bytes from http form POST') + else: + print('WARN: error while reading bytes ' + + 'from http form POST') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: failed to read bytes for POST') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + # extract each image type + actorChanged = True + profileMediaTypes = ('avatar', 'image', + 'banner', 'search_banner', + 'instanceLogo') + profileMediaTypesUploaded = {} + for mType in profileMediaTypes: + if debug: + print('DEBUG: profile update extracting ' + mType + + ' image or font from POST') + mediaBytes, postBytes = \ + extractMediaInFormPOST(postBytes, boundary, mType) + if mediaBytes: + if debug: + print('DEBUG: profile update ' + mType + + ' image or font was found. ' + + str(len(mediaBytes)) + ' bytes') + else: + if debug: + print('DEBUG: profile update, no ' + mType + + ' image or font was found in POST') + continue + + # Note: a .temp extension is used here so that at no + # time is an image with metadata publicly exposed, + # even for a few mS + if mType == 'instanceLogo': + filenameBase = \ + baseDir + '/accounts/login.temp' + else: + filenameBase = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/' + mType + '.temp' + + filename, attachmentMediaType = \ + saveMediaInFormPOST(mediaBytes, debug, + filenameBase) + if filename: + print('Profile update POST ' + mType + + ' media or font filename is ' + filename) + else: + print('Profile update, no ' + mType + + ' media or font filename in POST') + continue + + postImageFilename = filename.replace('.temp', '') + if debug: + print('DEBUG: POST ' + mType + + ' media removing metadata') + # remove existing etag + if os.path.isfile(postImageFilename + '.etag'): + try: + os.remove(postImageFilename + '.etag') + except BaseException: + pass + removeMetaData(filename, postImageFilename) + if os.path.isfile(postImageFilename): + print('profile update POST ' + mType + + ' image or font saved to ' + postImageFilename) + if mType != 'instanceLogo': + lastPartOfImageFilename = \ + postImageFilename.split('/')[-1] + profileMediaTypesUploaded[mType] = \ + lastPartOfImageFilename + actorChanged = True + else: + print('ERROR: profile update POST ' + mType + + ' image or font could not be saved to ' + + postImageFilename) + + fields = \ + extractTextFieldsInPOST(postBytes, boundary, + debug) + if debug: + if fields: + print('DEBUG: profile update text ' + + 'field extracted from POST ' + str(fields)) + else: + print('WARN: profile update, no text ' + + 'fields could be extracted from POST') + + actorFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '.json' + if os.path.isfile(actorFilename): + actorJson = loadJson(actorFilename) + if actorJson: + # update the avatar/image url file extension + uploads = profileMediaTypesUploaded.items() + for mType, lastPart in uploads: + repStr = '/' + lastPart + if mType == 'avatar': + lastPartOfUrl = \ + actorJson['icon']['url'].split('/')[-1] + srchStr = '/' + lastPartOfUrl + actorJson['icon']['url'] = \ + actorJson['icon']['url'].replace(srchStr, + repStr) + elif mType == 'image': + lastPartOfUrl = \ + actorJson['image']['url'].split('/')[-1] + srchStr = '/' + lastPartOfUrl + actorJson['image']['url'] = \ + actorJson['image']['url'].replace(srchStr, + repStr) + + skillCtr = 1 + newSkills = {} + while skillCtr < 10: + skillName = \ + fields.get('skillName' + str(skillCtr)) + if not skillName: + skillCtr += 1 + continue + skillValue = \ + fields.get('skillValue' + str(skillCtr)) + if not skillValue: + skillCtr += 1 + continue + if not actorJson['skills'].get(skillName): + actorChanged = True + else: + if actorJson['skills'][skillName] != \ + int(skillValue): + actorChanged = True + newSkills[skillName] = int(skillValue) + skillCtr += 1 + if len(actorJson['skills'].items()) != \ + len(newSkills.items()): + actorChanged = True + actorJson['skills'] = newSkills + if fields.get('password'): + if fields.get('passwordconfirm'): + if actorJson['password'] == \ + fields['passwordconfirm']: + if len(actorJson['password']) > 2: + # set password + pwd = actorJson['password'] + storeBasicCredentials(baseDir, + nickname, + pwd) + if fields.get('displayNickname'): + if fields['displayNickname'] != actorJson['name']: + actorJson['name'] = fields['displayNickname'] + actorChanged = True + if fields.get('themeDropdown'): + setTheme(baseDir, + fields['themeDropdown']) + + currentEmailAddress = getEmailAddress(actorJson) + if fields.get('email'): + if fields['email'] != currentEmailAddress: + setEmailAddress(actorJson, fields['email']) + actorChanged = True + else: + if currentEmailAddress: + setEmailAddress(actorJson, '') + actorChanged = True + + currentXmppAddress = getXmppAddress(actorJson) + if fields.get('xmppAddress'): + if fields['xmppAddress'] != currentXmppAddress: + setXmppAddress(actorJson, + fields['xmppAddress']) + actorChanged = True + else: + if currentXmppAddress: + setXmppAddress(actorJson, '') + actorChanged = True + + currentMatrixAddress = getMatrixAddress(actorJson) + if fields.get('matrixAddress'): + if fields['matrixAddress'] != currentMatrixAddress: + setMatrixAddress(actorJson, + fields['matrixAddress']) + actorChanged = True + else: + if currentMatrixAddress: + setMatrixAddress(actorJson, '') + actorChanged = True + + currentSSBAddress = getSSBAddress(actorJson) + if fields.get('ssbAddress'): + if fields['ssbAddress'] != currentSSBAddress: + setSSBAddress(actorJson, + fields['ssbAddress']) + actorChanged = True + else: + if currentSSBAddress: + setSSBAddress(actorJson, '') + actorChanged = True + + currentBlogAddress = getBlogAddress(actorJson) + if fields.get('blogAddress'): + if fields['blogAddress'] != currentBlogAddress: + setBlogAddress(actorJson, + fields['blogAddress']) + actorChanged = True + else: + if currentBlogAddress: + setBlogAddress(actorJson, '') + actorChanged = True + + currentToxAddress = getToxAddress(actorJson) + if fields.get('toxAddress'): + if fields['toxAddress'] != currentToxAddress: + setToxAddress(actorJson, + fields['toxAddress']) + actorChanged = True + else: + if currentToxAddress: + setToxAddress(actorJson, '') + actorChanged = True + + currentPGPpubKey = getPGPpubKey(actorJson) + if fields.get('pgp'): + if fields['pgp'] != currentPGPpubKey: + setPGPpubKey(actorJson, + fields['pgp']) + actorChanged = True + else: + if currentPGPpubKey: + setPGPpubKey(actorJson, '') + actorChanged = True + + currentPGPfingerprint = getPGPfingerprint(actorJson) + if fields.get('openpgp'): + if fields['openpgp'] != currentPGPfingerprint: + setPGPfingerprint(actorJson, + fields['openpgp']) + actorChanged = True + else: + if currentPGPfingerprint: + setPGPfingerprint(actorJson, '') + actorChanged = True + + currentDonateUrl = getDonationUrl(actorJson) + if fields.get('donateUrl'): + if fields['donateUrl'] != currentDonateUrl: + setDonationUrl(actorJson, + fields['donateUrl']) + actorChanged = True + else: + if currentDonateUrl: + setDonationUrl(actorJson, '') + actorChanged = True + + if fields.get('instanceTitle'): + currInstanceTitle = \ + getConfigParam(baseDir, + 'instanceTitle') + if fields['instanceTitle'] != currInstanceTitle: + setConfigParam(baseDir, + 'instanceTitle', + fields['instanceTitle']) + + if fields.get('ytdomain'): + currYTDomain = self.server.YTReplacementDomain + if fields['ytdomain'] != currYTDomain: + newYTDomain = fields['ytdomain'] + if '://' in newYTDomain: + newYTDomain = newYTDomain.split('://')[1] + if '/' in newYTDomain: + newYTDomain = newYTDomain.split('/')[0] + if '.' in newYTDomain: + setConfigParam(baseDir, + 'youtubedomain', + newYTDomain) + self.server.YTReplacementDomain = \ + newYTDomain + else: + setConfigParam(baseDir, + 'youtubedomain', '') + self.server.YTReplacementDomain = None + + currInstanceDescriptionShort = \ + getConfigParam(baseDir, + 'instanceDescriptionShort') + if fields.get('instanceDescriptionShort'): + if fields['instanceDescriptionShort'] != \ + currInstanceDescriptionShort: + iDesc = fields['instanceDescriptionShort'] + setConfigParam(baseDir, + 'instanceDescriptionShort', + iDesc) + else: + if currInstanceDescriptionShort: + setConfigParam(baseDir, + 'instanceDescriptionShort', '') + currInstanceDescription = \ + getConfigParam(baseDir, + 'instanceDescription') + if fields.get('instanceDescription'): + if fields['instanceDescription'] != \ + currInstanceDescription: + setConfigParam(baseDir, + 'instanceDescription', + fields['instanceDescription']) + else: + if currInstanceDescription: + setConfigParam(baseDir, + 'instanceDescription', '') + if fields.get('bio'): + if fields['bio'] != actorJson['summary']: + actorTags = {} + actorJson['summary'] = \ + addHtmlTags(baseDir, + httpPrefix, + nickname, + domainFull, + fields['bio'], [], actorTags) + if actorTags: + actorJson['tag'] = [] + for tagName, tag in actorTags.items(): + actorJson['tag'].append(tag) + actorChanged = True + else: + if actorJson['summary']: + actorJson['summary'] = '' + actorChanged = True + if fields.get('moderators'): + adminNickname = \ + getConfigParam(baseDir, 'admin') + if path.startswith('/users/' + + adminNickname + '/'): + moderatorsFile = \ + baseDir + \ + '/accounts/moderators.txt' + clearModeratorStatus(baseDir) + if ',' in fields['moderators']: + # if the list was given as comma separated + modFile = open(moderatorsFile, "w+") + mods = fields['moderators'].split(',') + for modNick in mods: + modNick = modNick.strip() + modDir = baseDir + \ + '/accounts/' + modNick + \ + '@' + domain + if os.path.isdir(modDir): + modFile.write(modNick + '\n') + modFile.close() + mods = fields['moderators'].split(',') + for modNick in mods: + modNick = modNick.strip() + modDir = baseDir + \ + '/accounts/' + modNick + \ + '@' + domain + if os.path.isdir(modDir): + setRole(baseDir, + modNick, domain, + 'instance', 'moderator') + else: + # nicknames on separate lines + modFile = open(moderatorsFile, "w+") + mods = fields['moderators'].split('\n') + for modNick in mods: + modNick = modNick.strip() + modDir = \ + baseDir + \ + '/accounts/' + modNick + \ + '@' + domain + if os.path.isdir(modDir): + modFile.write(modNick + '\n') + modFile.close() + mods = fields['moderators'].split('\n') + for modNick in mods: + modNick = modNick.strip() + modDir = \ + baseDir + \ + '/accounts/' + \ + modNick + '@' + \ + domain + if os.path.isdir(modDir): + setRole(baseDir, + modNick, domain, + 'instance', + 'moderator') + + if fields.get('removeScheduledPosts'): + if fields['removeScheduledPosts'] == 'on': + removeScheduledPosts(baseDir, + nickname, domain) + + approveFollowers = False + if fields.get('approveFollowers'): + if fields['approveFollowers'] == 'on': + approveFollowers = True + if approveFollowers != \ + actorJson['manuallyApprovesFollowers']: + actorJson['manuallyApprovesFollowers'] = \ + approveFollowers + actorChanged = True + + if fields.get('removeCustomFont'): + if fields['removeCustomFont'] == 'on': + fontExt = ('woff', 'woff2', 'otf', 'ttf') + for ext in fontExt: + if os.path.isfile(baseDir + + '/fonts/custom.' + ext): + os.remove(baseDir + + '/fonts/custom.' + ext) + if os.path.isfile(baseDir + + '/fonts/custom.' + ext + + '.etag'): + os.remove(baseDir + + '/fonts/custom.' + ext + + '.etag') + currTheme = getTheme(baseDir) + if currTheme: + setTheme(baseDir, currTheme) + + if fields.get('mediaInstance'): + self.server.mediaInstance = False + self.server.defaultTimeline = 'inbox' + if fields['mediaInstance'] == 'on': + self.server.mediaInstance = True + self.server.defaultTimeline = 'tlmedia' + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + else: + if self.server.mediaInstance: + self.server.mediaInstance = False + self.server.defaultTimeline = 'inbox' + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + if fields.get('blogsInstance'): + self.server.blogsInstance = False + self.server.defaultTimeline = 'inbox' + if fields['blogsInstance'] == 'on': + self.server.blogsInstance = True + self.server.defaultTimeline = 'tlblogs' + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + else: + if self.server.blogsInstance: + self.server.blogsInstance = False + self.server.defaultTimeline = 'inbox' + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + # only receive DMs from accounts you follow + followDMsFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/.followDMs' + followDMsActive = False + if fields.get('followDMs'): + if fields['followDMs'] == 'on': + followDMsActive = True + with open(followDMsFilename, 'w+') as fFile: + fFile.write('\n') + if not followDMsActive: + if os.path.isfile(followDMsFilename): + os.remove(followDMsFilename) + # remove Twitter retweets + removeTwitterFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/.removeTwitter' + removeTwitterActive = False + if fields.get('removeTwitter'): + if fields['removeTwitter'] == 'on': + removeTwitterActive = True + with open(removeTwitterFilename, + 'w+') as rFile: + rFile.write('\n') + if not removeTwitterActive: + if os.path.isfile(removeTwitterFilename): + os.remove(removeTwitterFilename) + # hide Like button + hideLikeButtonFile = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/.hideLikeButton' + notifyLikesFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/.notifyLikes' + hideLikeButtonActive = False + if fields.get('hideLikeButton'): + if fields['hideLikeButton'] == 'on': + hideLikeButtonActive = True + with open(hideLikeButtonFile, 'w+') as rFile: + rFile.write('\n') + # remove notify likes selection + if os.path.isfile(notifyLikesFilename): + os.remove(notifyLikesFilename) + if not hideLikeButtonActive: + if os.path.isfile(hideLikeButtonFile): + os.remove(hideLikeButtonFile) + # notify about new Likes + notifyLikesActive = False + if fields.get('notifyLikes'): + if fields['notifyLikes'] == 'on' and \ + not hideLikeButtonActive: + notifyLikesActive = True + with open(notifyLikesFilename, 'w+') as rFile: + rFile.write('\n') + if not notifyLikesActive: + if os.path.isfile(notifyLikesFilename): + os.remove(notifyLikesFilename) + # this account is a bot + if fields.get('isBot'): + if fields['isBot'] == 'on': + if actorJson['type'] != 'Service': + actorJson['type'] = 'Service' + actorChanged = True + else: + # this account is a group + if fields.get('isGroup'): + if fields['isGroup'] == 'on': + if actorJson['type'] != 'Group': + actorJson['type'] = 'Group' + actorChanged = True + else: + # this account is a person (default) + if actorJson['type'] != 'Person': + actorJson['type'] = 'Person' + actorChanged = True + grayscale = False + if fields.get('grayscale'): + if fields['grayscale'] == 'on': + grayscale = True + if grayscale: + enableGrayscale(baseDir) + else: + disableGrayscale(baseDir) + # save filtered words list + filterFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/filters.txt' + if fields.get('filteredWords'): + with open(filterFilename, 'w+') as filterfile: + filterfile.write(fields['filteredWords']) + else: + if os.path.isfile(filterFilename): + os.remove(filterFilename) + # word replacements + switchFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/replacewords.txt' + if fields.get('switchWords'): + with open(switchFilename, 'w+') as switchfile: + switchfile.write(fields['switchWords']) + else: + if os.path.isfile(switchFilename): + os.remove(switchFilename) + # save blocked accounts list + blockedFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/blocking.txt' + if fields.get('blocked'): + with open(blockedFilename, 'w+') as blockedfile: + blockedfile.write(fields['blocked']) + else: + if os.path.isfile(blockedFilename): + os.remove(blockedFilename) + # save allowed instances list + allowedInstancesFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/allowedinstances.txt' + if fields.get('allowedInstances'): + with open(allowedInstancesFilename, 'w+') as aFile: + aFile.write(fields['allowedInstances']) + else: + if os.path.isfile(allowedInstancesFilename): + os.remove(allowedInstancesFilename) + # save git project names list + gitProjectsFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + \ + '/gitprojects.txt' + if fields.get('gitProjects'): + with open(gitProjectsFilename, 'w+') as aFile: + aFile.write(fields['gitProjects'].lower()) + else: + if os.path.isfile(gitProjectsFilename): + os.remove(gitProjectsFilename) + # save actor json file within accounts + if actorChanged: + # update the context for the actor + actorJson['@context'] = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + getDefaultPersonContext() + ] + randomizeActorImages(actorJson) + saveJson(actorJson, actorFilename) + webfingerUpdate(baseDir, + nickname, domain, + onionDomain, + self.server.cachedWebfingers) + # also copy to the actors cache and + # personCache in memory + storePersonInCache(baseDir, + actorJson['id'], actorJson, + self.server.personCache, + True) + # clear any cached images for this actor + idStr = actorJson['id'].replace('/', '-') + removeAvatarFromCache(baseDir, idStr) + # save the actor to the cache + actorCacheFilename = \ + baseDir + '/cache/actors/' + \ + actorJson['id'].replace('/', '#') + '.json' + saveJson(actorJson, actorCacheFilename) + # send profile update to followers + ccStr = 'https://www.w3.org/ns/' + \ + 'activitystreams#Public' + updateActorJson = { + 'type': 'Update', + 'actor': actorJson['id'], + 'to': [actorJson['id'] + '/followers'], + 'cc': [ccStr], + 'object': actorJson + } + self._postToOutbox(updateActorJson, + __version__, nickname) + if fields.get('deactivateThisAccount'): + if fields['deactivateThisAccount'] == 'on': + deactivateAccount(baseDir, + nickname, domain) + self._clearLoginDetails(nickname, + callingDomain) + self.server.POSTbusy = False + return + if callingDomain.endswith('.onion') and \ + onionDomain: + actorStr = \ + 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + i2pDomain): + actorStr = \ + 'http://' + i2pDomain + usersPath + self._redirect_headers(actorStr, cookie, callingDomain) + self.server.POSTbusy = False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6980,721 +7693,15 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2) - # update of profile/avatar from web interface + # update of profile/avatar from web interface, + # after selecting Edit button then Submit if authorized and self.path.endswith('/profiledata'): - usersPath = self.path.replace('/profiledata', '') - usersPath = usersPath.replace('/editprofile', '') - actorStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - if ' boundary=' in self.headers['Content-type']: - boundary = self.headers['Content-type'].split('boundary=')[1] - if ';' in boundary: - boundary = boundary.split(';')[0] - - nickname = getNicknameFromActor(actorStr) - if not nickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: nickname not found in ' + actorStr) - self._redirect_headers(actorStr, cookie, callingDomain) - self.server.POSTbusy = False - return - length = int(self.headers['Content-length']) - if length > self.server.maxPostLength: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('Maximum profile data length exceeded ' + - str(length)) - self._redirect_headers(actorStr, cookie, callingDomain) - self.server.POSTbusy = False - return - - try: - # read the bytes of the http form POST - postBytes = self.rfile.read(length) - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: connection was reset while ' + - 'reading bytes from http form POST') - else: - print('WARN: error while reading bytes ' + - 'from http form POST') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: failed to read bytes for POST') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - - # extract each image type - actorChanged = True - profileMediaTypes = ('avatar', 'image', - 'banner', 'search_banner', - 'instanceLogo') - profileMediaTypesUploaded = {} - for mType in profileMediaTypes: - if self.server.debug: - print('DEBUG: profile update extracting ' + mType + - ' image or font from POST') - mediaBytes, postBytes = \ - extractMediaInFormPOST(postBytes, boundary, mType) - if mediaBytes: - if self.server.debug: - print('DEBUG: profile update ' + mType + - ' image or font was found. ' + - str(len(mediaBytes)) + ' bytes') - else: - if self.server.debug: - print('DEBUG: profile update, no ' + mType + - ' image or font was found in POST') - continue - - # Note: a .temp extension is used here so that at no - # time is an image with metadata publicly exposed, - # even for a few mS - if mType == 'instanceLogo': - filenameBase = \ - self.server.baseDir + '/accounts/login.temp' - else: - filenameBase = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/' + mType + '.temp' - - filename, attachmentMediaType = \ - saveMediaInFormPOST(mediaBytes, self.server.debug, - filenameBase) - if filename: - print('Profile update POST ' + mType + - ' media or font filename is ' + filename) - else: - print('Profile update, no ' + mType + - ' media or font filename in POST') - continue - - postImageFilename = filename.replace('.temp', '') - if self.server.debug: - print('DEBUG: POST ' + mType + - ' media removing metadata') - # remove existing etag - if os.path.isfile(postImageFilename + '.etag'): - try: - os.remove(postImageFilename + '.etag') - except BaseException: - pass - removeMetaData(filename, postImageFilename) - if os.path.isfile(postImageFilename): - print('profile update POST ' + mType + - ' image or font saved to ' + postImageFilename) - if mType != 'instanceLogo': - lastPartOfImageFilename = \ - postImageFilename.split('/')[-1] - profileMediaTypesUploaded[mType] = \ - lastPartOfImageFilename - actorChanged = True - else: - print('ERROR: profile update POST ' + mType + - ' image or font could not be saved to ' + - postImageFilename) - - fields = \ - extractTextFieldsInPOST(postBytes, boundary, - self.server.debug) - if self.server.debug: - if fields: - print('DEBUG: profile update text ' + - 'field extracted from POST ' + str(fields)) - else: - print('WARN: profile update, no text ' + - 'fields could be extracted from POST') - - actorFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + '.json' - if os.path.isfile(actorFilename): - actorJson = loadJson(actorFilename) - if actorJson: - # update the avatar/image url file extension - uploads = profileMediaTypesUploaded.items() - for mType, lastPart in uploads: - repStr = '/' + lastPart - if mType == 'avatar': - lastPartOfUrl = \ - actorJson['icon']['url'].split('/')[-1] - srchStr = '/' + lastPartOfUrl - actorJson['icon']['url'] = \ - actorJson['icon']['url'].replace(srchStr, - repStr) - elif mType == 'image': - lastPartOfUrl = \ - actorJson['image']['url'].split('/')[-1] - srchStr = '/' + lastPartOfUrl - actorJson['image']['url'] = \ - actorJson['image']['url'].replace(srchStr, - repStr) - - skillCtr = 1 - newSkills = {} - while skillCtr < 10: - skillName = \ - fields.get('skillName' + str(skillCtr)) - if not skillName: - skillCtr += 1 - continue - skillValue = \ - fields.get('skillValue' + str(skillCtr)) - if not skillValue: - skillCtr += 1 - continue - if not actorJson['skills'].get(skillName): - actorChanged = True - else: - if actorJson['skills'][skillName] != \ - int(skillValue): - actorChanged = True - newSkills[skillName] = int(skillValue) - skillCtr += 1 - if len(actorJson['skills'].items()) != \ - len(newSkills.items()): - actorChanged = True - actorJson['skills'] = newSkills - if fields.get('password'): - if fields.get('passwordconfirm'): - if actorJson['password'] == \ - fields['passwordconfirm']: - if len(actorJson['password']) > 2: - # set password - baseDir = self.server.baseDir - pwd = actorJson['password'] - storeBasicCredentials(baseDir, - nickname, - pwd) - if fields.get('displayNickname'): - if fields['displayNickname'] != actorJson['name']: - actorJson['name'] = fields['displayNickname'] - actorChanged = True - if fields.get('themeDropdown'): - setTheme(self.server.baseDir, - fields['themeDropdown']) -# self.server.iconsCache={} - - currentEmailAddress = getEmailAddress(actorJson) - if fields.get('email'): - if fields['email'] != currentEmailAddress: - setEmailAddress(actorJson, fields['email']) - actorChanged = True - else: - if currentEmailAddress: - setEmailAddress(actorJson, '') - actorChanged = True - - currentXmppAddress = getXmppAddress(actorJson) - if fields.get('xmppAddress'): - if fields['xmppAddress'] != currentXmppAddress: - setXmppAddress(actorJson, - fields['xmppAddress']) - actorChanged = True - else: - if currentXmppAddress: - setXmppAddress(actorJson, '') - actorChanged = True - - currentMatrixAddress = getMatrixAddress(actorJson) - if fields.get('matrixAddress'): - if fields['matrixAddress'] != currentMatrixAddress: - setMatrixAddress(actorJson, - fields['matrixAddress']) - actorChanged = True - else: - if currentMatrixAddress: - setMatrixAddress(actorJson, '') - actorChanged = True - - currentSSBAddress = getSSBAddress(actorJson) - if fields.get('ssbAddress'): - if fields['ssbAddress'] != currentSSBAddress: - setSSBAddress(actorJson, - fields['ssbAddress']) - actorChanged = True - else: - if currentSSBAddress: - setSSBAddress(actorJson, '') - actorChanged = True - - currentBlogAddress = getBlogAddress(actorJson) - if fields.get('blogAddress'): - if fields['blogAddress'] != currentBlogAddress: - setBlogAddress(actorJson, - fields['blogAddress']) - actorChanged = True - else: - if currentBlogAddress: - setBlogAddress(actorJson, '') - actorChanged = True - - currentToxAddress = getToxAddress(actorJson) - if fields.get('toxAddress'): - if fields['toxAddress'] != currentToxAddress: - setToxAddress(actorJson, - fields['toxAddress']) - actorChanged = True - else: - if currentToxAddress: - setToxAddress(actorJson, '') - actorChanged = True - - currentPGPpubKey = getPGPpubKey(actorJson) - if fields.get('pgp'): - if fields['pgp'] != currentPGPpubKey: - setPGPpubKey(actorJson, - fields['pgp']) - actorChanged = True - else: - if currentPGPpubKey: - setPGPpubKey(actorJson, '') - actorChanged = True - - currentPGPfingerprint = getPGPfingerprint(actorJson) - if fields.get('openpgp'): - if fields['openpgp'] != currentPGPfingerprint: - setPGPfingerprint(actorJson, - fields['openpgp']) - actorChanged = True - else: - if currentPGPfingerprint: - setPGPfingerprint(actorJson, '') - actorChanged = True - - currentDonateUrl = getDonationUrl(actorJson) - if fields.get('donateUrl'): - if fields['donateUrl'] != currentDonateUrl: - setDonationUrl(actorJson, - fields['donateUrl']) - actorChanged = True - else: - if currentDonateUrl: - setDonationUrl(actorJson, '') - actorChanged = True - - if fields.get('instanceTitle'): - currInstanceTitle = \ - getConfigParam(self.server.baseDir, - 'instanceTitle') - if fields['instanceTitle'] != currInstanceTitle: - setConfigParam(self.server.baseDir, - 'instanceTitle', - fields['instanceTitle']) - - if fields.get('ytdomain'): - currYTDomain = self.server.YTReplacementDomain - if fields['ytdomain'] != currYTDomain: - newYTDomain = fields['ytdomain'] - if '://' in newYTDomain: - newYTDomain = newYTDomain.split('://')[1] - if '/' in newYTDomain: - newYTDomain = newYTDomain.split('/')[0] - if '.' in newYTDomain: - setConfigParam(self.server.baseDir, - 'youtubedomain', - newYTDomain) - self.server.YTReplacementDomain = \ - newYTDomain - else: - setConfigParam(self.server.baseDir, - 'youtubedomain', '') - self.server.YTReplacementDomain = None - - currInstanceDescriptionShort = \ - getConfigParam(self.server.baseDir, - 'instanceDescriptionShort') - if fields.get('instanceDescriptionShort'): - if fields['instanceDescriptionShort'] != \ - currInstanceDescriptionShort: - iDesc = fields['instanceDescriptionShort'] - setConfigParam(self.server.baseDir, - 'instanceDescriptionShort', - iDesc) - else: - if currInstanceDescriptionShort: - setConfigParam(self.server.baseDir, - 'instanceDescriptionShort', '') - currInstanceDescription = \ - getConfigParam(self.server.baseDir, - 'instanceDescription') - if fields.get('instanceDescription'): - if fields['instanceDescription'] != \ - currInstanceDescription: - setConfigParam(self.server.baseDir, - 'instanceDescription', - fields['instanceDescription']) - else: - if currInstanceDescription: - setConfigParam(self.server.baseDir, - 'instanceDescription', '') - if fields.get('bio'): - if fields['bio'] != actorJson['summary']: - actorTags = {} - actorJson['summary'] = \ - addHtmlTags(self.server.baseDir, - self.server.httpPrefix, - nickname, - self.server.domainFull, - fields['bio'], [], actorTags) - if actorTags: - actorJson['tag'] = [] - for tagName, tag in actorTags.items(): - actorJson['tag'].append(tag) - actorChanged = True - else: - if actorJson['summary']: - actorJson['summary'] = '' - actorChanged = True - if fields.get('moderators'): - adminNickname = \ - getConfigParam(self.server.baseDir, 'admin') - if self.path.startswith('/users/' + - adminNickname + '/'): - moderatorsFile = \ - self.server.baseDir + \ - '/accounts/moderators.txt' - clearModeratorStatus(self.server.baseDir) - if ',' in fields['moderators']: - # if the list was given as comma separated - modFile = open(moderatorsFile, "w+") - mods = fields['moderators'].split(',') - for modNick in mods: - modNick = modNick.strip() - modDir = self.server.baseDir + \ - '/accounts/' + modNick + \ - '@' + self.server.domain - if os.path.isdir(modDir): - modFile.write(modNick + '\n') - modFile.close() - mods = fields['moderators'].split(',') - for modNick in mods: - modNick = modNick.strip() - modDir = self.server.baseDir + \ - '/accounts/' + modNick + \ - '@' + self.server.domain - if os.path.isdir(modDir): - setRole(self.server.baseDir, - modNick, - self.server.domain, - 'instance', 'moderator') - else: - # nicknames on separate lines - modFile = open(moderatorsFile, "w+") - mods = fields['moderators'].split('\n') - for modNick in mods: - modNick = modNick.strip() - modDir = \ - self.server.baseDir + \ - '/accounts/' + modNick + \ - '@' + self.server.domain - if os.path.isdir(modDir): - modFile.write(modNick + '\n') - modFile.close() - mods = fields['moderators'].split('\n') - for modNick in mods: - modNick = modNick.strip() - modDir = \ - self.server.baseDir + \ - '/accounts/' + \ - modNick + '@' + \ - self.server.domain - if os.path.isdir(modDir): - setRole(self.server.baseDir, - modNick, - self.server.domain, - 'instance', - 'moderator') - - if fields.get('removeScheduledPosts'): - if fields['removeScheduledPosts'] == 'on': - removeScheduledPosts(self.server.baseDir, - nickname, - self.server.domain) - - approveFollowers = False - if fields.get('approveFollowers'): - if fields['approveFollowers'] == 'on': - approveFollowers = True - if approveFollowers != \ - actorJson['manuallyApprovesFollowers']: - actorJson['manuallyApprovesFollowers'] = \ - approveFollowers - actorChanged = True - - if fields.get('removeCustomFont'): - if fields['removeCustomFont'] == 'on': - fontExt = ('woff', 'woff2', 'otf', 'ttf') - for ext in fontExt: - if os.path.isfile(self.server.baseDir + - '/fonts/custom.' + ext): - os.remove(self.server.baseDir + - '/fonts/custom.' + ext) - if os.path.isfile(self.server.baseDir + - '/fonts/custom.' + ext + - '.etag'): - os.remove(self.server.baseDir + - '/fonts/custom.' + ext + - '.etag') - currTheme = getTheme(self.server.baseDir) - if currTheme: - setTheme(self.server.baseDir, currTheme) - - if fields.get('mediaInstance'): - self.server.mediaInstance = False - self.server.defaultTimeline = 'inbox' - if fields['mediaInstance'] == 'on': - self.server.mediaInstance = True - self.server.defaultTimeline = 'tlmedia' - setConfigParam(self.server.baseDir, - "mediaInstance", - self.server.mediaInstance) - else: - if self.server.mediaInstance: - self.server.mediaInstance = False - self.server.defaultTimeline = 'inbox' - setConfigParam(self.server.baseDir, - "mediaInstance", - self.server.mediaInstance) - if fields.get('blogsInstance'): - self.server.blogsInstance = False - self.server.defaultTimeline = 'inbox' - if fields['blogsInstance'] == 'on': - self.server.blogsInstance = True - self.server.defaultTimeline = 'tlblogs' - setConfigParam(self.server.baseDir, - "blogsInstance", - self.server.blogsInstance) - else: - if self.server.blogsInstance: - self.server.blogsInstance = False - self.server.defaultTimeline = 'inbox' - setConfigParam(self.server.baseDir, - "blogsInstance", - self.server.blogsInstance) - # only receive DMs from accounts you follow - followDMsFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/.followDMs' - followDMsActive = False - if fields.get('followDMs'): - if fields['followDMs'] == 'on': - followDMsActive = True - with open(followDMsFilename, 'w+') as fFile: - fFile.write('\n') - if not followDMsActive: - if os.path.isfile(followDMsFilename): - os.remove(followDMsFilename) - # remove Twitter retweets - removeTwitterFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/.removeTwitter' - removeTwitterActive = False - if fields.get('removeTwitter'): - if fields['removeTwitter'] == 'on': - removeTwitterActive = True - with open(removeTwitterFilename, - 'w+') as rFile: - rFile.write('\n') - if not removeTwitterActive: - if os.path.isfile(removeTwitterFilename): - os.remove(removeTwitterFilename) - # hide Like button - hideLikeButtonFile = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/.hideLikeButton' - notifyLikesFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/.notifyLikes' - hideLikeButtonActive = False - if fields.get('hideLikeButton'): - if fields['hideLikeButton'] == 'on': - hideLikeButtonActive = True - with open(hideLikeButtonFile, 'w+') as rFile: - rFile.write('\n') - # remove notify likes selection - if os.path.isfile(notifyLikesFilename): - os.remove(notifyLikesFilename) - if not hideLikeButtonActive: - if os.path.isfile(hideLikeButtonFile): - os.remove(hideLikeButtonFile) - # notify about new Likes - notifyLikesActive = False - if fields.get('notifyLikes'): - if fields['notifyLikes'] == 'on' and \ - not hideLikeButtonActive: - notifyLikesActive = True - with open(notifyLikesFilename, 'w+') as rFile: - rFile.write('\n') - if not notifyLikesActive: - if os.path.isfile(notifyLikesFilename): - os.remove(notifyLikesFilename) - # this account is a bot - if fields.get('isBot'): - if fields['isBot'] == 'on': - if actorJson['type'] != 'Service': - actorJson['type'] = 'Service' - actorChanged = True - else: - # this account is a group - if fields.get('isGroup'): - if fields['isGroup'] == 'on': - if actorJson['type'] != 'Group': - actorJson['type'] = 'Group' - actorChanged = True - else: - # this account is a person (default) - if actorJson['type'] != 'Person': - actorJson['type'] = 'Person' - actorChanged = True - grayscale = False - if fields.get('grayscale'): - if fields['grayscale'] == 'on': - grayscale = True - if grayscale: - enableGrayscale(self.server.baseDir) - else: - disableGrayscale(self.server.baseDir) - # save filtered words list - filterFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/filters.txt' - if fields.get('filteredWords'): - with open(filterFilename, 'w+') as filterfile: - filterfile.write(fields['filteredWords']) - else: - if os.path.isfile(filterFilename): - os.remove(filterFilename) - # word replacements - switchFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/replacewords.txt' - if fields.get('switchWords'): - with open(switchFilename, 'w+') as switchfile: - switchfile.write(fields['switchWords']) - else: - if os.path.isfile(switchFilename): - os.remove(switchFilename) - # save blocked accounts list - blockedFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/blocking.txt' - if fields.get('blocked'): - with open(blockedFilename, 'w+') as blockedfile: - blockedfile.write(fields['blocked']) - else: - if os.path.isfile(blockedFilename): - os.remove(blockedFilename) - # save allowed instances list - allowedInstancesFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/allowedinstances.txt' - if fields.get('allowedInstances'): - with open(allowedInstancesFilename, 'w+') as aFile: - aFile.write(fields['allowedInstances']) - else: - if os.path.isfile(allowedInstancesFilename): - os.remove(allowedInstancesFilename) - # save git project names list - gitProjectsFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + \ - '/gitprojects.txt' - if fields.get('gitProjects'): - with open(gitProjectsFilename, 'w+') as aFile: - aFile.write(fields['gitProjects'].lower()) - else: - if os.path.isfile(gitProjectsFilename): - os.remove(gitProjectsFilename) - # save actor json file within accounts - if actorChanged: - # update the context for the actor - actorJson['@context'] = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - getDefaultPersonContext() - ] - randomizeActorImages(actorJson) - saveJson(actorJson, actorFilename) - webfingerUpdate(self.server.baseDir, - nickname, - self.server.domain, - self.server.onionDomain, - self.server.cachedWebfingers) - # also copy to the actors cache and - # personCache in memory - storePersonInCache(self.server.baseDir, - actorJson['id'], actorJson, - self.server.personCache, - True) - # clear any cached images for this actor - idStr = actorJson['id'].replace('/', '-') - removeAvatarFromCache(self.server.baseDir, idStr) - # save the actor to the cache - actorCacheFilename = \ - self.server.baseDir + '/cache/actors/' + \ - actorJson['id'].replace('/', '#') + '.json' - saveJson(actorJson, actorCacheFilename) - # send profile update to followers - ccStr = 'https://www.w3.org/ns/' + \ - 'activitystreams#Public' - updateActorJson = { - 'type': 'Update', - 'actor': actorJson['id'], - 'to': [actorJson['id'] + '/followers'], - 'cc': [ccStr], - 'object': actorJson - } - self._postToOutbox(updateActorJson, - __version__, nickname) - if fields.get('deactivateThisAccount'): - if fields['deactivateThisAccount'] == 'on': - deactivateAccount(self.server.baseDir, - nickname, - self.server.domain) - self._clearLoginDetails(nickname, - callingDomain) - self.server.POSTbusy = False - return - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actorStr, cookie, callingDomain) - self.server.POSTbusy = False + self._profileUpdate(callingDomain, cookie, authorized, self.path, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 3) From c4413ea3a9573a4575a37d514ea9e6aa165c9d1b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 13:10:33 +0100 Subject: [PATCH 005/108] Comments --- daemon.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index ff263967c..0bba667cc 100644 --- a/daemon.py +++ b/daemon.py @@ -1197,6 +1197,7 @@ class PubServer(BaseHTTPRequestHandler): if ';' in boundary: boundary = boundary.split(';')[0] + # get the nickname nickname = getNicknameFromActor(actorStr) if not nickname: if callingDomain.endswith('.onion') and \ @@ -1211,7 +1212,10 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(actorStr, cookie, callingDomain) self.server.POSTbusy = False return + length = int(self.headers['Content-length']) + + # check that the POST isn't too large if length > self.server.maxPostLength: if callingDomain.endswith('.onion') and \ onionDomain: @@ -1249,7 +1253,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # extract each image type + # get the various avatar, banner and background images actorChanged = True profileMediaTypes = ('avatar', 'image', 'banner', 'search_banner', @@ -1320,6 +1324,7 @@ class PubServer(BaseHTTPRequestHandler): ' image or font could not be saved to ' + postImageFilename) + # extract all of the text fields into a dict fields = \ extractTextFieldsInPOST(postBytes, boundary, debug) @@ -1331,6 +1336,7 @@ class PubServer(BaseHTTPRequestHandler): print('WARN: profile update, no text ' + 'fields could be extracted from POST') + # load the json for the actor for this user actorFilename = \ baseDir + '/accounts/' + \ nickname + '@' + domain + '.json' @@ -1356,6 +1362,7 @@ class PubServer(BaseHTTPRequestHandler): actorJson['image']['url'].replace(srchStr, repStr) + # set skill levels skillCtr = 1 newSkills = {} while skillCtr < 10: @@ -1381,6 +1388,8 @@ class PubServer(BaseHTTPRequestHandler): len(newSkills.items()): actorChanged = True actorJson['skills'] = newSkills + + # change password if fields.get('password'): if fields.get('passwordconfirm'): if actorJson['password'] == \ @@ -1391,6 +1400,8 @@ class PubServer(BaseHTTPRequestHandler): storeBasicCredentials(baseDir, nickname, pwd) + + # change displayed name if fields.get('displayNickname'): if fields['displayNickname'] != actorJson['name']: actorJson['name'] = fields['displayNickname'] @@ -1399,6 +1410,7 @@ class PubServer(BaseHTTPRequestHandler): setTheme(baseDir, fields['themeDropdown']) + # change email address currentEmailAddress = getEmailAddress(actorJson) if fields.get('email'): if fields['email'] != currentEmailAddress: @@ -1409,6 +1421,7 @@ class PubServer(BaseHTTPRequestHandler): setEmailAddress(actorJson, '') actorChanged = True + # change xmpp address currentXmppAddress = getXmppAddress(actorJson) if fields.get('xmppAddress'): if fields['xmppAddress'] != currentXmppAddress: @@ -1420,6 +1433,7 @@ class PubServer(BaseHTTPRequestHandler): setXmppAddress(actorJson, '') actorChanged = True + # change matrix address currentMatrixAddress = getMatrixAddress(actorJson) if fields.get('matrixAddress'): if fields['matrixAddress'] != currentMatrixAddress: @@ -1431,6 +1445,7 @@ class PubServer(BaseHTTPRequestHandler): setMatrixAddress(actorJson, '') actorChanged = True + # change SSB address currentSSBAddress = getSSBAddress(actorJson) if fields.get('ssbAddress'): if fields['ssbAddress'] != currentSSBAddress: @@ -1442,6 +1457,7 @@ class PubServer(BaseHTTPRequestHandler): setSSBAddress(actorJson, '') actorChanged = True + # change blog address currentBlogAddress = getBlogAddress(actorJson) if fields.get('blogAddress'): if fields['blogAddress'] != currentBlogAddress: @@ -1453,6 +1469,7 @@ class PubServer(BaseHTTPRequestHandler): setBlogAddress(actorJson, '') actorChanged = True + # change tox address currentToxAddress = getToxAddress(actorJson) if fields.get('toxAddress'): if fields['toxAddress'] != currentToxAddress: @@ -1464,6 +1481,7 @@ class PubServer(BaseHTTPRequestHandler): setToxAddress(actorJson, '') actorChanged = True + # change PGP public key currentPGPpubKey = getPGPpubKey(actorJson) if fields.get('pgp'): if fields['pgp'] != currentPGPpubKey: @@ -1475,6 +1493,7 @@ class PubServer(BaseHTTPRequestHandler): setPGPpubKey(actorJson, '') actorChanged = True + # change PGP fingerprint currentPGPfingerprint = getPGPfingerprint(actorJson) if fields.get('openpgp'): if fields['openpgp'] != currentPGPfingerprint: @@ -1486,6 +1505,7 @@ class PubServer(BaseHTTPRequestHandler): setPGPfingerprint(actorJson, '') actorChanged = True + # change donation link currentDonateUrl = getDonationUrl(actorJson) if fields.get('donateUrl'): if fields['donateUrl'] != currentDonateUrl: @@ -1497,6 +1517,7 @@ class PubServer(BaseHTTPRequestHandler): setDonationUrl(actorJson, '') actorChanged = True + # change instance title if fields.get('instanceTitle'): currInstanceTitle = \ getConfigParam(baseDir, @@ -1506,6 +1527,7 @@ class PubServer(BaseHTTPRequestHandler): 'instanceTitle', fields['instanceTitle']) + # change YouTube alternate domain if fields.get('ytdomain'): currYTDomain = self.server.YTReplacementDomain if fields['ytdomain'] != currYTDomain: @@ -1525,6 +1547,7 @@ class PubServer(BaseHTTPRequestHandler): 'youtubedomain', '') self.server.YTReplacementDomain = None + # change instance description currInstanceDescriptionShort = \ getConfigParam(baseDir, 'instanceDescriptionShort') @@ -1552,6 +1575,8 @@ class PubServer(BaseHTTPRequestHandler): if currInstanceDescription: setConfigParam(baseDir, 'instanceDescription', '') + + # change user bio if fields.get('bio'): if fields['bio'] != actorJson['summary']: actorTags = {} @@ -1570,6 +1595,8 @@ class PubServer(BaseHTTPRequestHandler): if actorJson['summary']: actorJson['summary'] = '' actorChanged = True + + # change moderators list if fields.get('moderators'): adminNickname = \ getConfigParam(baseDir, 'admin') @@ -1628,11 +1655,13 @@ class PubServer(BaseHTTPRequestHandler): 'instance', 'moderator') + # remove scheduled posts if fields.get('removeScheduledPosts'): if fields['removeScheduledPosts'] == 'on': removeScheduledPosts(baseDir, nickname, domain) + # approve followers approveFollowers = False if fields.get('approveFollowers'): if fields['approveFollowers'] == 'on': @@ -1643,6 +1672,7 @@ class PubServer(BaseHTTPRequestHandler): approveFollowers actorChanged = True + # remove a custom font if fields.get('removeCustomFont'): if fields['removeCustomFont'] == 'on': fontExt = ('woff', 'woff2', 'otf', 'ttf') @@ -1661,6 +1691,7 @@ class PubServer(BaseHTTPRequestHandler): if currTheme: setTheme(baseDir, currTheme) + # change media instance status if fields.get('mediaInstance'): self.server.mediaInstance = False self.server.defaultTimeline = 'inbox' @@ -1677,6 +1708,8 @@ class PubServer(BaseHTTPRequestHandler): setConfigParam(baseDir, "mediaInstance", self.server.mediaInstance) + + # change blog instance status if fields.get('blogsInstance'): self.server.blogsInstance = False self.server.defaultTimeline = 'inbox' @@ -1693,6 +1726,7 @@ class PubServer(BaseHTTPRequestHandler): setConfigParam(baseDir, "blogsInstance", self.server.blogsInstance) + # only receive DMs from accounts you follow followDMsFilename = \ baseDir + '/accounts/' + \ @@ -1707,6 +1741,7 @@ class PubServer(BaseHTTPRequestHandler): if not followDMsActive: if os.path.isfile(followDMsFilename): os.remove(followDMsFilename) + # remove Twitter retweets removeTwitterFilename = \ baseDir + '/accounts/' + \ @@ -1722,6 +1757,7 @@ class PubServer(BaseHTTPRequestHandler): if not removeTwitterActive: if os.path.isfile(removeTwitterFilename): os.remove(removeTwitterFilename) + # hide Like button hideLikeButtonFile = \ baseDir + '/accounts/' + \ @@ -1743,6 +1779,7 @@ class PubServer(BaseHTTPRequestHandler): if not hideLikeButtonActive: if os.path.isfile(hideLikeButtonFile): os.remove(hideLikeButtonFile) + # notify about new Likes notifyLikesActive = False if fields.get('notifyLikes'): @@ -1754,6 +1791,7 @@ class PubServer(BaseHTTPRequestHandler): if not notifyLikesActive: if os.path.isfile(notifyLikesFilename): os.remove(notifyLikesFilename) + # this account is a bot if fields.get('isBot'): if fields['isBot'] == 'on': @@ -1772,6 +1810,8 @@ class PubServer(BaseHTTPRequestHandler): if actorJson['type'] != 'Person': actorJson['type'] = 'Person' actorChanged = True + + # grayscale theme grayscale = False if fields.get('grayscale'): if fields['grayscale'] == 'on': @@ -1780,6 +1820,7 @@ class PubServer(BaseHTTPRequestHandler): enableGrayscale(baseDir) else: disableGrayscale(baseDir) + # save filtered words list filterFilename = \ baseDir + '/accounts/' + \ @@ -1791,6 +1832,7 @@ class PubServer(BaseHTTPRequestHandler): else: if os.path.isfile(filterFilename): os.remove(filterFilename) + # word replacements switchFilename = \ baseDir + '/accounts/' + \ @@ -1802,6 +1844,7 @@ class PubServer(BaseHTTPRequestHandler): else: if os.path.isfile(switchFilename): os.remove(switchFilename) + # save blocked accounts list blockedFilename = \ baseDir + '/accounts/' + \ @@ -1813,6 +1856,7 @@ class PubServer(BaseHTTPRequestHandler): else: if os.path.isfile(blockedFilename): os.remove(blockedFilename) + # save allowed instances list allowedInstancesFilename = \ baseDir + '/accounts/' + \ @@ -1824,6 +1868,7 @@ class PubServer(BaseHTTPRequestHandler): else: if os.path.isfile(allowedInstancesFilename): os.remove(allowedInstancesFilename) + # save git project names list gitProjectsFilename = \ baseDir + '/accounts/' + \ @@ -1835,6 +1880,7 @@ class PubServer(BaseHTTPRequestHandler): else: if os.path.isfile(gitProjectsFilename): os.remove(gitProjectsFilename) + # save actor json file within accounts if actorChanged: # update the context for the actor @@ -1875,6 +1921,8 @@ class PubServer(BaseHTTPRequestHandler): } self._postToOutbox(updateActorJson, __version__, nickname) + + # deactivate the account if fields.get('deactivateThisAccount'): if fields['deactivateThisAccount'] == 'on': deactivateAccount(baseDir, @@ -1883,6 +1931,8 @@ class PubServer(BaseHTTPRequestHandler): callingDomain) self.server.POSTbusy = False return + + # redirect back to the profile screen if callingDomain.endswith('.onion') and \ onionDomain: actorStr = \ From 6b5b0ea16a3dedc1a5fdf270f846d1f3ca398aa6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 14:52:46 +0100 Subject: [PATCH 006/108] Tidying of person options --- daemon.py | 659 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 343 insertions(+), 316 deletions(-) diff --git a/daemon.py b/daemon.py index 0bba667cc..5bbb90d3a 100644 --- a/daemon.py +++ b/daemon.py @@ -1181,6 +1181,342 @@ class PubServer(BaseHTTPRequestHandler): '/users/' + nickname + '/statuses/' + userEnding2[1] return locatePost(baseDir, nickname, domain, messageId), nickname + def _personOptions(self, path: str, callingDomain: str, cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + debug: bool): + """Receive POST from person options screen + """ + pageNumber = 1 + usersPath = path.split('/personoptions')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + + chooserNickname = getNicknameFromActor(originPathStr) + if not chooserNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + originPathStr) + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + return + + length = int(self.headers['Content-length']) + + try: + optionsConfirmParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST optionsConfirmParams ' + + 'connection reset by peer') + else: + print('WARN: POST optionsConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST optionsConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + optionsConfirmParams = \ + urllib.parse.unquote_plus(optionsConfirmParams) + + # page number to return to + if 'pageNumber=' in optionsConfirmParams: + pageNumberStr = optionsConfirmParams.split('pageNumber=')[1] + if '&' in pageNumberStr: + pageNumberStr = pageNumberStr.split('&')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + + # actor for the person + optionsActor = optionsConfirmParams.split('actor=')[1] + if '&' in optionsActor: + optionsActor = optionsActor.split('&')[0] + + # url of the avatar + optionsAvatarUrl = optionsConfirmParams.split('avatarUrl=')[1] + if '&' in optionsAvatarUrl: + optionsAvatarUrl = optionsAvatarUrl.split('&')[0] + + # link to a post, which can then be included in reports + postUrl = None + if 'postUrl' in optionsConfirmParams: + postUrl = optionsConfirmParams.split('postUrl=')[1] + if '&' in postUrl: + postUrl = postUrl.split('&')[0] + + # petname for this person + petname = None + if 'optionpetname' in optionsConfirmParams: + petname = optionsConfirmParams.split('optionpetname=')[1] + if '&' in petname: + petname = petname.split('&')[0] + # Limit the length of the petname + if len(petname) > 20 or \ + ' ' in petname or '/' in petname or \ + '?' in petname or '#' in petname: + petname = None + + # notes about this person + personNotes = None + if 'optionnotes' in optionsConfirmParams: + personNotes = optionsConfirmParams.split('optionnotes=')[1] + if '&' in personNotes: + personNotes = personNotes.split('&')[0] + personNotes = urllib.parse.unquote_plus(personNotes.strip()) + # Limit the length of the notes + if len(personNotes) > 64000: + personNotes = None + + # get the nickname + optionsNickname = getNicknameFromActor(optionsActor) + if not optionsNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + optionsActor) + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + return + + optionsDomain, optionsPort = getDomainFromActor(optionsActor) + optionsDomainFull = optionsDomain + if optionsPort: + if optionsPort != 80 and optionsPort != 443: + if ':' not in optionsDomain: + optionsDomainFull = optionsDomain + ':' + \ + str(optionsPort) + if chooserNickname == optionsNickname and \ + optionsDomain == domain and \ + optionsPort == port: + if debug: + print('You cannot perform an option action on yourself') + + # view button on person option screen + if '&submitView=' in optionsConfirmParams: + if debug: + print('Viewing ' + optionsActor) + self._redirect_headers(optionsActor, + cookie, callingDomain) + self.server.POSTbusy = False + return + + # petname submit button on person option screen + if '&submitPetname=' in optionsConfirmParams and petname: + if debug: + print('Change petname to ' + petname) + handle = optionsNickname + '@' + optionsDomainFull + setPetName(baseDir, + chooserNickname, + domain, + handle, petname) + self._redirect_headers(usersPath + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # person notes submit button on person option screen + if '&submitPersonNotes=' in optionsConfirmParams: + if debug: + print('Change person notes') + handle = optionsNickname + '@' + optionsDomainFull + if not personNotes: + personNotes = '' + setPersonNotes(baseDir, + chooserNickname, + domain, + handle, personNotes) + self._redirect_headers(usersPath + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # person on calendar checkbox on person option screen + if '&submitOnCalendar=' in optionsConfirmParams: + onCalendar = None + if 'onCalendar=' in optionsConfirmParams: + onCalendar = optionsConfirmParams.split('onCalendar=')[1] + if '&' in onCalendar: + onCalendar = onCalendar.split('&')[0] + if onCalendar == 'on': + addPersonToCalendar(baseDir, + chooserNickname, + domain, + optionsNickname, + optionsDomainFull) + else: + removePersonFromCalendar(baseDir, + chooserNickname, + domain, + optionsNickname, + optionsDomainFull) + self._redirect_headers(usersPath + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # block person button on person option screen + if '&submitBlock=' in optionsConfirmParams: + if debug: + print('Adding block by ' + chooserNickname + + ' of ' + optionsActor) + addBlock(baseDir, chooserNickname, + domain, + optionsNickname, optionsDomainFull) + + # unblock button on person option screen + if '&submitUnblock=' in optionsConfirmParams: + if debug: + print('Unblocking ' + optionsActor) + msg = \ + htmlUnblockConfirm(self.server.translate, + baseDir, + usersPath, + optionsActor, + optionsAvatarUrl).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + + # follow button on person option screen + if '&submitFollow=' in optionsConfirmParams: + if debug: + print('Following ' + optionsActor) + msg = \ + htmlFollowConfirm(self.server.translate, + baseDir, + usersPath, + optionsActor, + optionsAvatarUrl).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + + # unfollow button on person option screen + if '&submitUnfollow=' in optionsConfirmParams: + if debug: + print('Unfollowing ' + optionsActor) + msg = \ + htmlUnfollowConfirm(self.server.translate, + baseDir, + usersPath, + optionsActor, + optionsAvatarUrl).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + + # DM button on person option screen + if '&submitDM=' in optionsConfirmParams: + if debug: + print('Sending DM to ' + optionsActor) + reportPath = self.path.replace('/personoptions', '') + '/newdm' + msg = htmlNewPost(False, self.server.translate, + baseDir, + httpPrefix, + reportPath, None, + [optionsActor], None, + pageNumber, + chooserNickname, + domain, + domainFull).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + + # snooze button on person option screen + if '&submitSnooze=' in optionsConfirmParams: + usersPath = self.path.split('/personoptions')[0] + thisActor = httpPrefix + '://' + domainFull + usersPath + if debug: + print('Snoozing ' + optionsActor + ' ' + thisActor) + if '/users/' in thisActor: + nickname = thisActor.split('/users/')[1] + personSnooze(baseDir, nickname, + domain, optionsActor) + if callingDomain.endswith('.onion') and onionDomain: + thisActor = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + thisActor = 'http://' + i2pDomain + usersPath + self._redirect_headers(thisActor + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # unsnooze button on person option screen + if '&submitUnSnooze=' in optionsConfirmParams: + usersPath = path.split('/personoptions')[0] + thisActor = httpPrefix + '://' + domainFull + usersPath + if debug: + print('Unsnoozing ' + optionsActor + ' ' + thisActor) + if '/users/' in thisActor: + nickname = thisActor.split('/users/')[1] + personUnsnooze(baseDir, nickname, + domain, optionsActor) + if callingDomain.endswith('.onion') and onionDomain: + thisActor = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + thisActor = 'http://' + i2pDomain + usersPath + self._redirect_headers(thisActor + '/' + + self.server.defaultTimeline + + '?page=' + str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # report button on person option screen + if '&submitReport=' in optionsConfirmParams: + if debug: + print('Reporting ' + optionsActor) + reportPath = \ + path.replace('/personoptions', '') + '/newreport' + msg = htmlNewPost(False, self.server.translate, + baseDir, + httpPrefix, + reportPath, None, [], + postUrl, pageNumber, + chooserNickname, + domain, + domainFull).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + + # redirect back from person options screen + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif callingDomain.endswith('.i2p') and i2pDomain: + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + return + def _profileUpdate(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -8746,322 +9082,13 @@ class PubServer(BaseHTTPRequestHandler): # an option was chosen from person options screen # view/follow/block/report if authorized and self.path.endswith('/personoptions'): - pageNumber = 1 - usersPath = self.path.split('/personoptions')[0] - originPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - - chooserNickname = getNicknameFromActor(originPathStr) - if not chooserNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + originPathStr) - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False - return - length = int(self.headers['Content-length']) - try: - optionsConfirmParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST optionsConfirmParams ' + - 'connection reset by peer') - else: - print('WARN: POST optionsConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST optionsConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - optionsConfirmParams = \ - urllib.parse.unquote_plus(optionsConfirmParams) - # page number to return to - if 'pageNumber=' in optionsConfirmParams: - pageNumberStr = optionsConfirmParams.split('pageNumber=')[1] - if '&' in pageNumberStr: - pageNumberStr = pageNumberStr.split('&')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - # actor for the person - optionsActor = optionsConfirmParams.split('actor=')[1] - if '&' in optionsActor: - optionsActor = optionsActor.split('&')[0] - # url of the avatar - optionsAvatarUrl = optionsConfirmParams.split('avatarUrl=')[1] - if '&' in optionsAvatarUrl: - optionsAvatarUrl = optionsAvatarUrl.split('&')[0] - # link to a post, which can then be included in reports - postUrl = None - if 'postUrl' in optionsConfirmParams: - postUrl = optionsConfirmParams.split('postUrl=')[1] - if '&' in postUrl: - postUrl = postUrl.split('&')[0] - petname = None - if 'optionpetname' in optionsConfirmParams: - petname = optionsConfirmParams.split('optionpetname=')[1] - if '&' in petname: - petname = petname.split('&')[0] - # Limit the length of the petname - if len(petname) > 20 or \ - ' ' in petname or '/' in petname or \ - '?' in petname or '#' in petname: - petname = None - - personNotes = None - if 'optionnotes' in optionsConfirmParams: - personNotes = optionsConfirmParams.split('optionnotes=')[1] - if '&' in personNotes: - personNotes = personNotes.split('&')[0] - personNotes = urllib.parse.unquote_plus(personNotes.strip()) - # Limit the length of the notes - if len(personNotes) > 64000: - personNotes = None - - optionsNickname = getNicknameFromActor(optionsActor) - if not optionsNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + optionsActor) - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False - return - optionsDomain, optionsPort = getDomainFromActor(optionsActor) - optionsDomainFull = optionsDomain - if optionsPort: - if optionsPort != 80 and optionsPort != 443: - if ':' not in optionsDomain: - optionsDomainFull = optionsDomain + ':' + \ - str(optionsPort) - if chooserNickname == optionsNickname and \ - optionsDomain == self.server.domain and \ - optionsPort == self.server.port: - if self.server.debug: - print('You cannot perform an option action on yourself') - - if '&submitView=' in optionsConfirmParams: - if self.server.debug: - print('Viewing ' + optionsActor) - self._redirect_headers(optionsActor, - cookie, callingDomain) - self.server.POSTbusy = False - return - if '&submitPetname=' in optionsConfirmParams and petname: - if self.server.debug: - print('Change petname to ' + petname) - handle = optionsNickname + '@' + optionsDomainFull - setPetName(self.server.baseDir, - chooserNickname, - self.server.domain, - handle, petname) - self._redirect_headers(usersPath + '/' + - self.server.defaultTimeline + - '?page='+str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False - return - if '&submitPersonNotes=' in optionsConfirmParams: - if self.server.debug: - print('Change person notes') - handle = optionsNickname + '@' + optionsDomainFull - if not personNotes: - personNotes = '' - setPersonNotes(self.server.baseDir, - chooserNickname, - self.server.domain, - handle, personNotes) - self._redirect_headers(usersPath + '/' + - self.server.defaultTimeline + - '?page='+str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False - return - if '&submitOnCalendar=' in optionsConfirmParams: - onCalendar = None - if 'onCalendar=' in optionsConfirmParams: - onCalendar = optionsConfirmParams.split('onCalendar=')[1] - if '&' in onCalendar: - onCalendar = onCalendar.split('&')[0] - if onCalendar == 'on': - addPersonToCalendar(self.server.baseDir, - chooserNickname, - self.server.domain, - optionsNickname, - optionsDomainFull) - else: - removePersonFromCalendar(self.server.baseDir, - chooserNickname, - self.server.domain, - optionsNickname, - optionsDomainFull) - self._redirect_headers(usersPath + '/' + - self.server.defaultTimeline + - '?page='+str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False - return - if '&submitBlock=' in optionsConfirmParams: - if self.server.debug: - print('Adding block by ' + chooserNickname + - ' of ' + optionsActor) - addBlock(self.server.baseDir, chooserNickname, - self.server.domain, - optionsNickname, optionsDomainFull) - if '&submitUnblock=' in optionsConfirmParams: - if self.server.debug: - print('Unblocking ' + optionsActor) - msg = \ - htmlUnblockConfirm(self.server.translate, - self.server.baseDir, - usersPath, - optionsActor, - optionsAvatarUrl).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - if '&submitFollow=' in optionsConfirmParams: - if self.server.debug: - print('Following ' + optionsActor) - msg = \ - htmlFollowConfirm(self.server.translate, - self.server.baseDir, - usersPath, - optionsActor, - optionsAvatarUrl).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - if '&submitUnfollow=' in optionsConfirmParams: - if self.server.debug: - print('Unfollowing ' + optionsActor) - msg = \ - htmlUnfollowConfirm(self.server.translate, - self.server.baseDir, - usersPath, - optionsActor, - optionsAvatarUrl).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - if '&submitDM=' in optionsConfirmParams: - if self.server.debug: - print('Sending DM to ' + optionsActor) - reportPath = self.path.replace('/personoptions', '') + '/newdm' - msg = htmlNewPost(False, self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - reportPath, None, - [optionsActor], None, - pageNumber, - chooserNickname, - self.server.domain, - self.server.domainFull).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - if '&submitSnooze=' in optionsConfirmParams: - usersPath = self.path.split('/personoptions')[0] - thisActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull+usersPath - if self.server.debug: - print('Snoozing ' + optionsActor + ' ' + thisActor) - if '/users/' in thisActor: - nickname = thisActor.split('/users/')[1] - personSnooze(self.server.baseDir, nickname, - self.server.domain, optionsActor) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - thisActor = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - thisActor = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(thisActor + '/' + - self.server.defaultTimeline + - '?page='+str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False - return - if '&submitUnSnooze=' in optionsConfirmParams: - usersPath = self.path.split('/personoptions')[0] - thisActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - if self.server.debug: - print('Unsnoozing ' + optionsActor + ' ' + thisActor) - if '/users/' in thisActor: - nickname = thisActor.split('/users/')[1] - personUnsnooze(self.server.baseDir, nickname, - self.server.domain, optionsActor) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - thisActor = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - thisActor = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(thisActor + '/' + - self.server.defaultTimeline + - '?page=' + str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False - return - if '&submitReport=' in optionsConfirmParams: - if self.server.debug: - print('Reporting ' + optionsActor) - reportPath = \ - self.path.replace('/personoptions', '') + '/newreport' - msg = htmlNewPost(False, self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - reportPath, None, [], - postUrl, pageNumber, - chooserNickname, - self.server.domain, - self.server.domainFull).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - - if callingDomain.endswith('.onion') and self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif callingDomain.endswith('.i2p') and self.server.i2pDomain: - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False + self._personOptions(self, self.path, callingDomain, cookie, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 14) From c0454061747e014b67760844659997b1ff3eaefe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 15:09:47 +0100 Subject: [PATCH 007/108] Tidy moderator actions --- daemon.py | 285 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 146 insertions(+), 139 deletions(-) diff --git a/daemon.py b/daemon.py index 5bbb90d3a..3b71dc01c 100644 --- a/daemon.py +++ b/daemon.py @@ -1181,6 +1181,143 @@ class PubServer(BaseHTTPRequestHandler): '/users/' + nickname + '/statuses/' + userEnding2[1] return locatePost(baseDir, nickname, domain, messageId), nickname + def _moderatorActions(self, path: str, callingDomain: str, cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + debug: bool): + """Actions on the moderator screeen + """ + usersPath = path.replace('/moderationaction', '') + actorStr = httpPrefix + '://' + domainFull + usersPath + + length = int(self.headers['Content-length']) + + try: + moderationParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST moderationParams connection was reset') + else: + print('WARN: POST moderationParams ' + + 'rfile.read socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST moderationParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&' in moderationParams: + moderationText = None + moderationButton = None + for moderationStr in moderationParams.split('&'): + if moderationStr.startswith('moderationAction'): + if '=' in moderationStr: + moderationText = \ + moderationStr.split('=')[1].strip() + modText = moderationText.replace('+', ' ') + moderationText = \ + urllib.parse.unquote_plus(modText.strip()) + elif moderationStr.startswith('submitInfo'): + msg = htmlModerationInfo(self.server.translate, + baseDir, httpPrefix) + msg = msg.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + elif moderationStr.startswith('submitBlock'): + moderationButton = 'block' + elif moderationStr.startswith('submitUnblock'): + moderationButton = 'unblock' + elif moderationStr.startswith('submitSuspend'): + moderationButton = 'suspend' + elif moderationStr.startswith('submitUnsuspend'): + moderationButton = 'unsuspend' + elif moderationStr.startswith('submitRemove'): + moderationButton = 'remove' + if moderationButton and moderationText: + if debug: + print('moderationButton: ' + moderationButton) + print('moderationText: ' + moderationText) + nickname = moderationText + if nickname.startswith('http') or \ + nickname.startswith('dat'): + nickname = getNicknameFromActor(nickname) + if '@' in nickname: + nickname = nickname.split('@')[0] + if moderationButton == 'suspend': + suspendAccount(baseDir, nickname, domain) + if moderationButton == 'unsuspend': + unsuspendAccount(baseDir, nickname) + if moderationButton == 'block': + fullBlockDomain = None + if moderationText.startswith('http') or \ + moderationText.startswith('dat'): + blockDomain, blockPort = \ + getDomainFromActor(moderationText) + fullBlockDomain = blockDomain + if blockPort: + if blockPort != 80 and blockPort != 443: + if ':' not in blockDomain: + fullBlockDomain = \ + blockDomain + ':' + str(blockPort) + if '@' in moderationText: + fullBlockDomain = moderationText.split('@')[1] + if fullBlockDomain or nickname.startswith('#'): + addGlobalBlock(baseDir, nickname, fullBlockDomain) + if moderationButton == 'unblock': + fullBlockDomain = None + if moderationText.startswith('http') or \ + moderationText.startswith('dat'): + blockDomain, blockPort = \ + getDomainFromActor(moderationText) + fullBlockDomain = blockDomain + if blockPort: + if blockPort != 80 and blockPort != 443: + if ':' not in blockDomain: + fullBlockDomain = \ + blockDomain + ':' + str(blockPort) + if '@' in moderationText: + fullBlockDomain = moderationText.split('@')[1] + if fullBlockDomain or nickname.startswith('#'): + removeGlobalBlock(baseDir, nickname, fullBlockDomain) + if moderationButton == 'remove': + if '/statuses/' not in moderationText: + removeAccount(baseDir, nickname, domain, port) + else: + # remove a post or thread + postFilename = \ + locatePost(baseDir, nickname, domain, + moderationText) + if postFilename: + if canRemovePost(baseDir, + nickname, + domain, + port, + moderationText): + deletePost(baseDir, + httpPrefix, + nickname, domain, + postFilename, + debug, + self.server.recentPostsCache) + if callingDomain.endswith('.onion') and onionDomain: + actorStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(actorStr + '/moderation', + cookie, callingDomain) + self.server.POSTbusy = False + return + def _personOptions(self, path: str, callingDomain: str, cookie: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, @@ -8095,145 +8232,15 @@ class PubServer(BaseHTTPRequestHandler): # moderator action buttons if authorized and '/users/' in self.path and \ self.path.endswith('/moderationaction'): - usersPath = self.path.replace('/moderationaction', '') - actorStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - length = int(self.headers['Content-length']) - try: - moderationParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST moderationParams connection was reset') - else: - print('WARN: POST moderationParams ' + - 'rfile.read socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST moderationParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&' in moderationParams: - moderationText = None - moderationButton = None - for moderationStr in moderationParams.split('&'): - if moderationStr.startswith('moderationAction'): - if '=' in moderationStr: - moderationText = \ - moderationStr.split('=')[1].strip() - modText = moderationText.replace('+', ' ') - moderationText = \ - urllib.parse.unquote_plus(modText.strip()) - elif moderationStr.startswith('submitInfo'): - msg = htmlModerationInfo(self.server.translate, - self.server.baseDir, - self.server.httpPrefix) - msg = msg.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - elif moderationStr.startswith('submitBlock'): - moderationButton = 'block' - elif moderationStr.startswith('submitUnblock'): - moderationButton = 'unblock' - elif moderationStr.startswith('submitSuspend'): - moderationButton = 'suspend' - elif moderationStr.startswith('submitUnsuspend'): - moderationButton = 'unsuspend' - elif moderationStr.startswith('submitRemove'): - moderationButton = 'remove' - if moderationButton and moderationText: - if self.server.debug: - print('moderationButton: ' + moderationButton) - print('moderationText: ' + moderationText) - nickname = moderationText - if nickname.startswith('http') or \ - nickname.startswith('dat'): - nickname = getNicknameFromActor(nickname) - if '@' in nickname: - nickname = nickname.split('@')[0] - if moderationButton == 'suspend': - suspendAccount(self.server.baseDir, nickname, - self.server.domain) - if moderationButton == 'unsuspend': - unsuspendAccount(self.server.baseDir, nickname) - if moderationButton == 'block': - fullBlockDomain = None - if moderationText.startswith('http') or \ - moderationText.startswith('dat'): - blockDomain, blockPort = \ - getDomainFromActor(moderationText) - fullBlockDomain = blockDomain - if blockPort: - if blockPort != 80 and blockPort != 443: - if ':' not in blockDomain: - fullBlockDomain = \ - blockDomain + ':' + str(blockPort) - if '@' in moderationText: - fullBlockDomain = moderationText.split('@')[1] - if fullBlockDomain or nickname.startswith('#'): - addGlobalBlock(self.server.baseDir, - nickname, fullBlockDomain) - if moderationButton == 'unblock': - fullBlockDomain = None - if moderationText.startswith('http') or \ - moderationText.startswith('dat'): - blockDomain, blockPort = \ - getDomainFromActor(moderationText) - fullBlockDomain = blockDomain - if blockPort: - if blockPort != 80 and blockPort != 443: - if ':' not in blockDomain: - fullBlockDomain = \ - blockDomain + ':' + str(blockPort) - if '@' in moderationText: - fullBlockDomain = moderationText.split('@')[1] - if fullBlockDomain or nickname.startswith('#'): - removeGlobalBlock(self.server.baseDir, - nickname, fullBlockDomain) - if moderationButton == 'remove': - if '/statuses/' not in moderationText: - removeAccount(self.server.baseDir, - nickname, - self.server.domain, - self.server.port) - else: - # remove a post or thread - postFilename = \ - locatePost(self.server.baseDir, - nickname, self.server.domain, - moderationText) - if postFilename: - if canRemovePost(self.server.baseDir, - nickname, - self.server.domain, - self.server.port, - moderationText): - deletePost(self.server.baseDir, - self.server.httpPrefix, - nickname, self.server.domain, - postFilename, - self.server.debug, - self.server.recentPostsCache) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actorStr + '/moderation', - cookie, callingDomain) - self.server.POSTbusy = False + self._moderatorActions(self.path, callingDomain, cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 4) From 3bcb634863a8fa49fbd0ccf90ebfa4d7d134bd99 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 15:40:55 +0100 Subject: [PATCH 008/108] Tidy login screen --- daemon.py | 318 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 164 insertions(+), 154 deletions(-) diff --git a/daemon.py b/daemon.py index 3b71dc01c..6b0ddc1a2 100644 --- a/daemon.py +++ b/daemon.py @@ -1181,6 +1181,163 @@ class PubServer(BaseHTTPRequestHandler): '/users/' + nickname + '/statuses/' + userEnding2[1] return locatePost(baseDir, nickname, domain, messageId), nickname + def _loginScreen(self, path: str, callingDomain: str, cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + debug: bool): + """Shows the login screen + """ + # get the contents of POST containing login credentials + length = int(self.headers['Content-length']) + if length > 512: + print('Login failed - credentials too long') + self.send_response(401) + self.end_headers() + self.server.POSTbusy = False + return + + try: + loginParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST login read ' + + 'connection reset by peer') + else: + print('WARN: POST login read socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST login read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + loginNickname, loginPassword, register = \ + htmlGetLoginCredentials(loginParams, self.server.lastLoginTime) + if loginNickname: + self.server.lastLoginTime = int(time.time()) + if register: + if not registerAccount(baseDir, + httpPrefix, + domain, + port, + loginNickname, + loginPassword, + self.server.manualFollowerApproval): + self.server.POSTbusy = False + if callingDomain.endswith('.onion') and onionDomain: + self._redirect_headers('http://' + + onionDomain + + '/login', + cookie, callingDomain) + elif (callingDomain.endswith('.i2p') and i2pDomain): + self._redirect_headers('http://' + + i2pDomain + + '/login', + cookie, callingDomain) + else: + self._redirect_headers(httpPrefix + + '://' + + domainFull + + '/login', + cookie, callingDomain) + return + authHeader = createBasicAuthHeader(loginNickname, + loginPassword) + if not authorizeBasic(baseDir, '/users/' + + loginNickname + '/outbox', + authHeader, False): + print('Login failed: ' + loginNickname) + self._clearLoginDetails(loginNickname, callingDomain) + self.server.POSTbusy = False + return + else: + if isSuspended(baseDir, loginNickname): + msg = \ + htmlSuspended(baseDir).encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + # login success - redirect with authorization + print('Login success: ' + loginNickname) + # re-activate account if needed + activateAccount(baseDir, loginNickname, domain) + # This produces a deterministic token based + # on nick+password+salt + saltFilename = \ + baseDir+'/accounts/' + \ + loginNickname + '@' + domain + '/.salt' + salt = createPassword(32) + if os.path.isfile(saltFilename): + try: + with open(saltFilename, 'r') as fp: + salt = fp.read() + except Exception as e: + print('WARN: Unable to read salt for ' + + loginNickname + ' ' + str(e)) + else: + try: + with open(saltFilename, 'w+') as fp: + fp.write(salt) + except Exception as e: + print('WARN: Unable to save salt for ' + + loginNickname + ' ' + str(e)) + + tokenText = loginNickname + loginPassword + salt + token = sha256(tokenText.encode('utf-8')).hexdigest() + self.server.tokens[loginNickname] = token + loginHandle = loginNickname + '@' + domain + tokenFilename = \ + baseDir+'/accounts/' + \ + loginHandle + '/.token' + try: + with open(tokenFilename, 'w+') as fp: + fp.write(token) + except Exception as e: + print('WARN: Unable to save token for ' + + loginNickname + ' ' + str(e)) + + personUpgradeActor(baseDir, None, loginHandle, + baseDir + '/accounts/' + + loginHandle + '.json') + + index = self.server.tokens[loginNickname] + self.server.tokensLookup[index] = loginNickname + cookieStr = 'SET:epicyon=' + \ + self.server.tokens[loginNickname] + '; SameSite=Strict' + if callingDomain.endswith('.onion') and onionDomain: + self._redirect_headers('http://' + + onionDomain + + '/users/' + + loginNickname + '/' + + self.server.defaultTimeline, + cookieStr, callingDomain) + elif (callingDomain.endswith('.i2p') and i2pDomain): + self._redirect_headers('http://' + + i2pDomain + + '/users/' + + loginNickname + '/' + + self.server.defaultTimeline, + cookieStr, callingDomain) + else: + self._redirect_headers(httpPrefix + '://' + + domainFull + + '/users/' + + loginNickname + '/' + + self.server.defaultTimeline, + cookieStr, callingDomain) + self.server.POSTbusy = False + return + self._200() + self.server.POSTbusy = False + def _moderatorActions(self, path: str, callingDomain: str, cookie: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, @@ -8057,161 +8214,14 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 1) + # login screen if self.path.startswith('/login'): - # get the contents of POST containing login credentials - length = int(self.headers['Content-length']) - if length > 512: - print('Login failed - credentials too long') - self.send_response(401) - self.end_headers() - self.server.POSTbusy = False - return - - try: - loginParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST login read ' + - 'connection reset by peer') - else: - print('WARN: POST login read socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST login read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - - loginNickname, loginPassword, register = \ - htmlGetLoginCredentials(loginParams, self.server.lastLoginTime) - if loginNickname: - self.server.lastLoginTime = int(time.time()) - if register: - if not registerAccount(self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.port, - loginNickname, - loginPassword, - self.server.manualFollowerApproval): - self.server.POSTbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - self._redirect_headers('http://' + - self.server.onionDomain + - '/login', - cookie, callingDomain) - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - self._redirect_headers('http://' + - self.server.i2pDomain + - '/login', - cookie, callingDomain) - else: - self._redirect_headers(self.server.httpPrefix + - '://' + - self.server.domainFull + - '/login', - cookie, callingDomain) - return - authHeader = createBasicAuthHeader(loginNickname, - loginPassword) - if not authorizeBasic(self.server.baseDir, '/users/' + - loginNickname + '/outbox', - authHeader, False): - print('Login failed: ' + loginNickname) - self._clearLoginDetails(loginNickname, callingDomain) - self.server.POSTbusy = False - return - else: - if isSuspended(self.server.baseDir, loginNickname): - msg = \ - htmlSuspended(self.server.baseDir).encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - # login success - redirect with authorization - print('Login success: ' + loginNickname) - # re-activate account if needed - activateAccount(self.server.baseDir, loginNickname, - self.server.domain) - # This produces a deterministic token based - # on nick+password+salt - saltFilename = \ - self.server.baseDir+'/accounts/' + \ - loginNickname + '@' + self.server.domain + '/.salt' - salt = createPassword(32) - if os.path.isfile(saltFilename): - try: - with open(saltFilename, 'r') as fp: - salt = fp.read() - except Exception as e: - print('WARN: Unable to read salt for ' + - loginNickname + ' ' + str(e)) - else: - try: - with open(saltFilename, 'w+') as fp: - fp.write(salt) - except Exception as e: - print('WARN: Unable to save salt for ' + - loginNickname + ' ' + str(e)) - - tokenText = loginNickname + loginPassword + salt - token = sha256(tokenText.encode('utf-8')).hexdigest() - self.server.tokens[loginNickname] = token - loginHandle = loginNickname + '@' + self.server.domain - tokenFilename = \ - self.server.baseDir+'/accounts/' + \ - loginHandle + '/.token' - try: - with open(tokenFilename, 'w+') as fp: - fp.write(token) - except Exception as e: - print('WARN: Unable to save token for ' + - loginNickname + ' ' + str(e)) - - personUpgradeActor(self.server.baseDir, None, loginHandle, - self.server.baseDir + '/accounts/' + - loginHandle + '.json') - - index = self.server.tokens[loginNickname] - self.server.tokensLookup[index] = loginNickname - cookieStr = 'SET:epicyon=' + \ - self.server.tokens[loginNickname] + '; SameSite=Strict' - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - self._redirect_headers('http://' + - self.server.onionDomain + - '/users/' + - loginNickname + '/' + - self.server.defaultTimeline, - cookieStr, callingDomain) - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - self._redirect_headers('http://' + - self.server.i2pDomain + - '/users/' + - loginNickname + '/' + - self.server.defaultTimeline, - cookieStr, callingDomain) - else: - self._redirect_headers(self.server.httpPrefix+'://' + - self.server.domainFull + - '/users/' + - loginNickname + '/' + - self.server.defaultTimeline, - cookieStr, callingDomain) - self.server.POSTbusy = False - return - self._200() - self.server.POSTbusy = False + self._loginScreen(self.path, callingDomain, cookie, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, self.server.domainFull, + self.server.port, + self.server.onionDomain, self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2) From 149acb0c4d262d5a0c2333c86eb3cf2962194a28 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 15:44:50 +0100 Subject: [PATCH 009/108] Tidying --- daemon.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/daemon.py b/daemon.py index 6b0ddc1a2..41df01f5d 100644 --- a/daemon.py +++ b/daemon.py @@ -1222,33 +1222,25 @@ class PubServer(BaseHTTPRequestHandler): if loginNickname: self.server.lastLoginTime = int(time.time()) if register: - if not registerAccount(baseDir, - httpPrefix, - domain, - port, - loginNickname, - loginPassword, + if not registerAccount(baseDir, httpPrefix, domain, port, + loginNickname, loginPassword, self.server.manualFollowerApproval): self.server.POSTbusy = False if callingDomain.endswith('.onion') and onionDomain: - self._redirect_headers('http://' + - onionDomain + - '/login', - cookie, callingDomain) + self._redirect_headers('http://' + onionDomain + + '/login', cookie, + callingDomain) elif (callingDomain.endswith('.i2p') and i2pDomain): - self._redirect_headers('http://' + - i2pDomain + - '/login', - cookie, callingDomain) + self._redirect_headers('http://' + i2pDomain + + '/login', cookie, + callingDomain) else: - self._redirect_headers(httpPrefix + - '://' + - domainFull + - '/login', + self._redirect_headers(httpPrefix + '://' + + domainFull + '/login', cookie, callingDomain) return - authHeader = createBasicAuthHeader(loginNickname, - loginPassword) + authHeader = \ + createBasicAuthHeader(loginNickname, loginPassword) if not authorizeBasic(baseDir, '/users/' + loginNickname + '/outbox', authHeader, False): @@ -1328,8 +1320,7 @@ class PubServer(BaseHTTPRequestHandler): cookieStr, callingDomain) else: self._redirect_headers(httpPrefix + '://' + - domainFull + - '/users/' + + domainFull + '/users/' + loginNickname + '/' + self.server.defaultTimeline, cookieStr, callingDomain) From f35ec2997bb89c301bbb1cb4ba8f175d9af8d5ea Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 17:00:33 +0100 Subject: [PATCH 010/108] Tidy follow/unfollow endpoints --- daemon.py | 330 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 181 insertions(+), 149 deletions(-) diff --git a/daemon.py b/daemon.py index 41df01f5d..cd564b0b1 100644 --- a/daemon.py +++ b/daemon.py @@ -1802,6 +1802,167 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return + def _unfollowConfirm(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, debug: bool): + """Confirm to unfollow + """ + usersPath = path.split('/unfollowconfirm')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + followerNickname = getNicknameFromActor(originPathStr) + + length = int(self.headers['Content-length']) + + try: + followConfirmParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST followConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST followConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST followConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitYes=' in followConfirmParams: + followingActor = \ + urllib.parse.unquote_plus(followConfirmParams) + followingActor = followingActor.split('actor=')[1] + if '&' in followingActor: + followingActor = followingActor.split('&')[0] + followingNickname = getNicknameFromActor(followingActor) + followingDomain, followingPort = \ + getDomainFromActor(followingActor) + if followerNickname == followingNickname and \ + followingDomain == domain and \ + followingPort == port: + if debug: + print('You cannot unfollow yourself!') + else: + if debug: + print(followerNickname + ' stops following ' + + followingActor) + followActor = \ + httpPrefix + '://' + domainFull + \ + '/users/' + followerNickname + statusNumber, published = getStatusNumber() + followId = followActor + '/statuses/' + str(statusNumber) + unfollowJson = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': followId + '/undo', + 'type': 'Undo', + 'actor': followActor, + 'object': { + 'id': followId, + 'type': 'Follow', + 'actor': followActor, + 'object': followingActor + } + } + pathUsersSection = path.split('/users/')[1] + self.postToNickname = pathUsersSection.split('/')[0] + self._postToOutboxThread(unfollowJson) + + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + + def _followConfirm(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, debug: bool): + """Confirm to follow + """ + usersPath = path.split('/followconfirm')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + followerNickname = getNicknameFromActor(originPathStr) + + length = int(self.headers['Content-length']) + + try: + followConfirmParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST followConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST followConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST followConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitView=' in followConfirmParams: + followingActor = \ + urllib.parse.unquote_plus(followConfirmParams) + followingActor = followingActor.split('actor=')[1] + if '&' in followingActor: + followingActor = followingActor.split('&')[0] + self._redirect_headers(followingActor, cookie, callingDomain) + self.server.POSTbusy = False + return + + if '&submitYes=' in followConfirmParams: + followingActor = \ + urllib.parse.unquote_plus(followConfirmParams) + followingActor = followingActor.split('actor=')[1] + if '&' in followingActor: + followingActor = followingActor.split('&')[0] + followingNickname = getNicknameFromActor(followingActor) + followingDomain, followingPort = \ + getDomainFromActor(followingActor) + if followerNickname == followingNickname and \ + followingDomain == domain and \ + followingPort == port: + if debug: + print('You cannot follow yourself!') + else: + if debug: + print('Sending follow request from ' + + followerNickname + ' to ' + followingActor) + sendFollowRequest(self.server.session, + baseDir, followerNickname, + domain, port, + httpPrefix, + followingNickname, + followingDomain, + followingPort, httpPrefix, + False, self.server.federationList, + self.server.sendThreads, + self.server.postLog, + self.server.cachedWebfingers, + self.server.personCache, + debug, + self.server.projectVersion) + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + def _profileUpdate(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -8734,161 +8895,32 @@ class PubServer(BaseHTTPRequestHandler): # decision to follow in the web interface is confirmed if authorized and self.path.endswith('/followconfirm'): - usersPath = self.path.split('/followconfirm')[0] - originPathStr = self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - followerNickname = getNicknameFromActor(originPathStr) - length = int(self.headers['Content-length']) - try: - followConfirmParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST followConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST followConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST followConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitView=' in followConfirmParams: - followingActor = \ - urllib.parse.unquote_plus(followConfirmParams) - followingActor = followingActor.split('actor=')[1] - if '&' in followingActor: - followingActor = followingActor.split('&')[0] - self._redirect_headers(followingActor, cookie, callingDomain) - self.server.POSTbusy = False - return - if '&submitYes=' in followConfirmParams: - followingActor = \ - urllib.parse.unquote_plus(followConfirmParams) - followingActor = followingActor.split('actor=')[1] - if '&' in followingActor: - followingActor = followingActor.split('&')[0] - followingNickname = getNicknameFromActor(followingActor) - followingDomain, followingPort = \ - getDomainFromActor(followingActor) - if followerNickname == followingNickname and \ - followingDomain == self.server.domain and \ - followingPort == self.server.port: - if self.server.debug: - print('You cannot follow yourself!') - else: - if self.server.debug: - print('Sending follow request from ' + - followerNickname + ' to ' + followingActor) - sendFollowRequest(self.server.session, - self.server.baseDir, - followerNickname, - self.server.domain, self.server.port, - self.server.httpPrefix, - followingNickname, - followingDomain, - followingPort, self.server.httpPrefix, - False, self.server.federationList, - self.server.sendThreads, - self.server.postLog, - self.server.cachedWebfingers, - self.server.personCache, - self.server.debug, - self.server.projectVersion) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False + self._followConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 10) # decision to unfollow in the web interface is confirmed if authorized and self.path.endswith('/unfollowconfirm'): - usersPath = self.path.split('/unfollowconfirm')[0] - originPathStr = self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - followerNickname = getNicknameFromActor(originPathStr) - length = int(self.headers['Content-length']) - try: - followConfirmParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST followConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST followConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST followConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitYes=' in followConfirmParams: - followingActor = \ - urllib.parse.unquote_plus(followConfirmParams) - followingActor = followingActor.split('actor=')[1] - if '&' in followingActor: - followingActor = followingActor.split('&')[0] - followingNickname = getNicknameFromActor(followingActor) - followingDomain, followingPort = \ - getDomainFromActor(followingActor) - if followerNickname == followingNickname and \ - followingDomain == self.server.domain and \ - followingPort == self.server.port: - if self.server.debug: - print('You cannot unfollow yourself!') - else: - if self.server.debug: - print(followerNickname + ' stops following ' + - followingActor) - followActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + \ - '/users/' + followerNickname - statusNumber, published = getStatusNumber() - followId = followActor + '/statuses/' + str(statusNumber) - unfollowJson = { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': followId + '/undo', - 'type': 'Undo', - 'actor': followActor, - 'object': { - 'id': followId, - 'type': 'Follow', - 'actor': followActor, - 'object': followingActor - } - } - pathUsersSection = self.path.split('/users/')[1] - self.postToNickname = pathUsersSection.split('/')[0] - self._postToOutboxThread(unfollowJson) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False + self._unfollowConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 11) From 4e454a0d389197cd280c446ed59f0d0238f6d7a0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 17:21:35 +0100 Subject: [PATCH 011/108] Tidy block/unblock endpoints --- daemon.py | 374 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 192 insertions(+), 182 deletions(-) diff --git a/daemon.py b/daemon.py index cd564b0b1..1a5a402d8 100644 --- a/daemon.py +++ b/daemon.py @@ -1963,6 +1963,182 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(originPathStr, cookie, callingDomain) self.server.POSTbusy = False + def _blockConfirm(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, debug: bool): + """Confirms a block + """ + usersPath = path.split('/blockconfirm')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + blockerNickname = getNicknameFromActor(originPathStr) + if not blockerNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + originPathStr) + self._redirect_headers(originPathStr, + cookie, callingDomain) + self.server.POSTbusy = False + return + + length = int(self.headers['Content-length']) + + try: + blockConfirmParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST blockConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST blockConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST blockConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitYes=' in blockConfirmParams: + blockingActor = \ + urllib.parse.unquote_plus(blockConfirmParams) + blockingActor = blockingActor.split('actor=')[1] + if '&' in blockingActor: + blockingActor = blockingActor.split('&')[0] + blockingNickname = getNicknameFromActor(blockingActor) + if not blockingNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + blockingActor) + self._redirect_headers(originPathStr, + cookie, callingDomain) + self.server.POSTbusy = False + return + blockingDomain, blockingPort = \ + getDomainFromActor(blockingActor) + blockingDomainFull = blockingDomain + if blockingPort: + if blockingPort != 80 and blockingPort != 443: + if ':' not in blockingDomain: + blockingDomainFull = \ + blockingDomain + ':' + str(blockingPort) + if blockerNickname == blockingNickname and \ + blockingDomain == domain and \ + blockingPort == port: + if debug: + print('You cannot block yourself!') + else: + if debug: + print('Adding block by ' + blockerNickname + + ' of ' + blockingActor) + addBlock(baseDir, blockerNickname, + domain, + blockingNickname, + blockingDomainFull) + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr, cookie, callingDomain) + self.server.POSTbusy = False + + def _unblockConfirm(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, debug: bool): + """Confirms a unblock + """ + usersPath = path.split('/unblockconfirm')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + blockerNickname = getNicknameFromActor(originPathStr) + if not blockerNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + originPathStr) + self._redirect_headers(originPathStr, + cookie, callingDomain) + self.server.POSTbusy = False + return + + length = int(self.headers['Content-length']) + + try: + blockConfirmParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST blockConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST blockConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST blockConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitYes=' in blockConfirmParams: + blockingActor = \ + urllib.parse.unquote_plus(blockConfirmParams) + blockingActor = blockingActor.split('actor=')[1] + if '&' in blockingActor: + blockingActor = blockingActor.split('&')[0] + blockingNickname = getNicknameFromActor(blockingActor) + if not blockingNickname: + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + print('WARN: unable to find nickname in ' + blockingActor) + self._redirect_headers(originPathStr, + cookie, callingDomain) + self.server.POSTbusy = False + return + blockingDomain, blockingPort = \ + getDomainFromActor(blockingActor) + blockingDomainFull = blockingDomain + if blockingPort: + if blockingPort != 80 and blockingPort != 443: + if ':' not in blockingDomain: + blockingDomainFull = \ + blockingDomain + ':' + str(blockingPort) + if blockerNickname == blockingNickname and \ + blockingDomain == domain and \ + blockingPort == port: + if debug: + print('You cannot unblock yourself!') + else: + if debug: + print(blockerNickname + ' stops blocking ' + + blockingActor) + removeBlock(baseDir, + blockerNickname, domain, + blockingNickname, blockingDomainFull) + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr, + cookie, callingDomain) + self.server.POSTbusy = False + def _profileUpdate(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -8927,194 +9103,28 @@ class PubServer(BaseHTTPRequestHandler): # decision to unblock in the web interface is confirmed if authorized and self.path.endswith('/unblockconfirm'): - usersPath = self.path.split('/unblockconfirm')[0] - originPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - blockerNickname = getNicknameFromActor(originPathStr) - if not blockerNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + originPathStr) - self._redirect_headers(originPathStr, - cookie, callingDomain) - self.server.POSTbusy = False - return - length = int(self.headers['Content-length']) - try: - blockConfirmParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST blockConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST blockConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST blockConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitYes=' in blockConfirmParams: - blockingActor = \ - urllib.parse.unquote_plus(blockConfirmParams) - blockingActor = blockingActor.split('actor=')[1] - if '&' in blockingActor: - blockingActor = blockingActor.split('&')[0] - blockingNickname = getNicknameFromActor(blockingActor) - if not blockingNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + blockingActor) - self._redirect_headers(originPathStr, - cookie, callingDomain) - self.server.POSTbusy = False - return - blockingDomain, blockingPort = \ - getDomainFromActor(blockingActor) - blockingDomainFull = blockingDomain - if blockingPort: - if blockingPort != 80 and blockingPort != 443: - if ':' not in blockingDomain: - blockingDomainFull = \ - blockingDomain + ':' + str(blockingPort) - if blockerNickname == blockingNickname and \ - blockingDomain == self.server.domain and \ - blockingPort == self.server.port: - if self.server.debug: - print('You cannot unblock yourself!') - else: - if self.server.debug: - print(blockerNickname + ' stops blocking ' + - blockingActor) - removeBlock(self.server.baseDir, - blockerNickname, self.server.domain, - blockingNickname, blockingDomainFull) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr, - cookie, callingDomain) - self.server.POSTbusy = False + self._unblockConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 12) # decision to block in the web interface is confirmed if authorized and self.path.endswith('/blockconfirm'): - usersPath = self.path.split('/blockconfirm')[0] - originPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - blockerNickname = getNicknameFromActor(originPathStr) - if not blockerNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + originPathStr) - self._redirect_headers(originPathStr, - cookie, callingDomain) - self.server.POSTbusy = False - return - length = int(self.headers['Content-length']) - try: - blockConfirmParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST blockConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST blockConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST blockConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitYes=' in blockConfirmParams: - blockingActor = \ - urllib.parse.unquote_plus(blockConfirmParams) - blockingActor = blockingActor.split('actor=')[1] - if '&' in blockingActor: - blockingActor = blockingActor.split('&')[0] - blockingNickname = getNicknameFromActor(blockingActor) - if not blockingNickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - print('WARN: unable to find nickname in ' + blockingActor) - self._redirect_headers(originPathStr, - cookie, callingDomain) - self.server.POSTbusy = False - return - blockingDomain, blockingPort = \ - getDomainFromActor(blockingActor) - blockingDomainFull = blockingDomain - if blockingPort: - if blockingPort != 80 and blockingPort != 443: - if ':' not in blockingDomain: - blockingDomainFull = \ - blockingDomain + ':' + str(blockingPort) - if blockerNickname == blockingNickname and \ - blockingDomain == self.server.domain and \ - blockingPort == self.server.port: - if self.server.debug: - print('You cannot block yourself!') - else: - if self.server.debug: - print('Adding block by ' + blockerNickname + - ' of ' + blockingActor) - addBlock(self.server.baseDir, blockerNickname, - self.server.domain, - blockingNickname, - blockingDomainFull) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr, cookie, callingDomain) - self.server.POSTbusy = False + self._blockConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 13) From 433dbbccd3cbcdddbb7d31930b7ff08a118d0001 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 17:35:18 +0100 Subject: [PATCH 012/108] Tidy endpoint for removing posts --- daemon.py | 191 +++++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 88 deletions(-) diff --git a/daemon.py b/daemon.py index 1a5a402d8..e3eba06c2 100644 --- a/daemon.py +++ b/daemon.py @@ -2139,6 +2139,102 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _removePost(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool): + """Endpoint for removing posts + """ + pageNumber = 1 + usersPath = path.split('/rmpost')[0] + originPathStr = \ + httpPrefix + '://' + \ + domainFull + usersPath + + length = int(self.headers['Content-length']) + + try: + removePostConfirmParams = \ + self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST removePostConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST removePostConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST removePostConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + if '&submitYes=' in removePostConfirmParams: + removePostConfirmParams = \ + urllib.parse.unquote_plus(removePostConfirmParams) + removeMessageId = \ + removePostConfirmParams.split('messageId=')[1] + if '&' in removeMessageId: + removeMessageId = removeMessageId.split('&')[0] + if 'pageNumber=' in removePostConfirmParams: + pageNumberStr = \ + removePostConfirmParams.split('pageNumber=')[1] + if '&' in pageNumberStr: + pageNumberStr = pageNumberStr.split('&')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + yearStr = None + if 'year=' in removePostConfirmParams: + yearStr = removePostConfirmParams.split('year=')[1] + if '&' in yearStr: + yearStr = yearStr.split('&')[0] + monthStr = None + if 'month=' in removePostConfirmParams: + monthStr = removePostConfirmParams.split('month=')[1] + if '&' in monthStr: + monthStr = monthStr.split('&')[0] + if '/statuses/' in removeMessageId: + removePostActor = removeMessageId.split('/statuses/')[0] + if originPathStr in removePostActor: + toList = ['https://www.w3.org/ns/activitystreams#Public', + removePostActor] + deleteJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'actor': removePostActor, + 'object': removeMessageId, + 'to': toList, + 'cc': [removePostActor+'/followers'], + 'type': 'Delete' + } + self.postToNickname = getNicknameFromActor(removePostActor) + if self.postToNickname: + if monthStr and yearStr: + if monthStr.isdigit() and yearStr.isdigit(): + removeCalendarEvent(baseDir, + self.postToNickname, + domain, + int(yearStr), + int(monthStr), + removeMessageId) + self._postToOutboxThread(deleteJson) + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + if pageNumber == 1: + self._redirect_headers(originPathStr + '/outbox', cookie, + callingDomain) + else: + self._redirect_headers(originPathStr + '/outbox?page=' + + str(pageNumber), + cookie, callingDomain) + self.server.POSTbusy = False + def _profileUpdate(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -8977,94 +9073,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return if authorized and self.path.endswith('/rmpost'): - pageNumber = 1 - usersPath = self.path.split('/rmpost')[0] - originPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - length = int(self.headers['Content-length']) - try: - removePostConfirmParams = \ - self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST removePostConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST removePostConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST removePostConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitYes=' in removePostConfirmParams: - removePostConfirmParams = \ - urllib.parse.unquote_plus(removePostConfirmParams) - removeMessageId = \ - removePostConfirmParams.split('messageId=')[1] - if '&' in removeMessageId: - removeMessageId = removeMessageId.split('&')[0] - if 'pageNumber=' in removePostConfirmParams: - pageNumberStr = \ - removePostConfirmParams.split('pageNumber=')[1] - if '&' in pageNumberStr: - pageNumberStr = pageNumberStr.split('&')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - yearStr = None - if 'year=' in removePostConfirmParams: - yearStr = removePostConfirmParams.split('year=')[1] - if '&' in yearStr: - yearStr = yearStr.split('&')[0] - monthStr = None - if 'month=' in removePostConfirmParams: - monthStr = removePostConfirmParams.split('month=')[1] - if '&' in monthStr: - monthStr = monthStr.split('&')[0] - if '/statuses/' in removeMessageId: - removePostActor = removeMessageId.split('/statuses/')[0] - if originPathStr in removePostActor: - toList = ['https://www.w3.org/ns/activitystreams#Public', - removePostActor] - deleteJson = { - "@context": "https://www.w3.org/ns/activitystreams", - 'actor': removePostActor, - 'object': removeMessageId, - 'to': toList, - 'cc': [removePostActor+'/followers'], - 'type': 'Delete' - } - self.postToNickname = getNicknameFromActor(removePostActor) - if self.postToNickname: - if monthStr and yearStr: - if monthStr.isdigit() and yearStr.isdigit(): - removeCalendarEvent(self.server.baseDir, - self.postToNickname, - self.server.domain, - int(yearStr), - int(monthStr), - removeMessageId) - self._postToOutboxThread(deleteJson) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = 'http://' + self.server.i2pDomain + usersPath - if pageNumber == 1: - self._redirect_headers(originPathStr + '/outbox', cookie, - callingDomain) - else: - self._redirect_headers(originPathStr + '/outbox?page=' + - str(pageNumber), - cookie, callingDomain) - self.server.POSTbusy = False + self._removePost(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, self.server.httpPrefix, + self.server.domain, self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 9) From 5a44e821064c3dce96a22e03e893c8892159c595 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 20:26:50 +0100 Subject: [PATCH 013/108] Tidy endpoint for removal of shared items --- daemon.py | 119 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/daemon.py b/daemon.py index e3eba06c2..e81dabde6 100644 --- a/daemon.py +++ b/daemon.py @@ -2139,6 +2139,64 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _removeShare(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool): + """Removes a shared item + """ + usersPath = path.split('/rmshare')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + + length = int(self.headers['Content-length']) + + try: + removeShareConfirmParams = \ + self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST removeShareConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST removeShareConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST removeShareConfirmParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitYes=' in removeShareConfirmParams: + removeShareConfirmParams = \ + removeShareConfirmParams.replace('+', ' ').strip() + removeShareConfirmParams = \ + urllib.parse.unquote_plus(removeShareConfirmParams) + shareActor = removeShareConfirmParams.split('actor=')[1] + if '&' in shareActor: + shareActor = shareActor.split('&')[0] + shareName = removeShareConfirmParams.split('shareName=')[1] + if '&' in shareName: + shareName = shareName.split('&')[0] + shareNickname = getNicknameFromActor(shareActor) + if shareNickname: + shareDomain, sharePort = getDomainFromActor(shareActor) + removeShare(baseDir, + shareNickname, shareDomain, shareName) + + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr + '/tlshares', + cookie, callingDomain) + self.server.POSTbusy = False + def _removePost(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -9009,58 +9067,15 @@ class PubServer(BaseHTTPRequestHandler): # removes a shared item if authorized and self.path.endswith('/rmshare'): - usersPath = self.path.split('/rmshare')[0] - originPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - length = int(self.headers['Content-length']) - try: - removeShareConfirmParams = \ - self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST removeShareConfirmParams ' + - 'connection was reset') - else: - print('WARN: POST removeShareConfirmParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST removeShareConfirmParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if '&submitYes=' in removeShareConfirmParams: - removeShareConfirmParams = \ - removeShareConfirmParams.replace('+', ' ').strip() - removeShareConfirmParams = \ - urllib.parse.unquote_plus(removeShareConfirmParams) - shareActor = removeShareConfirmParams.split('actor=')[1] - if '&' in shareActor: - shareActor = shareActor.split('&')[0] - shareName = removeShareConfirmParams.split('shareName=')[1] - if '&' in shareName: - shareName = shareName.split('&')[0] - shareNickname = getNicknameFromActor(shareActor) - if shareNickname: - shareDomain, sharePort = getDomainFromActor(shareActor) - removeShare(self.server.baseDir, - shareNickname, shareDomain, shareName) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStr = \ - 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStr = \ - 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(originPathStr + '/tlshares', - cookie, callingDomain) - self.server.POSTbusy = False + self._removeShare(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 8) From 1517456f32a60a7a43af7d2761863db281d38a5b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 20:37:44 +0100 Subject: [PATCH 014/108] Tidy endpoint for receiving images --- daemon.py | 130 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 75 insertions(+), 55 deletions(-) diff --git a/daemon.py b/daemon.py index e81dabde6..db589c614 100644 --- a/daemon.py +++ b/daemon.py @@ -2139,6 +2139,72 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _receiveImage(self, length: int, + callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool): + """Receives an image via POST + """ + if not self.outboxAuthenticated: + if debug: + print('DEBUG: unauthenticated attempt to ' + + 'post image to outbox') + self.send_response(403) + self.end_headers() + self.server.POSTbusy = False + return + pathUsersSection = path.split('/users/')[1] + if '/' not in pathUsersSection: + self._404() + self.server.POSTbusy = False + return + self.postFromNickname = pathUsersSection.split('/')[0] + accountsDir = \ + baseDir + '/accounts/' + \ + self.postFromNickname + '@' + domain + if not os.path.isdir(accountsDir): + self._404() + self.server.POSTbusy = False + return + + try: + mediaBytes = self.rfile.read(length) + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST mediaBytes ' + + 'connection reset by peer') + else: + print('WARN: POST mediaBytes socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST mediaBytes rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + mediaFilenameBase = accountsDir + '/upload' + mediaFilename = mediaFilenameBase + '.png' + if self.headers['Content-type'].endswith('jpeg'): + mediaFilename = mediaFilenameBase + '.jpg' + if self.headers['Content-type'].endswith('gif'): + mediaFilename = mediaFilenameBase + '.gif' + if self.headers['Content-type'].endswith('webp'): + mediaFilename = mediaFilenameBase + '.webp' + with open(mediaFilename, 'wb') as avFile: + avFile.write(mediaBytes) + if debug: + print('DEBUG: image saved to ' + mediaFilename) + self.send_response(201) + self.end_headers() + self.server.POSTbusy = False + def _removeShare(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -9271,61 +9337,15 @@ class PubServer(BaseHTTPRequestHandler): # receive images to the outbox if self.headers['Content-type'].startswith('image/') and \ '/users/' in self.path: - if not self.outboxAuthenticated: - if self.server.debug: - print('DEBUG: unauthenticated attempt to ' + - 'post image to outbox') - self.send_response(403) - self.end_headers() - self.server.POSTbusy = False - return - pathUsersSection = self.path.split('/users/')[1] - if '/' not in pathUsersSection: - self._404() - self.server.POSTbusy = False - return - self.postFromNickname = pathUsersSection.split('/')[0] - accountsDir = \ - self.server.baseDir + '/accounts/' + \ - self.postFromNickname + '@' + self.server.domain - if not os.path.isdir(accountsDir): - self._404() - self.server.POSTbusy = False - return - try: - mediaBytes = self.rfile.read(length) - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST mediaBytes ' + - 'connection reset by peer') - else: - print('WARN: POST mediaBytes socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST mediaBytes rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - mediaFilenameBase = accountsDir + '/upload' - mediaFilename = mediaFilenameBase + '.png' - if self.headers['Content-type'].endswith('jpeg'): - mediaFilename = mediaFilenameBase + '.jpg' - if self.headers['Content-type'].endswith('gif'): - mediaFilename = mediaFilenameBase + '.gif' - if self.headers['Content-type'].endswith('webp'): - mediaFilename = mediaFilenameBase + '.webp' - with open(mediaFilename, 'wb') as avFile: - avFile.write(mediaBytes) - if self.server.debug: - print('DEBUG: image saved to ' + mediaFilename) - self.send_response(201) - self.end_headers() - self.server.POSTbusy = False + self._receiveImage(length, callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return # refuse to receive non-json content From 85c75878e8a49df757405f036fae98d3f7790394 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 20:52:12 +0100 Subject: [PATCH 015/108] Tidy endpoint for receiving votes --- daemon.py | 160 +++++++++++++++++++++++++++++------------------------- 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/daemon.py b/daemon.py index db589c614..574086d49 100644 --- a/daemon.py +++ b/daemon.py @@ -2139,6 +2139,83 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _receiveVote(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, debug: bool): + """Receive a vote via POST + """ + pageNumber = 1 + if '?page=' in path: + pageNumberStr = path.split('?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + path = path.split('?page=')[0] + # the actor who votes + usersPath = path.replace('/question', '') + actor = httpPrefix + '://' + domainFull + usersPath + nickname = getNicknameFromActor(actor) + if not nickname: + if callingDomain.endswith('.onion') and onionDomain: + actor = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actor = 'http://' + i2pDomain + usersPath + self._redirect_headers(actor + '/' + + self.server.defaultTimeline + + '?page=' + str(pageNumber), + cookie, callingDomain) + self.server.POSTbusy = False + return + # get the parameters + length = int(self.headers['Content-length']) + try: + questionParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST questionParams connection was reset') + else: + print('WARN: POST questionParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST questionParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + questionParams = questionParams.replace('+', ' ') + questionParams = questionParams.replace('%3F', '') + questionParams = \ + urllib.parse.unquote_plus(questionParams.strip()) + # post being voted on + messageId = None + if 'messageId=' in questionParams: + messageId = questionParams.split('messageId=')[1] + if '&' in messageId: + messageId = messageId.split('&')[0] + answer = None + if 'answer=' in questionParams: + answer = questionParams.split('answer=')[1] + if '&' in answer: + answer = answer.split('&')[0] + self._sendReplyToQuestion(nickname, messageId, answer) + if callingDomain.endswith('.onion') and onionDomain: + actor = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actor = 'http://' + i2pDomain + usersPath + self._redirect_headers(actor + '/' + + self.server.defaultTimeline + + '?page=' + str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + def _receiveImage(self, length: int, callingDomain: str, cookie: str, authorized: bool, path: str, @@ -8818,80 +8895,15 @@ class PubServer(BaseHTTPRequestHandler): if (authorized and (self.path.endswith('/question') or '/question?page=' in self.path)): - pageNumber = 1 - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - self.path = self.path.split('?page=')[0] - # the actor who votes - usersPath = self.path.replace('/question', '') - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - nickname = getNicknameFromActor(actor) - if not nickname: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actor + '/' + - self.server.defaultTimeline + - '?page=' + str(pageNumber), - cookie, callingDomain) - self.server.POSTbusy = False - return - # get the parameters - length = int(self.headers['Content-length']) - try: - questionParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST questionParams connection was reset') - else: - print('WARN: POST questionParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST questionParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - questionParams = questionParams.replace('+', ' ') - questionParams = questionParams.replace('%3F', '') - questionParams = \ - urllib.parse.unquote_plus(questionParams.strip()) - # post being voted on - messageId = None - if 'messageId=' in questionParams: - messageId = questionParams.split('messageId=')[1] - if '&' in messageId: - messageId = messageId.split('&')[0] - answer = None - if 'answer=' in questionParams: - answer = questionParams.split('answer=')[1] - if '&' in answer: - answer = answer.split('&')[0] - self._sendReplyToQuestion(nickname, messageId, answer) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actor + '/' + - self.server.defaultTimeline + - '?page=' + str(pageNumber), cookie, - callingDomain) - self.server.POSTbusy = False + self._receiveVote(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 6) From c02afc7880ae0b6ccca2f87d0e6f6390a6767253 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 21:04:21 +0100 Subject: [PATCH 016/108] Simplify conditions --- daemon.py | 219 +++++++++++++++++++++++++++++------------------------- 1 file changed, 117 insertions(+), 102 deletions(-) diff --git a/daemon.py b/daemon.py index 574086d49..09d65c850 100644 --- a/daemon.py +++ b/daemon.py @@ -2154,6 +2154,7 @@ class PubServer(BaseHTTPRequestHandler): if pageNumberStr.isdigit(): pageNumber = int(pageNumberStr) path = path.split('?page=')[0] + # the actor who votes usersPath = path.replace('/question', '') actor = httpPrefix + '://' + domainFull + usersPath @@ -2169,8 +2170,10 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False return + # get the parameters length = int(self.headers['Content-length']) + try: questionParams = self.rfile.read(length).decode('utf-8') except SocketError as e: @@ -2189,21 +2192,25 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() self.server.POSTbusy = False return + questionParams = questionParams.replace('+', ' ') questionParams = questionParams.replace('%3F', '') questionParams = \ urllib.parse.unquote_plus(questionParams.strip()) + # post being voted on messageId = None if 'messageId=' in questionParams: messageId = questionParams.split('messageId=')[1] if '&' in messageId: messageId = messageId.split('&')[0] + answer = None if 'answer=' in questionParams: answer = questionParams.split('answer=')[1] if '&' in answer: answer = answer.split('&')[0] + self._sendReplyToQuestion(nickname, messageId, answer) if callingDomain.endswith('.onion') and onionDomain: actor = 'http://' + onionDomain + usersPath @@ -8891,21 +8898,6 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 5) - # a vote/question/poll is posted - if (authorized and - (self.path.endswith('/question') or - '/question?page=' in self.path)): - self._receiveVote(callingDomain, cookie, - authorized, self.path, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 6) # a search was made @@ -9143,111 +9135,134 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 7) - # removes a shared item - if authorized and self.path.endswith('/rmshare'): - self._removeShare(callingDomain, cookie, - authorized, self.path, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return - - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 8) - - # removes a post - if not authorized and self.path.endswith('/rmpost'): - print('ERROR: attempt to remove post was not authorized. ' + - self.path) - self._400() - self.server.POSTbusy = False - return - if authorized and self.path.endswith('/rmpost'): - self._removePost(callingDomain, cookie, - authorized, self.path, - self.server.baseDir, self.server.httpPrefix, - self.server.domain, self.server.domainFull, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return - - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 9) - - # decision to follow in the web interface is confirmed - if authorized and self.path.endswith('/followconfirm'): - self._followConfirm(callingDomain, cookie, - authorized, self.path, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return - - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 10) - - # decision to unfollow in the web interface is confirmed - if authorized and self.path.endswith('/unfollowconfirm'): - self._unfollowConfirm(callingDomain, cookie, + if authorized: + # a vote/question/poll is posted + if self.path.endswith('/question') or \ + '/question?page=' in self.path: + self._receiveVote(callingDomain, cookie, authorized, self.path, self.server.baseDir, self.server.httpPrefix, self.server.domain, self.server.domainFull, - self.server.port, self.server.onionDomain, self.server.i2pDomain, self.server.debug) - return + return - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 11) + # removes a shared item + if self.path.endswith('/rmshare'): + self._removeShare(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return - # decision to unblock in the web interface is confirmed - if authorized and self.path.endswith('/unblockconfirm'): - self._unblockConfirm(callingDomain, cookie, + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 8) + + # removes a post + if self.path.endswith('/rmpost'): + print('ERROR: attempt to remove post was not authorized. ' + + self.path) + self._400() + self.server.POSTbusy = False + return + if self.path.endswith('/rmpost'): + self._removePost(callingDomain, cookie, authorized, self.path, - self.server.baseDir, self.server.httpPrefix, - self.server.domain, self.server.domainFull, - self.server.port, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, self.server.onionDomain, self.server.i2pDomain, self.server.debug) - return + return - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 12) + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 9) - # decision to block in the web interface is confirmed - if authorized and self.path.endswith('/blockconfirm'): - self._blockConfirm(callingDomain, cookie, - authorized, self.path, - self.server.baseDir, self.server.httpPrefix, - self.server.domain, self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return + # decision to follow in the web interface is confirmed + if self.path.endswith('/followconfirm'): + self._followConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return - self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 13) + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 10) - # an option was chosen from person options screen - # view/follow/block/report - if authorized and self.path.endswith('/personoptions'): - self._personOptions(self, self.path, callingDomain, cookie, - self.server.baseDir, self.server.httpPrefix, - self.server.domain, self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - self.server.debug) - return + # decision to unfollow in the web interface is confirmed + if self.path.endswith('/unfollowconfirm'): + self._unfollowConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return + + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 11) + + # decision to unblock in the web interface is confirmed + if self.path.endswith('/unblockconfirm'): + self._unblockConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return + + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 12) + + # decision to block in the web interface is confirmed + if self.path.endswith('/blockconfirm'): + self._blockConfirm(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return + + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 13) + + # an option was chosen from person options screen + # view/follow/block/report + if self.path.endswith('/personoptions'): + self._personOptions(self, self.path, callingDomain, cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 14) From 41cb5d3c0cb66cca3aee74e2232a17a7c3e7e4e7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 21:07:01 +0100 Subject: [PATCH 017/108] Extra parameter --- daemon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index 09d65c850..ee6625ad0 100644 --- a/daemon.py +++ b/daemon.py @@ -1466,7 +1466,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - def _personOptions(self, path: str, callingDomain: str, cookie: str, + def _personOptions(self, path: str, + callingDomain: str, cookie: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, @@ -9253,7 +9254,8 @@ class PubServer(BaseHTTPRequestHandler): # an option was chosen from person options screen # view/follow/block/report if self.path.endswith('/personoptions'): - self._personOptions(self, self.path, callingDomain, cookie, + self._personOptions(self.path, + callingDomain, cookie, self.server.baseDir, self.server.httpPrefix, self.server.domain, From 0e1bfe94d7b4c89475b8d4b1f2a671a0b5c9b602 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 30 Aug 2020 21:25:36 +0100 Subject: [PATCH 018/108] Tidy receiving search --- daemon.py | 466 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 239 insertions(+), 227 deletions(-) diff --git a/daemon.py b/daemon.py index ee6625ad0..fdc9a0ea0 100644 --- a/daemon.py +++ b/daemon.py @@ -2140,6 +2140,234 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _receiveSearchQuery(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + port: int, searchForEmoji: bool, + onionDomain: str, i2pDomain: str, debug: bool): + """Receive a search query + """ + # get the page number + pageNumber = 1 + if '/searchhandle?page=' in path: + pageNumberStr = path.split('/searchhandle?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + path = path.split('?page=')[0] + + usersPath = path.replace('/searchhandle', '') + actorStr = httpPrefix + '://' + domainFull + usersPath + length = int(self.headers['Content-length']) + try: + searchParams = self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST searchParams connection was reset') + else: + print('WARN: POST searchParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST searchParams rfile.read failed') + print(e) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + if 'submitBack=' in searchParams: + # go back on search screen + if callingDomain.endswith('.onion') and onionDomain: + actorStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(actorStr + '/' + + self.server.defaultTimeline, + cookie, callingDomain) + self.server.POSTbusy = False + return + if 'searchtext=' in searchParams: + searchStr = searchParams.split('searchtext=')[1] + if '&' in searchStr: + searchStr = searchStr.split('&')[0] + searchStr = \ + urllib.parse.unquote_plus(searchStr.strip()) + searchStr2 = searchStr.lower().strip('\n').strip('\r') + print('searchStr: ' + searchStr) + if searchForEmoji: + searchStr = ':' + searchStr + ':' + if searchStr.startswith('#'): + nickname = getNicknameFromActor(actorStr) + # hashtag search + hashtagStr = \ + htmlHashtagSearch(nickname, + domain, + port, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + baseDir, + searchStr[1:], 1, + maxPostsInFeed, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain) + if hashtagStr: + msg = hashtagStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + elif searchStr.startswith('*'): + # skill search + searchStr = searchStr.replace('*', '').strip() + skillStr = \ + htmlSkillsSearch(self.server.translate, + baseDir, + httpPrefix, + searchStr, + self.server.instanceOnlySkillsSearch, + 64) + if skillStr: + msg = skillStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + elif searchStr.startswith('!'): + # your post history search + nickname = getNicknameFromActor(actorStr) + searchStr = searchStr.replace('!', '').strip() + historyStr = \ + htmlHistorySearch(self.server.translate, + baseDir, + httpPrefix, + nickname, + domain, + searchStr, + maxPostsInFeed, + pageNumber, + self.server.projectVersion, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + port, + self.server.YTReplacementDomain) + if historyStr: + msg = historyStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + elif ('@' in searchStr or + ('://' in searchStr and + ('/users/' in searchStr or + '/profile/' in searchStr or + '/accounts/' in searchStr or + '/channel/' in searchStr))): + # profile search + nickname = getNicknameFromActor(actorStr) + if not self.server.session: + print('Starting new session during handle search') + self.server.session = \ + createSession(self.server.proxyType) + if not self.server.session: + print('ERROR: POST failed to create session ' + + 'during handle search') + self._404() + self.server.POSTbusy = False + return + profilePathStr = self.path.replace('/searchhandle', '') + profileStr = \ + htmlProfileAfterSearch(self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + baseDir, + profilePathStr, + httpPrefix, + nickname, + domain, + port, + searchStr, + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.server.debug, + self.server.projectVersion, + self.server.YTReplacementDomain) + if profileStr: + msg = profileStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + else: + if callingDomain.endswith('.onion') and onionDomain: + actorStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(actorStr + '/search', + cookie, callingDomain) + self.server.POSTbusy = False + return + elif (searchStr.startswith(':') or + searchStr2.endswith(' emoji')): + # eg. "cat emoji" + if searchStr2.endswith(' emoji'): + searchStr = \ + searchStr2.replace(' emoji', '') + # emoji search + emojiStr = \ + htmlSearchEmoji(self.server.translate, + baseDir, + httpPrefix, + searchStr) + if emojiStr: + msg = emojiStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + else: + # shared items search + sharedItemsStr = \ + htmlSearchSharedItems(self.server.translate, + baseDir, + searchStr, pageNumber, + maxPostsInFeed, + httpPrefix, + domainFull, + actorStr, callingDomain) + if sharedItemsStr: + msg = sharedItemsStr.encode('utf-8') + self._login_headers('text/html', + len(msg), callingDomain) + self._write(msg) + self.server.POSTbusy = False + return + if callingDomain.endswith('.onion') and onionDomain: + actorStr = 'http://' + onionDomain + usersPath + elif callingDomain.endswith('.i2p') and i2pDomain: + actorStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(actorStr + '/' + + self.server.defaultTimeline, + cookie, callingDomain) + self.server.POSTbusy = False + def _receiveVote(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -8905,233 +9133,17 @@ class PubServer(BaseHTTPRequestHandler): if ((authorized or searchForEmoji) and (self.path.endswith('/searchhandle') or '/searchhandle?page=' in self.path)): - # get the page number - pageNumber = 1 - if '/searchhandle?page=' in self.path: - pageNumberStr = self.path.split('/searchhandle?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - self.path = self.path.split('?page=')[0] - - usersPath = self.path.replace('/searchhandle', '') - actorStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - length = int(self.headers['Content-length']) - try: - searchParams = self.rfile.read(length).decode('utf-8') - except SocketError as e: - if e.errno == errno.ECONNRESET: - print('WARN: POST searchParams connection was reset') - else: - print('WARN: POST searchParams socket error') - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - except ValueError as e: - print('ERROR: POST searchParams rfile.read failed') - print(e) - self.send_response(400) - self.end_headers() - self.server.POSTbusy = False - return - if 'submitBack=' in searchParams: - # go back on search screen - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actorStr + '/' + - self.server.defaultTimeline, - cookie, callingDomain) - self.server.POSTbusy = False - return - if 'searchtext=' in searchParams: - searchStr = searchParams.split('searchtext=')[1] - if '&' in searchStr: - searchStr = searchStr.split('&')[0] - searchStr = \ - urllib.parse.unquote_plus(searchStr.strip()) - searchStr2 = searchStr.lower().strip('\n').strip('\r') - print('searchStr: ' + searchStr) - if searchForEmoji: - searchStr = ':' + searchStr + ':' - if searchStr.startswith('#'): - nickname = getNicknameFromActor(actorStr) - # hashtag search - hashtagStr = \ - htmlHashtagSearch(nickname, - self.server.domain, - self.server.port, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.baseDir, - searchStr[1:], 1, - maxPostsInFeed, - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.httpPrefix, - self.server.projectVersion, - self.server.YTReplacementDomain) - if hashtagStr: - msg = hashtagStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - elif searchStr.startswith('*'): - # skill search - searchStr = searchStr.replace('*', '').strip() - skillStr = \ - htmlSkillsSearch(self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - searchStr, - self.server.instanceOnlySkillsSearch, - 64) - if skillStr: - msg = skillStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - elif searchStr.startswith('!'): - # your post history search - nickname = getNicknameFromActor(actorStr) - searchStr = searchStr.replace('!', '').strip() - historyStr = \ - htmlHistorySearch(self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - nickname, - self.server.domain, - searchStr, - maxPostsInFeed, - pageNumber, - self.server.projectVersion, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.port, - self.server.YTReplacementDomain) - if historyStr: - msg = historyStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - elif ('@' in searchStr or - ('://' in searchStr and - ('/users/' in searchStr or - '/profile/' in searchStr or - '/accounts/' in searchStr or - '/channel/' in searchStr))): - # profile search - nickname = getNicknameFromActor(actorStr) - if not self.server.session: - print('Starting new session during handle search') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: POST failed to create session ' + - 'during handle search') - self._404() - self.server.POSTbusy = False - return - profilePathStr = self.path.replace('/searchhandle', '') - profileStr = \ - htmlProfileAfterSearch(self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.baseDir, - profilePathStr, - self.server.httpPrefix, - nickname, - self.server.domain, - self.server.port, - searchStr, - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.debug, - self.server.projectVersion, - self.server.YTReplacementDomain) - if profileStr: - msg = profileStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - else: - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorStr = 'http://' + self.server.onionDomain + \ - usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorStr = 'http://' + self.server.i2pDomain + \ - usersPath - self._redirect_headers(actorStr + '/search', - cookie, callingDomain) - self.server.POSTbusy = False - return - elif (searchStr.startswith(':') or - searchStr2.endswith(' emoji')): - # eg. "cat emoji" - if searchStr2.endswith(' emoji'): - searchStr = \ - searchStr2.replace(' emoji', '') - # emoji search - emojiStr = \ - htmlSearchEmoji(self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - searchStr) - if emojiStr: - msg = emojiStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - else: - # shared items search - sharedItemsStr = \ - htmlSearchSharedItems(self.server.translate, - self.server.baseDir, - searchStr, pageNumber, - maxPostsInFeed, - self.server.httpPrefix, - self.server.domainFull, - actorStr, callingDomain) - if sharedItemsStr: - msg = sharedItemsStr.encode('utf-8') - self._login_headers('text/html', - len(msg), callingDomain) - self._write(msg) - self.server.POSTbusy = False - return - if callingDomain.endswith('.onion') and self.server.onionDomain: - actorStr = 'http://' + self.server.onionDomain + usersPath - elif callingDomain.endswith('.i2p') and self.server.i2pDomain: - actorStr = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actorStr + '/' + - self.server.defaultTimeline, - cookie, callingDomain) - self.server.POSTbusy = False + self._receiveSearchQuery(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + searchForEmoji, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) return self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 7) From 329c86e73105f64d16534d2fe07d0ad4225e2154 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:05:20 +0100 Subject: [PATCH 019/108] Implement mute and unmute more efficiently by avoiding regenerating the post --- daemon.py | 2 +- posts.py | 58 +++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/daemon.py b/daemon.py index fdc9a0ea0..689176492 100644 --- a/daemon.py +++ b/daemon.py @@ -5901,7 +5901,7 @@ class PubServer(BaseHTTPRequestHandler): 'unbookmark shown done', 'delete shown done') - # mute a post from the web interface icon + # The mute button is pressed if htmlGET and '?mute=' in self.path: pageNumber = 1 if '?page=' in self.path: diff --git a/posts.py b/posts.py index 95026c113..01233b0ab 100644 --- a/posts.py +++ b/posts.py @@ -31,7 +31,6 @@ from webfinger import webfingerHandle from httpsig import createSignedHeader from utils import removeIdEnding from utils import siteIsActive -from utils import removePostFromCache from utils import getCachedPostFilename from utils import getStatusNumber from utils import createPersonDir @@ -3535,19 +3534,25 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if not postJsonObject: return + # change the mute icon in the cached post file + cachedPostFilename = \ + getCachedPostFilename(baseDir, nickname, domain, postJsonObject) + if cachedPostFilename: + with open(cachedPostFilename, 'r') as cacheFile: + postHtml = cacheFile.read() + if '/unmute.png' in postHtml: + postHtml = postHtml.replace('/unmute.png', '/mute.png') + newCacheFile = open(cachedPostFilename, 'w+') + if newCacheFile: + newCacheFile.write(postHtml) + newCacheFile.close() + print('MUTE: ' + postFilename) muteFile = open(postFilename + '.muted', 'w+') if muteFile: muteFile.write('\n') muteFile.close() - # remove cached posts so that the muted version gets created - cachedPostFilename = \ - getCachedPostFilename(baseDir, nickname, domain, postJsonObject) - if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - # if the post is in the recent posts cache then mark it as muted if recentPostsCache.get('index'): postId = \ @@ -3557,6 +3562,12 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if recentPostsCache['json'].get(postId): postJsonObject['muted'] = True recentPostsCache['json'][postId] = json.dumps(postJsonObject) + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + postHtml = recentPostsCache['html'][postId] + if '/unmute.png' in postHtml: + recentPostsCache['html'][postId] = \ + postHtml.replace('/unmute.png', '/mute.png') print('MUTE: ' + postId + ' marked as muted in recent posts cache') @@ -3577,13 +3588,36 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if os.path.isfile(muteFilename): os.remove(muteFilename) - # remove cached posts so that it gets recreated + # change the mute icon in the cached post file cachedPostFilename = \ getCachedPostFilename(baseDir, nickname, domain, postJsonObject) if cachedPostFilename: - if os.path.isfile(cachedPostFilename): - os.remove(cachedPostFilename) - removePostFromCache(postJsonObject, recentPostsCache) + with open(cachedPostFilename, 'r') as cacheFile: + postHtml = cacheFile.read() + if '/mute.png' in postHtml: + postHtml = postHtml.replace('/mute.png', '/unmute.png') + newCacheFile = open(cachedPostFilename, 'w+') + if newCacheFile: + newCacheFile.write(postHtml) + newCacheFile.close() + + # if the post is in the recent posts cache then mark it as muted + if recentPostsCache.get('index'): + postId = \ + removeIdEnding(postJsonObject['id']).replace('/', '#') + if postId in recentPostsCache['index']: + print('UNMUTE: ' + postId + ' is in recent posts cache') + if recentPostsCache['json'].get(postId): + postJsonObject['muted'] = False + recentPostsCache['json'][postId] = json.dumps(postJsonObject) + if recentPostsCache.get('html'): + if recentPostsCache['html'].get(postId): + postHtml = recentPostsCache['html'][postId] + if '/mute.png' in postHtml: + recentPostsCache['html'][postId] = \ + postHtml.replace('/mute.png', '/unmute.png') + print('UNMUTE: ' + postId + + ' marked as unmuted in recent posts cache') def sendBlockViaServer(baseDir: str, session, From 74fd37ee00e3e7fa0f9a5f6d15c83e2562843e05 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:16:49 +0100 Subject: [PATCH 020/108] More debug for mute --- posts.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/posts.py b/posts.py index 01233b0ab..c248ae0e2 100644 --- a/posts.py +++ b/posts.py @@ -3546,12 +3546,19 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if newCacheFile: newCacheFile.write(postHtml) newCacheFile.close() + print('MUTE: ' + cachedPostFilename + + ' icon changed') + else: + print('MUTE: ' + cachedPostFilename + + ' icon not changed') + else: + print('MUTE: ' + cachedPostFilename + ' not cached to file') - print('MUTE: ' + postFilename) muteFile = open(postFilename + '.muted', 'w+') if muteFile: muteFile.write('\n') muteFile.close() + print('MUTE: ' + postFilename + '.muted file added') # if the post is in the recent posts cache then mark it as muted if recentPostsCache.get('index'): @@ -3569,7 +3576,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, recentPostsCache['html'][postId] = \ postHtml.replace('/unmute.png', '/mute.png') print('MUTE: ' + postId + - ' marked as muted in recent posts cache') + ' marked as muted in recent posts memory cache') def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, @@ -3583,10 +3590,10 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if not postJsonObject: return - print('UNMUTE: ' + postFilename) muteFilename = postFilename + '.muted' if os.path.isfile(muteFilename): os.remove(muteFilename) + print('UNMUTE: ' + muteFilename + ' file removed') # change the mute icon in the cached post file cachedPostFilename = \ @@ -3600,6 +3607,11 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if newCacheFile: newCacheFile.write(postHtml) newCacheFile.close() + else: + print('MUTE: ' + cachedPostFilename + + ' icon not changed') + else: + print('MUTE: ' + cachedPostFilename + ' not cached to file') # if the post is in the recent posts cache then mark it as muted if recentPostsCache.get('index'): From f59d411c9ed265c3661bdd012a3337b484395b64 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:26:23 +0100 Subject: [PATCH 021/108] Change mute link --- posts.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/posts.py b/posts.py index c248ae0e2..322f7f11c 100644 --- a/posts.py +++ b/posts.py @@ -3542,6 +3542,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, postHtml = cacheFile.read() if '/unmute.png' in postHtml: postHtml = postHtml.replace('/unmute.png', '/mute.png') + postHtml = postHtml.replace('?mute=', '?unmute=') newCacheFile = open(cachedPostFilename, 'w+') if newCacheFile: newCacheFile.write(postHtml) @@ -3573,8 +3574,10 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if recentPostsCache['html'].get(postId): postHtml = recentPostsCache['html'][postId] if '/unmute.png' in postHtml: - recentPostsCache['html'][postId] = \ + postHtml = \ postHtml.replace('/unmute.png', '/mute.png') + recentPostsCache['html'][postId] = \ + postHtml.replace('?mute=', '?unmute=') print('MUTE: ' + postId + ' marked as muted in recent posts memory cache') @@ -3603,6 +3606,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, postHtml = cacheFile.read() if '/mute.png' in postHtml: postHtml = postHtml.replace('/mute.png', '/unmute.png') + postHtml = postHtml.replace('?unmute=', '?mute=') newCacheFile = open(cachedPostFilename, 'w+') if newCacheFile: newCacheFile.write(postHtml) @@ -3626,8 +3630,10 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if recentPostsCache['html'].get(postId): postHtml = recentPostsCache['html'][postId] if '/mute.png' in postHtml: - recentPostsCache['html'][postId] = \ + postHtml = \ postHtml.replace('/mute.png', '/unmute.png') + recentPostsCache['html'][postId] = \ + postHtml.replace('?unmute=', '?mute=') print('UNMUTE: ' + postId + ' marked as unmuted in recent posts cache') From e5eb6efad6f8e370bb6f49fc30d1ccd65ef6cbbf Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:30:47 +0100 Subject: [PATCH 022/108] mute icon names the other way around --- posts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/posts.py b/posts.py index 322f7f11c..1400e4a3b 100644 --- a/posts.py +++ b/posts.py @@ -3540,8 +3540,8 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if cachedPostFilename: with open(cachedPostFilename, 'r') as cacheFile: postHtml = cacheFile.read() - if '/unmute.png' in postHtml: - postHtml = postHtml.replace('/unmute.png', '/mute.png') + if '/mute.png' in postHtml: + postHtml = postHtml.replace('/mute.png', '/unmute.png') postHtml = postHtml.replace('?mute=', '?unmute=') newCacheFile = open(cachedPostFilename, 'w+') if newCacheFile: @@ -3575,7 +3575,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, postHtml = recentPostsCache['html'][postId] if '/unmute.png' in postHtml: postHtml = \ - postHtml.replace('/unmute.png', '/mute.png') + postHtml.replace('/mute.png', '/unmute.png') recentPostsCache['html'][postId] = \ postHtml.replace('?mute=', '?unmute=') print('MUTE: ' + postId + @@ -3605,7 +3605,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, with open(cachedPostFilename, 'r') as cacheFile: postHtml = cacheFile.read() if '/mute.png' in postHtml: - postHtml = postHtml.replace('/mute.png', '/unmute.png') + postHtml = postHtml.replace('/unmute.png', '/mute.png') postHtml = postHtml.replace('?unmute=', '?mute=') newCacheFile = open(cachedPostFilename, 'w+') if newCacheFile: @@ -3631,7 +3631,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, postHtml = recentPostsCache['html'][postId] if '/mute.png' in postHtml: postHtml = \ - postHtml.replace('/mute.png', '/unmute.png') + postHtml.replace('/unmute.png', '/mute.png') recentPostsCache['html'][postId] = \ postHtml.replace('?unmute=', '?mute=') print('UNMUTE: ' + postId + From 932f5d4a5a3ea0808bfb5233582403e24b31444c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:38:23 +0100 Subject: [PATCH 023/108] Muted post does need to be recreated --- posts.py | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/posts.py b/posts.py index 1400e4a3b..c8017f33d 100644 --- a/posts.py +++ b/posts.py @@ -3534,26 +3534,13 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if not postJsonObject: return - # change the mute icon in the cached post file + # remove cached post so that the muted version gets recreated + # without its content text and/or image cachedPostFilename = \ getCachedPostFilename(baseDir, nickname, domain, postJsonObject) if cachedPostFilename: - with open(cachedPostFilename, 'r') as cacheFile: - postHtml = cacheFile.read() - if '/mute.png' in postHtml: - postHtml = postHtml.replace('/mute.png', '/unmute.png') - postHtml = postHtml.replace('?mute=', '?unmute=') - newCacheFile = open(cachedPostFilename, 'w+') - if newCacheFile: - newCacheFile.write(postHtml) - newCacheFile.close() - print('MUTE: ' + cachedPostFilename + - ' icon changed') - else: - print('MUTE: ' + cachedPostFilename + - ' icon not changed') - else: - print('MUTE: ' + cachedPostFilename + ' not cached to file') + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) muteFile = open(postFilename + '.muted', 'w+') if muteFile: @@ -3598,24 +3585,13 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, os.remove(muteFilename) print('UNMUTE: ' + muteFilename + ' file removed') - # change the mute icon in the cached post file + # remove cached post so that the muted version gets recreated + # with its content text and/or image cachedPostFilename = \ getCachedPostFilename(baseDir, nickname, domain, postJsonObject) if cachedPostFilename: - with open(cachedPostFilename, 'r') as cacheFile: - postHtml = cacheFile.read() - if '/mute.png' in postHtml: - postHtml = postHtml.replace('/unmute.png', '/mute.png') - postHtml = postHtml.replace('?unmute=', '?mute=') - newCacheFile = open(cachedPostFilename, 'w+') - if newCacheFile: - newCacheFile.write(postHtml) - newCacheFile.close() - else: - print('MUTE: ' + cachedPostFilename + - ' icon not changed') - else: - print('MUTE: ' + cachedPostFilename + ' not cached to file') + if os.path.isfile(cachedPostFilename): + os.remove(cachedPostFilename) # if the post is in the recent posts cache then mark it as muted if recentPostsCache.get('index'): From 42fe4721cc807dedfa87d9f4aea73710d9b6f7ca Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:42:47 +0100 Subject: [PATCH 024/108] Switch mutes --- posts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posts.py b/posts.py index c8017f33d..4339d22ef 100644 --- a/posts.py +++ b/posts.py @@ -3560,7 +3560,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, if recentPostsCache.get('html'): if recentPostsCache['html'].get(postId): postHtml = recentPostsCache['html'][postId] - if '/unmute.png' in postHtml: + if '/mute.png' in postHtml: postHtml = \ postHtml.replace('/mute.png', '/unmute.png') recentPostsCache['html'][postId] = \ @@ -3605,7 +3605,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if recentPostsCache.get('html'): if recentPostsCache['html'].get(postId): postHtml = recentPostsCache['html'][postId] - if '/mute.png' in postHtml: + if '/unmute.png' in postHtml: postHtml = \ postHtml.replace('/unmute.png', '/mute.png') recentPostsCache['html'][postId] = \ From 6e2d2d112f7454c67893710bebdaa3d2ba91967e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 10:58:29 +0100 Subject: [PATCH 025/108] Remove cached html for muted post when state changes --- posts.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/posts.py b/posts.py index 4339d22ef..235aa0f3e 100644 --- a/posts.py +++ b/posts.py @@ -3559,12 +3559,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str, recentPostsCache['json'][postId] = json.dumps(postJsonObject) if recentPostsCache.get('html'): if recentPostsCache['html'].get(postId): - postHtml = recentPostsCache['html'][postId] - if '/mute.png' in postHtml: - postHtml = \ - postHtml.replace('/mute.png', '/unmute.png') - recentPostsCache['html'][postId] = \ - postHtml.replace('?mute=', '?unmute=') + del recentPostsCache['html'][postId] print('MUTE: ' + postId + ' marked as muted in recent posts memory cache') @@ -3604,12 +3599,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, recentPostsCache['json'][postId] = json.dumps(postJsonObject) if recentPostsCache.get('html'): if recentPostsCache['html'].get(postId): - postHtml = recentPostsCache['html'][postId] - if '/unmute.png' in postHtml: - postHtml = \ - postHtml.replace('/unmute.png', '/mute.png') - recentPostsCache['html'][postId] = \ - postHtml.replace('?unmute=', '?mute=') + del recentPostsCache['html'][postId] print('UNMUTE: ' + postId + ' marked as unmuted in recent posts cache') From 4d15aab7c799d097ad40f14ab41def15cef29aef Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 11:02:45 +0100 Subject: [PATCH 026/108] Comment --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index 235aa0f3e..1d32205f7 100644 --- a/posts.py +++ b/posts.py @@ -3588,7 +3588,7 @@ def unmutePost(baseDir: str, nickname: str, domain: str, postId: str, if os.path.isfile(cachedPostFilename): os.remove(cachedPostFilename) - # if the post is in the recent posts cache then mark it as muted + # if the post is in the recent posts cache then mark it as unmuted if recentPostsCache.get('index'): postId = \ removeIdEnding(postJsonObject['id']).replace('/', '#') From 40c4e6e766fc92a8eb64744012e50378f0ce1d16 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 12:31:31 +0100 Subject: [PATCH 027/108] An extra font --- fonts/JetBrainsMono-Regular.woff2 | Bin 0 -> 50800 bytes fonts/LICENSES | 1 + 2 files changed, 1 insertion(+) create mode 100644 fonts/JetBrainsMono-Regular.woff2 diff --git a/fonts/JetBrainsMono-Regular.woff2 b/fonts/JetBrainsMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e57de0b1bcdb1115e3e61d86a09041c2513f86f7 GIT binary patch literal 50800 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 Date: Mon, 31 Aug 2020 12:51:31 +0100 Subject: [PATCH 028/108] Default theme font --- theme.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/theme.py b/theme.py index 8924232e5..75b97923c 100644 --- a/theme.py +++ b/theme.py @@ -223,15 +223,16 @@ def setThemeDefault(baseDir: str): name = 'default' removeTheme(baseDir) setThemeInConfig(baseDir, name) - themeParams = { - "dummyValue": "1234" - } bgParams = { "login": "jpg", "follow": "jpg", "options": "jpg", "search": "jpg" } + themeParams = { + "*font-family": "'JetBrainsMono-Regular'", + "*src": "url('./fonts/JetBrainsMono-Regular.woff2') format('woff2')" + } setThemeFromDict(baseDir, name, themeParams, bgParams) From 65e793e67f5113867b439f317b5eb052556c6100 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 13:07:00 +0100 Subject: [PATCH 029/108] Minimum button width --- epicyon-profile.css | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 62ee6412e..00a6d3f28 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -992,8 +992,8 @@ aside .toggle-inside li { font-family: Arial, Helvetica, sans-serif; padding: var(--button-height-padding); width: 10%; - max-width: 100px; - min-width: 80px; + max-width: 200px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1009,7 +1009,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1025,7 +1025,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1041,7 +1041,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1432,7 +1432,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1448,7 +1448,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1464,7 +1464,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1480,7 +1480,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 80px; + min-width: 10ch; transition: all 0.5s; cursor: pointer; margin: 15px; From 1f6a8c02ee8a2e9f3abfa9c8ce117e387a3c174b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 13:17:25 +0100 Subject: [PATCH 030/108] Back to original default theme font --- theme.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/theme.py b/theme.py index 75b97923c..9227aa99a 100644 --- a/theme.py +++ b/theme.py @@ -230,8 +230,7 @@ def setThemeDefault(baseDir: str): "search": "jpg" } themeParams = { - "*font-family": "'JetBrainsMono-Regular'", - "*src": "url('./fonts/JetBrainsMono-Regular.woff2') format('woff2')" + "dummy": "1234" } setThemeFromDict(baseDir, name, themeParams, bgParams) From 47d68b3c4e80a0bdf1e0ff2184f4f3efd029221b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 14:50:16 +0100 Subject: [PATCH 031/108] Remove trailing spaces from search string --- daemon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/daemon.py b/daemon.py index 689176492..ef3b18f0f 100644 --- a/daemon.py +++ b/daemon.py @@ -2196,7 +2196,7 @@ class PubServer(BaseHTTPRequestHandler): searchStr = searchStr.split('&')[0] searchStr = \ urllib.parse.unquote_plus(searchStr.strip()) - searchStr2 = searchStr.lower().strip('\n').strip('\r') + searchStr = searchStr.lower().strip() print('searchStr: ' + searchStr) if searchForEmoji: searchStr = ':' + searchStr + ':' @@ -2324,11 +2324,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return elif (searchStr.startswith(':') or - searchStr2.endswith(' emoji')): + searchStr.endswith(' emoji')): # eg. "cat emoji" - if searchStr2.endswith(' emoji'): + if searchStr.endswith(' emoji'): searchStr = \ - searchStr2.replace(' emoji', '') + searchStr.replace(' emoji', '') # emoji search emojiStr = \ htmlSearchEmoji(self.server.translate, From 7043f79355a98dbfd3aed2e43fc41d183ddcf640 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 15:31:00 +0100 Subject: [PATCH 032/108] Tidy pwa manifest --- daemon.py | 188 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 97 insertions(+), 91 deletions(-) diff --git a/daemon.py b/daemon.py index ef3b18f0f..3b22becba 100644 --- a/daemon.py +++ b/daemon.py @@ -3435,6 +3435,101 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(actorStr, cookie, callingDomain) self.server.POSTbusy = False + def _progressiveWebAppManifest(self, callingDomain: str, + GETstartTime, GETtimings: {}): + """gets the PWA manifest + """ + app1 = "https://f-droid.org/en/packages/eu.siacs.conversations" + app2 = "https://staging.f-droid.org/en/packages/im.vector.app" + manifest = { + "name": "Epicyon", + "short_name": "Epicyon", + "start_url": "/index.html", + "display": "standalone", + "background_color": "black", + "theme_color": "grey", + "orientation": "portrait-primary", + "categories": ["microblog", "fediverse", "activitypub"], + "screenshots": [ + { + "src": "/mobile.jpg", + "sizes": "418x851", + "type": "image/jpeg" + }, + { + "src": "/mobile_person.jpg", + "sizes": "429x860", + "type": "image/jpeg" + }, + { + "src": "/mobile_search.jpg", + "sizes": "422x861", + "type": "image/jpeg" + } + ], + "icons": [ + { + "src": "/logo72.png", + "type": "image/png", + "sizes": "72x72" + }, + { + "src": "/logo96.png", + "type": "image/png", + "sizes": "96x96" + }, + { + "src": "/logo128.png", + "type": "image/png", + "sizes": "128x128" + }, + { + "src": "/logo144.png", + "type": "image/png", + "sizes": "144x144" + }, + { + "src": "/logo152.png", + "type": "image/png", + "sizes": "152x152" + }, + { + "src": "/logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/logo256.png", + "type": "image/png", + "sizes": "256x256" + }, + { + "src": "/logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "related_applications": [ + { + "platform": "fdroid", + "url": app1 + }, + { + "platform": "fdroid", + "url": app2 + } + ] + } + msg = json.dumps(manifest, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + if self.server.debug: + print('Sent manifest: ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show logout', 'send manifest') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -3515,97 +3610,8 @@ class PubServer(BaseHTTPRequestHandler): # manifest for progressive web apps if '/manifest.json' in self.path: - app1 = "https://f-droid.org/en/packages/eu.siacs.conversations" - app2 = "https://staging.f-droid.org/en/packages/im.vector.app" - manifest = { - "name": "Epicyon", - "short_name": "Epicyon", - "start_url": "/index.html", - "display": "standalone", - "background_color": "black", - "theme_color": "grey", - "orientation": "portrait-primary", - "categories": ["microblog", "fediverse", "activitypub"], - "screenshots": [ - { - "src": "/mobile.jpg", - "sizes": "418x851", - "type": "image/jpeg" - }, - { - "src": "/mobile_person.jpg", - "sizes": "429x860", - "type": "image/jpeg" - }, - { - "src": "/mobile_search.jpg", - "sizes": "422x861", - "type": "image/jpeg" - } - ], - "icons": [ - { - "src": "/logo72.png", - "type": "image/png", - "sizes": "72x72" - }, - { - "src": "/logo96.png", - "type": "image/png", - "sizes": "96x96" - }, - { - "src": "/logo128.png", - "type": "image/png", - "sizes": "128x128" - }, - { - "src": "/logo144.png", - "type": "image/png", - "sizes": "144x144" - }, - { - "src": "/logo152.png", - "type": "image/png", - "sizes": "152x152" - }, - { - "src": "/logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "/logo256.png", - "type": "image/png", - "sizes": "256x256" - }, - { - "src": "/logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "related_applications": [ - { - "platform": "fdroid", - "url": app1 - }, - { - "platform": "fdroid", - "url": app2 - } - ] - } - msg = json.dumps(manifest, - ensure_ascii=False).encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - if self.server.debug: - print('Sent manifest: ' + callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show logout', 'send manifest') + self._progressiveWebAppManifest(callingDomain, + GETstartTime, GETtimings) return # favicon image From 853684c333923873cf972250fff20fe4bd9ad1ff Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 15:36:44 +0100 Subject: [PATCH 033/108] Add favicon method --- daemon.py | 96 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/daemon.py b/daemon.py index 3b22becba..a002a017a 100644 --- a/daemon.py +++ b/daemon.py @@ -3530,6 +3530,55 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 'show logout', 'send manifest') + def _getFavicon(self, callingDomain: str, + baseDir: str, debug: bool): + """Return the favicon + """ + favType = 'image/x-icon' + favFilename = 'favicon.ico' + if self._hasAccept(callingDomain): + if 'image/webp' in self.headers['Accept']: + favType = 'image/webp' + favFilename = 'favicon.webp' + # custom favicon + faviconFilename = baseDir + '/' + favFilename + if not os.path.isfile(faviconFilename): + # default favicon + faviconFilename = \ + baseDir + '/img/icons/' + favFilename + if self._etag_exists(faviconFilename): + # The file has not changed + if debug: + print('favicon icon has not changed: ' + callingDomain) + self._304() + return + if self.server.iconsCache.get(favFilename): + favBinary = self.server.iconsCache[favFilename] + self._set_headers_etag(faviconFilename, + favType, + favBinary, None, + callingDomain) + self._write(favBinary) + if debug: + print('Sent favicon from cache: ' + callingDomain) + return + else: + if os.path.isfile(faviconFilename): + with open(faviconFilename, 'rb') as favFile: + favBinary = favFile.read() + self._set_headers_etag(faviconFilename, + favType, + favBinary, None, + callingDomain) + self._write(favBinary) + self.server.iconsCache[favFilename] = favBinary + if self.server.debug: + print('Sent favicon from file: ' + callingDomain) + return + if debug: + print('favicon not sent: ' + callingDomain) + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -3616,51 +3665,8 @@ class PubServer(BaseHTTPRequestHandler): # favicon image if 'favicon.ico' in self.path: - favType = 'image/x-icon' - favFilename = 'favicon.ico' - if self._hasAccept(callingDomain): - if 'image/webp' in self.headers['Accept']: - favType = 'image/webp' - favFilename = 'favicon.webp' - # custom favicon - faviconFilename = \ - self.server.baseDir + '/' + favFilename - if not os.path.isfile(faviconFilename): - # default favicon - faviconFilename = \ - self.server.baseDir + '/img/icons/' + favFilename - if self._etag_exists(faviconFilename): - # The file has not changed - if self.server.debug: - print('favicon icon has not changed: ' + callingDomain) - self._304() - return - if self.server.iconsCache.get(favFilename): - favBinary = self.server.iconsCache[favFilename] - self._set_headers_etag(faviconFilename, - favType, - favBinary, cookie, - callingDomain) - self._write(favBinary) - if self.server.debug: - print('Sent favicon from cache: ' + callingDomain) - return - else: - if os.path.isfile(faviconFilename): - with open(faviconFilename, 'rb') as favFile: - favBinary = favFile.read() - self._set_headers_etag(faviconFilename, - favType, - favBinary, cookie, - callingDomain) - self._write(favBinary) - self.server.iconsCache[favFilename] = favBinary - if self.server.debug: - print('Sent favicon from file: ' + callingDomain) - return - if self.server.debug: - print('favicon not sent: ' + callingDomain) - self._404() + self._getFavicon(callingDomain, self.server.baseDir, + self.server.debug) return # check authorization From 1646d8e40410008b42dc57c12895126239d3ab80 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 15:44:51 +0100 Subject: [PATCH 034/108] Function for getting fonts --- daemon.py | 115 +++++++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/daemon.py b/daemon.py index a002a017a..982ac519f 100644 --- a/daemon.py +++ b/daemon.py @@ -3579,6 +3579,65 @@ class PubServer(BaseHTTPRequestHandler): print('favicon not sent: ' + callingDomain) self._404() + def _getFonts(self, callingDomain: str, path: str, + baseDir: str, debug: bool, + GETstartTime, GETtimings: {}): + """Returns a font + """ + fontStr = path.split('/fonts/')[1] + if fontStr.endswith('.otf') or \ + fontStr.endswith('.ttf') or \ + fontStr.endswith('.woff') or \ + fontStr.endswith('.woff2'): + if fontStr.endswith('.otf'): + fontType = 'font/otf' + elif fontStr.endswith('.ttf'): + fontType = 'font/ttf' + elif fontStr.endswith('.woff'): + fontType = 'font/woff' + else: + fontType = 'font/woff2' + fontFilename = \ + baseDir + '/fonts/' + fontStr + if self._etag_exists(fontFilename): + # The file has not changed + self._304() + return + if self.server.fontsCache.get(fontStr): + fontBinary = self.server.fontsCache[fontStr] + self._set_headers_etag(fontFilename, + fontType, + fontBinary, None, + callingDomain) + self._write(fontBinary) + if debug: + print('font sent from cache: ' + + path + ' ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'hasAccept', + 'send font from cache') + return + else: + if os.path.isfile(fontFilename): + with open(fontFilename, 'rb') as fontFile: + fontBinary = fontFile.read() + self._set_headers_etag(fontFilename, + fontType, + fontBinary, None, + callingDomain) + self._write(fontBinary) + self.server.fontsCache[fontStr] = fontBinary + if debug: + print('font sent from file: ' + + path + ' ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'hasAccept', + 'send font from file') + return + if debug: + print('font not found: ' + path + ' ' + callingDomain) + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -3719,59 +3778,9 @@ class PubServer(BaseHTTPRequestHandler): # get fonts if '/fonts/' in self.path: - fontStr = self.path.split('/fonts/')[1] - if fontStr.endswith('.otf') or \ - fontStr.endswith('.ttf') or \ - fontStr.endswith('.woff') or \ - fontStr.endswith('.woff2'): - if fontStr.endswith('.otf'): - fontType = 'font/otf' - elif fontStr.endswith('.ttf'): - fontType = 'font/ttf' - elif fontStr.endswith('.woff'): - fontType = 'font/woff' - else: - fontType = 'font/woff2' - fontFilename = \ - self.server.baseDir + '/fonts/' + fontStr - if self._etag_exists(fontFilename): - # The file has not changed - self._304() - return - if self.server.fontsCache.get(fontStr): - fontBinary = self.server.fontsCache[fontStr] - self._set_headers_etag(fontFilename, - fontType, - fontBinary, cookie, - callingDomain) - self._write(fontBinary) - if self.server.debug: - print('font sent from cache: ' + - self.path + ' ' + callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'hasAccept', - 'send font from cache') - return - else: - if os.path.isfile(fontFilename): - with open(fontFilename, 'rb') as fontFile: - fontBinary = fontFile.read() - self._set_headers_etag(fontFilename, - fontType, - fontBinary, cookie, - callingDomain) - self._write(fontBinary) - self.server.fontsCache[fontStr] = fontBinary - if self.server.debug: - print('font sent from file: ' + - self.path + ' ' + callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'hasAccept', - 'send font from file') - return - if self.server.debug: - print('font not found: ' + self.path + ' ' + callingDomain) - self._404() + self._getFonts(callingDomain, self.path, + self.server.baseDir, self.server.debug, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 7a864db6aa2b3ac9f634f6f36704841afcf0562c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 16:48:46 +0100 Subject: [PATCH 035/108] Move rss2 feed to its own method --- daemon.py | 103 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/daemon.py b/daemon.py index 982ac519f..e4607279d 100644 --- a/daemon.py +++ b/daemon.py @@ -3638,6 +3638,57 @@ class PubServer(BaseHTTPRequestHandler): print('font not found: ' + path + ' ' + callingDomain) self._404() + def _getRSS2feed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, port: int, proxyType: str, + GETstartTime, GETtimings: {}, + debug: bool): + """Returns an RSS2 feed for the blog + """ + nickname = path.split('/blog/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if not nickname.startswith('rss.'): + if os.path.isdir(self.server.baseDir + + '/accounts/' + nickname + '@' + domain): + if not self.server.session: + print('Starting new session during RSS request') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during RSS request') + self._404() + return + + msg = \ + htmlBlogPageRSS2(authorized, + self.server.session, + baseDir, + httpPrefix, + self.server.translate, + nickname, + domain, + port, + maxPostsInRSSFeed, 1) + if msg is not None: + msg = msg.encode('utf-8') + self._set_headers('text/xml', len(msg), + None, callingDomain) + self._write(msg) + if debug: + print('Sent rss2 feed: ' + + path + ' ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'sharedInbox enabled', + 'blog rss2') + return + if debug: + print('Failed to get rss2 feed: ' + + path + ' ' + callingDomain) + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -3804,49 +3855,15 @@ class PubServer(BaseHTTPRequestHandler): # RSS 2.0 if self.path.startswith('/blog/') and \ self.path.endswith('/rss.xml'): - nickname = self.path.split('/blog/')[1] - if '/' in nickname: - nickname = nickname.split('/')[0] - if not nickname.startswith('rss.'): - if os.path.isdir(self.server.baseDir + - '/accounts/' + nickname + - '@' + self.server.domain): - if not self.server.session: - print('Starting new session during RSS request') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during RSS request') - self._404() - return - - msg = \ - htmlBlogPageRSS2(authorized, - self.server.session, - self.server.baseDir, - self.server.httpPrefix, - self.server.translate, - nickname, - self.server.domain, - self.server.port, - maxPostsInRSSFeed, 1) - if msg is not None: - msg = msg.encode('utf-8') - self._set_headers('text/xml', len(msg), - cookie, callingDomain) - self._write(msg) - if self.server.debug: - print('Sent rss2 feed: ' + - self.path + ' ' + callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'sharedInbox enabled', - 'blog rss2') - return - if self.server.debug: - print('Failed to get rss2 feed: ' + - self.path + ' ' + callingDomain) - self._404() + self._getRSS2feed(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.port, + self.server.proxyType, + GETstartTime, GETtimings, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From a11295572dc83117303f08a85c3a1300678bf361 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 16:55:00 +0100 Subject: [PATCH 036/108] Move rss3 feed to its own method --- daemon.py | 98 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/daemon.py b/daemon.py index e4607279d..ac1521d0d 100644 --- a/daemon.py +++ b/daemon.py @@ -3689,6 +3689,53 @@ class PubServer(BaseHTTPRequestHandler): path + ' ' + callingDomain) self._404() + def _getRSS3feed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, port: int, proxyType: str, + GETstartTime, GETtimings: {}, + debug: bool): + """Returns an RSS3 feed + """ + nickname = path.split('/blog/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if not nickname.startswith('rss.'): + if os.path.isdir(baseDir + + '/accounts/' + nickname + '@' + domain): + if not self.server.session: + print('Starting new session during RSS3 request') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during RSS3 request') + self._404() + return + msg = \ + htmlBlogPageRSS3(authorized, + self.server.session, + baseDir, httpPrefix, + self.server.translate, + nickname, domain, port, + maxPostsInRSSFeed, 1) + if msg is not None: + msg = msg.encode('utf-8') + self._set_headers('text/plain; charset=utf-8', + len(msg), None, callingDomain) + self._write(msg) + if self.server.debug: + print('Sent rss3 feed: ' + + path + ' ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'sharedInbox enabled', + 'blog rss3') + return + if debug: + print('Failed to get rss3 feed: ' + + path + ' ' + callingDomain) + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -3872,48 +3919,15 @@ class PubServer(BaseHTTPRequestHandler): # RSS 3.0 if self.path.startswith('/blog/') and \ self.path.endswith('/rss.txt'): - nickname = self.path.split('/blog/')[1] - if '/' in nickname: - nickname = nickname.split('/')[0] - if not nickname.startswith('rss.'): - if os.path.isdir(self.server.baseDir + - '/accounts/' + nickname + - '@' + self.server.domain): - if not self.server.session: - print('Starting new session during RSS3 request') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during RSS3 request') - self._404() - return - msg = \ - htmlBlogPageRSS3(authorized, - self.server.session, - self.server.baseDir, - self.server.httpPrefix, - self.server.translate, - nickname, - self.server.domain, - self.server.port, - maxPostsInRSSFeed, 1) - if msg is not None: - msg = msg.encode('utf-8') - self._set_headers('text/plain; charset=utf-8', - len(msg), cookie, callingDomain) - self._write(msg) - if self.server.debug: - print('Sent rss3 feed: ' + - self.path + ' ' + callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'sharedInbox enabled', - 'blog rss3') - return - if self.server.debug: - print('Failed to get rss3 feed: ' + - self.path + ' ' + callingDomain) - self._404() + self._getRSS3feed(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.port, + self.server.proxyType, + GETstartTime, GETtimings, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 08578da6e1af9a85dbd2ace20d37c1d6b909023b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 17:13:48 +0100 Subject: [PATCH 037/108] Move person options to its own method --- daemon.py | 154 +++++++++++++++++++++++++++++------------------------- 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/daemon.py b/daemon.py index ac1521d0d..958cbd24e 100644 --- a/daemon.py +++ b/daemon.py @@ -3736,6 +3736,81 @@ class PubServer(BaseHTTPRequestHandler): path + ' ' + callingDomain) self._404() + def _showPersonOptions(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + GETstartTime, GETtimings: {}, + onionDomain: str, i2pDomain: str, + cookie: str, debug: bool): + """Show person options screen + """ + optionsStr = self.path.split('?options=')[1] + originPathStr = self.path.split('?options=')[0] + if ';' in optionsStr: + pageNumber = 1 + optionsList = optionsStr.split(';') + optionsActor = optionsList[0] + optionsPageNumber = optionsList[1] + optionsProfileUrl = optionsList[2] + if optionsPageNumber.isdigit(): + pageNumber = int(optionsPageNumber) + optionsLink = None + if len(optionsList) > 3: + optionsLink = optionsList[3] + donateUrl = None + PGPpubKey = None + PGPfingerprint = None + xmppAddress = None + matrixAddress = None + blogAddress = None + toxAddress = None + ssbAddress = None + emailAddress = None + actorJson = getPersonFromCache(baseDir, + optionsActor, + self.server.personCache, + True) + if actorJson: + donateUrl = getDonationUrl(actorJson) + xmppAddress = getXmppAddress(actorJson) + matrixAddress = getMatrixAddress(actorJson) + ssbAddress = getSSBAddress(actorJson) + blogAddress = getBlogAddress(actorJson) + toxAddress = getToxAddress(actorJson) + emailAddress = getEmailAddress(actorJson) + PGPpubKey = getPGPpubKey(actorJson) + PGPfingerprint = getPGPfingerprint(actorJson) + msg = htmlPersonOptions(self.server.translate, + baseDir, domain, + originPathStr, + optionsActor, + optionsProfileUrl, + optionsLink, + pageNumber, donateUrl, + xmppAddress, matrixAddress, + ssbAddress, blogAddress, + toxAddress, + PGPpubKey, PGPfingerprint, + emailAddress).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'registered devices done', + 'person options') + return + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif callingDomain.endswith('.i2p') and i2pDomain: + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + else: + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + self._redirect_headers(originPathStrAbsolute, cookie, + callingDomain) + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4050,76 +4125,15 @@ class PubServer(BaseHTTPRequestHandler): if htmlGET and '/users/' in self.path: # show the person options screen with view/follow/block/report if '?options=' in self.path: - optionsStr = self.path.split('?options=')[1] - originPathStr = self.path.split('?options=')[0] - if ';' in optionsStr: - pageNumber = 1 - optionsList = optionsStr.split(';') - optionsActor = optionsList[0] - optionsPageNumber = optionsList[1] - optionsProfileUrl = optionsList[2] - if optionsPageNumber.isdigit(): - pageNumber = int(optionsPageNumber) - optionsLink = None - if len(optionsList) > 3: - optionsLink = optionsList[3] - donateUrl = None - PGPpubKey = None - PGPfingerprint = None - xmppAddress = None - matrixAddress = None - blogAddress = None - toxAddress = None - ssbAddress = None - emailAddress = None - actorJson = getPersonFromCache(self.server.baseDir, - optionsActor, - self.server.personCache, - True) - if actorJson: - donateUrl = getDonationUrl(actorJson) - xmppAddress = getXmppAddress(actorJson) - matrixAddress = getMatrixAddress(actorJson) - ssbAddress = getSSBAddress(actorJson) - blogAddress = getBlogAddress(actorJson) - toxAddress = getToxAddress(actorJson) - emailAddress = getEmailAddress(actorJson) - PGPpubKey = getPGPpubKey(actorJson) - PGPfingerprint = getPGPfingerprint(actorJson) - msg = htmlPersonOptions(self.server.translate, - self.server.baseDir, - self.server.domain, - originPathStr, - optionsActor, - optionsProfileUrl, - optionsLink, - pageNumber, donateUrl, - xmppAddress, matrixAddress, - ssbAddress, blogAddress, - toxAddress, - PGPpubKey, PGPfingerprint, - emailAddress).encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'registered devices done', - 'person options') - return - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStrAbsolute = \ - 'http://' + self.server.onionDomain + originPathStr - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStrAbsolute = \ - 'http://' + self.server.i2pDomain + originPathStr - else: - originPathStrAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + originPathStr - self._redirect_headers(originPathStrAbsolute, cookie, - callingDomain) + self._showPersonOptions(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + GETstartTime, GETtimings, + self.server.onionDomain, + self.server.i2pDomain, + cookie, self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 4c7cbe219bec7a7e843cc148094d648b800798f7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 18:55:13 +0100 Subject: [PATCH 038/108] Move media display to its own method --- daemon.py | 126 +++++++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/daemon.py b/daemon.py index 958cbd24e..b3e7ccda6 100644 --- a/daemon.py +++ b/daemon.py @@ -250,23 +250,23 @@ def readFollowList(filename: str) -> None: class PubServer(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' - def _pathIsImage(self) -> bool: - if self.path.endswith('.png') or \ - self.path.endswith('.jpg') or \ - self.path.endswith('.gif') or \ - self.path.endswith('.webp'): + def _pathIsImage(self, path: str) -> bool: + if path.endswith('.png') or \ + path.endswith('.jpg') or \ + path.endswith('.gif') or \ + path.endswith('.webp'): return True return False - def _pathIsVideo(self) -> bool: - if self.path.endswith('.ogv') or \ - self.path.endswith('.mp4'): + def _pathIsVideo(self, path: str) -> bool: + if path.endswith('.ogv') or \ + path.endswith('.mp4'): return True return False - def _pathIsAudio(self) -> bool: - if self.path.endswith('.ogg') or \ - self.path.endswith('.mp3'): + def _pathIsAudio(self, path: str) -> bool: + if path.endswith('.ogg') or \ + path.endswith('.mp3'): return True return False @@ -3811,6 +3811,52 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(originPathStrAbsolute, cookie, callingDomain) + def _showMedia(self, callingDomain: str, + path: str, baseDir: str, + GETstartTime, GETtimings: {}): + """Returns a media file + """ + if self._pathIsImage(path) or \ + self._pathIsVideo(path) or \ + self._pathIsAudio(path): + mediaStr = path.split('/media/')[1] + mediaFilename = baseDir + '/media/' + mediaStr + if os.path.isfile(mediaFilename): + if self._etag_exists(mediaFilename): + # The file has not changed + self._304() + return + + mediaFileType = 'image/png' + if mediaFilename.endswith('.png'): + mediaFileType = 'image/png' + elif mediaFilename.endswith('.jpg'): + mediaFileType = 'image/jpeg' + elif mediaFilename.endswith('.gif'): + mediaFileType = 'image/gif' + elif mediaFilename.endswith('.webp'): + mediaFileType = 'image/webp' + elif mediaFilename.endswith('.mp4'): + mediaFileType = 'video/mp4' + elif mediaFilename.endswith('.ogv'): + mediaFileType = 'video/ogv' + elif mediaFilename.endswith('.mp3'): + mediaFileType = 'audio/mpeg' + elif mediaFilename.endswith('.ogg'): + mediaFileType = 'audio/ogg' + + with open(mediaFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self._set_headers_etag(mediaFilename, mediaFileType, + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show emoji done', + 'show media') + return + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4292,7 +4338,7 @@ class PubServer(BaseHTTPRequestHandler): # if not authorized then show the login screen if htmlGET and self.path != '/login' and \ - not self._pathIsImage() and self.path != '/': + not self._pathIsImage(self.path) and self.path != '/': if '/media/' not in self.path and \ '/sharefiles/' not in self.path and \ '/statuses/' not in self.path and \ @@ -4636,7 +4682,7 @@ class PubServer(BaseHTTPRequestHandler): # emoji images if '/emoji/' in self.path: - if self._pathIsImage(): + if self._pathIsImage(self.path): emojiStr = self.path.split('/emoji/')[1] emojiFilename = \ self.server.baseDir + '/emoji/' + emojiStr @@ -4676,47 +4722,9 @@ class PubServer(BaseHTTPRequestHandler): # show media # Note that this comes before the busy flag to avoid conflicts if '/media/' in self.path: - if self._pathIsImage() or \ - self._pathIsVideo() or \ - self._pathIsAudio(): - mediaStr = self.path.split('/media/')[1] - mediaFilename = \ - self.server.baseDir + '/media/' + mediaStr - if os.path.isfile(mediaFilename): - if self._etag_exists(mediaFilename): - # The file has not changed - self._304() - return - - mediaFileType = 'image/png' - if mediaFilename.endswith('.png'): - mediaFileType = 'image/png' - elif mediaFilename.endswith('.jpg'): - mediaFileType = 'image/jpeg' - elif mediaFilename.endswith('.gif'): - mediaFileType = 'image/gif' - elif mediaFilename.endswith('.webp'): - mediaFileType = 'image/webp' - elif mediaFilename.endswith('.mp4'): - mediaFileType = 'video/mp4' - elif mediaFilename.endswith('.ogv'): - mediaFileType = 'video/ogv' - elif mediaFilename.endswith('.mp3'): - mediaFileType = 'audio/mpeg' - elif mediaFilename.endswith('.ogg'): - mediaFileType = 'audio/ogg' - - with open(mediaFilename, 'rb') as avFile: - mediaBinary = avFile.read() - self._set_headers_etag(mediaFilename, mediaFileType, - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show emoji done', - 'show media') - return - self._404() + self._showMedia(callingDomain, + self.path, self.server.baseDir, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -4726,7 +4734,7 @@ class PubServer(BaseHTTPRequestHandler): # show shared item images # Note that this comes before the busy flag to avoid conflicts if '/sharefiles/' in self.path: - if self._pathIsImage(): + if self._pathIsImage(self.path): mediaStr = self.path.split('/sharefiles/')[1] mediaFilename = \ self.server.baseDir + '/sharefiles/' + mediaStr @@ -4858,7 +4866,7 @@ class PubServer(BaseHTTPRequestHandler): # show avatar or background image # Note that this comes before the busy flag to avoid conflicts if '/users/' in self.path: - if self._pathIsImage(): + if self._pathIsImage(self.path): avatarStr = self.path.split('/users/')[1] if '/' in avatarStr and '.temp.' not in self.path: avatarNickname = avatarStr.split('/')[0] @@ -8198,9 +8206,9 @@ class PubServer(BaseHTTPRequestHandler): fileLength = -1 if '/media/' in self.path: - if self._pathIsImage() or \ - self._pathIsVideo() or \ - self._pathIsAudio(): + if self._pathIsImage(self.path) or \ + self._pathIsVideo(self.path) or \ + self._pathIsAudio(self.path): mediaStr = self.path.split('/media/')[1] mediaFilename = \ self.server.baseDir + '/media/' + mediaStr From eb40fc63c2d32a36fb7b7785ad07c1edfb72c42a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 19:00:40 +0100 Subject: [PATCH 039/108] Move emoji display to its own method --- daemon.py | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/daemon.py b/daemon.py index b3e7ccda6..438b8699f 100644 --- a/daemon.py +++ b/daemon.py @@ -3857,6 +3857,42 @@ class PubServer(BaseHTTPRequestHandler): return self._404() + def _showEmoji(self, callingDomain: str, path: str, + baseDir: str, + GETstartTime, GETtimings: {}): + """Returns an emoji image + """ + if self._pathIsImage(path): + emojiStr = path.split('/emoji/')[1] + emojiFilename = baseDir + '/emoji/' + emojiStr + if os.path.isfile(emojiFilename): + if self._etag_exists(emojiFilename): + # The file has not changed + self._304() + return + + mediaImageType = 'png' + if emojiFilename.endswith('.png'): + mediaImageType = 'png' + elif emojiFilename.endswith('.jpg'): + mediaImageType = 'jpeg' + elif emojiFilename.endswith('.webp'): + mediaImageType = 'webp' + else: + mediaImageType = 'gif' + with open(emojiFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self._set_headers_etag(emojiFilename, + 'image/' + mediaImageType, + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'background shown done', + 'show emoji') + return + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4682,37 +4718,9 @@ class PubServer(BaseHTTPRequestHandler): # emoji images if '/emoji/' in self.path: - if self._pathIsImage(self.path): - emojiStr = self.path.split('/emoji/')[1] - emojiFilename = \ - self.server.baseDir + '/emoji/' + emojiStr - if os.path.isfile(emojiFilename): - if self._etag_exists(emojiFilename): - # The file has not changed - self._304() - return - - mediaImageType = 'png' - if emojiFilename.endswith('.png'): - mediaImageType = 'png' - elif emojiFilename.endswith('.jpg'): - mediaImageType = 'jpeg' - elif emojiFilename.endswith('.webp'): - mediaImageType = 'webp' - else: - mediaImageType = 'gif' - with open(emojiFilename, 'rb') as avFile: - mediaBinary = avFile.read() - self._set_headers_etag(emojiFilename, - 'image/' + mediaImageType, - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'background shown done', - 'show emoji') - return - self._404() + self._showEmoji(callingDomain, self.path, + self.server.baseDir, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 4dbe934d3b9415108bcf7e1d3108ba6c4bbd1413 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 19:15:53 +0100 Subject: [PATCH 040/108] Move icon display to its own method --- daemon.py | 70 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/daemon.py b/daemon.py index 438b8699f..7bc3eb28c 100644 --- a/daemon.py +++ b/daemon.py @@ -3893,6 +3893,42 @@ class PubServer(BaseHTTPRequestHandler): return self._404() + def _showIcon(self, callingDomain: str, path: str, + baseDir: str, + GETstartTime, GETtimings: {}): + """Shows an icon + """ + if path.endswith('.png'): + mediaStr = path.split('/icons/')[1] + mediaFilename = baseDir + '/img/icons/' + mediaStr + if self._etag_exists(mediaFilename): + # The file has not changed + self._304() + return + if self.server.iconsCache.get(mediaStr): + mediaBinary = self.server.iconsCache[mediaStr] + self._set_headers_etag(mediaFilename, + 'image/png', + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + return + else: + if os.path.isfile(mediaFilename): + with open(mediaFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self._set_headers_etag(mediaFilename, + 'image/png', + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self.server.iconsCache[mediaStr] = mediaBinary + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show files done', + 'icon shown') + return + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4782,37 +4818,9 @@ class PubServer(BaseHTTPRequestHandler): # icon images # Note that this comes before the busy flag to avoid conflicts if self.path.startswith('/icons/'): - if self.path.endswith('.png'): - mediaStr = self.path.split('/icons/')[1] - mediaFilename = \ - self.server.baseDir + '/img/icons/' + mediaStr - if self._etag_exists(mediaFilename): - # The file has not changed - self._304() - return - if self.server.iconsCache.get(mediaStr): - mediaBinary = self.server.iconsCache[mediaStr] - self._set_headers_etag(mediaFilename, - 'image/png', - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - return - else: - if os.path.isfile(mediaFilename): - with open(mediaFilename, 'rb') as avFile: - mediaBinary = avFile.read() - self._set_headers_etag(mediaFilename, - 'image/png', - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self.server.iconsCache[mediaStr] = mediaBinary - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show files done', - 'icon shown') - return - self._404() + self._showIcon(callingDomain, self.path, + self.server.baseDir, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 7c2f0cb5d7f1610783a91f7e7d4d88375fc837c1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 19:19:57 +0100 Subject: [PATCH 041/108] Extraneous slash --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 7bc3eb28c..d8d3d325b 100644 --- a/daemon.py +++ b/daemon.py @@ -4831,7 +4831,7 @@ class PubServer(BaseHTTPRequestHandler): # Note that this comes before the busy flag to avoid conflicts if self.path.startswith('/avatars/'): mediaFilename = \ - self.server.baseDir + '/cache/' + self.path + self.server.baseDir + '/cache' + self.path if os.path.isfile(mediaFilename): if self._etag_exists(mediaFilename): # The file has not changed From 9868ba4846bec1158e63145d23148feef6cf4841 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 19:29:05 +0100 Subject: [PATCH 042/108] Move cached avatars display to its own method --- daemon.py | 94 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/daemon.py b/daemon.py index d8d3d325b..2559ad37d 100644 --- a/daemon.py +++ b/daemon.py @@ -3929,6 +3929,54 @@ class PubServer(BaseHTTPRequestHandler): return self._404() + def _showCachedAvatar(self, callingDomain: str, path: str, + baseDir: str, + GETstartTime, GETtimings: {}): + """Shows an avatar image obtained from the cache + """ + mediaFilename = baseDir + '/cache' + path + if os.path.isfile(mediaFilename): + if self._etag_exists(mediaFilename): + # The file has not changed + self._304() + return + with open(mediaFilename, 'rb') as avFile: + mediaBinary = avFile.read() + if mediaFilename.endswith('.png'): + self._set_headers_etag(mediaFilename, + 'image/png', + mediaBinary, None, + callingDomain) + elif mediaFilename.endswith('.jpg'): + self._set_headers_etag(mediaFilename, + 'image/jpeg', + mediaBinary, None, + callingDomain) + elif mediaFilename.endswith('.gif'): + self._set_headers_etag(mediaFilename, + 'image/gif', + mediaBinary, None, + callingDomain) + elif mediaFilename.endswith('.webp'): + self._set_headers_etag(mediaFilename, + 'image/webp', + mediaBinary, None, + callingDomain) + else: + # default to jpeg + self._set_headers_etag(mediaFilename, + 'image/jpeg', + mediaBinary, None, + callingDomain) + # self._404() + return + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'icon shown done', + 'avatar shown') + return + self._404() + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4830,49 +4878,9 @@ class PubServer(BaseHTTPRequestHandler): # cached avatar images # Note that this comes before the busy flag to avoid conflicts if self.path.startswith('/avatars/'): - mediaFilename = \ - self.server.baseDir + '/cache' + self.path - if os.path.isfile(mediaFilename): - if self._etag_exists(mediaFilename): - # The file has not changed - self._304() - return - with open(mediaFilename, 'rb') as avFile: - mediaBinary = avFile.read() - if mediaFilename.endswith('.png'): - self._set_headers_etag(mediaFilename, - 'image/png', - mediaBinary, cookie, - callingDomain) - elif mediaFilename.endswith('.jpg'): - self._set_headers_etag(mediaFilename, - 'image/jpeg', - mediaBinary, cookie, - callingDomain) - elif mediaFilename.endswith('.gif'): - self._set_headers_etag(mediaFilename, - 'image/gif', - mediaBinary, cookie, - callingDomain) - elif mediaFilename.endswith('.webp'): - self._set_headers_etag(mediaFilename, - 'image/webp', - mediaBinary, cookie, - callingDomain) - else: - # default to jpeg - self._set_headers_etag(mediaFilename, - 'image/jpeg', - mediaBinary, cookie, - callingDomain) - # self._404() - return - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'icon shown done', - 'avatar shown') - return - self._404() + self._showCachedAvatar(callingDomain, self.path, + self.server.baseDir, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, From fc062e0342540f616276d4bc6d430e3c43239966 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 20:21:52 +0100 Subject: [PATCH 043/108] Extra invalid nickname --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 1a5e1b708..125906f13 100644 --- a/utils.py +++ b/utils.py @@ -621,7 +621,7 @@ def validNickname(domain: str, nickname: str) -> bool: 'likes', 'users', 'statuses', 'accounts', 'channels', 'profile', 'updates', 'repeat', 'announce', - 'shares', 'fonts', 'icons') + 'shares', 'fonts', 'icons', 'avatars') if nickname in reservedNames: return False return True From a9a51d672c71134bfb855bd36dd4e9ef2ed10519 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:16:21 +0100 Subject: [PATCH 044/108] Move hashtag search to its own method --- daemon.py | 135 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 60 deletions(-) diff --git a/daemon.py b/daemon.py index 2559ad37d..63c63aa8d 100644 --- a/daemon.py +++ b/daemon.py @@ -3977,6 +3977,71 @@ class PubServer(BaseHTTPRequestHandler): return self._404() + def _hashtagSearch(self, callingDomain: str, path: str, + cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}): + """Return the result of a hashtag search + """ + pageNumber = 1 + if '?page=' in path: + pageNumberStr = path.split('?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + hashtag = path.split('/tags/')[1] + if '?page=' in hashtag: + hashtag = hashtag.split('?page=')[0] + if isBlockedHashtag(baseDir, hashtag): + msg = htmlHashtagBlocked(baseDir).encode('utf-8') + self._login_headers('text/html', len(msg), callingDomain) + self._write(msg) + self.server.GETbusy = False + return + nickname = None + if '/users/' in path: + actor = \ + httpPrefix + '://' + domainFull + path + nickname = \ + getNicknameFromActor(actor) + hashtagStr = \ + htmlHashtagSearch(nickname, + domain, port, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + baseDir, hashtag, pageNumber, + maxPostsInFeed, self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain) + if hashtagStr: + msg = hashtagStr.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + else: + originPathStr = path.split('/tags/')[0] + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif (callingDomain.endswith('.i2p') and onionDomain): + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + self._redirect_headers(originPathStrAbsolute + '/search', + cookie, callingDomain) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'login shown done', + 'hashtag search') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -4991,66 +5056,16 @@ class PubServer(BaseHTTPRequestHandler): # hashtag search if self.path.startswith('/tags/') or \ (authorized and '/tags/' in self.path): - pageNumber = 1 - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - hashtag = self.path.split('/tags/')[1] - if '?page=' in hashtag: - hashtag = hashtag.split('?page=')[0] - if isBlockedHashtag(self.server.baseDir, hashtag): - msg = htmlHashtagBlocked(self.server.baseDir).encode('utf-8') - self._login_headers('text/html', len(msg), callingDomain) - self._write(msg) - self.server.GETbusy = False - return - nickname = None - if '/users/' in self.path: - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + self.path - nickname = \ - getNicknameFromActor(actor) - hashtagStr = \ - htmlHashtagSearch(nickname, - self.server.domain, self.server.port, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.baseDir, hashtag, pageNumber, - maxPostsInFeed, self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.httpPrefix, - self.server.projectVersion, - self.server.YTReplacementDomain) - if hashtagStr: - msg = hashtagStr.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - else: - originPathStr = self.path.split('/tags/')[0] - originPathStrAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + originPathStr - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStrAbsolute = 'http://' + \ - self.server.onionDomain + originPathStr - elif (callingDomain.endswith('.i2p') and - self.server.onionDomain): - originPathStrAbsolute = 'http://' + \ - self.server.i2pDomain + originPathStr - self._redirect_headers(originPathStrAbsolute + '/search', - cookie, callingDomain) - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'login shown done', - 'hashtag search') + self._hashtagSearch(self, callingDomain, + self.server.path, cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings) return self._benchmarkGETtimings(GETstartTime, GETtimings, From ea7cea9b6ffc739c2d5a493514099fa0c6167199 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:18:00 +0100 Subject: [PATCH 045/108] path --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 63c63aa8d..3f204ce00 100644 --- a/daemon.py +++ b/daemon.py @@ -5057,7 +5057,7 @@ class PubServer(BaseHTTPRequestHandler): if self.path.startswith('/tags/') or \ (authorized and '/tags/' in self.path): self._hashtagSearch(self, callingDomain, - self.server.path, cookie, + self.path, cookie, self.server.baseDir, self.server.httpPrefix, self.server.domain, From 6b32fdb0a0e4fa842b8fe5aee21f540577c1a051 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:19:57 +0100 Subject: [PATCH 046/108] No self --- daemon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index 3f204ce00..62d3119cf 100644 --- a/daemon.py +++ b/daemon.py @@ -3977,8 +3977,8 @@ class PubServer(BaseHTTPRequestHandler): return self._404() - def _hashtagSearch(self, callingDomain: str, path: str, - cookie: str, + def _hashtagSearch(self, callingDomain: str, + path: str, cookie: str, baseDir: str, httpPrefix: str, domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, @@ -5056,7 +5056,7 @@ class PubServer(BaseHTTPRequestHandler): # hashtag search if self.path.startswith('/tags/') or \ (authorized and '/tags/' in self.path): - self._hashtagSearch(self, callingDomain, + self._hashtagSearch(callingDomain, self.path, cookie, self.server.baseDir, self.server.httpPrefix, From 3fbc75ae976d5d5dbadea63eecb2c363f34e996b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:50:43 +0100 Subject: [PATCH 047/108] Move announce button to its own method --- daemon.py | 198 +++++++++++++++++++++++++++++------------------------- 1 file changed, 107 insertions(+), 91 deletions(-) diff --git a/daemon.py b/daemon.py index 62d3119cf..c8c0c336a 100644 --- a/daemon.py +++ b/daemon.py @@ -4042,6 +4042,100 @@ class PubServer(BaseHTTPRequestHandler): 'login shown done', 'hashtag search') + def _announceButton(self, callingDomain: str, path: str, + baseDir: str, + cookie: str, proxyType: str, + httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + repeatPrivate: bool, debug: bool): + """The announce/repeat button was pressed on a post + """ + pageNumber = 1 + repeatUrl = path.split('?repeat=')[1] + if '?' in repeatUrl: + repeatUrl = repeatUrl.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('?repeat=')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber), cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during repeat button') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during repeat button') + self._404() + self.server.GETbusy = False + return + self.server.actorRepeat = path.split('?actor=')[1] + announceToStr = \ + httpPrefix + '://' + domainFull + '/users/' + \ + self.postToNickname + '/followers' + if not repeatPrivate: + announceToStr = 'https://www.w3.org/ns/activitystreams#Public' + announceJson = \ + createAnnounce(self.server.session, + baseDir, + self.server.federationList, + self.postToNickname, + domain, port, + announceToStr, + None, httpPrefix, + repeatUrl, False, False, + self.server.sendThreads, + self.server.postLog, + self.server.personCache, + self.server.cachedWebfingers, + debug, + self.server.projectVersion) + if announceJson: + self._postToOutboxThread(announceJson) + self.server.GETbusy = False + actorAbsolute = httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + + timelineStr + '?page=' + + str(pageNumber) + + timelineBookmark, cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'emoji search shown done', + 'show announce') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5217,98 +5311,20 @@ class PubServer(BaseHTTPRequestHandler): if htmlGET and '?repeatprivate=' in self.path: repeatPrivate = True self.path = self.path.replace('?repeatprivate=', '?repeat=') - # announce/repeat from the web interface + # announce/repeat button was pressed if htmlGET and '?repeat=' in self.path: - pageNumber = 1 - repeatUrl = self.path.split('?repeat=')[1] - if '?' in repeatUrl: - repeatUrl = repeatUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = self.path.split('?repeat=')[0] - self.postToNickname = getNicknameFromActor(actor) - if not self.postToNickname: - print('WARN: unable to find nickname in ' + actor) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull+actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber), cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during repeat button') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during repeat button') - self._404() - self.server.GETbusy = False - return - self.server.actorRepeat = self.path.split('?actor=')[1] - announceToStr = \ - self.server.httpPrefix + '://' + \ - self.server.domain + '/users/' + \ - self.postToNickname + '/followers' - if not repeatPrivate: - announceToStr = 'https://www.w3.org/ns/activitystreams#Public' - announceJson = \ - createAnnounce(self.server.session, - self.server.baseDir, - self.server.federationList, - self.postToNickname, - self.server.domain, self.server.port, - announceToStr, - None, self.server.httpPrefix, - repeatUrl, False, False, - self.server.sendThreads, - self.server.postLog, - self.server.personCache, - self.server.cachedWebfingers, - self.server.debug, - self.server.projectVersion) - if announceJson: - self._postToOutboxThread(announceJson) - self.server.GETbusy = False - actorAbsolute = self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + - timelineStr + '?page=' + - str(pageNumber) + - timelineBookmark, cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'emoji search shown done', - 'show announce') + self._announceButton(callingDomain, self.path, + self.server.baseDir, + cookie, self.server.proxyType, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + repeatPrivate, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From d19e345e47d1bbab529c3fc5ab12b7c438c1fc80 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:58:18 +0100 Subject: [PATCH 048/108] Move undo announce to its own method --- daemon.py | 187 +++++++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 85 deletions(-) diff --git a/daemon.py b/daemon.py index c8c0c336a..5691f00c8 100644 --- a/daemon.py +++ b/daemon.py @@ -4136,6 +4136,96 @@ class PubServer(BaseHTTPRequestHandler): 'emoji search shown done', 'show announce') + def _undoAnnounceButton(self, callingDomain: str, path: str, + baseDir: str, + cookie: str, proxyType: str, + httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + repeatPrivate: bool, debug: bool): + """Undo announce/repeat button was pressed + """ + pageNumber = 1 + repeatUrl = path.split('?unrepeat=')[1] + if '?' in repeatUrl: + repeatUrl = repeatUrl.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('?unrepeat=')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + + timelineStr + '?page=' + + str(pageNumber), cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during undo repeat') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during undo repeat') + self._404() + self.server.GETbusy = False + return + undoAnnounceActor = \ + httpPrefix + '://' + domainFull + \ + '/users/' + self.postToNickname + unRepeatToStr = 'https://www.w3.org/ns/activitystreams#Public' + newUndoAnnounce = { + "@context": "https://www.w3.org/ns/activitystreams", + 'actor': undoAnnounceActor, + 'type': 'Undo', + 'cc': [undoAnnounceActor+'/followers'], + 'to': [unRepeatToStr], + 'object': { + 'actor': undoAnnounceActor, + 'cc': [undoAnnounceActor+'/followers'], + 'object': repeatUrl, + 'to': [unRepeatToStr], + 'type': 'Announce' + } + } + self._postToOutboxThread(newUndoAnnounce) + self.server.GETbusy = False + actorAbsolute = httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + + timelineStr + '?page=' + + str(pageNumber) + + timelineBookmark, cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show announce done', + 'unannounce') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5336,91 +5426,18 @@ class PubServer(BaseHTTPRequestHandler): # undo an announce/repeat from the web interface if htmlGET and '?unrepeat=' in self.path: - pageNumber = 1 - repeatUrl = self.path.split('?unrepeat=')[1] - if '?' in repeatUrl: - repeatUrl = repeatUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = self.path.split('?unrepeat=')[0] - self.postToNickname = getNicknameFromActor(actor) - if not self.postToNickname: - print('WARN: unable to find nickname in ' + actor) - self.server.GETbusy = False - actorAbsolute = self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + - timelineStr + '?page=' + - str(pageNumber), cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during undo repeat') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during undo repeat') - self._404() - self.server.GETbusy = False - return - undoAnnounceActor = \ - self.server.httpPrefix + '://' + self.server.domainFull + \ - '/users/' + self.postToNickname - unRepeatToStr = 'https://www.w3.org/ns/activitystreams#Public' - newUndoAnnounce = { - "@context": "https://www.w3.org/ns/activitystreams", - 'actor': undoAnnounceActor, - 'type': 'Undo', - 'cc': [undoAnnounceActor+'/followers'], - 'to': [unRepeatToStr], - 'object': { - 'actor': undoAnnounceActor, - 'cc': [undoAnnounceActor+'/followers'], - 'object': repeatUrl, - 'to': [unRepeatToStr], - 'type': 'Announce' - } - } - self._postToOutboxThread(newUndoAnnounce) - self.server.GETbusy = False - actorAbsolute = self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.onionDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + - timelineStr + '?page=' + - str(pageNumber) + - timelineBookmark, cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show announce done', - 'unannounce') + self._announceButton(callingDomain, self.path, + self.server.baseDir, + cookie, self.server.proxyType, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + repeatPrivate, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From e22e31f1316f3360e53349ceb97c1e2e64dd1482 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 22:58:52 +0100 Subject: [PATCH 049/108] Undo --- daemon.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/daemon.py b/daemon.py index 5691f00c8..f9978163c 100644 --- a/daemon.py +++ b/daemon.py @@ -5426,18 +5426,18 @@ class PubServer(BaseHTTPRequestHandler): # undo an announce/repeat from the web interface if htmlGET and '?unrepeat=' in self.path: - self._announceButton(callingDomain, self.path, - self.server.baseDir, - cookie, self.server.proxyType, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - GETstartTime, GETtimings, - repeatPrivate, - self.server.debug) + self._undoAnnounceButton(callingDomain, self.path, + self.server.baseDir, + cookie, self.server.proxyType, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + repeatPrivate, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From ca2db38b953f0ebfddce84a4882ca18b18d844d1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 23:23:06 +0100 Subject: [PATCH 050/108] Move follow approve to its own method --- daemon.py | 107 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/daemon.py b/daemon.py index f9978163c..6ecf92622 100644 --- a/daemon.py +++ b/daemon.py @@ -4226,6 +4226,56 @@ class PubServer(BaseHTTPRequestHandler): 'show announce done', 'unannounce') + def _followApproveButton(self, callingDomain: str, path: str, + cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, debug: bool): + """Follow approve button was pressed + """ + originPathStr = path.split('/followapprove=')[0] + followerNickname = originPathStr.replace('/users/', '') + followingHandle = path.split('/followapprove=')[1] + if '@' in followingHandle: + if not self.server.session: + print('Starting new session during follow approval') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during follow approval') + self._404() + self.server.GETbusy = False + return + manualApproveFollowRequest(self.server.session, + baseDir, httpPrefix, + followerNickname, + domain, port, + followingHandle, + self.server.federationList, + self.server.sendThreads, + self.server.postLog, + self.server.cachedWebfingers, + self.server.personCache, + self.server.acceptedCaps, + debug, + self.server.projectVersion) + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + self._redirect_headers(originPathStrAbsolute, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unannounce done', + 'follow approve shown') + self.server.GETbusy = False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5447,51 +5497,18 @@ class PubServer(BaseHTTPRequestHandler): # send a follow request approval from the web interface if authorized and '/followapprove=' in self.path and \ self.path.startswith('/users/'): - originPathStr = self.path.split('/followapprove=')[0] - followerNickname = originPathStr.replace('/users/', '') - followingHandle = self.path.split('/followapprove=')[1] - if '@' in followingHandle: - if not self.server.session: - print('Starting new session during follow approval') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during follow approval') - self._404() - self.server.GETbusy = False - return - manualApproveFollowRequest(self.server.session, - self.server.baseDir, - self.server.httpPrefix, - followerNickname, - self.server.domain, - self.server.port, - followingHandle, - self.server.federationList, - self.server.sendThreads, - self.server.postLog, - self.server.cachedWebfingers, - self.server.personCache, - self.server.acceptedCaps, - self.server.debug, - self.server.projectVersion) - originPathStrAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + originPathStr - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStrAbsolute = \ - 'http://' + self.server.onionDomain + originPathStr - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStrAbsolute = \ - 'http://' + self.server.i2pDomain + originPathStr - self._redirect_headers(originPathStrAbsolute, - cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'unannounce done', - 'follow approve shown') - self.server.GETbusy = False + self._followApproveButton(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 3847c10731019fbb5c43a341cc318ac36f5daead Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 31 Aug 2020 23:29:40 +0100 Subject: [PATCH 051/108] Move follow deny to its own method --- daemon.py | 87 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/daemon.py b/daemon.py index 6ecf92622..10d8c9f14 100644 --- a/daemon.py +++ b/daemon.py @@ -4276,6 +4276,46 @@ class PubServer(BaseHTTPRequestHandler): 'follow approve shown') self.server.GETbusy = False + def _followDenyButton(self, callingDomain: str, path: str, + cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, debug: bool): + """Follow deny button was pressed + """ + originPathStr = path.split('/followdeny=')[0] + followerNickname = originPathStr.replace('/users/', '') + followingHandle = path.split('/followdeny=')[1] + if '@' in followingHandle: + manualDenyFollowRequest(self.server.session, + baseDir, httpPrefix, + followerNickname, + domain, port, + followingHandle, + self.server.federationList, + self.server.sendThreads, + self.server.postLog, + self.server.cachedWebfingers, + self.server.personCache, + debug, + self.server.projectVersion) + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif callingDomain.endswith('.i2p') and i2pDomain: + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + self._redirect_headers(originPathStrAbsolute, + cookie, callingDomain) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'follow approve done', + 'follow deny shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5518,41 +5558,18 @@ class PubServer(BaseHTTPRequestHandler): # deny a follow request from the web interface if authorized and '/followdeny=' in self.path and \ self.path.startswith('/users/'): - originPathStr = self.path.split('/followdeny=')[0] - followerNickname = originPathStr.replace('/users/', '') - followingHandle = self.path.split('/followdeny=')[1] - if '@' in followingHandle: - manualDenyFollowRequest(self.server.session, - self.server.baseDir, - self.server.httpPrefix, - followerNickname, - self.server.domain, - self.server.port, - followingHandle, - self.server.federationList, - self.server.sendThreads, - self.server.postLog, - self.server.cachedWebfingers, - self.server.personCache, - self.server.debug, - self.server.projectVersion) - originPathStrAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + originPathStr - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - originPathStrAbsolute = 'http://' + \ - self.server.onionDomain + originPathStr - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - originPathStrAbsolute = 'http://' + \ - self.server.i2pDomain + originPathStr - self._redirect_headers(originPathStrAbsolute, - cookie, callingDomain) - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'follow approve done', - 'follow deny shown') + self._followDenyButton(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 4bf4e93d28b94890d3aeb58066e13d44801586a1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:03:50 +0100 Subject: [PATCH 052/108] Move like button to its own method --- daemon.py | 213 +++++++++++++++++++++++++++++------------------------- 1 file changed, 114 insertions(+), 99 deletions(-) diff --git a/daemon.py b/daemon.py index 10d8c9f14..b5274c6d0 100644 --- a/daemon.py +++ b/daemon.py @@ -4316,6 +4316,109 @@ class PubServer(BaseHTTPRequestHandler): 'follow approve done', 'follow deny shown') + def _likeButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Press the like button + """ + pageNumber = 1 + likeUrl = path.split('?like=')[1] + if '?' in likeUrl: + likeUrl = likeUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + actor = path.split('?like=')[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 = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber) + + timelineBookmark, cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during like') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session during like') + self._404() + self.server.GETbusy = False + return + likeActor = \ + httpPrefix + '://' + \ + domainFull + '/users/' + self.postToNickname + actorLiked = path.split('?actor=')[1] + if '?' in actorLiked: + actorLiked = actorLiked.split('?')[0] + likeJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Like', + 'actor': likeActor, + 'to': [actorLiked], + 'object': likeUrl + } + # directly like the post file + likedPostFilename = locatePost(baseDir, + self.postToNickname, + domain, + likeUrl) + if likedPostFilename: + if debug: + print('Updating likes for ' + likedPostFilename) + updateLikesCollection(self.server.recentPostsCache, + baseDir, + likedPostFilename, likeUrl, + likeActor, domain, + debug) + else: + print('WARN: unable to locate file for liked post ' + + likeUrl) + # send out the like to followers + self._postToOutbox(likeJson, self.server.projectVersion) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and i2pDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber) + + timelineBookmark, cookie, + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'follow deny done', + 'like shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5578,105 +5681,17 @@ class PubServer(BaseHTTPRequestHandler): # like from the web interface icon if htmlGET and '?like=' in self.path: - pageNumber = 1 - likeUrl = self.path.split('?like=')[1] - if '?' in likeUrl: - likeUrl = likeUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - actor = self.path.split('?like=')[0] - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.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.server.httpPrefix + '://' + \ - self.server.domainFull+actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber) + - timelineBookmark, cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during like') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session during like') - self._404() - self.server.GETbusy = False - return - likeActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + self.postToNickname - actorLiked = self.path.split('?actor=')[1] - if '?' in actorLiked: - actorLiked = actorLiked.split('?')[0] - likeJson = { - "@context": "https://www.w3.org/ns/activitystreams", - 'type': 'Like', - 'actor': likeActor, - 'to': [actorLiked], - 'object': likeUrl - } - # directly like the post file - likedPostFilename = locatePost(self.server.baseDir, - self.postToNickname, - self.server.domain, - likeUrl) - if likedPostFilename: - if self.server.debug: - print('Updating likes for ' + likedPostFilename) - updateLikesCollection(self.server.recentPostsCache, - self.server.baseDir, - likedPostFilename, likeUrl, - likeActor, self.server.domain, - self.server.debug) - else: - print('WARN: unable to locate file for liked post ' + - likeUrl) - # send out the like to followers - self._postToOutbox(likeJson, self.server.projectVersion) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber) + - timelineBookmark, cookie, - callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'follow deny done', - 'like shown') + self._likeButton(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 7caee9bf30ca66fda051e72675414ce3bd66ab3d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:20:13 +0100 Subject: [PATCH 053/108] Move unlike button to its own method --- daemon.py | 209 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 110 insertions(+), 99 deletions(-) diff --git a/daemon.py b/daemon.py index b5274c6d0..253b8f374 100644 --- a/daemon.py +++ b/daemon.py @@ -4419,6 +4419,106 @@ class PubServer(BaseHTTPRequestHandler): 'follow deny done', 'like shown') + def _undoLikeButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """A button is pressed to undo + """ + pageNumber = 1 + likeUrl = path.split('?unlike=')[1] + if '?' in likeUrl: + likeUrl = likeUrl.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('?unlike=')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif (callingDomain.endswith('.i2p') and onionDomain): + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber), cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during undo like') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during undo like') + self._404() + self.server.GETbusy = False + return + undoActor = \ + httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + actorLiked = path.split('?actor=')[1] + if '?' in actorLiked: + actorLiked = actorLiked.split('?')[0] + undoLikeJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Undo', + 'actor': undoActor, + 'to': [actorLiked], + 'object': { + 'type': 'Like', + 'actor': undoActor, + 'to': [actorLiked], + 'object': likeUrl + } + } + # directly undo the like within the post file + likedPostFilename = locatePost(baseDir, + self.postToNickname, + domain, likeUrl) + if likedPostFilename: + if debug: + print('Removing likes for ' + likedPostFilename) + undoLikesCollectionEntry(self.server.recentPostsCache, + baseDir, + likedPostFilename, likeUrl, + undoActor, domain, debug) + # send out the undo like to followers + self._postToOutbox(undoLikeJson, self.server.projectVersion) + self.server.GETbusy = False + actorAbsolute = httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber) + + timelineBookmark, cookie, + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'like shown done', + 'unlike shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5700,105 +5800,16 @@ class PubServer(BaseHTTPRequestHandler): # undo a like from the web interface icon if htmlGET and '?unlike=' in self.path: - pageNumber = 1 - likeUrl = self.path.split('?unlike=')[1] - if '?' in likeUrl: - likeUrl = likeUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = self.path.split('?unlike=')[0] - self.postToNickname = getNicknameFromActor(actor) - if not self.postToNickname: - print('WARN: unable to find nickname in ' + actor) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.onionDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber), cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during undo like') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during undo like') - self._404() - self.server.GETbusy = False - return - undoActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + self.postToNickname - actorLiked = self.path.split('?actor=')[1] - if '?' in actorLiked: - actorLiked = actorLiked.split('?')[0] - undoLikeJson = { - "@context": "https://www.w3.org/ns/activitystreams", - 'type': 'Undo', - 'actor': undoActor, - 'to': [actorLiked], - 'object': { - 'type': 'Like', - 'actor': undoActor, - 'to': [actorLiked], - 'object': likeUrl - } - } - # directly undo the like within the post file - likedPostFilename = locatePost(self.server.baseDir, - self.postToNickname, - self.server.domain, - likeUrl) - if likedPostFilename: - if self.server.debug: - print('Removing likes for ' + likedPostFilename) - undoLikesCollectionEntry(self.server.recentPostsCache, - self.server.baseDir, - likedPostFilename, likeUrl, - undoActor, self.server.domain, - self.server.debug) - # send out the undo like to followers - self._postToOutbox(undoLikeJson, self.server.projectVersion) - self.server.GETbusy = False - actorAbsolute = self.server.httpPrefix + '://' + \ - self.server.domainFull+actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.onionDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber) + - timelineBookmark, cookie, - callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'like shown done', - 'unlike shown') + self._undoLikeButton(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 443ecd430bd8d0fdb5b6c9ec5d9545e9d2574f91 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:27:58 +0100 Subject: [PATCH 054/108] Move bookmark button to its own method --- daemon.py | 189 +++++++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 87 deletions(-) diff --git a/daemon.py b/daemon.py index 253b8f374..bf154af0c 100644 --- a/daemon.py +++ b/daemon.py @@ -4519,6 +4519,97 @@ class PubServer(BaseHTTPRequestHandler): 'like shown done', 'unlike shown') + def _bookmarkButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Bookmark button was pressed + """ + pageNumber = 1 + bookmarkUrl = path.split('?bookmark=')[1] + if '?' in bookmarkUrl: + bookmarkUrl = bookmarkUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + actor = path.split('?bookmark=')[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 = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber), cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during bookmark') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during bookmark') + self._404() + self.server.GETbusy = False + return + bookmarkActor = \ + httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + ccList = [] + bookmark(self.server.recentPostsCache, + self.server.session, + baseDir, + self.server.federationList, + self.postToNickname, + domain, port, + ccList, + httpPrefix, + bookmarkUrl, bookmarkActor, False, + self.server.sendThreads, + self.server.postLog, + self.server.personCache, + self.server.cachedWebfingers, + self.server.debug, + self.server.projectVersion) + # self._postToOutbox(bookmarkJson, self.server.projectVersion) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber) + + timelineBookmark, cookie, + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unlike shown done', + 'bookmark shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5818,93 +5909,17 @@ class PubServer(BaseHTTPRequestHandler): # bookmark from the web interface icon if htmlGET and '?bookmark=' in self.path: - pageNumber = 1 - bookmarkUrl = self.path.split('?bookmark=')[1] - if '?' in bookmarkUrl: - bookmarkUrl = bookmarkUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - actor = self.path.split('?bookmark=')[0] - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.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.server.httpPrefix + '://' + \ - self.server.domainFull+actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber), cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during bookmark') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during bookmark') - self._404() - self.server.GETbusy = False - return - bookmarkActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + self.postToNickname - ccList = [] - bookmark(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.federationList, - self.postToNickname, - self.server.domain, self.server.port, - ccList, - self.server.httpPrefix, - bookmarkUrl, bookmarkActor, False, - self.server.sendThreads, - self.server.postLog, - self.server.personCache, - self.server.cachedWebfingers, - self.server.debug, - self.server.projectVersion) - # self._postToOutbox(bookmarkJson, self.server.projectVersion) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber) + - timelineBookmark, cookie, - callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'unlike shown done', - 'bookmark shown') + self._bookmarkButton(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, GETtimings, + self.server.proxyType, + cookie, self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 15f5d4088d3191c2b4317a2bc43a527c459f6ced Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:34:52 +0100 Subject: [PATCH 055/108] Move undo bookmark button to its own method --- daemon.py | 188 +++++++++++++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/daemon.py b/daemon.py index bf154af0c..33ad720c6 100644 --- a/daemon.py +++ b/daemon.py @@ -4610,6 +4610,96 @@ class PubServer(BaseHTTPRequestHandler): 'unlike shown done', 'bookmark shown') + def _undoBookmarkButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Button pressed to undo a bookmark + """ + pageNumber = 1 + bookmarkUrl = path.split('?unbookmark=')[1] + if '?' in bookmarkUrl: + bookmarkUrl = bookmarkUrl.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('?unbookmark=')[0] + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber), cookie, + callingDomain) + return + if not self.server.session: + print('Starting new session during undo bookmark') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during undo bookmark') + self._404() + self.server.GETbusy = False + return + undoActor = \ + httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + ccList = [] + undoBookmark(self.server.recentPostsCache, + self.server.session, + baseDir, + self.server.federationList, + self.postToNickname, + domain, port, + ccList, + httpPrefix, + bookmarkUrl, undoActor, False, + self.server.sendThreads, + self.server.postLog, + self.server.personCache, + self.server.cachedWebfingers, + debug, + self.server.projectVersion) + # self._postToOutbox(undoBookmarkJson, self.server.projectVersion) + self.server.GETbusy = False + actorAbsolute = \ + httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute + '/' + timelineStr + + '?page=' + str(pageNumber) + + timelineBookmark, cookie, + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'bookmark shown done', + 'unbookmark shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -5928,93 +6018,17 @@ class PubServer(BaseHTTPRequestHandler): # undo a bookmark from the web interface icon if htmlGET and '?unbookmark=' in self.path: - pageNumber = 1 - bookmarkUrl = self.path.split('?unbookmark=')[1] - if '?' in bookmarkUrl: - bookmarkUrl = bookmarkUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - if '?page=' in self.path: - pageNumberStr = self.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 self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = self.path.split('?unbookmark=')[0] - self.postToNickname = getNicknameFromActor(actor) - if not self.postToNickname: - print('WARN: unable to find nickname in ' + actor) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + \ - self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber), cookie, - callingDomain) - return - if not self.server.session: - print('Starting new session during undo bookmark') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during undo bookmark') - self._404() - self.server.GETbusy = False - return - undoActor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + self.postToNickname - ccList = [] - undoBookmark(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.federationList, - self.postToNickname, - self.server.domain, self.server.port, - ccList, - self.server.httpPrefix, - bookmarkUrl, undoActor, False, - self.server.sendThreads, - self.server.postLog, - self.server.personCache, - self.server.cachedWebfingers, - self.server.debug, - self.server.projectVersion) - # self._postToOutbox(undoBookmarkJson, self.server.projectVersion) - self.server.GETbusy = False - actorAbsolute = \ - self.server.httpPrefix + '://' + self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute + '/' + timelineStr + - '?page=' + str(pageNumber) + - timelineBookmark, cookie, - callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'bookmark shown done', - 'unbookmark shown') + self._undoBookmarkButton(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, GETtimings, + self.server.proxyType, cookie, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 523719688569890c6d4c5db566515058527680c5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:42:44 +0100 Subject: [PATCH 056/108] Move delete button to its own method --- daemon.py | 202 +++++++++++++++++++++++++++++------------------------- 1 file changed, 108 insertions(+), 94 deletions(-) diff --git a/daemon.py b/daemon.py index 33ad720c6..2be0011aa 100644 --- a/daemon.py +++ b/daemon.py @@ -4700,6 +4700,102 @@ class PubServer(BaseHTTPRequestHandler): 'bookmark shown done', 'unbookmark shown') + def _deleteButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Delete button is pressed + """ + if not cookie: + print('ERROR: no cookie given when deleting') + self._400() + self.server.GETbusy = False + return + pageNumber = 1 + 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) + deleteUrl = path.split('?delete=')[1] + if '?' in deleteUrl: + deleteUrl = deleteUrl.split('?')[0] + timelineStr = self.server.defaultTimeline + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + usersPath = path.split('?delete=')[0] + actor = \ + httpPrefix + '://' + domainFull + usersPath + if self.server.allowDeletion or \ + deleteUrl.startswith(actor): + if self.server.debug: + print('DEBUG: deleteUrl=' + deleteUrl) + print('DEBUG: actor=' + actor) + if actor not in deleteUrl: + # You can only delete your own posts + self.server.GETbusy = False + if callingDomain.endswith('.onion') and onionDomain: + actor = 'http://' + onionDomain + usersPath + elif callingDomain.endswith('.i2p') and i2pDomain: + actor = 'http://' + i2pDomain + usersPath + self._redirect_headers(actor + '/' + timelineStr, + cookie, callingDomain) + return + self.postToNickname = getNicknameFromActor(actor) + if not self.postToNickname: + print('WARN: unable to find nickname in ' + actor) + self.server.GETbusy = False + if callingDomain.endswith('.onion') and onionDomain: + actor = 'http://' + onionDomain + usersPath + elif callingDomain.endswith('.i2p') and i2pDomain: + actor = 'http://' + i2pDomain + usersPath + self._redirect_headers(actor + '/' + timelineStr, + cookie, callingDomain) + return + if not self.server.session: + print('Starting new session during delete') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during delete') + self._404() + self.server.GETbusy = False + return + + deleteStr = \ + htmlDeletePost(self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, pageNumber, + self.server.session, baseDir, + deleteUrl, httpPrefix, + __version__, self.server.cachedWebfingers, + self.server.personCache, callingDomain, + self.server.TYReplacementDomain) + if deleteStr: + self._set_headers('text/html', len(deleteStr), + cookie, callingDomain) + self._write(deleteStr.encode('utf-8')) + self.server.GETbusy = False + return + self.server.GETbusy = False + if callingDomain.endswith('.onion') and onionDomain: + actor = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + actor = 'http://' + i2pDomain + usersPath + self._redirect_headers(actor + '/' + timelineStr, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unbookmark shown done', + 'delete shown') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6035,101 +6131,19 @@ class PubServer(BaseHTTPRequestHandler): 'bookmark shown done', 'unbookmark shown done') - # delete a post from the web interface icon + # delete button is pressed on a post if htmlGET and '?delete=' in self.path: - if not cookie: - print('ERROR: no cookie given when deleting') - self._400() - self.server.GETbusy = False - return - pageNumber = 1 - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '?' in pageNumberStr: - pageNumberStr = pageNumberStr.split('?')[0] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - deleteUrl = self.path.split('?delete=')[1] - if '?' in deleteUrl: - deleteUrl = deleteUrl.split('?')[0] - timelineStr = self.server.defaultTimeline - if '?tl=' in self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - usersPath = self.path.split('?delete=')[0] - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - if self.server.allowDeletion or \ - deleteUrl.startswith(actor): - if self.server.debug: - print('DEBUG: deleteUrl=' + deleteUrl) - print('DEBUG: actor=' + actor) - if actor not in deleteUrl: - # You can only delete your own posts - self.server.GETbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actor + '/' + timelineStr, - cookie, callingDomain) - return - self.postToNickname = getNicknameFromActor(actor) - if not self.postToNickname: - print('WARN: unable to find nickname in ' + actor) - self.server.GETbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actor + '/' + timelineStr, - cookie, callingDomain) - return - if not self.server.session: - print('Starting new session during delete') - self.server.session = createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during delete') - self._404() - self.server.GETbusy = False - return - - deleteStr = \ - htmlDeletePost(self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, pageNumber, - self.server.session, self.server.baseDir, - deleteUrl, self.server.httpPrefix, - __version__, self.server.cachedWebfingers, - self.server.personCache, callingDomain, - self.server.TYReplacementDomain) - if deleteStr: - self._set_headers('text/html', len(deleteStr), - cookie, callingDomain) - self._write(deleteStr.encode('utf-8')) - self.server.GETbusy = False - return - self.server.GETbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = 'http://' + self.server.onionDomain + usersPath - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = 'http://' + self.server.i2pDomain + usersPath - self._redirect_headers(actor + '/' + timelineStr, - cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'unbookmark shown done', - 'delete shown') + self._deleteButton(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, GETtimings, + self.server.proxyType, cookie, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From 0bf7d0e4c422f51751c16886eb720570dd9d0332 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:50:34 +0100 Subject: [PATCH 057/108] Move mute button to its own method --- daemon.py | 101 +++++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/daemon.py b/daemon.py index 2be0011aa..6676638b8 100644 --- a/daemon.py +++ b/daemon.py @@ -4796,6 +4796,50 @@ class PubServer(BaseHTTPRequestHandler): 'unbookmark shown done', 'delete shown') + def _muteButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Mute button is pressed + """ + muteUrl = path.split('?mute=')[1] + if '?' in muteUrl: + muteUrl = muteUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + timelineStr = self.server.defaultTimeline + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + actor = \ + httpPrefix + '://' + domainFull + path.split('?mute=')[0] + nickname = getNicknameFromActor(actor) + mutePost(baseDir, nickname, domain, + muteUrl, self.server.recentPostsCache) + self.server.GETbusy = False + if callingDomain.endswith('.onion') and onionDomain: + actor = \ + 'http://' + onionDomain + \ + path.split('?mute=')[0] + elif (callingDomain.endswith('.i2p') and i2pDomain): + actor = \ + 'http://' + i2pDomain + \ + path.split('?mute=')[0] + self._redirect_headers(actor + '/' + + timelineStr + timelineBookmark, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'delete shown done', + 'post muted') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6152,52 +6196,17 @@ class PubServer(BaseHTTPRequestHandler): # The mute button is pressed if htmlGET and '?mute=' in self.path: - pageNumber = 1 - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '?' in pageNumberStr: - pageNumberStr = pageNumberStr.split('?')[0] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - muteUrl = self.path.split('?mute=')[1] - if '?' in muteUrl: - muteUrl = muteUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - timelineStr = self.server.defaultTimeline - if '?tl=' in self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + self.path.split('?mute=')[0] - nickname = getNicknameFromActor(actor) - mutePost(self.server.baseDir, nickname, self.server.domain, - muteUrl, self.server.recentPostsCache) - self.server.GETbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = \ - 'http://' + self.server.onionDomain + \ - self.path.split('?mute=')[0] - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = \ - 'http://' + self.server.i2pDomain + \ - self.path.split('?mute=')[0] - self._redirect_headers(actor + '/' + - timelineStr + timelineBookmark, - cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'delete shown done', - 'post muted') + self._muteButton(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, GETtimings, + self.server.proxyType, cookie, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From c48288055402c4f1d15b10f6f5b11353ebc43c3a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 10:56:10 +0100 Subject: [PATCH 058/108] Move undo mute to its own method --- daemon.py | 102 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 49 deletions(-) diff --git a/daemon.py b/daemon.py index 6676638b8..c23cfb7cb 100644 --- a/daemon.py +++ b/daemon.py @@ -4840,6 +4840,48 @@ class PubServer(BaseHTTPRequestHandler): 'delete shown done', 'post muted') + def _undoMuteButton(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str): + """Undo mute button is pressed + """ + muteUrl = path.split('?unmute=')[1] + if '?' in muteUrl: + muteUrl = muteUrl.split('?')[0] + timelineBookmark = '' + if '?bm=' in path: + timelineBookmark = path.split('?bm=')[1] + if '?' in timelineBookmark: + timelineBookmark = timelineBookmark.split('?')[0] + timelineBookmark = '#' + timelineBookmark + timelineStr = self.server.defaultTimeline + if '?tl=' in path: + timelineStr = path.split('?tl=')[1] + if '?' in timelineStr: + timelineStr = timelineStr.split('?')[0] + actor = \ + httpPrefix + '://' + domainFull + path.split('?unmute=')[0] + nickname = getNicknameFromActor(actor) + unmutePost(baseDir, nickname, domain, + muteUrl, self.server.recentPostsCache) + self.server.GETbusy = False + if callingDomain.endswith('.onion') and onionDomain: + actor = \ + 'http://' + onionDomain + path.split('?unmute=')[0] + elif callingDomain.endswith('.i2p') and i2pDomain: + actor = \ + 'http://' + i2pDomain + path.split('?unmute=')[0] + self._redirect_headers(actor + '/' + timelineStr + + timelineBookmark, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'post muted done', + 'unmute activated') + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6215,55 +6257,17 @@ class PubServer(BaseHTTPRequestHandler): # unmute a post from the web interface icon if htmlGET and '?unmute=' in self.path: - pageNumber = 1 - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '?' in pageNumberStr: - pageNumberStr = pageNumberStr.split('?')[0] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - muteUrl = self.path.split('?unmute=')[1] - if '?' in muteUrl: - muteUrl = muteUrl.split('?')[0] - timelineBookmark = '' - if '?bm=' in self.path: - timelineBookmark = self.path.split('?bm=')[1] - if '?' in timelineBookmark: - timelineBookmark = timelineBookmark.split('?')[0] - timelineBookmark = '#' + timelineBookmark - timelineStr = self.server.defaultTimeline - if '?tl=' in self.path: - timelineStr = self.path.split('?tl=')[1] - if '?' in timelineStr: - timelineStr = timelineStr.split('?')[0] - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + self.path.split('?unmute=')[0] - nickname = getNicknameFromActor(actor) - unmutePost(self.server.baseDir, - nickname, - self.server.domain, - muteUrl, - self.server.recentPostsCache) - self.server.GETbusy = False - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = \ - 'http://' + \ - self.server.onionDomain + self.path.split('?unmute=')[0] - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = \ - 'http://' + \ - self.server.i2pDomain + self.path.split('?unmute=')[0] - self._redirect_headers(actor + '/' + timelineStr + - timelineBookmark, - cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'post muted done', - 'unmute activated') + self._undoMuteButton(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, GETtimings, + self.server.proxyType, cookie, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, From c1ae893bc73c5f3652139173d769c92af2c71abd Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 11:11:51 +0100 Subject: [PATCH 059/108] Move the display of replies to its own method --- daemon.py | 491 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 253 insertions(+), 238 deletions(-) diff --git a/daemon.py b/daemon.py index c23cfb7cb..f7e2eda67 100644 --- a/daemon.py +++ b/daemon.py @@ -4882,6 +4882,246 @@ class PubServer(BaseHTTPRequestHandler): 'post muted done', 'unmute activated') + def _showReplies(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the replies timeline + Returns true if the timeline was shown + """ + if '/statuses/' in path and '/users/' in path: + namedStatus = path.split('/users/')[1] + if '/' in namedStatus: + postSections = namedStatus.split('/') + if len(postSections) >= 4: + if postSections[3].startswith('replies'): + nickname = postSections[0] + statusNumber = postSections[2] + if len(statusNumber) > 10 and \ + statusNumber.isdigit(): + boxname = 'outbox' + # get the replies file + postDir = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain+'/' + \ + boxname + postRepliesFilename = \ + postDir + '/' + \ + httpPrefix + ':##' + \ + domainFull + '#users#' + \ + nickname + '#statuses#' + \ + statusNumber + '.replies' + if not os.path.isfile(postRepliesFilename): + # There are no replies, + # so show empty collection + contextStr = \ + 'https://www.w3.org/ns/activitystreams' + firstStr = \ + httpPrefix + \ + '://' + domainFull + \ + '/users/' + nickname + \ + '/statuses/' + statusNumber + \ + '/replies?page=true' + idStr = \ + httpPrefix + \ + '://' + domainFull + \ + '/users/' + nickname + \ + '/statuses/' + statusNumber + \ + '/replies' + lastStr = \ + httpPrefix + \ + '://' + domainFull + \ + '/users/' + nickname + \ + '/statuses/' + statusNumber + \ + '/replies?page=true' + repliesJson = { + '@context': contextStr, + 'first': firstStr, + 'id': idStr, + 'last': lastStr, + 'totalItems': 0, + 'type': 'OrderedCollection' + } + if self._requestHTTP(): + if not self.server.session: + print('DEBUG: ' + + 'creating new session ' + + 'during get replies') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to ' + + 'create session ' + + 'during get replies') + self._404() + self.server.GETbusy = False + return + recentPostsCache = \ + self.server.recentPostsCache + maxRecentPosts = \ + self.server.maxRecentPosts + translate = \ + self.server.translate + session = \ + self.server.session + cachedWebfingers = \ + self.server.cachedWebfingers + personCache = \ + self.server.personCache + projectVersion = \ + self.server.projectVersion + ytDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlPostReplies(recentPostsCache, + maxRecentPosts, + translate, + baseDir, + session, + cachedWebfingers, + personCache, + nickname, + domain, + port, + repliesJson, + httpPrefix, + projectVersion, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', + len(msg), + cookie, + callingDomain) + self._write(msg) + else: + if self._fetchAuthenticated(): + msg = \ + json.dumps(repliesJson, + ensure_ascii=False) + msg = msg.encode('utf-8') + protocolStr = 'application/json' + self._set_headers(protocolStr, + len(msg), None, + callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + else: + # replies exist. Itterate through the + # text file containing message ids + contextStr = \ + 'https://www.w3.org/ns/activitystreams' + idStr = \ + httpPrefix + \ + '://' + domainFull + \ + '/users/' + nickname + '/statuses/' + \ + statusNumber + '?page=true' + partOfStr = \ + httpPrefix + \ + '://' + domainFull + \ + '/users/' + nickname + \ + '/statuses/' + statusNumber + repliesJson = { + '@context': contextStr, + 'id': idStr, + 'orderedItems': [ + ], + 'partOf': partOfStr, + 'type': 'OrderedCollectionPage' + } + + # populate the items list with replies + populateRepliesJson(baseDir, + nickname, + domain, + postRepliesFilename, + authorized, + repliesJson) + + # send the replies json + if self._requestHTTP(): + if not self.server.session: + print('DEBUG: ' + + 'creating new session ' + + 'during get replies 2') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to ' + + 'create session ' + + 'during get replies 2') + self._404() + self.server.GETbusy = False + return + recentPostsCache = \ + self.server.recentPostsCache + maxRecentPosts = \ + self.server.maxRecentPosts + translate = \ + self.server.translate + session = \ + self.server.session + cachedWebfingers = \ + self.server.cachedWebfingers + personCache = \ + self.server.personCache + projectVersion = \ + self.server.projectVersion + ytDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlPostReplies(recentPostsCache, + maxRecentPosts, + translate, + baseDir, + session, + cachedWebfingers, + personCache, + nickname, + self.server.domain, + self.server.port, + repliesJson, + httpPrefix, + projectVersion, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', + len(msg), + cookie, + callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'indiv' + + 'idual' + + ' post done', + 'post ' + + 'replies ' + + 'done') + else: + if self._fetchAuthenticated(): + msg = \ + json.dumps(repliesJson, + ensure_ascii=False) + msg = msg.encode('utf-8') + protocolStr = 'application/json' + self._set_headers(protocolStr, + len(msg), + None, + callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6581,244 +6821,19 @@ class PubServer(BaseHTTPRequestHandler): # get replies to a post /users/nickname/statuses/number/replies if self.path.endswith('/replies') or '/replies?page=' in self.path: - if '/statuses/' in self.path and '/users/' in self.path: - namedStatus = self.path.split('/users/')[1] - if '/' in namedStatus: - postSections = namedStatus.split('/') - if len(postSections) >= 4: - if postSections[3].startswith('replies'): - nickname = postSections[0] - statusNumber = postSections[2] - if len(statusNumber) > 10 and \ - statusNumber.isdigit(): - boxname = 'outbox' - # get the replies file - postDir = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain+'/' + \ - boxname - postRepliesFilename = \ - postDir + '/' + \ - self.server.httpPrefix + ':##' + \ - self.server.domainFull + '#users#' + \ - nickname + '#statuses#' + \ - statusNumber + '.replies' - if not os.path.isfile(postRepliesFilename): - # There are no replies, - # so show empty collection - contextStr = \ - 'https://www.w3.org/ns/activitystreams' - firstStr = \ - self.server.httpPrefix + \ - '://' + self.server.domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies?page=true' - idStr = \ - self.server.httpPrefix + \ - '://' + self.server.domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies' - lastStr = \ - self.server.httpPrefix + \ - '://' + self.server.domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies?page=true' - repliesJson = { - '@context': contextStr, - 'first': firstStr, - 'id': idStr, - 'last': lastStr, - 'totalItems': 0, - 'type': 'OrderedCollection' - } - if self._requestHTTP(): - if not self.server.session: - print('DEBUG: ' + - 'creating new session ' + - 'during get replies') - proxyType = \ - self.server.proxyType - self.server.session = \ - createSession(proxyType) - if not self.server.session: - print('ERROR: GET failed to ' + - 'create session ' + - 'during get replies') - self._404() - self.server.GETbusy = False - return - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - baseDir = \ - self.server.baseDir - session = \ - self.server.session - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - httpPrefix = \ - self.server.httpPrefix - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlPostReplies(recentPostsCache, - maxRecentPosts, - translate, - baseDir, - session, - cachedWebfingers, - personCache, - nickname, - self.server.domain, - self.server.port, - repliesJson, - httpPrefix, - projectVersion, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - else: - if self._fetchAuthenticated(): - msg = \ - json.dumps(repliesJson, - ensure_ascii=False) - msg = msg.encode('utf-8') - protocolStr = 'application/json' - self._set_headers(protocolStr, - len(msg), None, - callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return - else: - # replies exist. Itterate through the - # text file containing message ids - contextStr = \ - 'https://www.w3.org/ns/activitystreams' - idStr = \ - self.server.httpPrefix + \ - '://' + self.server.domainFull + \ - '/users/' + nickname + '/statuses/' + \ - statusNumber + '?page=true' - partOfStr = \ - self.server.httpPrefix + \ - '://' + self.server.domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber - repliesJson = { - '@context': contextStr, - 'id': idStr, - 'orderedItems': [ - ], - 'partOf': partOfStr, - 'type': 'OrderedCollectionPage' - } - - # populate the items list with replies - populateRepliesJson(self.server.baseDir, - nickname, - self.server.domain, - postRepliesFilename, - authorized, - repliesJson) - - # send the replies json - if self._requestHTTP(): - if not self.server.session: - print('DEBUG: ' + - 'creating new session ' + - 'during get replies 2') - proxyType = self.server.proxyType - self.server.session = \ - createSession(proxyType) - if not self.server.session: - print('ERROR: GET failed to ' + - 'create session ' + - 'during get replies 2') - self._404() - self.server.GETbusy = False - return - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - baseDir = \ - self.server.baseDir - session = \ - self.server.session - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - httpPrefix = \ - self.server.httpPrefix - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlPostReplies(recentPostsCache, - maxRecentPosts, - translate, - baseDir, - session, - cachedWebfingers, - personCache, - nickname, - self.server.domain, - self.server.port, - repliesJson, - httpPrefix, - projectVersion, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'indiv' + - 'idual' + - ' post done', - 'post ' + - 'replies ' + - 'done') - else: - if self._fetchAuthenticated(): - msg = \ - json.dumps(repliesJson, - ensure_ascii=False) - msg = msg.encode('utf-8') - protocolStr = 'application/json' - self._set_headers(protocolStr, - len(msg), - None, - callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return + if self._showReplies(authorized, + 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, GETtimings, + self.server.proxyType, cookie, + self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'individual post done', From 20f33a580fca2af413b78752d0571be912c18752 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 11:24:58 +0100 Subject: [PATCH 060/108] Reduce indentation --- daemon.py | 403 ++++++++++++++++++++++++------------------------------ 1 file changed, 178 insertions(+), 225 deletions(-) diff --git a/daemon.py b/daemon.py index f7e2eda67..373a5d1bb 100644 --- a/daemon.py +++ b/daemon.py @@ -4893,233 +4893,186 @@ class PubServer(BaseHTTPRequestHandler): """Shows the replies timeline Returns true if the timeline was shown """ - if '/statuses/' in path and '/users/' in path: - namedStatus = path.split('/users/')[1] - if '/' in namedStatus: - postSections = namedStatus.split('/') - if len(postSections) >= 4: - if postSections[3].startswith('replies'): - nickname = postSections[0] - statusNumber = postSections[2] - if len(statusNumber) > 10 and \ - statusNumber.isdigit(): - boxname = 'outbox' - # get the replies file - postDir = \ - baseDir + '/accounts/' + \ - nickname + '@' + domain+'/' + \ - boxname - postRepliesFilename = \ - postDir + '/' + \ - httpPrefix + ':##' + \ - domainFull + '#users#' + \ - nickname + '#statuses#' + \ - statusNumber + '.replies' - if not os.path.isfile(postRepliesFilename): - # There are no replies, - # so show empty collection - contextStr = \ - 'https://www.w3.org/ns/activitystreams' - firstStr = \ - httpPrefix + \ - '://' + domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies?page=true' - idStr = \ - httpPrefix + \ - '://' + domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies' - lastStr = \ - httpPrefix + \ - '://' + domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber + \ - '/replies?page=true' - repliesJson = { - '@context': contextStr, - 'first': firstStr, - 'id': idStr, - 'last': lastStr, - 'totalItems': 0, - 'type': 'OrderedCollection' - } - if self._requestHTTP(): - if not self.server.session: - print('DEBUG: ' + - 'creating new session ' + - 'during get replies') - self.server.session = \ - createSession(proxyType) - if not self.server.session: - print('ERROR: GET failed to ' + - 'create session ' + - 'during get replies') - self._404() - self.server.GETbusy = False - return - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - session = \ - self.server.session - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlPostReplies(recentPostsCache, - maxRecentPosts, - translate, - baseDir, - session, - cachedWebfingers, - personCache, - nickname, - domain, - port, - repliesJson, - httpPrefix, - projectVersion, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - else: - if self._fetchAuthenticated(): - msg = \ - json.dumps(repliesJson, - ensure_ascii=False) - msg = msg.encode('utf-8') - protocolStr = 'application/json' - self._set_headers(protocolStr, - len(msg), None, - callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return True - else: - # replies exist. Itterate through the - # text file containing message ids - contextStr = \ - 'https://www.w3.org/ns/activitystreams' - idStr = \ - httpPrefix + \ - '://' + domainFull + \ - '/users/' + nickname + '/statuses/' + \ - statusNumber + '?page=true' - partOfStr = \ - httpPrefix + \ - '://' + domainFull + \ - '/users/' + nickname + \ - '/statuses/' + statusNumber - repliesJson = { - '@context': contextStr, - 'id': idStr, - 'orderedItems': [ - ], - 'partOf': partOfStr, - 'type': 'OrderedCollectionPage' - } + if not ('/statuses/' in path and '/users/' in path): + return False + namedStatus = path.split('/users/')[1] + if '/' not in namedStatus: + return False + postSections = namedStatus.split('/') + if len(postSections) < 4: + return False + if not postSections[3].startswith('replies'): + return False + nickname = postSections[0] + statusNumber = postSections[2] + if not (len(statusNumber) > 10 and statusNumber.isdigit()): + return False - # populate the items list with replies - populateRepliesJson(baseDir, - nickname, - domain, - postRepliesFilename, - authorized, - repliesJson) + boxname = 'outbox' + # get the replies file + postDir = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/' + boxname + postRepliesFilename = \ + postDir + '/' + \ + httpPrefix + ':##' + domainFull + '#users#' + \ + nickname + '#statuses#' + statusNumber + '.replies' + if not os.path.isfile(postRepliesFilename): + # There are no replies, + # so show empty collection + contextStr = \ + 'https://www.w3.org/ns/activitystreams' + firstStr = \ + httpPrefix + '://' + domainFull + '/users/' + nickname + \ + '/statuses/' + statusNumber + '/replies?page=true' + idStr = \ + httpPrefix + '://' + domainFull + '/users/' + nickname + \ + '/statuses/' + statusNumber + '/replies' + lastStr = \ + httpPrefix + '://' + domainFull + '/users/' + nickname + \ + '/statuses/' + statusNumber + '/replies?page=true' + repliesJson = { + '@context': contextStr, + 'first': firstStr, + 'id': idStr, + 'last': lastStr, + 'totalItems': 0, + 'type': 'OrderedCollection' + } + if self._requestHTTP(): + if not self.server.session: + print('DEBUG: creating new session during get replies') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during get replies') + self._404() + self.server.GETbusy = False + return + recentPostsCache = self.server.recentPostsCache + maxRecentPosts = self.server.maxRecentPosts + translate = self.server.translate + session = self.server.session + cachedWebfingers = self.server.cachedWebfingers + personCache = self.server.personCache + projectVersion = self.server.projectVersion + ytDomain = self.server.YTReplacementDomain + msg = \ + htmlPostReplies(recentPostsCache, + maxRecentPosts, + translate, + baseDir, + session, + cachedWebfingers, + personCache, + nickname, + domain, + port, + repliesJson, + httpPrefix, + projectVersion, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + else: + if self._fetchAuthenticated(): + msg = json.dumps(repliesJson, ensure_ascii=False) + msg = msg.encode('utf-8') + protocolStr = 'application/json' + self._set_headers(protocolStr, len(msg), None, + callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + else: + # replies exist. Itterate through the + # text file containing message ids + contextStr = 'https://www.w3.org/ns/activitystreams' + idStr = \ + httpPrefix + '://' + domainFull + \ + '/users/' + nickname + '/statuses/' + \ + statusNumber + '?page=true' + partOfStr = \ + httpPrefix + '://' + domainFull + \ + '/users/' + nickname + '/statuses/' + statusNumber + repliesJson = { + '@context': contextStr, + 'id': idStr, + 'orderedItems': [ + ], + 'partOf': partOfStr, + 'type': 'OrderedCollectionPage' + } - # send the replies json - if self._requestHTTP(): - if not self.server.session: - print('DEBUG: ' + - 'creating new session ' + - 'during get replies 2') - self.server.session = \ - createSession(proxyType) - if not self.server.session: - print('ERROR: GET failed to ' + - 'create session ' + - 'during get replies 2') - self._404() - self.server.GETbusy = False - return - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - session = \ - self.server.session - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlPostReplies(recentPostsCache, - maxRecentPosts, - translate, - baseDir, - session, - cachedWebfingers, - personCache, - nickname, - self.server.domain, - self.server.port, - repliesJson, - httpPrefix, - projectVersion, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'indiv' + - 'idual' + - ' post done', - 'post ' + - 'replies ' + - 'done') - else: - if self._fetchAuthenticated(): - msg = \ - json.dumps(repliesJson, - ensure_ascii=False) - msg = msg.encode('utf-8') - protocolStr = 'application/json' - self._set_headers(protocolStr, - len(msg), - None, - callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return True + # populate the items list with replies + populateRepliesJson(baseDir, + nickname, + domain, + postRepliesFilename, + authorized, + repliesJson) + + # send the replies json + if self._requestHTTP(): + if not self.server.session: + print('DEBUG: creating new session ' + + 'during get replies 2') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to ' + + 'create session ' + + 'during get replies 2') + self._404() + self.server.GETbusy = False + return + recentPostsCache = self.server.recentPostsCache + maxRecentPosts = self.server.maxRecentPosts + translate = self.server.translate + session = self.server.session + cachedWebfingers = self.server.cachedWebfingers + personCache = self.server.personCache + projectVersion = self.server.projectVersion + ytDomain = self.server.YTReplacementDomain + msg = \ + htmlPostReplies(recentPostsCache, + maxRecentPosts, + translate, + baseDir, + session, + cachedWebfingers, + personCache, + nickname, + domain, + port, + repliesJson, + httpPrefix, + projectVersion, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'individual post done', + 'post replies done') + else: + if self._fetchAuthenticated(): + msg = \ + json.dumps(repliesJson, + ensure_ascii=False) + msg = msg.encode('utf-8') + protocolStr = 'application/json' + self._set_headers(protocolStr, len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True return False def do_GET(self): From 0c6aeb67e3549b960a2c0c054c71a6881548a87e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 11:28:27 +0100 Subject: [PATCH 061/108] Tidying --- daemon.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/daemon.py b/daemon.py index 373a5d1bb..9cffbb553 100644 --- a/daemon.py +++ b/daemon.py @@ -4895,12 +4895,15 @@ class PubServer(BaseHTTPRequestHandler): """ if not ('/statuses/' in path and '/users/' in path): return False + namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False + postSections = namedStatus.split('/') if len(postSections) < 4: return False + if not postSections[3].startswith('replies'): return False nickname = postSections[0] @@ -4921,15 +4924,19 @@ class PubServer(BaseHTTPRequestHandler): # so show empty collection contextStr = \ 'https://www.w3.org/ns/activitystreams' + firstStr = \ httpPrefix + '://' + domainFull + '/users/' + nickname + \ '/statuses/' + statusNumber + '/replies?page=true' + idStr = \ httpPrefix + '://' + domainFull + '/users/' + nickname + \ '/statuses/' + statusNumber + '/replies' + lastStr = \ httpPrefix + '://' + domainFull + '/users/' + nickname + \ '/statuses/' + statusNumber + '/replies?page=true' + repliesJson = { '@context': contextStr, 'first': firstStr, @@ -4938,6 +4945,7 @@ class PubServer(BaseHTTPRequestHandler): 'totalItems': 0, 'type': 'OrderedCollection' } + if self._requestHTTP(): if not self.server.session: print('DEBUG: creating new session during get replies') @@ -4991,13 +4999,16 @@ class PubServer(BaseHTTPRequestHandler): # replies exist. Itterate through the # text file containing message ids contextStr = 'https://www.w3.org/ns/activitystreams' + idStr = \ httpPrefix + '://' + domainFull + \ '/users/' + nickname + '/statuses/' + \ statusNumber + '?page=true' + partOfStr = \ httpPrefix + '://' + domainFull + \ '/users/' + nickname + '/statuses/' + statusNumber + repliesJson = { '@context': contextStr, 'id': idStr, @@ -5008,12 +5019,9 @@ class PubServer(BaseHTTPRequestHandler): } # populate the items list with replies - populateRepliesJson(baseDir, - nickname, - domain, + populateRepliesJson(baseDir, nickname, domain, postRepliesFilename, - authorized, - repliesJson) + authorized, repliesJson) # send the replies json if self._requestHTTP(): @@ -5061,9 +5069,8 @@ class PubServer(BaseHTTPRequestHandler): 'post replies done') else: if self._fetchAuthenticated(): - msg = \ - json.dumps(repliesJson, - ensure_ascii=False) + msg = json.dumps(repliesJson, + ensure_ascii=False) msg = msg.encode('utf-8') protocolStr = 'application/json' self._set_headers(protocolStr, len(msg), From 4653ea14ee2d0f4f543ca128aeafceec9c6a7153 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 12:25:05 +0100 Subject: [PATCH 062/108] Move display of roles to its own method --- daemon.py | 153 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 65 deletions(-) diff --git a/daemon.py b/daemon.py index 9cffbb553..544acfb2c 100644 --- a/daemon.py +++ b/daemon.py @@ -5082,6 +5082,81 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showRoles(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Show roles within profile screen + """ + namedStatus = path.split('/users/')[1] + if '/' not in namedStatus: + return False + + postSections = namedStatus.split('/') + nickname = postSections[0] + actorFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '.json' + if not os.path.isfile(actorFilename): + return False + + actorJson = loadJson(actorFilename) + if not actorJson: + return False + + if actorJson.get('roles'): + if self._requestHTTP(): + getPerson = \ + personLookup(domain, path.replace('/roles', ''), + baseDir) + if getPerson: + defaultTimeline = \ + self.server.defaultTimeline + recentPostsCache = \ + self.server.recentPostsCache + cachedWebfingers = \ + self.server.cachedWebfingers + YTReplacementDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlProfile(defaultTimeline, + recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, httpPrefix, True, + self.server.ocapAlways, + getPerson, 'roles', + self.server.session, + cachedWebfingers, + self.server.personCache, + YTReplacementDomain, + actorJson['roles'], + None, None) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'post replies done', + 'show roles') + else: + if self._fetchAuthenticated(): + msg = json.dumps(actorJson['roles'], + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6800,71 +6875,19 @@ class PubServer(BaseHTTPRequestHandler): 'post replies done') if self.path.endswith('/roles') and '/users/' in self.path: - namedStatus = self.path.split('/users/')[1] - if '/' in namedStatus: - postSections = namedStatus.split('/') - nickname = postSections[0] - actorFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + '.json' - if os.path.isfile(actorFilename): - actorJson = loadJson(actorFilename) - if actorJson: - if actorJson.get('roles'): - if self._requestHTTP(): - getPerson = \ - personLookup(self.server.domain, - self.path.replace('/roles', - ''), - self.server.baseDir) - if getPerson: - defaultTimeline = \ - self.server.defaultTimeline - recentPostsCache = \ - self.server.recentPostsCache - cachedWebfingers = \ - self.server.cachedWebfingers - YTReplacementDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlProfile(defaultTimeline, - recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - True, - self.server.ocapAlways, - getPerson, 'roles', - self.server.session, - cachedWebfingers, - self.server.personCache, - YTReplacementDomain, - actorJson['roles'], - None, None) - msg = msg.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'post replies ' + - 'done', - 'show roles') - else: - if self._fetchAuthenticated(): - msg = json.dumps(actorJson['roles'], - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return + if self._showRoles(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'post replies done', From 897dfa75b111c8f105cc75b958cb8fd2cd9b0b86 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 13:06:21 +0100 Subject: [PATCH 063/108] Move display of skills to its own method --- daemon.py | 173 +++++++++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 80 deletions(-) diff --git a/daemon.py b/daemon.py index 544acfb2c..6d143b716 100644 --- a/daemon.py +++ b/daemon.py @@ -5157,6 +5157,86 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showSkills(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Show skills on the profile screen + """ + namedStatus = path.split('/users/')[1] + if '/' in namedStatus: + postSections = namedStatus.split('/') + nickname = postSections[0] + actorFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '.json' + if os.path.isfile(actorFilename): + actorJson = loadJson(actorFilename) + if actorJson: + if actorJson.get('skills'): + if self._requestHTTP(): + getPerson = \ + personLookup(domain, + path.replace('/skills', ''), + baseDir) + if getPerson: + defaultTimeline = \ + self.server.defaultTimeline + recentPostsCache = \ + self.server.recentPostsCache + cachedWebfingers = \ + self.server.cachedWebfingers + YTReplacementDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlProfile(defaultTimeline, + recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, httpPrefix, True, + self.server.ocapAlways, + getPerson, 'skills', + self.server.session, + cachedWebfingers, + self.server.personCache, + YTReplacementDomain, + actorJson['skills'], + None, None) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'post roles done', + 'show skills') + else: + if self._fetchAuthenticated(): + msg = json.dumps(actorJson['skills'], + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), None, + callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return + actor = path.replace('/skills', '') + actorAbsolute = httpPrefix + '://' + domainFull + actor + if callingDomain.endswith('.onion') and onionDomain: + actorAbsolute = 'http://' + onionDomain + actor + elif callingDomain.endswith('.i2p') and i2pDomain: + actorAbsolute = 'http://' + i2pDomain + actor + self._redirect_headers(actorAbsolute, cookie, callingDomain) + self.server.GETbusy = False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6895,86 +6975,19 @@ class PubServer(BaseHTTPRequestHandler): # show skills on the profile page if self.path.endswith('/skills') and '/users/' in self.path: - namedStatus = self.path.split('/users/')[1] - if '/' in namedStatus: - postSections = namedStatus.split('/') - nickname = postSections[0] - actorFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + '.json' - if os.path.isfile(actorFilename): - actorJson = loadJson(actorFilename) - if actorJson: - if actorJson.get('skills'): - if self._requestHTTP(): - getPerson = \ - personLookup(self.server.domain, - self.path.replace('/skills', - ''), - self.server.baseDir) - if getPerson: - defaultTimeline = \ - self.server.defaultTimeline - recentPostsCache = \ - self.server.recentPostsCache - cachedWebfingers = \ - self.server.cachedWebfingers - YTReplacementDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlProfile(defaultTimeline, - recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - True, - self.server.ocapAlways, - getPerson, 'skills', - self.server.session, - cachedWebfingers, - self.server.personCache, - YTReplacementDomain, - actorJson['skills'], - None, None) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'post roles ' + - 'done', - 'show skills') - else: - if self._fetchAuthenticated(): - msg = json.dumps(actorJson['skills'], - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, - callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return - actor = self.path.replace('/skills', '') - actorAbsolute = self.server.httpPrefix + '://' + \ - self.server.domainFull + actor - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actorAbsolute = 'http://' + self.server.onionDomain + actor - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actorAbsolute = 'http://' + self.server.i2pDomain + actor - self._redirect_headers(actorAbsolute, cookie, callingDomain) - self.server.GETbusy = False - return + if self._showRoles(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'post roles done', From 8e5ae4fcd1924fef7cdb40faa2138a93594d4b7b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 13:08:09 +0100 Subject: [PATCH 064/108] Move display of skills to its own method --- daemon.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/daemon.py b/daemon.py index 6d143b716..aedd6bc0d 100644 --- a/daemon.py +++ b/daemon.py @@ -6975,18 +6975,18 @@ class PubServer(BaseHTTPRequestHandler): # show skills on the profile page if self.path.endswith('/skills') and '/users/' in self.path: - if self._showRoles(authorized, - 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, GETtimings, - self.server.proxyType, - cookie, self.server.debug): + if self._showSkills(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From dbbe143f948863611a783680c6e8315f17aa3395 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 13:40:40 +0100 Subject: [PATCH 065/108] Move individual post to its own method --- daemon.py | 225 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 125 insertions(+), 100 deletions(-) diff --git a/daemon.py b/daemon.py index aedd6bc0d..1191507a1 100644 --- a/daemon.py +++ b/daemon.py @@ -5227,7 +5227,7 @@ class PubServer(BaseHTTPRequestHandler): else: self._404() self.server.GETbusy = False - return + return True actor = path.replace('/skills', '') actorAbsolute = httpPrefix + '://' + domainFull + actor if callingDomain.endswith('.onion') and onionDomain: @@ -5236,6 +5236,117 @@ class PubServer(BaseHTTPRequestHandler): actorAbsolute = 'http://' + i2pDomain + actor self._redirect_headers(actorAbsolute, cookie, callingDomain) self.server.GETbusy = False + return True + + def _showIndividualPost(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows an individual post + """ + likedBy = None + if '?likedBy=' in path: + likedBy = path.split('?likedBy=')[1].strip() + if '?' in likedBy: + likedBy = likedBy.split('?')[0] + self.path = path.split('?likedBy=')[0] + namedStatus = path.split('/users/')[1] + if '/' not in namedStatus: + return False + postSections = namedStatus.split('/') + if len(postSections) < 3: + return False + nickname = postSections[0] + statusNumber = postSections[2] + if not (len(statusNumber) > 10 and statusNumber.isdigit()): + return False + postFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + \ + domain + '/outbox/' + \ + httpPrefix + ':##' + \ + domainFull + '#users#' + \ + nickname + '#statuses#' + \ + statusNumber + '.json' + if os.path.isfile(postFilename): + postJsonObject = loadJson(postFilename) + if not postJsonObject: + self.send_response(429) + self.end_headers() + self.server.GETbusy = False + return True + else: + # Only authorized viewers get to see likes + # on posts + # Otherwize marketers could gain more social + # graph info + if not authorized: + pjo = postJsonObject + self._removePostInteractions(pjo) + + if self._requestHTTP(): + recentPostsCache = \ + self.server.recentPostsCache + maxRecentPosts = \ + self.server.maxRecentPosts + translate = \ + self.server.translate + cachedWebfingers = \ + self.server.cachedWebfingers + personCache = \ + self.server.personCache + projectVersion = \ + self.server.projectVersion + ytDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlIndividualPost(recentPostsCache, + maxRecentPosts, + translate, + baseDir, + self.server.session, + cachedWebfingers, + personCache, + nickname, + domain, + port, + authorized, + postJsonObject, + httpPrefix, + projectVersion, + likedBy, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'show skills ' + + 'done', + 'show status') + else: + if self._fetchAuthenticated(): + msg = json.dumps(postJsonObject, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + else: + self._404() + self.server.GETbusy = False + return True + return False def do_GET(self): callingDomain = self.server.domainFull @@ -6996,105 +7107,19 @@ class PubServer(BaseHTTPRequestHandler): # get an individual post from the path # /users/nickname/statuses/number if '/statuses/' in self.path and '/users/' in self.path: - likedBy = None - if '?likedBy=' in self.path: - likedBy = self.path.split('?likedBy=')[1].strip() - if '?' in likedBy: - likedBy = likedBy.split('?')[0] - self.path = self.path.split('?likedBy=')[0] - namedStatus = self.path.split('/users/')[1] - if '/' in namedStatus: - postSections = namedStatus.split('/') - if len(postSections) >= 3: - nickname = postSections[0] - statusNumber = postSections[2] - if len(statusNumber) > 10 and statusNumber.isdigit(): - postFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + \ - self.server.domain + '/outbox/' + \ - self.server.httpPrefix + ':##' + \ - self.server.domainFull + '#users#' + \ - nickname + '#statuses#' + \ - statusNumber + '.json' - if os.path.isfile(postFilename): - postJsonObject = loadJson(postFilename) - if not postJsonObject: - self.send_response(429) - self.end_headers() - self.server.GETbusy = False - return - else: - # Only authorized viewers get to see likes - # on posts - # Otherwize marketers could gain more social - # graph info - if not authorized: - pjo = postJsonObject - self._removePostInteractions(pjo) - - if self._requestHTTP(): - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - httpPrefix = \ - self.server.httpPrefix - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlIndividualPost(recentPostsCache, - maxRecentPosts, - translate, - self.server.baseDir, - self.server.session, - cachedWebfingers, - personCache, - nickname, - self.server.domain, - self.server.port, - authorized, - postJsonObject, - httpPrefix, - projectVersion, - likedBy, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, - callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'show skills ' + - 'done', - 'show status') - else: - if self._fetchAuthenticated(): - msg = json.dumps(postJsonObject, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return - else: - self._404() - self.server.GETbusy = False - return + if self._showIndividualPost(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show skills done', From b9ccc973ae679ebc18c934263458239b95ccebea Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 13:50:12 +0100 Subject: [PATCH 066/108] Move inbox to its own method --- daemon.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/daemon.py b/daemon.py index 1191507a1..7be9ee864 100644 --- a/daemon.py +++ b/daemon.py @@ -5348,6 +5348,115 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showInbox(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the inbox timeline + """ + if '/users/' in path: + if authorized: + inboxFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'inbox', + authorized, + self.server.ocapAlways) + if inboxFeed: + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show status done', + 'show inbox json') + if self._requestHTTP(): + nickname = self.path.replace('/users/', '') + nickname = nickname.replace('/inbox', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, 'inbox', + authorized, + self.server.ocapAlways) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'show status done', + 'show inbox page') + msg = htmlInbox(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show status done', + 'show inbox html') + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show status done', + 'show inbox') + else: + # don't need authenticated fetch here because + # there is already the authorization check + msg = json.dumps(inboxFeed, ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = self.path.replace('/users/', '') + nickname = nickname.replace('/inbox', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/inbox': + # not the shared inbox + if debug: + print('DEBUG: GET access to inbox is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): From 254748f48f4b3557c653c429305685a3e9d523cc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 14:05:48 +0100 Subject: [PATCH 067/108] Move DMs timeline to its own method --- daemon.py | 312 ++++++++++++++++++++++-------------------------------- 1 file changed, 125 insertions(+), 187 deletions(-) diff --git a/daemon.py b/daemon.py index 7be9ee864..f4e4bf82c 100644 --- a/daemon.py +++ b/daemon.py @@ -5457,6 +5457,107 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showDMs(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the DMs timeline + """ + if '/users/' in path: + if authorized: + inboxDMFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'dm', + authorized, + self.server.ocapAlways) + if inboxDMFeed: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/dm', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxDMFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + self.path + '?page=1', + httpPrefix, + maxPostsInFeed, 'dm', + authorized, + self.server.ocapAlways) + msg = \ + htmlInboxDMs(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxDMFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show inbox done', + 'show dms') + else: + # don't need authenticated fetch here because + # there is already the authorization check + msg = json.dumps(inboxDMFeed, ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/dm', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/dm': + # not the DM inbox + if debug: + print('DEBUG: GET access to DM timeline is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7236,104 +7337,18 @@ class PubServer(BaseHTTPRequestHandler): # get the inbox for a given person if self.path.endswith('/inbox') or '/inbox?page=' in self.path: - if '/users/' in self.path: - if authorized: - inboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'inbox', - authorized, - self.server.ocapAlways) - if inboxFeed: - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show status done', - 'show inbox json') - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/inbox', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - inboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, 'inbox', - authorized, - self.server.ocapAlways) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'show status done', - 'show inbox page') - msg = htmlInbox(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - inboxFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show status done', - 'show inbox html') - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show status done', - 'show inbox') - else: - # don't need authenticated fetch here because - # there is already the authorization check - msg = json.dumps(inboxFeed, ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/inbox', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.path != '/inbox': - # not the shared inbox - if self.server.debug: - print('DEBUG: GET access to inbox is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False + if self._showInbox(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -7342,95 +7357,18 @@ class PubServer(BaseHTTPRequestHandler): # get the direct messages for a given person if self.path.endswith('/dm') or '/dm?page=' in self.path: - if '/users/' in self.path: - if authorized: - inboxDMFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'dm', - authorized, - self.server.ocapAlways) - if inboxDMFeed: - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/dm', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - inboxDMFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path+'?page=1', - self.server.httpPrefix, - maxPostsInFeed, 'dm', - authorized, - self.server.ocapAlways) - msg = \ - htmlInboxDMs(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - inboxDMFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show inbox done', - 'show dms') - else: - # don't need authenticated fetch here because - # there is already the authorization check - msg = json.dumps(inboxDMFeed, ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/dm', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.path != '/dm': - # not the DM inbox - if self.server.debug: - print('DEBUG: GET access to inbox is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False + if self._showDMs(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From b1e3f8440282e864b38223a47a0b9da3bf2965a1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 14:12:00 +0100 Subject: [PATCH 068/108] Change function name --- daemon.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/daemon.py b/daemon.py index f4e4bf82c..262042e66 100644 --- a/daemon.py +++ b/daemon.py @@ -4882,16 +4882,15 @@ class PubServer(BaseHTTPRequestHandler): 'post muted done', 'unmute activated') - def _showReplies(self, authorized: bool, - callingDomain: str, path: str, - baseDir: str, httpPrefix: str, - domain: str, domainFull: str, port: int, - onionDomain: str, i2pDomain: str, - GETstartTime, GETtimings: {}, - proxyType: str, cookie: str, - debug: str) -> bool: - """Shows the replies timeline - Returns true if the timeline was shown + def _showRepliesToPost(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the replies to a post """ if not ('/statuses/' in path and '/users/' in path): return False @@ -7257,18 +7256,18 @@ class PubServer(BaseHTTPRequestHandler): # get replies to a post /users/nickname/statuses/number/replies if self.path.endswith('/replies') or '/replies?page=' in self.path: - if self._showReplies(authorized, - 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, GETtimings, - self.server.proxyType, cookie, - self.server.debug): + if self._showRepliesToPost(authorized, + 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, GETtimings, + self.server.proxyType, cookie, + self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From 65baa4fdedb86b3018a009d9e73565546ccea696 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 14:19:19 +0100 Subject: [PATCH 069/108] Move replies timeline to its own method --- daemon.py | 201 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 112 insertions(+), 89 deletions(-) diff --git a/daemon.py b/daemon.py index 262042e66..9f3a1fd9b 100644 --- a/daemon.py +++ b/daemon.py @@ -5557,6 +5557,106 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showReplies(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the replies timeline + """ + if '/users/' in path: + if authorized: + inboxRepliesFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'tlreplies', + True, self.server.ocapAlways) + if not inboxRepliesFeed: + inboxRepliesFeed = [] + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlreplies', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxRepliesFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, 'tlreplies', + True, self.server.ocapAlways) + msg = \ + htmlInboxReplies(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxRepliesFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show dms done', + 'show replies 2') + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg = json.dumps(inboxRepliesFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlreplies', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/tlreplies': + # not the replies inbox + if debug: + print('DEBUG: GET access to inbox is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7376,95 +7476,18 @@ class PubServer(BaseHTTPRequestHandler): # get the replies for a given person if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path: - if '/users/' in self.path: - if authorized: - inboxRepliesFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'tlreplies', - True, self.server.ocapAlways) - if not inboxRepliesFeed: - inboxRepliesFeed = [] - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlreplies', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - inboxRepliesFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, 'tlreplies', - True, self.server.ocapAlways) - msg = \ - htmlInboxReplies(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - inboxRepliesFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show dms done', - 'show replies 2') - else: - # don't need authenticated fetch here because there is - # already the authorization check - msg = json.dumps(inboxRepliesFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlreplies', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.path != '/tlreplies': - # not the replies inbox - if self.server.debug: - print('DEBUG: GET access to inbox is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False + if self._showReplies(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From 7ca102b16977b2cc62d7ec4d0ab5ded36231b923 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 14:26:45 +0100 Subject: [PATCH 070/108] Move media timeline to its own method --- daemon.py | 201 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 112 insertions(+), 89 deletions(-) diff --git a/daemon.py b/daemon.py index 9f3a1fd9b..f70544aa4 100644 --- a/daemon.py +++ b/daemon.py @@ -5657,6 +5657,106 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showMediaTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the media timeline + """ + if '/users/' in path: + if authorized: + inboxMediaFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInMediaFeed, 'tlmedia', + True, self.server.ocapAlways) + if not inboxMediaFeed: + inboxMediaFeed = [] + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlmedia', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxMediaFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInMediaFeed, 'tlmedia', + True, self.server.ocapAlways) + msg = \ + htmlInboxMedia(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInMediaFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxMediaFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show replies 2 done', + 'show media 2') + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg = json.dumps(inboxMediaFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlmedia', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/tlmedia': + # not the media inbox + if debug: + print('DEBUG: GET access to inbox is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7496,95 +7596,18 @@ class PubServer(BaseHTTPRequestHandler): # get the media for a given person if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path: - if '/users/' in self.path: - if authorized: - inboxMediaFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInMediaFeed, 'tlmedia', - True, self.server.ocapAlways) - if not inboxMediaFeed: - inboxMediaFeed = [] - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlmedia', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - inboxMediaFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInMediaFeed, 'tlmedia', - True, self.server.ocapAlways) - msg = \ - htmlInboxMedia(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInMediaFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - inboxMediaFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show replies 2 done', - 'show media 2') - else: - # don't need authenticated fetch here because there is - # already the authorization check - msg = json.dumps(inboxMediaFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlmedia', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.path != '/tlmedia': - # not the media inbox - if self.server.debug: - print('DEBUG: GET access to inbox is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False + if self._showMediaTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From c0f2d3de900e2234e9c7f928e4774c6c7d7ca481 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 14:35:05 +0100 Subject: [PATCH 071/108] Move blogs timeline to its own method --- daemon.py | 210 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 117 insertions(+), 93 deletions(-) diff --git a/daemon.py b/daemon.py index f70544aa4..68fae1671 100644 --- a/daemon.py +++ b/daemon.py @@ -5757,6 +5757,107 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showBlogsTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the blogs timeline + """ + if '/users/' in path: + if authorized: + inboxBlogsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInBlogsFeed, 'tlblogs', + True, self.server.ocapAlways) + if not inboxBlogsFeed: + inboxBlogsFeed = [] + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlblogs', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxBlogsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInBlogsFeed, 'tlblogs', + True, self.server.ocapAlways) + msg = \ + htmlInboxBlogs(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInBlogsFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxBlogsFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show media 2 done', + 'show blogs 2') + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg = json.dumps(inboxBlogsFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = self.path.replace('/users/', '') + nickname = nickname.replace('/tlblogs', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/tlblogs': + # not the blogs inbox + if debug: + print('DEBUG: GET access to blogs is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7534,7 +7635,7 @@ class PubServer(BaseHTTPRequestHandler): 'show skills done', 'show status done') - # get the inbox for a given person + # get the inbox timeline for a given person if self.path.endswith('/inbox') or '/inbox?page=' in self.path: if self._showInbox(authorized, callingDomain, self.path, @@ -7554,7 +7655,7 @@ class PubServer(BaseHTTPRequestHandler): 'show status done', 'show inbox done') - # get the direct messages for a given person + # get the direct messages timeline for a given person if self.path.endswith('/dm') or '/dm?page=' in self.path: if self._showDMs(authorized, callingDomain, self.path, @@ -7574,7 +7675,7 @@ class PubServer(BaseHTTPRequestHandler): 'show inbox done', 'show dms done') - # get the replies for a given person + # get the replies timeline for a given person if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path: if self._showReplies(authorized, callingDomain, self.path, @@ -7594,7 +7695,7 @@ class PubServer(BaseHTTPRequestHandler): 'show dms done', 'show replies 2 done') - # get the media for a given person + # get the media timeline for a given person if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path: if self._showMediaTimeline(authorized, callingDomain, self.path, @@ -7616,95 +7717,18 @@ class PubServer(BaseHTTPRequestHandler): # get the blogs for a given person if self.path.endswith('/tlblogs') or '/tlblogs?page=' in self.path: - if '/users/' in self.path: - if authorized: - inboxBlogsFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInBlogsFeed, 'tlblogs', - True, self.server.ocapAlways) - if not inboxBlogsFeed: - inboxBlogsFeed = [] - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlblogs', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - inboxBlogsFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInBlogsFeed, 'tlblogs', - True, self.server.ocapAlways) - msg = \ - htmlInboxBlogs(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInBlogsFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - inboxBlogsFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show media 2 done', - 'show blogs 2') - else: - # don't need authenticated fetch here because there is - # already the authorization check - msg = json.dumps(inboxBlogsFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlblogs', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.path != '/tlblogs': - # not the blogs inbox - if self.server.debug: - print('DEBUG: GET access to blogs is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False + if self._showBlogsTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From ae1b9c63be875bbc0c2f95068127c4985bf40bf7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 15:06:15 +0100 Subject: [PATCH 072/108] Move shares timeline to its own method --- daemon.py | 117 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/daemon.py b/daemon.py index 68fae1671..d9913a266 100644 --- a/daemon.py +++ b/daemon.py @@ -5858,6 +5858,63 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showSharesTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the shares timeline + """ + if '/users/' in path: + if authorized: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlshares', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + msg = \ + htmlShares(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show blogs 2 done', + 'show shares 2') + self.server.GETbusy = False + return True + # not the shares timeline + if debug: + print('DEBUG: GET access to shares timeline is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7737,53 +7794,19 @@ class PubServer(BaseHTTPRequestHandler): # get the shared items timeline for a given person if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path: - if '/users/' in self.path: - if authorized: - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlshares', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - msg = \ - htmlShares(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show blogs 2 done', - 'show shares 2') - self.server.GETbusy = False - return - # not the shares timeline - if self.server.debug: - print('DEBUG: GET access to shares timeline is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False - return + if self._showSharesTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', From 9c205e9ab92649c09d6b53b2845e71cfbb1a3203 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 15:16:46 +0100 Subject: [PATCH 073/108] Move bookmarks timeline to its own method --- daemon.py | 206 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 114 insertions(+), 92 deletions(-) diff --git a/daemon.py b/daemon.py index d9913a266..54e72f855 100644 --- a/daemon.py +++ b/daemon.py @@ -5915,6 +5915,106 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showBookmarksTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the bookmarks timeline + """ + if '/users/' in path: + if authorized: + bookmarksFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'tlbookmarks', + authorized, self.server.ocapAlways) + if bookmarksFeed: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlbookmarks', '') + nickname = nickname.replace('/bookmarks', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + bookmarksFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, + 'tlbookmarks', + authorized, + self.server.ocapAlways) + msg = \ + htmlBookmarks(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + bookmarksFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show shares 2 done', + 'show bookmarks 2') + else: + # don't need authenticated fetch here because + # there is already the authorization check + msg = json.dumps(bookmarksFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlbookmarks', '') + nickname = nickname.replace('/bookmarks', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if debug: + print('DEBUG: GET access to bookmarks is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7812,102 +7912,24 @@ class PubServer(BaseHTTPRequestHandler): 'show blogs 2 done', 'show shares 2 done') - # get the bookmarks for a given person + # get the bookmarks timeline for a given person if self.path.endswith('/tlbookmarks') or \ '/tlbookmarks?page=' in self.path or \ self.path.endswith('/bookmarks') or \ '/bookmarks?page=' in self.path: - if '/users/' in self.path: - if authorized: - bookmarksFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'tlbookmarks', - authorized, self.server.ocapAlways) - if bookmarksFeed: - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlbookmarks', '') - nickname = nickname.replace('/bookmarks', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - bookmarksFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, - 'tlbookmarks', - authorized, - self.server.ocapAlways) - msg = \ - htmlBookmarks(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - bookmarksFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show shares 2 done', - 'show bookmarks 2') - else: - # don't need authenticated fetch here because - # there is already the authorization check - msg = json.dumps(bookmarksFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlbookmarks', '') - nickname = nickname.replace('/bookmarks', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.server.debug: - print('DEBUG: GET access to bookmarks is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False - return + if self._showBookmarksTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show shares 2 done', From 342aec95731cb58a18248492544877b70c551e75 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 15:31:39 +0100 Subject: [PATCH 074/108] Move outbox timeline to its own method --- daemon.py | 382 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 214 insertions(+), 168 deletions(-) diff --git a/daemon.py b/daemon.py index 54e72f855..d2608492d 100644 --- a/daemon.py +++ b/daemon.py @@ -6015,6 +6015,194 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showEventsTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the events timeline + """ + if '/users/' in path: + if authorized: + # convert /events to /tlevents + if path.endswith('/events') or \ + '/events?page=' in path: + path = path.replace('/events', '/tlevents') + eventsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'tlevents', + authorized, self.server.ocapAlways) + print('eventsFeed: ' + str(eventsFeed)) + if eventsFeed: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlevents', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + eventsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, + 'tlevents', + authorized, + self.server.ocapAlways) + msg = \ + htmlEvents(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + eventsFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show bookmarks 2 done', + 'show events') + else: + # don't need authenticated fetch here because + # there is already the authorization check + msg = json.dumps(eventsFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlevents', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if debug: + print('DEBUG: GET access to events is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + + def _showOutboxTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the outbox timeline + """ + # get outbox feed for a person + outboxFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, domain, + port, path, + httpPrefix, + maxPostsInFeed, 'outbox', + authorized, + self.server.ocapAlways) + if outboxFeed: + if self._requestHTTP(): + nickname = \ + path.replace('/users/', '').replace('/outbox', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if a page wasn't specified then show the first one + outboxFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, 'outbox', + authorized, + self.server.ocapAlways) + msg = \ + htmlOutbox(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + outboxFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show events done', + 'show outbox') + else: + if self._fetchAuthenticated(): + msg = json.dumps(outboxFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7940,179 +8128,37 @@ class PubServer(BaseHTTPRequestHandler): '/tlevents?page=' in self.path or \ self.path.endswith('/events') or \ '/events?page=' in self.path: - if '/users/' in self.path: - if authorized: - # convert /events to /tlevents - if self.path.endswith('/events') or \ - '/events?page=' in self.path: - self.path = self.path.replace('/events', '/tlevents') - eventsFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'tlevents', - authorized, self.server.ocapAlways) - print('eventsFeed: ' + str(eventsFeed)) - if eventsFeed: - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlevents', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - eventsFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, - 'tlevents', - authorized, - self.server.ocapAlways) - msg = \ - htmlEvents(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - eventsFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show bookmarks 2 done', - 'show events') - else: - # don't need authenticated fetch here because - # there is already the authorization check - msg = json.dumps(eventsFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/tlevents', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.server.debug: - print('DEBUG: GET access to events is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False - return + if self._showEventsTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show bookmarks 2 done', 'show events done') - # get outbox feed for a person - outboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, self.server.domain, - self.server.port, self.path, - self.server.httpPrefix, - maxPostsInFeed, 'outbox', - authorized, - self.server.ocapAlways) - if outboxFeed: - if self._requestHTTP(): - nickname = \ - self.path.replace('/users/', '').replace('/outbox', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if a page wasn't specified then show the first one - outboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, 'outbox', - authorized, - self.server.ocapAlways) - msg = \ - htmlOutbox(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - outboxFeed, - self.server.allowDeletion, - self.server.httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show events done', - 'show outbox') - else: - if self._fetchAuthenticated(): - msg = json.dumps(outboxFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False + # outbox timeline + if self._showOutboxTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From 7bb12148132c777c4648e79ca493b846ea320f12 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 16:13:50 +0100 Subject: [PATCH 075/108] Path --- daemon.py | 216 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 119 insertions(+), 97 deletions(-) diff --git a/daemon.py b/daemon.py index d2608492d..136917534 100644 --- a/daemon.py +++ b/daemon.py @@ -1716,7 +1716,7 @@ class PubServer(BaseHTTPRequestHandler): if '&submitDM=' in optionsConfirmParams: if debug: print('Sending DM to ' + optionsActor) - reportPath = self.path.replace('/personoptions', '') + '/newdm' + reportPath = path.replace('/personoptions', '') + '/newdm' msg = htmlNewPost(False, self.server.translate, baseDir, httpPrefix, @@ -1734,7 +1734,7 @@ class PubServer(BaseHTTPRequestHandler): # snooze button on person option screen if '&submitSnooze=' in optionsConfirmParams: - usersPath = self.path.split('/personoptions')[0] + usersPath = path.split('/personoptions')[0] thisActor = httpPrefix + '://' + domainFull + usersPath if debug: print('Snoozing ' + optionsActor + ' ' + thisActor) @@ -2289,7 +2289,7 @@ class PubServer(BaseHTTPRequestHandler): self._404() self.server.POSTbusy = False return - profilePathStr = self.path.replace('/searchhandle', '') + profilePathStr = path.replace('/searchhandle', '') profileStr = \ htmlProfileAfterSearch(self.server.recentPostsCache, self.server.maxRecentPosts, @@ -3744,8 +3744,8 @@ class PubServer(BaseHTTPRequestHandler): cookie: str, debug: bool): """Show person options screen """ - optionsStr = self.path.split('?options=')[1] - originPathStr = self.path.split('?options=')[0] + optionsStr = path.split('?options=')[1] + originPathStr = path.split('?options=')[0] if ';' in optionsStr: pageNumber = 1 optionsList = optionsStr.split(';') @@ -5252,7 +5252,7 @@ class PubServer(BaseHTTPRequestHandler): likedBy = path.split('?likedBy=')[1].strip() if '?' in likedBy: likedBy = likedBy.split('?')[0] - self.path = path.split('?likedBy=')[0] + path = path.split('?likedBy=')[0] namedStatus = path.split('/users/')[1] if '/' not in namedStatus: return False @@ -5261,7 +5261,7 @@ class PubServer(BaseHTTPRequestHandler): return False nickname = postSections[0] statusNumber = postSections[2] - if not (len(statusNumber) > 10 and statusNumber.isdigit()): + if len(statusNumber) <= 10 or (not statusNumber.isdigit()): return False postFilename = \ baseDir + '/accounts/' + \ @@ -5375,7 +5375,7 @@ class PubServer(BaseHTTPRequestHandler): 'show status done', 'show inbox json') if self._requestHTTP(): - nickname = self.path.replace('/users/', '') + nickname = path.replace('/users/', '') nickname = nickname.replace('/inbox', '') pageNumber = 1 if '?page=' in nickname: @@ -5442,7 +5442,7 @@ class PubServer(BaseHTTPRequestHandler): return True else: if debug: - nickname = self.path.replace('/users/', '') + nickname = path.replace('/users/', '') nickname = nickname.replace('/inbox', '') print('DEBUG: ' + nickname + ' was not authorized to access ' + path) @@ -5499,7 +5499,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir, domain, port, - self.path + '?page=1', + path + '?page=1', httpPrefix, maxPostsInFeed, 'dm', authorized, @@ -5844,7 +5844,7 @@ class PubServer(BaseHTTPRequestHandler): return True else: if debug: - nickname = self.path.replace('/users/', '') + nickname = path.replace('/users/', '') nickname = nickname.replace('/tlblogs', '') print('DEBUG: ' + nickname + ' was not authorized to access ' + path) @@ -6203,6 +6203,101 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showModTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the moderation timeline + """ + if '/users/' in path: + if authorized: + moderationFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path, + httpPrefix, + maxPostsInFeed, 'moderation', + True, self.server.ocapAlways) + if moderationFeed: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/moderation', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + moderationFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + maxPostsInFeed, 'moderation', + True, self.server.ocapAlways) + msg = \ + htmlModeration(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + moderationFeed, + True, + httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show outbox done', + 'show moderation') + else: + # don't need authenticated fetch here because + # there is already the authorization check + msg = json.dumps(moderationFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/moderation', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if debug: + print('DEBUG: GET access to moderation feed is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8168,92 +8263,19 @@ class PubServer(BaseHTTPRequestHandler): # get the moderation feed for a moderator if self.path.endswith('/moderation') or \ '/moderation?page=' in self.path: - if '/users/' in self.path: - if authorized: - moderationFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path, - self.server.httpPrefix, - maxPostsInFeed, 'moderation', - True, self.server.ocapAlways) - if moderationFeed: - if self._requestHTTP(): - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/moderation', '') - pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in self.path: - # if no page was specified then show the first - moderationFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - maxPostsInFeed, 'moderation', - True, self.server.ocapAlways) - msg = \ - htmlModeration(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInFeed, - self.server.session, - self.server.baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - self.server.domain, - self.server.port, - moderationFeed, - True, - self.server.httpPrefix, - self.server.projectVersion, - self.server.YTReplacementDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show outbox done', - 'show moderation') - else: - # don't need authenticated fetch here because - # there is already the authorization check - msg = json.dumps(moderationFeed, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - self.server.GETbusy = False - return - else: - if self.server.debug: - nickname = self.path.replace('/users/', '') - nickname = nickname.replace('/moderation', '') - print('DEBUG: ' + nickname + - ' was not authorized to access ' + self.path) - if self.server.debug: - print('DEBUG: GET access to moderation feed is unauthorized') - self.send_response(405) - self.end_headers() - self.server.GETbusy = False - return + if self._showModTimeline(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show outbox done', From 06a6c93131c4200ccfd5d44a7585ad5e82a2386c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 17:21:41 +0100 Subject: [PATCH 076/108] Move shares feed to its own method --- daemon.py | 178 +++++++++++++++++++++++++++++------------------------- 1 file changed, 97 insertions(+), 81 deletions(-) diff --git a/daemon.py b/daemon.py index 136917534..4db2950bc 100644 --- a/daemon.py +++ b/daemon.py @@ -6298,6 +6298,90 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showSharesFeed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the shares feed + """ + shares = \ + getSharesFeedForPerson(baseDir, domain, port, path, + httpPrefix, sharesPerPage) + if shares: + if self._requestHTTP(): + pageNumber = 1 + if '?page=' not in path: + searchPath = path + # get a page of shares, not the summary + shares = \ + getSharesFeedForPerson(baseDir, domain, port, + path + '?page=true', + httpPrefix, + sharesPerPage) + else: + pageNumberStr = path.split('?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + searchPath = path.split('?page=')[0] + getPerson = \ + personLookup(domain, + searchPath.replace('/shares', ''), + baseDir) + if getPerson: + if not self.server.session: + print('Starting new session during profile') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during profile') + self._404() + self.server.GETbusy = False + return True + msg = \ + htmlProfile(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, httpPrefix, + authorized, + self.server.ocapAlways, + getPerson, 'shares', + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.server.YTReplacementDomain, + shares, + pageNumber, sharesPerPage) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show moderation done', + 'show profile 2') + self.server.GETbusy = False + return True + else: + if self._fetchAuthenticated(): + msg = json.dumps(shares, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8281,87 +8365,19 @@ class PubServer(BaseHTTPRequestHandler): 'show outbox done', 'show moderation done') - shares = \ - getSharesFeedForPerson(self.server.baseDir, - self.server.domain, - self.server.port, self.path, - self.server.httpPrefix, - sharesPerPage) - if shares: - if self._requestHTTP(): - pageNumber = 1 - if '?page=' not in self.path: - searchPath = self.path - # get a page of shares, not the summary - shares = \ - getSharesFeedForPerson(self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=true', - self.server.httpPrefix, - sharesPerPage) - else: - pageNumberStr = self.path.split('?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - searchPath = self.path.split('?page=')[0] - getPerson = \ - personLookup(self.server.domain, - searchPath.replace('/shares', ''), - self.server.baseDir) - if getPerson: - if not self.server.session: - print('Starting new session during profile') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during profile') - self._404() - self.server.GETbusy = False - return - msg = \ - htmlProfile(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - authorized, - self.server.ocapAlways, - getPerson, 'shares', - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.YTReplacementDomain, - shares, - pageNumber, sharesPerPage) - msg = msg.encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show moderation done', - 'show profile 2') - self.server.GETbusy = False - return - else: - if self._fetchAuthenticated(): - msg = json.dumps(shares, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return + if self._showSharesFeed(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show moderation done', From 2587610924b23394fb6d6cd0ffb54ff7b30d6e8e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 17:28:22 +0100 Subject: [PATCH 077/108] Move following feed to its own method --- daemon.py | 178 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 99 insertions(+), 79 deletions(-) diff --git a/daemon.py b/daemon.py index 4db2950bc..7b13566fa 100644 --- a/daemon.py +++ b/daemon.py @@ -6382,6 +6382,92 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showFollowingFeed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the following feed + """ + following = \ + getFollowingFeed(baseDir, domain, port, path, + httpPrefix, authorized, followsPerPage) + if following: + if self._requestHTTP(): + pageNumber = 1 + if '?page=' not in path: + searchPath = path + # get a page of following, not the summary + following = \ + getFollowingFeed(baseDir, + domain, + port, + path + '?page=true', + httpPrefix, + authorized, followsPerPage) + else: + pageNumberStr = path.split('?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + searchPath = path.split('?page=')[0] + getPerson = \ + personLookup(domain, + searchPath.replace('/following', ''), + baseDir) + if getPerson: + if not self.server.session: + print('Starting new session during following') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during following') + self._404() + self.server.GETbusy = False + return True + + msg = \ + htmlProfile(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, httpPrefix, + authorized, + self.server.ocapAlways, + getPerson, 'following', + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.server.YTReplacementDomain, + following, + pageNumber, + followsPerPage).encode('utf-8') + self._set_headers('text/html', + len(msg), cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show profile 2 done', + 'show profile 3') + return True + else: + if self._fetchAuthenticated(): + msg = json.dumps(following, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8383,85 +8469,19 @@ class PubServer(BaseHTTPRequestHandler): 'show moderation done', 'show profile 2 done') - following = \ - getFollowingFeed(self.server.baseDir, self.server.domain, - self.server.port, self.path, - self.server.httpPrefix, - authorized, followsPerPage) - if following: - if self._requestHTTP(): - pageNumber = 1 - if '?page=' not in self.path: - searchPath = self.path - # get a page of following, not the summary - following = \ - getFollowingFeed(self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=true', - self.server.httpPrefix, - authorized, followsPerPage) - else: - pageNumberStr = self.path.split('?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - searchPath = self.path.split('?page=')[0] - getPerson = \ - personLookup(self.server.domain, - searchPath.replace('/following', ''), - self.server.baseDir) - if getPerson: - if not self.server.session: - print('Starting new session during following') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during following') - self._404() - self.server.GETbusy = False - return - - msg = \ - htmlProfile(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - authorized, - self.server.ocapAlways, - getPerson, 'following', - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.YTReplacementDomain, - following, - pageNumber, - followsPerPage).encode('utf-8') - self._set_headers('text/html', - len(msg), cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show profile 2 done', - 'show profile 3') - return - else: - if self._fetchAuthenticated(): - msg = json.dumps(following, - ensure_ascii=False).encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - return + if self._showFollowingFeed(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show profile 2 done', From 9d7aef0718fe368495b89cfc73f69fd36dcb172e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 17:35:10 +0100 Subject: [PATCH 078/108] Move followers feed to its own method --- daemon.py | 178 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 99 insertions(+), 79 deletions(-) diff --git a/daemon.py b/daemon.py index 7b13566fa..25bef5478 100644 --- a/daemon.py +++ b/daemon.py @@ -6468,6 +6468,93 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showFollowersFeed(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the followers feed + """ + followers = \ + getFollowingFeed(baseDir, domain, port, path, httpPrefix, + authorized, followsPerPage, 'followers') + if followers: + if self._requestHTTP(): + pageNumber = 1 + if '?page=' not in path: + searchPath = path + # get a page of followers, not the summary + followers = \ + getFollowingFeed(baseDir, + domain, + port, + path + '?page=1', + httpPrefix, + authorized, followsPerPage, + 'followers') + else: + pageNumberStr = path.split('?page=')[1] + if '#' in pageNumberStr: + pageNumberStr = pageNumberStr.split('#')[0] + if pageNumberStr.isdigit(): + pageNumber = int(pageNumberStr) + searchPath = path.split('?page=')[0] + getPerson = \ + personLookup(domain, + searchPath.replace('/followers', ''), + baseDir) + if getPerson: + if not self.server.session: + print('Starting new session during following2') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during following2') + self._404() + self.server.GETbusy = False + return True + msg = \ + htmlProfile(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, + httpPrefix, + authorized, + self.server.ocapAlways, + getPerson, 'followers', + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.server.YTReplacementDomain, + followers, + pageNumber, + followsPerPage).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show profile 3 done', + 'show profile 4') + return True + else: + if self._fetchAuthenticated(): + msg = json.dumps(followers, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8487,85 +8574,18 @@ class PubServer(BaseHTTPRequestHandler): 'show profile 2 done', 'show profile 3 done') - followers = \ - getFollowingFeed(self.server.baseDir, self.server.domain, - self.server.port, self.path, - self.server.httpPrefix, - authorized, followsPerPage, 'followers') - if followers: - if self._requestHTTP(): - pageNumber = 1 - if '?page=' not in self.path: - searchPath = self.path - # get a page of followers, not the summary - followers = \ - getFollowingFeed(self.server.baseDir, - self.server.domain, - self.server.port, - self.path + '?page=1', - self.server.httpPrefix, - authorized, followsPerPage, - 'followers') - else: - pageNumberStr = self.path.split('?page=')[1] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - searchPath = self.path.split('?page=')[0] - getPerson = \ - personLookup(self.server.domain, - searchPath.replace('/followers', ''), - self.server.baseDir) - if getPerson: - if not self.server.session: - print('Starting new session during following2') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during following2') - self._404() - self.server.GETbusy = False - return - msg = \ - htmlProfile(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - authorized, - self.server.ocapAlways, - getPerson, 'followers', - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.YTReplacementDomain, - followers, - pageNumber, - followsPerPage).encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show profile 3 done', - 'show profile 4') - return - else: - if self._fetchAuthenticated(): - msg = json.dumps(followers, - ensure_ascii=False).encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False + if self._showFollowersFeed(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From 13d35af9341493d0d12a2c782c1bb71cb70a00bc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 17:47:27 +0100 Subject: [PATCH 079/108] Move person profile to its own method --- daemon.py | 119 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/daemon.py b/daemon.py index 25bef5478..e926b60dd 100644 --- a/daemon.py +++ b/daemon.py @@ -6555,6 +6555,64 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showPersonProfile(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the profile for a person + """ + # look up a person + getPerson = personLookup(domain, path, baseDir) + if getPerson: + if self._requestHTTP(): + if not self.server.session: + print('Starting new session during person lookup') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during person lookup') + self._404() + self.server.GETbusy = False + return True + msg = \ + htmlProfile(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + self.server.projectVersion, + baseDir, + httpPrefix, + authorized, + self.server.ocapAlways, + getPerson, 'posts', + self.server.session, + self.server.cachedWebfingers, + self.server.personCache, + self.server.YTReplacementDomain, + None, None).encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show profile 4 done', + 'show profile posts') + else: + if self._fetchAuthenticated(): + msg = json.dumps(getPerson, + ensure_ascii=False).encode('utf-8') + self._set_headers('application/json', len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8593,55 +8651,18 @@ class PubServer(BaseHTTPRequestHandler): 'show profile 4 done') # look up a person - getPerson = \ - personLookup(self.server.domain, self.path, - self.server.baseDir) - if getPerson: - if self._requestHTTP(): - if not self.server.session: - print('Starting new session during person lookup') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during person lookup') - self._404() - self.server.GETbusy = False - return - msg = \ - htmlProfile(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - self.server.projectVersion, - self.server.baseDir, - self.server.httpPrefix, - authorized, - self.server.ocapAlways, - getPerson, 'posts', - self.server.session, - self.server.cachedWebfingers, - self.server.personCache, - self.server.YTReplacementDomain, - None, None).encode('utf-8') - self._set_headers('text/html', - len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show profile 4 done', - 'show profile posts') - else: - if self._fetchAuthenticated(): - msg = json.dumps(getPerson, - ensure_ascii=False).encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False + if self._showPersonProfile(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): return self._benchmarkGETtimings(GETstartTime, GETtimings, From fcf36cdac2ab6e94c3d4bdd8e9bba044c0b2edf4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 17:59:01 +0100 Subject: [PATCH 080/108] Move individual @ post to its own method --- daemon.py | 219 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 122 insertions(+), 97 deletions(-) diff --git a/daemon.py b/daemon.py index e926b60dd..978317d1e 100644 --- a/daemon.py +++ b/daemon.py @@ -5237,6 +5237,115 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showIndividualAtPost(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """get an individual post from the path /@nickname/statusnumber + """ + if '/@' not in path: + return False + + likedBy = None + if '?likedBy=' in path: + likedBy = path.split('?likedBy=')[1].strip() + if '?' in likedBy: + likedBy = likedBy.split('?')[0] + path = path.split('?likedBy=')[0] + + namedStatus = path.split('/@')[1] + if '/' not in namedStatus: + # show actor + nickname = namedStatus + else: + postSections = namedStatus.split('/') + if len(postSections) == 2: + nickname = postSections[0] + statusNumber = postSections[1] + if len(statusNumber) > 10 and statusNumber.isdigit(): + postFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + \ + domain + '/outbox/' + \ + httpPrefix + ':##' + \ + domainFull + '#users#' + \ + nickname + '#statuses#' + \ + statusNumber + '.json' + if os.path.isfile(postFilename): + postJsonObject = loadJson(postFilename) + loadedPost = False + if postJsonObject: + loadedPost = True + else: + postJsonObject = {} + if loadedPost: + # Only authorized viewers get to see likes + # on posts. Otherwize marketers could gain + # more social graph info + if not authorized: + pjo = postJsonObject + self._removePostInteractions(pjo) + if self._requestHTTP(): + recentPostsCache = \ + self.server.recentPostsCache + maxRecentPosts = \ + self.server.maxRecentPosts + translate = \ + self.server.translate + cachedWebfingers = \ + self.server.cachedWebfingers + personCache = \ + self.server.personCache + projectVersion = \ + self.server.projectVersion + ytDomain = \ + self.server.YTReplacementDomain + msg = \ + htmlIndividualPost(recentPostsCache, + maxRecentPosts, + translate, + self.server.session, + cachedWebfingers, + personCache, + nickname, + domain, + port, + authorized, + postJsonObject, + httpPrefix, + projectVersion, + likedBy, + ytDomain) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + else: + if self._fetchAuthenticated(): + msg = json.dumps(postJsonObject, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'new post done', + 'individual post shown') + return True + else: + self._404() + self.server.GETbusy = False + return True + return False + def _showIndividualPost(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -8208,103 +8317,19 @@ class PubServer(BaseHTTPRequestHandler): 'new post done') # get an individual post from the path /@nickname/statusnumber - if '/@' in self.path: - likedBy = None - if '?likedBy=' in self.path: - likedBy = self.path.split('?likedBy=')[1].strip() - if '?' in likedBy: - likedBy = likedBy.split('?')[0] - self.path = self.path.split('?likedBy=')[0] - - namedStatus = self.path.split('/@')[1] - if '/' not in namedStatus: - # show actor - nickname = namedStatus - else: - postSections = namedStatus.split('/') - if len(postSections) == 2: - nickname = postSections[0] - statusNumber = postSections[1] - if len(statusNumber) > 10 and statusNumber.isdigit(): - postFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + \ - self.server.domain + '/outbox/' + \ - self.server.httpPrefix + ':##' + \ - self.server.domainFull + '#users#' + \ - nickname + '#statuses#' + \ - statusNumber + '.json' - if os.path.isfile(postFilename): - postJsonObject = loadJson(postFilename) - loadedPost = False - if postJsonObject: - loadedPost = True - else: - postJsonObject = {} - if loadedPost: - # Only authorized viewers get to see likes - # on posts. Otherwize marketers could gain - # more social graph info - if not authorized: - pjo = postJsonObject - self._removePostInteractions(pjo) - if self._requestHTTP(): - recentPostsCache = \ - self.server.recentPostsCache - maxRecentPosts = \ - self.server.maxRecentPosts - translate = \ - self.server.translate - cachedWebfingers = \ - self.server.cachedWebfingers - personCache = \ - self.server.personCache - httpPrefix = \ - self.server.httpPrefix - projectVersion = \ - self.server.projectVersion - ytDomain = \ - self.server.YTReplacementDomain - msg = \ - htmlIndividualPost(recentPostsCache, - maxRecentPosts, - translate, - self.server.session, - cachedWebfingers, - personCache, - nickname, - self.server.domain, - self.server.port, - authorized, - postJsonObject, - httpPrefix, - projectVersion, - likedBy, - ytDomain) - msg = msg.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - else: - if self._fetchAuthenticated(): - msg = json.dumps(postJsonObject, - ensure_ascii=False) - msg = msg.encode('utf-8') - self._set_headers('application/json', - len(msg), - None, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'new post done', - 'individual post shown') - return - else: - self._404() - self.server.GETbusy = False - return + if self._showIndividualAtPost(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'new post done', From b65cee7f9f1a736568adb2cdc2c8be201ade043e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 20:34:52 +0100 Subject: [PATCH 081/108] More inbox arguments --- daemon.py | 57 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/daemon.py b/daemon.py index 978317d1e..254a8ea41 100644 --- a/daemon.py +++ b/daemon.py @@ -5463,14 +5463,24 @@ class PubServer(BaseHTTPRequestHandler): onionDomain: str, i2pDomain: str, GETstartTime, GETtimings: {}, proxyType: str, cookie: str, - debug: str) -> bool: + debug: str, + recentPostsCache: {}, session, + ocapAlways: bool, + defaultTimeline: str, + maxRecentPosts: int, + translate: {}, + cachedWebfingers: {}, + personCache: {}, + allowDeletion: bool, + projectVersion: str, + YTReplacementDomain: str) -> bool: """Shows the inbox timeline """ if '/users/' in path: if authorized: inboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, + personBoxJson(recentPostsCache, + session, baseDir, domain, port, @@ -5478,7 +5488,7 @@ class PubServer(BaseHTTPRequestHandler): httpPrefix, maxPostsInFeed, 'inbox', authorized, - self.server.ocapAlways) + ocapAlways) if inboxFeed: self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', @@ -5497,8 +5507,8 @@ class PubServer(BaseHTTPRequestHandler): if 'page=' not in path: # if no page was specified then show the first inboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, + personBoxJson(recentPostsCache, + session, baseDir, domain, port, @@ -5506,29 +5516,29 @@ class PubServer(BaseHTTPRequestHandler): httpPrefix, maxPostsInFeed, 'inbox', authorized, - self.server.ocapAlways) + ocapAlways) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', 'show inbox page') - msg = htmlInbox(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, + msg = htmlInbox(defaultTimeline, + recentPostsCache, + maxRecentPosts, + translate, pageNumber, maxPostsInFeed, - self.server.session, + session, baseDir, - self.server.cachedWebfingers, - self.server.personCache, + cachedWebfingers, + personCache, nickname, domain, port, inboxFeed, - self.server.allowDeletion, + allowDeletion, httpPrefix, - self.server.projectVersion, + projectVersion, self._isMinimal(nickname), - self.server.YTReplacementDomain) + YTReplacementDomain) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', 'show inbox html') @@ -8428,7 +8438,18 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2pDomain, GETstartTime, GETtimings, self.server.proxyType, - cookie, self.server.debug): + cookie, self.server.debug, + self.server.recentPostsCache, + self.server.session, + self.server.ocapAlways, + self.server.defaultTimeline, + self.server.maxRecentPosts, + self.server.translate, + self.server.cachedWebfingers, + self.server.personCache, + self.server.allowDeletion, + self.server.projectVersion, + self.server.YTReplacementDomain): return self._benchmarkGETtimings(GETstartTime, GETtimings, From eefbbae7d013b1e59173c6c5f66275cbe16e9a81 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 21:46:50 +0100 Subject: [PATCH 082/108] Roadmap --- README_roadmap.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 README_roadmap.md diff --git a/README_roadmap.md b/README_roadmap.md new file mode 100644 index 000000000..39ad1fee5 --- /dev/null +++ b/README_roadmap.md @@ -0,0 +1,16 @@ +# Roadman + +## UX + * Change animation on buttons (themeable?) + +## Teams + + * Test groups + * Groups can be defined as having particular roles/skills + * Templates for different group organizations + +## Events + + * Events timeline + * Events appear on calendar + * Check compatibility with Mobilizon \ No newline at end of file From 993a463c708156bf17d105b2b1fa1be837451897 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 1 Sep 2020 21:48:57 +0100 Subject: [PATCH 083/108] Roadmap --- README_roadmap.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README_roadmap.md b/README_roadmap.md index 39ad1fee5..62b2ba3ba 100644 --- a/README_roadmap.md +++ b/README_roadmap.md @@ -13,4 +13,10 @@ * Events timeline * Events appear on calendar - * Check compatibility with Mobilizon \ No newline at end of file + * Check compatibility with Mobilizon + +## Code + + * Modularize daemon + * Move modules out of the daemon + * Make comment notes linking daemon functions to webinterface \ No newline at end of file From eaac57228e4ef7522735e47ab2117631b1815876 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 10:44:37 +0100 Subject: [PATCH 084/108] Move blog page to its own module --- daemon.py | 112 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 68 insertions(+), 44 deletions(-) diff --git a/daemon.py b/daemon.py index 254a8ea41..a5be074ee 100644 --- a/daemon.py +++ b/daemon.py @@ -6732,6 +6732,61 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showBlogPage(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + translate: {}, debug: str) -> bool: + """Shows a blog page + """ + pageNumber = 1 + nickname = path.split('/blog/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if '?' in nickname: + nickname = nickname.split('?')[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) + if pageNumber < 1: + pageNumber = 1 + elif pageNumber > 10: + pageNumber = 10 + if not self.server.session: + print('Starting new session during blog page') + self.server.session = createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during blog page') + self._404() + return True + msg = htmlBlogPage(authorized, + self.server.session, + baseDir, + httpPrefix, + translate, + nickname, + domain, port, + maxPostsInBlogsFeed, pageNumber) + if msg is not None: + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'blog view done', 'blog page') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -6970,51 +7025,20 @@ class PubServer(BaseHTTPRequestHandler): # for a particular account if htmlGET and self.path.startswith('/blog/'): if '/rss.xml' not in self.path: - pageNumber = 1 - nickname = self.path.split('/blog/')[1] - if '/' in nickname: - nickname = nickname.split('/')[0] - if '?' in nickname: - nickname = nickname.split('?')[0] - if '?page=' in self.path: - pageNumberStr = self.path.split('?page=')[1] - if '?' in pageNumberStr: - pageNumberStr = pageNumberStr.split('?')[0] - if '#' in pageNumberStr: - pageNumberStr = pageNumberStr.split('#')[0] - if pageNumberStr.isdigit(): - pageNumber = int(pageNumberStr) - if pageNumber < 1: - pageNumber = 1 - elif pageNumber > 10: - pageNumber = 10 - if not self.server.session: - print('Starting new session during blog page') - self.server.session = \ - createSession(self.server.proxyType) - if not self.server.session: - print('ERROR: GET failed to create session ' + - 'during blog page') - self._404() - return - msg = htmlBlogPage(authorized, - self.server.session, - self.server.baseDir, - self.server.httpPrefix, - self.server.translate, - nickname, - self.server.domain, self.server.port, - maxPostsInBlogsFeed, pageNumber) - if msg is not None: - msg = msg.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'blog view done', 'blog page') + if self._showBlogPage(authorized, + 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, GETtimings, + self.server.proxyType, + cookie, self.server.translate, + self.server.debug): return - self._404() - return # list of registered devices for e2ee # see https://github.com/tootsuite/mastodon/pull/13820 From 0a656ab9ca11a4cb03a50f3e2ae99ba3109f87e7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 10:57:16 +0100 Subject: [PATCH 085/108] Move redirect to login screen to its own module --- daemon.py | 111 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/daemon.py b/daemon.py index a5be074ee..6606b9711 100644 --- a/daemon.py +++ b/daemon.py @@ -6787,6 +6787,61 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _redirectToLoginScreen(self, callingDomain: str, path: str, + httpPrefix: str, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + authorized: bool, debug: bool): + """Redirects to the login screen if necessary + """ + divertToLoginScreen = False + if '/media/' not in path and \ + '/sharefiles/' not in path and \ + '/statuses/' not in path and \ + '/emoji/' not in path and \ + '/tags/' not in path and \ + '/avatars/' not in path and \ + '/fonts/' not in path and \ + '/icons/' not in path: + divertToLoginScreen = True + if path.startswith('/users/'): + nickStr = path.split('/users/')[1] + if '/' not in nickStr and '?' not in nickStr: + divertToLoginScreen = False + else: + if path.endswith('/following') or \ + '/following?page=' in path or \ + path.endswith('/followers') or \ + '/followers?page=' in path or \ + path.endswith('/skills') or \ + path.endswith('/roles') or \ + path.endswith('/shares'): + divertToLoginScreen = False + + if divertToLoginScreen and not authorized: + if debug: + print('DEBUG: divertToLoginScreen=' + + str(divertToLoginScreen)) + print('DEBUG: authorized=' + str(authorized)) + print('DEBUG: path=' + path) + if callingDomain.endswith('.onion') and onionDomain: + self._redirect_headers('http://' + + onionDomain + '/login', + None, callingDomain) + elif callingDomain.endswith('.i2p') and i2pDomain: + self._redirect_headers('http://' + + i2pDomain + '/login', + None, callingDomain) + else: + self._redirect_headers(httpPrefix + '://' + + domainFull + + '/login', None, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'robots txt', + 'show login screen') + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7238,54 +7293,14 @@ class PubServer(BaseHTTPRequestHandler): # if not authorized then show the login screen if htmlGET and self.path != '/login' and \ not self._pathIsImage(self.path) and self.path != '/': - if '/media/' not in self.path and \ - '/sharefiles/' not in self.path and \ - '/statuses/' not in self.path and \ - '/emoji/' not in self.path and \ - '/tags/' not in self.path and \ - '/avatars/' not in self.path and \ - '/fonts/' not in self.path and \ - '/icons/' not in self.path: - divertToLoginScreen = True - if self.path.startswith('/users/'): - nickStr = self.path.split('/users/')[1] - if '/' not in nickStr and '?' not in nickStr: - divertToLoginScreen = False - else: - if self.path.endswith('/following') or \ - '/following?page=' in self.path or \ - self.path.endswith('/followers') or \ - '/followers?page=' in self.path or \ - self.path.endswith('/skills') or \ - self.path.endswith('/roles') or \ - self.path.endswith('/shares'): - divertToLoginScreen = False - if divertToLoginScreen and not authorized: - if self.server.debug: - print('DEBUG: divertToLoginScreen=' + - str(divertToLoginScreen)) - print('DEBUG: authorized=' + str(authorized)) - print('DEBUG: path=' + self.path) - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - self._redirect_headers('http://' + - self.server.onionDomain + - '/login', - None, callingDomain) - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - self._redirect_headers('http://' + - self.server.i2pDomain + - '/login', - None, callingDomain) - else: - self._redirect_headers(self.server.httpPrefix + '://' + - self.server.domainFull + - '/login', None, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'robots txt', - 'show login screen') - return + if self._redirectToLoginScreen(callingDomain, self.path, + self.server.httpPrefix, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + authorized, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'robots txt', From c6c17f4743e8664ab505d7d5c9d1e93fc4b7f1f6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 11:49:35 +0100 Subject: [PATCH 086/108] Fix css --- daemon.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/daemon.py b/daemon.py index 6606b9711..de5e2900f 100644 --- a/daemon.py +++ b/daemon.py @@ -7309,11 +7309,15 @@ class PubServer(BaseHTTPRequestHandler): # get css # Note that this comes before the busy flag to avoid conflicts if self.path.endswith('.css'): - if os.path.isfile('epicyon-profile.css'): + # get the last part of the path + # eg. /my/path/file.css becomes file.css + if '/' in self.path: + self.path = self.path.split('/')[-1] + if os.path.isfile(self.path): tries = 0 while tries < 5: try: - with open('epicyon-profile.css', 'r') as cssfile: + with open(self.path, 'r') as cssfile: css = cssfile.read() break except Exception as e: From 13e23763fb5bc15f91e2d549d024c6b2215a3b29 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 11:52:07 +0100 Subject: [PATCH 087/108] Don't need to send cookie --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index de5e2900f..5c0231d6f 100644 --- a/daemon.py +++ b/daemon.py @@ -7326,7 +7326,7 @@ class PubServer(BaseHTTPRequestHandler): tries += 1 msg = css.encode('utf-8') self._set_headers('text/css', len(msg), - cookie, callingDomain) + None, callingDomain) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show login screen done', From 3b7d8a721f6f4912033f2d4faa55c439fe8c32de Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 11:57:50 +0100 Subject: [PATCH 088/108] Move style sheet to its own method --- daemon.py | 56 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/daemon.py b/daemon.py index 5c0231d6f..d374fd176 100644 --- a/daemon.py +++ b/daemon.py @@ -6842,6 +6842,36 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _getStyleSheet(self, callingDomain: str, path: str, + GETstartTime, GETtimings: {}) -> bool: + """Returns the content of a css file + """ + # get the last part of the path + # eg. /my/path/file.css becomes file.css + if '/' in path: + path = path.split('/')[-1] + if os.path.isfile(path): + tries = 0 + while tries < 5: + try: + with open(path, 'r') as cssfile: + css = cssfile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + msg = css.encode('utf-8') + self._set_headers('text/css', len(msg), + None, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show login screen done', + 'show profile.css') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7309,31 +7339,9 @@ class PubServer(BaseHTTPRequestHandler): # get css # Note that this comes before the busy flag to avoid conflicts if self.path.endswith('.css'): - # get the last part of the path - # eg. /my/path/file.css becomes file.css - if '/' in self.path: - self.path = self.path.split('/')[-1] - if os.path.isfile(self.path): - tries = 0 - while tries < 5: - try: - with open(self.path, 'r') as cssfile: - css = cssfile.read() - break - except Exception as e: - print(e) - time.sleep(1) - tries += 1 - msg = css.encode('utf-8') - self._set_headers('text/css', len(msg), - None, callingDomain) - self._write(msg) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show login screen done', - 'show profile.css') + if self._getStyleSheet(callingDomain, self.path, + GETstartTime, GETtimings): return - self._404() - return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show login screen done', From 167a5e3b5e66f83aa59d02d8a6c038cc2ede18c8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 12:20:12 +0100 Subject: [PATCH 089/108] Move qr code to its own method --- daemon.py | 79 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/daemon.py b/daemon.py index d374fd176..2e3e05565 100644 --- a/daemon.py +++ b/daemon.py @@ -6872,6 +6872,44 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _showQRcode(self, callingDomain: str, path: str, + baseDir: str, domain: str, port: int, + GETstartTime, GETtimings: {}) -> bool: + """Shows a QR code for an account + """ + nickname = getNicknameFromActor(path) + savePersonQrcode(baseDir, nickname, domain, port) + qrFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + '/qrcode.png' + if os.path.isfile(qrFilename): + if self._etag_exists(qrFilename): + # The file has not changed + self._304() + return + + tries = 0 + mediaBinary = None + while tries < 5: + try: + with open(qrFilename, 'rb') as avFile: + mediaBinary = avFile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + if mediaBinary: + self._set_headers_etag(qrFilename, 'image/png', + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'login screen logo done', + 'account qrcode') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7480,41 +7518,12 @@ class PubServer(BaseHTTPRequestHandler): # QR code for account handle if '/users/' in self.path and \ self.path.endswith('/qrcode.png'): - nickname = getNicknameFromActor(self.path) - savePersonQrcode(self.server.baseDir, - nickname, self.server.domain, - self.server.port) - qrFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + '/qrcode.png' - if os.path.isfile(qrFilename): - if self._etag_exists(qrFilename): - # The file has not changed - self._304() - return - - tries = 0 - mediaBinary = None - while tries < 5: - try: - with open(qrFilename, 'rb') as avFile: - mediaBinary = avFile.read() - break - except Exception as e: - print(e) - time.sleep(1) - tries += 1 - if mediaBinary: - self._set_headers_etag(qrFilename, 'image/png', - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'login screen logo done', - 'account qrcode') - return - self._404() - return + if self._showQRcode(callingDomain, self.path, + self.server.baseDir, + self.server.domain, + self.server.port, + GETstartTime, GETtimings): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'login screen logo done', From 4e09974de73431578d825d8cfbf36e99b0462a1e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 12:54:44 +0100 Subject: [PATCH 090/108] Move search screen banner to its own module --- daemon.py | 76 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/daemon.py b/daemon.py index 2e3e05565..c602a871b 100644 --- a/daemon.py +++ b/daemon.py @@ -6910,6 +6910,44 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _searchScreenBanner(self, callingDomain: str, path: str, + baseDir: str, domain: str, port: int, + GETstartTime, GETtimings: {}) -> bool: + """Shows a banner image on the search screen + """ + nickname = getNicknameFromActor(path) + bannerFilename = \ + baseDir + '/accounts/' + \ + nickname + '@' + domain + '/search_banner.png' + if os.path.isfile(bannerFilename): + if self._etag_exists(bannerFilename): + # The file has not changed + self._304() + return True + + tries = 0 + mediaBinary = None + while tries < 5: + try: + with open(bannerFilename, 'rb') as avFile: + mediaBinary = avFile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + if mediaBinary: + self._set_headers_etag(bannerFilename, 'image/png', + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'account qrcode done', + 'search screen banner') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7532,38 +7570,12 @@ class PubServer(BaseHTTPRequestHandler): # search screen banner image if '/users/' in self.path and \ self.path.endswith('/search_banner.png'): - nickname = getNicknameFromActor(self.path) - bannerFilename = \ - self.server.baseDir + '/accounts/' + \ - nickname + '@' + self.server.domain + '/search_banner.png' - if os.path.isfile(bannerFilename): - if self._etag_exists(bannerFilename): - # The file has not changed - self._304() - return - - tries = 0 - mediaBinary = None - while tries < 5: - try: - with open(bannerFilename, 'rb') as avFile: - mediaBinary = avFile.read() - break - except Exception as e: - print(e) - time.sleep(1) - tries += 1 - if mediaBinary: - self._set_headers_etag(bannerFilename, 'image/png', - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'account qrcode done', - 'search screen banner') - return - self._404() - return + if self._searchScreenBanner(callingDomain, self.path, + self.server.baseDir, + self.server.domain, + self.server.port, + GETstartTime, GETtimings): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'account qrcode done', From 33d5ea22dea3b27fff9e99ca5fc2ba8e5c628620 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 13:01:26 +0100 Subject: [PATCH 091/108] Move background image to its own method --- daemon.py | 90 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/daemon.py b/daemon.py index c602a871b..b90282527 100644 --- a/daemon.py +++ b/daemon.py @@ -6948,6 +6948,52 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _showBackgroundImage(self, callingDomain: str, path: str, + baseDir: str, + GETstartTime, GETtimings: {}) -> bool: + """Show a background image + """ + for ext in ('webp', 'gif', 'jpg', 'png'): + for bg in ('follow', 'options', 'login'): + # follow screen background image + if path.endswith('/' + bg + '-background.' + ext): + bgFilename = \ + baseDir + '/accounts/' + \ + bg + '-background.' + ext + if os.path.isfile(bgFilename): + if self._etag_exists(bgFilename): + # The file has not changed + self._304() + return True + + tries = 0 + bgBinary = None + while tries < 5: + try: + with open(bgFilename, 'rb') as avFile: + bgBinary = avFile.read() + break + except Exception as e: + print(e) + time.sleep(1) + tries += 1 + if bgBinary: + if ext == 'jpg': + ext = 'jpeg' + self._set_headers_etag(bgFilename, + 'image/' + ext, + bgBinary, None, + callingDomain) + self._write(bgBinary) + self._benchmarkGETtimings(GETstartTime, + GETtimings, + 'search screen ' + + 'banner done', + 'background shown') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7582,46 +7628,10 @@ class PubServer(BaseHTTPRequestHandler): 'search screen banner done') if '-background.' in self.path: - for ext in ('webp', 'gif', 'jpg', 'png'): - for bg in ('follow', 'options', 'login'): - # follow screen background image - if self.path.endswith('/' + bg + '-background.' + ext): - bgFilename = \ - self.server.baseDir + '/accounts/' + \ - bg + '-background.' + ext - if os.path.isfile(bgFilename): - if self._etag_exists(bgFilename): - # The file has not changed - self._304() - return - - tries = 0 - bgBinary = None - while tries < 5: - try: - with open(bgFilename, 'rb') as avFile: - bgBinary = avFile.read() - break - except Exception as e: - print(e) - time.sleep(1) - tries += 1 - if bgBinary: - if ext == 'jpg': - ext = 'jpeg' - self._set_headers_etag(bgFilename, - 'image/' + ext, - bgBinary, cookie, - callingDomain) - self._write(bgBinary) - self._benchmarkGETtimings(GETstartTime, - GETtimings, - 'search screen ' + - 'banner done', - 'background shown') - return - self._404() - return + if self._showBackgroundImage(callingDomain, self.path, + self.server.baseDir, + GETstartTime, GETtimings): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'search screen banner done', From 902b3e4a252e9868dd047071858c4e216e432e37 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 13:11:15 +0100 Subject: [PATCH 092/108] Move shared item image to its own method --- daemon.py | 74 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/daemon.py b/daemon.py index b90282527..ba07fd573 100644 --- a/daemon.py +++ b/daemon.py @@ -6994,6 +6994,44 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _showShareImage(self, callingDomain: str, path: str, + baseDir: str, + GETstartTime, GETtimings: {}) -> bool: + """Show a shared item image + """ + if self._pathIsImage(path): + mediaStr = path.split('/sharefiles/')[1] + mediaFilename = \ + baseDir + '/sharefiles/' + mediaStr + if os.path.isfile(mediaFilename): + if self._etag_exists(mediaFilename): + # The file has not changed + self._304() + return True + + mediaFileType = 'png' + if mediaFilename.endswith('.png'): + mediaFileType = 'png' + elif mediaFilename.endswith('.jpg'): + mediaFileType = 'jpeg' + elif mediaFilename.endswith('.webp'): + mediaFileType = 'webp' + else: + mediaFileType = 'gif' + with open(mediaFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self._set_headers_etag(mediaFilename, + 'image/' + mediaFileType, + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show media done', + 'share files shown') + return True + self._404() + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7663,38 +7701,10 @@ class PubServer(BaseHTTPRequestHandler): # show shared item images # Note that this comes before the busy flag to avoid conflicts if '/sharefiles/' in self.path: - if self._pathIsImage(self.path): - mediaStr = self.path.split('/sharefiles/')[1] - mediaFilename = \ - self.server.baseDir + '/sharefiles/' + mediaStr - if os.path.isfile(mediaFilename): - if self._etag_exists(mediaFilename): - # The file has not changed - self._304() - return - - mediaFileType = 'png' - if mediaFilename.endswith('.png'): - mediaFileType = 'png' - elif mediaFilename.endswith('.jpg'): - mediaFileType = 'jpeg' - elif mediaFilename.endswith('.webp'): - mediaFileType = 'webp' - else: - mediaFileType = 'gif' - with open(mediaFilename, 'rb') as avFile: - mediaBinary = avFile.read() - self._set_headers_etag(mediaFilename, - 'image/' + mediaFileType, - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'show media done', - 'share files shown') - return - self._404() - return + if self._showShareImage(callingDomain, self.path, + self.server.baseDir, + GETstartTime, GETtimings): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show media done', From 758ac014ce9a7bd61b78270dedfddc29444eeb42 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 13:18:49 +0100 Subject: [PATCH 093/108] Move avatar image to its own method --- daemon.py | 91 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/daemon.py b/daemon.py index ba07fd573..390770a08 100644 --- a/daemon.py +++ b/daemon.py @@ -7032,6 +7032,52 @@ class PubServer(BaseHTTPRequestHandler): self._404() return True + def _showAvatarOrBackground(self, callingDomain: str, path: str, + baseDir: str, domain: str, + GETstartTime, GETtimings: {}) -> bool: + """Shows an avatar or profile background image + """ + if '/users/' in path: + if self._pathIsImage(path): + avatarStr = path.split('/users/')[1] + if '/' in avatarStr and '.temp.' not in path: + avatarNickname = avatarStr.split('/')[0] + avatarFile = avatarStr.split('/')[1] + # remove any numbers, eg. avatar123.png becomes avatar.png + if avatarFile.startswith('avatar'): + avatarFile = 'avatar.' + avatarFile.split('.')[1] + elif avatarFile.startswith('image'): + avatarFile = 'image.' + avatarFile.split('.')[1] + avatarFilename = \ + baseDir + '/accounts/' + \ + avatarNickname + '@' + domain + '/' + avatarFile + if os.path.isfile(avatarFilename): + if self._etag_exists(avatarFilename): + # The file has not changed + self._304() + return True + mediaImageType = 'png' + if avatarFile.endswith('.png'): + mediaImageType = 'png' + elif avatarFile.endswith('.jpg'): + mediaImageType = 'jpeg' + elif avatarFile.endswith('.gif'): + mediaImageType = 'gif' + else: + mediaImageType = 'webp' + with open(avatarFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self._set_headers_etag(avatarFilename, + 'image/' + mediaImageType, + mediaBinary, None, + callingDomain) + self._write(mediaBinary) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'icon shown done', + 'avatar background shown') + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7736,46 +7782,11 @@ class PubServer(BaseHTTPRequestHandler): # show avatar or background image # Note that this comes before the busy flag to avoid conflicts - if '/users/' in self.path: - if self._pathIsImage(self.path): - avatarStr = self.path.split('/users/')[1] - if '/' in avatarStr and '.temp.' not in self.path: - avatarNickname = avatarStr.split('/')[0] - avatarFile = avatarStr.split('/')[1] - # remove any numbers, eg. avatar123.png becomes avatar.png - if avatarFile.startswith('avatar'): - avatarFile = 'avatar.' + avatarFile.split('.')[1] - elif avatarFile.startswith('image'): - avatarFile = 'image.'+avatarFile.split('.')[1] - avatarFilename = \ - self.server.baseDir + '/accounts/' + \ - avatarNickname + '@' + \ - self.server.domain + '/' + avatarFile - if os.path.isfile(avatarFilename): - if self._etag_exists(avatarFilename): - # The file has not changed - self._304() - return - mediaImageType = 'png' - if avatarFile.endswith('.png'): - mediaImageType = 'png' - elif avatarFile.endswith('.jpg'): - mediaImageType = 'jpeg' - elif avatarFile.endswith('.gif'): - mediaImageType = 'gif' - else: - mediaImageType = 'webp' - with open(avatarFilename, 'rb') as avFile: - mediaBinary = avFile.read() - self._set_headers_etag(avatarFilename, - 'image/' + mediaImageType, - mediaBinary, cookie, - callingDomain) - self._write(mediaBinary) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'icon shown done', - 'avatar background shown') - return + if self._showAvatarOrBackground(callingDomain, self.path, + self.server.baseDir, + self.server.domain, + GETstartTime, GETtimings): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'icon shown done', From bd0783a8d5c5d8d26ae187c78584435fd35793cc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 13:32:05 +0100 Subject: [PATCH 094/108] Move delete confirm to its own method --- daemon.py | 116 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/daemon.py b/daemon.py index 390770a08..1925f9941 100644 --- a/daemon.py +++ b/daemon.py @@ -7078,6 +7078,63 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _confirmDeleteEvent(self, callingDomain: str, path: str, + baseDir: str, httpPrefix: str, cookie: str, + translate: {}, domainFull: str, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}) -> bool: + """Confirm whether to delete a calendar event + """ + postId = path.split('?id=')[1] + if '?' in postId: + postId = postId.split('?')[0] + postTime = path.split('?time=')[1] + if '?' in postTime: + postTime = postTime.split('?')[0] + postYear = path.split('?year=')[1] + if '?' in postYear: + postYear = postYear.split('?')[0] + postMonth = path.split('?month=')[1] + if '?' in postMonth: + postMonth = postMonth.split('?')[0] + postDay = path.split('?day=')[1] + if '?' in postDay: + postDay = postDay.split('?')[0] + # show the confirmation screen screen + msg = htmlCalendarDeleteConfirm(translate, + baseDir, + path, + httpPrefix, + domainFull, + postId, postTime, + postYear, postMonth, postDay, + callingDomain) + if not msg: + actor = \ + httpPrefix + '://' + \ + domainFull + \ + path.split('/eventdelete')[0] + if callingDomain.endswith('.onion') and onionDomain: + actor = \ + 'http://' + onionDomain + \ + path.split('/eventdelete')[0] + elif callingDomain.endswith('.i2p') and i2pDomain: + actor = \ + 'http://' + i2pDomain + \ + path.split('/eventdelete')[0] + self._redirect_headers(actor + '/calendar', + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'calendar shown done', + 'calendar delete shown') + return True + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -7930,57 +7987,16 @@ class PubServer(BaseHTTPRequestHandler): if '/eventdelete' in self.path and \ '?time=' in self.path and \ '?id=' in self.path: - postId = self.path.split('?id=')[1] - if '?' in postId: - postId = postId.split('?')[0] - postTime = self.path.split('?time=')[1] - if '?' in postTime: - postTime = postTime.split('?')[0] - postYear = self.path.split('?year=')[1] - if '?' in postYear: - postYear = postYear.split('?')[0] - postMonth = self.path.split('?month=')[1] - if '?' in postMonth: - postMonth = postMonth.split('?')[0] - postDay = self.path.split('?day=')[1] - if '?' in postDay: - postDay = postDay.split('?')[0] - # show the confirmation screen screen - msg = htmlCalendarDeleteConfirm(self.server.translate, - self.server.baseDir, - self.path, - self.server.httpPrefix, - self.server.domainFull, - postId, postTime, - postYear, postMonth, postDay, - callingDomain) - if not msg: - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + \ - self.path.split('/eventdelete')[0] - if callingDomain.endswith('.onion') and \ - self.server.onionDomain: - actor = \ - 'http://' + self.server.onionDomain + \ - self.path.split('/eventdelete')[0] - elif (callingDomain.endswith('.i2p') and - self.server.i2pDomain): - actor = \ - 'http://' + self.server.i2pDomain + \ - self.path.split('/eventdelete')[0] - self._redirect_headers(actor + '/calendar', - cookie, callingDomain) - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'calendar shown done', - 'calendar delete shown') + if self._confirmDeleteEvent(callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + cookie, + self.server.translate, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings): return - msg = msg.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - return self._benchmarkGETtimings(GETstartTime, GETtimings, 'calendar shown done', From 7cfa74df06e2ceaf10d8a2c593245fd5d83c892f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 17:32:50 +0100 Subject: [PATCH 095/108] Single write function --- daemon.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/daemon.py b/daemon.py index 1925f9941..c102eabd4 100644 --- a/daemon.py +++ b/daemon.py @@ -624,9 +624,7 @@ class PubServer(BaseHTTPRequestHandler): self.send_header('Content-Length', str(len(msg))) self.send_header('X-Robots-Tag', 'noindex') self.end_headers() - try: - self.wfile.write(msg) - except Exception as e: + if not self._write(msg): print('Error when showing ' + str(httpCode)) print(e) @@ -685,16 +683,17 @@ class PubServer(BaseHTTPRequestHandler): 'The server is busy. Please try again ' + 'later') - def _write(self, msg) -> None: + def _write(self, msg) -> bool: tries = 0 while tries < 5: try: self.wfile.write(msg) - break + return True except Exception as e: print(e) - time.sleep(1) + time.sleep(0.5) tries += 1 + return False def _robotsTxt(self) -> bool: if not self.path.lower().startswith('/robot'): From 632d2977dd97e0aefecfb940037ad232f689c726 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 17:34:05 +0100 Subject: [PATCH 096/108] Increment tries --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index c102eabd4..f36cdf0c1 100644 --- a/daemon.py +++ b/daemon.py @@ -692,7 +692,7 @@ class PubServer(BaseHTTPRequestHandler): except Exception as e: print(e) time.sleep(0.5) - tries += 1 + tries += 1 return False def _robotsTxt(self) -> bool: From 38f9f91664d9c51171d93b19004de3e2ea6ae7a9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 18:09:51 +0100 Subject: [PATCH 097/108] Move new post to its own module --- daemon.py | 94 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/daemon.py b/daemon.py index f36cdf0c1..bdacb70f5 100644 --- a/daemon.py +++ b/daemon.py @@ -626,7 +626,6 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() if not self._write(msg): print('Error when showing ' + str(httpCode)) - print(e) def _200(self) -> None: if self.server.translate: @@ -7134,6 +7133,53 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showNewPost(self, callingDomain: str, path: str, + mediaInstance: bool, translate: {}, + baseDir: str, httpPrefix: str, + inReplyToUrl: str, replyToList: [], + shareDescription: str, replyPageNumber: int, + domain: str, domainFull: str, + GETstartTime, GETtimings: {}, cookie) -> bool: + """Shows the new post screen + """ + isNewPostEndpoint = False + if '/users/' in path and '/new' in path: + # Various types of new post in the web interface + newPostEnd = ('newpost', 'newblog', 'newunlisted', + 'newfollowers', 'newdm', 'newreminder', + 'newevent', 'newreport', 'newquestion', + 'newshare') + for postType in newPostEnd: + if path.endswith('/' + postType): + isNewPostEndpoint = True + break + if isNewPostEndpoint: + nickname = getNicknameFromActor(path) + msg = htmlNewPost(mediaInstance, + translate, + baseDir, + httpPrefix, + path, inReplyToUrl, + replyToList, + shareDescription, + replyPageNumber, + nickname, domain, + domainFull).encode('utf-8') + if not msg: + print('Error replying to ' + inReplyToUrl) + self._404() + self.server.GETbusy = False + return True + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unmute activated done', + 'new post made') + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8403,41 +8449,17 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return - # Various types of new post in the web interface - if ('/users/' in self.path and - (self.path.endswith('/newpost') or - self.path.endswith('/newblog') or - self.path.endswith('/newunlisted') or - self.path.endswith('/newfollowers') or - self.path.endswith('/newdm') or - self.path.endswith('/newreminder') or - self.path.endswith('/newevent') or - self.path.endswith('/newreport') or - self.path.endswith('/newquestion') or - self.path.endswith('/newshare'))): - nickname = getNicknameFromActor(self.path) - msg = htmlNewPost(self.server.mediaInstance, - self.server.translate, - self.server.baseDir, - self.server.httpPrefix, - self.path, inReplyToUrl, - replyToList, - shareDescription, - replyPageNumber, - nickname, self.server.domain, - self.server.domainFull).encode('utf-8') - if not msg: - print('Error replying to ' + inReplyToUrl) - self._404() - self.server.GETbusy = False - return - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - self._benchmarkGETtimings(GETstartTime, GETtimings, - 'unmute activated done', - 'new post made') + if self._showNewPost(callingDomain, self.path, + self.server.mediaInstance, + self.server.translate, + self.server.baseDir, + self.server.httpPrefix, + inReplyToUrl, replyToList, + shareDescription, replyPageNumber, + self.server.domain, + self.server.domainFull, + GETstartTime, GETtimings, + cookie): return self._benchmarkGETtimings(GETstartTime, GETtimings, From 2a2279b05c5cf49181c72727d12c8b3b83609d2b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 18:23:36 +0100 Subject: [PATCH 098/108] Move edit profile screen to its own method --- daemon.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/daemon.py b/daemon.py index bdacb70f5..0cd4e3afc 100644 --- a/daemon.py +++ b/daemon.py @@ -7180,6 +7180,28 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _editProfile(self, callingDomain: str, path: str, + translate: {}, baseDir: str, + httpPrefix: str, domain: str, port: int, + cookie: str) -> bool: + """Show the edit profile screen + """ + if '/users/' in path and path.endswith('/editprofile'): + msg = htmlEditProfile(translate, + baseDir, + path, domain, + port, + httpPrefix).encode('utf-8') + if msg: + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + else: + self._404() + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8434,19 +8456,13 @@ class PubServer(BaseHTTPRequestHandler): return # edit profile in web interface - if '/users/' in self.path and self.path.endswith('/editprofile'): - msg = htmlEditProfile(self.server.translate, - self.server.baseDir, - self.path, self.server.domain, - self.server.port, - self.server.httpPrefix).encode('utf-8') - if msg: - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - else: - self._404() - self.server.GETbusy = False + if self._editProfile(callingDomain, self.path, + self.server.translate, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.port, + cookie): return if self._showNewPost(callingDomain, self.path, From 9a1de6d01214e2c20fc16b6940a75373cac412b4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 18:33:16 +0100 Subject: [PATCH 099/108] Move edit event screen to its own method --- daemon.py | 74 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/daemon.py b/daemon.py index 0cd4e3afc..ca39a6623 100644 --- a/daemon.py +++ b/daemon.py @@ -7202,6 +7202,43 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _editEvent(self, callingDomain: str, path: str, + httpPrefix: str, domain: str, domainFull: str, + baseDir: str, translate: {}, + mediaInstance: bool, + cookie: str) -> bool: + """Show edit event screen + """ + messageId = path.split('?editeventpost=')[1] + if '?' in messageId: + messageId = messageId.split('?')[0] + actor = path.split('?actor=')[1] + if '?' in actor: + actor = actor.split('?')[0] + nickname = getNicknameFromActor(path) + if nickname == actor: + # postUrl = \ + # httpPrefix + '://' + \ + # domainFull + '/users/' + nickname + \ + # '/statuses/' + messageId + msg = None + # TODO + # htmlEditEvent(mediaInstance, + # translate, + # baseDir, + # httpPrefix, + # path, + # nickname, domain, + # postUrl) + if msg: + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + return True + return False + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -8426,34 +8463,15 @@ class PubServer(BaseHTTPRequestHandler): '/tlevents' in self.path and \ '?editeventpost=' in self.path and \ '?actor=' in self.path: - messageId = self.path.split('?editeventpost=')[1] - if '?' in messageId: - messageId = messageId.split('?')[0] - actor = self.path.split('?actor=')[1] - if '?' in actor: - actor = actor.split('?')[0] - nickname = getNicknameFromActor(self.path) - if nickname == actor: - postUrl = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + nickname + \ - '/statuses/' + messageId - msg = None - # TODO - # htmlEditEvent(self.server.mediaInstance, - # self.server.translate, - # self.server.baseDir, - # self.server.httpPrefix, - # self.path, - # nickname, self.server.domain, - # postUrl) - if msg: - msg = msg.encode('utf-8') - self._set_headers('text/html', len(msg), - cookie, callingDomain) - self._write(msg) - self.server.GETbusy = False - return + if self._editEvent(callingDomain, self.path, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.baseDir, + self.server.translate, + self.server.mediaInstance, + cookie): + return # edit profile in web interface if self._editProfile(callingDomain, self.path, From fc2c6e6aa08f1243f9b154eaaa500101f208c3dd Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 2 Sep 2020 22:56:54 +0100 Subject: [PATCH 100/108] Create accounts directory before qrcode gets created --- daemon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon.py b/daemon.py index ca39a6623..c4c929ca2 100644 --- a/daemon.py +++ b/daemon.py @@ -10515,6 +10515,10 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool, serverAddress = ('', proxyPort) pubHandler = partial(PubServer) + if not os.path.isdir(baseDir + '/accounts'): + print('Creating accounts directory') + os.mkdir(baseDir + '/accounts') + try: httpd = EpicyonServer(serverAddress, pubHandler) except Exception as e: From 61ff46ad1c804f13428234e4b7115764767c8b88 Mon Sep 17 00:00:00 2001 From: sireebob Date: Wed, 2 Sep 2020 22:42:29 +0000 Subject: [PATCH 101/108] prevent an error for posts missing an inReplyTo key. --- inbox.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/inbox.py b/inbox.py index f5ae165a7..58a19ee24 100644 --- a/inbox.py +++ b/inbox.py @@ -2364,17 +2364,18 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int, if nickname != 'inbox': # replies index will be updated updateIndexList.append('tlreplies') - inReplyTo = postJsonObject['object']['inReplyTo'] - if inReplyTo: - if isinstance(inReplyTo, str): - if not isMuted(baseDir, nickname, domain, - inReplyTo): - replyNotify(baseDir, handle, - httpPrefix + '://' + domain + - '/users/' + nickname + - '/tlreplies') - else: - isReplyToMutedPost = True + if postJsonObject['object'].get('inReplyTo'): + inReplyTo = postJsonObject['object']['inReplyTo'] + if inReplyTo: + if isinstance(inReplyTo, str): + if not isMuted(baseDir, nickname, domain, + inReplyTo): + replyNotify(baseDir, handle, + httpPrefix + '://' + domain + + '/users/' + nickname + + '/tlreplies') + else: + isReplyToMutedPost = True if isImageMedia(session, baseDir, httpPrefix, nickname, domain, postJsonObject, From a6a715df6385c79d8f3414aaceef2041b101a91f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 10:01:27 +0100 Subject: [PATCH 102/108] Indent --- inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 58a19ee24..7fec6d6a4 100644 --- a/inbox.py +++ b/inbox.py @@ -2369,7 +2369,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int, if inReplyTo: if isinstance(inReplyTo, str): if not isMuted(baseDir, nickname, domain, - inReplyTo): + inReplyTo): replyNotify(baseDir, handle, httpPrefix + '://' + domain + '/users/' + nickname + From 605be761ff43b27f7af2da3f9bff6f4ae55b14f1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 10:09:58 +0100 Subject: [PATCH 103/108] Add a tags directory if it doesn't exist --- inbox.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 7fec6d6a4..89f8c6949 100644 --- a/inbox.py +++ b/inbox.py @@ -87,7 +87,13 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: return if not isinstance(postJsonObject['object']['tag'], list): return - tagsDir = baseDir+'/tags' + tagsDir = baseDir + '/tags' + + # add tags directory if it doesn't exist + if not os.path.isdir(tagsDir): + print('Creating tags directory') + os.mkdir(tagsDir) + for tag in postJsonObject['object']['tag']: if not tag.get('type'): continue From ed2aafc8cff0b346dec9d8378c0e6fe8c3f31e34 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 10:22:23 +0100 Subject: [PATCH 104/108] Only run the daemon if this is the main module --- epicyon.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/epicyon.py b/epicyon.py index 276dd32bf..fe98b2e05 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1819,21 +1819,22 @@ if YTDomain: if setTheme(baseDir, themeName): print('Theme set to ' + themeName) -runDaemon(args.blogsinstance, args.mediainstance, - args.maxRecentPosts, - not args.nosharedinbox, - registration, args.language, __version__, - instanceId, args.client, baseDir, - domain, onionDomain, i2pDomain, - args.YTReplacementDomain, - port, proxyPort, httpPrefix, - federationList, args.maxMentions, - args.maxEmoji, args.authenticatedFetch, - args.noreply, args.nolike, args.nopics, - args.noannounce, args.cw, ocapAlways, - proxyType, args.maxReplies, - args.domainMaxPostsPerDay, - args.accountMaxPostsPerDay, - args.allowdeletion, debug, False, - args.instanceOnlySkillsSearch, [], - args.blurhash, not args.noapproval) +if __name__ == "__main__": + runDaemon(args.blogsinstance, args.mediainstance, + args.maxRecentPosts, + not args.nosharedinbox, + registration, args.language, __version__, + instanceId, args.client, baseDir, + domain, onionDomain, i2pDomain, + args.YTReplacementDomain, + port, proxyPort, httpPrefix, + federationList, args.maxMentions, + args.maxEmoji, args.authenticatedFetch, + args.noreply, args.nolike, args.nopics, + args.noannounce, args.cw, ocapAlways, + proxyType, args.maxReplies, + args.domainMaxPostsPerDay, + args.accountMaxPostsPerDay, + args.allowdeletion, debug, False, + args.instanceOnlySkillsSearch, [], + args.blurhash, not args.noapproval) From 89f410a9046571d650e9dbd3c84e05f75308c410 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 10:58:32 +0100 Subject: [PATCH 105/108] Formatting --- acceptreject.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/acceptreject.py b/acceptreject.py index dcfdeb226..b3229992a 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -40,7 +40,7 @@ def createAcceptReject(baseDir: str, federationList: [], newAccept = { "@context": "https://www.w3.org/ns/activitystreams", 'type': acceptType, - 'actor': httpPrefix+'://'+domain+'/users/'+nickname, + 'actor': httpPrefix+'://' + domain + '/users/' + nickname, 'to': [toUrl], 'cc': [], 'object': objectJson @@ -107,33 +107,33 @@ def acceptFollow(baseDir: str, domain: str, messageJson: {}, thisActor = messageJson['object']['actor'] nickname = getNicknameFromActor(thisActor) if not nickname: - print('WARN: no nickname found in '+thisActor) + print('WARN: no nickname found in ' + thisActor) return acceptedDomain, acceptedPort = getDomainFromActor(thisActor) if not acceptedDomain: if debug: - print('DEBUG: domain not found in '+thisActor) + print('DEBUG: domain not found in ' + thisActor) return if not nickname: if debug: - print('DEBUG: nickname not found in '+thisActor) + print('DEBUG: nickname not found in ' + thisActor) return if acceptedPort: if '/' + acceptedDomain + ':' + str(acceptedPort) + \ '/users/' + nickname not in thisActor: if debug: - print('Port: '+str(acceptedPort)) + print('Port: ' + str(acceptedPort)) print('Expected: /' + acceptedDomain + ':' + - str(acceptedPort) + '/users/'+nickname) - print('Actual: '+thisActor) - print('DEBUG: unrecognized actor '+thisActor) + str(acceptedPort) + '/users/' + nickname) + print('Actual: ' + thisActor) + print('DEBUG: unrecognized actor ' + thisActor) return else: if not '/' + acceptedDomain+'/users/' + nickname in thisActor: if debug: - print('Expected: /'+acceptedDomain+'/users/'+nickname) - print('Actual: '+thisActor) - print('DEBUG: unrecognized actor '+thisActor) + print('Expected: /' + acceptedDomain+'/users/' + nickname) + print('Actual: ' + thisActor) + print('DEBUG: unrecognized actor ' + thisActor) return followedActor = messageJson['object']['object'] followedDomain, port = getDomainFromActor(followedActor) From c47a046c95c4ee9357ac5833871c600e40547af6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 11:04:44 +0100 Subject: [PATCH 106/108] Default to adding new follows to the calendar --- utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 125906f13..b723097a6 100644 --- a/utils.py +++ b/utils.py @@ -356,6 +356,7 @@ def followPerson(baseDir: str, nickname: str, domain: str, if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') + followAdded = False handleToFollow = followNickname + '@' + followDomain filename = baseDir + '/accounts/' + handle + '/' + followFile if os.path.isfile(filename): @@ -371,16 +372,21 @@ def followPerson(baseDir: str, nickname: str, domain: str, followFile.write(handleToFollow + '\n' + content) if debug: print('DEBUG: follow added') - return True + followAdded = True except Exception as e: print('WARN: Failed to write entry to follow file ' + filename + ' ' + str(e)) + if followAdded: + # Default to adding new follows to the calendar. + # Possibly this could be made optional if followFile == 'following.txt': # if following a person add them to the list of # calendar follows addPersonToCalendar(baseDir, nickname, domain, followNickname, followDomain) + return True + if debug: print('DEBUG: creating new following file to follow ' + handleToFollow) with open(filename, 'w+') as followfile: From b54c9e52f7bb2bb7acf7a361b90e92d77fc41d76 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 11:09:40 +0100 Subject: [PATCH 107/108] Improve handling of the first follow --- utils.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/utils.py b/utils.py index b723097a6..eae0e75f6 100644 --- a/utils.py +++ b/utils.py @@ -356,7 +356,6 @@ def followPerson(baseDir: str, nickname: str, domain: str, if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') - followAdded = False handleToFollow = followNickname + '@' + followDomain filename = baseDir + '/accounts/' + handle + '/' + followFile if os.path.isfile(filename): @@ -372,25 +371,24 @@ def followPerson(baseDir: str, nickname: str, domain: str, followFile.write(handleToFollow + '\n' + content) if debug: print('DEBUG: follow added') - followAdded = True except Exception as e: print('WARN: Failed to write entry to follow file ' + filename + ' ' + str(e)) + else: + # first follow + if debug: + print('DEBUG: creating new following file to follow ' + + handleToFollow) + with open(filename, 'w+') as followfile: + followfile.write(handleToFollow + '\n') - if followAdded: - # Default to adding new follows to the calendar. - # Possibly this could be made optional - if followFile == 'following.txt': - # if following a person add them to the list of - # calendar follows - addPersonToCalendar(baseDir, nickname, domain, - followNickname, followDomain) - return True - - if debug: - print('DEBUG: creating new following file to follow ' + handleToFollow) - with open(filename, 'w+') as followfile: - followfile.write(handleToFollow + '\n') + # Default to adding new follows to the calendar. + # Possibly this could be made optional + if followFile == 'following.txt': + # if following a person add them to the list of + # calendar follows + addPersonToCalendar(baseDir, nickname, domain, + followNickname, followDomain) return True From 8d642fe267688ffea2c319f2fb64e595073a436d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 3 Sep 2020 11:12:11 +0100 Subject: [PATCH 108/108] Avoid confusing variable name --- utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utils.py b/utils.py index eae0e75f6..c39b042fd 100644 --- a/utils.py +++ b/utils.py @@ -365,10 +365,10 @@ def followPerson(baseDir: str, nickname: str, domain: str, return True # prepend to follow file try: - with open(filename, 'r+') as followFile: - content = followFile.read() - followFile.seek(0, 0) - followFile.write(handleToFollow + '\n' + content) + with open(filename, 'r+') as f: + content = f.read() + f.seek(0, 0) + f.write(handleToFollow + '\n' + content) if debug: print('DEBUG: follow added') except Exception as e: @@ -379,8 +379,8 @@ def followPerson(baseDir: str, nickname: str, domain: str, if debug: print('DEBUG: creating new following file to follow ' + handleToFollow) - with open(filename, 'w+') as followfile: - followfile.write(handleToFollow + '\n') + with open(filename, 'w+') as f: + f.write(handleToFollow + '\n') # Default to adding new follows to the calendar. # Possibly this could be made optional