diff --git a/daemon.py b/daemon.py
index 6b8c61a40..fc77b0bd6 100644
--- a/daemon.py
+++ b/daemon.py
@@ -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) + \
diff --git a/desktop_client.py b/desktop_client.py
index 054f8b835..1cf70d316 100644
--- a/desktop_client.py
+++ b/desktop_client.py
@@ -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)
diff --git a/epicyon.py b/epicyon.py
index 9dced32f0..52b0fcca0 100644
--- a/epicyon.py
+++ b/epicyon.py
@@ -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:
diff --git a/filters.py b/filters.py
index 2c2de0cb7..db8474442 100644
--- a/filters.py
+++ b/filters.py
@@ -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
diff --git a/follow.py b/follow.py
index a5840dbcf..a908fcf24 100644
--- a/follow.py
+++ b/follow.py
@@ -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
diff --git a/inbox.py b/inbox.py
index 92c4493be..215dfd3be 100644
--- a/inbox.py
+++ b/inbox.py
@@ -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)
diff --git a/migrate.py b/migrate.py
index 5298200e3..98daed5d4 100644
--- a/migrate.py
+++ b/migrate.py
@@ -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'):
diff --git a/person.py b/person.py
index 8823a10a1..181e2ea8d 100644
--- a/person.py
+++ b/person.py
@@ -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
diff --git a/pgp.py b/pgp.py
index 193b50951..2b4de826a 100644
--- a/pgp.py
+++ b/pgp.py
@@ -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)
diff --git a/translations/ar.json b/translations/ar.json
index ed7d8aeb7..cfd71044d 100644
--- a/translations/ar.json
+++ b/translations/ar.json
@@ -501,5 +501,6 @@
"New link title and URL": "عنوان الارتباط الجديد وعنوان URL",
"Theme Designer": "مصمم المظهر",
"Reset": "إعادة ضبط",
- "Encryption Keys": "مفاتيح التشفير"
+ "Encryption Keys": "مفاتيح التشفير",
+ "Filtered words within bio": "كلمات مفلترة داخل السيرة الذاتية"
}
diff --git a/translations/ca.json b/translations/ca.json
index 1ffae3edb..24d353dd3 100644
--- a/translations/ca.json
+++ b/translations/ca.json
@@ -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"
}
diff --git a/translations/cy.json b/translations/cy.json
index c40c5f380..a0408376e 100644
--- a/translations/cy.json
+++ b/translations/cy.json
@@ -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"
}
diff --git a/translations/de.json b/translations/de.json
index 5061a4b95..59f5c2b52 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -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"
}
diff --git a/translations/en.json b/translations/en.json
index 9f655e1f0..48fae7462 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -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"
}
diff --git a/translations/es.json b/translations/es.json
index dc9d6c9a5..be6e0cd62 100644
--- a/translations/es.json
+++ b/translations/es.json
@@ -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"
}
diff --git a/translations/fr.json b/translations/fr.json
index 43811a0fb..3dbf85e40 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -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"
}
diff --git a/translations/ga.json b/translations/ga.json
index 17bfe2c57..9c4908c2f 100644
--- a/translations/ga.json
+++ b/translations/ga.json
@@ -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"
}
diff --git a/translations/hi.json b/translations/hi.json
index e022e00a3..2177de773 100644
--- a/translations/hi.json
+++ b/translations/hi.json
@@ -501,5 +501,6 @@
"New link title and URL": "नया लिंक शीर्षक और URL",
"Theme Designer": "थीम डिजाइनर",
"Reset": "रीसेट",
- "Encryption Keys": "एन्क्रिप्शन कुंजी"
+ "Encryption Keys": "एन्क्रिप्शन कुंजी",
+ "Filtered words within bio": "जीवनी के भीतर फ़िल्टर किए गए शब्द"
}
diff --git a/translations/it.json b/translations/it.json
index 421cab2d8..74420ce18 100644
--- a/translations/it.json
+++ b/translations/it.json
@@ -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"
}
diff --git a/translations/ja.json b/translations/ja.json
index 69afffeca..605a29904 100644
--- a/translations/ja.json
+++ b/translations/ja.json
@@ -501,5 +501,6 @@
"New link title and URL": "新しいリンクのタイトルとURL",
"Theme Designer": "テーマデザイナー",
"Reset": "リセット",
- "Encryption Keys": "暗号化キー"
+ "Encryption Keys": "暗号化キー",
+ "Filtered words within bio": "伝記内のフィルタリングされた単語"
}
diff --git a/translations/ku.json b/translations/ku.json
index bba24d3cb..8e9101149 100644
--- a/translations/ku.json
+++ b/translations/ku.json
@@ -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"
}
diff --git a/translations/oc.json b/translations/oc.json
index 596f734cc..9431782ba 100644
--- a/translations/oc.json
+++ b/translations/oc.json
@@ -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"
}
diff --git a/translations/pt.json b/translations/pt.json
index ab313aa5e..c0d3e32d3 100644
--- a/translations/pt.json
+++ b/translations/pt.json
@@ -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"
}
diff --git a/translations/ru.json b/translations/ru.json
index 5067f89d9..70b8032b9 100644
--- a/translations/ru.json
+++ b/translations/ru.json
@@ -501,5 +501,6 @@
"New link title and URL": "Новое название ссылки и URL",
"Theme Designer": "Дизайнер тем",
"Reset": "Сброс настроек",
- "Encryption Keys": "Ключи шифрования"
+ "Encryption Keys": "Ключи шифрования",
+ "Filtered words within bio": "Отфильтрованные слова в биографии"
}
diff --git a/translations/sw.json b/translations/sw.json
index 966993c1f..3ecc75fa6 100644
--- a/translations/sw.json
+++ b/translations/sw.json
@@ -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"
}
diff --git a/translations/zh.json b/translations/zh.json
index cc0422b86..bf8cca74d 100644
--- a/translations/zh.json
+++ b/translations/zh.json
@@ -501,5 +501,6 @@
"New link title and URL": "新链接标题和 URL",
"Theme Designer": "主题设计师",
"Reset": "重启",
- "Encryption Keys": "加密密钥"
+ "Encryption Keys": "加密密钥",
+ "Filtered words within bio": "传记中的过滤词"
}
diff --git a/webapp_profile.py b/webapp_profile.py
index 9da17967f..a841b4c31 100644
--- a/webapp_profile.py
+++ b/webapp_profile.py
@@ -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 + '\n' + \
'
\n' + \
+ '
\n' + \
+ ' \n' + \
+ '
\n' + \
'
\n' + \
'