inbox.py 96 KB

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