diff --git a/daemon.py b/daemon.py index 3789becc..8ae2d41a 100644 --- a/daemon.py +++ b/daemon.py @@ -6491,6 +6491,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 +6571,7 @@ class PubServer(BaseHTTPRequestHandler): YTReplacementDomain, showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, actorJson['skills'], None, None) msg = msg.encode('utf-8') @@ -8258,6 +8260,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 +8352,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, following, pageNumber, followsPerPage).encode('utf-8') @@ -8440,6 +8444,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.newswire, + self.server.dormantMonths, followers, pageNumber, followsPerPage).encode('utf-8') @@ -8506,6 +8511,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) @@ -12926,7 +12932,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(maxNewswirePosts: int, +def runDaemon(dormantMonths: int, + maxNewswirePosts: int, allowLocalNetworkAccess: bool, maxFeedItemSizeKb: int, publishButtonAtTop: bool, @@ -13120,6 +13127,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: diff --git a/epicyon.py b/epicyon.py index 37df367e..a9bfef60 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, @@ -2080,7 +2085,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/tests.py b/tests.py index f856cf75..c6c08770 100644 --- a/tests.py +++ b/tests.py @@ -296,8 +296,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 +366,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 +410,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, diff --git a/utils.py b/utils.py index b02b7193..b79f760a 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,33 @@ 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() + if not daysSinceEpochStr: + return False + if not daysSinceEpochStr.isdigit(): + return False + currTime = datetime.datetime.utcnow() + currDaysSinceEpoch = (currTime - datetime.datetime(1970, 1, 1)).days + timeDiffMonths = \ + int((currDaysSinceEpoch - int(daysSinceEpochStr)) / 30) + if timeDiffMonths >= dormantMonths: + return True + return False + + def getHashtagCategory(baseDir: str, hashtag: str) -> str: """Returns the category for the hashtag """ diff --git a/webapp_profile.py b/webapp_profile.py index a6985d1f..b8a1eb81 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,12 +741,18 @@ def htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str, translate['Page up'] + '">\n' + \ ' \n' - for item in followingJson['orderedItems']: + for followingActor in followingJson['orderedItems']: + dormant = False + if 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: @@ -1436,12 +1446,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'