Implement reply interval in hours

The time after publication of a post during which replies are permitted
merge-requests/30/head
Bob Mottram 2021-09-08 19:37:04 +01:00
parent f84149cd3a
commit 9601220e66
22 changed files with 199 additions and 32 deletions

View File

@ -232,6 +232,7 @@ from categories import updateHashtagCategories
from languages import getActorLanguages from languages import getActorLanguages
from languages import setActorLanguages from languages import setActorLanguages
from like import updateLikesCollection from like import updateLikesCollection
from utils import canReplyTo
from utils import isDM from utils import isDM
from utils import replaceUsersWithAt from utils import replaceUsersWithAt
from utils import localActorUrl from utils import localActorUrl
@ -857,6 +858,14 @@ class PubServer(BaseHTTPRequestHandler):
'This is nothing less ' + 'This is nothing less ' +
'than an utter triumph') '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: def _404(self) -> None:
if self.server.translate: if self.server.translate:
self._httpReturnCode(404, self.server.translate['Not Found'], self._httpReturnCode(404, self.server.translate['Not Found'],
@ -11287,6 +11296,18 @@ class PubServer(BaseHTTPRequestHandler):
if isNewPostEndpoint: if isNewPostEndpoint:
nickname = getNicknameFromActor(path) 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 accessKeys = self.server.accessKeys
if self.server.keyShortcuts.get(nickname): if self.server.keyShortcuts.get(nickname):
accessKeys = self.server.keyShortcuts[nickname] accessKeys = self.server.keyShortcuts[nickname]
@ -16245,7 +16266,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
break break
def runDaemon(lowBandwidth: bool, def runDaemon(defaultReplyIntervalHours: int,
lowBandwidth: bool,
maxLikeCount: int, maxLikeCount: int,
sharedItemsFederatedDomains: [], sharedItemsFederatedDomains: [],
userAgentsBlocked: [], userAgentsBlocked: [],
@ -16376,6 +16398,11 @@ def runDaemon(lowBandwidth: bool,
'Public': 'p', 'Public': 'p',
'Reminder': 'r' 'Reminder': 'r'
} }
# how many hours after a post was publushed can a reply be made
defaultReplyIntervalHours = 9999999
httpd.defaultReplyIntervalHours = defaultReplyIntervalHours
httpd.keyShortcuts = {} httpd.keyShortcuts = {}
loadAccessKeysForAccounts(baseDir, httpd.keyShortcuts, httpd.accessKeys) loadAccessKeysForAccounts(baseDir, httpd.keyShortcuts, httpd.accessKeys)
@ -16704,7 +16731,8 @@ def runDaemon(lowBandwidth: bool,
httpd.themeName, httpd.themeName,
httpd.systemLanguage, httpd.systemLanguage,
httpd.maxLikeCount, httpd.maxLikeCount,
httpd.signingPrivateKeyPem), daemon=True) httpd.signingPrivateKeyPem,
httpd.defaultReplyIntervalHours), daemon=True)
print('Creating scheduled post thread') print('Creating scheduled post thread')
httpd.thrPostSchedule = \ httpd.thrPostSchedule = \

View File

