follow.py 47 KB

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