follow.py 31 KB

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