inbox.py 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379
  1. __filename__ = "inbox.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 os
  10. import datetime
  11. import time
  12. import json
  13. import commentjson
  14. from shutil import copyfile
  15. from utils import urlPermitted
  16. from utils import createInboxQueueDir
  17. from utils import getStatusNumber
  18. from utils import getDomainFromActor
  19. from utils import getNicknameFromActor
  20. from utils import domainPermitted
  21. from utils import locatePost
  22. from utils import deletePost
  23. from utils import removeAttachment
  24. from utils import removeModerationPostFromIndex
  25. from httpsig import verifyPostHeaders
  26. from session import createSession
  27. from session import getJson
  28. from follow import receiveFollowRequest
  29. from follow import getFollowersOfActor
  30. from follow import unfollowerOfPerson
  31. from pprint import pprint
  32. from cache import getPersonFromCache
  33. from cache import storePersonInCache
  34. from acceptreject import receiveAcceptReject
  35. from capabilities import getOcapFilename
  36. from capabilities import CapablePost
  37. from capabilities import capabilitiesReceiveUpdate
  38. from like import updateLikesCollection
  39. from like import undoLikesCollectionEntry
  40. from blocking import isBlocked
  41. from filters import isFiltered
  42. from announce import updateAnnounceCollection
  43. from httpsig import messageContentDigest
  44. def validInbox(baseDir: str,nickname: str,domain: str) -> bool:
  45. """Checks whether files were correctly saved to the inbox
  46. """
  47. if ':' in domain:
  48. domain=domain.split(':')[0]
  49. inboxDir=baseDir+'/accounts/'+nickname+'@'+domain+'/inbox'
  50. if not os.path.isdir(inboxDir):
  51. return True
  52. for subdir, dirs, files in os.walk(inboxDir):
  53. for f in files:
  54. filename = os.path.join(subdir, f)
  55. if not os.path.isfile(filename):
  56. print('filename: '+filename)
  57. return False
  58. if 'postNickname' in open(filename).read():
  59. print('queue file incorrectly saved to '+filename)
  60. return False
  61. return True
  62. def validInboxFilenames(baseDir: str,nickname: str,domain: str, \
  63. expectedDomain: str,expectedPort: int) -> bool:
  64. """Used by unit tests to check that the port number gets appended to
  65. domain names within saved post filenames
  66. """
  67. if ':' in domain:
  68. domain=domain.split(':')[0]
  69. inboxDir=baseDir+'/accounts/'+nickname+'@'+domain+'/inbox'
  70. if not os.path.isdir(inboxDir):
  71. return True
  72. expectedStr=expectedDomain+':'+str(expectedPort)
  73. for subdir, dirs, files in os.walk(inboxDir):
  74. for f in files:
  75. filename = os.path.join(subdir, f)
  76. if not os.path.isfile(filename):
  77. print('filename: '+filename)
  78. return False
  79. if not expectedStr in filename:
  80. print('Expected: '+expectedStr)
  81. print('Invalid filename: '+filename)
  82. return False
  83. return True
  84. def getPersonPubKey(session,personUrl: str,personCache: {},debug: bool, \
  85. projectVersion: str,httpPrefix: str,domain: str) -> str:
  86. if not personUrl:
  87. return None
  88. personUrl=personUrl.replace('#main-key','')
  89. if personUrl.endswith('/users/inbox'):
  90. if debug:
  91. print('DEBUG: Obtaining public key for shared inbox')
  92. personUrl=personUrl.replace('/users/inbox','/inbox')
  93. personJson = getPersonFromCache(personUrl,personCache)
  94. if not personJson:
  95. if debug:
  96. print('DEBUG: Obtaining public key for '+personUrl)
  97. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  98. personJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  99. if not personJson:
  100. return None
  101. pubKey=None
  102. if personJson.get('publicKey'):
  103. if personJson['publicKey'].get('publicKeyPem'):
  104. pubKey=personJson['publicKey']['publicKeyPem']
  105. else:
  106. if personJson.get('publicKeyPem'):
  107. pubKey=personJson['publicKeyPem']
  108. if not pubKey:
  109. if debug:
  110. print('DEBUG: Public key not found for '+personUrl)
  111. storePersonInCache(personUrl,personJson,personCache)
  112. return pubKey
  113. def inboxMessageHasParams(messageJson: {}) -> bool:
  114. """Checks whether an incoming message contains expected parameters
  115. """
  116. expectedParams=['type','actor','object']
  117. for param in expectedParams:
  118. if not messageJson.get(param):
  119. return False
  120. if not messageJson.get('to'):
  121. allowedWithoutToParam=['Like','Follow','Request','Accept','Capability','Undo']
  122. if messageJson['type'] not in allowedWithoutToParam:
  123. return False
  124. return True
  125. def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> bool:
  126. """ check that we are receiving from a permitted domain
  127. """
  128. testParam='actor'
  129. if not messageJson.get(testParam):
  130. return False
  131. actor=messageJson[testParam]
  132. # always allow the local domain
  133. if domain in actor:
  134. return True
  135. if not urlPermitted(actor,federationList,"inbox:write"):
  136. return False
  137. if messageJson['type']!='Follow' and \
  138. messageJson['type']!='Like' and \
  139. messageJson['type']!='Delete' and \
  140. messageJson['type']!='Announce':
  141. if messageJson.get('object'):
  142. if not isinstance(messageJson['object'], dict):
  143. return False
  144. if messageJson['object'].get('inReplyTo'):
  145. inReplyTo=messageJson['object']['inReplyTo']
  146. if not urlPermitted(inReplyTo,federationList,"inbox:write"):
  147. return False
  148. return True
  149. def validPublishedDate(published) -> bool:
  150. currTime=datetime.datetime.utcnow()
  151. pubDate=datetime.datetime.strptime(published,"%Y-%m-%dT%H:%M:%SZ")
  152. daysSincePublished = (currTime - pubTime).days
  153. if daysSincePublished>30:
  154. return False
  155. return True
  156. def savePostToInboxQueue(baseDir: str,httpPrefix: str, \
  157. nickname: str, domain: str, \
  158. postJsonObject: {}, \
  159. messageBytes: str, \
  160. httpHeaders: {}, \
  161. postPath: str,debug: bool) -> str:
  162. """Saves the give json to the inbox queue for the person
  163. keyId specifies the actor sending the post
  164. """
  165. originalDomain=domain
  166. if ':' in domain:
  167. domain=domain.split(':')[0]
  168. # block at the ealiest stage possible, which means the data
  169. # isn't written to file
  170. postNickname=None
  171. postDomain=None
  172. actor=None
  173. if postJsonObject.get('actor'):
  174. actor=postJsonObject['actor']
  175. postNickname=getNicknameFromActor(postJsonObject['actor'])
  176. postDomain,postPort=getDomainFromActor(postJsonObject['actor'])
  177. if isBlocked(baseDir,nickname,domain,postNickname,postDomain):
  178. if debug:
  179. print('DEBUG: post from '+postNickname+' blocked')
  180. return None
  181. if postPort:
  182. if postPort!=80 and postPort!=443:
  183. if ':' not in postDomain:
  184. postDomain=postDomain+':'+str(postPort)
  185. if postJsonObject.get('object'):
  186. if isinstance(postJsonObject['object'], dict):
  187. if postJsonObject['object'].get('content'):
  188. if isinstance(postJsonObject['object']['content'], str):
  189. if isFiltered(baseDir,nickname,domain,postJsonObject['object']['content']):
  190. if debug:
  191. print('DEBUG: post was filtered out due to content')
  192. return None
  193. originalPostId=None
  194. if postJsonObject.get('id'):
  195. originalPostId=postJsonObject['id'].replace('/activity','').replace('/undo','')
  196. currTime=datetime.datetime.utcnow()
  197. postId=None
  198. if postJsonObject.get('id'):
  199. #if '/statuses/' not in postJsonObject['id']:
  200. postId=postJsonObject['id'].replace('/activity','').replace('/undo','')
  201. published=currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
  202. if not postId:
  203. statusNumber,published = getStatusNumber()
  204. if actor:
  205. postId=actor+'/statuses/'+statusNumber
  206. else:
  207. postId=httpPrefix+'://'+originalDomain+'/users/'+nickname+'/statuses/'+statusNumber
  208. # NOTE: don't change postJsonObject['id'] before signature check
  209. inboxQueueDir=createInboxQueueDir(nickname,domain,baseDir)
  210. handle=nickname+'@'+domain
  211. destination=baseDir+'/accounts/'+handle+'/inbox/'+postId.replace('/','#')+'.json'
  212. #if os.path.isfile(destination):
  213. # if debug:
  214. # print(destination)
  215. # print('DEBUG: inbox item already exists')
  216. # return None
  217. filename=inboxQueueDir+'/'+postId.replace('/','#')+'.json'
  218. sharedInboxItem=False
  219. if nickname=='inbox':
  220. sharedInboxItem=True
  221. newQueueItem = {
  222. 'originalId': originalPostId,
  223. 'id': postId,
  224. 'actor': actor,
  225. 'nickname': nickname,
  226. 'domain': domain,
  227. 'postNickname': postNickname,
  228. 'postDomain': postDomain,
  229. 'sharedInbox': sharedInboxItem,
  230. 'published': published,
  231. 'httpHeaders': httpHeaders,
  232. 'path': postPath,
  233. 'post': postJsonObject,
  234. 'digest': messageContentDigest(messageBytes),
  235. 'filename': filename,
  236. 'destination': destination
  237. }
  238. if debug:
  239. print('Inbox queue item created')
  240. pprint(newQueueItem)
  241. with open(filename, 'w') as fp:
  242. commentjson.dump(newQueueItem, fp, indent=4, sort_keys=False)
  243. return filename
  244. def inboxCheckCapabilities(baseDir :str,nickname :str,domain :str, \
  245. actor: str,queue: [],queueJson: {}, \
  246. capabilityId: str,debug : bool) -> bool:
  247. if nickname=='inbox':
  248. return True
  249. ocapFilename= \
  250. getOcapFilename(baseDir, \
  251. queueJson['nickname'],queueJson['domain'], \
  252. actor,'accept')
  253. if not ocapFilename:
  254. return False
  255. if not os.path.isfile(ocapFilename):
  256. if debug:
  257. print('DEBUG: capabilities for '+ \
  258. actor+' do not exist')
  259. os.remove(queueFilename)
  260. queue.pop(0)
  261. return False
  262. with open(ocapFilename, 'r') as fp:
  263. oc=commentjson.load(fp)
  264. if not oc.get('id'):
  265. if debug:
  266. print('DEBUG: capabilities for '+actor+' do not contain an id')
  267. os.remove(queueFilename)
  268. queue.pop(0)
  269. return False
  270. if oc['id']!=capabilityId:
  271. if debug:
  272. print('DEBUG: capability id mismatch')
  273. os.remove(queueFilename)
  274. queue.pop(0)
  275. return False
  276. if not oc.get('capability'):
  277. if debug:
  278. print('DEBUG: missing capability list')
  279. os.remove(queueFilename)
  280. queue.pop(0)
  281. return False
  282. if not CapablePost(queueJson['post'],oc['capability'],debug):
  283. if debug:
  284. print('DEBUG: insufficient capabilities to write to inbox from '+actor)
  285. os.remove(queueFilename)
  286. queue.pop(0)
  287. return False
  288. if debug:
  289. print('DEBUG: object capabilities check success')
  290. return True
  291. def inboxPostRecipientsAdd(baseDir :str,httpPrefix :str,toList :[], \
  292. recipientsDict :{}, \
  293. domainMatch: str,domain :str, \
  294. actor :str,debug: bool) -> bool:
  295. """Given a list of post recipients (toList) from 'to' or 'cc' parameters
  296. populate a recipientsDict with the handle and capabilities id for each
  297. """
  298. followerRecipients=False
  299. for recipient in toList:
  300. # is this a to a local account?
  301. if domainMatch in recipient:
  302. # get the handle for the local account
  303. nickname=recipient.split(domainMatch)[1]
  304. handle=nickname+'@'+domain
  305. if os.path.isdir(baseDir+'/accounts/'+handle):
  306. # are capabilities granted for this account to the
  307. # sender (actor) of the post?
  308. ocapFilename=baseDir+'/accounts/'+handle+'/ocap/accept/'+actor.replace('/','#')+'.json'
  309. if os.path.isfile(ocapFilename):
  310. # read the granted capabilities and obtain the id
  311. with open(ocapFilename, 'r') as fp:
  312. ocapJson=commentjson.load(fp)
  313. if ocapJson.get('id'):
  314. # append with the capabilities id
  315. recipientsDict[handle]=ocapJson['id']
  316. else:
  317. recipientsDict[handle]=None
  318. else:
  319. if debug:
  320. print('DEBUG: '+ocapFilename+' not found')
  321. recipientsDict[handle]=None
  322. else:
  323. if debug:
  324. print('DEBUG: '+baseDir+'/accounts/'+handle+' does not exist')
  325. else:
  326. if debug:
  327. print('DEBUG: '+recipient+' is not local to '+domainMatch)
  328. print(str(toList))
  329. if recipient.endswith('followers'):
  330. if debug:
  331. print('DEBUG: followers detected as post recipients')
  332. followerRecipients=True
  333. return followerRecipients,recipientsDict
  334. def inboxPostRecipients(baseDir :str,postJsonObject :{},httpPrefix :str,domain : str,port :int, debug :bool) -> ([],[]):
  335. """Returns dictionaries containing the recipients of the given post
  336. The shared dictionary contains followers
  337. """
  338. recipientsDict={}
  339. recipientsDictFollowers={}
  340. if not postJsonObject.get('actor'):
  341. if debug:
  342. pprint(postJsonObject)
  343. print('WARNING: inbox post has no actor')
  344. return recipientsDict,recipientsDictFollowers
  345. if ':' in domain:
  346. domain=domain.split(':')[0]
  347. domainBase=domain
  348. if port:
  349. if port!=80 and port!=443:
  350. if ':' not in domain:
  351. domain=domain+':'+str(port)
  352. domainMatch='/'+domain+'/users/'
  353. actor = postJsonObject['actor']
  354. # first get any specific people which the post is addressed to
  355. followerRecipients=False
  356. if postJsonObject.get('object'):
  357. if isinstance(postJsonObject['object'], dict):
  358. if postJsonObject['object'].get('to'):
  359. if isinstance(postJsonObject['object']['to'], list):
  360. recipientsList=postJsonObject['object']['to']
  361. else:
  362. recipientsList=[postJsonObject['object']['to']]
  363. if debug:
  364. print('DEBUG: resolving "to"')
  365. includesFollowers,recipientsDict= \
  366. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  367. recipientsList, \
  368. recipientsDict, \
  369. domainMatch,domainBase, \
  370. actor,debug)
  371. if includesFollowers:
  372. followerRecipients=True
  373. else:
  374. if debug:
  375. print('DEBUG: inbox post has no "to"')
  376. if postJsonObject['object'].get('cc'):
  377. if isinstance(postJsonObject['object']['cc'], list):
  378. recipientsList=postJsonObject['object']['cc']
  379. else:
  380. recipientsList=[postJsonObject['object']['cc']]
  381. includesFollowers,recipientsDict= \
  382. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  383. recipientsList, \
  384. recipientsDict, \
  385. domainMatch,domainBase, \
  386. actor,debug)
  387. if includesFollowers:
  388. followerRecipients=True
  389. else:
  390. if debug:
  391. print('DEBUG: inbox post has no cc')
  392. else:
  393. if debug:
  394. if isinstance(postJsonObject['object'], str):
  395. if '/statuses/' in postJsonObject['object']:
  396. print('DEBUG: inbox item is a link to a post')
  397. else:
  398. if '/users/' in postJsonObject['object']:
  399. print('DEBUG: inbox item is a link to an actor')
  400. if postJsonObject.get('to'):
  401. if isinstance(postJsonObject['to'], list):
  402. recipientsList=postJsonObject['to']
  403. else:
  404. recipientsList=[postJsonObject['to']]
  405. includesFollowers,recipientsDict= \
  406. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  407. recipientsList, \
  408. recipientsDict, \
  409. domainMatch,domainBase, \
  410. actor,debug)
  411. if includesFollowers:
  412. followerRecipients=True
  413. if postJsonObject.get('cc'):
  414. if isinstance(postJsonObject['cc'], list):
  415. recipientsList=postJsonObject['cc']
  416. else:
  417. recipientsList=[postJsonObject['cc']]
  418. includesFollowers,recipientsDict= \
  419. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  420. recipientsList, \
  421. recipientsDict, \
  422. domainMatch,domainBase, \
  423. actor,debug)
  424. if includesFollowers:
  425. followerRecipients=True
  426. if not followerRecipients:
  427. if debug:
  428. print('DEBUG: no followers were resolved')
  429. return recipientsDict,recipientsDictFollowers
  430. # now resolve the followers
  431. recipientsDictFollowers= \
  432. getFollowersOfActor(baseDir,actor,debug)
  433. return recipientsDict,recipientsDictFollowers
  434. def receiveUndoFollow(session,baseDir: str,httpPrefix: str, \
  435. port: int,messageJson: {}, \
  436. federationList: [], \
  437. debug : bool) -> bool:
  438. if not messageJson['object'].get('actor'):
  439. if debug:
  440. print('DEBUG: follow request has no actor within object')
  441. return False
  442. if '/users/' not in messageJson['object']['actor']:
  443. if debug:
  444. print('DEBUG: "users" missing from actor within object')
  445. return False
  446. if messageJson['object']['actor'] != messageJson['actor']:
  447. if debug:
  448. print('DEBUG: actors do not match')
  449. return False
  450. nicknameFollower=getNicknameFromActor(messageJson['object']['actor'])
  451. domainFollower,portFollower=getDomainFromActor(messageJson['object']['actor'])
  452. domainFollowerFull=domainFollower
  453. if portFollower:
  454. if portFollower!=80 and portFollower!=443:
  455. if ':' not in domainFollower:
  456. domainFollowerFull=domainFollower+':'+str(portFollower)
  457. nicknameFollowing=getNicknameFromActor(messageJson['object']['object'])
  458. domainFollowing,portFollowing=getDomainFromActor(messageJson['object']['object'])
  459. domainFollowingFull=domainFollowing
  460. if portFollowing:
  461. if portFollowing!=80 and portFollowing!=443:
  462. if ':' not in domainFollowing:
  463. domainFollowingFull=domainFollowing+':'+str(portFollowing)
  464. if unfollowerOfPerson(baseDir, \
  465. nicknameFollowing,domainFollowingFull, \
  466. nicknameFollower,domainFollowerFull, \
  467. debug):
  468. if debug:
  469. print('DEBUG: Follower '+nicknameFollower+'@'+domainFollowerFull+' was removed')
  470. return True
  471. if debug:
  472. print('DEBUG: Follower '+nicknameFollower+'@'+domainFollowerFull+' was not removed')
  473. return False
  474. def receiveUndo(session,baseDir: str,httpPrefix: str, \
  475. port: int,sendThreads: [],postLog: [], \
  476. cachedWebfingers: {},personCache: {}, \
  477. messageJson: {},federationList: [], \
  478. debug : bool, \
  479. acceptedCaps=["inbox:write","objects:read"]) -> bool:
  480. """Receives an undo request within the POST section of HTTPServer
  481. """
  482. if not messageJson['type'].startswith('Undo'):
  483. return False
  484. if debug:
  485. print('DEBUG: Undo activity received')
  486. if not messageJson.get('actor'):
  487. if debug:
  488. print('DEBUG: follow request has no actor')
  489. return False
  490. if '/users/' not in messageJson['actor']:
  491. if debug:
  492. print('DEBUG: "users" missing from actor')
  493. return False
  494. if not messageJson.get('object'):
  495. if debug:
  496. print('DEBUG: '+messageJson['type']+' has no object')
  497. return False
  498. if not isinstance(messageJson['object'], dict):
  499. if debug:
  500. print('DEBUG: '+messageJson['type']+' object is not a dict')
  501. return False
  502. if not messageJson['object'].get('type'):
  503. if debug:
  504. print('DEBUG: '+messageJson['type']+' has no object type')
  505. return False
  506. if not messageJson['object'].get('object'):
  507. if debug:
  508. print('DEBUG: '+messageJson['type']+' has no object within object')
  509. return False
  510. if not isinstance(messageJson['object']['object'], str):
  511. if debug:
  512. print('DEBUG: '+messageJson['type']+' object within object is not a string')
  513. return False
  514. if messageJson['object']['type']=='Follow':
  515. return receiveUndoFollow(session,baseDir,httpPrefix, \
  516. port,messageJson, \
  517. federationList, \
  518. debug)
  519. return False
  520. def receiveUpdate(session,baseDir: str, \
  521. httpPrefix: str,domain :str,port: int, \
  522. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  523. personCache: {},messageJson: {},federationList: [], \
  524. debug : bool) -> bool:
  525. """Receives an Update activity within the POST section of HTTPServer
  526. """
  527. if messageJson['type']!='Update':
  528. return False
  529. if not messageJson.get('actor'):
  530. if debug:
  531. print('DEBUG: '+messageJson['type']+' has no actor')
  532. return False
  533. if not messageJson.get('object'):
  534. if debug:
  535. print('DEBUG: '+messageJson['type']+' has no object')
  536. return False
  537. if not isinstance(messageJson['object'], dict):
  538. if debug:
  539. print('DEBUG: '+messageJson['type']+' object is not a dict')
  540. return False
  541. if not messageJson['object'].get('type'):
  542. if debug:
  543. print('DEBUG: '+messageJson['type']+' object has no type')
  544. return False
  545. if '/users/' not in messageJson['actor']:
  546. if debug:
  547. print('DEBUG: "users" missing from actor in '+messageJson['type'])
  548. return False
  549. if messageJson['object'].get('capability') and messageJson['object'].get('scope'):
  550. domain,tempPort=getDomainFromActor(messageJson['object']['scope'])
  551. nickname=getNicknameFromActor(messageJson['object']['scope'])
  552. if messageJson['object']['type']=='Capability':
  553. if capabilitiesReceiveUpdate(baseDir,nickname,domain,port,
  554. messageJson['actor'], \
  555. messageJson['object']['id'], \
  556. messageJson['object']['capability'], \
  557. debug):
  558. if debug:
  559. print('DEBUG: An update was received')
  560. return True
  561. return False
  562. def receiveLike(session,handle: str,baseDir: str, \
  563. httpPrefix: str,domain :str,port: int, \
  564. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  565. personCache: {},messageJson: {},federationList: [], \
  566. debug : bool) -> bool:
  567. """Receives a Like activity within the POST section of HTTPServer
  568. """
  569. if messageJson['type']!='Like':
  570. return False
  571. if not messageJson.get('actor'):
  572. if debug:
  573. print('DEBUG: '+messageJson['type']+' has no actor')
  574. return False
  575. if not messageJson.get('object'):
  576. if debug:
  577. print('DEBUG: '+messageJson['type']+' has no object')
  578. return False
  579. if not isinstance(messageJson['object'], str):
  580. if debug:
  581. print('DEBUG: '+messageJson['type']+' object is not a string')
  582. return False
  583. if not messageJson.get('to'):
  584. if debug:
  585. print('DEBUG: '+messageJson['type']+' has no "to" list')
  586. return False
  587. if '/users/' not in messageJson['actor']:
  588. if debug:
  589. print('DEBUG: "users" missing from actor in '+messageJson['type'])
  590. return False
  591. if '/statuses/' not in messageJson['object']:
  592. if debug:
  593. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  594. return False
  595. if not os.path.isdir(baseDir+'/accounts/'+handle):
  596. print('DEBUG: unknown recipient of like - '+handle)
  597. # if this post in the outbox of the person?
  598. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object'])
  599. if not postFilename:
  600. if debug:
  601. print('DEBUG: post not found in inbox or outbox')
  602. print(messageJson['object'])
  603. return True
  604. if debug:
  605. print('DEBUG: liked post found in inbox')
  606. updateLikesCollection(postFilename,messageJson['object'],messageJson['actor'],debug)
  607. return True
  608. def receiveUndoLike(session,handle: str,baseDir: str, \
  609. httpPrefix: str,domain :str,port: int, \
  610. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  611. personCache: {},messageJson: {},federationList: [], \
  612. debug : bool) -> bool:
  613. """Receives an undo like activity within the POST section of HTTPServer
  614. """
  615. if messageJson['type']!='Undo':
  616. return False
  617. if not messageJson.get('actor'):
  618. return False
  619. if not messageJson.get('object'):
  620. return False
  621. if not isinstance(messageJson['object'], dict):
  622. return False
  623. if not messageJson['object'].get('type'):
  624. return False
  625. if messageJson['object']['type']!='Like':
  626. return False
  627. if not messageJson['object'].get('object'):
  628. if debug:
  629. print('DEBUG: '+messageJson['type']+' like has no object')
  630. return False
  631. if not isinstance(messageJson['object']['object'], str):
  632. if debug:
  633. print('DEBUG: '+messageJson['type']+' like object is not a string')
  634. return False
  635. if '/users/' not in messageJson['actor']:
  636. if debug:
  637. print('DEBUG: "users" missing from actor in '+messageJson['type']+' like')
  638. return False
  639. if '/statuses/' not in messageJson['object']['object']:
  640. if debug:
  641. print('DEBUG: "statuses" missing from like object in '+messageJson['type'])
  642. return False
  643. if not os.path.isdir(baseDir+'/accounts/'+handle):
  644. print('DEBUG: unknown recipient of undo like - '+handle)
  645. # if this post in the outbox of the person?
  646. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object']['object'])
  647. if not postFilename:
  648. if debug:
  649. print('DEBUG: unliked post not found in inbox or outbox')
  650. print(messageJson['object']['object'])
  651. return True
  652. if debug:
  653. print('DEBUG: liked post found in inbox. Now undoing.')
  654. undoLikesCollectionEntry(postFilename,messageJson['object'],messageJson['actor'],debug)
  655. return True
  656. def receiveDelete(session,handle: str,baseDir: str, \
  657. httpPrefix: str,domain :str,port: int, \
  658. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  659. personCache: {},messageJson: {},federationList: [], \
  660. debug : bool,allowDeletion: bool) -> bool:
  661. """Receives a Delete activity within the POST section of HTTPServer
  662. """
  663. if messageJson['type']!='Delete':
  664. return False
  665. if not messageJson.get('actor'):
  666. if debug:
  667. print('DEBUG: '+messageJson['type']+' has no actor')
  668. return False
  669. if debug:
  670. print('DEBUG: Delete activity arrived')
  671. if not messageJson.get('object'):
  672. if debug:
  673. print('DEBUG: '+messageJson['type']+' has no object')
  674. return False
  675. if not isinstance(messageJson['object'], str):
  676. if debug:
  677. print('DEBUG: '+messageJson['type']+' object is not a string')
  678. return False
  679. domainFull=domain
  680. if port:
  681. if port!=80 and port!=443:
  682. if ':' not in domain:
  683. domainFull=domain+':'+str(port)
  684. deletePrefix=httpPrefix+'://'+domainFull+'/'
  685. if not allowDeletion and \
  686. (not messageJson['object'].startswith(deletePrefix) or \
  687. not messageJson['actor'].startswith(deletePrefix)):
  688. if debug:
  689. print('DEBUG: delete not permitted from other instances')
  690. return False
  691. if not messageJson.get('to'):
  692. if debug:
  693. print('DEBUG: '+messageJson['type']+' has no "to" list')
  694. return False
  695. if '/users/' not in messageJson['actor']:
  696. if debug:
  697. print('DEBUG: "users" missing from actor in '+messageJson['type'])
  698. return False
  699. if '/statuses/' not in messageJson['object']:
  700. if debug:
  701. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  702. return False
  703. if messageJson['actor'] not in messageJson['object']:
  704. if debug:
  705. print('DEBUG: actor is not the owner of the post to be deleted')
  706. if not os.path.isdir(baseDir+'/accounts/'+handle):
  707. print('DEBUG: unknown recipient of like - '+handle)
  708. # if this post in the outbox of the person?
  709. messageId=messageJson['object'].replace('/activity','').replace('/undo','')
  710. removeModerationPostFromIndex(baseDir,messageId,debug)
  711. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageId)
  712. if not postFilename:
  713. if debug:
  714. print('DEBUG: delete post not found in inbox or outbox')
  715. print(messageId)
  716. return True
  717. deletePost(baseDir,httpPrefix,handle.split('@')[0],handle.split('@')[1],postFilename,debug)
  718. if debug:
  719. print('DEBUG: post deleted - '+postFilename)
  720. return True
  721. def receiveAnnounce(session,handle: str,baseDir: str, \
  722. httpPrefix: str,domain :str,port: int, \
  723. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  724. personCache: {},messageJson: {},federationList: [], \
  725. debug : bool) -> bool:
  726. """Receives an announce activity within the POST section of HTTPServer
  727. """
  728. if messageJson['type']!='Announce':
  729. return False
  730. if not messageJson.get('actor'):
  731. if debug:
  732. print('DEBUG: '+messageJson['type']+' has no actor')
  733. return False
  734. if debug:
  735. print('DEBUG: receiving announce on '+handle)
  736. if not messageJson.get('object'):
  737. if debug:
  738. print('DEBUG: '+messageJson['type']+' has no object')
  739. return False
  740. if not isinstance(messageJson['object'], str):
  741. if debug:
  742. print('DEBUG: '+messageJson['type']+' object is not a string')
  743. return False
  744. if not messageJson.get('to'):
  745. if debug:
  746. print('DEBUG: '+messageJson['type']+' has no "to" list')
  747. return False
  748. if '/users/' not in messageJson['actor']:
  749. if debug:
  750. print('DEBUG: "users" missing from actor in '+messageJson['type'])
  751. return False
  752. if '/statuses/' not in messageJson['object']:
  753. if debug:
  754. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  755. return False
  756. if not os.path.isdir(baseDir+'/accounts/'+handle):
  757. print('DEBUG: unknown recipient of announce - '+handle)
  758. # is this post in the outbox of the person?
  759. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object'])
  760. if not postFilename:
  761. if debug:
  762. print('DEBUG: announce post not found in inbox or outbox')
  763. print(messageJson['object'])
  764. return True
  765. updateAnnounceCollection(postFilename,messageJson['actor'],debug)
  766. if debug:
  767. print('DEBUG: announced/repeated post found in inbox')
  768. return True
  769. def receiveUndoAnnounce(session,handle: str,baseDir: str, \
  770. httpPrefix: str,domain :str,port: int, \
  771. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  772. personCache: {},messageJson: {},federationList: [], \
  773. debug : bool) -> bool:
  774. """Receives an undo announce activity within the POST section of HTTPServer
  775. """
  776. if messageJson['type']!='Undo':
  777. return False
  778. if not messageJson.get('actor'):
  779. return False
  780. if not messageJson.get('object'):
  781. return False
  782. if not isinstance(messageJson['object'], dict):
  783. return False
  784. if not messageJson['object'].get('object'):
  785. return False
  786. if not isinstance(messageJson['object']['object'], str):
  787. return False
  788. if messageJson['object']['type']!='Announce':
  789. return False
  790. if '/users/' not in messageJson['actor']:
  791. if debug:
  792. print('DEBUG: "users" missing from actor in '+messageJson['type']+' announce')
  793. return False
  794. if '/statuses/' not in messageJson['object']:
  795. if debug:
  796. print('DEBUG: "statuses" missing from object in '+messageJson['type']+' announce')
  797. return False
  798. if not os.path.isdir(baseDir+'/accounts/'+handle):
  799. print('DEBUG: unknown recipient of undo announce - '+handle)
  800. # if this post in the outbox of the person?
  801. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object'])
  802. if not postFilename:
  803. if debug:
  804. print('DEBUG: undo announce post not found in inbox or outbox')
  805. print(messageJson['object']['object'])
  806. return True
  807. if debug:
  808. print('DEBUG: announced/repeated post to be undone found in inbox')
  809. with open(postFilename, 'r') as fp:
  810. postJsonObject=commentjson.load(fp)
  811. if not postJsonObject.get('type'):
  812. if postJsonObject['type']!='Announce':
  813. if debug:
  814. print("DEBUG: Attempt to undo something which isn't an announcement")
  815. return False
  816. undoAnnounceCollectionEntry(postFilename,messageJson['actor'],debug)
  817. os.remove(postFilename)
  818. return True
  819. def populateReplies(baseDir :str,httpPrefix :str,domain :str, \
  820. messageJson :{},maxReplies: int,debug :bool) -> bool:
  821. """Updates the list of replies for a post on this domain if
  822. a reply to it arrives
  823. """
  824. if not messageJson.get('id'):
  825. return False
  826. if not messageJson.get('object'):
  827. return False
  828. if not isinstance(messageJson['object'], dict):
  829. return False
  830. if not messageJson['object'].get('inReplyTo'):
  831. return False
  832. if not messageJson['object'].get('to'):
  833. return False
  834. replyTo=messageJson['object']['inReplyTo']
  835. if debug:
  836. print('DEBUG: post contains a reply')
  837. # is this a reply to a post on this domain?
  838. if not replyTo.startswith(httpPrefix+'://'+domain+'/'):
  839. if debug:
  840. print('DEBUG: post is a reply to another not on this domain')
  841. print(replyTo)
  842. print('Expected: '+httpPrefix+'://'+domain+'/')
  843. return False
  844. replyToNickname=getNicknameFromActor(replyTo)
  845. if not replyToNickname:
  846. if debug:
  847. print('DEBUG: no nickname found for '+replyTo)
  848. return False
  849. replyToDomain,replyToPort=getDomainFromActor(replyTo)
  850. if not replyToDomain:
  851. if debug:
  852. print('DEBUG: no domain found for '+replyTo)
  853. return False
  854. postFilename=locatePost(baseDir,replyToNickname,replyToDomain,replyTo)
  855. if not postFilename:
  856. if debug:
  857. print('DEBUG: post may have expired - '+replyTo)
  858. return False
  859. # populate a text file containing the ids of replies
  860. postRepliesFilename=postFilename.replace('.json','.replies')
  861. messageId=messageJson['id'].replace('/activity','').replace('/undo','')
  862. if os.path.isfile(postRepliesFilename):
  863. numLines = sum(1 for line in open(postRepliesFilename))
  864. if numLines>maxReplies:
  865. return False
  866. if messageId not in open(postRepliesFilename).read():
  867. repliesFile=open(postRepliesFilename, "a")
  868. repliesFile.write(messageId+'\n')
  869. repliesFile.close()
  870. else:
  871. repliesFile=open(postRepliesFilename, "w")
  872. repliesFile.write(messageId+'\n')
  873. repliesFile.close()
  874. return True
  875. def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \
  876. baseDir: str,httpPrefix: str,sendThreads: [], \
  877. postLog: [],cachedWebfingers: {},personCache: {}, \
  878. queue: [],domain: str,port: int,useTor: bool, \
  879. federationList: [],ocapAlways: bool,debug: bool, \
  880. acceptedCaps: [],
  881. queueFilename :str,destinationFilename :str,
  882. maxReplies: int,allowDeletion: bool) -> bool:
  883. """ Anything which needs to be done after capabilities checks have passed
  884. """
  885. if receiveLike(session,handle, \
  886. baseDir,httpPrefix, \
  887. domain,port, \
  888. sendThreads,postLog, \
  889. cachedWebfingers, \
  890. personCache, \
  891. messageJson, \
  892. federationList, \
  893. debug):
  894. if debug:
  895. print('DEBUG: Like accepted from '+keyId)
  896. return False
  897. if receiveUndoLike(session,handle, \
  898. baseDir,httpPrefix, \
  899. domain,port, \
  900. sendThreads,postLog, \
  901. cachedWebfingers, \
  902. personCache, \
  903. messageJson, \
  904. federationList, \
  905. debug):
  906. if debug:
  907. print('DEBUG: Undo like accepted from '+keyId)
  908. return False
  909. if receiveAnnounce(session,handle, \
  910. baseDir,httpPrefix, \
  911. domain,port, \
  912. sendThreads,postLog, \
  913. cachedWebfingers, \
  914. personCache, \
  915. messageJson, \
  916. federationList, \
  917. debug):
  918. if debug:
  919. print('DEBUG: Announce accepted from '+keyId)
  920. if receiveUndoAnnounce(session,handle, \
  921. baseDir,httpPrefix, \
  922. domain,port, \
  923. sendThreads,postLog, \
  924. cachedWebfingers, \
  925. personCache, \
  926. messageJson, \
  927. federationList, \
  928. debug):
  929. if debug:
  930. print('DEBUG: Undo announce accepted from '+keyId)
  931. return False
  932. if receiveDelete(session,handle, \
  933. baseDir,httpPrefix, \
  934. domain,port, \
  935. sendThreads,postLog, \
  936. cachedWebfingers, \
  937. personCache, \
  938. messageJson, \
  939. federationList, \
  940. debug,allowDeletion):
  941. if debug:
  942. print('DEBUG: Delete accepted from '+keyId)
  943. return False
  944. populateReplies(baseDir,httpPrefix,domain,messageJson,maxReplies,debug)
  945. if debug:
  946. print('DEBUG: object capabilities passed')
  947. print('copy queue file from '+queueFilename+' to '+destinationFilename)
  948. if messageJson.get('postNickname'):
  949. with open(destinationFilename, 'w+') as fp:
  950. commentjson.dump(messageJson['post'], fp, indent=4, sort_keys=False)
  951. else:
  952. with open(destinationFilename, 'w+') as fp:
  953. commentjson.dump(messageJson, fp, indent=4, sort_keys=False)
  954. if not os.path.isfile(destinationFilename):
  955. return False
  956. return True
  957. def restoreQueueItems(baseDir: str,queue: []) -> None:
  958. """Checks the queue for each account and appends filenames
  959. """
  960. queue.clear()
  961. for subdir,dirs,files in os.walk(baseDir+'/accounts'):
  962. for account in dirs:
  963. queueDir=baseDir+'/accounts/'+account+'/queue'
  964. if os.path.isdir(queueDir):
  965. for queuesubdir,queuedirs,queuefiles in os.walk(queueDir):
  966. for qfile in queuefiles:
  967. queue.append(os.path.join(queueDir, qfile))
  968. if len(queue)>0:
  969. print('Restored '+str(len(queue))+' inbox queue items')
  970. def runInboxQueue(projectVersion: str, \
  971. baseDir: str,httpPrefix: str,sendThreads: [],postLog: [], \
  972. cachedWebfingers: {},personCache: {},queue: [], \
  973. domain: str,port: int,useTor: bool,federationList: [], \
  974. ocapAlways: bool,maxReplies: int, \
  975. domainMaxPostsPerDay: int,accountMaxPostsPerDay: int, \
  976. allowDeletion: bool,debug: bool, \
  977. acceptedCaps=["inbox:write","objects:read"]) -> None:
  978. """Processes received items and moves them to
  979. the appropriate directories
  980. """
  981. currSessionTime=int(time.time())
  982. sessionLastUpdate=currSessionTime
  983. session=createSession(domain,port,useTor)
  984. inboxHandle='inbox@'+domain
  985. if debug:
  986. print('DEBUG: Inbox queue running')
  987. # if queue processing was interrupted (eg server crash)
  988. # then this loads any outstanding items back into the queue
  989. restoreQueueItems(baseDir,queue)
  990. # keep track of numbers of incoming posts per unit of time
  991. quotasLastUpdate=int(time.time())
  992. quotas={
  993. 'domains': {},
  994. 'accounts': {}
  995. }
  996. # keep track of the number of queue item read failures
  997. # so that if a file is corrupt then it will eventually
  998. # be ignored rather than endlessly retried
  999. itemReadFailed=0
  1000. while True:
  1001. time.sleep(1)
  1002. if len(queue)>0:
  1003. currTime=int(time.time())
  1004. # recreate the session periodically
  1005. if not session or currTime-sessionLastUpdate>1200:
  1006. print('Creating inbox session')
  1007. session=createSession(domain,port,useTor)
  1008. sessionLastUpdate=currTime
  1009. # oldest item first
  1010. queue.sort()
  1011. queueFilename=queue[0]
  1012. if not os.path.isfile(queueFilename):
  1013. if debug:
  1014. print("DEBUG: queue item rejected because it has no file: "+queueFilename)
  1015. queue.pop(0)
  1016. continue
  1017. print('Loading queue item '+queueFilename)
  1018. # Load the queue json
  1019. try:
  1020. with open(queueFilename, 'r') as fp:
  1021. queueJson=commentjson.load(fp)
  1022. except:
  1023. itemReadFailed+=1
  1024. print('WARN: Failed to load inbox queue item '+queueFilename+' (try '+str(itemReadFailed)+')')
  1025. if itemReadFailed>4:
  1026. # After a few tries we can assume that the file
  1027. # is probably corrupt/unreadable
  1028. queue.pop(0)
  1029. itemReadFailed=0
  1030. continue
  1031. itemReadFailed=0
  1032. # clear the daily quotas for maximum numbers of received posts
  1033. if currTime-quotasLastUpdate>60*60*24:
  1034. quotas={
  1035. 'domains': {},
  1036. 'accounts': {}
  1037. }
  1038. quotasLastUpdate=currTime
  1039. # limit the number of posts which can arrive per domain per day
  1040. postDomain=queueJson['postDomain']
  1041. if postDomain:
  1042. if domainMaxPostsPerDay>0:
  1043. if quotas['domains'].get(postDomain):
  1044. if quotas['domains'][postDomain]>domainMaxPostsPerDay:
  1045. if debug:
  1046. print('DEBUG: Maximum posts for '+postDomain+' reached')
  1047. queue.pop(0)
  1048. continue
  1049. quotas['domains'][postDomain]+=1
  1050. else:
  1051. quotas['domains'][postDomain]=1
  1052. if accountMaxPostsPerDay>0:
  1053. postHandle=queueJson['postNickname']+'@'+postDomain
  1054. if quotas['accounts'].get(postHandle):
  1055. if quotas['accounts'][postHandle]>accountMaxPostsPerDay:
  1056. if debug:
  1057. print('DEBUG: Maximum posts for '+postHandle+' reached')
  1058. queue.pop(0)
  1059. continue
  1060. quotas['accounts'][postHandle]+=1
  1061. else:
  1062. quotas['accounts'][postHandle]=1
  1063. if debug:
  1064. if accountMaxPostsPerDay>0 or domainMaxPostsPerDay>0:
  1065. pprint(quotas)
  1066. print('Obtaining public key for actor '+queueJson['actor'])
  1067. # Try a few times to obtain the public key
  1068. pubKey=None
  1069. keyId=None
  1070. for tries in range(8):
  1071. keyId=None
  1072. signatureParams=queueJson['httpHeaders']['signature'].split(',')
  1073. for signatureItem in signatureParams:
  1074. if signatureItem.startswith('keyId='):
  1075. if '"' in signatureItem:
  1076. keyId=signatureItem.split('"')[1]
  1077. break
  1078. if not keyId:
  1079. if debug:
  1080. print('DEBUG: No keyId in signature: '+ \
  1081. queueJson['httpHeaders']['signature'])
  1082. os.remove(queueFilename)
  1083. queue.pop(0)
  1084. continue
  1085. pubKey= \
  1086. getPersonPubKey(session,keyId,personCache,debug, \
  1087. projectVersion,httpPrefix,domain)
  1088. if pubKey:
  1089. print('DEBUG: public key: '+str(pubKey))
  1090. break
  1091. if debug:
  1092. print('DEBUG: Retry '+str(tries+1)+ \
  1093. ' obtaining public key for '+keyId)
  1094. time.sleep(5)
  1095. if not pubKey:
  1096. if debug:
  1097. print('DEBUG: public key could not be obtained from '+keyId)
  1098. os.remove(queueFilename)
  1099. queue.pop(0)
  1100. continue
  1101. # check the signature
  1102. if debug:
  1103. print('DEBUG: checking http headers')
  1104. pprint(queueJson['httpHeaders'])
  1105. if not verifyPostHeaders(httpPrefix, \
  1106. pubKey, \
  1107. queueJson['httpHeaders'], \
  1108. queueJson['path'],False, \
  1109. queueJson['digest'], \
  1110. json.dumps(queueJson['post'])):
  1111. if debug:
  1112. print('DEBUG: Header signature check failed')
  1113. os.remove(queueFilename)
  1114. queue.pop(0)
  1115. continue
  1116. if debug:
  1117. print('DEBUG: Signature check success')
  1118. # set the id to the same as the post filename
  1119. # This makes the filename and the id consistent
  1120. #if queueJson['post'].get('id'):
  1121. # queueJson['post']['id']=queueJson['id']
  1122. if receiveUndo(session, \
  1123. baseDir,httpPrefix,port, \
  1124. sendThreads,postLog, \
  1125. cachedWebfingers,
  1126. personCache, \
  1127. queueJson['post'], \
  1128. federationList, \
  1129. debug, \
  1130. acceptedCaps=["inbox:write","objects:read"]):
  1131. if debug:
  1132. print('DEBUG: Undo accepted from '+keyId)
  1133. os.remove(queueFilename)
  1134. queue.pop(0)
  1135. continue
  1136. if debug:
  1137. print('DEBUG: checking for follow requests')
  1138. if receiveFollowRequest(session, \
  1139. baseDir,httpPrefix,port, \
  1140. sendThreads,postLog, \
  1141. cachedWebfingers,
  1142. personCache, \
  1143. queueJson['post'], \
  1144. federationList, \
  1145. debug,projectVersion, \
  1146. acceptedCaps=["inbox:write","objects:read"]):
  1147. if debug:
  1148. print('DEBUG: Follow accepted from '+keyId)
  1149. os.remove(queueFilename)
  1150. queue.pop(0)
  1151. continue
  1152. else:
  1153. if debug:
  1154. print('DEBUG: No follow requests')
  1155. if receiveAcceptReject(session, \
  1156. baseDir,httpPrefix,domain,port, \
  1157. sendThreads,postLog, \
  1158. cachedWebfingers, \
  1159. personCache, \
  1160. queueJson['post'], \
  1161. federationList, \
  1162. debug):
  1163. if debug:
  1164. print('DEBUG: Accept/Reject received from '+keyId)
  1165. os.remove(queueFilename)
  1166. queue.pop(0)
  1167. continue
  1168. if receiveUpdate(session, \
  1169. baseDir,httpPrefix, \
  1170. domain,port, \
  1171. sendThreads,postLog, \
  1172. cachedWebfingers, \
  1173. personCache, \
  1174. queueJson['post'], \
  1175. federationList, \
  1176. debug):
  1177. if debug:
  1178. print('DEBUG: Update accepted from '+keyId)
  1179. os.remove(queueFilename)
  1180. queue.pop(0)
  1181. continue
  1182. # get recipients list
  1183. recipientsDict,recipientsDictFollowers= \
  1184. inboxPostRecipients(baseDir,queueJson['post'], \
  1185. httpPrefix,domain,port,debug)
  1186. if len(recipientsDict.items())==0 and \
  1187. len(recipientsDictFollowers.items())==0:
  1188. if debug:
  1189. pprint(queueJson['post'])
  1190. print('DEBUG: no recipients were resolved for post arriving in inbox')
  1191. os.remove(queueFilename)
  1192. queue.pop(0)
  1193. continue
  1194. # if there are only a small number of followers then process them as if they
  1195. # were specifically addresses to particular accounts
  1196. noOfFollowItems=len(recipientsDictFollowers.items())
  1197. if noOfFollowItems>0:
  1198. if noOfFollowItems<5:
  1199. if debug:
  1200. print('DEBUG: moving '+str(noOfFollowItems)+ \
  1201. ' inbox posts addressed to followers')
  1202. for handle,postItem in recipientsDictFollowers.items():
  1203. recipientsDict[handle]=postItem
  1204. recipientsDictFollowers={}
  1205. recipientsList=[recipientsDict,recipientsDictFollowers]
  1206. if debug:
  1207. print('*************************************')
  1208. print('Resolved recipients list:')
  1209. pprint(recipientsDict)
  1210. print('Resolved followers list:')
  1211. pprint(recipientsDictFollowers)
  1212. print('*************************************')
  1213. if queueJson['post'].get('capability'):
  1214. if not isinstance(queueJson['post']['capability'], list):
  1215. if debug:
  1216. print('DEBUG: capability on post should be a list')
  1217. os.remove(queueFilename)
  1218. queue.pop(0)
  1219. continue
  1220. # Copy any posts addressed to followers into the shared inbox
  1221. # this avoid copying file multiple times to potentially many
  1222. # individual inboxes
  1223. # This obviously bypasses object capabilities and so
  1224. # any checking will needs to be handled at the time when inbox
  1225. # GET happens on individual accounts.
  1226. # See posts.py/createBoxBase
  1227. if len(recipientsDictFollowers)>0:
  1228. with open(queueJson['destination'].replace(inboxHandle,inboxHandle), 'w') as fp:
  1229. commentjson.dump(queueJson['post'],fp,indent=4, \
  1230. sort_keys=False)
  1231. # for posts addressed to specific accounts
  1232. for handle,capsId in recipientsDict.items():
  1233. destination=queueJson['destination'].replace(inboxHandle,handle)
  1234. # check that capabilities are accepted
  1235. if queueJson['post'].get('capability'):
  1236. capabilityIdList=queueJson['post']['capability']
  1237. # does the capability id list within the post contain the id
  1238. # of the recipient with this handle?
  1239. # Here the capability id begins with the handle, so this could also
  1240. # be matched separately, but it's probably not necessary
  1241. if capsId in capabilityIdList:
  1242. inboxAfterCapabilities(session,keyId,handle, \
  1243. queueJson['post'], \
  1244. baseDir,httpPrefix, \
  1245. sendThreads,postLog, \
  1246. cachedWebfingers, \
  1247. personCache,queue,domain, \
  1248. port,useTor, \
  1249. federationList,ocapAlways, \
  1250. debug,acceptedCaps, \
  1251. queueFilename,destination, \
  1252. maxReplies,allowDeletion)
  1253. else:
  1254. if debug:
  1255. print('DEBUG: object capabilities check has failed')
  1256. pprint(queueJson['post'])
  1257. else:
  1258. if not ocapAlways:
  1259. inboxAfterCapabilities(session,keyId,handle, \
  1260. queueJson['post'], \
  1261. baseDir,httpPrefix, \
  1262. sendThreads,postLog, \
  1263. cachedWebfingers, \
  1264. personCache,queue,domain, \
  1265. port,useTor, \
  1266. federationList,ocapAlways, \
  1267. debug,acceptedCaps, \
  1268. queueFilename,destination, \
  1269. maxReplies,allowDeletion)
  1270. if debug:
  1271. pprint(queueJson['post'])
  1272. print('No capability list within post')
  1273. print('ocapAlways: '+str(ocapAlways))
  1274. print('DEBUG: object capabilities check failed')
  1275. if debug:
  1276. print('DEBUG: Queue post accepted')
  1277. os.remove(queueFilename)
  1278. queue.pop(0)