follow.py 35 KB

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