mirror of https://gitlab.com/bashrc2/epicyon
				
				
				
			
		
			
				
	
	
		
			1249 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1249 lines
		
	
	
		
			47 KiB
		
	
	
	
		
			Python
		
	
	
| __filename__ = "follow.py"
 | |
| __author__ = "Bob Mottram"
 | |
| __license__ = "AGPL3+"
 | |
| __version__ = "1.1.0"
 | |
| __maintainer__ = "Bob Mottram"
 | |
| __email__ = "bob@freedombone.net"
 | |
| __status__ = "Production"
 | |
| 
 | |
| from pprint import pprint
 | |
| import os
 | |
| from utils import isSystemAccount
 | |
| from utils import getFollowersList
 | |
| from utils import validNickname
 | |
| from utils import domainPermitted
 | |
| from utils import getDomainFromActor
 | |
| from utils import getNicknameFromActor
 | |
| from utils import getStatusNumber
 | |
| from utils import followPerson
 | |
| from posts import sendSignedJson
 | |
| from posts import getPersonBox
 | |
| from utils import loadJson
 | |
| from utils import saveJson
 | |
| from acceptreject import createAccept
 | |
| from acceptreject import createReject
 | |
| from webfinger import webfingerHandle
 | |
| from auth import createBasicAuthHeader
 | |
| from session import postJson
 | |
| 
 | |
| 
 | |
| def preApprovedFollower(baseDir: str,
 | |
|                         nickname: str, domain: str,
 | |
|                         approveHandle: str,
 | |
|                         allowNewsFollowers: bool) -> bool:
 | |
|     """Is the given handle an already manually approved follower?
 | |
|     """
 | |
|     # optionally allow the news account to be followed
 | |
|     if nickname == 'news' and allowNewsFollowers:
 | |
|         return True
 | |
| 
 | |
|     handle = nickname + '@' + domain
 | |
|     accountDir = baseDir + '/accounts/' + handle
 | |
|     approvedFilename = accountDir + '/approved.txt'
 | |
|     if os.path.isfile(approvedFilename):
 | |
|         if approveHandle in open(approvedFilename).read():
 | |
|             return True
 | |
|     return False
 | |
| 
 | |
| 
 | |
| def removeFromFollowBase(baseDir: str,
 | |
|                          nickname: str, domain: str,
 | |
|                          acceptOrDenyHandle: str, followFile: str,
 | |
|                          debug: bool) -> None:
 | |
|     """Removes a handle from follow requests or rejects file
 | |
|     """
 | |
|     handle = nickname + '@' + domain
 | |
|     accountsDir = baseDir + '/accounts/' + handle
 | |
|     approveFollowsFilename = accountsDir + '/' + followFile + '.txt'
 | |
|     if not os.path.isfile(approveFollowsFilename):
 | |
|         if debug:
 | |
|             print('WARN: Approve follow requests file ' +
 | |
|                   approveFollowsFilename + ' not found')
 | |
|         return
 | |
|     if acceptOrDenyHandle not in open(approveFollowsFilename).read():
 | |
|         return
 | |
|     approvefilenew = open(approveFollowsFilename + '.new', 'w+')
 | |
|     with open(approveFollowsFilename, 'r') as approvefile:
 | |
|         for approveHandle in approvefile:
 | |
|             if not approveHandle.startswith(acceptOrDenyHandle):
 | |
|                 approvefilenew.write(approveHandle)
 | |
|     approvefilenew.close()
 | |
|     os.rename(approveFollowsFilename + '.new', approveFollowsFilename)
 | |
| 
 | |
| 
 | |
| def removeFromFollowRequests(baseDir: str,
 | |
|                              nickname: str, domain: str,
 | |
|                              denyHandle: str, debug: bool) -> None:
 | |
|     """Removes a handle from follow requests
 | |
|     """
 | |
|     removeFromFollowBase(baseDir, nickname, domain,
 | |
|                          denyHandle, 'followrequests', debug)
 | |
| 
 | |
| 
 | |
| def removeFromFollowRejects(baseDir: str,
 | |
|                             nickname: str, domain: str,
 | |
|                             acceptHandle: str, debug: bool) -> None:
 | |
|     """Removes a handle from follow rejects
 | |
|     """
 | |
|     removeFromFollowBase(baseDir, nickname, domain,
 | |
|                          acceptHandle, 'followrejects', debug)
 | |
| 
 | |
| 
 | |
| def isFollowingActor(baseDir: str,
 | |
|                      nickname: str, domain: str, actor: str) -> bool:
 | |
|     """Is the given nickname following the given actor?
 | |
|     """
 | |
|     if ':' in domain:
 | |
|         domain = domain.split(':')[0]
 | |
|     handle = nickname + '@' + domain
 | |
|     if not os.path.isdir(baseDir + '/accounts/' + handle):
 | |
|         return False
 | |
|     followingFile = baseDir + '/accounts/' + handle + '/following.txt'
 | |
|     if not os.path.isfile(followingFile):
 | |
|         return False
 | |
|     if actor.lower() in open(followingFile).read().lower():
 | |
|         return True
 | |
|     followingNickname = getNicknameFromActor(actor)
 | |
|     if not followingNickname:
 | |
|         print('WARN: unable to find nickname in ' + actor)
 | |
|         return False
 | |
|     followingDomain, followingPort = getDomainFromActor(actor)
 | |
|     followingHandle = followingNickname + '@' + followingDomain
 | |
|     if followingPort:
 | |
|         if followingPort != 80 and followingPort != 443:
 | |
|             if ':' not in followingHandle:
 | |
|                 followingHandle += ':' + str(followingPort)
 | |
|     if followingHandle.lower() in open(followingFile).read().lower():
 | |
|         return True
 | |
|     return False
 | |
| 
 | |
| 
 | |
| def getMutualsOfPerson(baseDir: str,
 | |
|                        nickname: str, domain: str) -> []:
 | |
|     """Returns the mutuals of a person
 | |
|     i.e. accounts which they follow and which also follow back
 | |
|     """
 | |
|     followers = \
 | |
|         getFollowersList(baseDir, nickname, domain, 'followers.txt')
 | |
|     following = \
 | |
|         getFollowersList(baseDir, nickname, domain, 'following.txt')
 | |
|     mutuals = []
 | |
|     for handle in following:
 | |
|         if handle in followers:
 | |
|             mutuals.append(handle)
 | |
|     return mutuals
 | |
| 
 | |
| 
 | |
| def followerOfPerson(baseDir: str, nickname: str, domain: str,
 | |
|                      followerNickname: str, followerDomain: str,
 | |
|                      federationList: [], debug: bool) -> bool:
 | |
