follow.py 38 KB

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