diff --git a/blog.py b/blog.py index 418821163..3216f9b8a 100644 --- a/blog.py +++ b/blog.py @@ -622,6 +622,7 @@ def getBlogIndexesForAccounts(baseDir: str) -> {}: blogsIndex = accountDir + '/tlblogs.index' if os.path.isfile(blogsIndex): blogIndexes[acct] = blogsIndex + break return blogIndexes @@ -639,6 +640,7 @@ def noOfBlogAccounts(baseDir: str) -> int: blogsIndex = accountDir + '/tlblogs.index' if os.path.isfile(blogsIndex): ctr += 1 + break return ctr @@ -655,6 +657,7 @@ def singleBlogAccountNickname(baseDir: str) -> str: blogsIndex = accountDir + '/tlblogs.index' if os.path.isfile(blogsIndex): return acct.split('@')[0] + break return None @@ -698,6 +701,7 @@ def htmlBlogView(authorized: bool, httpPrefix + '://' + domainFull + '/blog/' + \ acct.split('@')[0] + '">' + acct + '' blogStr += '

' + break return blogStr + htmlFooter() diff --git a/content.py b/content.py index 4de6effa4..be9afd3ae 100644 --- a/content.py +++ b/content.py @@ -508,6 +508,15 @@ def addEmoji(baseDir: str, wordStr: str, return True +def tagExists(tagType: str, tagName: str, tags: {}) -> bool: + """Returns true if a tag exists in the given dict + """ + for tag in tags: + if tag['name'] == tagName and tag['type'] == tagType: + return True + return False + + def addMention(wordStr: str, httpPrefix: str, following: str, replaceMentions: {}, recipients: [], tags: {}) -> bool: """Detects mentions and adds them to the replacements dict and diff --git a/daemon.py b/daemon.py index 3789becc2..cb8add174 100644 --- a/daemon.py +++ b/daemon.py @@ -89,6 +89,7 @@ from inbox import getPersonPubKey from follow import getFollowingFeed from follow import sendFollowRequest from follow import unfollowPerson +from follow import createInitialLastSeen from auth import authorize from auth import createPassword from auth import createBasicAuthHeader @@ -4788,6 +4789,7 @@ class PubServer(BaseHTTPRequestHandler): port, maxPostsInRSSFeed, 1, False) + break if msg: msg = rss2Header(httpPrefix, 'news', domainFull, @@ -4987,7 +4989,8 @@ class PubServer(BaseHTTPRequestHandler): ssbAddress, blogAddress, toxAddress, jamiAddress, PGPpubKey, PGPfingerprint, - emailAddress).encode('utf-8') + emailAddress, + self.server.dormantMonths).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) self._write(msg) @@ -6491,6 +6494,7 @@ class PubServer(BaseHTTPRequestHandler): YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, actorJson['roles'], None, None) msg = msg.encode('utf-8') @@ -6570,6 +6574,7 @@ class PubServer(BaseHTTPRequestHandler): YTReplacementDomain, showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, actorJson['skills'], None, None) msg = msg.encode('utf-8') @@ -8258,6 +8263,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, shares, pageNumber, sharesPerPage) msg = msg.encode('utf-8') @@ -8349,6 +8355,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, following, pageNumber, followsPerPage).encode('utf-8') @@ -8440,6 +8447,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, followers, pageNumber, followsPerPage).encode('utf-8') @@ -8506,6 +8514,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, None, None).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -12125,6 +12134,7 @@ class PubServer(BaseHTTPRequestHandler): contentJson = loadJson(deviceFilename) if contentJson: devicesList.append(contentJson) + break # return the list of devices for this handle msg = \ json.dumps(devicesList, @@ -12924,9 +12934,11 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: continue tokensDict[nickname] = token tokensLookup[token] = nickname + break -def runDaemon(maxNewswirePosts: int, +def runDaemon(dormantMonths: int, + maxNewswirePosts: int, allowLocalNetworkAccess: bool, maxFeedItemSizeKb: int, publishButtonAtTop: bool, @@ -13120,6 +13132,10 @@ def runDaemon(maxNewswirePosts: int, # maximum size of a hashtag category, in K httpd.maxCategoriesFeedItemSizeKb = 1024 + # how many months does a followed account need to be unseen + # for it to be considered dormant? + httpd.dormantMonths = dormantMonths + if registration == 'open': httpd.registration = True else: @@ -13249,6 +13265,8 @@ def runDaemon(maxNewswirePosts: int, httpd.iconsCache = {} httpd.fontsCache = {} + createInitialLastSeen(baseDir, httpPrefix) + print('Creating inbox queue') httpd.thrInboxQueue = \ threadWithTrace(target=runInboxQueue, diff --git a/delete.py b/delete.py index 3b195db7e..58dbd5d28 100644 --- a/delete.py +++ b/delete.py @@ -321,6 +321,7 @@ def removeOldHashtags(baseDir: str, maxMonths: int) -> str: # check of the file is too old if fileDaysSinceEpoch < maxDaysSinceEpoch: removeHashtags.append(tagsFilename) + break for removeFilename in removeHashtags: try: diff --git a/devices.py b/devices.py index 69c8d2ca7..3f0ef52bc 100644 --- a/devices.py +++ b/devices.py @@ -152,6 +152,7 @@ def E2EEdevicesCollection(baseDir: str, nickname: str, domain: str, devJson = loadJson(deviceFilename) if devJson: deviceList.append(devJson) + break devicesDict = { 'id': personId + '/collections/devices', diff --git a/epicyon.py b/epicyon.py index 37df367e9..214605140 100644 --- a/epicyon.py +++ b/epicyon.py @@ -116,6 +116,11 @@ parser.add_argument('--postsPerSource', dest='maxNewswirePostsPerSource', type=int, default=4, help='Maximum newswire posts per feed or account') +parser.add_argument('--dormantMonths', + dest='dormantMonths', type=int, + default=3, + help='How many months does a followed account need to ' + + 'be unseen for before being considered dormant') parser.add_argument('--maxNewswirePosts', dest='maxNewswirePosts', type=int, default=20, @@ -2032,6 +2037,11 @@ maxFeedItemSizeKb = \ if maxFeedItemSizeKb is not None: args.maxFeedItemSizeKb = int(maxFeedItemSizeKb) +dormantMonths = \ + getConfigParam(baseDir, 'dormantMonths') +if dormantMonths is not None: + args.dormantMonths = int(dormantMonths) + allowNewsFollowers = \ getConfigParam(baseDir, 'allowNewsFollowers') if allowNewsFollowers is not None: @@ -2080,7 +2090,8 @@ if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.maxNewswirePosts, + runDaemon(args.dormantMonths, + args.maxNewswirePosts, args.allowLocalNetworkAccess, args.maxFeedItemSizeKb, args.publishButtonAtTop, diff --git a/follow.py b/follow.py index 9403f92ea..bf44bc83e 100644 --- a/follow.py +++ b/follow.py @@ -27,6 +27,43 @@ from auth import createBasicAuthHeader from session import postJson +def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None: + """Creates initial lastseen files for all follows + """ + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct or 'news@' in acct: + continue + accountDir = os.path.join(baseDir + '/accounts', acct) + followingFilename = accountDir + '/following.txt' + if not os.path.isfile(followingFilename): + continue + lastSeenDir = accountDir + '/lastseen' + if not os.path.isdir(lastSeenDir): + os.mkdir(lastSeenDir) + with open(followingFilename, 'r') as fp: + followingHandles = fp.readlines() + for handle in followingHandles: + if '#' in handle: + continue + if '@' not in handle: + continue + handle = handle.replace('\n', '') + nickname = handle.split('@')[0] + domain = handle.split('@')[1] + actor = \ + httpPrefix + '://' + domain + '/users/' + nickname + lastSeenFilename = \ + lastSeenDir + '/' + actor.replace('/', '#') + '.txt' + print('lastSeenFilename: ' + lastSeenFilename) + if not os.path.isfile(lastSeenFilename): + with open(lastSeenFilename, 'w+') as fp: + fp.write(str(100)) + break + + def preApprovedFollower(baseDir: str, nickname: str, domain: str, approveHandle: str, @@ -1167,6 +1204,7 @@ def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}: print('DEBUG: ' + account + ' follows ' + actorHandle) recipientsDict[account] = None + break return recipientsDict diff --git a/inbox.py b/inbox.py index 8969bc4b5..4772a763b 100644 --- a/inbox.py +++ b/inbox.py @@ -67,6 +67,7 @@ from followingCalendar import receivingCalendarEvents from content import dangerousMarkup from happening import saveEventPost from delete import removeOldHashtags +from follow import isFollowingActor def guessHashtagCategory(tagName: str, hashtagCategories: {}) -> str: @@ -199,6 +200,7 @@ def validInbox(baseDir: str, nickname: str, domain: str) -> bool: if 'postNickname' in open(filename).read(): print('queue file incorrectly saved to ' + filename) return False + break return True @@ -223,6 +225,7 @@ def validInboxFilenames(baseDir: str, nickname: str, domain: str, print('Expected: ' + expectedStr) print('Invalid filename: ' + filename) return False + break return True @@ -2066,6 +2069,38 @@ def inboxUpdateIndex(boxname: str, baseDir: str, handle: str, return False +def updateLastSeen(baseDir: str, handle: str, actor: str) -> None: + """Updates the time when the given handle last saw the given actor + This can later be used to indicate if accounts are dormant/abandoned/moved + """ + if '@' not in handle: + return + nickname = handle.split('@')[0] + domain = handle.split('@')[1] + if ':' in domain: + domain = domain.split(':')[0] + accountPath = baseDir + '/accounts/' + nickname + '@' + domain + if not os.path.isdir(accountPath): + return + if not isFollowingActor(baseDir, nickname, domain, actor): + return + lastSeenPath = accountPath + '/lastseen' + if not os.path.isdir(lastSeenPath): + os.mkdir(lastSeenPath) + lastSeenFilename = lastSeenPath + '/' + actor.replace('/', '#') + '.txt' + currTime = datetime.datetime.utcnow() + daysSinceEpoch = (currTime - datetime.datetime(1970, 1, 1)).days + # has the value changed? + if os.path.isfile(lastSeenFilename): + with open(lastSeenFilename, 'r') as lastSeenFile: + daysSinceEpochFile = lastSeenFile.read() + if int(daysSinceEpochFile) == daysSinceEpoch: + # value hasn't changed, so we can save writing anything to file + return + with open(lastSeenFilename, 'w+') as lastSeenFile: + lastSeenFile.write(str(daysSinceEpoch)) + + def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, session, keyId: str, handle: str, messageJson: {}, baseDir: str, httpPrefix: str, sendThreads: [], @@ -2086,6 +2121,8 @@ def inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, if '#' in actor: actor = keyId.split('#')[0] + updateLastSeen(baseDir, handle, actor) + isGroup = groupHandle(baseDir, handle) if receiveLike(recentPostsCache, @@ -2436,6 +2473,7 @@ def clearQueueItems(baseDir: str, queue: []) -> None: ctr += 1 except BaseException: pass + break if ctr > 0: print('Removed ' + str(ctr) + ' inbox queue items') @@ -2452,6 +2490,7 @@ def restoreQueueItems(baseDir: str, queue: []) -> None: for queuesubdir, queuedirs, queuefiles in os.walk(queueDir): for qfile in queuefiles: queue.append(os.path.join(queueDir, qfile)) + break if len(queue) > 0: print('Restored ' + str(len(queue)) + ' inbox queue items') diff --git a/media.py b/media.py index 2163820d8..df6409f78 100644 --- a/media.py +++ b/media.py @@ -221,3 +221,4 @@ def archiveMedia(baseDir: str, archiveDirectory: str, maxWeeks=4) -> None: else: # archive to /dev/null rmtree(os.path.join(baseDir + '/media', weekDir)) + break diff --git a/migrate.py b/migrate.py index 6fb8bee28..4b04d8520 100644 --- a/migrate.py +++ b/migrate.py @@ -51,3 +51,4 @@ def migrateAccount(baseDir: str, oldHandle: str, newHandle: str) -> None: migrateFollows(followFilename, oldHandle, newHandle) followFilename = accountDir + '/followers.txt' migrateFollows(followFilename, oldHandle, newHandle) + break diff --git a/newswire.py b/newswire.py index 144e7854a..a71ee2bff 100644 --- a/newswire.py +++ b/newswire.py @@ -760,6 +760,7 @@ def addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, addAccountBlogsToNewswire(baseDir, nickname, domain, newswire, maxBlogsPerAccount, blogsIndex, maxTags) + break # sort the moderation dict into chronological order, latest first sortedModerationDict = \ diff --git a/posts.py b/posts.py index 01d8b9fd6..b35c0505b 100644 --- a/posts.py +++ b/posts.py @@ -52,6 +52,7 @@ from utils import votesOnNewswireItem from utils import removeHtml from media import attachMedia from media import replaceYouTube +from content import tagExists from content import removeLongWords from content import addHtmlTags from content import replaceEmojiFromTags @@ -801,7 +802,8 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, isPublic = True break for tagName, tag in hashtagsDict.items(): - tags.append(tag) + if not tagExists(tag['type'], tag['name'], tags): + tags.append(tag) if isPublic: updateHashtagsIndex(baseDir, tag, newPostId) print('Content tags: ' + str(tags)) @@ -1010,6 +1012,16 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, if newPost.get('object'): newPost['object']['cc'] = [ccUrl] + # if this is a public post then include any mentions in cc + toCC = newPost['object']['cc'] + if len(toRecipients) == 1: + if toRecipients[0].endswith('#Public') and \ + ccUrl.endswith('/followers'): + for tag in tags: + if tag['type'] == 'Mention': + if tag['href'] not in toCC: + toCC.append(tag['href']) + # if this is a moderation report then add a status if isModerationReport: # add status @@ -3183,6 +3195,7 @@ def archivePosts(baseDir: str, httpPrefix: str, archiveDir: str, archivePostsForPerson(httpPrefix, nickname, domain, baseDir, 'outbox', archiveSubdir, recentPostsCache, maxPostsInBox) + break def archivePostsForPerson(httpPrefix: str, nickname: str, domain: str, diff --git a/schedule.py b/schedule.py index 919ab2b70..c213c4ec5 100644 --- a/schedule.py +++ b/schedule.py @@ -146,6 +146,7 @@ def runPostSchedule(baseDir: str, httpd, maxScheduledPosts: int): if not os.path.isfile(scheduleIndexFilename): continue updatePostSchedule(baseDir, account, httpd, maxScheduledPosts) + break def runPostScheduleWatchdog(projectVersion: str, httpd) -> None: diff --git a/shares.py b/shares.py index 9c9cba297..d4b19b36e 100644 --- a/shares.py +++ b/shares.py @@ -167,6 +167,7 @@ def addShare(baseDir: str, '/users/' + nickname + '/tlshares') except BaseException: pass + break def expireShares(baseDir: str) -> None: @@ -179,6 +180,7 @@ def expireShares(baseDir: str) -> None: nickname = account.split('@')[0] domain = account.split('@')[1] expireSharesForAccount(baseDir, nickname, domain) + break def expireSharesForAccount(baseDir: str, nickname: str, domain: str) -> None: diff --git a/tests.py b/tests.py index 3af8473b3..fc8d9d739 100644 --- a/tests.py +++ b/tests.py @@ -20,6 +20,7 @@ from cache import getPersonFromCache from threads import threadWithTrace from daemon import runDaemon from session import createSession +from posts import getMentionedPeople from posts import validContentWarning from posts import deleteAllPosts from posts import createPublicPost @@ -75,6 +76,7 @@ from inbox import guessHashtagCategory from content import htmlReplaceEmailQuote from content import htmlReplaceQuoteMarks from content import dangerousMarkup +from content import dangerousCSS from content import addWebLinks from content import replaceEmojiFromTags from content import addHtmlTags @@ -296,8 +298,10 @@ def createServerAlice(path: str, domain: str, port: int, i2pDomain = None allowLocalNetworkAccess = True maxNewswirePosts = 20 + dormantMonths = 3 print('Server running: Alice') - runDaemon(maxNewswirePosts, allowLocalNetworkAccess, + runDaemon(dormantMonths, maxNewswirePosts, + allowLocalNetworkAccess, 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, @@ -364,8 +368,10 @@ def createServerBob(path: str, domain: str, port: int, i2pDomain = None allowLocalNetworkAccess = True maxNewswirePosts = 20 + dormantMonths = 3 print('Server running: Bob') - runDaemon(maxNewswirePosts, allowLocalNetworkAccess, + runDaemon(dormantMonths, maxNewswirePosts, + allowLocalNetworkAccess, 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, @@ -406,8 +412,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], i2pDomain = None allowLocalNetworkAccess = True maxNewswirePosts = 20 + dormantMonths = 3 print('Server running: Eve') - runDaemon(maxNewswirePosts, allowLocalNetworkAccess, + runDaemon(dormantMonths, maxNewswirePosts, + allowLocalNetworkAccess, 2048, False, True, False, False, True, 10, False, 0, 100, 1024, 5, False, 0, False, 1, False, False, False, @@ -1683,7 +1691,7 @@ def testWebLinks(): 'This post has a web links https://somesite.net\n\nAnd some other text' linkedText = addWebLinks(exampleText) assert \ - 'somesite.net None: assert guess == "bar" +def testGetMentionedPeople() -> None: + print('testGetMentionedPeople') + baseDir = os.getcwd() + + content = "@dragon@cave.site @bat@cave.site This is a test." + actors = getMentionedPeople(baseDir, 'https', + content, + 'mydomain', False) + assert actors + assert len(actors) == 2 + assert actors[0] == "https://cave.site/users/dragon" + assert actors[1] == "https://cave.site/users/bat" + + +def testReplyToPublicPost() -> None: + baseDir = os.getcwd() + nickname = 'test7492362' + domain = 'other.site' + port = 443 + httpPrefix = 'https' + postId = httpPrefix + '://rat.site/users/ninjarodent/statuses/63746173435' + reply = \ + createPublicPost(baseDir, nickname, domain, port, httpPrefix, + "@ninjarodent@rat.site This is a test.", + False, False, False, True, + None, None, False, postId) + # print(str(reply)) + assert reply['object']['content'] == \ + '

' + \ + '@ninjarodent' + \ + ' This is a test.

' + assert reply['object']['tag'][0]['type'] == 'Mention' + assert reply['object']['tag'][0]['name'] == '@ninjarodent@rat.site' + assert reply['object']['tag'][0]['href'] == \ + 'https://rat.site/users/ninjarodent' + assert len(reply['object']['to']) == 1 + assert reply['object']['to'][0].endswith('#Public') + assert len(reply['object']['cc']) >= 1 + assert reply['object']['cc'][0].endswith(nickname + '/followers') + assert len(reply['object']['tag']) == 1 + assert len(reply['object']['cc']) == 2 + assert reply['object']['cc'][1] == \ + httpPrefix + '://rat.site/users/ninjarodent' + + def runAllTests(): print('Running tests...') + testReplyToPublicPost() + testGetMentionedPeople() testGuessHashtagCategory() testValidNickname() testParseFeedDate() @@ -2477,6 +2544,7 @@ def runAllTests(): testRemoveIdEnding() testJsonPostAllowsComments() runHtmlReplaceQuoteMarks() + testDangerousCSS() testDangerousMarkup() testRemoveHtml() testSiteIsActive() diff --git a/theme.py b/theme.py index 4f85a9d5c..bc34fe1c3 100644 --- a/theme.py +++ b/theme.py @@ -557,6 +557,7 @@ def setThemeImages(baseDir: str, name: str) -> None: os.remove(accountDir + '/right_col_image.png') except BaseException: pass + break def setNewsAvatar(baseDir: str, name: str, diff --git a/utils.py b/utils.py index b02b71936..4fd05450d 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,30 @@ from calendar import monthrange from followingCalendar import addPersonToCalendar +def isDormant(baseDir: str, nickname: str, domain: str, actor: str, + dormantMonths=3) -> bool: + """Is the given followed actor dormant, from the standpoint + of the given account + """ + lastSeenFilename = \ + baseDir + '/accounts/' + nickname + '@' + domain + \ + '/lastseen/' + actor.replace('/', '#') + '.txt' + + if not os.path.isfile(lastSeenFilename): + return False + + with open(lastSeenFilename, 'r') as lastSeenFile: + daysSinceEpochStr = lastSeenFile.read() + daysSinceEpoch = int(daysSinceEpochStr) + currTime = datetime.datetime.utcnow() + currDaysSinceEpoch = (currTime - datetime.datetime(1970, 1, 1)).days + timeDiffMonths = \ + int((currDaysSinceEpoch - daysSinceEpoch) / 30) + if timeDiffMonths >= dormantMonths: + return True + return False + + def getHashtagCategory(baseDir: str, hashtag: str) -> str: """Returns the category for the hashtag """ @@ -86,6 +110,7 @@ def getHashtagCategories(baseDir: str, recent=False, category=None) -> None: else: if hashtag not in hashtagCategories[categoryStr]: hashtagCategories[categoryStr].append(hashtag) + break return hashtagCategories @@ -383,6 +408,7 @@ def getFollowersOfPerson(baseDir: str, if account not in followers: followers.append(account) break + break return followers @@ -908,6 +934,7 @@ def clearFromPostCaches(baseDir: str, recentPostsCache: {}, if recentPostsCache.get('html'): if recentPostsCache['html'].get(postId): del recentPostsCache['html'][postId] + break def locatePost(baseDir: str, nickname: str, domain: str, @@ -1171,6 +1198,7 @@ def noOfAccounts(baseDir: str) -> bool: if '@' in account: if not account.startswith('inbox@'): accountCtr += 1 + break return accountCtr @@ -1193,6 +1221,7 @@ def noOfActiveAccountsMonthly(baseDir: str, months: int) -> bool: timeDiff = (currTime - int(lastUsed)) if timeDiff < monthSeconds: accountCtr += 1 + break return accountCtr @@ -1469,6 +1498,7 @@ def searchBoxPosts(baseDir: str, nickname: str, domain: str, res.append(filePath) if len(res) >= maxResults: return res + break return res diff --git a/webapp_hashtagswarm.py b/webapp_hashtagswarm.py index 37e51060a..3dd92f54c 100644 --- a/webapp_hashtagswarm.py +++ b/webapp_hashtagswarm.py @@ -197,6 +197,7 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: if categoryStr not in categorySwarm: categorySwarm.append(categoryStr) break + break if not tagSwarm: return '' diff --git a/webapp_person_options.py b/webapp_person_options.py index aa60ed3e1..b18d5460a 100644 --- a/webapp_person_options.py +++ b/webapp_person_options.py @@ -11,6 +11,7 @@ from shutil import copyfile from petnames import getPetName from person import isPersonSnoozed from posts import isModerator +from utils import isDormant from utils import removeHtml from utils import getDomainFromActor from utils import getNicknameFromActor @@ -39,7 +40,8 @@ def htmlPersonOptions(defaultTimeline: str, jamiAddress: str, PGPpubKey: str, PGPfingerprint: str, - emailAddress) -> str: + emailAddress: str, + dormantMonths: int) -> str: """Show options for a person: view/follow/block/report """ optionsDomain, optionsPort = getDomainFromActor(optionsActor) @@ -53,6 +55,7 @@ def htmlPersonOptions(defaultTimeline: str, copyfile(baseDir + '/accounts/options-background.jpg', baseDir + '/accounts/options-background.jpg') + dormant = False followStr = 'Follow' blockStr = 'Block' nickname = None @@ -66,6 +69,9 @@ def htmlPersonOptions(defaultTimeline: str, followerDomain, followerPort = getDomainFromActor(optionsActor) if isFollowingActor(baseDir, nickname, domain, optionsActor): followStr = 'Unfollow' + dormant = \ + isDormant(baseDir, nickname, domain, optionsActor, + dormantMonths) optionsNickname = getNicknameFromActor(optionsActor) optionsDomainFull = optionsDomain @@ -107,9 +113,12 @@ def htmlPersonOptions(defaultTimeline: str, optionsStr += ' \n' handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain + handleShown = handle + if dormant: + handleShown += ' 💤' optionsStr += \ '

' + translate['Options for'] + \ - ' @' + handle + '

\n' + ' @' + handleShown + '

\n' if emailAddress: optionsStr += \ '

' + translate['Email'] + \ diff --git a/webapp_profile.py b/webapp_profile.py index a6985d1f9..21fdbc4e1 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -8,6 +8,7 @@ __status__ = "Production" import os from pprint import pprint +from utils import isDormant from utils import getNicknameFromActor from utils import getDomainFromActor from utils import isSystemAccount @@ -364,8 +365,9 @@ def htmlProfile(rssIconAtTop: bool, session, wfRequest: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, - newswire: {}, extraJson=None, - pageNumber=None, maxItemsPerPage=None) -> str: + newswire: {}, dormantMonths: int, + extraJson=None, pageNumber=None, + maxItemsPerPage=None) -> str: """Show the profile page as html """ nickname = profileJson['preferredUsername'] @@ -628,7 +630,8 @@ def htmlProfile(rssIconAtTop: bool, domain, port, session, wfRequest, personCache, extraJson, projectVersion, ["unfollow"], selected, - usersPath, pageNumber, maxItemsPerPage) + usersPath, pageNumber, maxItemsPerPage, + dormantMonths) elif selected == 'followers': profileStr += \ htmlProfileFollowing(translate, baseDir, httpPrefix, @@ -637,7 +640,7 @@ def htmlProfile(rssIconAtTop: bool, wfRequest, personCache, extraJson, projectVersion, ["block"], selected, usersPath, pageNumber, - maxItemsPerPage) + maxItemsPerPage, dormantMonths) elif selected == 'roles': profileStr += \ htmlProfileRoles(translate, nickname, domainFull, @@ -719,7 +722,8 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, buttons: [], feedName: str, actor: str, pageNumber: int, - maxItemsPerPage: int) -> str: + maxItemsPerPage: int, + dormantMonths: int) -> str: """Shows following on the profile screen """ profileStr = '' @@ -737,13 +741,22 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, translate['Page up'] + '">\n' + \ ' \n' - for item in followingJson['orderedItems']: + for followingActor in followingJson['orderedItems']: + # is this a dormant followed account? + dormant = False + if authorized and feedName == 'following': + dormant = \ + isDormant(baseDir, nickname, domain, followingActor, + dormantMonths) + profileStr += \ individualFollowAsHtml(translate, baseDir, session, wfRequest, personCache, - domain, item, authorized, nickname, - httpPrefix, projectVersion, + domain, followingActor, + authorized, nickname, + httpPrefix, projectVersion, dormant, buttons) + if authorized and maxItemsPerPage and pageNumber: if len(followingJson['orderedItems']) >= maxItemsPerPage: # page down arrow @@ -1436,12 +1449,15 @@ def individualFollowAsHtml(translate: {}, actorNickname: str, httpPrefix: str, projectVersion: str, + dormant: bool, buttons=[]) -> str: """An individual follow entry on the profile screen """ nickname = getNicknameFromActor(followUrl) domain, port = getDomainFromActor(followUrl) titleStr = '@' + nickname + '@' + domain + if dormant: + titleStr += ' 💤' avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True) if not avatarUrl: avatarUrl = followUrl + '/avatar.png' diff --git a/webapp_search.py b/webapp_search.py index 378fa653d..13d8ea87a 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -256,6 +256,7 @@ def htmlSearchSharedItems(cssCache: {}, translate: {}, sharedItemsForm += '\n' break ctr = 0 + break if not resultsExist: sharedItemsForm += \ '

' + translate['No results'] + '
\n' @@ -428,6 +429,7 @@ def htmlSkillsSearch(actor: str, ';' + actorJson['icon']['url'] if indexStr not in results: results.append(indexStr) + break if not instanceOnly: # search actor cache for subdir, dirs, files in os.walk(baseDir + '/cache/actors/'): @@ -465,6 +467,7 @@ def htmlSkillsSearch(actor: str, ';' + actorJson['icon']['url'] if indexStr not in results: results.append(indexStr) + break results.sort(reverse=True) diff --git a/webapp_utils.py b/webapp_utils.py index ce54a0f49..f70d76036 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -429,6 +429,7 @@ def sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int, ctr += 1 if ctr >= maxSharesPerAccount: break + break # sort the shared items in descending order of publication date sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True)) lastPage = False