|     """Adds a follower of the given person
 | |
|     """
 | |
|     return followPerson(baseDir, nickname, domain,
 | |
|                         followerNickname, followerDomain,
 | |
|                         federationList, debug, 'followers.txt')
 | |
| 
 | |
| 
 | |
| def isFollowerOfPerson(baseDir: str, nickname: str, domain: str,
 | |
|                        followerNickname: str, followerDomain: str) -> bool:
 | |
|     """is the given nickname a follower of followerNickname?
 | |
|     """
 | |
|     if ':' in domain:
 | |
|         domain = domain.split(':')[0]
 | |
|     followersFile = baseDir + '/accounts/' + \
 | |
|         nickname + '@' + domain + '/followers.txt'
 | |
|     if not os.path.isfile(followersFile):
 | |
|         return False
 | |
|     handle = followerNickname + '@' + followerDomain
 | |
| 
 | |
|     alreadyFollowing = False
 | |
| 
 | |
|     followersStr = ''
 | |
|     with open(followersFile, 'r') as fpFollowers:
 | |
|         followersStr = fpFollowers.read()
 | |
| 
 | |
|     if handle in followersStr:
 | |
|         alreadyFollowing = True
 | |
|     elif '://' + followerDomain + \
 | |
|          '/profile/' + followerNickname in followersStr:
 | |
|         alreadyFollowing = True
 | |
|     elif '://' + followerDomain + \
 | |
|          '/channel/' + followerNickname in followersStr:
 | |
|         alreadyFollowing = True
 | |
|     elif '://' + followerDomain + \
 | |
|          '/accounts/' + followerNickname in followersStr:
 | |
|         alreadyFollowing = True
 | |
| 
 | |
|     return alreadyFollowing
 | |
| 
 | |
| 
 | |
| def unfollowPerson(baseDir: str, nickname: str, domain: str,
 | |
|                    followNickname: str, followDomain: str,
 | |
|                    followFile='following.txt',
 | |
|                    debug=False) -> bool:
 | |
|     """Removes a person to the follow list
 | |
|     """
 | |
|     if ':' in domain:
 | |
|         domain = domain.split(':')[0]
 | |
|     handle = nickname + '@' + domain
 | |
|     handleToUnfollow = followNickname + '@' + followDomain
 | |
|     if not os.path.isdir(baseDir + '/accounts'):
 | |
|         os.mkdir(baseDir + '/accounts')
 | |
|     if not os.path.isdir(baseDir + '/accounts/' + handle):
 | |
|         os.mkdir(baseDir + '/accounts/' + handle)
 | |
| 
 | |
|     filename = baseDir + '/accounts/' + handle + '/' + followFile
 | |
|     if not os.path.isfile(filename):
 | |
|         if debug:
 | |
|             print('DEBUG: follow file ' + filename + ' was not found')
 | |
|         return False
 | |
|     handleToUnfollowLower = handleToUnfollow.lower()
 | |
|     if handleToUnfollowLower not in open(filename).read().lower():
 | |
|         if debug:
 | |
|             print('DEBUG: handle to unfollow ' + handleToUnfollow +
 | |
|                   ' is not in ' + filename)
 | |
|         return
 | |
|     with open(filename, "r") as f:
 | |
|         lines = f.readlines()
 | |
|         with open(filename, 'w+') as f:
 | |
|             for line in lines:
 | |
|                 if line.strip("\n").strip("\r").lower() != \
 | |
|                    handleToUnfollowLower:
 | |
|                     f.write(line)
 | |
| 
 | |
|     # write to an unfollowed file so that if a follow accept
 | |
|     # later arrives then it can be ignored
 | |
|     unfollowedFilename = baseDir + '/accounts/' + handle + '/unfollowed.txt'
 | |
|     if os.path.isfile(unfollowedFilename):
 | |
|         if handleToUnfollowLower not in \
 | |
|            open(unfollowedFilename).read().lower():
 | |
|             with open(unfollowedFilename, "a+") as f:
 | |
|                 f.write(handleToUnfollow + '\n')
 | |
|     else:
 | |
|         with open(unfollowedFilename, "w+") as f:
 | |
|             f.write(handleToUnfollow + '\n')
 | |
| 
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def unfollowerOfPerson(baseDir: str, nickname: str, domain: str,
 | |
|                        followerNickname: str, followerDomain: str,
 | |
|                        debug=False) -> bool:
 | |
|     """Remove a follower of a person
 | |
|     """
 | |
|     return unfollowPerson(baseDir, nickname, domain,
 | |
|                           followerNickname, followerDomain,
 | |
|                           'followers.txt', debug)
 | |
| 
 | |
| 
 | |
| def clearFollows(baseDir: str, nickname: str, domain: str,
 | |
|                  followFile='following.txt') -> None:
 | |
|     """Removes all follows
 | |
|     """
 | |
|     handle = nickname + '@' + domain
 | |
|     if not os.path.isdir(baseDir + '/accounts'):
 | |
|         os.mkdir(baseDir + '/accounts')
 | |
|     if not os.path.isdir(baseDir + '/accounts/' + handle):
 | |
|         os.mkdir(baseDir + '/accounts/' + handle)
 | |
|     filename = baseDir + '/accounts/' + handle + '/' + followFile
 | |
|     if os.path.isfile(filename):
 | |
|         os.remove(filename)
 | |
| 
 | |
| 
 | |
| def clearFollowers(baseDir: str, nickname: str, domain: str) -> None:
 | |
|     """Removes all followers
 | |
|     """
 | |
|     clearFollows(baseDir, nickname, domain, 'followers.txt')
 | |
| 
 | |
| 
 | |
| def getNoOfFollows(baseDir: str, nickname: str, domain: str,
 | |
|                    authenticated: bool,
 | |
|                    followFile='following.txt') -> int:
 | |
|     """Returns the number of follows or followers
 | |
|     """
 | |
|     # only show number of followers to authenticated
 | |
|     # account holders
 | |
|     # if not authenticated:
 | |
|     #     return 9999
 | |
|     handle = nickname + '@' + domain
 | |
|     filename = baseDir + '/accounts/' + handle + '/' + followFile
 | |
|     if not os.path.isfile(filename):
 | |
|         return 0
 | |
|     ctr = 0
 | |
|     with open(filename, "r") as f:
 | |
|         lines = f.readlines()
 | |
|         for line in lines:
 | |
|             if '#' in line:
 | |
|                 continue
 | |
|             if '@' in line and \
 | |
|                '.' in line and \
 | |
|                not line.startswith('http'):
 | |
