Merge branch 'main' of gitlab.com:bashrc2/epicyon

merge-requests/30/head
Bob Mottram 2021-12-14 15:15:07 +00:00
commit dd4ae57fbc
27 changed files with 491 additions and 289 deletions

View File

@ -6221,9 +6221,29 @@ class PubServer(BaseHTTPRequestHandler):
os.remove(filterFilename)
except OSError:
print('EX: _profileUpdate ' +
'unable to delete ' +
'unable to delete filter ' +
filterFilename)
# save filtered words within bio list
filterBioFilename = \
acctDir(baseDir, nickname, domain) + \
'/filters_bio.txt'
if fields.get('filteredWordsBio'):
try:
with open(filterBioFilename, 'w+') as filterfile:
filterfile.write(fields['filteredWordsBio'])
except OSError:
print('EX: unable to write bio filter ' +
filterBioFilename)
else:
if os.path.isfile(filterBioFilename):
try:
os.remove(filterBioFilename)
except OSError:
print('EX: _profileUpdate ' +
'unable to delete bio filter ' +
filterBioFilename)
# word replacements
switchFilename = \
acctDir(baseDir, nickname, domain) + \

View File

@ -324,15 +324,16 @@ def _speakerPicospeaker(pitch: int, rate: int, systemLanguage: str,
"""TTS using picospeaker
"""
speakerLang = 'en-GB'
if systemLanguage:
if systemLanguage.startswith('fr'):
speakerLang = 'fr-FR'
elif systemLanguage.startswith('es'):
speakerLang = 'es-ES'
elif systemLanguage.startswith('de'):
speakerLang = 'de-DE'
elif systemLanguage.startswith('it'):
speakerLang = 'it-IT'
supportedLanguages = {
"fr": "fr-FR",
"es": "es-ES",
"de": "de-DE",
"it": "it-IT"
}
for lang, speakerStr in supportedLanguages.items():
if systemLanguage.startswith(lang):
speakerLang = speakerStr
break
sayText = str(sayText).replace('"', "'")
speakerCmd = 'picospeaker ' + \
'-l ' + speakerLang + \
@ -386,8 +387,7 @@ def _textToSpeech(sayStr: str, screenreader: str,
if screenreader == 'espeak':
_speakerEspeak(espeak, pitch, rate, srange, sayStr)
elif screenreader == 'picospeaker':
_speakerPicospeaker(pitch, rate,
systemLanguage, sayStr)
_speakerPicospeaker(pitch, rate, systemLanguage, sayStr)
def _sayCommand(content: str, sayStr: str, screenreader: str,
@ -624,15 +624,16 @@ def _showLikesOnPost(postJsonObject: {}, maxLikes: int) -> None:
return
if not postJsonObject['object'].get('likes'):
return
if not isinstance(postJsonObject['object']['likes'], dict):
objectLikes = postJsonObject['object']['likes']
if not isinstance(objectLikes, dict):
return
if not postJsonObject['object']['likes'].get('items'):
if not objectLikes.get('items'):
return
if not isinstance(postJsonObject['object']['likes']['items'], list):
if not isinstance(objectLikes['items'], list):
return
print('')
ctr = 0
for item in postJsonObject['object']['likes']['items']:
for item in objectLikes['items']:
print('' + str(item['actor']))
ctr += 1
if ctr >= maxLikes:
@ -646,15 +647,16 @@ def _showRepliesOnPost(postJsonObject: {}, maxReplies: int) -> None:
return
if not postJsonObject['object'].get('replies'):
return
if not isinstance(postJsonObject['object']['replies'], dict):
objectReplies = postJsonObject['object']['replies']
if not isinstance(objectReplies, dict):
return
if not postJsonObject['object']['replies'].get('items'):
if not objectReplies.get('items'):
return
if not isinstance(postJsonObject['object']['replies']['items'], list):
if not isinstance(objectReplies['items'], list):
return
print('')
ctr = 0
for item in postJsonObject['object']['replies']['items']:
for item in objectReplies['items']:
print('' + str(item['url']))
ctr += 1
if ctr >= maxReplies:
@ -867,7 +869,7 @@ def _desktopShowProfile(session, nickname: str, domain: str,
isHttp = True
actorJson, asHeader = \
getActorJson(domain, actor, isHttp, False, False, True,
signingPrivateKeyPem)
signingPrivateKeyPem, session)
_desktopShowActor(baseDir, actorJson, translate,
systemLanguage, screenreader, espeak)
@ -888,7 +890,7 @@ def _desktopShowProfileFromHandle(session, nickname: str, domain: str,
"""
actorJson, asHeader = \
getActorJson(domain, handle, False, False, False, True,
signingPrivateKeyPem)
signingPrivateKeyPem, session)
_desktopShowActor(baseDir, actorJson, translate,
systemLanguage, screenreader, espeak)

View File

@ -2086,7 +2086,7 @@ if args.actor:
else:
print('Did not obtain instance actor key for ' + domain)
getActorJson(domain, args.actor, args.http, args.gnunet,
debug, False, signingPrivateKeyPem)
debug, False, signingPrivateKeyPem, None)
sys.exit()
if args.followers:

View File

@ -144,6 +144,20 @@ def isFilteredGlobally(baseDir: str, content: str) -> bool:
return False
def isFilteredBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool:
"""Should the given actor bio be filtered out?
"""
if isFilteredGlobally(baseDir, bio):
return True
if not nickname or not domain:
return False
accountFiltersFilename = \
acctDir(baseDir, nickname, domain) + '/filters_bio.txt'
return _isFilteredBase(accountFiltersFilename, bio)
def isFiltered(baseDir: str, nickname: str, domain: str, content: str) -> bool:
"""Should the given content be filtered out?
This is a simple type of filter which just matches words, not a regex

252
follow.py
View File

@ -11,11 +11,9 @@ from pprint import pprint
import os
from utils import hasObjectStringObject
from utils import hasObjectStringType
from utils import hasActor
from utils import removeDomainPort
from utils import hasUsersPath
from utils import getFullDomain
from utils import isSystemAccount
from utils import getFollowersList
from utils import validNickname
from utils import domainPermitted
@ -31,7 +29,6 @@ from utils import isAccountDir
from utils import getUserPaths
from utils import acctDir
from utils import hasGroupType
from utils import isGroupAccount
from utils import localActorUrl
from acceptreject import createAccept
from acceptreject import createReject
@ -39,7 +36,6 @@ from webfinger import webfingerHandle
from auth import createBasicAuthHeader
from session import getJson
from session import postJson
from cache import getPersonPubKey
def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None:
@ -418,8 +414,8 @@ def _getNoOfFollows(baseDir: str, nickname: str, domain: str,
return ctr
def _getNoOfFollowers(baseDir: str,
nickname: str, domain: str, authenticated: bool) -> int:
def getNoOfFollowers(baseDir: str,
nickname: str, domain: str, authenticated: bool) -> int:
"""Returns the number of followers of the given person
"""
return _getNoOfFollows(baseDir, nickname, domain,
@ -562,9 +558,9 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
return following
def _followApprovalRequired(baseDir: str, nicknameToFollow: str,
domainToFollow: str, debug: bool,
followRequestHandle: str) -> bool:
def followApprovalRequired(baseDir: str, nicknameToFollow: str,
domainToFollow: str, debug: bool,
followRequestHandle: str) -> bool:
""" Returns the policy for follower approvals
"""
# has this handle already been manually approved?
@ -591,10 +587,10 @@ def _followApprovalRequired(baseDir: str, nicknameToFollow: str,
return manuallyApproveFollows
def _noOfFollowRequests(baseDir: str,
nicknameToFollow: str, domainToFollow: str,
nickname: str, domain: str, fromPort: int,
followType: str) -> int:
def noOfFollowRequests(baseDir: str,
nicknameToFollow: str, domainToFollow: str,
nickname: str, domain: str, fromPort: int,
followType: str) -> int:
"""Returns the current number of follow requests
"""
accountsDir = baseDir + '/accounts/' + \
@ -608,7 +604,7 @@ def _noOfFollowRequests(baseDir: str,
with open(approveFollowsFilename, 'r') as f:
lines = f.readlines()
except OSError:
print('EX: _noOfFollowRequests ' + approveFollowsFilename)
print('EX: noOfFollowRequests ' + approveFollowsFilename)
if lines:
if followType == "onion":
for fileLine in lines:
@ -623,12 +619,12 @@ def _noOfFollowRequests(baseDir: str,
return ctr
def _storeFollowRequest(baseDir: str,
nicknameToFollow: str, domainToFollow: str, port: int,
nickname: str, domain: str, fromPort: int,
followJson: {},
debug: bool, personUrl: str,
groupAccount: bool) -> bool:
def storeFollowRequest(baseDir: str,
nicknameToFollow: str, domainToFollow: str, port: int,
nickname: str, domain: str, fromPort: int,
followJson: {},
debug: bool, personUrl: str,
groupAccount: bool) -> bool:
"""Stores the follow request for later use
"""
accountsDir = baseDir + '/accounts/' + \
@ -651,7 +647,7 @@ def _storeFollowRequest(baseDir: str,
with open(followersFilename, 'r') as fpFollowers:
followersStr = fpFollowers.read()
except OSError:
print('EX: _storeFollowRequest ' + followersFilename)
print('EX: storeFollowRequest ' + followersFilename)
if approveHandle in followersStr:
alreadyFollowing = True
@ -696,7 +692,7 @@ def _storeFollowRequest(baseDir: str,
with open(approveFollowsFilename, 'a+') as fp:
fp.write(approveHandleStored + '\n')
except OSError:
print('EX: _storeFollowRequest 2 ' + approveFollowsFilename)
print('EX: storeFollowRequest 2 ' + approveFollowsFilename)
else:
if debug:
print('DEBUG: ' + approveHandleStored +
@ -706,7 +702,7 @@ def _storeFollowRequest(baseDir: str,
with open(approveFollowsFilename, 'w+') as fp:
fp.write(approveHandleStored + '\n')
except OSError:
print('EX: _storeFollowRequest 3 ' + approveFollowsFilename)
print('EX: storeFollowRequest 3 ' + approveFollowsFilename)
# store the follow request in its own directory
# We don't rely upon the inbox because items in there could expire
@ -717,212 +713,6 @@ def _storeFollowRequest(baseDir: str,
return saveJson(followJson, followActivityfilename)
def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [],
debug: bool, projectVersion: str,
maxFollowers: int, onionDomain: str,
signingPrivateKeyPem: str) -> bool:
"""Receives a follow request within the POST section of HTTPServer
"""
if not messageJson['type'].startswith('Follow'):
if not messageJson['type'].startswith('Join'):
return False
print('Receiving follow request')
if not hasActor(messageJson, debug):
return False
if not hasUsersPath(messageJson['actor']):
if debug:
print('DEBUG: users/profile/accounts/channel missing from actor')
return False
domain, tempPort = getDomainFromActor(messageJson['actor'])
fromPort = port
domainFull = getFullDomain(domain, tempPort)
if tempPort:
fromPort = tempPort
if not domainPermitted(domain, federationList):
if debug:
print('DEBUG: follower from domain not permitted - ' + domain)
return False
nickname = getNicknameFromActor(messageJson['actor'])
if not nickname:
# single user instance
nickname = 'dev'
if debug:
print('DEBUG: follow request does not contain a ' +
'nickname. Assuming single user instance.')
if not messageJson.get('to'):
messageJson['to'] = messageJson['object']
if not hasUsersPath(messageJson['object']):
if debug:
print('DEBUG: users/profile/channel/accounts ' +
'not found within object')
return False
domainToFollow, tempPort = getDomainFromActor(messageJson['object'])
if not domainPermitted(domainToFollow, federationList):
if debug:
print('DEBUG: follow domain not permitted ' + domainToFollow)
return True
domainToFollowFull = getFullDomain(domainToFollow, tempPort)
nicknameToFollow = getNicknameFromActor(messageJson['object'])
if not nicknameToFollow:
if debug:
print('DEBUG: follow request does not contain a ' +
'nickname for the account followed')
return True
if isSystemAccount(nicknameToFollow):
if debug:
print('DEBUG: Cannot follow system account - ' +
nicknameToFollow)
return True
if maxFollowers > 0:
if _getNoOfFollowers(baseDir,
nicknameToFollow, domainToFollow,
True) > maxFollowers:
print('WARN: ' + nicknameToFollow +
' has reached their maximum number of followers')
return True
handleToFollow = nicknameToFollow + '@' + domainToFollow
if domainToFollow == domain:
if not os.path.isdir(baseDir + '/accounts/' + handleToFollow):
if debug:
print('DEBUG: followed account not found - ' +
baseDir + '/accounts/' + handleToFollow)
return True
if isFollowerOfPerson(baseDir,
nicknameToFollow, domainToFollowFull,
nickname, domainFull):
if debug:
print('DEBUG: ' + nickname + '@' + domain +
' is already a follower of ' +
nicknameToFollow + '@' + domainToFollow)
return True
# what is the followers policy?
approveHandle = nickname + '@' + domainFull
if _followApprovalRequired(baseDir, nicknameToFollow,
domainToFollow, debug, approveHandle):
print('Follow approval is required')
if domain.endswith('.onion'):
if _noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'onion') > 5:
print('Too many follow requests from onion addresses')
return False
elif domain.endswith('.i2p'):
if _noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'i2p') > 5:
print('Too many follow requests from i2p addresses')
return False
else:
if _noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'') > 10:
print('Too many follow requests')
return False
# Get the actor for the follower and add it to the cache.
# Getting their public key has the same result
if debug:
print('Obtaining the following actor: ' + messageJson['actor'])
if not getPersonPubKey(baseDir, session, messageJson['actor'],
personCache, debug, projectVersion,
httpPrefix, domainToFollow, onionDomain,
signingPrivateKeyPem):
if debug:
print('Unable to obtain following actor: ' +
messageJson['actor'])
groupAccount = \
hasGroupType(baseDir, messageJson['actor'], personCache)
if groupAccount and isGroupAccount(baseDir, nickname, domain):
print('Group cannot follow a group')
return False
print('Storing follow request for approval')
return _storeFollowRequest(baseDir,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson, debug, messageJson['actor'],
groupAccount)
else:
print('Follow request does not require approval ' + approveHandle)
# update the followers
accountToBeFollowed = \
acctDir(baseDir, nicknameToFollow, domainToFollow)
if os.path.isdir(accountToBeFollowed):
followersFilename = accountToBeFollowed + '/followers.txt'
# for actors which don't follow the mastodon
# /users/ path convention store the full actor
if '/users/' not in messageJson['actor']:
approveHandle = messageJson['actor']
# Get the actor for the follower and add it to the cache.
# Getting their public key has the same result
if debug:
print('Obtaining the following actor: ' + messageJson['actor'])
if not getPersonPubKey(baseDir, session, messageJson['actor'],
personCache, debug, projectVersion,
httpPrefix, domainToFollow, onionDomain,
signingPrivateKeyPem):
if debug:
print('Unable to obtain following actor: ' +
messageJson['actor'])
print('Updating followers file: ' +
followersFilename + ' adding ' + approveHandle)
if os.path.isfile(followersFilename):
if approveHandle not in open(followersFilename).read():
groupAccount = \
hasGroupType(baseDir,
messageJson['actor'], personCache)
if debug:
print(approveHandle + ' / ' + messageJson['actor'] +
' is Group: ' + str(groupAccount))
if groupAccount and \
isGroupAccount(baseDir, nickname, domain):
print('Group cannot follow a group')
return False
try:
with open(followersFilename, 'r+') as followersFile:
content = followersFile.read()
if approveHandle + '\n' not in content:
followersFile.seek(0, 0)
if not groupAccount:
followersFile.write(approveHandle +
'\n' + content)
else:
followersFile.write('!' + approveHandle +
'\n' + content)
except Exception as e:
print('WARN: ' +
'Failed to write entry to followers file ' +
str(e))
else:
try:
with open(followersFilename, 'w+') as followersFile:
followersFile.write(approveHandle + '\n')
except OSError:
print('EX: unable to write ' + followersFilename)
print('Beginning follow accept')
return followedAccountAccepts(session, baseDir, httpPrefix,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson['actor'], federationList,
messageJson, sendThreads, postLog,
cachedWebfingers, personCache,
debug, projectVersion, True,
signingPrivateKeyPem)
def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
nicknameToFollow: str, domainToFollow: str,
port: int,
@ -1124,8 +914,8 @@ def sendFollowRequest(session, baseDir: str,
newFollowJson['to'] = followedId
print('Follow request: ' + str(newFollowJson))
if _followApprovalRequired(baseDir, nickname, domain, debug,
followHandle):
if followApprovalRequired(baseDir, nickname, domain, debug,
followHandle):
# Remove any follow requests rejected for the account being followed.
# It's assumed that if you are following someone then you are
# ok with them following back. If this isn't the case then a rejected

255
inbox.py
View File

@ -17,6 +17,9 @@ from languages import understoodPostLanguage
from like import updateLikesCollection
from reaction import updateReactionCollection
from reaction import validEmojiContent
from utils import domainPermitted
from utils import isGroupAccount
from utils import isSystemAccount
from utils import invalidCiphertext
from utils import removeHtml
from utils import fileLastModified
@ -65,9 +68,14 @@ from httpsig import verifyPostHeaders
from session import createSession
from follow import followerApprovalActive
from follow import isFollowingActor
from follow import receiveFollowRequest
from follow import getFollowersOfActor
from follow import unfollowerOfAccount
from follow import isFollowerOfPerson
from follow import followedAccountAccepts
from follow import storeFollowRequest
from follow import noOfFollowRequests
from follow import getNoOfFollowers
from follow import followApprovalRequired
from pprint import pprint
from cache import storePersonInCache
from cache import getPersonPubKey
@ -113,6 +121,7 @@ from notifyOnPost import notifyWhenPersonPosts
from conversation import updateConversation
from content import validHashTag
from webapp_hashtagswarm import htmlHashTagSwarm
from person import validSendingActor
def _storeLastPostId(baseDir: str, nickname: str, domain: str,
@ -2184,7 +2193,9 @@ def _validPostContent(baseDir: str, nickname: str, domain: str,
"""Is the content of a received post valid?
Check for bad html
Check for hellthreads
Check number of tags is reasonable
Check that the language is understood
Check if it's a git patch
Check number of tags and mentions is reasonable
"""
if not hasObjectDict(messageJson):
return True
@ -3406,6 +3417,11 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
allowLocalNetworkAccess, debug,
systemLanguage, httpPrefix,
domainFull, personCache):
# is the sending actor valid?
if not validSendingActor(session, baseDir, nickname, domain,
personCache, postJsonObject,
signingPrivateKeyPem, debug, unitTest):
return False
if postJsonObject.get('object'):
jsonObj = postJsonObject['object']
@ -3826,6 +3842,221 @@ def _checkJsonSignature(baseDir: str, queueJson: {}) -> (bool, bool):
return hasJsonSignature, jwebsigType
def _receiveFollowRequest(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [],
debug: bool, projectVersion: str,
maxFollowers: int, onionDomain: str,
signingPrivateKeyPem: str, unitTest: bool) -> bool:
"""Receives a follow request within the POST section of HTTPServer
"""
if not messageJson['type'].startswith('Follow'):
if not messageJson['type'].startswith('Join'):
return False
print('Receiving follow request')
if not hasActor(messageJson, debug):
return False
if not hasUsersPath(messageJson['actor']):
if debug:
print('DEBUG: users/profile/accounts/channel missing from actor')
return False
domain, tempPort = getDomainFromActor(messageJson['actor'])
fromPort = port
domainFull = getFullDomain(domain, tempPort)
if tempPort:
fromPort = tempPort
if not domainPermitted(domain, federationList):
if debug:
print('DEBUG: follower from domain not permitted - ' + domain)
return False
nickname = getNicknameFromActor(messageJson['actor'])
if not nickname:
# single user instance
nickname = 'dev'
if debug:
print('DEBUG: follow request does not contain a ' +
'nickname. Assuming single user instance.')
if not messageJson.get('to'):
messageJson['to'] = messageJson['object']
if not hasUsersPath(messageJson['object']):
if debug:
print('DEBUG: users/profile/channel/accounts ' +
'not found within object')
return False
domainToFollow, tempPort = getDomainFromActor(messageJson['object'])
if not domainPermitted(domainToFollow, federationList):
if debug:
print('DEBUG: follow domain not permitted ' + domainToFollow)
return True
domainToFollowFull = getFullDomain(domainToFollow, tempPort)
nicknameToFollow = getNicknameFromActor(messageJson['object'])
if not nicknameToFollow:
if debug:
print('DEBUG: follow request does not contain a ' +
'nickname for the account followed')
return True
if isSystemAccount(nicknameToFollow):
if debug:
print('DEBUG: Cannot follow system account - ' +
nicknameToFollow)
return True
if maxFollowers > 0:
if getNoOfFollowers(baseDir,
nicknameToFollow, domainToFollow,
True) > maxFollowers:
print('WARN: ' + nicknameToFollow +
' has reached their maximum number of followers')
return True
handleToFollow = nicknameToFollow + '@' + domainToFollow
if domainToFollow == domain:
if not os.path.isdir(baseDir + '/accounts/' + handleToFollow):
if debug:
print('DEBUG: followed account not found - ' +
baseDir + '/accounts/' + handleToFollow)
return True
if isFollowerOfPerson(baseDir,
nicknameToFollow, domainToFollowFull,
nickname, domainFull):
if debug:
print('DEBUG: ' + nickname + '@' + domain +
' is already a follower of ' +
nicknameToFollow + '@' + domainToFollow)
return True
approveHandle = nickname + '@' + domainFull
# is the actor sending the request valid?
if not validSendingActor(session, baseDir,
nicknameToFollow, domainToFollow,
personCache, messageJson,
signingPrivateKeyPem, debug, unitTest):
print('REJECT spam follow request ' + approveHandle)
return False
# what is the followers policy?
if followApprovalRequired(baseDir, nicknameToFollow,
domainToFollow, debug, approveHandle):
print('Follow approval is required')
if domain.endswith('.onion'):
if noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'onion') > 5:
print('Too many follow requests from onion addresses')
return False
elif domain.endswith('.i2p'):
if noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'i2p') > 5:
print('Too many follow requests from i2p addresses')
return False
else:
if noOfFollowRequests(baseDir,
nicknameToFollow, domainToFollow,
nickname, domain, fromPort,
'') > 10:
print('Too many follow requests')
return False
# Get the actor for the follower and add it to the cache.
# Getting their public key has the same result
if debug:
print('Obtaining the following actor: ' + messageJson['actor'])
if not getPersonPubKey(baseDir, session, messageJson['actor'],
personCache, debug, projectVersion,
httpPrefix, domainToFollow, onionDomain,
signingPrivateKeyPem):
if debug:
print('Unable to obtain following actor: ' +
messageJson['actor'])
groupAccount = \
hasGroupType(baseDir, messageJson['actor'], personCache)
if groupAccount and isGroupAccount(baseDir, nickname, domain):
print('Group cannot follow a group')
return False
print('Storing follow request for approval')
return storeFollowRequest(baseDir,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson, debug, messageJson['actor'],
groupAccount)
else:
print('Follow request does not require approval ' + approveHandle)
# update the followers
accountToBeFollowed = \
acctDir(baseDir, nicknameToFollow, domainToFollow)
if os.path.isdir(accountToBeFollowed):
followersFilename = accountToBeFollowed + '/followers.txt'
# for actors which don't follow the mastodon
# /users/ path convention store the full actor
if '/users/' not in messageJson['actor']:
approveHandle = messageJson['actor']
# Get the actor for the follower and add it to the cache.
# Getting their public key has the same result
if debug:
print('Obtaining the following actor: ' + messageJson['actor'])
if not getPersonPubKey(baseDir, session, messageJson['actor'],
personCache, debug, projectVersion,
httpPrefix, domainToFollow, onionDomain,
signingPrivateKeyPem):
if debug:
print('Unable to obtain following actor: ' +
messageJson['actor'])
print('Updating followers file: ' +
followersFilename + ' adding ' + approveHandle)
if os.path.isfile(followersFilename):
if approveHandle not in open(followersFilename).read():
groupAccount = \
hasGroupType(baseDir,
messageJson['actor'], personCache)
if debug:
print(approveHandle + ' / ' + messageJson['actor'] +
' is Group: ' + str(groupAccount))
if groupAccount and \
isGroupAccount(baseDir, nickname, domain):
print('Group cannot follow a group')
return False
try:
with open(followersFilename, 'r+') as followersFile:
content = followersFile.read()
if approveHandle + '\n' not in content:
followersFile.seek(0, 0)
if not groupAccount:
followersFile.write(approveHandle +
'\n' + content)
else:
followersFile.write('!' + approveHandle +
'\n' + content)
except Exception as e:
print('WARN: ' +
'Failed to write entry to followers file ' +
str(e))
else:
try:
with open(followersFilename, 'w+') as followersFile:
followersFile.write(approveHandle + '\n')
except OSError:
print('EX: unable to write ' + followersFilename)
print('Beginning follow accept')
return followedAccountAccepts(session, baseDir, httpPrefix,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson['actor'], federationList,
messageJson, sendThreads, postLog,
cachedWebfingers, personCache,
debug, projectVersion, True,
signingPrivateKeyPem)
def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
projectVersion: str,
baseDir: str, httpPrefix: str, sendThreads: [], postLog: [],
@ -4126,16 +4357,16 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
if debug:
print('DEBUG: checking for follow requests')
if receiveFollowRequest(session,
baseDir, httpPrefix, port,
sendThreads, postLog,
cachedWebfingers,
personCache,
queueJson['post'],
federationList,
debug, projectVersion,
maxFollowers, onionDomain,
signingPrivateKeyPem):
if _receiveFollowRequest(session,
baseDir, httpPrefix, port,
sendThreads, postLog,
cachedWebfingers,
personCache,
queueJson['post'],
federationList,
debug, projectVersion,
maxFollowers, onionDomain,
signingPrivateKeyPem, unitTest):
if os.path.isfile(queueFilename):
try:
os.remove(queueFilename)

View File

@ -86,7 +86,7 @@ def _updateMovedHandle(baseDir: str, nickname: str, domain: str,
gnunet = True
personJson = \
getActorJson(domain, personUrl, httpPrefix, gnunet, debug, False,
signingPrivateKeyPem)
signingPrivateKeyPem, None)
if not personJson:
return ctr
if not personJson.get('movedTo'):

118
person.py
View File

@ -38,6 +38,8 @@ from roles import setRole
from roles import setRolesFromList
from roles import getActorRolesList
from media import processMetaData
from utils import removeHtml
from utils import containsInvalidChars
from utils import replaceUsersWithAt
from utils import removeLineEndings
from utils import removeDomainPort
@ -63,6 +65,8 @@ from session import getJson
from webfinger import webfingerHandle
from pprint import pprint
from cache import getPersonFromCache
from cache import storePersonInCache
from filters import isFilteredBio
def generateRSAKey() -> (str, str):
@ -1415,7 +1419,8 @@ def _detectUsersPath(url: str) -> str:
def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool,
debug: bool, quiet: bool,
signingPrivateKeyPem: str) -> ({}, {}):
signingPrivateKeyPem: str,
existingSession) -> ({}, {}):
"""Returns the actor json
"""
if debug:
@ -1498,7 +1503,10 @@ def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool,
httpPrefix = 'https'
else:
httpPrefix = 'http'
session = createSession(proxyType)
if existingSession:
session = existingSession
else:
session = createSession(proxyType)
if nickname == 'inbox':
nickname = domain
@ -1634,3 +1642,109 @@ def addActorUpdateTimestamp(actorJson: {}) -> None:
# add updated timestamp to avatar and banner
actorJson['icon']['updated'] = currDateStr
actorJson['image']['updated'] = currDateStr
def validSendingActor(session, baseDir: str,
nickname: str, domain: str,
personCache: {},
postJsonObject: {},
signingPrivateKeyPem: str,
debug: bool, unitTest: bool) -> bool:
"""When a post arrives in the inbox this is used to check that
the sending actor is valid
"""
# who sent this post?
sendingActor = postJsonObject['actor']
# sending to yourself (reminder)
if sendingActor.endswith(domain + '/users/' + nickname):
return True
# get their actor
actorJson = getPersonFromCache(baseDir, sendingActor, personCache, True)
downloadedActor = False
if not actorJson:
# download the actor
actorJson, _ = getActorJson(domain, sendingActor,
True, False, debug, True,
signingPrivateKeyPem, session)
if actorJson:
downloadedActor = True
if not actorJson:
# if the actor couldn't be obtained then proceed anyway
return True
if not actorJson.get('preferredUsername'):
print('REJECT: no preferredUsername within actor ' + str(actorJson))
return False
# does the actor have a bio ?
if not unitTest:
if not actorJson.get('summary'):
# allow no bio if it's an actor in this instance
if domain not in sendingActor:
# probably a spam actor with no bio
print('REJECT: spam actor ' + sendingActor)
return False
bioStr = removeHtml(actorJson['summary'])
bioStr += ' ' + removeHtml(actorJson['preferredUsername'])
if actorJson.get('attachment'):
if isinstance(actorJson['attachment'], list):
for tag in actorJson['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name'):
continue
if isinstance(tag['name'], str):
bioStr += ' ' + tag['name']
if tag.get('value'):
continue
if isinstance(tag['value'], str):
bioStr += ' ' + tag['value']
if actorJson.get('name'):
bioStr += ' ' + removeHtml(actorJson['name'])
if containsInvalidChars(bioStr):
print('REJECT: post actor bio contains invalid characters')
return False
if isFilteredBio(baseDir, nickname, domain, bioStr):
print('REJECT: post actor bio contains filtered text')
return False
else:
print('Skipping check for missing bio in ' + sendingActor)
# Check any attached fields for the actor.
# Spam actors will sometimes have attached fields which are all empty
if actorJson.get('attachment'):
if isinstance(actorJson['attachment'], list):
noOfTags = 0
tagsWithoutValue = 0
for tag in actorJson['attachment']:
if not isinstance(tag, dict):
continue
if not tag.get('name'):
continue
noOfTags += 1
if not tag.get('value'):
tagsWithoutValue += 1
continue
if not isinstance(tag['value'], str):
tagsWithoutValue += 1
continue
if not tag['value'].strip():
tagsWithoutValue += 1
continue
if len(tag['value']) < 2:
tagsWithoutValue += 1
continue
if noOfTags > 0:
if int(tagsWithoutValue * 100 / noOfTags) > 50:
print('REJECT: actor has empty attachments ' +
sendingActor)
return False
if downloadedActor:
# if the actor is valid and was downloaded then
# store it in the cache, but don't write it to file
storePersonInCache(baseDir, sendingActor, actorJson, personCache,
False)
return True

4
pgp.py
View File

@ -343,7 +343,7 @@ def _getPGPPublicKeyFromActor(signingPrivateKeyPem: str,
if not actorJson:
actorJson, asHeader = \
getActorJson(domain, handle, False, False, False, True,
signingPrivateKeyPem)
signingPrivateKeyPem, None)
if not actorJson:
return None
if not actorJson.get('attachment'):
@ -491,7 +491,7 @@ def pgpPublicKeyUpload(baseDir: str, session,
actorJson, asHeader = \
getActorJson(domainFull, handle, False, False, debug, True,
signingPrivateKeyPem)
signingPrivateKeyPem, session)
if not actorJson:
if debug:
print('No actor returned for ' + handle)

View File

@ -501,5 +501,6 @@
"New link title and URL": "عنوان الارتباط الجديد وعنوان URL",
"Theme Designer": "مصمم المظهر",
"Reset": "إعادة ضبط",
"Encryption Keys": "مفاتيح التشفير"
"Encryption Keys": "مفاتيح التشفير",
"Filtered words within bio": "كلمات مفلترة داخل السيرة الذاتية"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Títol i URL de l'enllaç nous",
"Theme Designer": "Dissenyador temàtic",
"Reset": "Restableix",
"Encryption Keys": "Claus de xifratge"
"Encryption Keys": "Claus de xifratge",
"Filtered words within bio": "Paraules filtrades dins de la biografia"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Teitl dolen ac URL newydd",
"Theme Designer": "Dylunydd Thema",
"Reset": "Ail gychwyn",
"Encryption Keys": "Allweddi Amgryptio"
"Encryption Keys": "Allweddi Amgryptio",
"Filtered words within bio": "Geiriau wedi'u hidlo o fewn cofiant"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Neuer Linktitel und URL",
"Theme Designer": "Themendesigner",
"Reset": "Zurücksetzen",
"Encryption Keys": "Verschlüsselungsschlüssel"
"Encryption Keys": "Verschlüsselungsschlüssel",
"Filtered words within bio": "Gefilterte Wörter in der Biografie"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "New link title and URL",
"Theme Designer": "Theme Designer",
"Reset": "Reset",
"Encryption Keys": "Encryption Keys"
"Encryption Keys": "Encryption Keys",
"Filtered words within bio": "Filtered words within bio"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Nuevo título de enlace y URL",
"Theme Designer": "Diseñadora de temas",
"Reset": "Reiniciar",
"Encryption Keys": "Claves de cifrado"
"Encryption Keys": "Claves de cifrado",
"Filtered words within bio": "Palabras filtradas dentro de la biografía"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Nouveau titre et URL du lien",
"Theme Designer": "Concepteur de thème",
"Reset": "Réinitialiser",
"Encryption Keys": "Clés de cryptage"
"Encryption Keys": "Clés de cryptage",
"Filtered words within bio": "Mots filtrés dans la biographie"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Teideal nasc nua agus URL",
"Theme Designer": "Dearthóir Téama",
"Reset": "Athshocraigh",
"Encryption Keys": "Eochracha Criptithe"
"Encryption Keys": "Eochracha Criptithe",
"Filtered words within bio": "Focail scagtha laistigh den bheathaisnéis"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "नया लिंक शीर्षक और URL",
"Theme Designer": "थीम डिजाइनर",
"Reset": "रीसेट",
"Encryption Keys": "एन्क्रिप्शन कुंजी"
"Encryption Keys": "एन्क्रिप्शन कुंजी",
"Filtered words within bio": "जीवनी के भीतर फ़िल्टर किए गए शब्द"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Nuovo titolo e URL del collegamento",
"Theme Designer": "Progettista di temi",
"Reset": "Ripristina",
"Encryption Keys": "Chiavi di crittografia"
"Encryption Keys": "Chiavi di crittografia",
"Filtered words within bio": "Parole filtrate all'interno della biografia"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "新しいリンクのタイトルとURL",
"Theme Designer": "テーマデザイナー",
"Reset": "リセット",
"Encryption Keys": "暗号化キー"
"Encryption Keys": "暗号化キー",
"Filtered words within bio": "伝記内のフィルタリングされた単語"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Sernav û URL-ya girêdana nû",
"Theme Designer": "Theme Designer",
"Reset": "Reset",
"Encryption Keys": "Bişkojkên Şîfrekirinê"
"Encryption Keys": "Bişkojkên Şîfrekirinê",
"Filtered words within bio": "Peyvên fîlterkirî di hundurê biyografiyê de"
}

View File

@ -497,5 +497,6 @@
"New link title and URL": "New link title and URL",
"Theme Designer": "Theme Designer",
"Reset": "Reset",
"Encryption Keys": "Encryption Keys"
"Encryption Keys": "Encryption Keys",
"Filtered words within bio": "Filtered words within bio"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Novo título e URL do link",
"Theme Designer": "Designer de Tema",
"Reset": "Redefinir",
"Encryption Keys": "Chaves de criptografia"
"Encryption Keys": "Chaves de criptografia",
"Filtered words within bio": "Palavras filtradas na biografia"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Новое название ссылки и URL",
"Theme Designer": "Дизайнер тем",
"Reset": "Сброс настроек",
"Encryption Keys": "Ключи шифрования"
"Encryption Keys": "Ключи шифрования",
"Filtered words within bio": "Отфильтрованные слова в биографии"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "Kichwa kipya cha kiungo na URL",
"Theme Designer": "Mbuni wa Mandhari",
"Reset": "Weka upya",
"Encryption Keys": "Vifunguo vya Usimbaji"
"Encryption Keys": "Vifunguo vya Usimbaji",
"Filtered words within bio": "Maneno yaliyochujwa ndani ya wasifu"
}

View File

@ -501,5 +501,6 @@
"New link title and URL": "新链接标题和 URL",
"Theme Designer": "主题设计师",
"Reset": "重启",
"Encryption Keys": "加密密钥"
"Encryption Keys": "加密密钥",
"Filtered words within bio": "传记中的过滤词"
}

View File

@ -151,7 +151,7 @@ def htmlProfileAfterSearch(cssCache: {},
gnunet = True
profileJson, asHeader = \
getActorJson(domain, profileHandle, http, gnunet, debug, False,
signingPrivateKeyPem)
signingPrivateKeyPem, session)
if not profileJson:
return None
@ -1604,6 +1604,13 @@ def _htmlEditProfileFiltering(baseDir: str, nickname: str, domain: str,
with open(filterFilename, 'r') as filterfile:
filterStr = filterfile.read()
filterBioStr = ''
filterBioFilename = \
acctDir(baseDir, nickname, domain) + '/filters_bio.txt'
if os.path.isfile(filterBioFilename):
with open(filterBioFilename, 'r') as filterfile:
filterBioStr = filterfile.read()
switchStr = ''
switchFilename = \
acctDir(baseDir, nickname, domain) + '/replacewords.txt'
@ -1700,6 +1707,13 @@ def _htmlEditProfileFiltering(baseDir: str, nickname: str, domain: str,
'name="filteredWords" style="height:200px" spellcheck="false">' + \
filterStr + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Filtered words within bio'] + '</label></b>\n' + \
' <br><label class="labels">' + \
translate['One per line'] + '</label>\n' + \
' <textarea id="message" ' + \
'name="filteredWordsBio" style="height:200px" spellcheck="false">' + \
filterBioStr + '</textarea>\n' + \
' <br><b><label class="labels">' + \
translate['Word Replacements'] + '</label></b>\n' + \
' <br><label class="labels">A -> B</label>\n' + \
' <textarea id="message" name="switchWords" ' + \