From 0732419c33519c3214f1b0eeb94c2b754fc1cb1c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 13 Dec 2021 15:49:28 +0000 Subject: [PATCH 01/10] Tidying --- desktop_client.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 054f8b835..915cdfe31 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 + \ From bbee34929199939cbd1a01b2aeff840bf2f89e0e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 13 Dec 2021 15:51:07 +0000 Subject: [PATCH 02/10] Single line --- desktop_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 915cdfe31..7fd9888bc 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -387,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, From 5d1f7212e74bc15d48c000ca43f348418389cf07 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 13 Dec 2021 16:04:12 +0000 Subject: [PATCH 03/10] Tidying --- desktop_client.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 7fd9888bc..8584b6844 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -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: From f8b62c538850d5d45575401d522edc82956f3bb0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 10:11:10 +0000 Subject: [PATCH 04/10] Comments --- inbox.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index 92c4493be..c9ee3daeb 100644 --- a/inbox.py +++ b/inbox.py @@ -2184,7 +2184,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 From 0e0160a69827cec9eaf75c18c77f4ef3c04620a4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 13:27:00 +0000 Subject: [PATCH 05/10] Validate sending actor of incoming post --- desktop_client.py | 4 +- epicyon.py | 2 +- filters.py | 14 ++++++ inbox.py | 6 +++ migrate.py | 2 +- person.py | 111 +++++++++++++++++++++++++++++++++++++++++++++- pgp.py | 4 +- webapp_profile.py | 2 +- 8 files changed, 136 insertions(+), 9 deletions(-) diff --git a/desktop_client.py b/desktop_client.py index 8584b6844..1cf70d316 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -869,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) @@ -890,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/inbox.py b/inbox.py index c9ee3daeb..f19adbf78 100644 --- a/inbox.py +++ b/inbox.py @@ -113,6 +113,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, @@ -3408,6 +3409,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'] 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..4f75cf027 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,102 @@ 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('name'): + print('REJECT: no name within actor ' + str(actorJson)) + return False + 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']) + 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 containsInvalidChars(tag['name']): + tagsWithoutValue += 1 + continue + if containsInvalidChars(tag['value']): + 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/webapp_profile.py b/webapp_profile.py index 9da17967f..7303c72d6 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 From 23af727becaf00f2de99ea4aea333702f9623f4a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 13:59:42 +0000 Subject: [PATCH 06/10] Validate sending actor when follow request is received --- follow.py | 252 +++++------------------------------------------------- inbox.py | 245 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 255 insertions(+), 242 deletions(-) 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 f19adbf78..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 @@ -3834,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: [], @@ -4134,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) From 243a0a50fd10465574baae0ca722912dd575d5c8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 14:02:32 +0000 Subject: [PATCH 07/10] Optional name check --- person.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/person.py b/person.py index 4f75cf027..b31f916b3 100644 --- a/person.py +++ b/person.py @@ -1673,9 +1673,6 @@ def validSendingActor(session, baseDir: str, if not actorJson: # if the actor couldn't be obtained then proceed anyway return True - if not actorJson.get('name'): - print('REJECT: no name within actor ' + str(actorJson)) - return False if not actorJson.get('preferredUsername'): print('REJECT: no preferredUsername within actor ' + str(actorJson)) return False @@ -1689,7 +1686,8 @@ def validSendingActor(session, baseDir: str, return False bioStr = removeHtml(actorJson['summary']) bioStr += ' ' + removeHtml(actorJson['preferredUsername']) - bioStr += ' ' + removeHtml(actorJson['name']) + if actorJson.get('name'): + bioStr += ' ' + removeHtml(actorJson['name']) if containsInvalidChars(bioStr): print('REJECT: post actor bio contains invalid characters') return False From d21659d0ef95e8ff5c37cb6c0e7c2f15fe7ff0cc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 14:25:09 +0000 Subject: [PATCH 08/10] Filtering of bio words with profile settings --- daemon.py | 22 +++++++++++++++++++++- translations/ar.json | 3 ++- translations/ca.json | 3 ++- translations/cy.json | 3 ++- translations/de.json | 3 ++- translations/en.json | 3 ++- translations/es.json | 3 ++- translations/fr.json | 3 ++- translations/ga.json | 3 ++- translations/hi.json | 3 ++- translations/it.json | 3 ++- translations/ja.json | 3 ++- translations/ku.json | 3 ++- translations/oc.json | 3 ++- translations/pt.json | 3 ++- translations/ru.json | 3 ++- translations/sw.json | 3 ++- translations/zh.json | 3 ++- webapp_profile.py | 7 +++++++ 19 files changed, 62 insertions(+), 18 deletions(-) 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/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 7303c72d6..aceb9853f 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -1700,6 +1700,13 @@ def _htmlEditProfileFiltering(baseDir: str, nickname: str, domain: str, 'name="filteredWords" style="height:200px" spellcheck="false">' + \ filterStr + '\n' + \ '
\n' + \ + '
\n' + \ + ' \n' + \ + '
\n' + \ '
\n' + \ ' \n' + \ + filterBioStr + '\n' + \ '
\n' + \ '
\n' + \ From 2c295919e63eb35fa607a46e0b4e9dc49b619cfb Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 14 Dec 2021 14:35:51 +0000 Subject: [PATCH 10/10] Include attachments text within filter scan of profile bio --- person.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/person.py b/person.py index b31f916b3..181e2ea8d 100644 --- a/person.py +++ b/person.py @@ -1686,6 +1686,21 @@ def validSendingActor(session, baseDir: str, 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): @@ -1721,12 +1736,6 @@ def validSendingActor(session, baseDir: str, if len(tag['value']) < 2: tagsWithoutValue += 1 continue - if containsInvalidChars(tag['name']): - tagsWithoutValue += 1 - continue - if containsInvalidChars(tag['value']): - tagsWithoutValue += 1 - continue if noOfTags > 0: if int(tagsWithoutValue * 100 / noOfTags) > 50: print('REJECT: actor has empty attachments ' +