|                 ctr += 1
 | |
|             elif ((line.startswith('http') or
 | |
|                    line.startswith('dat')) and
 | |
|                   ('/users/' in line or
 | |
|                    '/profile/' in line or
 | |
|                    '/accounts/' in line or
 | |
|                    '/channel/' in line)):
 | |
|                 ctr += 1
 | |
|     return ctr
 | |
| 
 | |
| 
 | |
| 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,
 | |
|                           authenticated, 'followers.txt')
 | |
| 
 | |
| 
 | |
| def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
 | |
|                      httpPrefix: str, authenticated: bool,
 | |
|                      followsPerPage=12,
 | |
|                      followFile='following') -> {}:
 | |
|     """Returns the following and followers feeds from GET requests.
 | |
|     This accesses the following.txt or followers.txt and builds a collection.
 | |
|     """
 | |
|     # Show a small number of follows to non-authenticated viewers
 | |
|     if not authenticated:
 | |
|         followsPerPage = 6
 | |
| 
 | |
|     if '/' + followFile not in path:
 | |
|         return None
 | |
|     # handle page numbers
 | |
|     headerOnly = True
 | |
|     pageNumber = None
 | |
|     if '?page=' in path:
 | |
|         pageNumber = path.split('?page=')[1]
 | |
|         if pageNumber == 'true' or not authenticated:
 | |
|             pageNumber = 1
 | |
|         else:
 | |
|             try:
 | |
|                 pageNumber = int(pageNumber)
 | |
|             except BaseException:
 | |
|                 pass
 | |
|         path = path.split('?page=')[0]
 | |
|         headerOnly = False
 | |
| 
 | |
|     if not path.endswith('/' + followFile):
 | |
|         return None
 | |
|     nickname = None
 | |
|     if path.startswith('/users/'):
 | |
|         nickname = path.replace('/users/', '', 1).replace('/' + followFile, '')
 | |
|     if path.startswith('/@'):
 | |
|         nickname = path.replace('/@', '', 1).replace('/' + followFile, '')
 | |
|     if not nickname:
 | |
|         return None
 | |
|     if not validNickname(domain, nickname):
 | |
|         return None
 | |
| 
 | |
|     if port:
 | |
|         if port != 80 and port != 443:
 | |
|             if ':' not in domain:
 | |
|                 domain = domain + ':' + str(port)
 | |
| 
 | |
|     if headerOnly:
 | |
|         firstStr = \
 | |
|             httpPrefix + '://' + domain + '/users/' + \
 | |
|             nickname + '/' + followFile + '?page=1'
 | |
|         idStr = \
 | |
|             httpPrefix + '://' + domain + '/users/' + \
 | |
|             nickname + '/' + followFile
 | |
|         totalStr = \
 | |
|             getNoOfFollows(baseDir, nickname, domain, authenticated)
 | |
|         following = {
 | |
|             '@context': 'https://www.w3.org/ns/activitystreams',
 | |
|             'first': firstStr,
 | |
|             'id': idStr,
 | |
|             'totalItems': totalStr,
 | |
|             'type': 'OrderedCollection'
 | |
|         }
 | |
|         return following
 | |
| 
 | |
|     if not pageNumber:
 | |
|         pageNumber = 1
 | |
| 
 | |
|     nextPageNumber = int(pageNumber + 1)
 | |
|     idStr = \
 | |
|         httpPrefix + '://' + domain + '/users/' + \
 | |
|         nickname + '/' + followFile + '?page=' + str(pageNumber)
 | |
|     partOfStr = \
 | |
|         httpPrefix + '://' + domain + '/users/' + nickname + '/' + followFile
 | |
|     following = {
 | |
|         '@context': 'https://www.w3.org/ns/activitystreams',
 | |
|         'id': idStr,
 | |
|         'orderedItems': [],
 | |
|         'partOf': partOfStr,
 | |
|         'totalItems': 0,
 | |
|         'type': 'OrderedCollectionPage'
 | |
|     }
 | |
| 
 | |
|     handleDomain = domain
 | |
|     if ':' in handleDomain:
 | |
|         handleDomain = domain.split(':')[0]
 | |
|     handle = nickname + '@' + handleDomain
 | |
|     filename = baseDir + '/accounts/' + handle + '/' + followFile + '.txt'
 | |
|     if not os.path.isfile(filename):
 | |
|         return following
 | |
|     currPage = 1
 | |
|     pageCtr = 0
 | |
|     totalCtr = 0
 | |
|     with open(filename, "r") as f:
 | |
|         lines = f.readlines()
 | |
|         for line in lines:
 | |
|             if '#' not in line:
 | |
|                 if '@' in line and not line.startswith('http'):
 | |
|                     # nickname@domain
 | |
|                     pageCtr += 1
 | |
|                     totalCtr += 1
 | |
|                     if currPage == pageNumber:
 | |
|                         line2 = \
 | |
|                             line.lower().replace('\n', '').replace('\r', '')
 | |
|                         url = httpPrefix + '://' + \
 | |
|                             line2.split('@')[1] + \
 | |
|                             '/users/' + \
 | |
|                             line2.split('@')[0]
 | |
|                         following['orderedItems'].append(url)
 | |
|                 elif ((line.startswith('http') or
 | |
|                        line.startswith('dat')) and
 | |
|                       ('/users/' in line or
 | |
|                        '/profile/' in line or
 | |
|                        '/accounts/' in line or
 | |
|                        '/channel/' in line)):
 | |
|                     # https://domain/users/nickname
 | |
|                     pageCtr += 1
 | |
|                     totalCtr += 1
 | |
|                     if currPage == pageNumber:
 | |
|                         appendStr = \
 | |
|                             line.lower().replace('\n', '').replace('\r', '')
 | |
|                         following['orderedItems'].append(appendStr)
 | |
|             if pageCtr >= followsPerPage:
 | |
|                 pageCtr = 0
 | |
|                 currPage += 1
 | |
|     following['totalItems'] = totalCtr
 | |
|     lastPage = int(totalCtr / followsPerPage)
 | |
|     if lastPage < 1:
 | |
|         lastPage = 1
 | |
|     if nextPageNumber > lastPage:
 | |
|         following['next'] = \
 | |
|             httpPrefix + '://' + domain + '/users/' + \
 | |
|             nickname + '/' + followFile + '?page=' + str(lastPage)
 | |
|     return following
 | |
| 
 | |
| 
 | |
| def followApprovalRequired(baseDir: str, nicknameToFollow: str,
 | |
|                            domainToFollow: str, debug: bool,
 | |
|                            followRequestHandle: str,
 | |
|                            allowNewsFollowers: bool) -> bool:
 | |
