inbox.py 99 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387
  1. __filename__ = "inbox.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. import json
  9. import os
  10. import datetime
  11. import time
  12. import json
  13. from shutil import copyfile
  14. from utils import isPublicPost
  15. from utils import getCachedPostFilename
  16. from utils import removePostFromCache
  17. from utils import urlPermitted
  18. from utils import createInboxQueueDir
  19. from utils import getStatusNumber
  20. from utils import getDomainFromActor
  21. from utils import getNicknameFromActor
  22. from utils import domainPermitted
  23. from utils import locatePost
  24. from utils import deletePost
  25. from utils import removeAttachment
  26. from utils import removeModerationPostFromIndex
  27. from utils import loadJson
  28. from utils import saveJson
  29. from httpsig import verifyPostHeaders
  30. from session import createSession
  31. from session import getJson
  32. from follow import receiveFollowRequest
  33. from follow import getFollowersOfActor
  34. from follow import unfollowerOfPerson
  35. from pprint import pprint
  36. from cache import getPersonFromCache
  37. from cache import storePersonInCache
  38. from acceptreject import receiveAcceptReject
  39. from capabilities import getOcapFilename
  40. from capabilities import CapablePost
  41. from capabilities import capabilitiesReceiveUpdate
  42. from like import updateLikesCollection
  43. from like import undoLikesCollectionEntry
  44. from bookmarks import updateBookmarksCollection
  45. from bookmarks import undoBookmarksCollectionEntry
  46. from blocking import isBlocked
  47. from blocking import isBlockedDomain
  48. from filters import isFiltered
  49. from announce import updateAnnounceCollection
  50. from announce import undoAnnounceCollectionEntry
  51. from httpsig import messageContentDigest
  52. from posts import downloadAnnounce
  53. from posts import isDM
  54. from posts import isReply
  55. from posts import isImageMedia
  56. from posts import sendSignedJson
  57. from posts import sendToFollowersThread
  58. from webinterface import individualPostAsHtml
  59. from webinterface import getIconsDir
  60. from question import questionUpdateVotes
  61. from media import replaceYouTube
  62. def storeHashTags(baseDir: str,nickname: str,postJsonObject: {}) -> None:
  63. """Extracts hashtags from an incoming post and updates the
  64. relevant tags files.
  65. """
  66. if not isPublicPost(postJsonObject):
  67. return
  68. if not postJsonObject.get('object'):
  69. return
  70. if not isinstance(postJsonObject['object'], dict):
  71. return
  72. if not postJsonObject['object'].get('tag'):
  73. return
  74. if not postJsonObject.get('id'):
  75. return
  76. if not isinstance(postJsonObject['object']['tag'], list):
  77. return
  78. tagsDir=baseDir+'/tags'
  79. for tag in postJsonObject['object']['tag']:
  80. if not tag.get('type'):
  81. continue
  82. if tag['type']!='Hashtag':
  83. continue
  84. if not tag.get('name'):
  85. continue
  86. tagName=tag['name'].replace('#','').strip()
  87. tagsFilename=tagsDir+'/'+tagName+'.txt'
  88. postUrl=postJsonObject['id'].replace('/activity','').replace('/','#')
  89. daysSinceEpoch=(datetime.datetime.utcnow() - datetime.datetime(1970,1,1)).days
  90. tagline=str(daysSinceEpoch)+' '+nickname+' '+postUrl+'\n'
  91. if not os.path.isfile(tagsFilename):
  92. tagsFile=open(tagsFilename, "w+")
  93. if tagsFile:
  94. tagsFile.write(tagline)
  95. tagsFile.close()
  96. else:
  97. if postUrl not in open(tagsFilename).read():
  98. try:
  99. with open(tagsFilename, 'r+') as tagsFile:
  100. content = tagsFile.read()
  101. tagsFile.seek(0, 0)
  102. tagsFile.write(tagline+content)
  103. except Exception as e:
  104. print('WARN: Failed to write entry to tags file '+ \
  105. tagsFilename+' '+str(e))
  106. def inboxStorePostToHtmlCache(recentPostsCache: {},maxRecentPosts: int, \
  107. translate: {}, \
  108. baseDir: str,httpPrefix: str, \
  109. session,cachedWebfingers: {},personCache: {}, \
  110. nickname: str,domain: str,port: int, \
  111. postJsonObject: {}, \
  112. allowDeletion: bool) -> None:
  113. """Converts the json post into html and stores it in a cache
  114. This enables the post to be quickly displayed later
  115. """
  116. pageNumber=-999
  117. showAvatarOptions=True
  118. avatarUrl=None
  119. boxName='inbox'
  120. htmlStr= \
  121. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  122. getIconsDir(baseDir),translate,pageNumber, \
  123. baseDir,session,cachedWebfingers,personCache, \
  124. nickname,domain,port,postJsonObject, \
  125. avatarUrl,True,allowDeletion, \
  126. httpPrefix,__version__,boxName, \
  127. not isDM(postJsonObject), \
  128. True,True,False,True)
  129. def validInbox(baseDir: str,nickname: str,domain: str) -> bool:
  130. """Checks whether files were correctly saved to the inbox
  131. """
  132. if ':' in domain:
  133. domain=domain.split(':')[0]
  134. inboxDir=baseDir+'/accounts/'+nickname+'@'+domain+'/inbox'
  135. if not os.path.isdir(inboxDir):
  136. return True
  137. for subdir, dirs, files in os.walk(inboxDir):
  138. for f in files:
  139. filename = os.path.join(subdir, f)
  140. if not os.path.isfile(filename):
  141. print('filename: '+filename)
  142. return False
  143. if 'postNickname' in open(filename).read():
  144. print('queue file incorrectly saved to '+filename)
  145. return False
  146. return True
  147. def validInboxFilenames(baseDir: str,nickname: str,domain: str, \
  148. expectedDomain: str,expectedPort: int) -> bool:
  149. """Used by unit tests to check that the port number gets appended to
  150. domain names within saved post filenames
  151. """
  152. if ':' in domain:
  153. domain=domain.split(':')[0]
  154. inboxDir=baseDir+'/accounts/'+nickname+'@'+domain+'/inbox'
  155. if not os.path.isdir(inboxDir):
  156. return True
  157. expectedStr=expectedDomain+':'+str(expectedPort)
  158. for subdir, dirs, files in os.walk(inboxDir):
  159. for f in files:
  160. filename = os.path.join(subdir, f)
  161. if not os.path.isfile(filename):
  162. print('filename: '+filename)
  163. return False
  164. if not expectedStr in filename:
  165. print('Expected: '+expectedStr)
  166. print('Invalid filename: '+filename)
  167. return False
  168. return True
  169. def getPersonPubKey(baseDir: str,session,personUrl: str, \
  170. personCache: {},debug: bool, \
  171. projectVersion: str,httpPrefix: str,domain: str) -> str:
  172. if not personUrl:
  173. return None
  174. personUrl=personUrl.replace('#main-key','')
  175. if personUrl.endswith('/users/inbox'):
  176. if debug:
  177. print('DEBUG: Obtaining public key for shared inbox')
  178. personUrl=personUrl.replace('/users/inbox','/inbox')
  179. personJson = getPersonFromCache(baseDir,personUrl,personCache)
  180. if not personJson:
  181. if debug:
  182. print('DEBUG: Obtaining public key for '+personUrl)
  183. asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
  184. personJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  185. if not personJson:
  186. return None
  187. pubKey=None
  188. if personJson.get('publicKey'):
  189. if personJson['publicKey'].get('publicKeyPem'):
  190. pubKey=personJson['publicKey']['publicKeyPem']
  191. else:
  192. if personJson.get('publicKeyPem'):
  193. pubKey=personJson['publicKeyPem']
  194. if not pubKey:
  195. if debug:
  196. print('DEBUG: Public key not found for '+personUrl)
  197. storePersonInCache(baseDir,personUrl,personJson,personCache)
  198. return pubKey
  199. def inboxMessageHasParams(messageJson: {}) -> bool:
  200. """Checks whether an incoming message contains expected parameters
  201. """
  202. expectedParams=['type','actor','object']
  203. for param in expectedParams:
  204. if not messageJson.get(param):
  205. return False
  206. if not messageJson.get('to'):
  207. allowedWithoutToParam=['Like','Follow','Request','Accept','Capability','Undo']
  208. if messageJson['type'] not in allowedWithoutToParam:
  209. return False
  210. return True
  211. def inboxPermittedMessage(domain: str,messageJson: {},federationList: []) -> bool:
  212. """ check that we are receiving from a permitted domain
  213. """
  214. if not messageJson.get('actor'):
  215. return False
  216. actor=messageJson['actor']
  217. # always allow the local domain
  218. if domain in actor:
  219. return True
  220. if not urlPermitted(actor,federationList,"inbox:write"):
  221. return False
  222. alwaysAllowedTypes=('Follow','Like','Delete','Announce')
  223. if messageJson['type'] not in alwaysAllowedTypes:
  224. if not messageJson.get('object'):
  225. return True
  226. if not isinstance(messageJson['object'], dict):
  227. return False
  228. if messageJson['object'].get('inReplyTo'):
  229. inReplyTo=messageJson['object']['inReplyTo']
  230. if not urlPermitted(inReplyTo,federationList,"inbox:write"):
  231. return False
  232. return True
  233. def validPublishedDate(published: str) -> bool:
  234. currTime=datetime.datetime.utcnow()
  235. pubDate=datetime.datetime.strptime(published,"%Y-%m-%dT%H:%M:%SZ")
  236. daysSincePublished = (currTime - pubTime).days
  237. if daysSincePublished>30:
  238. return False
  239. return True
  240. def savePostToInboxQueue(baseDir: str,httpPrefix: str, \
  241. nickname: str, domain: str, \
  242. postJsonObject: {}, \
  243. messageBytes: str, \
  244. httpHeaders: {}, \
  245. postPath: str,debug: bool) -> str:
  246. """Saves the give json to the inbox queue for the person
  247. keyId specifies the actor sending the post
  248. """
  249. if len(messageBytes)>10240:
  250. print('WARN: inbox message too long '+str(len(messageBytes))+' bytes')
  251. return None
  252. originalDomain=domain
  253. if ':' in domain:
  254. domain=domain.split(':')[0]
  255. # block at the ealiest stage possible, which means the data
  256. # isn't written to file
  257. postNickname=None
  258. postDomain=None
  259. actor=None
  260. if postJsonObject.get('actor'):
  261. actor=postJsonObject['actor']
  262. postNickname=getNicknameFromActor(postJsonObject['actor'])
  263. if not postNickname:
  264. print('No post Nickname in actor '+postJsonObject['actor'])
  265. return None
  266. postDomain,postPort=getDomainFromActor(postJsonObject['actor'])
  267. if not postDomain:
  268. if debug:
  269. pprint(postJsonObject)
  270. print('No post Domain in actor')
  271. return None
  272. if isBlocked(baseDir,nickname,domain,postNickname,postDomain):
  273. if debug:
  274. print('DEBUG: post from '+postNickname+' blocked')
  275. return None
  276. if postPort:
  277. if postPort!=80 and postPort!=443:
  278. if ':' not in postDomain:
  279. postDomain=postDomain+':'+str(postPort)
  280. if postJsonObject.get('object'):
  281. if isinstance(postJsonObject['object'], dict):
  282. if postJsonObject['object'].get('inReplyTo'):
  283. if isinstance(postJsonObject['object']['inReplyTo'], str):
  284. replyDomain,replyPort=getDomainFromActor(postJsonObject['object']['inReplyTo'])
  285. if isBlockedDomain(baseDir,replyDomain):
  286. print('WARN: post contains reply from '+str(actor)+' to a blocked domain: '+replyDomain)
  287. return None
  288. else:
  289. replyNickname=getNicknameFromActor(postJsonObject['object']['inReplyTo'])
  290. if replyNickname and replyDomain:
  291. if isBlocked(baseDir,nickname,domain,replyNickname,replyDomain):
  292. print('WARN: post contains reply from '+str(actor)+ \
  293. ' to a blocked account: '+replyNickname+'@'+replyDomain)
  294. return None
  295. #else:
  296. # print('WARN: post is a reply to an unidentified account: '+postJsonObject['object']['inReplyTo'])
  297. # return None
  298. if postJsonObject['object'].get('content'):
  299. if isinstance(postJsonObject['object']['content'], str):
  300. if isFiltered(baseDir,nickname,domain,postJsonObject['object']['content']):
  301. print('WARN: post was filtered out due to content')
  302. return None
  303. originalPostId=None
  304. if postJsonObject.get('id'):
  305. originalPostId=postJsonObject['id'].replace('/activity','').replace('/undo','')
  306. currTime=datetime.datetime.utcnow()
  307. postId=None
  308. if postJsonObject.get('id'):
  309. #if '/statuses/' not in postJsonObject['id']:
  310. postId=postJsonObject['id'].replace('/activity','').replace('/undo','')
  311. published=currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
  312. if not postId:
  313. statusNumber,published = getStatusNumber()
  314. if actor:
  315. postId=actor+'/statuses/'+statusNumber
  316. else:
  317. postId=httpPrefix+'://'+originalDomain+'/users/'+nickname+'/statuses/'+statusNumber
  318. # NOTE: don't change postJsonObject['id'] before signature check
  319. inboxQueueDir=createInboxQueueDir(nickname,domain,baseDir)
  320. handle=nickname+'@'+domain
  321. destination=baseDir+'/accounts/'+handle+'/inbox/'+postId.replace('/','#')+'.json'
  322. #if os.path.isfile(destination):
  323. # if debug:
  324. # print(destination)
  325. # print('DEBUG: inbox item already exists')
  326. # return None
  327. filename=inboxQueueDir+'/'+postId.replace('/','#')+'.json'
  328. sharedInboxItem=False
  329. if nickname=='inbox':
  330. nickname=originalDomain
  331. sharedInboxItem=True
  332. digestStartTime=time.time()
  333. digest=messageContentDigest(messageBytes)
  334. timeDiffStr=str(int((time.time()-digestStartTime)*1000))
  335. if debug:
  336. while len(timeDiffStr)<6:
  337. timeDiffStr='0'+timeDiffStr
  338. print('DIGEST|'+timeDiffStr+'|'+filename)
  339. newQueueItem = {
  340. 'originalId': originalPostId,
  341. 'id': postId,
  342. 'actor': actor,
  343. 'nickname': nickname,
  344. 'domain': domain,
  345. 'postNickname': postNickname,
  346. 'postDomain': postDomain,
  347. 'sharedInbox': sharedInboxItem,
  348. 'published': published,
  349. 'httpHeaders': httpHeaders,
  350. 'path': postPath,
  351. 'post': postJsonObject,
  352. 'digest': digest,
  353. 'filename': filename,
  354. 'destination': destination
  355. }
  356. if debug:
  357. print('Inbox queue item created')
  358. saveJson(newQueueItem,filename)
  359. return filename
  360. def inboxCheckCapabilities(baseDir :str,nickname :str,domain :str, \
  361. actor: str,queue: [],queueJson: {}, \
  362. capabilityId: str,debug : bool) -> bool:
  363. if nickname=='inbox':
  364. return True
  365. ocapFilename= \
  366. getOcapFilename(baseDir, \
  367. queueJson['nickname'],queueJson['domain'], \
  368. actor,'accept')
  369. if not ocapFilename:
  370. return False
  371. if not os.path.isfile(ocapFilename):
  372. if debug:
  373. print('DEBUG: capabilities for '+ \
  374. actor+' do not exist')
  375. if os.path.isfile(queueFilename):
  376. os.remove(queueFilename)
  377. if len(queue)>0:
  378. queue.pop(0)
  379. return False
  380. oc=loadJson(ocapFilename)
  381. if not oc:
  382. return False
  383. if not oc.get('id'):
  384. if debug:
  385. print('DEBUG: capabilities for '+actor+' do not contain an id')
  386. if os.path.isfile(queueFilename):
  387. os.remove(queueFilename)
  388. if len(queue)>0:
  389. queue.pop(0)
  390. return False
  391. if oc['id']!=capabilityId:
  392. if debug:
  393. print('DEBUG: capability id mismatch')
  394. if os.path.isfile(queueFilename):
  395. os.remove(queueFilename)
  396. if len(queue)>0:
  397. queue.pop(0)
  398. return False
  399. if not oc.get('capability'):
  400. if debug:
  401. print('DEBUG: missing capability list')
  402. if os.path.isfile(queueFilename):
  403. os.remove(queueFilename)
  404. if len(queue)>0:
  405. queue.pop(0)
  406. return False
  407. if not CapablePost(queueJson['post'],oc['capability'],debug):
  408. if debug:
  409. print('DEBUG: insufficient capabilities to write to inbox from '+actor)
  410. if os.path.isfile(queueFilename):
  411. os.remove(queueFilename)
  412. if len(queue)>0:
  413. queue.pop(0)
  414. return False
  415. if debug:
  416. print('DEBUG: object capabilities check success')
  417. return True
  418. def inboxPostRecipientsAdd(baseDir :str,httpPrefix :str,toList :[], \
  419. recipientsDict :{}, \
  420. domainMatch: str,domain :str, \
  421. actor :str,debug: bool) -> bool:
  422. """Given a list of post recipients (toList) from 'to' or 'cc' parameters
  423. populate a recipientsDict with the handle and capabilities id for each
  424. """
  425. followerRecipients=False
  426. for recipient in toList:
  427. if not recipient:
  428. continue
  429. # is this a to a local account?
  430. if domainMatch in recipient:
  431. # get the handle for the local account
  432. nickname=recipient.split(domainMatch)[1]
  433. handle=nickname+'@'+domain
  434. if os.path.isdir(baseDir+'/accounts/'+handle):
  435. # are capabilities granted for this account to the
  436. # sender (actor) of the post?
  437. ocapFilename=baseDir+'/accounts/'+handle+'/ocap/accept/'+actor.replace('/','#')+'.json'
  438. if os.path.isfile(ocapFilename):
  439. # read the granted capabilities and obtain the id
  440. ocapJson=loadJson(ocapFilename)
  441. if ocapJson:
  442. if ocapJson.get('id'):
  443. # append with the capabilities id
  444. recipientsDict[handle]=ocapJson['id']
  445. else:
  446. recipientsDict[handle]=None
  447. else:
  448. if debug:
  449. print('DEBUG: '+ocapFilename+' not found')
  450. recipientsDict[handle]=None
  451. else:
  452. if debug:
  453. print('DEBUG: '+baseDir+'/accounts/'+handle+' does not exist')
  454. else:
  455. if debug:
  456. print('DEBUG: '+recipient+' is not local to '+domainMatch)
  457. print(str(toList))
  458. if recipient.endswith('followers'):
  459. if debug:
  460. print('DEBUG: followers detected as post recipients')
  461. followerRecipients=True
  462. return followerRecipients,recipientsDict
  463. def inboxPostRecipients(baseDir :str,postJsonObject :{}, \
  464. httpPrefix :str,domain : str,port :int, \
  465. debug :bool) -> ([],[]):
  466. """Returns dictionaries containing the recipients of the given post
  467. The shared dictionary contains followers
  468. """
  469. recipientsDict={}
  470. recipientsDictFollowers={}
  471. if not postJsonObject.get('actor'):
  472. if debug:
  473. pprint(postJsonObject)
  474. print('WARNING: inbox post has no actor')
  475. return recipientsDict,recipientsDictFollowers
  476. if ':' in domain:
  477. domain=domain.split(':')[0]
  478. domainBase=domain
  479. if port:
  480. if port!=80 and port!=443:
  481. if ':' not in domain:
  482. domain=domain+':'+str(port)
  483. domainMatch='/'+domain+'/users/'
  484. actor = postJsonObject['actor']
  485. # first get any specific people which the post is addressed to
  486. followerRecipients=False
  487. if postJsonObject.get('object'):
  488. if isinstance(postJsonObject['object'], dict):
  489. if postJsonObject['object'].get('to'):
  490. if isinstance(postJsonObject['object']['to'], list):
  491. recipientsList=postJsonObject['object']['to']
  492. else:
  493. recipientsList=[postJsonObject['object']['to']]
  494. if debug:
  495. print('DEBUG: resolving "to"')
  496. includesFollowers,recipientsDict= \
  497. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  498. recipientsList, \
  499. recipientsDict, \
  500. domainMatch,domainBase, \
  501. actor,debug)
  502. if includesFollowers:
  503. followerRecipients=True
  504. else:
  505. if debug:
  506. print('DEBUG: inbox post has no "to"')
  507. if postJsonObject['object'].get('cc'):
  508. if isinstance(postJsonObject['object']['cc'], list):
  509. recipientsList=postJsonObject['object']['cc']
  510. else:
  511. recipientsList=[postJsonObject['object']['cc']]
  512. includesFollowers,recipientsDict= \
  513. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  514. recipientsList, \
  515. recipientsDict, \
  516. domainMatch,domainBase, \
  517. actor,debug)
  518. if includesFollowers:
  519. followerRecipients=True
  520. else:
  521. if debug:
  522. print('DEBUG: inbox post has no cc')
  523. else:
  524. if debug:
  525. if isinstance(postJsonObject['object'], str):
  526. if '/statuses/' in postJsonObject['object']:
  527. print('DEBUG: inbox item is a link to a post')
  528. else:
  529. if '/users/' in postJsonObject['object']:
  530. print('DEBUG: inbox item is a link to an actor')
  531. if postJsonObject.get('to'):
  532. if isinstance(postJsonObject['to'], list):
  533. recipientsList=postJsonObject['to']
  534. else:
  535. recipientsList=[postJsonObject['to']]
  536. includesFollowers,recipientsDict= \
  537. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  538. recipientsList, \
  539. recipientsDict, \
  540. domainMatch,domainBase, \
  541. actor,debug)
  542. if includesFollowers:
  543. followerRecipients=True
  544. if postJsonObject.get('cc'):
  545. if isinstance(postJsonObject['cc'], list):
  546. recipientsList=postJsonObject['cc']
  547. else:
  548. recipientsList=[postJsonObject['cc']]
  549. includesFollowers,recipientsDict= \
  550. inboxPostRecipientsAdd(baseDir,httpPrefix, \
  551. recipientsList, \
  552. recipientsDict, \
  553. domainMatch,domainBase, \
  554. actor,debug)
  555. if includesFollowers:
  556. followerRecipients=True
  557. if not followerRecipients:
  558. if debug:
  559. print('DEBUG: no followers were resolved')
  560. return recipientsDict,recipientsDictFollowers
  561. # now resolve the followers
  562. recipientsDictFollowers= \
  563. getFollowersOfActor(baseDir,actor,debug)
  564. return recipientsDict,recipientsDictFollowers
  565. def receiveUndoFollow(session,baseDir: str,httpPrefix: str, \
  566. port: int,messageJson: {}, \
  567. federationList: [], \
  568. debug : bool) -> bool:
  569. if not messageJson['object'].get('actor'):
  570. if debug:
  571. print('DEBUG: follow request has no actor within object')
  572. return False
  573. if '/users/' not in messageJson['object']['actor'] and \
  574. '/channel/' not in messageJson['object']['actor'] and \
  575. '/profile/' not in messageJson['object']['actor']:
  576. if debug:
  577. print('DEBUG: "users" or "profile" missing from actor within object')
  578. return False
  579. if messageJson['object']['actor'] != messageJson['actor']:
  580. if debug:
  581. print('DEBUG: actors do not match')
  582. return False
  583. nicknameFollower=getNicknameFromActor(messageJson['object']['actor'])
  584. if not nicknameFollower:
  585. print('WARN: unable to find nickname in '+messageJson['object']['actor'])
  586. return False
  587. domainFollower,portFollower=getDomainFromActor(messageJson['object']['actor'])
  588. domainFollowerFull=domainFollower
  589. if portFollower:
  590. if portFollower!=80 and portFollower!=443:
  591. if ':' not in domainFollower:
  592. domainFollowerFull=domainFollower+':'+str(portFollower)
  593. nicknameFollowing=getNicknameFromActor(messageJson['object']['object'])
  594. if not nicknameFollowing:
  595. print('WARN: unable to find nickname in '+messageJson['object']['object'])
  596. return False
  597. domainFollowing,portFollowing=getDomainFromActor(messageJson['object']['object'])
  598. domainFollowingFull=domainFollowing
  599. if portFollowing:
  600. if portFollowing!=80 and portFollowing!=443:
  601. if ':' not in domainFollowing:
  602. domainFollowingFull=domainFollowing+':'+str(portFollowing)
  603. if unfollowerOfPerson(baseDir, \
  604. nicknameFollowing,domainFollowingFull, \
  605. nicknameFollower,domainFollowerFull, \
  606. debug):
  607. if debug:
  608. print('DEBUG: Follower '+nicknameFollower+'@'+domainFollowerFull+' was removed')
  609. return True
  610. if debug:
  611. print('DEBUG: Follower '+nicknameFollower+'@'+domainFollowerFull+' was not removed')
  612. return False
  613. def receiveUndo(session,baseDir: str,httpPrefix: str, \
  614. port: int,sendThreads: [],postLog: [], \
  615. cachedWebfingers: {},personCache: {}, \
  616. messageJson: {},federationList: [], \
  617. debug : bool, \
  618. acceptedCaps=["inbox:write","objects:read"]) -> bool:
  619. """Receives an undo request within the POST section of HTTPServer
  620. """
  621. if not messageJson['type'].startswith('Undo'):
  622. return False
  623. if debug:
  624. print('DEBUG: Undo activity received')
  625. if not messageJson.get('actor'):
  626. if debug:
  627. print('DEBUG: follow request has no actor')
  628. return False
  629. if '/users/' not in messageJson['actor'] and \
  630. '/channel/' not in messageJson['actor'] and \
  631. '/profile/' not in messageJson['actor']:
  632. if debug:
  633. print('DEBUG: "users" or "profile" missing from actor')
  634. return False
  635. if not messageJson.get('object'):
  636. if debug:
  637. print('DEBUG: '+messageJson['type']+' has no object')
  638. return False
  639. if not isinstance(messageJson['object'], dict):
  640. if debug:
  641. print('DEBUG: '+messageJson['type']+' object is not a dict')
  642. return False
  643. if not messageJson['object'].get('type'):
  644. if debug:
  645. print('DEBUG: '+messageJson['type']+' has no object type')
  646. return False
  647. if not messageJson['object'].get('object'):
  648. if debug:
  649. print('DEBUG: '+messageJson['type']+' has no object within object')
  650. return False
  651. if not isinstance(messageJson['object']['object'], str):
  652. if debug:
  653. print('DEBUG: '+messageJson['type']+' object within object is not a string')
  654. return False
  655. if messageJson['object']['type']=='Follow':
  656. return receiveUndoFollow(session,baseDir,httpPrefix, \
  657. port,messageJson, \
  658. federationList, \
  659. debug)
  660. return False
  661. def personReceiveUpdate(baseDir: str, \
  662. domain: str,port: int, \
  663. updateNickname: str,updateDomain: str,updatePort: int, \
  664. personJson: {},personCache: {},debug: bool) -> bool:
  665. """Changes an actor. eg: avatar or display name change
  666. """
  667. if debug:
  668. print('DEBUG: receiving actor update for '+personJson['url'])
  669. domainFull=domain
  670. if port:
  671. if port!=80 and port!=443:
  672. domainFull=domain+':'+str(port)
  673. updateDomainFull=updateDomain
  674. if updatePort:
  675. if updatePort!=80 and updatePort!=443:
  676. updateDomainFull=updateDomain+':'+str(updatePort)
  677. actor=updateDomainFull+'/users/'+updateNickname
  678. if actor not in personJson['id']:
  679. actor=updateDomainFull+'/profile/'+updateNickname
  680. if actor not in personJson['id']:
  681. actor=updateDomainFull+'/channel/'+updateNickname
  682. if actor not in personJson['id']:
  683. if debug:
  684. print('actor: '+actor)
  685. print('id: '+personJson['id'])
  686. print('DEBUG: Actor does not match id')
  687. return False
  688. if updateDomainFull==domainFull:
  689. if debug:
  690. print('DEBUG: You can only receive actor updates for domains other than your own')
  691. return False
  692. if not personJson.get('publicKey'):
  693. if debug:
  694. print('DEBUG: actor update does not contain a public key')
  695. return False
  696. if not personJson['publicKey'].get('publicKeyPem'):
  697. if debug:
  698. print('DEBUG: actor update does not contain a public key Pem')
  699. return False
  700. actorFilename=baseDir+'/cache/actors/'+personJson['id'].replace('/','#')+'.json'
  701. # check that the public keys match.
  702. # If they don't then this may be a nefarious attempt to hack an account
  703. if personCache.get(personJson['id']):
  704. if personCache[personJson['id']]['actor']['publicKey']['publicKeyPem']!=personJson['publicKey']['publicKeyPem']:
  705. if debug:
  706. print('WARN: Public key does not match when updating actor')
  707. return False
  708. else:
  709. if os.path.isfile(actorFilename):
  710. existingPersonJson=loadJson(actorFilename)
  711. if existingPersonJson:
  712. if existingPersonJson['publicKey']['publicKeyPem']!=personJson['publicKey']['publicKeyPem']:
  713. if debug:
  714. print('WARN: Public key does not match cached actor when updating')
  715. return False
  716. # save to cache in memory
  717. storePersonInCache(baseDir,personJson['id'],personJson,personCache)
  718. # save to cache on file
  719. if saveJson(personJson,actorFilename):
  720. print('actor updated for '+personJson['id'])
  721. # remove avatar if it exists so that it will be refreshed later
  722. # when a timeline is constructed
  723. actorStr=personJson['id'].replace('/','-')
  724. avatarFilename=baseDir+'/cache/avatars/'+actorStr+'.png'
  725. if os.path.isfile(avatarFilename):
  726. os.remove(avatarFilename)
  727. else:
  728. avatarFilename=baseDir+'/cache/avatars/'+actorStr+'.jpg'
  729. if os.path.isfile(avatarFilename):
  730. os.remove(avatarFilename)
  731. else:
  732. avatarFilename=baseDir+'/cache/avatars/'+actorStr+'.gif'
  733. if os.path.isfile(avatarFilename):
  734. os.remove(avatarFilename)
  735. return True
  736. def receiveUpdateToQuestion(recentPostsCache: {},messageJson: {}, \
  737. baseDir: str,nickname: str,domain: str) -> None:
  738. """Updating a question as new votes arrive
  739. """
  740. # message url of the question
  741. if not messageJson.get('id'):
  742. return
  743. if not messageJson.get('actor'):
  744. return
  745. messageId=messageJson['id'].replace('/activity','')
  746. if '#' in messageId:
  747. messageId=messageId.split('#',1)[0]
  748. # find the question post
  749. postFilename=locatePost(baseDir,nickname,domain,messageId)
  750. if not postFilename:
  751. return
  752. # load the json for the question
  753. postJsonObject=loadJson(postFilename,1)
  754. if not postJsonObject:
  755. return
  756. if not postJsonObject.get('actor'):
  757. return
  758. # does the actor match?
  759. if postJsonObject['actor']!=messageJson['actor']:
  760. return
  761. saveJson(messageJson,postFilename)
  762. # ensure that the cached post is removed if it exists, so
  763. # that it then will be recreated
  764. cachedPostFilename= \
  765. getCachedPostFilename(baseDir,nickname,domain,messageJson)
  766. if cachedPostFilename:
  767. if os.path.isfile(cachedPostFilename):
  768. os.remove(cachedPostFilename)
  769. # remove from memory cache
  770. removePostFromCache(messageJson,recentPostsCache)
  771. def receiveUpdate(recentPostsCache: {},session,baseDir: str, \
  772. httpPrefix: str,domain :str,port: int, \
  773. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  774. personCache: {},messageJson: {},federationList: [], \
  775. nickname: str,debug : bool) -> bool:
  776. """Receives an Update activity within the POST section of HTTPServer
  777. """
  778. if messageJson['type']!='Update':
  779. return False
  780. if not messageJson.get('actor'):
  781. if debug:
  782. print('DEBUG: '+messageJson['type']+' has no actor')
  783. return False
  784. if not messageJson.get('object'):
  785. if debug:
  786. print('DEBUG: '+messageJson['type']+' has no object')
  787. return False
  788. if not isinstance(messageJson['object'], dict):
  789. if debug:
  790. print('DEBUG: '+messageJson['type']+' object is not a dict')
  791. return False
  792. if not messageJson['object'].get('type'):
  793. if debug:
  794. print('DEBUG: '+messageJson['type']+' object has no type')
  795. return False
  796. if '/users/' not in messageJson['actor'] and \
  797. '/channel/' not in messageJson['actor'] and \
  798. '/profile/' not in messageJson['actor']:
  799. if debug:
  800. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type'])
  801. return False
  802. if messageJson['object']['type']=='Question':
  803. receiveUpdateToQuestion(recentPostsCache,messageJson, \
  804. baseDir,nickname,domain)
  805. if debug:
  806. print('DEBUG: Question update was received')
  807. return True
  808. if messageJson['type']=='Person':
  809. if messageJson.get('url') and messageJson.get('id'):
  810. print('Request to update unwrapped actor: '+messageJson['id'])
  811. updateNickname=getNicknameFromActor(messageJson['id'])
  812. if updateNickname:
  813. updateDomain,updatePort=getDomainFromActor(messageJson['id'])
  814. if personReceiveUpdate(baseDir, \
  815. domain,port, \
  816. updateNickname,updateDomain,updatePort, \
  817. messageJson, \
  818. personCache,debug):
  819. if debug:
  820. print('DEBUG: Unwrapped profile update was received for '+messageJson['url'])
  821. return True
  822. if messageJson['object']['type']=='Person' or \
  823. messageJson['object']['type']=='Application' or \
  824. messageJson['object']['type']=='Group' or \
  825. messageJson['object']['type']=='Service':
  826. if messageJson['object'].get('url') and messageJson['object'].get('id'):
  827. print('Request to update actor: '+messageJson['actor'])
  828. updateNickname=getNicknameFromActor(messageJson['actor'])
  829. if updateNickname:
  830. updateDomain,updatePort=getDomainFromActor(messageJson['actor'])
  831. if personReceiveUpdate(baseDir, \
  832. domain,port, \
  833. updateNickname,updateDomain,updatePort, \
  834. messageJson['object'], \
  835. personCache,debug):
  836. if debug:
  837. print('DEBUG: Profile update was received for '+messageJson['object']['url'])
  838. return True
  839. if messageJson['object'].get('capability') and messageJson['object'].get('scope'):
  840. nickname=getNicknameFromActor(messageJson['object']['scope'])
  841. if nickname:
  842. domain,tempPort=getDomainFromActor(messageJson['object']['scope'])
  843. if messageJson['object']['type']=='Capability':
  844. if capabilitiesReceiveUpdate(baseDir,nickname,domain,port,
  845. messageJson['actor'], \
  846. messageJson['object']['id'], \
  847. messageJson['object']['capability'], \
  848. debug):
  849. if debug:
  850. print('DEBUG: An update was received')
  851. return True
  852. return False
  853. def receiveLike(recentPostsCache: {}, \
  854. session,handle: str,isGroup: bool,baseDir: str, \
  855. httpPrefix: str,domain :str,port: int, \
  856. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  857. personCache: {},messageJson: {},federationList: [], \
  858. debug : bool) -> bool:
  859. """Receives a Like activity within the POST section of HTTPServer
  860. """
  861. if messageJson['type']!='Like':
  862. return False
  863. if not messageJson.get('actor'):
  864. if debug:
  865. print('DEBUG: '+messageJson['type']+' has no actor')
  866. return False
  867. if not messageJson.get('object'):
  868. if debug:
  869. print('DEBUG: '+messageJson['type']+' has no object')
  870. return False
  871. if not isinstance(messageJson['object'], str):
  872. if debug:
  873. print('DEBUG: '+messageJson['type']+' object is not a string')
  874. return False
  875. if not messageJson.get('to'):
  876. if debug:
  877. print('DEBUG: '+messageJson['type']+' has no "to" list')
  878. return False
  879. if '/users/' not in messageJson['actor'] and \
  880. '/channel/' not in messageJson['actor'] and \
  881. '/profile/' not in messageJson['actor']:
  882. if debug:
  883. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type'])
  884. return False
  885. if '/statuses/' not in messageJson['object']:
  886. if debug:
  887. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  888. return False
  889. if not os.path.isdir(baseDir+'/accounts/'+handle):
  890. print('DEBUG: unknown recipient of like - '+handle)
  891. # if this post in the outbox of the person?
  892. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object'])
  893. if not postFilename:
  894. if debug:
  895. print('DEBUG: post not found in inbox or outbox')
  896. print(messageJson['object'])
  897. return True
  898. if debug:
  899. print('DEBUG: liked post found in inbox')
  900. updateLikesCollection(recentPostsCache,baseDir,postFilename, \
  901. messageJson['object'], \
  902. messageJson['actor'],domain,debug)
  903. return True
  904. def receiveUndoLike(recentPostsCache: {}, \
  905. session,handle: str,isGroup: bool,baseDir: str, \
  906. httpPrefix: str,domain :str,port: int, \
  907. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  908. personCache: {},messageJson: {},federationList: [], \
  909. debug : bool) -> bool:
  910. """Receives an undo like activity within the POST section of HTTPServer
  911. """
  912. if messageJson['type']!='Undo':
  913. return False
  914. if not messageJson.get('actor'):
  915. return False
  916. if not messageJson.get('object'):
  917. return False
  918. if not isinstance(messageJson['object'], dict):
  919. return False
  920. if not messageJson['object'].get('type'):
  921. return False
  922. if messageJson['object']['type']!='Like':
  923. return False
  924. if not messageJson['object'].get('object'):
  925. if debug:
  926. print('DEBUG: '+messageJson['type']+' like has no object')
  927. return False
  928. if not isinstance(messageJson['object']['object'], str):
  929. if debug:
  930. print('DEBUG: '+messageJson['type']+' like object is not a string')
  931. return False
  932. if '/users/' not in messageJson['actor'] and \
  933. '/channel/' not in messageJson['actor'] and \
  934. '/profile/' not in messageJson['actor']:
  935. if debug:
  936. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type']+' like')
  937. return False
  938. if '/statuses/' not in messageJson['object']['object']:
  939. if debug:
  940. print('DEBUG: "statuses" missing from like object in '+messageJson['type'])
  941. return False
  942. if not os.path.isdir(baseDir+'/accounts/'+handle):
  943. print('DEBUG: unknown recipient of undo like - '+handle)
  944. # if this post in the outbox of the person?
  945. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object']['object'])
  946. if not postFilename:
  947. if debug:
  948. print('DEBUG: unliked post not found in inbox or outbox')
  949. print(messageJson['object']['object'])
  950. return True
  951. if debug:
  952. print('DEBUG: liked post found in inbox. Now undoing.')
  953. undoLikesCollectionEntry(recentPostsCache,baseDir,postFilename, \
  954. messageJson['object'],messageJson['actor'],domain,debug)
  955. return True
  956. def receiveBookmark(recentPostsCache: {}, \
  957. session,handle: str,isGroup: bool,baseDir: str, \
  958. httpPrefix: str,domain :str,port: int, \
  959. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  960. personCache: {},messageJson: {},federationList: [], \
  961. debug : bool) -> bool:
  962. """Receives a bookmark activity within the POST section of HTTPServer
  963. """
  964. if messageJson['type']!='Bookmark':
  965. return False
  966. if not messageJson.get('actor'):
  967. if debug:
  968. print('DEBUG: '+messageJson['type']+' has no actor')
  969. return False
  970. if not messageJson.get('object'):
  971. if debug:
  972. print('DEBUG: '+messageJson['type']+' has no object')
  973. return False
  974. if not isinstance(messageJson['object'], str):
  975. if debug:
  976. print('DEBUG: '+messageJson['type']+' object is not a string')
  977. return False
  978. if not messageJson.get('to'):
  979. if debug:
  980. print('DEBUG: '+messageJson['type']+' has no "to" list')
  981. return False
  982. if '/users/' not in messageJson['actor']:
  983. if debug:
  984. print('DEBUG: "users" missing from actor in '+messageJson['type'])
  985. return False
  986. if '/statuses/' not in messageJson['object']:
  987. if debug:
  988. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  989. return False
  990. if domain not in handle.split('@')[1]:
  991. if debug:
  992. print('DEBUG: unrecognized domain '+handle)
  993. return False
  994. domainFull=domain
  995. if port:
  996. if port!=80 and port!=443:
  997. domainFull=domain+':'+str(port)
  998. nickname=handle.split('@')[0]
  999. if not messageJson['actor'].endswith(domainFull+'/users/'+nickname):
  1000. if debug:
  1001. print('DEBUG: bookmark actor should be the same as the handle sent to '+handle+' != '+messageJson['actor'])
  1002. return False
  1003. if not os.path.isdir(baseDir+'/accounts/'+handle):
  1004. print('DEBUG: unknown recipient of bookmark - '+handle)
  1005. # if this post in the outbox of the person?
  1006. postFilename=locatePost(baseDir,nickname,domain,messageJson['object'])
  1007. if not postFilename:
  1008. if debug:
  1009. print('DEBUG: post not found in inbox or outbox')
  1010. print(messageJson['object'])
  1011. return True
  1012. if debug:
  1013. print('DEBUG: bookmarked post was found')
  1014. updateBookmarksCollection(recentPostsCache,baseDir,postFilename, \
  1015. messageJson['object'],messageJson['actor'],domain,debug)
  1016. return True
  1017. def receiveUndoBookmark(recentPostsCache: {}, \
  1018. session,handle: str,isGroup: bool,baseDir: str, \
  1019. httpPrefix: str,domain :str,port: int, \
  1020. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  1021. personCache: {},messageJson: {},federationList: [], \
  1022. debug : bool) -> bool:
  1023. """Receives an undo bookmark activity within the POST section of HTTPServer
  1024. """
  1025. if messageJson['type']!='Undo':
  1026. return False
  1027. if not messageJson.get('actor'):
  1028. return False
  1029. if not messageJson.get('object'):
  1030. return False
  1031. if not isinstance(messageJson['object'], dict):
  1032. return False
  1033. if not messageJson['object'].get('type'):
  1034. return False
  1035. if messageJson['object']['type']!='Bookmark':
  1036. return False
  1037. if not messageJson['object'].get('object'):
  1038. if debug:
  1039. print('DEBUG: '+messageJson['type']+' like has no object')
  1040. return False
  1041. if not isinstance(messageJson['object']['object'], str):
  1042. if debug:
  1043. print('DEBUG: '+messageJson['type']+' like object is not a string')
  1044. return False
  1045. if '/users/' not in messageJson['actor']:
  1046. if debug:
  1047. print('DEBUG: "users" missing from actor in '+messageJson['type']+' like')
  1048. return False
  1049. if '/statuses/' not in messageJson['object']['object']:
  1050. if debug:
  1051. print('DEBUG: "statuses" missing from like object in '+messageJson['type'])
  1052. return False
  1053. domainFull=domain
  1054. if port:
  1055. if port!=80 and port!=443:
  1056. domainFull=domain+':'+str(port)
  1057. nickname=handle.split('@')[0]
  1058. if domain not in handle.split('@')[1]:
  1059. if debug:
  1060. print('DEBUG: unrecognized bookmark domain '+handle)
  1061. return False
  1062. if not messageJson['actor'].endswith(domainFull+'/users/'+nickname):
  1063. if debug:
  1064. print('DEBUG: bookmark actor should be the same as the handle sent to '+handle+' != '+messageJson['actor'])
  1065. return False
  1066. if not os.path.isdir(baseDir+'/accounts/'+handle):
  1067. print('DEBUG: unknown recipient of bookmark undo - '+handle)
  1068. # if this post in the outbox of the person?
  1069. postFilename=locatePost(baseDir,nickname,domain,messageJson['object']['object'])
  1070. if not postFilename:
  1071. if debug:
  1072. print('DEBUG: unbookmarked post not found in inbox or outbox')
  1073. print(messageJson['object']['object'])
  1074. return True
  1075. if debug:
  1076. print('DEBUG: bookmarked post found. Now undoing.')
  1077. undoBookmarksCollectionEntry(recentPostsCache,baseDir,postFilename, \
  1078. messageJson['object'],messageJson['actor'],domain,debug)
  1079. return True
  1080. def receiveDelete(session,handle: str,isGroup: bool,baseDir: str, \
  1081. httpPrefix: str,domain :str,port: int, \
  1082. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  1083. personCache: {},messageJson: {},federationList: [], \
  1084. debug : bool,allowDeletion: bool) -> bool:
  1085. """Receives a Delete activity within the POST section of HTTPServer
  1086. """
  1087. if messageJson['type']!='Delete':
  1088. return False
  1089. if not messageJson.get('actor'):
  1090. if debug:
  1091. print('DEBUG: '+messageJson['type']+' has no actor')
  1092. return False
  1093. if debug:
  1094. print('DEBUG: Delete activity arrived')
  1095. if not messageJson.get('object'):
  1096. if debug:
  1097. print('DEBUG: '+messageJson['type']+' has no object')
  1098. return False
  1099. if not isinstance(messageJson['object'], str):
  1100. if debug:
  1101. print('DEBUG: '+messageJson['type']+' object is not a string')
  1102. return False
  1103. domainFull=domain
  1104. if port:
  1105. if port!=80 and port!=443:
  1106. if ':' not in domain:
  1107. domainFull=domain+':'+str(port)
  1108. deletePrefix=httpPrefix+'://'+domainFull+'/'
  1109. if not allowDeletion and \
  1110. (not messageJson['object'].startswith(deletePrefix) or \
  1111. not messageJson['actor'].startswith(deletePrefix)):
  1112. if debug:
  1113. print('DEBUG: delete not permitted from other instances')
  1114. return False
  1115. if not messageJson.get('to'):
  1116. if debug:
  1117. print('DEBUG: '+messageJson['type']+' has no "to" list')
  1118. return False
  1119. if '/users/' not in messageJson['actor'] and \
  1120. '/channel/' not in messageJson['actor'] and \
  1121. '/profile/' not in messageJson['actor']:
  1122. if debug:
  1123. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type'])
  1124. return False
  1125. if '/statuses/' not in messageJson['object']:
  1126. if debug:
  1127. print('DEBUG: "statuses" missing from object in '+messageJson['type'])
  1128. return False
  1129. if messageJson['actor'] not in messageJson['object']:
  1130. if debug:
  1131. print('DEBUG: actor is not the owner of the post to be deleted')
  1132. if not os.path.isdir(baseDir+'/accounts/'+handle):
  1133. print('DEBUG: unknown recipient of like - '+handle)
  1134. # if this post in the outbox of the person?
  1135. messageId=messageJson['object'].replace('/activity','').replace('/undo','')
  1136. removeModerationPostFromIndex(baseDir,messageId,debug)
  1137. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageId)
  1138. if not postFilename:
  1139. if debug:
  1140. print('DEBUG: delete post not found in inbox or outbox')
  1141. print(messageId)
  1142. return True
  1143. deletePost(baseDir,httpPrefix,handle.split('@')[0],handle.split('@')[1],postFilename,debug)
  1144. if debug:
  1145. print('DEBUG: post deleted - '+postFilename)
  1146. return True
  1147. def receiveAnnounce(recentPostsCache: {}, \
  1148. session,handle: str,isGroup: bool,baseDir: str, \
  1149. httpPrefix: str,domain :str,port: int, \
  1150. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  1151. personCache: {},messageJson: {},federationList: [], \
  1152. debug : bool) -> bool:
  1153. """Receives an announce activity within the POST section of HTTPServer
  1154. """
  1155. if messageJson['type']!='Announce':
  1156. return False
  1157. if '@' not in handle:
  1158. if debug:
  1159. print('DEBUG: bad handle '+handle)
  1160. return False
  1161. if not messageJson.get('actor'):
  1162. if debug:
  1163. print('DEBUG: '+messageJson['type']+' has no actor')
  1164. return False
  1165. if debug:
  1166. print('DEBUG: receiving announce on '+handle)
  1167. if not messageJson.get('object'):
  1168. if debug:
  1169. print('DEBUG: '+messageJson['type']+' has no object')
  1170. return False
  1171. if not isinstance(messageJson['object'], str):
  1172. if debug:
  1173. print('DEBUG: '+messageJson['type']+' object is not a string')
  1174. return False
  1175. if not messageJson.get('to'):
  1176. if debug:
  1177. print('DEBUG: '+messageJson['type']+' has no "to" list')
  1178. return False
  1179. if '/users/' not in messageJson['actor'] and \
  1180. '/channel/' not in messageJson['actor'] and \
  1181. '/profile/' not in messageJson['actor']:
  1182. if debug:
  1183. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type'])
  1184. return False
  1185. if '/users/' not in messageJson['object'] and \
  1186. '/channel/' not in messageJson['object'] and \
  1187. '/profile/' not in messageJson['object']:
  1188. if debug:
  1189. print('DEBUG: "users", "channel" or "profile" missing in '+messageJson['type'])
  1190. return False
  1191. objectDomain=messageJson['object'].replace('https://','').replace('http://','').replace('dat://','')
  1192. if '/' in objectDomain:
  1193. objectDomain=objectDomain.split('/')[0]
  1194. if isBlockedDomain(baseDir,objectDomain):
  1195. if debug:
  1196. print('DEBUG: announced domain is blocked')
  1197. return False
  1198. if not os.path.isdir(baseDir+'/accounts/'+handle):
  1199. print('DEBUG: unknown recipient of announce - '+handle)
  1200. # is this post in the outbox of the person?
  1201. nickname=handle.split('@')[0]
  1202. postFilename=locatePost(baseDir,nickname,handle.split('@')[1],messageJson['object'])
  1203. if not postFilename:
  1204. if debug:
  1205. print('DEBUG: announce post not found in inbox or outbox')
  1206. print(messageJson['object'])
  1207. return True
  1208. updateAnnounceCollection(recentPostsCache,baseDir,postFilename, \
  1209. messageJson['actor'],domain,debug)
  1210. if debug:
  1211. print('DEBUG: Downloading announce post '+messageJson['actor']+' -> '+messageJson['object'])
  1212. postJsonObject=downloadAnnounce(session,baseDir,httpPrefix,nickname,domain,messageJson,__version__)
  1213. if postJsonObject:
  1214. if debug:
  1215. print('DEBUG: Announce post downloaded for '+messageJson['actor']+' -> '+messageJson['object'])
  1216. storeHashTags(baseDir,nickname,postJsonObject)
  1217. # Try to obtain the actor for this person
  1218. # so that their avatar can be shown
  1219. lookupActor=None
  1220. if postJsonObject.get('attributedTo'):
  1221. lookupActor=postJsonObject['attributedTo']
  1222. else:
  1223. if postJsonObject.get('object'):
  1224. if isinstance(postJsonObject['object'], dict):
  1225. if postJsonObject['object'].get('attributedTo'):
  1226. lookupActor=postJsonObject['object']['attributedTo']
  1227. if lookupActor:
  1228. if '/users/' in lookupActor or \
  1229. '/channel/' in lookupActor or \
  1230. '/profile/' in lookupActor:
  1231. if '/statuses/' in lookupActor:
  1232. lookupActor=lookupActor.split('/statuses/')[0]
  1233. if debug:
  1234. print('DEBUG: Obtaining actor for announce post '+lookupActor)
  1235. for tries in range(6):
  1236. pubKey= \
  1237. getPersonPubKey(baseDir,session,lookupActor, \
  1238. personCache,debug, \
  1239. __version__,httpPrefix,domain)
  1240. if pubKey:
  1241. print('DEBUG: public key obtained for announce: '+lookupActor)
  1242. break
  1243. if debug:
  1244. print('DEBUG: Retry '+str(tries+1)+ \
  1245. ' obtaining actor for '+lookupActor)
  1246. time.sleep(5)
  1247. if debug:
  1248. print('DEBUG: announced/repeated post arrived in inbox')
  1249. return True
  1250. def receiveUndoAnnounce(recentPostsCache: {}, \
  1251. session,handle: str,isGroup: bool,baseDir: str, \
  1252. httpPrefix: str,domain :str,port: int, \
  1253. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  1254. personCache: {},messageJson: {},federationList: [], \
  1255. debug : bool) -> bool:
  1256. """Receives an undo announce activity within the POST section of HTTPServer
  1257. """
  1258. if messageJson['type']!='Undo':
  1259. return False
  1260. if not messageJson.get('actor'):
  1261. return False
  1262. if not messageJson.get('object'):
  1263. return False
  1264. if not isinstance(messageJson['object'], dict):
  1265. return False
  1266. if not messageJson['object'].get('object'):
  1267. return False
  1268. if not isinstance(messageJson['object']['object'], str):
  1269. return False
  1270. if messageJson['object']['type']!='Announce':
  1271. return False
  1272. if '/users/' not in messageJson['actor'] and \
  1273. '/channel/' not in messageJson['actor'] and \
  1274. '/profile/' not in messageJson['actor']:
  1275. if debug:
  1276. print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type']+' announce')
  1277. return False
  1278. if not os.path.isdir(baseDir+'/accounts/'+handle):
  1279. print('DEBUG: unknown recipient of undo announce - '+handle)
  1280. # if this post in the outbox of the person?
  1281. postFilename=locatePost(baseDir,handle.split('@')[0],handle.split('@')[1],messageJson['object']['object'])
  1282. if not postFilename:
  1283. if debug:
  1284. print('DEBUG: undo announce post not found in inbox or outbox')
  1285. print(messageJson['object']['object'])
  1286. return True
  1287. if debug:
  1288. print('DEBUG: announced/repeated post to be undone found in inbox')
  1289. postJsonObject=loadJson(postFilename)
  1290. if postJsonObject:
  1291. if not postJsonObject.get('type'):
  1292. if postJsonObject['type']!='Announce':
  1293. if debug:
  1294. print("DEBUG: Attempt to undo something which isn't an announcement")
  1295. return False
  1296. undoAnnounceCollectionEntry(recentPostsCache,baseDir,postFilename, \
  1297. messageJson['actor'],domain,debug)
  1298. if os.path.isfile(postFilename):
  1299. os.remove(postFilename)
  1300. return True
  1301. def populateReplies(baseDir :str,httpPrefix :str,domain :str, \
  1302. messageJson :{},maxReplies: int,debug :bool) -> bool:
  1303. """Updates the list of replies for a post on this domain if
  1304. a reply to it arrives
  1305. """
  1306. if not messageJson.get('id'):
  1307. return False
  1308. if not messageJson.get('object'):
  1309. return False
  1310. if not isinstance(messageJson['object'], dict):
  1311. return False
  1312. if not messageJson['object'].get('inReplyTo'):
  1313. return False
  1314. if not messageJson['object'].get('to'):
  1315. return False
  1316. replyTo=messageJson['object']['inReplyTo']
  1317. if debug:
  1318. print('DEBUG: post contains a reply')
  1319. # is this a reply to a post on this domain?
  1320. if not replyTo.startswith(httpPrefix+'://'+domain+'/'):
  1321. if debug:
  1322. print('DEBUG: post is a reply to another not on this domain')
  1323. print(replyTo)
  1324. print('Expected: '+httpPrefix+'://'+domain+'/')
  1325. return False
  1326. replyToNickname=getNicknameFromActor(replyTo)
  1327. if not replyToNickname:
  1328. print('DEBUG: no nickname found for '+replyTo)
  1329. return False
  1330. replyToDomain,replyToPort=getDomainFromActor(replyTo)
  1331. if not replyToDomain:
  1332. if debug:
  1333. print('DEBUG: no domain found for '+replyTo)
  1334. return False
  1335. postFilename=locatePost(baseDir,replyToNickname,replyToDomain,replyTo)
  1336. if not postFilename:
  1337. if debug:
  1338. print('DEBUG: post may have expired - '+replyTo)
  1339. return False
  1340. # populate a text file containing the ids of replies
  1341. postRepliesFilename=postFilename.replace('.json','.replies')
  1342. messageId=messageJson['id'].replace('/activity','').replace('/undo','')
  1343. if os.path.isfile(postRepliesFilename):
  1344. numLines = sum(1 for line in open(postRepliesFilename))
  1345. if numLines>maxReplies:
  1346. return False
  1347. if messageId not in open(postRepliesFilename).read():
  1348. repliesFile=open(postRepliesFilename, "a")
  1349. repliesFile.write(messageId+'\n')
  1350. repliesFile.close()
  1351. else:
  1352. repliesFile=open(postRepliesFilename, "w")
  1353. repliesFile.write(messageId+'\n')
  1354. repliesFile.close()
  1355. return True
  1356. def estimateNumberOfMentions(content: str) -> int:
  1357. """Returns a rough estimate of the number of mentions
  1358. """
  1359. return int(content.count('@')/2)
  1360. def estimateNumberOfEmoji(content: str) -> int:
  1361. """Returns a rough estimate of the number of emoji
  1362. """
  1363. return int(content.count(':')/2)
  1364. def validPostContent(messageJson: {},maxMentions: int,maxEmoji: int) -> bool:
  1365. """Is the content of a received post valid?
  1366. Check for bad html
  1367. Check for hellthreads
  1368. Check number of tags is reasonable
  1369. """
  1370. if not messageJson.get('object'):
  1371. return True
  1372. if not isinstance(messageJson['object'], dict):
  1373. return True
  1374. if not messageJson['object'].get('content'):
  1375. return True
  1376. if not messageJson['object'].get('published'):
  1377. return False
  1378. if 'T' not in messageJson['object']['published']:
  1379. return False
  1380. if 'Z' not in messageJson['object']['published']:
  1381. return False
  1382. # check for bad html
  1383. invalidStrings=['<script>','<canvas>','<style>','</html>','</body>','<br>','<hr>']
  1384. for badStr in invalidStrings:
  1385. if badStr in messageJson['object']['content']:
  1386. if messageJson['object'].get('id'):
  1387. print('REJECT ARBITRARY HTML: '+messageJson['object']['id'])
  1388. print('REJECT ARBITRARY HTML: bad string in post - '+messageJson['object']['content'])
  1389. return False
  1390. # check (rough) number of mentions
  1391. if estimateNumberOfMentions(messageJson['object']['content'])>maxMentions:
  1392. if messageJson['object'].get('id'):
  1393. print('REJECT HELLTHREAD: '+messageJson['object']['id'])
  1394. print('REJECT HELLTHREAD: Too many mentions in post - '+messageJson['object']['content'])
  1395. return False
  1396. if estimateNumberOfEmoji(messageJson['object']['content'])>maxEmoji:
  1397. if messageJson['object'].get('id'):
  1398. print('REJECT EMOJI OVERLOAD: '+messageJson['object']['id'])
  1399. print('REJECT EMOJI OVERLOAD: Too many emoji in post - '+messageJson['object']['content'])
  1400. return False
  1401. # check number of tags
  1402. if messageJson['object'].get('tag'):
  1403. if not isinstance(messageJson['object']['tag'], list):
  1404. messageJson['object']['tag']=[]
  1405. else:
  1406. if len(messageJson['object']['tag']) > maxMentions*2:
  1407. if messageJson['object'].get('id'):
  1408. print('REJECT: '+messageJson['object']['id'])
  1409. print('REJECT: Too many tags in post - '+messageJson['object']['tag'])
  1410. return False
  1411. print('ACCEPT: post content is valid')
  1412. return True
  1413. def obtainAvatarForReplyPost(session,baseDir: str,httpPrefix: str, \
  1414. domain: str,personCache: {}, \
  1415. postJsonObject: {},debug: bool) -> None:
  1416. """Tries to obtain the actor for the person being replied to
  1417. so that their avatar can later be shown
  1418. """
  1419. if not postJsonObject.get('object'):
  1420. return
  1421. if not isinstance(postJsonObject['object'], dict):
  1422. return
  1423. if not postJsonObject['object'].get('inReplyTo'):
  1424. return
  1425. lookupActor=postJsonObject['object']['inReplyTo']
  1426. if not lookupActor:
  1427. return
  1428. if not ('/users/' in lookupActor or \
  1429. '/channel/' in lookupActor or \
  1430. '/profile/' in lookupActor):
  1431. return
  1432. if '/statuses/' in lookupActor:
  1433. lookupActor=lookupActor.split('/statuses/')[0]
  1434. if debug:
  1435. print('DEBUG: Obtaining actor for reply post '+lookupActor)
  1436. for tries in range(6):
  1437. pubKey= \
  1438. getPersonPubKey(baseDir,session,lookupActor, \
  1439. personCache,debug, \
  1440. __version__,httpPrefix,domain)
  1441. if pubKey:
  1442. print('DEBUG: public key obtained for reply: '+lookupActor)
  1443. break
  1444. if debug:
  1445. print('DEBUG: Retry '+str(tries+1)+ \
  1446. ' obtaining actor for '+lookupActor)
  1447. time.sleep(5)
  1448. def dmNotify(baseDir: str,handle: str,url: str) -> None:
  1449. """Creates a notification that a new DM has arrived
  1450. """
  1451. accountDir=baseDir+'/accounts/'+handle
  1452. if not os.path.isdir(accountDir):
  1453. return
  1454. dmFile=accountDir+'/.newDM'
  1455. if not os.path.isfile(dmFile):
  1456. with open(dmFile, 'w') as fp:
  1457. fp.write(url)
  1458. def replyNotify(baseDir: str,handle: str,url: str) -> None:
  1459. """Creates a notification that a new reply has arrived
  1460. """
  1461. accountDir=baseDir+'/accounts/'+handle
  1462. if not os.path.isdir(accountDir):
  1463. return
  1464. replyFile=accountDir+'/.newReply'
  1465. if not os.path.isfile(replyFile):
  1466. with open(replyFile, 'w') as fp:
  1467. fp.write(url)
  1468. def groupHandle(baseDir: str,handle: str) -> bool:
  1469. """Is the given account handle a group?
  1470. """
  1471. actorFile=baseDir+'/accounts/'+handle+'.json'
  1472. if not os.path.isfile(actorFile):
  1473. return False
  1474. actorJson=loadJson(actorFile)
  1475. if not actorJson:
  1476. return False
  1477. return actorJson['type']=='Group'
  1478. def getGroupName(baseDir: str,handle: str) -> str:
  1479. """Returns the preferred name of a group
  1480. """
  1481. actorFile=baseDir+'/accounts/'+handle+'.json'
  1482. if not os.path.isfile(actorFile):
  1483. return False
  1484. actorJson=loadJson(actorFile)
  1485. if not actorJson:
  1486. return 'Group'
  1487. return actorJson['name']
  1488. def sendToGroupMembers(session,baseDir: str,handle: str,port: int,postJsonObject: {}, \
  1489. httpPrefix: str,federationList: [], \
  1490. sendThreads: [],postLog: [],cachedWebfingers: {}, \
  1491. personCache: {},debug: bool) -> None:
  1492. """When a post arrives for a group send it out to the group members
  1493. """
  1494. followersFile=baseDir+'/accounts/'+handle+'/followers.txt'
  1495. if not os.path.isfile(followersFile):
  1496. return
  1497. if not postJsonObject.get('object'):
  1498. return
  1499. nickname=handle.split('@')[0]
  1500. groupname=getGroupName(baseDir,handle)
  1501. domain=handle.split('@')[1]
  1502. domainFull=domain
  1503. if ':' not in domain:
  1504. if port:
  1505. if port!=80 and port !=443:
  1506. domain=domain+':'+str(port)
  1507. # set sender
  1508. cc=''
  1509. sendingActor=postJsonObject['actor']
  1510. sendingActorNickname=getNicknameFromActor(sendingActor)
  1511. sendingActorDomain,sendingActorPort=getDomainFromActor(sendingActor)
  1512. sendingActorDomainFull=sendingActorDomain
  1513. if ':' in sendingActorDomain:
  1514. if sendingActorPort:
  1515. if sendingActorPort!=80 and sendingActorPort!=443:
  1516. sendingActorDomainFull=sendingActorDomain+':'+str(sendingActorPort)
  1517. senderStr='@'+sendingActorNickname+'@'+sendingActorDomainFull
  1518. if not postJsonObject['object']['content'].startswith(senderStr):
  1519. postJsonObject['object']['content']=senderStr+' '+postJsonObject['object']['content']
  1520. # add mention to tag list
  1521. if not postJsonObject['object']['tag']:
  1522. postJsonObject['object']['tag']=[]
  1523. # check if the mention already exists
  1524. mentionExists=False
  1525. for mention in postJsonObject['object']['tag']:
  1526. if mention['type']=='Mention':
  1527. if mention.get('href'):
  1528. if mention['href']==sendingActor:
  1529. mentionExists=True
  1530. if not mentionExists:
  1531. # add the mention of the original sender
  1532. postJsonObject['object']['tag'].append({
  1533. 'href': sendingActor,
  1534. 'name': senderStr,
  1535. 'type': 'Mention'
  1536. })
  1537. postJsonObject['actor']=httpPrefix+'://'+domainFull+'/users/'+nickname
  1538. postJsonObject['to']=[httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers']
  1539. postJsonObject['cc']=[cc]
  1540. postJsonObject['object']['to']=postJsonObject['to']
  1541. postJsonObject['object']['cc']=[cc]
  1542. # set subject
  1543. if not postJsonObject['object'].get('summary'):
  1544. postJsonObject['object']['summary']='General Discussion'
  1545. if ':' in domain:
  1546. domain=domain.split(':')[0]
  1547. with open(followersFile, 'r') as groupMembers:
  1548. for memberHandle in groupMembers:
  1549. if memberHandle!=handle:
  1550. memberNickname=memberHandle.split('@')[0]
  1551. memberDomain=memberHandle.split('@')[1]
  1552. memberPort=port
  1553. if ':' in memberDomain:
  1554. memberPortStr=memberDomain.split(':')[1]
  1555. if memberPortStr.isdigit():
  1556. memberPort=int(memberPortStr)
  1557. memberDomain=memberDomain.split(':')[0]
  1558. sendSignedJson(postJsonObject,session,baseDir, \
  1559. nickname,domain,port, \
  1560. memberNickname,memberDomain,memberPort,cc, \
  1561. httpPrefix,False,False,federationList, \
  1562. sendThreads,postLog,cachedWebfingers, \
  1563. personCache,debug,projectVersion)
  1564. def inboxUpdateCalendar(baseDir: str,handle: str,postJsonObject: {}) -> None:
  1565. """Detects whether the tag list on a post contains calendar events
  1566. and if so saves the post id to a file in the calendar directory
  1567. for the account
  1568. """
  1569. if not postJsonObject.get('object'):
  1570. return
  1571. if not isinstance(postJsonObject['object'], dict):
  1572. return
  1573. if not postJsonObject['object'].get('tag'):
  1574. return
  1575. if not isinstance(postJsonObject['object']['tag'], list):
  1576. return
  1577. calendarPath=baseDir+'/accounts/'+handle+'/calendar'
  1578. if not os.path.isdir(calendarPath):
  1579. os.mkdir(calendarPath)
  1580. for tagDict in postJsonObject['object']['tag']:
  1581. if tagDict['type']!='Event':
  1582. continue
  1583. if not tagDict.get('startTime'):
  1584. continue
  1585. # get the year and month from the event
  1586. eventTime=datetime.datetime.strptime(tagDict['startTime'],"%Y-%m-%dT%H:%M:%S%z")
  1587. eventYear=int(eventTime.strftime("%Y"))
  1588. eventMonthNumber=int(eventTime.strftime("%m"))
  1589. eventDayOfMonth=int(eventTime.strftime("%d"))
  1590. if not os.path.isdir(calendarPath+'/'+str(eventYear)):
  1591. os.mkdir(calendarPath+'/'+str(eventYear))
  1592. calendarFilename=calendarPath+'/'+str(eventYear)+'/'+str(eventMonthNumber)+'.txt'
  1593. postId=postJsonObject['id'].replace('/activity','').replace('/','#')
  1594. if os.path.isfile(calendarFilename):
  1595. if postId in open(calendarFilename).read():
  1596. return
  1597. calendarFile=open(calendarFilename,'a+')
  1598. if calendarFile:
  1599. calendarFile.write(postId+'\n')
  1600. calendarFile.close()
  1601. calendarNotificationFilename=baseDir+'/accounts/'+handle+'/.newCalendar'
  1602. calendarNotificationFile=open(calendarNotificationFilename,'w')
  1603. if calendarNotificationFile:
  1604. calendarNotificationFile.write('/calendar?year='+str(eventYear)+'?month='+str(eventMonthNumber)+'?day='+str(eventDayOfMonth))
  1605. calendarNotificationFile.close()
  1606. def inboxUpdateIndex(boxname: str,baseDir: str,handle: str,destinationFilename: str,debug: bool) -> bool:
  1607. """Updates the index of received posts
  1608. The new entry is added to the top of the file
  1609. """
  1610. indexFilename=baseDir+'/accounts/'+handle+'/'+boxname+'.index'
  1611. if debug:
  1612. print('DEBUG: Updating index '+indexFilename)
  1613. if '/'+boxname+'/' in destinationFilename:
  1614. destinationFilename=destinationFilename.split('/'+boxname+'/')[1]
  1615. # remove the path
  1616. if '/' in destinationFilename:
  1617. destinationFilename=destinationFilename.split('/')[-1]
  1618. if os.path.isfile(indexFilename):
  1619. try:
  1620. with open(indexFilename, 'r+') as indexFile:
  1621. content = indexFile.read()
  1622. indexFile.seek(0, 0)
  1623. indexFile.write(destinationFilename+'\n'+content)
  1624. return True
  1625. except Exception as e:
  1626. print('WARN: Failed to write entry to index '+str(e))
  1627. else:
  1628. try:
  1629. indexFile=open(indexFilename,'w+')
  1630. if indexFile:
  1631. indexFile.write(destinationFilename+'\n')
  1632. indexFile.close()
  1633. except Exception as e:
  1634. print('WARN: Failed to write initial entry to index '+str(e))
  1635. return False
  1636. def inboxAfterCapabilities(recentPostsCache: {},maxRecentPosts: int, \
  1637. session,keyId: str,handle: str,messageJson: {}, \
  1638. baseDir: str,httpPrefix: str,sendThreads: [], \
  1639. postLog: [],cachedWebfingers: {},personCache: {}, \
  1640. queue: [],domain: str,port: int,useTor: bool, \
  1641. federationList: [],ocapAlways: bool,debug: bool, \
  1642. acceptedCaps: [], \
  1643. queueFilename :str,destinationFilename :str, \
  1644. maxReplies: int,allowDeletion: bool, \
  1645. maxMentions: int,maxEmoji: int,translate: {}, \
  1646. unitTest: bool) -> bool:
  1647. """ Anything which needs to be done after capabilities checks have passed
  1648. """
  1649. actor=keyId
  1650. if '#' in actor:
  1651. actor=keyId.split('#')[0]
  1652. isGroup=groupHandle(baseDir,handle)
  1653. if receiveLike(recentPostsCache, \
  1654. session,handle,isGroup, \
  1655. baseDir,httpPrefix, \
  1656. domain,port, \
  1657. sendThreads,postLog, \
  1658. cachedWebfingers, \
  1659. personCache, \
  1660. messageJson, \
  1661. federationList, \
  1662. debug):
  1663. if debug:
  1664. print('DEBUG: Like accepted from '+actor)
  1665. return False
  1666. if receiveUndoLike(recentPostsCache, \
  1667. session,handle,isGroup, \
  1668. baseDir,httpPrefix, \
  1669. domain,port, \
  1670. sendThreads,postLog, \
  1671. cachedWebfingers, \
  1672. personCache, \
  1673. messageJson, \
  1674. federationList, \
  1675. debug):
  1676. if debug:
  1677. print('DEBUG: Undo like accepted from '+actor)
  1678. return False
  1679. if receiveBookmark(recentPostsCache, \
  1680. session,handle,isGroup, \
  1681. baseDir,httpPrefix, \
  1682. domain,port, \
  1683. sendThreads,postLog, \
  1684. cachedWebfingers, \
  1685. personCache, \
  1686. messageJson, \
  1687. federationList, \
  1688. debug):
  1689. if debug:
  1690. print('DEBUG: Bookmark accepted from '+actor)
  1691. return False
  1692. if receiveUndoBookmark(recentPostsCache, \
  1693. session,handle,isGroup, \
  1694. baseDir,httpPrefix, \
  1695. domain,port, \
  1696. sendThreads,postLog, \
  1697. cachedWebfingers, \
  1698. personCache, \
  1699. messageJson, \
  1700. federationList, \
  1701. debug):
  1702. if debug:
  1703. print('DEBUG: Undo bookmark accepted from '+actor)
  1704. return False
  1705. if receiveAnnounce(recentPostsCache, \
  1706. session,handle,isGroup, \
  1707. baseDir,httpPrefix, \
  1708. domain,port, \
  1709. sendThreads,postLog, \
  1710. cachedWebfingers, \
  1711. personCache, \
  1712. messageJson, \
  1713. federationList, \
  1714. debug):
  1715. if debug:
  1716. print('DEBUG: Announce accepted from '+actor)
  1717. if receiveUndoAnnounce(recentPostsCache, \
  1718. session,handle,isGroup, \
  1719. baseDir,httpPrefix, \
  1720. domain,port, \
  1721. sendThreads,postLog, \
  1722. cachedWebfingers, \
  1723. personCache, \
  1724. messageJson, \
  1725. federationList, \
  1726. debug):
  1727. if debug:
  1728. print('DEBUG: Undo announce accepted from '+actor)
  1729. return False
  1730. if receiveDelete(session,handle,isGroup, \
  1731. baseDir,httpPrefix, \
  1732. domain,port, \
  1733. sendThreads,postLog, \
  1734. cachedWebfingers, \
  1735. personCache, \
  1736. messageJson, \
  1737. federationList, \
  1738. debug,allowDeletion):
  1739. if debug:
  1740. print('DEBUG: Delete accepted from '+actor)
  1741. return False
  1742. if debug:
  1743. print('DEBUG: object capabilities passed')
  1744. print('copy queue file from '+queueFilename+' to '+destinationFilename)
  1745. if os.path.isfile(destinationFilename):
  1746. return True
  1747. if messageJson.get('postNickname'):
  1748. postJsonObject=messageJson['post']
  1749. else:
  1750. postJsonObject=messageJson
  1751. if validPostContent(postJsonObject,maxMentions,maxEmoji):
  1752. # replace YouTube links, so they get less tracking data
  1753. replaceYouTube(postJsonObject)
  1754. # list of indexes to be updated
  1755. updateIndexList=['inbox']
  1756. populateReplies(baseDir,httpPrefix,domain,postJsonObject,maxReplies,debug)
  1757. nickname=handle.split('@')[0]
  1758. # if this is a reply to a question then update the votes
  1759. questionJson=questionUpdateVotes(baseDir,nickname,domain,postJsonObject)
  1760. if questionJson:
  1761. # Is this a question created by this instance?
  1762. if questionJson['object']['id'].startswith(httpPrefix+'://'+domain):
  1763. # if the votes on a question have changed then send out an update
  1764. questionJson['type']='Update'
  1765. sendToFollowersThread(session,baseDir, \
  1766. nickname,domain,port, \
  1767. httpPrefix,federationList, \
  1768. sendThreads,postLog, \
  1769. cachedWebfingers,personCache, \
  1770. postJsonObject,debug, \
  1771. __version__)
  1772. if not isGroup:
  1773. # create a DM notification file if needed
  1774. if isDM(postJsonObject):
  1775. if nickname!='inbox':
  1776. followDMsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/.followDMs'
  1777. if os.path.isfile(followDMsFilename):
  1778. followingFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/following.txt'
  1779. if not postJsonObject.get('actor'):
  1780. return False
  1781. sendingActor=postJsonObject['actor']
  1782. sendingActorNickname=getNicknameFromActor(sendingActor)
  1783. sendingActorDomain,sendingActorPort=getDomainFromActor(sendingActor)
  1784. if sendingActorNickname and sendingActorDomain:
  1785. if sendingActorNickname+'@'+sendingActorDomain != nickname+'@'+domain:
  1786. if sendingActorNickname+'@'+sendingActorDomain not in open(followingFilename).read():
  1787. print(nickname+'@'+domain+' cannot receive DM from '+sendingActorNickname+'@'+sendingActorDomain+' because they do not follow them')
  1788. return False
  1789. else:
  1790. return False
  1791. # dm index will be updated
  1792. updateIndexList.append('dm')
  1793. dmNotify(baseDir,handle,httpPrefix+'://'+domain+'/users/'+nickname+'/dm')
  1794. # get the actor being replied to
  1795. domainFull=domain
  1796. if port:
  1797. if ':' not in domain:
  1798. if port!=80 and port!=443:
  1799. domainFull=domainFull+':'+str(port)
  1800. actor=httpPrefix+'://'+domainFull+'/users/'+handle.split('@')[0]
  1801. # create a reply notification file if needed
  1802. if isReply(postJsonObject,actor):
  1803. if nickname!='inbox':
  1804. # replies index will be updated
  1805. updateIndexList.append('tlreplies')
  1806. replyNotify(baseDir,handle,httpPrefix+'://'+domain+'/users/'+nickname+'/tlreplies')
  1807. if isImageMedia(session,baseDir,httpPrefix,nickname,domain,postJsonObject):
  1808. # media index will be updated
  1809. updateIndexList.append('tlmedia')
  1810. # get the avatar for a reply/announce
  1811. obtainAvatarForReplyPost(session,baseDir,httpPrefix,domain,personCache,postJsonObject,debug)
  1812. # save the post to file
  1813. if saveJson(postJsonObject,destinationFilename):
  1814. # update the indexes for different timelines
  1815. for boxname in updateIndexList:
  1816. if not inboxUpdateIndex(boxname,baseDir,handle,destinationFilename,debug):
  1817. print('ERROR: unable to update '+boxname+' index')
  1818. inboxUpdateCalendar(baseDir,handle,postJsonObject)
  1819. storeHashTags(baseDir,handle.split('@')[0],postJsonObject)
  1820. if not unitTest:
  1821. if debug:
  1822. print('DEBUG: saving inbox post as html to cache')
  1823. htmlCacheStartTime=time.time()
  1824. inboxStorePostToHtmlCache(recentPostsCache,maxRecentPosts, \
  1825. translate,baseDir,httpPrefix, \
  1826. session,cachedWebfingers,personCache, \
  1827. handle.split('@')[0],domain,port, \
  1828. postJsonObject,allowDeletion)
  1829. if debug:
  1830. timeDiff=str(int((time.time()-htmlCacheStartTime)*1000))
  1831. print('DEBUG: saved inbox post as html to cache in '+timeDiff+' mS')
  1832. # send the post out to group members
  1833. if isGroup:
  1834. sendToGroupMembers(session,baseDir,handle,port,postJsonObject, \
  1835. httpPrefix,federationList,sendThreads, \
  1836. postLog,cachedWebfingers,personCache,debug)
  1837. # if the post wasn't saved
  1838. if not os.path.isfile(destinationFilename):
  1839. return False
  1840. return True
  1841. def restoreQueueItems(baseDir: str,queue: []) -> None:
  1842. """Checks the queue for each account and appends filenames
  1843. """
  1844. queue.clear()
  1845. for subdir,dirs,files in os.walk(baseDir+'/accounts'):
  1846. for account in dirs:
  1847. queueDir=baseDir+'/accounts/'+account+'/queue'
  1848. if os.path.isdir(queueDir):
  1849. for queuesubdir,queuedirs,queuefiles in os.walk(queueDir):
  1850. for qfile in queuefiles:
  1851. queue.append(os.path.join(queueDir, qfile))
  1852. if len(queue)>0:
  1853. print('Restored '+str(len(queue))+' inbox queue items')
  1854. def runInboxQueueWatchdog(projectVersion: str,httpd) -> None:
  1855. """This tries to keep the inbox thread running even if it dies
  1856. """
  1857. print('Starting inbox queue watchdog')
  1858. inboxQueueOriginal=httpd.thrInboxQueue.clone(runInboxQueue)
  1859. #httpd.thrInboxQueue=inboxQueueOriginal
  1860. httpd.thrInboxQueue.start()
  1861. while True:
  1862. time.sleep(20)
  1863. if not httpd.thrInboxQueue.isAlive():
  1864. httpd.thrInboxQueue.kill()
  1865. httpd.thrInboxQueue=inboxQueueOriginal.clone(runInboxQueue)
  1866. httpd.thrInboxQueue.start()
  1867. print('Restarting inbox queue...')
  1868. def runInboxQueue(recentPostsCache: {},maxRecentPosts: int, \
  1869. projectVersion: str, \
  1870. baseDir: str,httpPrefix: str,sendThreads: [],postLog: [], \
  1871. cachedWebfingers: {},personCache: {},queue: [], \
  1872. domain: str,port: int,useTor: bool,federationList: [], \
  1873. ocapAlways: bool,maxReplies: int, \
  1874. domainMaxPostsPerDay: int,accountMaxPostsPerDay: int, \
  1875. allowDeletion: bool,debug: bool,maxMentions: int, \
  1876. maxEmoji: int,translate: {},unitTest: bool, \
  1877. acceptedCaps=["inbox:write","objects:read"]) -> None:
  1878. """Processes received items and moves them to
  1879. the appropriate directories
  1880. """
  1881. currSessionTime=int(time.time())
  1882. sessionLastUpdate=currSessionTime
  1883. session=createSession(useTor)
  1884. inboxHandle='inbox@'+domain
  1885. if debug:
  1886. print('DEBUG: Inbox queue running')
  1887. # if queue processing was interrupted (eg server crash)
  1888. # then this loads any outstanding items back into the queue
  1889. restoreQueueItems(baseDir,queue)
  1890. # keep track of numbers of incoming posts per unit of time
  1891. quotasLastUpdate=int(time.time())
  1892. quotas={
  1893. 'domains': {},
  1894. 'accounts': {}
  1895. }
  1896. heartBeatCtr=0
  1897. queueRestoreCtr=0
  1898. while True:
  1899. time.sleep(5)
  1900. # heartbeat to monitor whether the inbox queue is running
  1901. heartBeatCtr+=5
  1902. if heartBeatCtr>=10:
  1903. print('>>> Heartbeat Q:{:d} {:%F %T}'.format(len(queue), datetime.datetime.now()))
  1904. heartBeatCtr=0
  1905. if len(queue)==0:
  1906. # restore any remaining queue items
  1907. queueRestoreCtr+=1
  1908. if queueRestoreCtr>=30:
  1909. queueRestoreCtr=0
  1910. restoreQueueItems(baseDir,queue)
  1911. else:
  1912. currTime=int(time.time())
  1913. # recreate the session periodically
  1914. if not session or currTime-sessionLastUpdate>1200:
  1915. print('Creating inbox session')
  1916. session=createSession(useTor)
  1917. sessionLastUpdate=currTime
  1918. # oldest item first
  1919. queue.sort()
  1920. queueFilename=queue[0]
  1921. if not os.path.isfile(queueFilename):
  1922. if debug:
  1923. print("DEBUG: queue item rejected because it has no file: "+queueFilename)
  1924. if len(queue)>0:
  1925. queue.pop(0)
  1926. continue
  1927. print('Loading queue item '+queueFilename)
  1928. # Load the queue json
  1929. queueJson=loadJson(queueFilename,1)
  1930. if not queueJson:
  1931. print('WARN: runInboxQueue failed to load inbox queue item '+queueFilename)
  1932. # Assume that the file is probably corrupt/unreadable
  1933. if len(queue)>0:
  1934. queue.pop(0)
  1935. # delete the queue file
  1936. if os.path.isfile(queueFilename):
  1937. try:
  1938. os.remove(queueFilename)
  1939. except:
  1940. pass
  1941. continue
  1942. # clear the daily quotas for maximum numbers of received posts
  1943. if currTime-quotasLastUpdate>60*60*24:
  1944. quotas={
  1945. 'domains': {},
  1946. 'accounts': {}
  1947. }
  1948. quotasLastUpdate=currTime
  1949. # limit the number of posts which can arrive per domain per day
  1950. postDomain=queueJson['postDomain']
  1951. if postDomain:
  1952. if domainMaxPostsPerDay>0:
  1953. if quotas['domains'].get(postDomain):
  1954. if quotas['domains'][postDomain]>domainMaxPostsPerDay:
  1955. if debug:
  1956. print('DEBUG: Maximum posts for '+postDomain+' reached')
  1957. if len(queue)>0:
  1958. queue.pop(0)
  1959. continue
  1960. quotas['domains'][postDomain]+=1
  1961. else:
  1962. quotas['domains'][postDomain]=1
  1963. if accountMaxPostsPerDay>0:
  1964. postHandle=queueJson['postNickname']+'@'+postDomain
  1965. if quotas['accounts'].get(postHandle):
  1966. if quotas['accounts'][postHandle]>accountMaxPostsPerDay:
  1967. if debug:
  1968. print('DEBUG: Maximum posts for '+postHandle+' reached')
  1969. if len(queue)>0:
  1970. queue.pop(0)
  1971. continue
  1972. quotas['accounts'][postHandle]+=1
  1973. else:
  1974. quotas['accounts'][postHandle]=1
  1975. if debug:
  1976. if accountMaxPostsPerDay>0 or domainMaxPostsPerDay>0:
  1977. pprint(quotas)
  1978. print('Obtaining public key for actor '+queueJson['actor'])
  1979. # Try a few times to obtain the public key
  1980. pubKey=None
  1981. keyId=None
  1982. for tries in range(8):
  1983. keyId=None
  1984. signatureParams=queueJson['httpHeaders']['signature'].split(',')
  1985. for signatureItem in signatureParams:
  1986. if signatureItem.startswith('keyId='):
  1987. if '"' in signatureItem:
  1988. keyId=signatureItem.split('"')[1]
  1989. break
  1990. if not keyId:
  1991. if debug:
  1992. print('DEBUG: No keyId in signature: '+ \
  1993. queueJson['httpHeaders']['signature'])
  1994. if os.path.isfile(queueFilename):
  1995. os.remove(queueFilename)
  1996. if len(queue)>0:
  1997. queue.pop(0)
  1998. continue
  1999. pubKey= \
  2000. getPersonPubKey(baseDir,session,keyId, \
  2001. personCache,debug, \
  2002. projectVersion,httpPrefix,domain)
  2003. if pubKey:
  2004. if debug:
  2005. print('DEBUG: public key: '+str(pubKey))
  2006. break
  2007. if debug:
  2008. print('DEBUG: Retry '+str(tries+1)+ \
  2009. ' obtaining public key for '+keyId)
  2010. time.sleep(5)
  2011. if not pubKey:
  2012. if debug:
  2013. print('DEBUG: public key could not be obtained from '+keyId)
  2014. if os.path.isfile(queueFilename):
  2015. os.remove(queueFilename)
  2016. if len(queue)>0:
  2017. queue.pop(0)
  2018. continue
  2019. # check the signature
  2020. if debug:
  2021. print('DEBUG: checking http headers')
  2022. pprint(queueJson['httpHeaders'])
  2023. if not verifyPostHeaders(httpPrefix, \
  2024. pubKey, \
  2025. queueJson['httpHeaders'], \
  2026. queueJson['path'],False, \
  2027. queueJson['digest'], \
  2028. json.dumps(queueJson['post']), \
  2029. debug):
  2030. if debug:
  2031. print('DEBUG: Header signature check failed')
  2032. if os.path.isfile(queueFilename):
  2033. os.remove(queueFilename)
  2034. if len(queue)>0:
  2035. queue.pop(0)
  2036. continue
  2037. if debug:
  2038. print('DEBUG: Signature check success')
  2039. # set the id to the same as the post filename
  2040. # This makes the filename and the id consistent
  2041. #if queueJson['post'].get('id'):
  2042. # queueJson['post']['id']=queueJson['id']
  2043. if receiveUndo(session, \
  2044. baseDir,httpPrefix,port, \
  2045. sendThreads,postLog, \
  2046. cachedWebfingers,
  2047. personCache, \
  2048. queueJson['post'], \
  2049. federationList, \
  2050. debug, \
  2051. acceptedCaps=["inbox:write","objects:read"]):
  2052. if debug:
  2053. print('DEBUG: Undo accepted from '+keyId)
  2054. if os.path.isfile(queueFilename):
  2055. os.remove(queueFilename)
  2056. if len(queue)>0:
  2057. queue.pop(0)
  2058. continue
  2059. if debug:
  2060. print('DEBUG: checking for follow requests')
  2061. if receiveFollowRequest(session, \
  2062. baseDir,httpPrefix,port, \
  2063. sendThreads,postLog, \
  2064. cachedWebfingers,
  2065. personCache, \
  2066. queueJson['post'], \
  2067. federationList, \
  2068. debug,projectVersion, \
  2069. acceptedCaps=["inbox:write","objects:read"]):
  2070. if os.path.isfile(queueFilename):
  2071. os.remove(queueFilename)
  2072. if len(queue)>0:
  2073. queue.pop(0)
  2074. if debug:
  2075. print('DEBUG: Follow activity for '+keyId+' removed from accepted from queue')
  2076. continue
  2077. else:
  2078. if debug:
  2079. print('DEBUG: No follow requests')
  2080. if receiveAcceptReject(session, \
  2081. baseDir,httpPrefix,domain,port, \
  2082. sendThreads,postLog, \
  2083. cachedWebfingers, \
  2084. personCache, \
  2085. queueJson['post'], \
  2086. federationList, \
  2087. debug):
  2088. if debug:
  2089. print('DEBUG: Accept/Reject received from '+keyId)
  2090. if os.path.isfile(queueFilename):
  2091. os.remove(queueFilename)
  2092. if len(queue)>0:
  2093. queue.pop(0)
  2094. continue
  2095. if receiveUpdate(recentPostsCache,session, \
  2096. baseDir,httpPrefix, \
  2097. domain,port, \
  2098. sendThreads,postLog, \
  2099. cachedWebfingers, \
  2100. personCache, \
  2101. queueJson['post'], \
  2102. federationList, \
  2103. queueJson['postNickname'], \
  2104. debug):
  2105. if debug:
  2106. print('DEBUG: Update accepted from '+keyId)
  2107. if os.path.isfile(queueFilename):
  2108. os.remove(queueFilename)
  2109. if len(queue)>0:
  2110. queue.pop(0)
  2111. continue
  2112. # get recipients list
  2113. recipientsDict,recipientsDictFollowers= \
  2114. inboxPostRecipients(baseDir,queueJson['post'], \
  2115. httpPrefix,domain,port,debug)
  2116. if len(recipientsDict.items())==0 and \
  2117. len(recipientsDictFollowers.items())==0:
  2118. if debug:
  2119. pprint(queueJson['post'])
  2120. print('DEBUG: no recipients were resolved for post arriving in inbox')
  2121. if os.path.isfile(queueFilename):
  2122. os.remove(queueFilename)
  2123. if len(queue)>0:
  2124. queue.pop(0)
  2125. continue
  2126. # if there are only a small number of followers then process them as if they
  2127. # were specifically addresses to particular accounts
  2128. noOfFollowItems=len(recipientsDictFollowers.items())
  2129. if noOfFollowItems>0:
  2130. # always deliver to individual inboxes
  2131. if noOfFollowItems<999999:
  2132. if debug:
  2133. print('DEBUG: moving '+str(noOfFollowItems)+ \
  2134. ' inbox posts addressed to followers')
  2135. for handle,postItem in recipientsDictFollowers.items():
  2136. recipientsDict[handle]=postItem
  2137. recipientsDictFollowers={}
  2138. recipientsList=[recipientsDict,recipientsDictFollowers]
  2139. if debug:
  2140. print('*************************************')
  2141. print('Resolved recipients list:')
  2142. pprint(recipientsDict)
  2143. print('Resolved followers list:')
  2144. pprint(recipientsDictFollowers)
  2145. print('*************************************')
  2146. if queueJson['post'].get('capability'):
  2147. if not isinstance(queueJson['post']['capability'], list):
  2148. if debug:
  2149. print('DEBUG: capability on post should be a list')
  2150. if os.path.isfile(queueFilename):
  2151. os.remove(queueFilename)
  2152. if len(queue)>0:
  2153. queue.pop(0)
  2154. continue
  2155. # Copy any posts addressed to followers into the shared inbox
  2156. # this avoid copying file multiple times to potentially many
  2157. # individual inboxes
  2158. # This obviously bypasses object capabilities and so
  2159. # any checking will needs to be handled at the time when inbox
  2160. # GET happens on individual accounts.
  2161. # See posts.py/createBoxBase
  2162. if len(recipientsDictFollowers)>0:
  2163. sharedInboxPostFilename=queueJson['destination'].replace(inboxHandle,inboxHandle)
  2164. if not os.path.isfile(sharedInboxPostFilename):
  2165. saveJson(queueJson['post'],sharedInboxPostFilename)
  2166. # for posts addressed to specific accounts
  2167. for handle,capsId in recipientsDict.items():
  2168. destination=queueJson['destination'].replace(inboxHandle,handle)
  2169. # check that capabilities are accepted
  2170. if queueJson['post'].get('capability'):
  2171. capabilityIdList=queueJson['post']['capability']
  2172. # does the capability id list within the post contain the id
  2173. # of the recipient with this handle?
  2174. # Here the capability id begins with the handle, so this could also
  2175. # be matched separately, but it's probably not necessary
  2176. if capsId in capabilityIdList:
  2177. inboxAfterCapabilities(recentPostsCache,maxRecentPosts, \
  2178. session,keyId,handle, \
  2179. queueJson['post'], \
  2180. baseDir,httpPrefix, \
  2181. sendThreads,postLog, \
  2182. cachedWebfingers, \
  2183. personCache,queue,domain, \
  2184. port,useTor, \
  2185. federationList,ocapAlways, \
  2186. debug,acceptedCaps, \
  2187. queueFilename,destination, \
  2188. maxReplies,allowDeletion, \
  2189. maxMentions,maxEmoji, \
  2190. translate,unitTest)
  2191. else:
  2192. if debug:
  2193. print('DEBUG: object capabilities check has failed')
  2194. pprint(queueJson['post'])
  2195. else:
  2196. if not ocapAlways:
  2197. inboxAfterCapabilities(recentPostsCache,maxRecentPosts, \
  2198. session,keyId,handle, \
  2199. queueJson['post'], \
  2200. baseDir,httpPrefix, \
  2201. sendThreads,postLog, \
  2202. cachedWebfingers, \
  2203. personCache,queue,domain, \
  2204. port,useTor, \
  2205. federationList,ocapAlways, \
  2206. debug,acceptedCaps, \
  2207. queueFilename,destination, \
  2208. maxReplies,allowDeletion, \
  2209. maxMentions,maxEmoji, \
  2210. translate,unitTest)
  2211. if debug:
  2212. pprint(queueJson['post'])
  2213. print('No capability list within post')
  2214. print('ocapAlways: '+str(ocapAlways))
  2215. print('DEBUG: object capabilities check failed')
  2216. if debug:
  2217. print('DEBUG: Queue post accepted')
  2218. if os.path.isfile(queueFilename):
  2219. os.remove(queueFilename)
  2220. if len(queue)>0:
  2221. queue.pop(0)