From 9601220e66346ed48796678c5efb0e1c9b467473 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 8 Sep 2021 19:37:04 +0100 Subject: [PATCH] Implement reply interval in hours The time after publication of a post during which replies are permitted --- daemon.py | 32 +++++++++++++++++++-- epicyon.py | 8 +++++- inbox.py | 39 +++++++++++++++++++------ tests.py | 16 ++++++++--- translations/ar.json | 4 ++- translations/ca.json | 4 ++- translations/cy.json | 4 ++- translations/de.json | 4 ++- translations/en.json | 4 ++- translations/es.json | 4 ++- translations/fr.json | 4 ++- translations/ga.json | 4 ++- translations/hi.json | 4 ++- translations/it.json | 4 ++- translations/ja.json | 4 ++- translations/ku.json | 4 ++- translations/oc.json | 4 ++- translations/pt.json | 4 ++- translations/ru.json | 4 ++- translations/sw.json | 4 ++- translations/zh.json | 4 ++- utils.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ 22 files changed, 199 insertions(+), 32 deletions(-) diff --git a/daemon.py b/daemon.py index d3ba03443..0fb40703e 100644 --- a/daemon.py +++ b/daemon.py @@ -232,6 +232,7 @@ from categories import updateHashtagCategories from languages import getActorLanguages from languages import setActorLanguages from like import updateLikesCollection +from utils import canReplyTo from utils import isDM from utils import replaceUsersWithAt from utils import localActorUrl @@ -857,6 +858,14 @@ class PubServer(BaseHTTPRequestHandler): 'This is nothing less ' + 'than an utter triumph') + def _403(self) -> None: + if self.server.translate: + self._httpReturnCode(403, self.server.translate['Forbidden'], + self.server.translate["You're not allowed"]) + else: + self._httpReturnCode(403, 'Forbidden', + "You're not allowed") + def _404(self) -> None: if self.server.translate: self._httpReturnCode(404, self.server.translate['Not Found'], @@ -11287,6 +11296,18 @@ class PubServer(BaseHTTPRequestHandler): if isNewPostEndpoint: nickname = getNicknameFromActor(path) + if inReplyToUrl: + replyIntervalHours = self.server.defaultReplyIntervalHours + if not canReplyTo(baseDir, nickname, domain, + inReplyToUrl, replyIntervalHours): + print('Reply outside of time window ' + inReplyToUrl) + self._403() + self.server.GETbusy = False + return True + elif self.server.debug: + print('Reply is within time interval: ' + + str(replyIntervalHours) + ' hours') + accessKeys = self.server.accessKeys if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] @@ -16245,7 +16266,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: break -def runDaemon(lowBandwidth: bool, +def runDaemon(defaultReplyIntervalHours: int, + lowBandwidth: bool, maxLikeCount: int, sharedItemsFederatedDomains: [], userAgentsBlocked: [], @@ -16376,6 +16398,11 @@ def runDaemon(lowBandwidth: bool, 'Public': 'p', 'Reminder': 'r' } + + # how many hours after a post was publushed can a reply be made + defaultReplyIntervalHours = 9999999 + httpd.defaultReplyIntervalHours = defaultReplyIntervalHours + httpd.keyShortcuts = {} loadAccessKeysForAccounts(baseDir, httpd.keyShortcuts, httpd.accessKeys) @@ -16704,7 +16731,8 @@ def runDaemon(lowBandwidth: bool, httpd.themeName, httpd.systemLanguage, httpd.maxLikeCount, - httpd.signingPrivateKeyPem), daemon=True) + httpd.signingPrivateKeyPem, + httpd.defaultReplyIntervalHours), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ diff --git a/epicyon.py b/epicyon.py index 496cc8df0..4953492ad 100644 --- a/epicyon.py +++ b/epicyon.py @@ -170,6 +170,11 @@ parser.add_argument('--dormantMonths', default=3, help='How many months does a followed account need to ' + 'be unseen for before being considered dormant') +parser.add_argument('--defaultReplyIntervalHours', + dest='defaultReplyIntervalHours', type=int, + default=9999999999, + help='How many hours after publication of a post ' + + 'are replies to it permitted') parser.add_argument('--sendThreadsTimeoutMins', dest='sendThreadsTimeoutMins', type=int, default=30, @@ -3031,7 +3036,8 @@ if args.defaultCurrency: print('Default currency set to ' + args.defaultCurrency) if __name__ == "__main__": - runDaemon(args.lowBandwidth, args.maxLikeCount, + runDaemon(args.defaultReplyIntervalHours, + args.lowBandwidth, args.maxLikeCount, sharedItemsFederatedDomains, userAgentsBlocked, args.logLoginFailures, diff --git a/inbox.py b/inbox.py index 5161f4ca8..002721d7d 100644 --- a/inbox.py +++ b/inbox.py @@ -15,6 +15,8 @@ import random from linked_data_sig import verifyJsonSignature from languages import understoodPostLanguage from like import updateLikesCollection +from utils import getReplyIntervalHours +from utils import canReplyTo from utils import getUserPaths from utils import getBaseContentFromPost from utils import acctDir @@ -2484,7 +2486,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, lastBounceMessage: [], themeName: str, systemLanguage: str, maxLikeCount: int, - signingPrivateKeyPem: str) -> bool: + signingPrivateKeyPem: str, + defaultReplyIntervalHours: int) -> bool: """ Anything which needs to be done after initial checks have passed """ actor = keyId @@ -2765,11 +2768,29 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, if isinstance(inReplyTo, str): if not isMuted(baseDir, nickname, domain, inReplyTo, conversationId): - actUrl = \ - localActorUrl(httpPrefix, - nickname, domain) - _replyNotify(baseDir, handle, - actUrl + '/tlreplies') + # check if the reply is within the allowed + # time period after publication + hrs = defaultReplyIntervalHours + replyIntervalHours = \ + getReplyIntervalHours(baseDir, + nickname, + domain, hrs) + if canReplyTo(baseDir, nickname, domain, + inReplyTo, + replyIntervalHours): + actUrl = \ + localActorUrl(httpPrefix, + nickname, domain) + _replyNotify(baseDir, handle, + actUrl + '/tlreplies') + else: + if debug: + print('Reply to ' + inReplyTo + + ' is outside of the ' + + 'permitted interval of ' + + str(replyIntervalHours) + + ' hours') + return False else: isReplyToMutedPost = True @@ -3119,7 +3140,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, peertubeInstances: [], verifyAllSignatures: bool, themeName: str, systemLanguage: str, - maxLikeCount: int, signingPrivateKeyPem: str) -> None: + maxLikeCount: int, signingPrivateKeyPem: str, + defaultReplyIntervalHours: int) -> None: """Processes received items and moves them to the appropriate directories """ @@ -3534,7 +3556,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, lastBounceMessage, themeName, systemLanguage, maxLikeCount, - signingPrivateKeyPem) + signingPrivateKeyPem, + defaultReplyIntervalHours) if debug: pprint(queueJson['post']) print('Queue: Queue post accepted') diff --git a/tests.py b/tests.py index 9519156ed..fe0a63615 100644 --- a/tests.py +++ b/tests.py @@ -649,8 +649,10 @@ def createServerAlice(path: str, domain: str, port: int, logLoginFailures = False userAgentsBlocked = [] maxLikeCount = 10 + defaultReplyIntervalHours = 9999999999 print('Server running: Alice') - runDaemon(lowBandwidth, maxLikeCount, + runDaemon(defaultReplyIntervalHours, + lowBandwidth, maxLikeCount, sharedItemsFederatedDomains, userAgentsBlocked, logLoginFailures, city, @@ -785,8 +787,10 @@ def createServerBob(path: str, domain: str, port: int, logLoginFailures = False userAgentsBlocked = [] maxLikeCount = 10 + defaultReplyIntervalHours = 9999999999 print('Server running: Bob') - runDaemon(lowBandwidth, maxLikeCount, + runDaemon(defaultReplyIntervalHours, + lowBandwidth, maxLikeCount, sharedItemsFederatedDomains, userAgentsBlocked, logLoginFailures, city, @@ -850,8 +854,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], userAgentsBlocked = [] maxLikeCount = 10 lowBandwidth = True + defaultReplyIntervalHours = 9999999999 print('Server running: Eve') - runDaemon(lowBandwidth, maxLikeCount, + runDaemon(defaultReplyIntervalHours, + lowBandwidth, maxLikeCount, sharedItemsFederatedDomains, userAgentsBlocked, logLoginFailures, city, @@ -917,8 +923,10 @@ def createServerGroup(path: str, domain: str, port: int, userAgentsBlocked = [] maxLikeCount = 10 lowBandwidth = True + defaultReplyIntervalHours = 9999999999 print('Server running: Group') - runDaemon(lowBandwidth, maxLikeCount, + runDaemon(defaultReplyIntervalHours, + lowBandwidth, maxLikeCount, sharedItemsFederatedDomains, userAgentsBlocked, logLoginFailures, city, diff --git a/translations/ar.json b/translations/ar.json index dcbd4d777..dfc32a780 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -475,5 +475,7 @@ "Wanted Items Search": "البحث عن العناصر المطلوبة", "Website": "موقع إلكتروني", "Low Bandwidth": "انخفاض النطاق الترددي", - "accommodation": "الإقامة" + "accommodation": "الإقامة", + "Forbidden": "محرم", + "You're not allowed": "كنت لا يسمح" } diff --git a/translations/ca.json b/translations/ca.json index e8fb1f099..8f73ec075 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Cerca d'articles desitjats", "Website": "Lloc web", "Low Bandwidth": "Ample de banda baixa", - "accommodation": "allotjament" + "accommodation": "allotjament", + "Forbidden": "Prohibit", + "You're not allowed": "No està permès" } diff --git a/translations/cy.json b/translations/cy.json index a896b147d..abb119458 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Chwilio Eitemau Eisiau", "Website": "Gwefan", "Low Bandwidth": "Lled band isel", - "accommodation": "llety" + "accommodation": "llety", + "Forbidden": "Wedi'i wahardd", + "You're not allowed": "Ni chaniateir i chi" } diff --git a/translations/de.json b/translations/de.json index 9c1177fd8..51f748e04 100644 --- a/translations/de.json +++ b/translations/de.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Gesuchte Artikel suchen", "Website": "Webseite", "Low Bandwidth": "Niedrige Bandbreite", - "accommodation": "unterkunft" + "accommodation": "unterkunft", + "Forbidden": "Verboten", + "You're not allowed": "Du darfst nicht" } diff --git a/translations/en.json b/translations/en.json index 195afa909..8c845274e 100644 --- a/translations/en.json +++ b/translations/en.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Wanted Items Search", "Website": "Website", "Low Bandwidth": "Low Bandwidth", - "accommodation": "accommodation" + "accommodation": "accommodation", + "Forbidden": "Forbidden", + "You're not allowed": "You're not allowed" } diff --git a/translations/es.json b/translations/es.json index 6d34af573..5f1954c63 100644 --- a/translations/es.json +++ b/translations/es.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Búsqueda de artículos deseados", "Website": "Sitio web", "Low Bandwidth": "Ancho de banda bajo", - "accommodation": "alojamiento" + "accommodation": "alojamiento", + "Forbidden": "Prohibida", + "You're not allowed": "No tienes permiso" } diff --git a/translations/fr.json b/translations/fr.json index 102915216..4faf980a8 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Recherche d'objets recherchés", "Website": "Site Internet", "Low Bandwidth": "Bas débit", - "accommodation": "hébergement" + "accommodation": "hébergement", + "Forbidden": "Interdite", + "You're not allowed": "Tu n'as pas le droit" } diff --git a/translations/ga.json b/translations/ga.json index 294542ffd..f50567c05 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Cuardaigh Míreanna Teastaíonn", "Website": "Suíomh gréasáin", "Low Bandwidth": "Bandaleithead íseal", - "accommodation": "lóistín" + "accommodation": "lóistín", + "Forbidden": "Toirmiscthe", + "You're not allowed": "Níl cead agat" } diff --git a/translations/hi.json b/translations/hi.json index e01413f12..cb948e1c6 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -475,5 +475,7 @@ "Wanted Items Search": "वांटेड आइटम सर्च", "Website": "वेबसाइट", "Low Bandwidth": "कम बैंडविड्थ", - "accommodation": "निवास स्थान" + "accommodation": "निवास स्थान", + "Forbidden": "निषिद्ध", + "You're not allowed": "आपको अनुमति नहीं है" } diff --git a/translations/it.json b/translations/it.json index d9eedff96..b1e46e249 100644 --- a/translations/it.json +++ b/translations/it.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Ricerca articoli ricercati", "Website": "Sito web", "Low Bandwidth": "Bassa larghezza di banda", - "accommodation": "struttura ricettiva" + "accommodation": "struttura ricettiva", + "Forbidden": "Proibita", + "You're not allowed": "Non ti è permesso" } diff --git a/translations/ja.json b/translations/ja.json index ab7bd3f6d..5ff84212d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -475,5 +475,7 @@ "Wanted Items Search": "欲しいアイテム検索", "Website": "Webサイト", "Low Bandwidth": "低帯域幅", - "accommodation": "宿泊施設" + "accommodation": "宿泊施設", + "Forbidden": "禁断", + "You're not allowed": "あなたは許可されていません" } diff --git a/translations/ku.json b/translations/ku.json index 7bfb58c67..3915ac9df 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Wanted Items Search", "Website": "Malper", "Low Bandwidth": "Bandwidth kêm", - "accommodation": "cih" + "accommodation": "cih", + "Forbidden": "Qedexekirî", + "You're not allowed": "Destûrê nadin te" } diff --git a/translations/oc.json b/translations/oc.json index fc9e2efdb..1199e3154 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -471,5 +471,7 @@ "Wanted Items Search": "Wanted Items Search", "Website": "Website", "Low Bandwidth": "Low Bandwidth", - "accommodation": "accommodation" + "accommodation": "accommodation", + "Forbidden": "Forbidden", + "You're not allowed": "You're not allowed" } diff --git a/translations/pt.json b/translations/pt.json index 7451b2f15..9c7f14fd5 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Pesquisa de Itens Desejados", "Website": "Local na rede Internet", "Low Bandwidth": "Baixa largura de banda", - "accommodation": "alojamento" + "accommodation": "alojamento", + "Forbidden": "Proibida", + "You're not allowed": "Você não tem permissão" } diff --git a/translations/ru.json b/translations/ru.json index 76b2e6c93..514ef74af 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Поиск требуемых предметов", "Website": "Интернет сайт", "Low Bandwidth": "Низкая пропускная способность", - "accommodation": "размещение" + "accommodation": "размещение", + "Forbidden": "Запрещенный", + "You're not allowed": "Вам не разрешено" } diff --git a/translations/sw.json b/translations/sw.json index 155b08db4..f7bb38937 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -475,5 +475,7 @@ "Wanted Items Search": "Utafutaji wa Vitu vinavyotafutwa", "Website": "Tovuti", "Low Bandwidth": "Bandwidth ya chini", - "accommodation": "malazi" + "accommodation": "malazi", + "Forbidden": "Imekatazwa", + "You're not allowed": "Hauruhusiwi" } diff --git a/translations/zh.json b/translations/zh.json index 4ae5e750e..684fe5322 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -475,5 +475,7 @@ "Wanted Items Search": "通缉物品搜索", "Website": "网站", "Low Bandwidth": "低带宽", - "accommodation": "住所" + "accommodation": "住所", + "Forbidden": "禁止的", + "You're not allowed": "你不被允许" } diff --git a/utils.py b/utils.py index 592ef0c25..716c35112 100644 --- a/utils.py +++ b/utils.py @@ -1317,6 +1317,74 @@ def locatePost(baseDir: str, nickname: str, domain: str, return None +def _getPublishedDate(postJsonObject: {}) -> str: + """Returns the published date on the given post + """ + published = None + if postJsonObject.get('published'): + published = postJsonObject['published'] + elif postJsonObject.get('object'): + if isinstance(postJsonObject['object'], dict): + if postJsonObject['object'].get('published'): + published = postJsonObject['object']['published'] + if not published: + return None + if not isinstance(published, str): + return None + return published + + +def getReplyIntervalHours(baseDir: str, nickname: str, domain: str, + defaultReplyIntervalHours: int) -> int: + """Returns the reply interval for the given account. + The reply interval is the number of hours after a post being made + during which replies are allowed + """ + replyIntervalFilename = \ + acctDir(baseDir, nickname, domain) + '/.replyIntervalHours' + if os.path.isfile(replyIntervalFilename): + with open(replyIntervalFilename, 'r') as fp: + hoursStr = fp.read() + if hoursStr.isdigit(): + return int(hoursStr) + return defaultReplyIntervalHours + + +def canReplyTo(baseDir: str, nickname: str, domain: str, + postUrl: str, replyIntervalHours: int, + currDateStr: str = None) -> bool: + """Is replying to the given post permitted? + This is a spam mitigation feature, so that spammers can't + add a lot of replies to old post which you don't notice. + """ + postFilename = locatePost(baseDir, nickname, domain, postUrl) + if not postFilename: + return False + postJsonObject = loadJson(postFilename) + if not postJsonObject: + return False + published = _getPublishedDate(postJsonObject) + if not published: + return False + try: + pubDate = datetime.datetime.strptime(published, '%Y-%m-%dT%H:%M:%SZ') + except BaseException: + return False + if not currDateStr: + currDate = datetime.datetime.utcnow() + else: + try: + currDate = datetime.datetime.strptime(currDateStr, + '%Y-%m-%dT%H:%M:%SZ') + except BaseException: + return False + hoursSincePublication = int((currDate - pubDate).total_seconds() / 3600) + if hoursSincePublication < 0 or \ + hoursSincePublication > replyIntervalHours: + return False + return True + + def _removeAttachment(baseDir: str, httpPrefix: str, domain: str, postJson: {}): if not postJson.get('attachment'):