|     """ Returns the policy for follower approvals
 | |
|     """
 | |
|     # has this handle already been manually approved?
 | |
|     if preApprovedFollower(baseDir, nicknameToFollow, domainToFollow,
 | |
|                            followRequestHandle, allowNewsFollowers):
 | |
|         return False
 | |
| 
 | |
|     manuallyApproveFollows = False
 | |
|     if ':' in domainToFollow:
 | |
|         domainToFollow = domainToFollow.split(':')[0]
 | |
|     actorFilename = baseDir + '/accounts/' + \
 | |
|         nicknameToFollow + '@' + domainToFollow + '.json'
 | |
|     if os.path.isfile(actorFilename):
 | |
|         actor = loadJson(actorFilename)
 | |
|         if actor:
 | |
|             if actor.get('manuallyApprovesFollowers'):
 | |
|                 manuallyApproveFollows = actor['manuallyApprovesFollowers']
 | |
|             else:
 | |
|                 if debug:
 | |
|                     print(nicknameToFollow + '@' + domainToFollow +
 | |
|                           ' automatically approves followers')
 | |
|     else:
 | |
|         if debug:
 | |
|             print('DEBUG: Actor file not found: ' + actorFilename)
 | |
|     return manuallyApproveFollows
 | |
| 
 | |
| 
 | |
| 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/' + \
 | |
|         nicknameToFollow + '@' + domainToFollow
 | |
|     approveFollowsFilename = accountsDir + '/followrequests.txt'
 | |
|     if not os.path.isfile(approveFollowsFilename):
 | |
|         return 0
 | |
|     ctr = 0
 | |
|     with open(approveFollowsFilename, "r") as f:
 | |
|         lines = f.readlines()
 | |
|         if followType == "onion":
 | |
|             for fileLine in lines:
 | |
|                 if '.onion' in fileLine:
 | |
|                     ctr += 1
 | |
|         elif followType == "i2p":
 | |
|             for fileLine in lines:
 | |
|                 if '.i2p' in fileLine:
 | |
|                     ctr += 1
 | |
|         else:
 | |
|             return len(lines)
 | |
|     return ctr
 | |
| 
 | |
| 
 | |
| def storeFollowRequest(baseDir: str,
 | |
|                        nicknameToFollow: str, domainToFollow: str, port: int,
 | |
|                        nickname: str, domain: str, fromPort: int,
 | |
|                        followJson: {},
 | |
|                        debug: bool, personUrl: str) -> bool:
 | |
|     """Stores the follow request for later use
 | |
|     """
 | |
|     accountsDir = baseDir + '/accounts/' + \
 | |
|         nicknameToFollow + '@' + domainToFollow
 | |
|     if not os.path.isdir(accountsDir):
 | |
|         return False
 | |
| 
 | |
|     approveHandle = nickname + '@' + domain
 | |
|     domainFull = domain
 | |
|     if fromPort:
 | |
|         if fromPort != 80 and fromPort != 443:
 | |
|             if ':' not in domain:
 | |
|                 approveHandle = nickname + '@' + domain + ':' + str(fromPort)
 | |
|                 domainFull = domain + ':' + str(fromPort)
 | |
| 
 | |
|     followersFilename = accountsDir + '/followers.txt'
 | |
|     if os.path.isfile(followersFilename):
 | |
|         alreadyFollowing = False
 | |
| 
 | |
|         followersStr = ''
 | |
|         with open(followersFilename, 'r') as fpFollowers:
 | |
|             followersStr = fpFollowers.read()
 | |
| 
 | |
|         if approveHandle in followersStr:
 | |
|             alreadyFollowing = True
 | |
|         elif '://' + domainFull + '/profile/' + nickname in followersStr:
 | |
|             alreadyFollowing = True
 | |
|         elif '://' + domainFull + '/channel/' + nickname in followersStr:
 | |
|             alreadyFollowing = True
 | |
|         elif '://' + domainFull + '/accounts/' + nickname in followersStr:
 | |
|             alreadyFollowing = True
 | |
| 
 | |
|         if alreadyFollowing:
 | |
|             if debug:
 | |
|                 print('DEBUG: ' +
 | |
|                       nicknameToFollow + '@' + domainToFollow +
 | |
|                       ' already following ' + approveHandle)
 | |
|             return True
 | |
| 
 | |
|     # should this follow be denied?
 | |
|     denyFollowsFilename = accountsDir + '/followrejects.txt'
 | |
|     if os.path.isfile(denyFollowsFilename):
 | |
|         if approveHandle in open(denyFollowsFilename).read():
 | |
|             removeFromFollowRequests(baseDir, nicknameToFollow,
 | |
|                                      domainToFollow, approveHandle, debug)
 | |
|             print(approveHandle + ' was already denied as a follower of ' +
 | |
|                   nicknameToFollow)
 | |
|             return True
 | |
| 
 | |
|     # add to a file which contains a list of requests
 | |
|     approveFollowsFilename = accountsDir + '/followrequests.txt'
 | |
| 
 | |
|     # store either nick@domain or the full person/actor url
 | |
|     approveHandleStored = approveHandle
 | |
|     if '/users/' not in personUrl:
 | |
|         approveHandleStored = personUrl
 | |
| 
 | |
|     if os.path.isfile(approveFollowsFilename):
 | |
|         if approveHandle not in open(approveFollowsFilename).read():
 | |
|             with open(approveFollowsFilename, 'a+') as fp:
 | |
|                 fp.write(approveHandleStored + '\n')
 | |
|         else:
 | |
|             if debug:
 | |
|                 print('DEBUG: ' + approveHandleStored +
 | |
|                       ' is already awaiting approval')
 | |
|     else:
 | |
|         with open(approveFollowsFilename, "w+") as fp:
 | |
|             fp.write(approveHandleStored + '\n')
 | |
| 
 | |
|     # store the follow request in its own directory
 | |
|     # We don't rely upon the inbox because items in there could expire
 | |
|     requestsDir = accountsDir + '/requests'
 | |
|     if not os.path.isdir(requestsDir):
 | |
|         os.mkdir(requestsDir)
 | |
|     followActivityfilename = requestsDir + '/' + approveHandle + '.follow'
 | |
|     return saveJson(followJson, followActivityfilename)
 | |
| 
 | |
| 
 | |
| def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
 | |
|                          port: int, sendThreads: [], postLog: [],
 | |
|                          cachedWebfingers: {}, personCache: {},
 | |
|                          messageJson: {}, federationList: [],
 | |
|                          debug: bool, projectVersion: str,
 | |
