__filename__ = "schedule.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" __module_group__ = "Calendar" import os import time import datetime from utils import hasObjectDict from utils import getStatusNumber from utils import loadJson from utils import isAccountDir from utils import acctDir from outbox import postMessageToOutbox def _updatePostSchedule(baseDir: str, handle: str, httpd, maxScheduledPosts: int) -> None: """Checks if posts are due to be delivered and if so moves them to the outbox """ scheduleIndexFilename = baseDir + '/accounts/' + handle + '/schedule.index' if not os.path.isfile(scheduleIndexFilename): return # get the current time as an int currTime = datetime.datetime.utcnow() daysSinceEpoch = (currTime - datetime.datetime(1970, 1, 1)).days scheduleDir = baseDir + '/accounts/' + handle + '/scheduled/' indexLines = [] deleteSchedulePost = False nickname = handle.split('@')[0] with open(scheduleIndexFilename, 'r') as fp: for line in fp: if ' ' not in line: continue dateStr = line.split(' ')[0] if 'T' not in dateStr: continue postId = line.split(' ', 1)[1].replace('\n', '').replace('\r', '') postFilename = scheduleDir + postId + '.json' if deleteSchedulePost: # delete extraneous scheduled posts if os.path.isfile(postFilename): os.remove(postFilename) continue # create the new index file indexLines.append(line) # convert string date to int postTime = \ datetime.datetime.strptime(dateStr, "%Y-%m-%dT%H:%M:%S%z") postTime = postTime.replace(tzinfo=None) postDaysSinceEpoch = \ (postTime - datetime.datetime(1970, 1, 1)).days if daysSinceEpoch < postDaysSinceEpoch: continue if daysSinceEpoch == postDaysSinceEpoch: if currTime.time().hour < postTime.time().hour: continue if currTime.time().minute < postTime.time().minute: continue if not os.path.isfile(postFilename): print('WARN: schedule missing postFilename=' + postFilename) indexLines.remove(line) continue # load post postJsonObject = loadJson(postFilename) if not postJsonObject: print('WARN: schedule json not loaded') indexLines.remove(line) continue # set the published time # If this is not recent then http checks on the receiving side # will reject it statusNumber, published = getStatusNumber() if postJsonObject.get('published'): postJsonObject['published'] = published if hasObjectDict(postJsonObject): if postJsonObject['object'].get('published'): postJsonObject['published'] = published print('Sending scheduled post ' + postId) if nickname: httpd.postToNickname = nickname if not postMessageToOutbox(httpd.session, httpd.translate, postJsonObject, nickname, httpd, baseDir, httpd.httpPrefix, httpd.domain, httpd.domainFull, httpd.onionDomain, httpd.i2pDomain, httpd.port, httpd.recentPostsCache, httpd.followersThreads, httpd.federationList, httpd.sendThreads, httpd.postLog, httpd.cachedWebfingers, httpd.personCache, httpd.allowDeletion, httpd.proxyType, httpd.projectVersion, httpd.debug, httpd.YTReplacementDomain, httpd.showPublishedDateOnly, httpd.allowLocalNetworkAccess, httpd.city, httpd.systemLanguage, httpd.sharedItemsFederatedDomains, httpd.sharedItemFederationTokens, httpd.lowBandwidth, httpd.signingPrivateKeyPem, httpd.peertubeInstances, httpd.themeName, httpd.maxLikeCount, httpd.maxRecentPosts): indexLines.remove(line) os.remove(postFilename) continue # move to the outbox outboxPostFilename = postFilename.replace('/scheduled/', '/outbox/') os.rename(postFilename, outboxPostFilename) print('Scheduled post sent ' + postId) indexLines.remove(line) if len(indexLines) > maxScheduledPosts: deleteSchedulePost = True # write the new schedule index file scheduleIndexFile = \ baseDir + '/accounts/' + handle + '/schedule.index' with open(scheduleIndexFile, 'w+') as scheduleFile: for line in indexLines: scheduleFile.write(line) def runPostSchedule(baseDir: str, httpd, maxScheduledPosts: int): """Dispatches scheduled posts """ while True: time.sleep(60) # for each account for subdir, dirs, files in os.walk(baseDir + '/accounts'): for account in dirs: if '@' not in account: continue if not isAccountDir(account): continue # scheduled posts index for this account scheduleIndexFilename = \ baseDir + '/accounts/' + account + '/schedule.index' if not os.path.isfile(scheduleIndexFilename): continue _updatePostSchedule(baseDir, account, httpd, maxScheduledPosts) break def runPostScheduleWatchdog(projectVersion: str, httpd) -> None: """This tries to keep the scheduled post thread running even if it dies """ print('Starting scheduled post watchdog') postScheduleOriginal = \ httpd.thrPostSchedule.clone(runPostSchedule) httpd.thrPostSchedule.start() while True: time.sleep(20) if httpd.thrPostSchedule.is_alive(): continue httpd.thrPostSchedule.kill() httpd.thrPostSchedule = \ postScheduleOriginal.clone(runPostSchedule) httpd.thrPostSchedule.start() print('Restarting scheduled posts...') def removeScheduledPosts(baseDir: str, nickname: str, domain: str) -> None: """Removes any scheduled posts """ # remove the index scheduleIndexFilename = \ acctDir(baseDir, nickname, domain) + '/schedule.index' if os.path.isfile(scheduleIndexFilename): os.remove(scheduleIndexFilename) # remove the scheduled posts scheduledDir = acctDir(baseDir, nickname, domain) + '/scheduled' if not os.path.isdir(scheduledDir): return for scheduledPostFilename in os.listdir(scheduledDir): filePath = os.path.join(scheduledDir, scheduledPostFilename) try: if os.path.isfile(filePath): os.remove(filePath) except BaseException: pass