follow.py 38 KB

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