|                          allowNewsFollowers: bool,
 | |
|                          maxFollowers: int) -> bool:
 | |
|     """Receives a follow request within the POST section of HTTPServer
 | |
|     """
 | |
|     if not messageJson['type'].startswith('Follow'):
 | |
|         return False
 | |
|     print('Receiving follow request')
 | |
|     if not messageJson.get('actor'):
 | |
|         if debug:
 | |
|             print('DEBUG: follow request has no actor')
 | |
|         return False
 | |
|     if '/users/' not in messageJson['actor'] and \
 | |
|        '/accounts/' not in messageJson['actor'] and \
 | |
|        '/channel/' not in messageJson['actor'] and \
 | |
|        '/profile/' not in messageJson['actor']:
 | |
|         if debug:
 | |
|             print('DEBUG: users/profile/accounts/channel missing from actor')
 | |
|         return False
 | |
|     domain, tempPort = getDomainFromActor(messageJson['actor'])
 | |
|     fromPort = port
 | |
|     domainFull = domain
 | |
|     if tempPort:
 | |
|         fromPort = tempPort
 | |
|         if tempPort != 80 and tempPort != 443:
 | |
|             if ':' not in domain:
 | |
|                 domainFull = domain + ':' + str(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 '/users/' not in messageJson['object'] and \
 | |
|        '/accounts/' not in messageJson['object'] and \
 | |
|        '/channel/' not in messageJson['object'] and \
 | |
|        '/profile/' not in 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 = domainToFollow
 | |
|     if tempPort:
 | |
|         if tempPort != 80 and tempPort != 443:
 | |
|             if ':' not in domainToFollow:
 | |
|                 domainToFollowFull = domainToFollow + ':' + str(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 not (nicknameToFollow == 'news' and allowNewsFollowers):
 | |
|             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,
 | |
|                               allowNewsFollowers):
 | |
|         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
 | |
| 
 | |
|         print('Storing follow request for approval')
 | |
|         return storeFollowRequest(baseDir,
 | |
|                                   nicknameToFollow, domainToFollow, port,
 | |
|                                   nickname, domain, fromPort,
 | |
|                                   messageJson, debug, messageJson['actor'])
 | |
|     else:
 | |
|         print('Follow request does not require approval')
 | |
|         # update the followers
 | |
|         if os.path.isdir(baseDir + '/accounts/' +
 | |
|                          nicknameToFollow + '@' + domainToFollow):
 | |
|             followersFilename = \
 | |
|                 baseDir + '/accounts/' + \
 | |
|                 nicknameToFollow + '@' + domainToFollow + '/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']
 | |
| 
 | |
|             print('Updating followers file: ' +
 | |
|                   followersFilename + ' adding ' + approveHandle)
 | |
|             if os.path.isfile(followersFilename):
 | |
|                 if approveHandle not in open(followersFilename).read():
 | |
|                     try:
 | |
|                         with open(followersFilename, 'r+') as followersFile:
 | |
|                             content = followersFile.read()
 | |
|                             followersFile.seek(0, 0)
 | |
|                             followersFile.write(approveHandle + '\n' + content)
 | |
|                     except Exception as e:
 | |
|                         print('WARN: ' +
 | |
|                               'Failed to write entry to followers file ' +
 | |
|                               str(e))
 | |
|             else:
 | |
|                 followersFile = open(followersFilename, "w+")
 | |
|                 followersFile.write(approveHandle + '\n')
 | |
|                 followersFile.close()
 | |
| 
 | |
|     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)
 | |
| 
 | |
| 
 | |
| def followedAccountAccepts(session, baseDir: str, httpPrefix: str,
 | |
|                            nicknameToFollow: str, domainToFollow: str,
 | |
|                            port: int,
 | |
|                            nickname: str, domain: str, fromPort: int,
 | |
|                            personUrl: str, federationList: [],
 | |
|                            followJson: {}, sendThreads: [], postLog: [],
 | |
|                            cachedWebfingers: {}, personCache: {},
 | |
|                            debug: bool, projectVersion: str,
 | |
|                            removeFollowActivity: bool):
 | |
|     """The person receiving a follow request accepts the new follower
 | |
|     and sends back an Accept activity
 | |
|     """
 | |
|     acceptHandle = nickname + '@' + domain
 | |
| 
 | |
|     # send accept back
 | |
|     if debug:
 | |
|         print('DEBUG: sending Accept activity for ' +
 | |
|               'follow request which arrived at ' +
 | |
|               nicknameToFollow + '@' + domainToFollow +
 | |
|               ' back to ' + acceptHandle)
 | |
|     acceptJson = createAccept(baseDir, federationList,
 | |
|                               nicknameToFollow, domainToFollow, port,
 | |
|                               personUrl, '', httpPrefix,
 | |
|                               followJson)
 | |
|     if debug:
 | |
|         pprint(acceptJson)
 | |
|         print('DEBUG: sending follow Accept from ' +
 | |
|               nicknameToFollow + '@' + domainToFollow +
 | |
|               ' port ' + str(port) + ' to ' +
 | |
|               acceptHandle + ' port ' + str(fromPort))
 | |
|     clientToServer = False
 | |
| 
 | |
|     if removeFollowActivity:
 | |
|         # remove the follow request json
 | |
|         followActivityfilename = \
 | |
|             baseDir + '/accounts/' + \
 | |
|             nicknameToFollow + '@' + domainToFollow + '/requests/' + \
 | |
|             nickname + '@' + domain + '.follow'
 | |
|         if os.path.isfile(followActivityfilename):
 | |
|             try:
 | |
|                 os.remove(followActivityfilename)
 | |
|             except BaseException:
 | |
|                 pass
 | |
| 
 | |
|     return sendSignedJson(acceptJson, session, baseDir,
 | |
|                           nicknameToFollow, domainToFollow, port,
 | |
|                           nickname, domain, fromPort, '',
 | |
|                           httpPrefix, True, clientToServer,
 | |
|                           federationList,
 | |
|                           sendThreads, postLog, cachedWebfingers,
 | |
|                           personCache, debug, projectVersion)
 | |
| 
 | |
| 
 | |
| def followedAccountRejects(session, baseDir: str, httpPrefix: str,
 | |
|                            nicknameToFollow: str, domainToFollow: str,
 | |
|                            port: int,
 | |
|                            nickname: str, domain: str, fromPort: int,
 | |
|                            federationList: [],
 | |
|                            sendThreads: [], postLog: [],
 | |
|                            cachedWebfingers: {}, personCache: {},
 | |
|                            debug: bool, projectVersion: str):
 | |
