epicyon/blocking.py

988 lines
36 KiB
Python
Raw Normal View History

2020-04-01 20:06:27 +00:00
__filename__ = "blocking.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-04-01 20:06:27 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-04-01 20:06:27 +00:00
__status__ = "Production"
2021-06-25 16:10:09 +00:00
__module_group__ = "Core"
2019-07-14 19:27:13 +00:00
import os
2021-03-20 21:20:41 +00:00
import json
2021-06-21 09:22:24 +00:00
import time
2021-02-15 22:26:25 +00:00
from datetime import datetime
2021-10-13 11:15:06 +00:00
from utils import hasObjectString
2021-10-13 10:37:52 +00:00
from utils import hasObjectStringObject
2021-10-13 10:11:02 +00:00
from utils import hasObjectStringType
2021-06-26 14:21:24 +00:00
from utils import removeDomainPort
from utils import hasObjectDict
from utils import isAccountDir
2021-03-20 21:20:41 +00:00
from utils import getCachedPostFilename
from utils import loadJson
from utils import saveJson
2021-02-15 22:26:25 +00:00
from utils import fileLastModified
2021-02-15 22:06:53 +00:00
from utils import setConfigParam
2020-12-23 10:57:44 +00:00
from utils import hasUsersPath
2020-12-16 10:30:54 +00:00
from utils import getFullDomain
2020-08-23 11:13:35 +00:00
from utils import removeIdEnding
2019-09-09 15:53:23 +00:00
from utils import isEvil
2020-04-01 20:06:27 +00:00
from utils import locatePost
2020-03-28 10:33:04 +00:00
from utils import evilIncarnate
2020-04-01 20:06:27 +00:00
from utils import getDomainFromActor
from utils import getNicknameFromActor
2021-07-13 21:59:53 +00:00
from utils import acctDir
2021-08-14 11:13:39 +00:00
from utils import localActorUrl
from utils import hasActor
2021-08-12 10:22:04 +00:00
from conversation import muteConversation
from conversation import unmuteConversation
2019-07-14 19:27:13 +00:00
2020-04-01 20:06:27 +00:00
def addGlobalBlock(baseDir: str,
blockNickname: str, blockDomain: str) -> bool:
"""Global block which applies to all accounts
"""
2020-04-01 20:06:27 +00:00
blockingFilename = baseDir + '/accounts/blocking.txt'
2020-03-22 21:16:02 +00:00
if not blockNickname.startswith('#'):
2020-09-05 09:41:09 +00:00
# is the handle already blocked?
2020-09-05 09:42:52 +00:00
blockHandle = blockNickname + '@' + blockDomain
2019-08-14 10:32:15 +00:00
if os.path.isfile(blockingFilename):
if blockHandle in open(blockingFilename).read():
return False
2020-09-05 09:41:09 +00:00
# block an account handle or domain
2021-06-22 12:27:10 +00:00
with open(blockingFilename, 'a+') as blockFile:
blockFile.write(blockHandle + '\n')
2019-08-14 10:32:15 +00:00
else:
2020-04-01 20:06:27 +00:00
blockHashtag = blockNickname
2020-09-05 09:41:09 +00:00
# is the hashtag already blocked?
2019-08-14 10:32:15 +00:00
if os.path.isfile(blockingFilename):
2020-04-01 20:06:27 +00:00
if blockHashtag + '\n' in open(blockingFilename).read():
2019-08-14 10:32:15 +00:00
return False
2020-09-05 09:41:09 +00:00
# block a hashtag
2021-06-22 12:27:10 +00:00
with open(blockingFilename, 'a+') as blockFile:
blockFile.write(blockHashtag + '\n')
return True
2020-04-01 20:06:27 +00:00
def addBlock(baseDir: str, nickname: str, domain: str,
blockNickname: str, blockDomain: str) -> bool:
2019-07-14 19:27:13 +00:00
"""Block the given account
"""
2021-08-11 21:21:56 +00:00
if blockDomain.startswith(domain) and nickname == blockNickname:
# don't block self
return False
domain = removeDomainPort(domain)
2021-07-13 21:59:53 +00:00
blockingFilename = acctDir(baseDir, nickname, domain) + '/blocking.txt'
2020-04-01 20:06:27 +00:00
blockHandle = blockNickname + '@' + blockDomain
2019-07-14 19:27:13 +00:00
if os.path.isfile(blockingFilename):
2021-08-11 21:00:01 +00:00
if blockHandle + '\n' in open(blockingFilename).read():
2019-07-14 19:57:05 +00:00
return False
2021-08-11 21:00:01 +00:00
# if we are following then unfollow
followingFilename = acctDir(baseDir, nickname, domain) + '/following.txt'
if os.path.isfile(followingFilename):
if blockHandle + '\n' in open(followingFilename).read():
followingStr = ''
with open(followingFilename, 'r') as followingFile:
followingStr = followingFile.read()
followingStr = followingStr.replace(blockHandle + '\n', '')
with open(followingFilename, 'w+') as followingFile:
followingFile.write(followingStr)
# if they are a follower then remove them
followersFilename = acctDir(baseDir, nickname, domain) + '/followers.txt'
if os.path.isfile(followersFilename):
if blockHandle + '\n' in open(followersFilename).read():
followersStr = ''
with open(followersFilename, 'r') as followersFile:
followersStr = followersFile.read()
followersStr = followersStr.replace(blockHandle + '\n', '')
with open(followersFilename, 'w+') as followersFile:
followersFile.write(followersStr)
2021-06-22 12:27:10 +00:00
with open(blockingFilename, 'a+') as blockFile:
blockFile.write(blockHandle + '\n')
2019-07-14 19:57:05 +00:00
return True
2019-07-14 19:27:13 +00:00
2020-04-01 20:06:27 +00:00
def removeGlobalBlock(baseDir: str,
unblockNickname: str,
unblockDomain: str) -> bool:
"""Unblock the given global block
"""
2020-04-01 20:06:27 +00:00
unblockingFilename = baseDir + '/accounts/blocking.txt'
2020-03-22 21:16:02 +00:00
if not unblockNickname.startswith('#'):
2020-04-01 20:06:27 +00:00
unblockHandle = unblockNickname + '@' + unblockDomain
2019-08-14 10:32:15 +00:00
if os.path.isfile(unblockingFilename):
if unblockHandle in open(unblockingFilename).read():
with open(unblockingFilename, 'r') as fp:
2020-07-12 20:04:58 +00:00
with open(unblockingFilename + '.new', 'w+') as fpnew:
2019-08-14 10:32:15 +00:00
for line in fp:
2020-05-22 11:32:38 +00:00
handle = line.replace('\n', '').replace('\r', '')
2019-08-14 10:32:15 +00:00
if unblockHandle not in line:
2020-04-01 20:06:27 +00:00
fpnew.write(handle + '\n')
if os.path.isfile(unblockingFilename + '.new'):
os.rename(unblockingFilename + '.new', unblockingFilename)
2019-08-14 10:32:15 +00:00
return True
else:
2020-04-01 20:06:27 +00:00
unblockHashtag = unblockNickname
2019-08-14 10:32:15 +00:00
if os.path.isfile(unblockingFilename):
2020-04-01 20:06:27 +00:00
if unblockHashtag + '\n' in open(unblockingFilename).read():
2019-08-14 10:32:15 +00:00
with open(unblockingFilename, 'r') as fp:
2020-07-12 20:04:58 +00:00
with open(unblockingFilename + '.new', 'w+') as fpnew:
2019-08-14 10:32:15 +00:00
for line in fp:
2020-05-22 11:32:38 +00:00
blockLine = \
line.replace('\n', '').replace('\r', '')
2019-08-14 10:32:15 +00:00
if unblockHashtag not in line:
2020-04-01 20:06:27 +00:00
fpnew.write(blockLine + '\n')
if os.path.isfile(unblockingFilename + '.new'):
os.rename(unblockingFilename + '.new', unblockingFilename)
2019-08-14 10:32:15 +00:00
return True
return False
2020-04-01 20:06:27 +00:00
def removeBlock(baseDir: str, nickname: str, domain: str,
unblockNickname: str, unblockDomain: str) -> bool:
2019-07-14 19:27:13 +00:00
"""Unblock the given account
"""
domain = removeDomainPort(domain)
2021-07-13 21:59:53 +00:00
unblockingFilename = acctDir(baseDir, nickname, domain) + '/blocking.txt'
2020-04-01 20:06:27 +00:00
unblockHandle = unblockNickname + '@' + unblockDomain
2019-07-14 19:27:13 +00:00
if os.path.isfile(unblockingFilename):
if unblockHandle in open(unblockingFilename).read():
with open(unblockingFilename, 'r') as fp:
2020-07-12 20:04:58 +00:00
with open(unblockingFilename + '.new', 'w+') as fpnew:
2019-07-14 19:27:13 +00:00
for line in fp:
2020-05-22 11:32:38 +00:00
handle = line.replace('\n', '').replace('\r', '')
2019-07-14 19:27:13 +00:00
if unblockHandle not in line:
2020-04-01 20:06:27 +00:00
fpnew.write(handle + '\n')
if os.path.isfile(unblockingFilename + '.new'):
os.rename(unblockingFilename + '.new', unblockingFilename)
2019-07-14 19:57:05 +00:00
return True
return False
2019-08-14 10:32:15 +00:00
2020-04-01 20:06:27 +00:00
def isBlockedHashtag(baseDir: str, hashtag: str) -> bool:
2019-08-14 10:32:15 +00:00
"""Is the given hashtag blocked?
"""
2020-08-07 20:40:53 +00:00
# avoid very long hashtags
if len(hashtag) > 32:
return True
2020-04-01 20:06:27 +00:00
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
2019-08-14 10:32:15 +00:00
if os.path.isfile(globalBlockingFilename):
2020-05-22 11:32:38 +00:00
hashtag = hashtag.strip('\n').strip('\r')
if not hashtag.startswith('#'):
hashtag = '#' + hashtag
2020-04-01 20:06:27 +00:00
if hashtag + '\n' in open(globalBlockingFilename).read():
2019-08-14 10:32:15 +00:00
return True
return False
2020-04-01 20:06:27 +00:00
2020-03-28 10:33:04 +00:00
def getDomainBlocklist(baseDir: str) -> str:
"""Returns all globally blocked domains as a string
This can be used for fast matching to mitigate flooding
"""
2020-04-01 20:06:27 +00:00
blockedStr = ''
2020-03-28 10:33:04 +00:00
2020-04-01 20:06:27 +00:00
evilDomains = evilIncarnate()
2020-03-28 10:33:04 +00:00
for evil in evilDomains:
2020-04-01 20:06:27 +00:00
blockedStr += evil + '\n'
2020-03-28 10:33:04 +00:00
2020-04-01 20:06:27 +00:00
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
2020-03-28 10:33:04 +00:00
if not os.path.isfile(globalBlockingFilename):
return blockedStr
2020-10-29 10:36:38 +00:00
with open(globalBlockingFilename, 'r') as fpBlocked:
blockedStr += fpBlocked.read()
2020-03-28 10:33:04 +00:00
return blockedStr
2020-04-01 20:06:27 +00:00
2021-06-21 09:22:24 +00:00
def updateBlockedCache(baseDir: str,
blockedCache: [],
blockedCacheLastUpdated: int,
blockedCacheUpdateSecs: int) -> int:
"""Updates the cache of globally blocked domains held in memory
"""
currTime = int(time.time())
2021-06-21 09:40:43 +00:00
if blockedCacheLastUpdated > currTime:
print('WARN: Cache updated in the future')
blockedCacheLastUpdated = 0
2021-06-21 09:22:24 +00:00
secondsSinceLastUpdate = currTime - blockedCacheLastUpdated
if secondsSinceLastUpdate < blockedCacheUpdateSecs:
return blockedCacheLastUpdated
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if not os.path.isfile(globalBlockingFilename):
return blockedCacheLastUpdated
with open(globalBlockingFilename, 'r') as fpBlocked:
blockedLines = fpBlocked.readlines()
2021-06-21 14:39:05 +00:00
# remove newlines
for index in range(len(blockedLines)):
blockedLines[index] = blockedLines[index].replace('\n', '')
# update the cache
2021-06-21 09:22:24 +00:00
blockedCache.clear()
2021-06-21 14:39:05 +00:00
blockedCache += blockedLines
2021-06-21 09:22:24 +00:00
return currTime
def _getShortDomain(domain: str) -> str:
""" by checking a shorter version we can thwart adversaries
who constantly change their subdomain
e.g. subdomain123.mydomain.com becomes mydomain.com
"""
sections = domain.split('.')
noOfSections = len(sections)
if noOfSections > 2:
return sections[noOfSections-2] + '.' + sections[-1]
return None
2021-06-21 09:22:24 +00:00
def isBlockedDomain(baseDir: str, domain: str,
blockedCache: [] = None) -> bool:
"""Is the given domain blocked?
"""
2020-10-29 10:36:38 +00:00
if '.' not in domain:
return False
if isEvil(domain):
return True
2020-10-29 10:36:38 +00:00
shortDomain = _getShortDomain(domain)
2020-10-29 10:36:38 +00:00
2021-06-21 11:10:08 +00:00
if not brochModeIsActive(baseDir):
2021-06-21 09:22:24 +00:00
if blockedCache:
for blockedStr in blockedCache:
2021-02-15 21:14:05 +00:00
if '*@' + domain in blockedStr:
2020-10-29 10:36:38 +00:00
return True
2021-02-15 21:14:05 +00:00
if shortDomain:
if '*@' + shortDomain in blockedStr:
return True
2021-06-21 09:22:24 +00:00
else:
# instance block list
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(globalBlockingFilename):
with open(globalBlockingFilename, 'r') as fpBlocked:
blockedStr = fpBlocked.read()
if '*@' + domain in blockedStr:
return True
if shortDomain:
if '*@' + shortDomain in blockedStr:
return True
2021-02-15 21:14:05 +00:00
else:
2021-06-21 11:10:08 +00:00
allowFilename = baseDir + '/accounts/allowedinstances.txt'
2021-02-15 21:14:05 +00:00
# instance allow list
if not shortDomain:
if domain not in open(allowFilename).read():
return True
else:
if shortDomain not in open(allowFilename).read():
return True
return False
2020-04-01 20:06:27 +00:00
def isBlocked(baseDir: str, nickname: str, domain: str,
blockNickname: str, blockDomain: str,
blockedCache: [] = None) -> bool:
2019-07-14 19:27:13 +00:00
"""Is the given nickname blocked?
"""
2019-09-09 15:53:23 +00:00
if isEvil(blockDomain):
return True
blockHandle = None
if blockNickname and blockDomain:
blockHandle = blockNickname + '@' + blockDomain
if not brochModeIsActive(baseDir):
# instance level block list
if blockedCache:
for blockedStr in blockedCache:
if '*@' + domain in blockedStr:
return True
if blockHandle:
if blockHandle in blockedStr:
return True
else:
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(globalBlockingFilename):
if '*@' + blockDomain in open(globalBlockingFilename).read():
return True
if blockHandle:
if blockHandle in open(globalBlockingFilename).read():
return True
else:
# instance allow list
allowFilename = baseDir + '/accounts/allowedinstances.txt'
shortDomain = _getShortDomain(blockDomain)
if not shortDomain:
if blockDomain not in open(allowFilename).read():
return True
else:
if shortDomain not in open(allowFilename).read():
return True
# account level allow list
2021-07-13 21:59:53 +00:00
accountDir = acctDir(baseDir, nickname, domain)
allowFilename = accountDir + '/allowedinstances.txt'
if os.path.isfile(allowFilename):
if blockDomain not in open(allowFilename).read():
return True
# account level block list
blockingFilename = accountDir + '/blocking.txt'
2019-07-14 19:27:13 +00:00
if os.path.isfile(blockingFilename):
2020-04-01 20:06:27 +00:00
if '*@' + blockDomain in open(blockingFilename).read():
2019-08-02 11:46:42 +00:00
return True
if blockHandle:
2020-02-05 17:39:41 +00:00
if blockHandle in open(blockingFilename).read():
return True
2019-07-14 19:27:13 +00:00
return False
2020-04-01 20:06:27 +00:00
def outboxBlock(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
2021-08-11 21:10:26 +00:00
messageJson: {}, debug: bool) -> bool:
2019-07-17 21:40:56 +00:00
""" When a block request is received by the outbox from c2s
"""
if not messageJson.get('type'):
if debug:
print('DEBUG: block - no type')
2021-08-11 21:10:26 +00:00
return False
2020-04-01 20:06:27 +00:00
if not messageJson['type'] == 'Block':
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: not a block')
2021-08-11 21:10:26 +00:00
return False
2021-10-13 11:15:06 +00:00
if not hasObjectString(messageJson, debug):
2021-08-11 21:10:26 +00:00
return False
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: c2s block request arrived in outbox')
2020-08-23 11:13:35 +00:00
messageId = removeIdEnding(messageJson['object'])
2019-07-17 21:40:56 +00:00
if '/statuses/' not in messageId:
if debug:
print('DEBUG: c2s block object is not a status')
2021-08-11 21:10:26 +00:00
return False
2020-12-23 10:57:44 +00:00
if not hasUsersPath(messageId):
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: c2s block object has no nickname')
2021-08-11 21:10:26 +00:00
return False
domain = removeDomainPort(domain)
2020-04-01 20:06:27 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
2019-07-17 21:40:56 +00:00
if not postFilename:
if debug:
print('DEBUG: c2s block post not found in inbox or outbox')
print(messageId)
2021-08-11 21:10:26 +00:00
return False
2020-04-01 20:06:27 +00:00
nicknameBlocked = getNicknameFromActor(messageJson['object'])
2019-09-02 09:43:43 +00:00
if not nicknameBlocked:
2020-04-01 20:06:27 +00:00
print('WARN: unable to find nickname in ' + messageJson['object'])
2021-08-11 21:10:26 +00:00
return False
2020-04-01 20:06:27 +00:00
domainBlocked, portBlocked = getDomainFromActor(messageJson['object'])
2020-12-16 10:30:54 +00:00
domainBlockedFull = getFullDomain(domainBlocked, portBlocked)
2019-07-17 21:40:56 +00:00
2020-04-01 20:06:27 +00:00
addBlock(baseDir, nickname, domain,
nicknameBlocked, domainBlockedFull)
2020-03-22 20:59:01 +00:00
2019-07-17 21:40:56 +00:00
if debug:
2020-04-01 20:06:27 +00:00
print('DEBUG: post blocked via c2s - ' + postFilename)
2021-08-11 21:10:26 +00:00
return True
2020-04-01 20:06:27 +00:00
2019-07-17 21:40:56 +00:00
2020-04-01 20:06:27 +00:00
def outboxUndoBlock(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
messageJson: {}, debug: bool) -> None:
2019-07-17 21:40:56 +00:00
""" When an undo block request is received by the outbox from c2s
"""
if not messageJson.get('type'):
if debug:
print('DEBUG: undo block - no type')
return
2020-04-01 20:06:27 +00:00
if not messageJson['type'] == 'Undo':
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: not an undo block')
return
2021-10-13 10:11:02 +00:00
if not hasObjectStringType(messageJson, debug):
2019-07-17 21:40:56 +00:00
return
2020-04-01 20:06:27 +00:00
if not messageJson['object']['type'] == 'Block':
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: not an undo block')
return
2021-10-13 10:37:52 +00:00
if not hasObjectStringObject(messageJson, debug):
2019-07-17 21:40:56 +00:00
return
if debug:
print('DEBUG: c2s undo block request arrived in outbox')
2020-08-23 11:13:35 +00:00
messageId = removeIdEnding(messageJson['object']['object'])
2019-07-17 21:40:56 +00:00
if '/statuses/' not in messageId:
if debug:
print('DEBUG: c2s undo block object is not a status')
return
2020-12-23 10:57:44 +00:00
if not hasUsersPath(messageId):
2019-07-17 21:40:56 +00:00
if debug:
print('DEBUG: c2s undo block object has no nickname')
return
domain = removeDomainPort(domain)
2020-04-01 20:06:27 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
2019-07-17 21:40:56 +00:00
if not postFilename:
if debug:
print('DEBUG: c2s undo block post not found in inbox or outbox')
print(messageId)
2019-09-02 09:43:43 +00:00
return
2020-04-01 20:06:27 +00:00
nicknameBlocked = getNicknameFromActor(messageJson['object']['object'])
2019-09-02 09:43:43 +00:00
if not nicknameBlocked:
2020-04-01 20:06:27 +00:00
print('WARN: unable to find nickname in ' +
2020-03-30 19:09:45 +00:00
messageJson['object']['object'])
2019-09-02 09:43:43 +00:00
return
2020-04-01 20:06:27 +00:00
domainObject = messageJson['object']['object']
domainBlocked, portBlocked = getDomainFromActor(domainObject)
2020-12-16 10:30:54 +00:00
domainBlockedFull = getFullDomain(domainBlocked, portBlocked)
2019-07-17 21:40:56 +00:00
2020-04-01 20:06:27 +00:00
removeBlock(baseDir, nickname, domain,
nicknameBlocked, domainBlockedFull)
2019-07-17 21:40:56 +00:00
if debug:
2020-04-01 20:06:27 +00:00
print('DEBUG: post undo blocked via c2s - ' + postFilename)
2021-02-15 22:06:53 +00:00
def mutePost(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, postId: str, recentPostsCache: {},
debug: bool) -> None:
2021-03-20 21:20:41 +00:00
""" Mutes the given post
"""
2021-09-27 20:49:04 +00:00
print('mutePost: postId ' + postId)
2021-03-20 21:20:41 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
2021-09-27 20:49:04 +00:00
print('mutePost: file not found ' + postId)
2021-03-20 21:20:41 +00:00
return
postJsonObject = loadJson(postFilename)
if not postJsonObject:
2021-09-27 20:49:04 +00:00
print('mutePost: object not loaded ' + postId)
2021-03-20 21:20:41 +00:00
return
2021-09-27 20:45:09 +00:00
print('mutePost: ' + str(postJsonObject))
2021-03-20 21:20:41 +00:00
2021-09-28 11:20:14 +00:00
postJsonObj = postJsonObject
alsoUpdatePostId = None
if hasObjectDict(postJsonObject):
2021-09-28 11:20:14 +00:00
postJsonObj = postJsonObject['object']
else:
2021-10-13 11:15:06 +00:00
if hasObjectString(postJsonObject, debug):
alsoUpdatePostId = removeIdEnding(postJsonObject['object'])
2021-09-28 11:20:14 +00:00
domainFull = getFullDomain(domain, port)
actor = localActorUrl(httpPrefix, nickname, domainFull)
2021-08-12 10:22:04 +00:00
2021-09-28 11:20:14 +00:00
if postJsonObj.get('conversation'):
muteConversation(baseDir, nickname, domain,
postJsonObj['conversation'])
2021-08-12 10:22:04 +00:00
2021-09-28 11:20:14 +00:00
# does this post have ignores on it from differenent actors?
if not postJsonObj.get('ignores'):
if debug:
print('DEBUG: Adding initial mute to ' + postId)
ignoresJson = {
"@context": "https://www.w3.org/ns/activitystreams",
'id': postId,
'type': 'Collection',
"totalItems": 1,
'items': [{
'type': 'Ignore',
'actor': actor
2021-09-28 11:20:14 +00:00
}]
}
postJsonObj['ignores'] = ignoresJson
else:
if not postJsonObj['ignores'].get('items'):
postJsonObj['ignores']['items'] = []
itemsList = postJsonObj['ignores']['items']
for ignoresItem in itemsList:
if ignoresItem.get('actor'):
if ignoresItem['actor'] == actor:
return
newIgnore = {
'type': 'Ignore',
'actor': actor
}
igIt = len(itemsList)
itemsList.append(newIgnore)
postJsonObj['ignores']['totalItems'] = igIt
2021-09-28 13:24:57 +00:00
postJsonObj['muted'] = True
if saveJson(postJsonObject, postFilename):
print('mutePost: saved ' + postFilename)
2021-03-20 21:20:41 +00:00
# remove cached post so that the muted version gets recreated
# without its content text and/or image
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
try:
os.remove(cachedPostFilename)
2021-09-28 11:50:48 +00:00
print('MUTE: cached post removed ' + cachedPostFilename)
except BaseException:
2021-10-29 16:31:20 +00:00
print('EX: MUTE cached post not removed ' +
2021-10-29 14:48:24 +00:00
cachedPostFilename)
pass
2021-09-28 11:52:25 +00:00
else:
print('MUTE: cached post not found ' + cachedPostFilename)
2021-03-20 21:20:41 +00:00
2021-06-22 12:27:10 +00:00
with open(postFilename + '.muted', 'w+') as muteFile:
muteFile.write('\n')
2021-03-20 21:20:41 +00:00
print('MUTE: ' + postFilename + '.muted file added')
# if the post is in the recent posts cache then mark it as muted
if recentPostsCache.get('index'):
postId = \
removeIdEnding(postJsonObject['id']).replace('/', '#')
if postId in recentPostsCache['index']:
print('MUTE: ' + postId + ' is in recent posts cache')
2021-09-28 13:24:57 +00:00
if recentPostsCache.get('json'):
recentPostsCache['json'][postId] = json.dumps(postJsonObject)
print('MUTE: ' + postId +
' marked as muted in recent posts memory cache')
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(postId):
del recentPostsCache['html'][postId]
print('MUTE: ' + postId + ' removed cached html')
2021-09-28 15:48:14 +00:00
if alsoUpdatePostId:
2021-09-28 16:32:54 +00:00
postFilename = locatePost(baseDir, nickname, domain, alsoUpdatePostId)
if os.path.isfile(postFilename):
postJsonObj = loadJson(postFilename)
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain,
postJsonObj)
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
try:
os.remove(cachedPostFilename)
print('MUTE: cached referenced post removed ' +
cachedPostFilename)
except BaseException:
2021-10-29 16:31:20 +00:00
print('EX: ' +
2021-10-29 14:48:24 +00:00
'MUTE cached referenced post not removed ' +
cachedPostFilename)
2021-09-28 16:32:54 +00:00
pass
2021-09-28 15:48:14 +00:00
if recentPostsCache.get('json'):
if recentPostsCache['json'].get(alsoUpdatePostId):
del recentPostsCache['json'][alsoUpdatePostId]
print('MUTE: ' + alsoUpdatePostId + ' removed referenced json')
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(alsoUpdatePostId):
del recentPostsCache['html'][alsoUpdatePostId]
print('MUTE: ' + alsoUpdatePostId + ' removed referenced html')
2021-03-20 21:20:41 +00:00
def unmutePost(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, postId: str, recentPostsCache: {},
debug: bool) -> None:
2021-03-20 21:20:41 +00:00
""" Unmutes the given post
"""
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
return
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return
muteFilename = postFilename + '.muted'
if os.path.isfile(muteFilename):
try:
os.remove(muteFilename)
except BaseException:
2021-10-29 14:48:24 +00:00
if debug:
2021-10-29 16:31:20 +00:00
print('EX: unmutePost mute filename not deleted ' +
2021-10-29 14:48:24 +00:00
str(muteFilename))
pass
2021-03-20 21:20:41 +00:00
print('UNMUTE: ' + muteFilename + ' file removed')
2021-09-28 11:20:14 +00:00
postJsonObj = postJsonObject
alsoUpdatePostId = None
if hasObjectDict(postJsonObject):
2021-09-28 11:20:14 +00:00
postJsonObj = postJsonObject['object']
else:
2021-10-13 11:15:06 +00:00
if hasObjectString(postJsonObject, debug):
alsoUpdatePostId = removeIdEnding(postJsonObject['object'])
2021-09-28 11:20:14 +00:00
if postJsonObj.get('conversation'):
unmuteConversation(baseDir, nickname, domain,
postJsonObj['conversation'])
if postJsonObj.get('ignores'):
domainFull = getFullDomain(domain, port)
actor = localActorUrl(httpPrefix, nickname, domainFull)
totalItems = 0
if postJsonObj['ignores'].get('totalItems'):
totalItems = postJsonObj['ignores']['totalItems']
itemsList = postJsonObj['ignores']['items']
for ignoresItem in itemsList:
if ignoresItem.get('actor'):
if ignoresItem['actor'] == actor:
if debug:
print('DEBUG: mute was removed for ' + actor)
itemsList.remove(ignoresItem)
break
if totalItems == 1:
if debug:
print('DEBUG: mute was removed from post')
del postJsonObj['ignores']
else:
igItLen = len(postJsonObj['ignores']['items'])
postJsonObj['ignores']['totalItems'] = igItLen
2021-09-28 13:24:57 +00:00
postJsonObj['muted'] = False
2021-09-28 13:11:00 +00:00
saveJson(postJsonObject, postFilename)
2021-03-20 21:20:41 +00:00
# remove cached post so that the muted version gets recreated
# with its content text and/or image
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
try:
os.remove(cachedPostFilename)
except BaseException:
2021-10-29 14:48:24 +00:00
if debug:
2021-10-29 16:31:20 +00:00
print('EX: unmutePost cached post not deleted ' +
2021-10-29 14:48:24 +00:00
str(cachedPostFilename))
pass
2021-03-20 21:20:41 +00:00
# if the post is in the recent posts cache then mark it as unmuted
if recentPostsCache.get('index'):
postId = \
removeIdEnding(postJsonObject['id']).replace('/', '#')
if postId in recentPostsCache['index']:
print('UNMUTE: ' + postId + ' is in recent posts cache')
2021-09-28 13:24:57 +00:00
if recentPostsCache.get('json'):
recentPostsCache['json'][postId] = json.dumps(postJsonObject)
print('UNMUTE: ' + postId +
' marked as unmuted in recent posts cache')
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(postId):
del recentPostsCache['html'][postId]
print('UNMUTE: ' + postId + ' removed cached html')
2021-09-28 15:48:14 +00:00
if alsoUpdatePostId:
2021-09-28 16:32:54 +00:00
postFilename = locatePost(baseDir, nickname, domain, alsoUpdatePostId)
if os.path.isfile(postFilename):
postJsonObj = loadJson(postFilename)
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain,
postJsonObj)
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
try:
os.remove(cachedPostFilename)
print('MUTE: cached referenced post removed ' +
cachedPostFilename)
except BaseException:
2021-10-29 14:48:24 +00:00
if debug:
2021-10-29 16:31:20 +00:00
print('EX: ' +
2021-10-29 14:48:24 +00:00
'unmutePost cached ref post not removed ' +
str(cachedPostFilename))
2021-09-28 16:32:54 +00:00
pass
2021-09-28 15:48:14 +00:00
if recentPostsCache.get('json'):
if recentPostsCache['json'].get(alsoUpdatePostId):
del recentPostsCache['json'][alsoUpdatePostId]
print('UNMUTE: ' +
alsoUpdatePostId + ' removed referenced json')
if recentPostsCache.get('html'):
if recentPostsCache['html'].get(alsoUpdatePostId):
del recentPostsCache['html'][alsoUpdatePostId]
print('UNMUTE: ' +
alsoUpdatePostId + ' removed referenced html')
2021-03-20 21:20:41 +00:00
def outboxMute(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
messageJson: {}, debug: bool,
recentPostsCache: {}) -> None:
"""When a mute is received by the outbox from c2s
"""
if not messageJson.get('type'):
return
if not hasActor(messageJson, debug):
2021-03-20 21:20:41 +00:00
return
domainFull = getFullDomain(domain, port)
if not messageJson['actor'].endswith(domainFull + '/users/' + nickname):
return
if not messageJson['type'] == 'Ignore':
return
2021-10-13 11:15:06 +00:00
if not hasObjectString(messageJson, debug):
2021-03-20 21:20:41 +00:00
return
if debug:
print('DEBUG: c2s mute request arrived in outbox')
messageId = removeIdEnding(messageJson['object'])
if '/statuses/' not in messageId:
if debug:
print('DEBUG: c2s mute object is not a status')
return
if not hasUsersPath(messageId):
if debug:
print('DEBUG: c2s mute object has no nickname')
return
domain = removeDomainPort(domain)
2021-03-20 21:20:41 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
if not postFilename:
if debug:
print('DEBUG: c2s mute post not found in inbox or outbox')
print(messageId)
return
nicknameMuted = getNicknameFromActor(messageJson['object'])
if not nicknameMuted:
print('WARN: unable to find nickname in ' + messageJson['object'])
return
mutePost(baseDir, nickname, domain, port,
httpPrefix, messageJson['object'], recentPostsCache,
debug)
2021-03-20 21:20:41 +00:00
if debug:
print('DEBUG: post muted via c2s - ' + postFilename)
def outboxUndoMute(baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
messageJson: {}, debug: bool,
recentPostsCache: {}) -> None:
"""When an undo mute is received by the outbox from c2s
"""
if not messageJson.get('type'):
return
if not hasActor(messageJson, debug):
2021-03-20 21:20:41 +00:00
return
domainFull = getFullDomain(domain, port)
if not messageJson['actor'].endswith(domainFull + '/users/' + nickname):
return
if not messageJson['type'] == 'Undo':
return
2021-10-13 10:11:02 +00:00
if not hasObjectStringType(messageJson, debug):
2021-03-20 21:20:41 +00:00
return
if messageJson['object']['type'] != 'Ignore':
return
if not isinstance(messageJson['object']['object'], str):
if debug:
print('DEBUG: undo mute object is not a string')
return
if debug:
print('DEBUG: c2s undo mute request arrived in outbox')
messageId = removeIdEnding(messageJson['object']['object'])
if '/statuses/' not in messageId:
if debug:
print('DEBUG: c2s undo mute object is not a status')
return
if not hasUsersPath(messageId):
if debug:
print('DEBUG: c2s undo mute object has no nickname')
return
domain = removeDomainPort(domain)
2021-03-20 21:20:41 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
if not postFilename:
if debug:
print('DEBUG: c2s undo mute post not found in inbox or outbox')
print(messageId)
return
nicknameMuted = getNicknameFromActor(messageJson['object']['object'])
if not nicknameMuted:
print('WARN: unable to find nickname in ' +
messageJson['object']['object'])
return
unmutePost(baseDir, nickname, domain, port,
httpPrefix, messageJson['object']['object'],
recentPostsCache, debug)
2021-03-20 21:20:41 +00:00
if debug:
print('DEBUG: post undo mute via c2s - ' + postFilename)
def brochModeIsActive(baseDir: str) -> bool:
"""Returns true if broch mode is active
"""
allowFilename = baseDir + '/accounts/allowedinstances.txt'
return os.path.isfile(allowFilename)
2021-02-15 22:06:53 +00:00
def setBrochMode(baseDir: str, domainFull: str, enabled: bool) -> None:
"""Broch mode can be used to lock down the instance during
a period of time when it is temporarily under attack.
For example, where an adversary is constantly spinning up new
instances.
It surveys the following lists of all accounts and uses that
to construct an instance level allow list. Anything arriving
which is then not from one of the allowed domains will be dropped
"""
allowFilename = baseDir + '/accounts/allowedinstances.txt'
if not enabled:
# remove instance allow list
if os.path.isfile(allowFilename):
try:
os.remove(allowFilename)
except BaseException:
2021-10-29 16:31:20 +00:00
print('EX: setBrochMode allow file not deleted ' +
2021-10-29 14:48:24 +00:00
str(allowFilename))
pass
2021-02-15 22:26:25 +00:00
print('Broch mode turned off')
2021-02-15 22:06:53 +00:00
else:
2021-02-16 09:50:50 +00:00
if os.path.isfile(allowFilename):
lastModified = fileLastModified(allowFilename)
print('Broch mode already activated ' + lastModified)
return
2021-02-15 22:06:53 +00:00
# generate instance allow list
allowedDomains = [domainFull]
followFiles = ('following.txt', 'followers.txt')
2021-02-15 22:06:53 +00:00
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
if not isAccountDir(acct):
2021-02-15 22:06:53 +00:00
continue
accountDir = os.path.join(baseDir + '/accounts', acct)
for followFileType in followFiles:
followingFilename = accountDir + '/' + followFileType
if not os.path.isfile(followingFilename):
continue
2021-07-13 14:40:49 +00:00
with open(followingFilename, 'r') as f:
followList = f.readlines()
for handle in followList:
if '@' not in handle:
continue
handle = handle.replace('\n', '')
handleDomain = handle.split('@')[1]
if handleDomain not in allowedDomains:
allowedDomains.append(handleDomain)
2021-02-15 22:06:53 +00:00
break
# write the allow file
2021-06-22 12:27:10 +00:00
with open(allowFilename, 'w+') as allowFile:
2021-02-15 22:06:53 +00:00
allowFile.write(domainFull + '\n')
for d in allowedDomains:
allowFile.write(d + '\n')
2021-02-15 22:26:25 +00:00
print('Broch mode enabled')
2021-02-15 22:06:53 +00:00
setConfigParam(baseDir, "brochMode", enabled)
2021-02-15 22:26:25 +00:00
def brochModeLapses(baseDir: str, lapseDays: int) -> bool:
2021-02-15 22:26:25 +00:00
"""After broch mode is enabled it automatically
elapses after a period of time
"""
allowFilename = baseDir + '/accounts/allowedinstances.txt'
if not os.path.isfile(allowFilename):
2021-02-15 23:01:07 +00:00
return False
2021-02-15 22:26:25 +00:00
lastModified = fileLastModified(allowFilename)
modifiedDate = None
try:
modifiedDate = \
datetime.strptime(lastModified, "%Y-%m-%dT%H:%M:%SZ")
except BaseException:
2021-10-29 16:31:20 +00:00
print('EX: brochModeLapses date not parsed ' + str(lastModified))
2021-06-05 13:38:57 +00:00
return False
2021-02-15 22:26:25 +00:00
if not modifiedDate:
2021-06-05 13:38:57 +00:00
return False
2021-02-15 22:26:25 +00:00
currTime = datetime.datetime.utcnow()
daysSinceBroch = (currTime - modifiedDate).days
if daysSinceBroch >= lapseDays:
removed = False
2021-02-15 22:26:25 +00:00
try:
os.remove(allowFilename)
removed = True
except BaseException:
2021-10-29 16:31:20 +00:00
print('EX: brochModeLapses allow file not deleted ' +
2021-10-29 14:48:24 +00:00
str(allowFilename))
pass
if removed:
2021-06-05 13:42:18 +00:00
setConfigParam(baseDir, "brochMode", False)
2021-02-15 22:26:25 +00:00
print('Broch mode has elapsed')
2021-06-05 13:38:57 +00:00
return True
return False
2021-10-21 11:13:24 +00:00
def loadCWLists(baseDir: str, verbose: bool) -> {}:
2021-10-21 13:08:21 +00:00
"""Load lists used for content warnings
2021-10-21 11:13:24 +00:00
"""
2021-10-21 13:08:21 +00:00
if not os.path.isdir(baseDir + '/cwlists'):
2021-10-21 11:13:24 +00:00
return {}
result = {}
2021-10-21 13:08:21 +00:00
for subdir, dirs, files in os.walk(baseDir + '/cwlists'):
2021-10-21 11:13:24 +00:00
for f in files:
if not f.endswith('.json'):
continue
2021-10-21 13:08:21 +00:00
listFilename = os.path.join(baseDir + '/cwlists', f)
print('listFilename: ' + listFilename)
listJson = loadJson(listFilename, 0, 1)
2021-10-21 11:13:24 +00:00
if not listJson:
continue
2021-10-21 11:17:26 +00:00
if not listJson.get('name'):
continue
2021-10-21 13:08:21 +00:00
if not listJson.get('words') and not listJson.get('domains'):
2021-10-21 11:17:26 +00:00
continue
name = listJson['name']
if verbose:
print('List: ' + name)
result[name] = listJson
2021-10-21 11:13:24 +00:00
return result
2021-10-21 13:08:21 +00:00
def addCWfromLists(postJsonObject: {}, CWlists: {}, translate: {},
listsEnabled: str) -> None:
2021-10-21 13:08:21 +00:00
"""Adds content warnings by matching the post content
against domains or keywords
"""
if not listsEnabled:
return
2021-10-21 13:08:21 +00:00
if not postJsonObject['object'].get('content'):
return
cw = ''
if postJsonObject['object'].get('summary'):
cw = postJsonObject['object']['summary']
content = postJsonObject['object']['content']
for name, item in CWlists.items():
if name not in listsEnabled:
continue
2021-10-21 13:08:21 +00:00
if not item.get('warning'):
continue
warning = item['warning']
# is there a translated version of the warning?
if translate.get(warning):
warning = translate[warning]
# is the warning already in the CW?
if warning in cw:
continue
matched = False
# match domains within the content
if item.get('domains'):
for domain in item['domains']:
if domain in content:
if cw:
cw = warning + ' / ' + cw
else:
cw = warning
matched = True
break
if matched:
continue
# match words within the content
if item.get('words'):
for wordStr in item['words']:
if wordStr in content:
if cw:
cw = warning + ' / ' + cw
else:
cw = warning
break
if cw:
postJsonObject['object']['summary'] = cw
postJsonObject['object']['sensitive'] = True
2021-10-21 19:19:44 +00:00
def getCWlistVariable(listName: str) -> str:
"""Returns the variable associated with a CW list
"""
return 'list' + listName.replace(' ', '').replace("'", '')