follow.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. __filename__ = "follow.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import json
  9. import time
  10. from pprint import pprint
  11. import os
  12. import sys
  13. from utils import validNickname
  14. from utils import domainPermitted
  15. from utils import getDomainFromActor
  16. from utils import getNicknameFromActor
  17. from utils import getStatusNumber
  18. from utils import followPerson
  19. from posts import sendSignedJson
  20. from posts import getPersonBox
  21. from utils import loadJson
  22. from utils import saveJson
  23. from acceptreject import createAccept
  24. from acceptreject import createReject
  25. from webfinger import webfingerHandle
  26. from auth import createBasicAuthHeader
  27. from auth import createPassword
  28. from session import postJson
  29. def preApprovedFollower(baseDir: str, \
  30. nickname: str,domain: str, \
  31. approveHandle: str) -> bool:
  32. """Is the given handle an already manually approved follower?
  33. """
  34. handle=nickname+'@'+domain
  35. accountDir=baseDir+'/accounts/'+handle
  36. approvedFilename=accountDir+'/approved.txt'
  37. if os.path.isfile(approvedFilename):
  38. if approveHandle in open(approvedFilename).read():
  39. return True
  40. return False
  41. def removeFromFollowBase(baseDir: str, \
  42. nickname: str,domain: str, \
  43. acceptOrDenyHandle: str,followFile: str, \
  44. debug: bool) -> None:
  45. """Removes a handle from follow requests or rejects file
  46. """
  47. handle=nickname+'@'+domain
  48. accountsDir=baseDir+'/accounts/'+handle
  49. approveFollowsFilename=accountsDir+'/'+followFile+'.txt'
  50. if not os.path.isfile(approveFollowsFilename):
  51. if debug:
  52. print('WARN: Follow requests file '+approveFollowsFilename+' not found')
  53. return
  54. if acceptOrDenyHandle not in open(approveFollowsFilename).read():
  55. return
  56. approvefilenew = open(approveFollowsFilename+'.new', 'w+')
  57. with open(approveFollowsFilename, 'r') as approvefile:
  58. for approveHandle in approvefile:
  59. if not approveHandle.startswith(acceptOrDenyHandle):
  60. approvefilenew.write(approveHandle)
  61. approvefilenew.close()
  62. os.rename(approveFollowsFilename+'.new',approveFollowsFilename)
  63. def removeFromFollowRequests(baseDir: str, \
  64. nickname: str,domain: str, \
  65. denyHandle: str,debug: bool) -> None:
  66. """Removes a handle from follow requests
  67. """
  68. removeFromFollowBase(baseDir,nickname,domain, \
  69. denyHandle,'followrequests',debug)
  70. def removeFromFollowRejects(baseDir: str, \
  71. nickname: str,domain: str, \
  72. acceptHandle: str,debug: bool) -> None:
  73. """Removes a handle from follow rejects
  74. """
  75. removeFromFollowBase(baseDir,nickname,domain, \
  76. acceptHandle,'followrejects',debug)
  77. def isFollowingActor(baseDir: str,nickname: str,domain: str,actor: str) -> bool:
  78. """Is the given actor a follower of the given nickname?
  79. """
  80. if ':' in domain:
  81. domain=domain.split(':')[0]
  82. handle=nickname+'@'+domain
  83. if not os.path.isdir(baseDir+'/accounts/'+handle):
  84. return False
  85. followingFile=baseDir+'/accounts/'+handle+'/following.txt'
  86. if not os.path.isfile(followingFile):
  87. return False
  88. if actor in open(followingFile).read():
  89. return True
  90. followingNickname=getNicknameFromActor(actor)
  91. if not followingNickname:
  92. print('WARN: unable to find nickname in '+actor)
  93. return False
  94. followingDomain,followingPort=getDomainFromActor(actor)
  95. followingHandle=followingNickname+'@'+followingDomain
  96. if followingPort:
  97. if followingPort!=80 and followingPort!=443:
  98. if ':' not in followingHandle:
  99. followingHandle+=':'+str(followingPort)
  100. if followingHandle in open(followingFile).read():
  101. return True
  102. return False
  103. def getMutualsOfPerson(baseDir: str, \
  104. nickname: str,domain: str, \
  105. followFile='following.txt') -> []:
  106. """Returns the mutuals of a person
  107. i.e. accounts which they follow and which also follow back
  108. """
  109. followers= \
  110. getFollowersOfPerson(baseDir,nickname,domain,'followers')
  111. following= \
  112. getFollowersOfPerson(baseDir,nickname,domain,'following')
  113. mutuals=[]
  114. for handle in following:
  115. if handle in followers:
  116. mutuals.append(handle)
  117. return mutuals
  118. def getFollowersOfPerson(baseDir: str, \
  119. nickname: str,domain: str, \
  120. followFile='following.txt') -> []:
  121. """Returns a list containing the followers of the given person
  122. Used by the shared inbox to know who to send incoming mail to
  123. """
  124. followers=[]
  125. if ':' in domain:
  126. domain=domain.split(':')[0]
  127. handle=nickname+'@'+domain
  128. if not os.path.isdir(baseDir+'/accounts/'+handle):
  129. return followers
  130. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  131. for account in dirs:
  132. filename = os.path.join(subdir, account)+'/'+followFile
  133. if account == handle or account.startswith('inbox@'):
  134. continue
  135. if not os.path.isfile(filename):
  136. continue
  137. with open(filename, 'r') as followingfile:
  138. for followingHandle in followingfile:
  139. if followingHandle.replace('\n','')==handle:
  140. if account not in followers:
  141. followers.append(account)
  142. break
  143. return followers
  144. def followerOfPerson(baseDir: str,nickname: str, domain: str, \
  145. followerNickname: str, followerDomain: str, \
  146. federationList: [],debug :bool) -> bool:
  147. """Adds a follower of the given person
  148. """
  149. return followPerson(baseDir,nickname,domain, \
  150. followerNickname,followerDomain, \
  151. federationList,debug,'followers.txt')
  152. def isFollowerOfPerson(baseDir: str,nickname: str, domain: str, \
  153. followerNickname: str, followerDomain: str) -> bool:
  154. """is the given nickname a follower of followerNickname?
  155. """
  156. if ':' in domain:
  157. domain=domain.split(':')[0]
  158. followersFile=baseDir+'/accounts/'+nickname+'@'+domain+'/followers.txt'
  159. if not os.path.isfile(followersFile):
  160. return False
  161. return followerNickname+'@'+followerDomain in open(followersFile).read()
  162. def unfollowPerson(baseDir: str,nickname: str, domain: str, \
  163. followNickname: str, followDomain: str, \
  164. followFile='following.txt', \
  165. debug=False) -> bool:
  166. """Removes a person to the follow list
  167. """
  168. if ':' in domain:
  169. domain=domain.split(':')[0]
  170. handle=nickname+'@'+domain
  171. handleToUnfollow=followNickname+'@'+followDomain
  172. if not os.path.isdir(baseDir+'/accounts'):
  173. os.mkdir(baseDir+'/accounts')
  174. if not os.path.isdir(baseDir+'/accounts/'+handle):
  175. os.mkdir(baseDir+'/accounts/'+handle)
  176. filename=baseDir+'/accounts/'+handle+'/'+followFile
  177. if not os.path.isfile(filename):
  178. if debug:
  179. print('DEBUG: follow file '+filename+' was not found')
  180. return False
  181. if handleToUnfollow not in open(filename).read():
  182. if debug:
  183. print('DEBUG: handle to unfollow '+handleToUnfollow+' is not in '+filename)
  184. return
  185. with open(filename, "r") as f:
  186. lines = f.readlines()
  187. with open(filename, "w") as f:
  188. for line in lines:
  189. if line.strip("\n") != handleToUnfollow:
  190. f.write(line)
  191. return True
  192. def unfollowerOfPerson(baseDir: str,nickname: str,domain: str, \
  193. followerNickname: str,followerDomain: str, \
  194. debug=False) -> bool:
  195. """Remove a follower of a person
  196. """
  197. return unfollowPerson(baseDir,nickname,domain, \
  198. followerNickname,followerDomain, \
  199. 'followers.txt',debug)
  200. def clearFollows(baseDir: str,nickname: str,domain: str, \
  201. followFile='following.txt') -> None:
  202. """Removes all follows
  203. """
  204. handle=nickname.lower()+'@'+domain.lower()
  205. if not os.path.isdir(baseDir+'/accounts'):
  206. os.mkdir(baseDir+'/accounts')
  207. if not os.path.isdir(baseDir+'/accounts/'+handle):
  208. os.mkdir(baseDir+'/accounts/'+handle)
  209. filename=baseDir+'/accounts/'+handle+'/'+followFile
  210. if os.path.isfile(filename):
  211. os.remove(filename)
  212. def clearFollowers(baseDir: str,nickname: str,domain: str) -> None:
  213. """Removes all followers
  214. """
  215. clearFollows(baseDir,nickname, domain,'followers.txt')
  216. def getNoOfFollows(baseDir: str,nickname: str,domain: str, \
  217. authenticated: bool, \
  218. followFile='following.txt') -> int:
  219. """Returns the number of follows or followers
  220. """
  221. # only show number of followers to authenticated
  222. # account holders
  223. #if not authenticated:
  224. # return 9999
  225. handle=nickname.lower()+'@'+domain.lower()
  226. filename=baseDir+'/accounts/'+handle+'/'+followFile
  227. if not os.path.isfile(filename):
  228. return 0
  229. ctr = 0
  230. with open(filename, "r") as f:
  231. lines = f.readlines()
  232. for line in lines:
  233. if '#' not in line:
  234. if '@' in line and '.' in line and not line.startswith('http'):
  235. ctr += 1
  236. elif line.startswith('http') and '/users/' in line:
  237. ctr += 1
  238. return ctr
  239. def getNoOfFollowers(baseDir: str,nickname: str,domain: str,authenticated: bool) -> int:
  240. """Returns the number of followers of the given person
  241. """
  242. return getNoOfFollows(baseDir,nickname,domain,authenticated,'followers.txt')
  243. def getFollowingFeed(baseDir: str,domain: str,port: int,path: str, \
  244. httpPrefix: str, authenticated: bool,
  245. followsPerPage=12, \
  246. followFile='following') -> {}:
  247. """Returns the following and followers feeds from GET requests
  248. """
  249. # Show a small number of follows to non-authenticated viewers
  250. if not authenticated:
  251. followsPerPage=6
  252. if '/'+followFile not in path:
  253. return None
  254. # handle page numbers
  255. headerOnly=True
  256. pageNumber=None
  257. if '?page=' in path:
  258. pageNumber=path.split('?page=')[1]
  259. if pageNumber=='true' or not authenticated:
  260. pageNumber=1
  261. else:
  262. try:
  263. pageNumber=int(pageNumber)
  264. except:
  265. pass
  266. path=path.split('?page=')[0]
  267. headerOnly=False
  268. if not path.endswith('/'+followFile):
  269. return None
  270. nickname=None
  271. if path.startswith('/users/'):
  272. nickname=path.replace('/users/','',1).replace('/'+followFile,'')
  273. if path.startswith('/@'):
  274. nickname=path.replace('/@','',1).replace('/'+followFile,'')
  275. if not nickname:
  276. return None
  277. if not validNickname(domain,nickname):
  278. return None
  279. if port:
  280. if port!=80 and port!=443:
  281. if ':' not in domain:
  282. domain=domain+':'+str(port)
  283. if headerOnly:
  284. following = {
  285. '@context': 'https://www.w3.org/ns/activitystreams',
  286. 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+followFile+'?page=1',
  287. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+followFile,
  288. 'totalItems': getNoOfFollows(baseDir,nickname,domain,authenticated),
  289. 'type': 'OrderedCollection'}
  290. return following
  291. if not pageNumber:
  292. pageNumber=1
  293. nextPageNumber=int(pageNumber+1)
  294. following = {
  295. '@context': 'https://www.w3.org/ns/activitystreams',
  296. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+followFile+'?page='+str(pageNumber),
  297. 'orderedItems': [],
  298. 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+followFile,
  299. 'totalItems': 0,
  300. 'type': 'OrderedCollectionPage'}
  301. handleDomain=domain
  302. if ':' in handleDomain:
  303. handleDomain=domain.split(':')[0]
  304. handle=nickname.lower()+'@'+handleDomain.lower()
  305. filename=baseDir+'/accounts/'+handle+'/'+followFile+'.txt'
  306. if not os.path.isfile(filename):
  307. return following
  308. currPage=1
  309. pageCtr=0
  310. totalCtr=0
  311. with open(filename, "r") as f:
  312. lines = f.readlines()
  313. for line in lines:
  314. if '#' not in line:
  315. if '@' in line and not line.startswith('http'):
  316. pageCtr += 1
  317. totalCtr += 1
  318. if currPage==pageNumber:
  319. url = httpPrefix + '://' + line.lower().replace('\n','').split('@')[1] + \
  320. '/users/' + line.lower().replace('\n','').split('@')[0]
  321. following['orderedItems'].append(url)
  322. elif (line.startswith('http') or line.startswith('dat')) and '/users/' in line:
  323. pageCtr += 1
  324. totalCtr += 1
  325. if currPage==pageNumber:
  326. following['orderedItems'].append(line.lower().replace('\n',''))
  327. if pageCtr>=followsPerPage:
  328. pageCtr=0
  329. currPage += 1
  330. following['totalItems']=totalCtr
  331. lastPage=int(totalCtr/followsPerPage)
  332. if lastPage<1:
  333. lastPage=1
  334. if nextPageNumber>lastPage:
  335. following['next']=httpPrefix+'://'+domain+'/users/'+nickname+'/'+followFile+'?page='+str(lastPage)
  336. return following
  337. def followApprovalRequired(baseDir: str,nicknameToFollow: str, \
  338. domainToFollow: str,debug: bool, \
  339. followRequestHandle: str) -> bool:
  340. """ Returns the policy for follower approvals
  341. """
  342. # has this handle already been manually approved?
  343. if preApprovedFollower(baseDir,nicknameToFollow,domainToFollow, \
  344. followRequestHandle):
  345. return False
  346. manuallyApproveFollows=False
  347. if ':' in domainToFollow:
  348. domainToFollow=domainToFollow.split(':')[0]
  349. actorFilename=baseDir+'/accounts/'+nicknameToFollow+'@'+domainToFollow+'.json'
  350. if os.path.isfile(actorFilename):
  351. actor=loadJson(actorFilename)
  352. if actor:
  353. if actor.get('manuallyApprovesFollowers'):
  354. manuallyApproveFollows=actor['manuallyApprovesFollowers']
  355. else:
  356. if debug:
  357. print('manuallyApprovesFollowers is missing from '+actorFilename)
  358. else:
  359. if debug:
  360. print('DEBUG: Actor file not found: '+actorFilename)
  361. return manuallyApproveFollows
  362. def noOfFollowRequests(baseDir: str, \
  363. nicknameToFollow: str,domainToFollow: str, \
  364. nickname: str,domain: str,fromPort: int, \
  365. followType: str) -> int:
  366. """Returns the current number of follow requests
  367. """
  368. accountsDir=baseDir+'/accounts/'+nicknameToFollow+'@'+domainToFollow
  369. approveFollowsFilename=accountsDir+'/followrequests.txt'
  370. if not os.path.isfile(approveFollowsFilename):
  371. return 0
  372. ctr=0
  373. with open(approveFollowsFilename, "r") as f:
  374. lines = f.readlines()
  375. if followType != "onion":
  376. return len(lines)
  377. for l in lines:
  378. if '.onion' in l:
  379. ctr+=1
  380. return ctr
  381. def storeFollowRequest(baseDir: str, \
  382. nicknameToFollow: str,domainToFollow: str,port: int, \
  383. nickname: str,domain: str,fromPort: int, \
  384. followJson: {}, \
  385. debug: bool) -> bool:
  386. """Stores the follow request for later use
  387. """
  388. accountsDir=baseDir+'/accounts/'+nicknameToFollow+'@'+domainToFollow
  389. if not os.path.isdir(accountsDir):
  390. return False
  391. approveHandle=nickname+'@'+domain
  392. if fromPort:
  393. if fromPort!=80 and fromPort!=443:
  394. if ':' not in domain:
  395. approveHandle=nickname+'@'+domain+':'+str(fromPort)
  396. followersFilename=accountsDir+'/followers.txt'
  397. if os.path.isfile(followersFilename):
  398. if approveHandle in open(followersFilename).read():
  399. if debug:
  400. print('DEBUG: '+ \
  401. nicknameToFollow+'@'+domainToFollow+ \
  402. ' already following '+approveHandle)
  403. return True
  404. # should this follow be denied?
  405. denyFollowsFilename=accountsDir+'/followrejects.txt'
  406. if os.path.isfile(denyFollowsFilename):
  407. if approveHandle in open(denyFollowsFilename).read():
  408. removeFromFollowRequests(baseDir,nicknameToFollow,domainToFollow,approveHandle,debug)
  409. print(approveHandle+' was already denied as a follower of '+nicknameToFollow)
  410. return True
  411. # add to a file which contains a list of requests
  412. approveFollowsFilename=accountsDir+'/followrequests.txt'
  413. if os.path.isfile(approveFollowsFilename):
  414. if approveHandle not in open(approveFollowsFilename).read():
  415. with open(approveFollowsFilename, "a") as fp:
  416. fp.write(approveHandle+'\n')
  417. else:
  418. if debug:
  419. print('DEBUG: '+approveHandle+' is already awaiting approval')
  420. else:
  421. with open(approveFollowsFilename, "w+") as fp:
  422. fp.write(approveHandle+'\n')
  423. # store the follow request in its own directory
  424. # We don't rely upon the inbox because items in there could expire
  425. requestsDir=accountsDir+'/requests'
  426. if not os.path.isdir(requestsDir):
  427. os.mkdir(requestsDir)
  428. followActivityfilename=requestsDir+'/'+approveHandle+'.follow'
  429. return saveJson(followJson,followActivityfilename)
  430. def receiveFollowRequest(session,baseDir: str,httpPrefix: str, \
  431. port: int,sendThreads: [],postLog: [], \
  432. cachedWebfingers: {},personCache: {}, \
  433. messageJson: {},federationList: [], \
  434. debug : bool,projectVersion: str, \
  435. acceptedCaps=["inbox:write","objects:read"]) -> bool:
  436. """Receives a follow request within the POST section of HTTPServer
  437. """
  438. if not messageJson['type'].startswith('Follow'):
  439. return False
  440. print('Receiving follow request')
  441. if not messageJson.get('actor'):
  442. if debug:
  443. print('DEBUG: follow request has no actor')
  444. return False
  445. if '/users/' not in messageJson['actor'] and \
  446. '/channel/' not in messageJson['actor'] and \
  447. '/profile/' not in messageJson['actor']:
  448. if debug:
  449. print('DEBUG: "users" or "profile" missing from actor')
  450. return False
  451. domain,tempPort=getDomainFromActor(messageJson['actor'])
  452. fromPort=port
  453. domainFull=domain
  454. if tempPort:
  455. fromPort=tempPort
  456. if tempPort!=80 and tempPort!=443:
  457. if ':' not in domain:
  458. domainFull=domain+':'+str(tempPort)
  459. if not domainPermitted(domain,federationList):
  460. if debug:
  461. print('DEBUG: follower from domain not permitted - '+domain)
  462. return False
  463. nickname=getNicknameFromActor(messageJson['actor'])
  464. if not nickname:
  465. # single user instance
  466. nickname='dev'
  467. if debug:
  468. print('DEBUG: follow request does not contain a nickname. Assuming single user instance.')
  469. if not messageJson.get('to'):
  470. messageJson['to']=messageJson['object']
  471. handle=nickname.lower()+'@'+domain.lower()
  472. if '/users/' not in messageJson['object'] and \
  473. '/channel/' not in messageJson['object'] and \
  474. '/profile/' not in messageJson['object']:
  475. if debug:
  476. print('DEBUG: "users" or "profile" not found within object')
  477. return False
  478. domainToFollow,tempPort=getDomainFromActor(messageJson['object'])
  479. if not domainPermitted(domainToFollow,federationList):
  480. if debug:
  481. print('DEBUG: follow domain not permitted '+domainToFollow)
  482. return True
  483. domainToFollowFull=domainToFollow
  484. if tempPort:
  485. if tempPort!=80 and tempPort!=443:
  486. if ':' not in domainToFollow:
  487. domainToFollowFull=domainToFollow+':'+str(tempPort)
  488. nicknameToFollow=getNicknameFromActor(messageJson['object'])
  489. if not nicknameToFollow:
  490. if debug:
  491. print('DEBUG: follow request does not contain a nickname for the account followed')
  492. return True
  493. handleToFollow=nicknameToFollow+'@'+domainToFollow
  494. if domainToFollow==domain:
  495. if not os.path.isdir(baseDir+'/accounts/'+handleToFollow):
  496. if debug:
  497. print('DEBUG: followed account not found - '+ \
  498. baseDir+'/accounts/'+handleToFollow)
  499. return True
  500. if isFollowerOfPerson(baseDir, \
  501. nicknameToFollow,domainToFollowFull, \
  502. nickname,domainFull):
  503. if debug:
  504. print('DEBUG: '+nickname+'@'+domain+ \
  505. ' is already a follower of '+ \
  506. nicknameToFollow+'@'+domainToFollow)
  507. return True
  508. # what is the followers policy?
  509. approveHandle=nickname+'@'+domainFull
  510. if followApprovalRequired(baseDir,nicknameToFollow, \
  511. domainToFollow,debug,approveHandle):
  512. if not domain.endswith('.onion'):
  513. if noOfFollowRequests(baseDir, \
  514. nicknameToFollow,domainToFollow, \
  515. nickname,domain,fromPort, \
  516. '') > 10:
  517. print('Too many follow requests')
  518. return False
  519. else:
  520. if noOfFollowRequests(baseDir, \
  521. nicknameToFollow,domainToFollow, \
  522. nickname,domain,fromPort, \
  523. 'onion') > 5:
  524. print('Too many follow requests from onion addresses')
  525. return False
  526. print('Storing follow request for approval')
  527. return storeFollowRequest(baseDir, \
  528. nicknameToFollow,domainToFollow,port, \
  529. nickname,domain,fromPort,
  530. messageJson,debug)
  531. else:
  532. print('Follow request does not require approval')
  533. # update the followers
  534. if os.path.isdir(baseDir+'/accounts/'+nicknameToFollow+'@'+domainToFollow):
  535. followersFilename=baseDir+'/accounts/'+nicknameToFollow+'@'+domainToFollow+'/followers.txt'
  536. print('Updating followers file: '+followersFilename+' adding '+approveHandle)
  537. if os.path.isfile(followersFilename):
  538. if approveHandle not in open(followersFilename).read():
  539. try:
  540. with open(followersFilename, 'r+') as followersFile:
  541. content = followersFile.read()
  542. followersFile.seek(0, 0)
  543. followersFile.write(approveHandle+'\n'+content)
  544. except Exception as e:
  545. print('WARN: Failed to write entry to followers file '+str(e))
  546. else:
  547. followersFile=open(followersFilename, "w+")
  548. followersFile.write(approveHandle+'\n')
  549. followersFile.close()
  550. print('Beginning follow accept')
  551. return followedAccountAccepts(session,baseDir,httpPrefix, \
  552. nicknameToFollow,domainToFollow,port, \
  553. nickname,domain,fromPort, \
  554. messageJson['actor'],federationList,
  555. messageJson,acceptedCaps, \
  556. sendThreads,postLog, \
  557. cachedWebfingers,personCache, \
  558. debug,projectVersion,True)
  559. def followedAccountAccepts(session,baseDir: str,httpPrefix: str, \
  560. nicknameToFollow: str,domainToFollow: str,port: int, \
  561. nickname: str,domain: str,fromPort: int, \
  562. personUrl: str,federationList: [], \
  563. followJson: {},acceptedCaps: [], \
  564. sendThreads: [],postLog: [], \
  565. cachedWebfingers: {},personCache: {}, \
  566. debug: bool,projectVersion: str, \
  567. removeFollowActivity: bool):
  568. """The person receiving a follow request accepts the new follower
  569. and sends back an Accept activity
  570. """
  571. acceptHandle=nickname+'@'+domain
  572. # send accept back
  573. if debug:
  574. print('DEBUG: sending Accept activity for follow request which arrived at '+ \
  575. nicknameToFollow+'@'+domainToFollow+' back to '+acceptHandle)
  576. acceptJson=createAccept(baseDir,federationList, \
  577. nicknameToFollow,domainToFollow,port, \
  578. personUrl,'',httpPrefix,followJson,acceptedCaps)
  579. if debug:
  580. pprint(acceptJson)
  581. print('DEBUG: sending follow Accept from '+ \
  582. nicknameToFollow+'@'+domainToFollow+ \
  583. ' port '+str(port)+' to '+ \
  584. acceptHandle+' port '+ str(fromPort))
  585. clientToServer=False
  586. if removeFollowActivity:
  587. # remove the follow request json
  588. followActivityfilename= \
  589. baseDir+'/accounts/'+ \
  590. nicknameToFollow+'@'+domainToFollow+'/requests/'+ \
  591. nickname+'@'+domain+'.follow'
  592. if os.path.isfile(followActivityfilename):
  593. try:
  594. os.remove(followActivityfilename)
  595. except:
  596. pass
  597. return sendSignedJson(acceptJson,session,baseDir, \
  598. nicknameToFollow,domainToFollow,port, \
  599. nickname,domain,fromPort, '', \
  600. httpPrefix,True,clientToServer, \
  601. federationList, \
  602. sendThreads,postLog,cachedWebfingers, \
  603. personCache,debug,projectVersion)
  604. def followedAccountRejects(session,baseDir: str,httpPrefix: str, \
  605. nicknameToFollow: str,domainToFollow: str,port: int, \
  606. nickname: str,domain: str,fromPort: int, \
  607. federationList: [], \
  608. sendThreads: [],postLog: [], \
  609. cachedWebfingers: {},personCache: {}, \
  610. debug: bool,projectVersion: str):
  611. """The person receiving a follow request rejects the new follower
  612. and sends back a Reject activity
  613. """
  614. # send reject back
  615. if debug:
  616. print('DEBUG: sending Reject activity for follow request which arrived at '+ \
  617. nicknameToFollow+'@'+domainToFollow+' back to '+nickname+'@'+domain)
  618. # get the json for the original follow request
  619. followActivityfilename= \
  620. baseDir+'/accounts/'+ \
  621. nicknameToFollow+'@'+domainToFollow+'/requests/'+ \
  622. nickname+'@'+domain+'.follow'
  623. followJson=loadJson(followActivityfilename)
  624. if not followJson:
  625. print('No follow request json was found for '+ \
  626. followActivityfilename)
  627. return None
  628. # actor who made the follow request
  629. personUrl=followJson['actor']
  630. # create the reject activity
  631. rejectJson= \
  632. createReject(baseDir,federationList, \
  633. nicknameToFollow,domainToFollow,port, \
  634. personUrl,'',httpPrefix,followJson)
  635. if debug:
  636. pprint(rejectJson)
  637. print('DEBUG: sending follow Reject from '+ \
  638. nicknameToFollow+'@'+domainToFollow+ \
  639. ' port '+str(port)+' to '+ \
  640. nickname+'@'+domain+' port '+ str(fromPort))
  641. clientToServer=False
  642. denyHandle=nickname+'@'+domain
  643. if fromPort:
  644. if fromPort!=80 and fromPort!=443:
  645. denyHandle=denyHandle+':'+str(fromPort)
  646. # remove from the follow requests file
  647. removeFromFollowRequests(baseDir,nicknameToFollow,domainToFollow,denyHandle,debug)
  648. # remove the follow request json
  649. try:
  650. os.remove(followActivityfilename)
  651. except:
  652. pass
  653. # send the reject activity
  654. return sendSignedJson(rejectJson,session,baseDir, \
  655. nicknameToFollow,domainToFollow,port, \
  656. nickname,domain,fromPort, '', \
  657. httpPrefix,True,clientToServer, \
  658. federationList, \
  659. sendThreads,postLog,cachedWebfingers, \
  660. personCache,debug,projectVersion)
  661. def sendFollowRequest(session,baseDir: str, \
  662. nickname: str,domain: str,port: int,httpPrefix: str, \
  663. followNickname: str,followDomain: str, \
  664. followPort: int,followHttpPrefix: str, \
  665. clientToServer: bool,federationList: [], \
  666. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  667. personCache: {},debug : bool, \
  668. projectVersion: str) -> {}:
  669. """Gets the json object for sending a follow request
  670. """
  671. if not domainPermitted(followDomain,federationList):
  672. return None
  673. fullDomain=domain
  674. followActor=httpPrefix+'://'+domain+'/users/'+nickname
  675. if port:
  676. if port!=80 and port!=443:
  677. if ':' not in domain:
  678. fullDomain=domain+':'+str(port)
  679. followActor=httpPrefix+'://'+domain+':'+str(port)+'/users/'+nickname
  680. requestDomain=followDomain
  681. if followPort:
  682. if followPort!=80 and followPort!=443:
  683. if ':' not in followDomain:
  684. requestDomain=followDomain+':'+str(followPort)
  685. statusNumber,published = getStatusNumber()
  686. if followNickname:
  687. followedId=followHttpPrefix+'://'+requestDomain+'/users/'+followNickname
  688. followHandle=followNickname+'@'+requestDomain
  689. else:
  690. if debug:
  691. print('DEBUG: sendFollowRequest - assuming single user instance')
  692. followedId=followHttpPrefix+'://'+requestDomain
  693. singleUserNickname='dev'
  694. followHandle=singleUserNickname+'@'+requestDomain
  695. newFollowJson = {
  696. '@context': 'https://www.w3.org/ns/activitystreams',
  697. 'id': followActor+'/statuses/'+str(statusNumber),
  698. 'type': 'Follow',
  699. 'actor': followActor,
  700. 'object': followedId
  701. }
  702. # Remove any follow requests rejected for the account being followed.
  703. # It's assumed that if you are following someone then you are
  704. # ok with them following back. If this isn't the case then a rejected
  705. # follow request will block them again.
  706. removeFromFollowRejects(baseDir, \
  707. nickname,domain, \
  708. followHandle,debug)
  709. sendSignedJson(newFollowJson,session,baseDir,nickname,domain,port, \
  710. followNickname,followDomain,followPort, \
  711. 'https://www.w3.org/ns/activitystreams#Public', \
  712. httpPrefix,True,clientToServer, \
  713. federationList, \
  714. sendThreads,postLog,cachedWebfingers,personCache, \
  715. debug,projectVersion)
  716. return newFollowJson
  717. def sendFollowRequestViaServer(baseDir: str,session, \
  718. fromNickname: str,password: str, \
  719. fromDomain: str,fromPort: int, \
  720. followNickname: str,followDomain: str,followPort: int, \
  721. httpPrefix: str, \
  722. cachedWebfingers: {},personCache: {}, \
  723. debug: bool,projectVersion: str) -> {}:
  724. """Creates a follow request via c2s
  725. """
  726. if not session:
  727. print('WARN: No session for sendFollowRequestViaServer')
  728. return 6
  729. fromDomainFull=fromDomain
  730. if fromPort:
  731. if fromPort!=80 and fromPort!=443:
  732. if ':' not in fromDomain:
  733. fromDomainFull=fromDomain+':'+str(fromPort)
  734. followDomainFull=followDomain
  735. if followPort:
  736. if followPort!=80 and followPort!=443:
  737. if ':' not in followDomain:
  738. followDomainFull=followDomain+':'+str(followPort)
  739. followActor=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname
  740. followedId=httpPrefix+'://'+followDomainFull+'/users/'+followNickname
  741. statusNumber,published = getStatusNumber()
  742. newFollowJson = {
  743. '@context': 'https://www.w3.org/ns/activitystreams',
  744. 'id': followActor+'/statuses/'+str(statusNumber),
  745. 'type': 'Follow',
  746. 'actor': followActor,
  747. 'object': followedId
  748. }
  749. handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
  750. # lookup the inbox for the To handle
  751. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  752. fromDomain,projectVersion)
  753. if not wfRequest:
  754. if debug:
  755. print('DEBUG: announce webfinger failed for '+handle)
  756. return 1
  757. postToBox='outbox'
  758. # get the actor inbox for the To handle
  759. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
  760. getPersonBox(baseDir,session,wfRequest,personCache, \
  761. projectVersion,httpPrefix,fromNickname, \
  762. fromDomain,postToBox)
  763. if not inboxUrl:
  764. if debug:
  765. print('DEBUG: No '+postToBox+' was found for '+handle)
  766. return 3
  767. if not fromPersonId:
  768. if debug:
  769. print('DEBUG: No actor was found for '+handle)
  770. return 4
  771. authHeader=createBasicAuthHeader(fromNickname,password)
  772. headers = {'host': fromDomain, \
  773. 'Content-type': 'application/json', \
  774. 'Authorization': authHeader}
  775. postResult = \
  776. postJson(session,newFollowJson,[],inboxUrl,headers,"inbox:write")
  777. #if not postResult:
  778. # if debug:
  779. # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
  780. # return 5
  781. if debug:
  782. print('DEBUG: c2s POST follow success')
  783. return newFollowJson
  784. def sendUnfollowRequestViaServer(baseDir: str,session, \
  785. fromNickname: str,password: str, \
  786. fromDomain: str,fromPort: int, \
  787. followNickname: str,followDomain: str,followPort: int, \
  788. httpPrefix: str, \
  789. cachedWebfingers: {},personCache: {}, \
  790. debug: bool,projectVersion: str) -> {}:
  791. """Creates a unfollow request via c2s
  792. """
  793. if not session:
  794. print('WARN: No session for sendUnfollowRequestViaServer')
  795. return 6
  796. fromDomainFull=fromDomain
  797. if fromPort:
  798. if fromPort!=80 and fromPort!=443:
  799. if ':' not in fromDomain:
  800. fromDomainFull=fromDomain+':'+str(fromPort)
  801. followDomainFull=followDomain
  802. if followPort:
  803. if followPort!=80 and followPort!=443:
  804. if ':' not in followDomain:
  805. followDomainFull=followDomain+':'+str(followPort)
  806. followActor=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname
  807. followedId=httpPrefix+'://'+followDomainFull+'/users/'+followNickname
  808. statusNumber,published = getStatusNumber()
  809. unfollowJson = {
  810. '@context': 'https://www.w3.org/ns/activitystreams',
  811. 'id': followActor+'/statuses/'+str(statusNumber)+'/undo',
  812. 'type': 'Undo',
  813. 'actor': followActor,
  814. 'object': {
  815. 'id': followActor+'/statuses/'+str(statusNumber),
  816. 'type': 'Follow',
  817. 'actor': followActor,
  818. 'object': followedId
  819. }
  820. }
  821. handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
  822. # lookup the inbox for the To handle
  823. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  824. fromDomain,projectVersion)
  825. if not wfRequest:
  826. if debug:
  827. print('DEBUG: announce webfinger failed for '+handle)
  828. return 1
  829. postToBox='outbox'
  830. # get the actor inbox for the To handle
  831. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
  832. getPersonBox(baseDir,session,wfRequest,personCache, \
  833. projectVersion,httpPrefix,fromNickname, \
  834. fromDomain,postToBox)
  835. if not inboxUrl:
  836. if debug:
  837. print('DEBUG: No '+postToBox+' was found for '+handle)
  838. return 3
  839. if not fromPersonId:
  840. if debug:
  841. print('DEBUG: No actor was found for '+handle)
  842. return 4
  843. authHeader=createBasicAuthHeader(fromNickname,password)
  844. headers = {'host': fromDomain, \
  845. 'Content-type': 'application/json', \
  846. 'Authorization': authHeader}
  847. postResult = \
  848. postJson(session,unfollowJson,[],inboxUrl,headers,"inbox:write")
  849. #if not postResult:
  850. # if debug:
  851. # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
  852. # return 5
  853. if debug:
  854. print('DEBUG: c2s POST unfollow success')
  855. return unfollowJson
  856. def getFollowersOfActor(baseDir :str,actor :str,debug: bool) -> {}:
  857. """In a shared inbox if we receive a post we know who it's from
  858. and if it's addressed to followers then we need to get a list of those.
  859. This returns a list of account handles which follow the given actor
  860. and also the corresponding capability id if it exists
  861. """
  862. if debug:
  863. print('DEBUG: getting followers of '+actor)
  864. recipientsDict={}
  865. if ':' not in actor:
  866. return recipientsDict
  867. httpPrefix=actor.split(':')[0]
  868. nickname=getNicknameFromActor(actor)
  869. if not nickname:
  870. if debug:
  871. print('DEBUG: no nickname found in '+actor)
  872. return recipientsDict
  873. domain,port=getDomainFromActor(actor)
  874. if not domain:
  875. if debug:
  876. print('DEBUG: no domain found in '+actor)
  877. return recipientsDict
  878. actorHandle=nickname+'@'+domain
  879. if debug:
  880. print('DEBUG: searching for handle '+actorHandle)
  881. # for each of the accounts
  882. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  883. for account in dirs:
  884. if '@' in account and not account.startswith('inbox@'):
  885. followingFilename = os.path.join(subdir, account)+'/following.txt'
  886. if debug:
  887. print('DEBUG: examining follows of '+account)
  888. print(followingFilename)
  889. if os.path.isfile(followingFilename):
  890. # does this account follow the given actor?
  891. if debug:
  892. print('DEBUG: checking if '+actorHandle+' in '+followingFilename)
  893. if actorHandle in open(followingFilename).read():
  894. if debug:
  895. print('DEBUG: '+account+' follows '+actorHandle)
  896. ocapFilename=baseDir+'/accounts/'+account+'/ocap/accept/'+httpPrefix+':##'+domain+':'+str(port)+'#users#'+nickname+'.json'
  897. if debug:
  898. print('DEBUG: checking capabilities of'+account)
  899. if os.path.isfile(ocapFilename):
  900. ocapJson=loadJson(ocapFilename)
  901. if ocapJson:
  902. if ocapJson.get('id'):
  903. if debug:
  904. print('DEBUG: capabilities id found for '+account)
  905. recipientsDict[account]=ocapJson['id']
  906. else:
  907. if debug:
  908. print('DEBUG: capabilities has no id attribute')
  909. recipientsDict[account]=None
  910. else:
  911. if debug:
  912. print('DEBUG: No capabilities file found for '+account+' granted by '+actorHandle)
  913. print(ocapFilename)
  914. recipientsDict[account]=None
  915. return recipientsDict
  916. def outboxUndoFollow(baseDir: str,messageJson: {},debug: bool) -> None:
  917. """When an unfollow request is received by the outbox from c2s
  918. This removes the followed handle from the following.txt file
  919. of the relevant account
  920. TODO the unfollow should also be sent to the previously followed account
  921. """
  922. if not messageJson.get('type'):
  923. return
  924. if not messageJson['type']=='Undo':
  925. return
  926. if not messageJson.get('object'):
  927. return
  928. if not isinstance(messageJson['object'], dict):
  929. return
  930. if not messageJson['object'].get('type'):
  931. return
  932. if not messageJson['object']['type']=='Follow':
  933. return
  934. if not messageJson['object'].get('object'):
  935. return
  936. if not messageJson['object'].get('actor'):
  937. return
  938. if not isinstance(messageJson['object']['object'], str):
  939. return
  940. if debug:
  941. print('DEBUG: undo follow arrived in outbox')
  942. nicknameFollower=getNicknameFromActor(messageJson['object']['actor'])
  943. if not nicknameFollower:
  944. print('WARN: unable to find nickname in '+messageJson['object']['actor'])
  945. return
  946. domainFollower,portFollower=getDomainFromActor(messageJson['object']['actor'])
  947. domainFollowerFull=domainFollower
  948. if portFollower:
  949. if portFollower!=80 and portFollower!=443:
  950. if ':' not in domainFollower:
  951. domainFollowerFull=domainFollower+':'+str(portFollower)
  952. nicknameFollowing=getNicknameFromActor(messageJson['object']['object'])
  953. if not nicknameFollowing:
  954. print('WARN: unable to find nickname in '+messageJson['object']['object'])
  955. return
  956. domainFollowing,portFollowing=getDomainFromActor(messageJson['object']['object'])
  957. domainFollowingFull=domainFollowing
  958. if portFollowing:
  959. if portFollowing!=80 and portFollowing!=443:
  960. if ':' not in domainFollowing:
  961. domainFollowingFull=domainFollowing+':'+str(portFollowing)
  962. if unfollowPerson(baseDir,nicknameFollower,domainFollowerFull, \
  963. nicknameFollowing,domainFollowingFull):
  964. if debug:
  965. print('DEBUG: '+nicknameFollower+' unfollowed '+nicknameFollowing+'@'+domainFollowingFull)
  966. else:
  967. if debug:
  968. print('WARN: '+nicknameFollower+' could not unfollow '+nicknameFollowing+'@'+domainFollowingFull)