|     """The person receiving a follow request rejects the new follower
 | |
|     and sends back a Reject activity
 | |
|     """
 | |
|     # send reject back
 | |
|     if debug:
 | |
|         print('DEBUG: sending Reject activity for ' +
 | |
|               'follow request which arrived at ' +
 | |
|               nicknameToFollow + '@' + domainToFollow +
 | |
|               ' back to ' + nickname + '@' + domain)
 | |
| 
 | |
|     # get the json for the original follow request
 | |
|     followActivityfilename = \
 | |
|         baseDir + '/accounts/' + \
 | |
|         nicknameToFollow + '@' + domainToFollow + '/requests/' + \
 | |
|         nickname + '@' + domain + '.follow'
 | |
|     followJson = loadJson(followActivityfilename)
 | |
|     if not followJson:
 | |
|         print('No follow request json was found for ' +
 | |
|               followActivityfilename)
 | |
|         return None
 | |
|     # actor who made the follow request
 | |
|     personUrl = followJson['actor']
 | |
| 
 | |
|     # create the reject activity
 | |
|     rejectJson = \
 | |
|         createReject(baseDir, federationList,
 | |
|                      nicknameToFollow, domainToFollow, port,
 | |
|                      personUrl, '', httpPrefix, followJson)
 | |
|     if debug:
 | |
|         pprint(rejectJson)
 | |
|         print('DEBUG: sending follow Reject from ' +
 | |
|               nicknameToFollow + '@' + domainToFollow +
 | |
|               ' port ' + str(port) + ' to ' +
 | |
|               nickname + '@' + domain + ' port ' + str(fromPort))
 | |
|     clientToServer = False
 | |
|     denyHandle = nickname + '@' + domain
 | |
|     if fromPort:
 | |
|         if fromPort != 80 and fromPort != 443:
 | |
|             denyHandle = denyHandle + ':' + str(fromPort)
 | |
|     # remove from the follow requests file
 | |
|     removeFromFollowRequests(baseDir, nicknameToFollow, domainToFollow,
 | |
|                              denyHandle, debug)
 | |
|     # remove the follow request json
 | |
|     try:
 | |
|         os.remove(followActivityfilename)
 | |
|     except BaseException:
 | |
|         pass
 | |
|     # send the reject activity
 | |
|     return sendSignedJson(rejectJson, session, baseDir,
 | |
|                           nicknameToFollow, domainToFollow, port,
 | |
|                           nickname, domain, fromPort, '',
 | |
|                           httpPrefix, True, clientToServer,
 | |
|                           federationList,
 | |
|                           sendThreads, postLog, cachedWebfingers,
 | |
|                           personCache, debug, projectVersion)
 | |
| 
 | |
| 
 | |
| def sendFollowRequest(session, baseDir: str,
 | |
|                       nickname: str, domain: str, port: int, httpPrefix: str,
 | |
|                       followNickname: str, followDomain: str,
 | |
|                       followPort: int, followHttpPrefix: str,
 | |
|                       clientToServer: bool, federationList: [],
 | |
|                       sendThreads: [], postLog: [], cachedWebfingers: {},
 | |
|                       personCache: {}, debug: bool,
 | |
|                       projectVersion: str, allowNewsFollowers: bool) -> {}:
 | |
|     """Gets the json object for sending a follow request
 | |
|     """
 | |
|     if not domainPermitted(followDomain, federationList):
 | |
|         return None
 | |
| 
 | |
|     fullDomain = domain
 | |
|     followActor = httpPrefix + '://' + domain + '/users/' + nickname
 | |
|     if port:
 | |
|         if port != 80 and port != 443:
 | |
|             if ':' not in domain:
 | |
|                 fullDomain = domain + ':' + str(port)
 | |
|                 followActor = httpPrefix + '://' + \
 | |
|                     fullDomain + '/users/' + nickname
 | |
| 
 | |
|     requestDomain = followDomain
 | |
|     if followPort:
 | |
|         if followPort != 80 and followPort != 443:
 | |
|             if ':' not in followDomain:
 | |
|                 requestDomain = followDomain + ':' + str(followPort)
 | |
| 
 | |
|     statusNumber, published = getStatusNumber()
 | |
| 
 | |
|     if followNickname:
 | |
|         followedId = followHttpPrefix + '://' + \
 | |
|             requestDomain + '/users/' + followNickname
 | |
|         followHandle = followNickname + '@' + requestDomain
 | |
|     else:
 | |
|         if debug:
 | |
|             print('DEBUG: sendFollowRequest - assuming single user instance')
 | |
|         followedId = followHttpPrefix + '://' + requestDomain
 | |
|         singleUserNickname = 'dev'
 | |
|         followHandle = singleUserNickname + '@' + requestDomain
 | |
| 
 | |
|     newFollowJson = {
 | |
|         '@context': 'https://www.w3.org/ns/activitystreams',
 | |
|         'id': followActor + '/statuses/' + str(statusNumber),
 | |
|         'type': 'Follow',
 | |
|         'actor': followActor,
 | |
|         'object': followedId
 | |
|     }
 | |
| 
 | |
|     if followApprovalRequired(baseDir, nickname, domain, debug,
 | |
|                               followHandle, allowNewsFollowers):
 | |
|         # 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
 | |
|         # follow request will block them again.
 | |
|         removeFromFollowRejects(baseDir,
 | |
|                                 nickname, domain,
 | |
|                                 followHandle, debug)
 | |
| 
 | |
|     sendSignedJson(newFollowJson, session, baseDir, nickname, domain, port,
 | |
|                    followNickname, followDomain, followPort,
 | |
|                    'https://www.w3.org/ns/activitystreams#Public',
 | |
|                    httpPrefix, True, clientToServer,
 | |
|                    federationList,
 | |
|                    sendThreads, postLog, cachedWebfingers, personCache,
 | |
|                    debug, projectVersion)
 | |
| 
 | |
|     return newFollowJson
 | |
| 
 | |
| 
 | |
| def sendFollowRequestViaServer(baseDir: str, session,
 | |
|                                fromNickname: str, password: str,
 | |
|                                fromDomain: str, fromPort: int,
 | |
|                                followNickname: str, followDomain: str,
 | |
|                                followPort: int,
 | |
|                                httpPrefix: str,
 | |
|                                cachedWebfingers: {}, personCache: {},
 | |
|                                debug: bool, projectVersion: str) -> {}:
 | |
|     """Creates a follow request via c2s
 | |
|     """
 | |
|     if not session:
 | |
|         print('WARN: No session for sendFollowRequestViaServer')
 | |
|         return 6
 | |
| 
 | |
|     fromDomainFull = fromDomain
 | |