@ -170,6 +170,11 @@ parser.add_argument('--dormantMonths',
default=3, default=3,
help='How many months does a followed account need to ' + help='How many months does a followed account need to ' +
'be unseen for before being considered dormant') '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', parser.add_argument('--sendThreadsTimeoutMins',
dest='sendThreadsTimeoutMins', type=int, dest='sendThreadsTimeoutMins', type=int,
default=30, default=30,
@ -3031,7 +3036,8 @@ if args.defaultCurrency:
print('Default currency set to ' + args.defaultCurrency) print('Default currency set to ' + args.defaultCurrency)
if __name__ == "__main__": if __name__ == "__main__":
runDaemon(args.lowBandwidth, args.maxLikeCount, runDaemon(args.defaultReplyIntervalHours,
args.lowBandwidth, args.maxLikeCount,
sharedItemsFederatedDomains, sharedItemsFederatedDomains,
userAgentsBlocked, userAgentsBlocked,
args.logLoginFailures, args.logLoginFailures,

View File

@ -15,6 +15,8 @@ import random
from linked_data_sig import verifyJsonSignature from linked_data_sig import verifyJsonSignature
from languages import understoodPostLanguage from languages import understoodPostLanguage
from like import updateLikesCollection from like import updateLikesCollection
from utils import getReplyIntervalHours
from utils import canReplyTo
from utils import getUserPaths from utils import getUserPaths
from utils import getBaseContentFromPost from utils import getBaseContentFromPost
from utils import acctDir from utils import acctDir
@ -2484,7 +2486,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
lastBounceMessage: [], lastBounceMessage: [],
themeName: str, systemLanguage: str, themeName: str, systemLanguage: str,
maxLikeCount: int, maxLikeCount: int,
signingPrivateKeyPem: str) -> bool: signingPrivateKeyPem: str,
defaultReplyIntervalHours: int) -> bool:
""" Anything which needs to be done after initial checks have passed """ Anything which needs to be done after initial checks have passed
""" """
actor = keyId actor = keyId
@ -2765,11 +2768,29 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
if isinstance(inReplyTo, str): if isinstance(inReplyTo, str):
if not isMuted(baseDir, nickname, domain, if not isMuted(baseDir, nickname, domain,
inReplyTo, conversationId): inReplyTo, conversationId):
actUrl = \ # check if the reply is within the allowed
localActorUrl(httpPrefix, # time period after publication
nickname, domain) hrs = defaultReplyIntervalHours
_replyNotify(baseDir, handle, replyIntervalHours = \
actUrl + '/tlreplies') 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: else:
isReplyToMutedPost = True isReplyToMutedPost = True
@ -3119,7 +3140,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
peertubeInstances: [], peertubeInstances: [],
verifyAllSignatures: bool, verifyAllSignatures: bool,
themeName: str, systemLanguage: str, 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 """Processes received items and moves them to the appropriate
directories directories
""" """
@ -3534,7 +3556,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
lastBounceMessage, lastBounceMessage,
themeName, systemLanguage, themeName, systemLanguage,
maxLikeCount, maxLikeCount,
signingPrivateKeyPem) signingPrivateKeyPem,
defaultReplyIntervalHours)
if debug: if debug:
pprint(queueJson['post']) pprint(queueJson['post'])
print('Queue: Queue post accepted') print('Queue: Queue post accepted')

View File

@ -649,8 +649,10 @@ def createServerAlice(path: str, domain: str, port: int,
logLoginFailures = False logLoginFailures = False
userAgentsBlocked = [] userAgentsBlocked = []
maxLikeCount = 10 maxLikeCount = 10
defaultReplyIntervalHours = 9999999999
print('Server running: Alice') print('Server running: Alice')
runDaemon(lowBandwidth, maxLikeCount, runDaemon(defaultReplyIntervalHours,
lowBandwidth, maxLikeCount,
sharedItemsFederatedDomains, sharedItemsFederatedDomains,
userAgentsBlocked, userAgentsBlocked,
logLoginFailures, city, logLoginFailures, city,
@ -785,8 +787,10 @@ def createServerBob(path: str, domain: str, port: int,
logLoginFailures = False logLoginFailures = False
userAgentsBlocked = [] userAgentsBlocked = []
maxLikeCount = 10 maxLikeCount = 10
defaultReplyIntervalHours = 9999999999
print('Server running: Bob') print('Server running: Bob')
runDaemon(lowBandwidth, maxLikeCount, runDaemon(defaultReplyIntervalHours,
lowBandwidth, maxLikeCount,
sharedItemsFederatedDomains, sharedItemsFederatedDomains,
userAgentsBlocked, userAgentsBlocked,
logLoginFailures, city, logLoginFailures, city,
@ -850,8 +854,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
userAgentsBlocked = [] userAgentsBlocked = []
maxLikeCount = 10 maxLikeCount = 10
lowBandwidth = True lowBandwidth = True
defaultReplyIntervalHours = 9999999999
print('Server running: Eve') print('Server running: Eve')
runDaemon(lowBandwidth, maxLikeCount, runDaemon(defaultReplyIntervalHours,
lowBandwidth, maxLikeCount,
sharedItemsFederatedDomains, sharedItemsFederatedDomains,
userAgentsBlocked, userAgentsBlocked,
logLoginFailures, city, logLoginFailures, city,
@ -917,8 +923,10 @@ def createServerGroup(path: str, domain: str, port: int,
userAgentsBlocked = [] userAgentsBlocked = []
maxLikeCount = 10 maxLikeCount = 10
lowBandwidth = True lowBandwidth = True
defaultReplyIntervalHours = 9999999999
print('Server running: Group') print('Server running: Group')
runDaemon(lowBandwidth, maxLikeCount, runDaemon(defaultReplyIntervalHours,
lowBandwidth, maxLikeCount,
sharedItemsFederatedDomains, sharedItemsFederatedDomains,
userAgentsBlocked, userAgentsBlocked,
logLoginFailures, city, logLoginFailures, city,

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "البحث عن العناصر المطلوبة", "Wanted Items Search": "البحث عن العناصر المطلوبة",
"Website": "موقع إلكتروني", "Website": "موقع إلكتروني",
"Low Bandwidth": "انخفاض النطاق الترددي", "Low Bandwidth": "انخفاض النطاق الترددي",
"accommodation": "الإقامة" "accommodation": "الإقامة",
"Forbidden": "محرم",
"You're not allowed": "كنت لا يسمح"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Cerca d'articles desitjats", "Wanted Items Search": "Cerca d'articles desitjats",
"Website": "Lloc web", "Website": "Lloc web",
"Low Bandwidth": "Ample de banda baixa", "Low Bandwidth": "Ample de banda baixa",
"accommodation": "allotjament" "accommodation": "allotjament",
"Forbidden": "Prohibit",
"You're not allowed": "No està permès"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Chwilio Eitemau Eisiau", "Wanted Items Search": "Chwilio Eitemau Eisiau",
"Website": "Gwefan", "Website": "Gwefan",
"Low Bandwidth": "Lled band isel", "Low Bandwidth": "Lled band isel",
"accommodation": "llety" "accommodation": "llety",
"Forbidden": "Wedi'i wahardd",
"You're not allowed": "Ni chaniateir i chi"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Gesuchte Artikel suchen", "Wanted Items Search": "Gesuchte Artikel suchen",
"Website": "Webseite", "Website": "Webseite",
"Low Bandwidth": "Niedrige Bandbreite", "Low Bandwidth": "Niedrige Bandbreite",
"accommodation": "unterkunft" "accommodation": "unterkunft",
"Forbidden": "Verboten",
"You're not allowed": "Du darfst nicht"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Wanted Items Search", "Wanted Items Search": "Wanted Items Search",
"Website": "Website", "Website": "Website",
"Low Bandwidth": "Low Bandwidth", "Low Bandwidth": "Low Bandwidth",
"accommodation": "accommodation" "accommodation": "accommodation",
"Forbidden": "Forbidden",
"You're not allowed": "You're not allowed"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Búsqueda de artículos deseados", "Wanted Items Search": "Búsqueda de artículos deseados",
"Website": "Sitio web", "Website": "Sitio web",
"Low Bandwidth": "Ancho de banda bajo", "Low Bandwidth": "Ancho de banda bajo",
"accommodation": "alojamiento" "accommodation": "alojamiento",
"Forbidden": "Prohibida",
"You're not allowed": "No tienes permiso"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Recherche d'objets recherchés", "Wanted Items Search": "Recherche d'objets recherchés",
"Website": "Site Internet", "Website": "Site Internet",
"Low Bandwidth": "Bas débit", "Low Bandwidth": "Bas débit",
"accommodation": "hébergement" "accommodation": "hébergement",
"Forbidden": "Interdite",
"You're not allowed": "Tu n'as pas le droit"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Cuardaigh Míreanna Teastaíonn", "Wanted Items Search": "Cuardaigh Míreanna Teastaíonn",
"Website": "Suíomh gréasáin", "Website": "Suíomh gréasáin",
"Low Bandwidth": "Bandaleithead íseal", "Low Bandwidth": "Bandaleithead íseal",
"accommodation": "lóistín" "accommodation": "lóistín",
"Forbidden": "Toirmiscthe",
"You're not allowed": "Níl cead agat"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "वांटेड आइटम सर्च", "Wanted Items Search": "वांटेड आइटम सर्च",
"Website": "वेबसाइट", "Website": "वेबसाइट",
"Low Bandwidth": "कम बैंडविड्थ", "Low Bandwidth": "कम बैंडविड्थ",
"accommodation": "निवास स्थान" "accommodation": "निवास स्थान",
"Forbidden": "निषिद्ध",
"You're not allowed": "आपको अनुमति नहीं है"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Ricerca articoli ricercati", "Wanted Items Search": "Ricerca articoli ricercati",
"Website": "Sito web", "Website": "Sito web",
"Low Bandwidth": "Bassa larghezza di banda", "Low Bandwidth": "Bassa larghezza di banda",
"accommodation": "struttura ricettiva" "accommodation": "struttura ricettiva",
"Forbidden": "Proibita",
"You're not allowed": "Non ti è permesso"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "欲しいアイテム検索", "Wanted Items Search": "欲しいアイテム検索",
"Website": "Webサイト", "Website": "Webサイト",
"Low Bandwidth": "低帯域幅", "Low Bandwidth": "低帯域幅",
"accommodation": "宿泊施設" "accommodation": "宿泊施設",
"Forbidden": "禁断",
"You're not allowed": "あなたは許可されていません"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Wanted Items Search", "Wanted Items Search": "Wanted Items Search",
"Website": "Malper", "Website": "Malper",
"Low Bandwidth": "Bandwidth kêm", "Low Bandwidth": "Bandwidth kêm",
"accommodation": "cih" "accommodation": "cih",
"Forbidden": "Qedexekirî",
"You're not allowed": "Destûrê nadin te"
} }

View File

@ -471,5 +471,7 @@
"Wanted Items Search": "Wanted Items Search", "Wanted Items Search": "Wanted Items Search",
"Website": "Website", "Website": "Website",
"Low Bandwidth": "Low Bandwidth", "Low Bandwidth": "Low Bandwidth",
"accommodation": "accommodation" "accommodation": "accommodation",
"Forbidden": "Forbidden",
"You're not allowed": "You're not allowed"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Pesquisa de Itens Desejados", "Wanted Items Search": "Pesquisa de Itens Desejados",
"Website": "Local na rede Internet", "Website": "Local na rede Internet",
"Low Bandwidth": "Baixa largura de banda", "Low Bandwidth": "Baixa largura de banda",
"accommodation": "alojamento" "accommodation": "alojamento",
"Forbidden": "Proibida",
"You're not allowed": "Você não tem permissão"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Поиск требуемых предметов", "Wanted Items Search": "Поиск требуемых предметов",
"Website": "Интернет сайт", "Website": "Интернет сайт",
"Low Bandwidth": "Низкая пропускная способность", "Low Bandwidth": "Низкая пропускная способность",
"accommodation": "размещение" "accommodation": "размещение",
"Forbidden": "Запрещенный",
"You're not allowed": "Вам не разрешено"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "Utafutaji wa Vitu vinavyotafutwa", "Wanted Items Search": "Utafutaji wa Vitu vinavyotafutwa",
"Website": "Tovuti", "Website": "Tovuti",
"Low Bandwidth": "Bandwidth ya chini", "Low Bandwidth": "Bandwidth ya chini",
"accommodation": "malazi" "accommodation": "malazi",
"Forbidden": "Imekatazwa",
"You're not allowed": "Hauruhusiwi"
} }

View File

@ -475,5 +475,7 @@
"Wanted Items Search": "通缉物品搜索", "Wanted Items Search": "通缉物品搜索",
"Website": "网站", "Website": "网站",
"Low Bandwidth": "低带宽", "Low Bandwidth": "低带宽",
"accommodation": "住所" "accommodation": "住所",
"Forbidden": "禁止的",
"You're not allowed": "你不被允许"
} }

View File

@ -1317,6 +1317,74 @@ def locatePost(baseDir: str, nickname: str, domain: str,
return None 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, def _removeAttachment(baseDir: str, httpPrefix: str, domain: str,
postJson: {}): postJson: {}):
if not postJson.get('attachment'): if not postJson.get('attachment'):