|     if fromPort:
 | |
|         if fromPort != 80 and fromPort != 443:
 | |
|             if ':' not in fromDomain:
 | |
|                 fromDomainFull = fromDomain + ':' + str(fromPort)
 | |
| 
 | |
|     followDomainFull = followDomain
 | |
|     if followPort:
 | |
|         if followPort != 80 and followPort != 443:
 | |
|             if ':' not in followDomain:
 | |
|                 followDomainFull = followDomain + ':' + str(followPort)
 | |
| 
 | |
|     followActor = httpPrefix + '://' + \
 | |
|         fromDomainFull + '/users/' + fromNickname
 | |
|     followedId = httpPrefix + '://' + \
 | |
|         followDomainFull + '/users/' + followNickname
 | |
| 
 | |
|     statusNumber, published = getStatusNumber()
 | |
|     newFollowJson = {
 | |
|         '@context': 'https://www.w3.org/ns/activitystreams',
 | |
|         'id': followActor + '/statuses/' + str(statusNumber),
 | |
|         'type': 'Follow',
 | |
|         'actor': followActor,
 | |
|         'object': followedId
 | |
|     }
 | |
| 
 | |
|     handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
 | |
| 
 | |
|     # lookup the inbox for the To handle
 | |
|     wfRequest = \
 | |
|         webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
 | |
|                         fromDomain, projectVersion)
 | |
|     if not wfRequest:
 | |
|         if debug:
 | |
|             print('DEBUG: announce webfinger failed for ' + handle)
 | |
|         return 1
 | |
|     if not isinstance(wfRequest, dict):
 | |
|         print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
 | |
|               str(wfRequest))
 | |
|         return 1
 | |
| 
 | |
|     postToBox = 'outbox'
 | |
| 
 | |
|     # get the actor inbox for the To handle
 | |
|     (inboxUrl, pubKeyId, pubKey,
 | |
|      fromPersonId, sharedInbox, avatarUrl,
 | |
|      displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
 | |
|                                  projectVersion, httpPrefix, fromNickname,
 | |
|                                  fromDomain, postToBox)
 | |
| 
 | |
|     if not inboxUrl:
 | |
|         if debug:
 | |
|             print('DEBUG: No ' + postToBox + ' was found for ' + handle)
 | |
|         return 3
 | |
|     if not fromPersonId:
 | |
|         if debug:
 | |
|             print('DEBUG: No actor was found for ' + handle)
 | |
|         return 4
 | |
| 
 | |
|     authHeader = createBasicAuthHeader(fromNickname, password)
 | |
| 
 | |
|     headers = {
 | |
|         'host': fromDomain,
 | |
|         'Content-type': 'application/json',
 | |
|         'Authorization': authHeader
 | |
|     }
 | |
|     postResult = \
 | |
|         postJson(session, newFollowJson, [], inboxUrl, headers)
 | |
|     if not postResult:
 | |
|         if debug:
 | |
|             print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
 | |
|         return 5
 | |
| 
 | |
|     if debug:
 | |
|         print('DEBUG: c2s POST follow success')
 | |
| 
 | |
|     return newFollowJson
 | |
| 
 | |
| 
 | |
| def sendUnfollowRequestViaServer(baseDir: str, session,
 | |
|                                  fromNickname: str, password: str,
 | |
|                                  fromDomain: str, fromPort: int,
 | |
|                                  followNickname: str, followDomain: str,
 | |
|                                  followPort: int,
 | |
|                                  httpPrefix: str,
 | |
|                                  cachedWebfingers: {}, personCache: {},
 | |
|                                  debug: bool, projectVersion: str) -> {}:
 | |
|     """Creates a unfollow request via c2s
 | |
|     """
 | |
|     if not session:
 | |
|         print('WARN: No session for sendUnfollowRequestViaServer')
 | |
|         return 6
 | |
| 
 | |
|     fromDomainFull = fromDomain
 | |
|     if fromPort:
 | |
|         if fromPort != 80 and fromPort != 443:
 | |
|             if ':' not in fromDomain:
 | |
|                 fromDomainFull = fromDomain + ':' + str(fromPort)
 | |
|     followDomainFull = followDomain
 | |
|     if followPort:
 | |
|         if followPort != 80 and followPort != 443:
 | |
|             if ':' not in followDomain:
 | |
|                 followDomainFull = followDomain + ':' + str(followPort)
 | |
| 
 | |
|     followActor = httpPrefix + '://' + \
 | |
|         fromDomainFull + '/users/' + fromNickname
 | |
|     followedId = httpPrefix + '://' + \
 | |
|         followDomainFull + '/users/' + followNickname
 | |
|     statusNumber, published = getStatusNumber()
 | |
| 
 | |
|     unfollowJson = {
 | |
|         '@context': 'https://www.w3.org/ns/activitystreams',
 | |
|         'id': followActor + '/statuses/' + str(statusNumber) + '/undo',
 | |
|         'type': 'Undo',
 | |
|         'actor': followActor,
 | |
|         'object': {
 | |
|             'id': followActor + '/statuses/' + str(statusNumber),
 | |
|             'type': 'Follow',
 | |
|             'actor': followActor,
 | |
|             'object': followedId
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
 | |
| 
 | |
|     # lookup the inbox for the To handle
 | |
|     wfRequest = \
 | |
|         webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
 | |
|                         fromDomain, projectVersion)
 | |
|     if not wfRequest:
 | |
|         if debug:
 | |
|             print('DEBUG: announce webfinger failed for ' + handle)
 | |
|         return 1
 | |
|     if not isinstance(wfRequest, dict):
 | |
|         print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
 | |
|               str(wfRequest))
 | |
|         return 1
 | |
| 
 | |
|     postToBox = 'outbox'
 | |
| 
 | |
|     # get the actor inbox for the To handle
 | |
|     (inboxUrl, pubKeyId, pubKey,
 | |
|      fromPersonId, sharedInbox,
 | |
|      avatarUrl, displayName) = getPersonBox(baseDir, session,
 | |
|                                             wfRequest, personCache,
 | |
|                                             projectVersion, httpPrefix,
 | |
|                                             fromNickname,
 | |
|                                             fromDomain, postToBox)
 | |
| 
 | |
|     if not inboxUrl:
 | |
|         if debug:
 | |
|             print('DEBUG: No ' + postToBox + ' was found for ' + handle)
 | |
|         return 3
 | |
|     if not fromPersonId:
 | |
|         if debug:
 | |
|             print('DEBUG: No actor was found for ' + handle)
 | |
|         return 4
 | |
| 
 | |
|     authHeader = createBasicAuthHeader(fromNickname, password)
 | |
| 
 | |
|     headers = {
 | |
|         'host': fromDomain,
 | |
|         'Content-type': 'application/json',
 | |
|         'Authorization': authHeader
 | |
|     }
 | |
|     postResult = \
 | |
|         postJson(session, unfollowJson, [], inboxUrl, headers)
 | |
|     if not postResult:
 | |
|         if debug:
 | |
|             print('DEBUG: POST announce failed for c2s to ' + inboxUrl)
 | |
|         return 5
 | |
| 
 | |
|     if debug:
 | |
|         print('DEBUG: c2s POST unfollow success')
 | |
| 
 | |
|     return unfollowJson
 | |
| 
 | |
| 
 | |
| def getFollowersOfActor(baseDir: str, actor: str, debug: bool) -> {}:
 | |
|     """In a shared inbox if we receive a post we know who it's from
 | |
|     and if it's addressed to followers then we need to get a list of those.
 | |
|     This returns a list of account handles which follow the given actor
 | |
|     """
 | |
|     if debug:
 | |
|         print('DEBUG: getting followers of ' + actor)
 | |
|     recipientsDict = {}
 | |
|     if ':' not in actor:
 | |
|         return recipientsDict
 | |
|     nickname = getNicknameFromActor(actor)
 | |
|     if not nickname:
 | |
|         if debug:
 | |
|             print('DEBUG: no nickname found in ' + actor)
 | |
|         return recipientsDict
 | |
|     domain, port = getDomainFromActor(actor)
 | |
|     if not domain:
 | |
|         if debug:
 | |
|             print('DEBUG: no domain found in ' + actor)
 | |
|         return recipientsDict
 | |
|     actorHandle = nickname + '@' + domain
 | |
|     if debug:
 | |
|         print('DEBUG: searching for handle ' + actorHandle)
 | |
|     # for each of the accounts
 | |
|     for subdir, dirs, files in os.walk(baseDir + '/accounts'):
 | |
|         for account in dirs:
 | |
|             if '@' in account and not account.startswith('inbox@'):
 | |
|                 followingFilename = \
 | |
|                     os.path.join(subdir, account) + '/following.txt'
 | |
|                 if debug:
 | |
|                     print('DEBUG: examining follows of ' + account)
 | |
|                     print(followingFilename)
 | |
|                 if os.path.isfile(followingFilename):
 | |
|                     # does this account follow the given actor?
 | |
|                     if debug:
 | |
|                         print('DEBUG: checking if ' + actorHandle +
 | |
|                               ' in ' + followingFilename)
 | |
|                     if actorHandle in open(followingFilename).read():
 | |
|                         if debug:
 | |
|                             print('DEBUG: ' + account +
 | |
|                                   ' follows ' + actorHandle)
 | |
|                         recipientsDict[account] = None
 | |
|     return recipientsDict
 | |
| 
 | |
| 
 | |
| def outboxUndoFollow(baseDir: str, messageJson: {}, debug: bool) -> None:
 | |
|     """When an unfollow request is received by the outbox from c2s
 | |
|     This removes the followed handle from the following.txt file
 | |
|     of the relevant account
 | |
|     """
 | |
|     if not messageJson.get('type'):
 | |
|         return
 | |
|     if not messageJson['type'] == 'Undo':
 | |
|         return
 | |
|     if not messageJson.get('object'):
 | |
|         return
 | |
|     if not isinstance(messageJson['object'], dict):
 | |
|         return
 | |
|     if not messageJson['object'].get('type'):
 | |
|         return
 | |
|     if not messageJson['object']['type'] == 'Follow':
 | |
|         return
 | |
|     if not messageJson['object'].get('object'):
 | |
|         return
 | |
|     if not messageJson['object'].get('actor'):
 | |
|         return
 | |
|     if not isinstance(messageJson['object']['object'], str):
 | |
|         return
 | |
|     if debug:
 | |
|         print('DEBUG: undo follow arrived in outbox')
 | |
| 
 | |
|     nicknameFollower = getNicknameFromActor(messageJson['object']['actor'])
 | |
|     if not nicknameFollower:
 | |
|         print('WARN: unable to find nickname in ' +
 | |
|               messageJson['object']['actor'])
 | |
|         return
 | |
|     domainFollower, portFollower = \
 | |
|         getDomainFromActor(messageJson['object']['actor'])
 | |
|     domainFollowerFull = domainFollower
 | |
|     if portFollower:
 | |
|         if portFollower != 80 and portFollower != 443:
 | |
|             if ':' not in domainFollower:
 | |
|                 domainFollowerFull = domainFollower + ':' + str(portFollower)
 | |
| 
 | |
|     nicknameFollowing = getNicknameFromActor(messageJson['object']['object'])
 | |
|     if not nicknameFollowing:
 | |
|         print('WARN: unable to find nickname in ' +
 | |
|               messageJson['object']['object'])
 | |
|         return
 | |
|     domainFollowing, portFollowing = \
 | |
|         getDomainFromActor(messageJson['object']['object'])
 | |
|     domainFollowingFull = domainFollowing
 | |
|     if portFollowing:
 | |
|         if portFollowing != 80 and portFollowing != 443:
 | |
|             if ':' not in domainFollowing:
 | |
|                 domainFollowingFull = \
 | |
|                     domainFollowing + ':' + str(portFollowing)
 | |
| 
 | |
|     if unfollowPerson(baseDir, nicknameFollower, domainFollowerFull,
 | |
|                       nicknameFollowing, domainFollowingFull):
 | |
|         if debug:
 | |
|             print('DEBUG: ' + nicknameFollower + ' unfollowed ' +
 | |
|                   nicknameFollowing + '@' + domainFollowingFull)
 | |
|     else:
 | |
|         if debug:
 | |
|             print('WARN: ' + nicknameFollower + ' could not unfollow ' +
 | |
|                   nicknameFollowing + '@' + domainFollowingFull)
 | |
| 
 | |
| 
 | |
| def followerApprovalActive(baseDir: str, nickname: str, domain: str) -> bool:
 | |
|     """Returns true if the given account requires follower approval
 | |
|     """
 | |
|     manuallyApprovesFollowers = False
 | |
|     actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
 | |
|     if os.path.isfile(actorFilename):
 | |
|         actorJson = loadJson(actorFilename)
 | |
|         if actorJson:
 | |
|             if actorJson.get('manuallyApprovesFollowers'):
 | |
|                 manuallyApprovesFollowers = \
 | |
|                     actorJson['manuallyApprovesFollowers']
 | |
|     return manuallyApprovesFollowers
 |