posts.py 76 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887
  1. __filename__ = "posts.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import requests
  9. import json
  10. import commentjson
  11. import html
  12. import datetime
  13. import os
  14. import shutil
  15. import threading
  16. import sys
  17. import trace
  18. import time
  19. from collections import OrderedDict
  20. from threads import threadWithTrace
  21. from cache import storePersonInCache
  22. from cache import getPersonFromCache
  23. from pprint import pprint
  24. from random import randint
  25. from session import createSession
  26. from session import getJson
  27. from session import postJsonString
  28. from session import postImage
  29. from webfinger import webfingerHandle
  30. from httpsig import createSignedHeader
  31. from utils import getStatusNumber
  32. from utils import createPersonDir
  33. from utils import urlPermitted
  34. from utils import getNicknameFromActor
  35. from utils import getDomainFromActor
  36. from utils import deletePost
  37. from utils import validNickname
  38. from capabilities import getOcapFilename
  39. from capabilities import capabilitiesUpdate
  40. from media import attachImage
  41. from content import addHtmlTags
  42. from auth import createBasicAuthHeader
  43. from config import getConfigParam
  44. try:
  45. from BeautifulSoup import BeautifulSoup
  46. except ImportError:
  47. from bs4 import BeautifulSoup
  48. def isModerator(baseDir: str,nickname: str) -> bool:
  49. """Returns true if the given nickname is a moderator
  50. """
  51. moderatorsFile=baseDir+'/accounts/moderators.txt'
  52. if not os.path.isfile(moderatorsFile):
  53. if getConfigParam(baseDir,'admin')==nickname:
  54. return True
  55. return False
  56. with open(moderatorsFile, "r") as f:
  57. lines = f.readlines()
  58. if len(lines)==0:
  59. if getConfigParam(baseDir,'admin')==nickname:
  60. return True
  61. for moderator in lines:
  62. moderator=moderator.strip('\n')
  63. if moderator==nickname:
  64. return True
  65. return False
  66. def noOfFollowersOnDomain(baseDir: str,handle: str, \
  67. domain: str, followFile='followers.txt') -> int:
  68. """Returns the number of followers of the given handle from the given domain
  69. """
  70. filename=baseDir+'/accounts/'+handle+'/'+followFile
  71. if not os.path.isfile(filename):
  72. return 0
  73. ctr=0
  74. with open(filename, "r") as followersFilename:
  75. for followerHandle in followersFilename:
  76. if '@' in followerHandle:
  77. followerDomain= \
  78. followerHandle.split('@')[1].replace('\n','')
  79. if domain==followerDomain:
  80. ctr+=1
  81. return ctr
  82. def getPersonKey(nickname: str,domain: str,baseDir: str,keyType='public', \
  83. debug=False):
  84. """Returns the public or private key of a person
  85. """
  86. handle=nickname+'@'+domain
  87. keyFilename=baseDir+'/keys/'+keyType+'/'+handle.lower()+'.key'
  88. if not os.path.isfile(keyFilename):
  89. if debug:
  90. print('DEBUG: private key file not found: '+keyFilename)
  91. return ''
  92. keyPem=''
  93. with open(keyFilename, "r") as pemFile:
  94. keyPem=pemFile.read()
  95. if len(keyPem)<20:
  96. if debug:
  97. print('DEBUG: private key was too short: '+keyPem)
  98. return ''
  99. return keyPem
  100. def cleanHtml(rawHtml: str) -> str:
  101. text = BeautifulSoup(rawHtml, 'html.parser').get_text()
  102. return html.unescape(text)
  103. def getUserUrl(wfRequest) -> str:
  104. if wfRequest.get('links'):
  105. for link in wfRequest['links']:
  106. if link.get('type') and link.get('href'):
  107. if link['type'] == 'application/activity+json':
  108. return link['href']
  109. return None
  110. def parseUserFeed(session,feedUrl: str,asHeader: {}, \
  111. projectVersion: str,httpPrefix: str,domain: str) -> None:
  112. feedJson = getJson(session,feedUrl,asHeader,None, \
  113. projectVersion,httpPrefix,domain)
  114. if not feedJson:
  115. return
  116. if 'orderedItems' in feedJson:
  117. for item in feedJson['orderedItems']:
  118. yield item
  119. nextUrl = None
  120. if 'first' in feedJson:
  121. nextUrl = feedJson['first']
  122. elif 'next' in feedJson:
  123. nextUrl = feedJson['next']
  124. if nextUrl:
  125. for item in parseUserFeed(session,nextUrl,asHeader, \
  126. projectVersion,httpPrefix,domain):
  127. yield item
  128. def getPersonBox(session,wfRequest: {},personCache: {}, \
  129. projectVersion: str,httpPrefix: str,domain: str, \
  130. boxName='inbox') -> (str,str,str,str,str,str,str,str):
  131. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  132. personUrl = getUserUrl(wfRequest)
  133. if not personUrl:
  134. return None,None,None,None,None,None,None,None
  135. personJson = getPersonFromCache(personUrl,personCache)
  136. if not personJson:
  137. personJson = getJson(session,personUrl,asHeader,None, \
  138. projectVersion,httpPrefix,domain)
  139. if not personJson:
  140. return None,None,None,None,None,None,None,None
  141. boxJson=None
  142. if not personJson.get(boxName):
  143. if personJson.get('endpoints'):
  144. if personJson['endpoints'].get(boxName):
  145. boxJson=personJson['endpoints'][boxName]
  146. else:
  147. boxJson=personJson[boxName]
  148. if not boxJson:
  149. return None,None,None,None,None,None,None,None
  150. personId=None
  151. if personJson.get('id'):
  152. personId=personJson['id']
  153. pubKeyId=None
  154. pubKey=None
  155. if personJson.get('publicKey'):
  156. if personJson['publicKey'].get('id'):
  157. pubKeyId=personJson['publicKey']['id']
  158. if personJson['publicKey'].get('publicKeyPem'):
  159. pubKey=personJson['publicKey']['publicKeyPem']
  160. sharedInbox=None
  161. if personJson.get('sharedInbox'):
  162. sharedInbox=personJson['sharedInbox']
  163. else:
  164. if personJson.get('endpoints'):
  165. if personJson['endpoints'].get('sharedInbox'):
  166. sharedInbox=personJson['endpoints']['sharedInbox']
  167. capabilityAcquisition=None
  168. if personJson.get('capabilityAcquisitionEndpoint'):
  169. capabilityAcquisition=personJson['capabilityAcquisitionEndpoint']
  170. avatarUrl=None
  171. if personJson.get('icon'):
  172. if personJson['icon'].get('url'):
  173. avatarUrl=personJson['icon']['url']
  174. preferredName=None
  175. if personJson.get('preferredUsername'):
  176. preferredName=personJson['preferredUsername']
  177. storePersonInCache(personUrl,personJson,personCache)
  178. return boxJson,pubKeyId,pubKey,personId,sharedInbox,capabilityAcquisition,avatarUrl,preferredName
  179. def getPosts(session,outboxUrl: str,maxPosts: int, \
  180. maxMentions: int, \
  181. maxEmoji: int,maxAttachments: int, \
  182. federationList: [], \
  183. personCache: {},raw: bool, \
  184. simple: bool,debug: bool, \
  185. projectVersion: str,httpPrefix: str,domain: str) -> {}:
  186. """Gets public posts from an outbox
  187. """
  188. personPosts={}
  189. if not outboxUrl:
  190. return personPosts
  191. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  192. if raw:
  193. result = []
  194. i = 0
  195. for item in parseUserFeed(session,outboxUrl,asHeader, \
  196. projectVersion,httpPrefix,domain):
  197. result.append(item)
  198. i += 1
  199. if i == maxPosts:
  200. break
  201. pprint(result)
  202. return None
  203. i = 0
  204. for item in parseUserFeed(session,outboxUrl,asHeader, \
  205. projectVersion,httpPrefix,domain):
  206. if not item.get('id'):
  207. if debug:
  208. print('No id')
  209. continue
  210. if not item.get('type'):
  211. if debug:
  212. print('No type')
  213. continue
  214. if item['type'] != 'Create':
  215. if debug:
  216. print('Not Create type')
  217. continue
  218. if not item.get('object'):
  219. if debug:
  220. print('No object')
  221. continue
  222. if not isinstance(item['object'], dict):
  223. if debug:
  224. print('item object is not a dict')
  225. continue
  226. if not item['object'].get('published'):
  227. if debug:
  228. print('No published attribute')
  229. continue
  230. #pprint(item)
  231. published = item['object']['published']
  232. if not personPosts.get(item['id']):
  233. # check that this is a public post
  234. # #Public should appear in the "to" list
  235. if item['object'].get('to'):
  236. isPublic=False
  237. for recipient in item['object']['to']:
  238. if recipient.endswith('#Public'):
  239. isPublic=True
  240. break
  241. if not isPublic:
  242. continue
  243. content = item['object']['content'].replace('&apos;',"'")
  244. mentions=[]
  245. emoji={}
  246. if item['object'].get('tag'):
  247. for tagItem in item['object']['tag']:
  248. tagType=tagItem['type'].lower()
  249. if tagType=='emoji':
  250. if tagItem.get('name') and tagItem.get('icon'):
  251. if tagItem['icon'].get('url'):
  252. # No emoji from non-permitted domains
  253. if urlPermitted(tagItem['icon']['url'], \
  254. federationList, \
  255. "objects:read"):
  256. emojiName=tagItem['name']
  257. emojiIcon=tagItem['icon']['url']
  258. emoji[emojiName]=emojiIcon
  259. else:
  260. if debug:
  261. print('url not permitted '+tagItem['icon']['url'])
  262. if tagType=='mention':
  263. if tagItem.get('name'):
  264. if tagItem['name'] not in mentions:
  265. mentions.append(tagItem['name'])
  266. if len(mentions)>maxMentions:
  267. if debug:
  268. print('max mentions reached')
  269. continue
  270. if len(emoji)>maxEmoji:
  271. if debug:
  272. print('max emojis reached')
  273. continue
  274. summary = ''
  275. if item['object'].get('summary'):
  276. if item['object']['summary']:
  277. summary = item['object']['summary']
  278. inReplyTo = ''
  279. if item['object'].get('inReplyTo'):
  280. if item['object']['inReplyTo']:
  281. # No replies to non-permitted domains
  282. if not urlPermitted(item['object']['inReplyTo'], \
  283. federationList, \
  284. "objects:read"):
  285. if debug:
  286. print('url not permitted '+item['object']['inReplyTo'])
  287. continue
  288. inReplyTo = item['object']['inReplyTo']
  289. conversation = ''
  290. if item['object'].get('conversation'):
  291. if item['object']['conversation']:
  292. # no conversations originated in non-permitted domains
  293. if urlPermitted(item['object']['conversation'], \
  294. federationList,"objects:read"):
  295. conversation = item['object']['conversation']
  296. attachment = []
  297. if item['object'].get('attachment'):
  298. if item['object']['attachment']:
  299. for attach in item['object']['attachment']:
  300. if attach.get('name') and attach.get('url'):
  301. # no attachments from non-permitted domains
  302. if urlPermitted(attach['url'], \
  303. federationList, \
  304. "objects:read"):
  305. attachment.append([attach['name'],attach['url']])
  306. else:
  307. if debug:
  308. print('url not permitted '+attach['url'])
  309. sensitive = False
  310. if item['object'].get('sensitive'):
  311. sensitive = item['object']['sensitive']
  312. if simple:
  313. print(cleanHtml(content)+'\n')
  314. else:
  315. pprint(item)
  316. personPosts[item['id']] = {
  317. "sensitive": sensitive,
  318. "inreplyto": inReplyTo,
  319. "summary": summary,
  320. "html": content,
  321. "plaintext": cleanHtml(content),
  322. "attachment": attachment,
  323. "mentions": mentions,
  324. "emoji": emoji,
  325. "conversation": conversation
  326. }
  327. i += 1
  328. if i == maxPosts:
  329. break
  330. return personPosts
  331. def deleteAllPosts(baseDir: str,nickname: str, domain: str,boxname: str) -> None:
  332. """Deletes all posts for a person from inbox or outbox
  333. """
  334. if boxname!='inbox' and boxname!='outbox':
  335. return
  336. boxDir = createPersonDir(nickname,domain,baseDir,boxname)
  337. for deleteFilename in os.listdir(boxDir):
  338. filePath = os.path.join(boxDir, deleteFilename)
  339. try:
  340. if os.path.isfile(filePath):
  341. os.unlink(filePath)
  342. elif os.path.isdir(filePath): shutil.rmtree(filePath)
  343. except Exception as e:
  344. print(e)
  345. def savePostToBox(baseDir: str,httpPrefix: str,postId: str, \
  346. nickname: str, domain: str,postJsonObject: {}, \
  347. boxname: str) -> str:
  348. """Saves the give json to the give box
  349. Returns the filename
  350. """
  351. if boxname!='inbox' and boxname!='outbox':
  352. return None
  353. originalDomain=domain
  354. if ':' in domain:
  355. domain=domain.split(':')[0]
  356. if not postId:
  357. statusNumber,published = getStatusNumber()
  358. postId=httpPrefix+'://'+originalDomain+'/users/'+nickname+'/statuses/'+statusNumber
  359. postJsonObject['id']=postId+'/activity'
  360. if postJsonObject.get('object'):
  361. if isinstance(postJsonObject['object'], dict):
  362. postJsonObject['object']['id']=postId
  363. postJsonObject['object']['atomUri']=postId
  364. boxDir = createPersonDir(nickname,domain,baseDir,boxname)
  365. filename=boxDir+'/'+postId.replace('/','#')+'.json'
  366. with open(filename, 'w') as fp:
  367. commentjson.dump(postJsonObject, fp, indent=4, sort_keys=False)
  368. return filename
  369. def updateHashtagsIndex(baseDir: str,tag: {},newPostId: str) -> None:
  370. """Writes the post url for hashtags to a file
  371. This allows posts for a hashtag to be quickly looked up
  372. """
  373. if tag['type']!='Hashtag':
  374. return
  375. # create hashtags directory
  376. tagsDir=baseDir+'/tags'
  377. if not os.path.isdir(tagsDir):
  378. os.mkdir(tagsDir)
  379. tagName=tag['name']
  380. tagsFilename=tagsDir+'/'+tagName[1:]+'.txt'
  381. tagFile=open(tagsFilename, "a+")
  382. if not tagFile:
  383. return
  384. tagFile.write(newPostId+'\n')
  385. tagFile.close()
  386. def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \
  387. toUrl: str, ccUrl: str, httpPrefix: str, content: str, \
  388. followersOnly: bool, saveToFile: bool, clientToServer: bool, \
  389. attachImageFilename: str,imageDescription: str, \
  390. useBlurhash: bool,isModerationReport: bool,inReplyTo=None, \
  391. inReplyToAtomUri=None, subject=None) -> {}:
  392. """Creates a message
  393. """
  394. mentionedRecipients=[]
  395. tags=[]
  396. hashtagsDict={}
  397. if port:
  398. if port!=80 and port!=443:
  399. if ':' not in domain:
  400. domain=domain+':'+str(port)
  401. # convert content to html
  402. content= \
  403. addHtmlTags(baseDir,httpPrefix, \
  404. nickname,domain,content, \
  405. mentionedRecipients, \
  406. hashtagsDict)
  407. statusNumber,published = getStatusNumber()
  408. conversationDate=published.split('T')[0]
  409. conversationId=statusNumber
  410. postTo='https://www.w3.org/ns/activitystreams#Public'
  411. postCC=httpPrefix+'://'+domain+'/users/'+nickname+'/followers'
  412. if followersOnly:
  413. postTo=postCC
  414. postCC=''
  415. newPostId=httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
  416. sensitive=False
  417. summary=None
  418. if subject:
  419. summary=subject
  420. sensitive=True
  421. if toUrl:
  422. if not isinstance(toUrl, str):
  423. print('ERROR: toUrl is not a string')
  424. return None
  425. # who to send to
  426. toRecipients=[toUrl] + mentionedRecipients
  427. # create a list of hashtags
  428. if hashtagsDict:
  429. isPublic=False
  430. for recipient in toRecipients:
  431. if recipient.endswith('#Public'):
  432. isPublic=True
  433. break
  434. for tagName,tag in hashtagsDict.items():
  435. tags.append(tag)
  436. if isPublic:
  437. updateHashtagsIndex(baseDir,tag,newPostId)
  438. if not clientToServer:
  439. actorUrl=httpPrefix+'://'+domain+'/users/'+nickname
  440. # if capabilities have been granted for this actor
  441. # then get the corresponding id
  442. capabilityId=None
  443. capabilityIdList=[]
  444. ocapFilename=getOcapFilename(baseDir,nickname,domain,toUrl,'granted')
  445. if ocapFilename:
  446. if os.path.isfile(ocapFilename):
  447. with open(ocapFilename, 'r') as fp:
  448. oc=commentjson.load(fp)
  449. if oc.get('id'):
  450. capabilityIdList=[oc['id']]
  451. newPost = {
  452. "@context": "https://www.w3.org/ns/activitystreams",
  453. 'id': newPostId+'/activity',
  454. 'capability': capabilityIdList,
  455. 'type': 'Create',
  456. 'actor': actorUrl,
  457. 'published': published,
  458. 'to': [toUrl],
  459. 'cc': [],
  460. 'object': {
  461. 'id': newPostId,
  462. 'type': 'Note',
  463. 'summary': summary,
  464. 'inReplyTo': inReplyTo,
  465. 'published': published,
  466. 'url': httpPrefix+'://'+domain+'/@'+nickname+'/'+statusNumber,
  467. 'attributedTo': httpPrefix+'://'+domain+'/users/'+nickname,
  468. 'to': toRecipients,
  469. 'cc': [],
  470. 'sensitive': sensitive,
  471. 'atomUri': newPostId,
  472. 'inReplyToAtomUri': inReplyToAtomUri,
  473. 'conversation': 'tag:'+domain+','+conversationDate+':objectId='+conversationId+':objectType=Conversation',
  474. 'content': content,
  475. 'contentMap': {
  476. 'en': content
  477. },
  478. 'attachment': [],
  479. 'tag': tags,
  480. 'replies': {
  481. 'id': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  482. 'type': 'Collection',
  483. 'first': {
  484. 'type': 'CollectionPage',
  485. 'partOf': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  486. 'items': []
  487. }
  488. }
  489. }
  490. }
  491. if attachImageFilename:
  492. newPost['object']= \
  493. attachImage(baseDir,httpPrefix,domain,port, \
  494. newPost['object'],attachImageFilename, \
  495. imageDescription,useBlurhash)
  496. else:
  497. newPost = {
  498. "@context": "https://www.w3.org/ns/activitystreams",
  499. 'id': newPostId,
  500. 'type': 'Note',
  501. 'summary': summary,
  502. 'inReplyTo': inReplyTo,
  503. 'published': published,
  504. 'url': httpPrefix+'://'+domain+'/@'+nickname+'/'+statusNumber,
  505. 'attributedTo': httpPrefix+'://'+domain+'/users/'+nickname,
  506. 'to': toRecipients,
  507. 'cc': [],
  508. 'sensitive': sensitive,
  509. 'atomUri': newPostId,
  510. 'inReplyToAtomUri': inReplyToAtomUri,
  511. 'conversation': 'tag:'+domain+','+conversationDate+':objectId='+conversationId+':objectType=Conversation',
  512. 'content': content,
  513. 'contentMap': {
  514. 'en': content
  515. },
  516. 'attachment': [],
  517. 'tag': tags,
  518. 'replies': {
  519. 'id': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  520. 'type': 'Collection',
  521. 'first': {
  522. 'type': 'CollectionPage',
  523. 'partOf': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  524. 'items': []
  525. }
  526. }
  527. }
  528. if attachImageFilename:
  529. newPost= \
  530. attachImage(baseDir,httpPrefix,domain,port, \
  531. newPost,attachImageFilename, \
  532. imageDescription,useBlurhash)
  533. if ccUrl:
  534. if len(ccUrl)>0:
  535. newPost['cc']=[ccUrl]
  536. if newPost.get('object'):
  537. newPost['object']['cc']=[ccUrl]
  538. # if this is a moderation report then add a status
  539. if isModerationReport:
  540. # add status
  541. if newPost.get('object'):
  542. newPost['object']['moderationStatus']='pending'
  543. else:
  544. newPost['moderationStatus']='pending'
  545. # save to index file
  546. moderationIndexFile=baseDir+'/accounts/moderation.txt'
  547. modFile=open(moderationIndexFile, "a+")
  548. if modFile:
  549. modFile.write(newPostId+'\n')
  550. modFile.close()
  551. if saveToFile:
  552. savePostToBox(baseDir,httpPrefix,newPostId, \
  553. nickname,domain,newPost,'outbox')
  554. return newPost
  555. def outboxMessageCreateWrap(httpPrefix: str, \
  556. nickname: str,domain: str,port: int, \
  557. messageJson: {}) -> {}:
  558. """Wraps a received message in a Create
  559. https://www.w3.org/TR/activitypub/#object-without-create
  560. """
  561. if port:
  562. if port!=80 and port!=443:
  563. if ':' not in domain:
  564. domain=domain+':'+str(port)
  565. statusNumber,published = getStatusNumber()
  566. if messageJson.get('published'):
  567. published = messageJson['published']
  568. newPostId=httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
  569. cc=[]
  570. if messageJson.get('cc'):
  571. cc=messageJson['cc']
  572. # TODO
  573. capabilityUrl=[]
  574. newPost = {
  575. "@context": "https://www.w3.org/ns/activitystreams",
  576. 'id': newPostId+'/activity',
  577. 'capability': capabilityUrl,
  578. 'type': 'Create',
  579. 'actor': httpPrefix+'://'+domain+'/users/'+nickname,
  580. 'published': published,
  581. 'to': messageJson['to'],
  582. 'cc': cc,
  583. 'object': messageJson
  584. }
  585. newPost['object']['id']=newPost['id']
  586. newPost['object']['url']= \
  587. httpPrefix+'://'+domain+'/@'+nickname+'/'+statusNumber
  588. newPost['object']['atomUri']= \
  589. httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
  590. return newPost
  591. def postIsAddressedToFollowers(baseDir: str,
  592. nickname: str, domain: str, port: int,httpPrefix: str,
  593. postJsonObject: {}) -> bool:
  594. """Returns true if the given post is addressed to followers of the nickname
  595. """
  596. if port:
  597. if port!=80 and port!=443:
  598. if ':' not in domain:
  599. domain=domain+':'+str(port)
  600. if not postJsonObject.get('object'):
  601. return False
  602. toList=[]
  603. ccList=[]
  604. if isinstance(postJsonObject['object'], dict):
  605. if not postJsonObject['object'].get('to'):
  606. return False
  607. toList=postJsonObject['object']['to']
  608. if postJsonObject['object'].get('cc'):
  609. ccList=postJsonObject['object']['cc']
  610. else:
  611. if not postJsonObject.get('to'):
  612. return False
  613. toList=postJsonObject['to']
  614. if postJsonObject.get('cc'):
  615. ccList=postJsonObject['cc']
  616. followersUrl=httpPrefix+'://'+domain+'/users/'+nickname+'/followers'
  617. # does the followers url exist in 'to' or 'cc' lists?
  618. addressedToFollowers=False
  619. if followersUrl in toList:
  620. addressedToFollowers=True
  621. if not addressedToFollowers:
  622. if followersUrl in ccList:
  623. addressedToFollowers=True
  624. return addressedToFollowers
  625. def postIsAddressedToPublic(baseDir: str,postJsonObject: {}) -> bool:
  626. """Returns true if the given post is addressed to public
  627. """
  628. if not postJsonObject.get('object'):
  629. return False
  630. if not postJsonObject['object'].get('to'):
  631. return False
  632. publicUrl='https://www.w3.org/ns/activitystreams#Public'
  633. # does the public url exist in 'to' or 'cc' lists?
  634. addressedToPublic=False
  635. if publicUrl in postJsonObject['object']['to']:
  636. addressedToPublic=True
  637. if not addressedToPublic:
  638. if not postJsonObject['object'].get('cc'):
  639. return False
  640. if publicUrl in postJsonObject['object']['cc']:
  641. addressedToPublic=True
  642. return addressedToPublic
  643. def createPublicPost(baseDir: str,
  644. nickname: str, domain: str, port: int,httpPrefix: str, \
  645. content: str, followersOnly: bool, saveToFile: bool,
  646. clientToServer: bool,\
  647. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  648. inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
  649. """Public post
  650. """
  651. domainFull=domain
  652. if port:
  653. if port!=80 and port!=443:
  654. if ':' not in domain:
  655. domainFull=domain+':'+str(port)
  656. return createPostBase(baseDir,nickname, domain, port, \
  657. 'https://www.w3.org/ns/activitystreams#Public', \
  658. httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
  659. httpPrefix, content, followersOnly, saveToFile, \
  660. clientToServer, \
  661. attachImageFilename,imageDescription,useBlurhash, \
  662. False,inReplyTo,inReplyToAtomUri,subject)
  663. def createUnlistedPost(baseDir: str,
  664. nickname: str, domain: str, port: int,httpPrefix: str, \
  665. content: str, followersOnly: bool, saveToFile: bool,
  666. clientToServer: bool,\
  667. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  668. inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
  669. """Unlisted post. This has the #Public and followers links inverted.
  670. """
  671. domainFull=domain
  672. if port:
  673. if port!=80 and port!=443:
  674. if ':' not in domain:
  675. domainFull=domain+':'+str(port)
  676. return createPostBase(baseDir,nickname, domain, port, \
  677. httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
  678. 'https://www.w3.org/ns/activitystreams#Public', \
  679. httpPrefix, content, followersOnly, saveToFile, \
  680. clientToServer, \
  681. attachImageFilename,imageDescription,useBlurhash, \
  682. False,inReplyTo, inReplyToAtomUri, subject)
  683. def createFollowersOnlyPost(baseDir: str,
  684. nickname: str, domain: str, port: int,httpPrefix: str, \
  685. content: str, followersOnly: bool, saveToFile: bool,
  686. clientToServer: bool,\
  687. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  688. inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
  689. """Followers only post
  690. """
  691. domainFull=domain
  692. if port:
  693. if port!=80 and port!=443:
  694. if ':' not in domain:
  695. domainFull=domain+':'+str(port)
  696. return createPostBase(baseDir,nickname, domain, port, \
  697. httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
  698. None,
  699. httpPrefix, content, followersOnly, saveToFile, \
  700. clientToServer, \
  701. attachImageFilename,imageDescription,useBlurhash, \
  702. False,inReplyTo, inReplyToAtomUri, subject)
  703. def getMentionedPeople(baseDir: str,httpPrefix: str,content: str,domain: str) -> []:
  704. """Extracts a list of mentioned actors from the given message content
  705. """
  706. if '@' not in content:
  707. return None
  708. mentions=[]
  709. words=content.split(' ')
  710. for wrd in words:
  711. if wrd.startswith('@'):
  712. handle=wrd[1:]
  713. if '@' not in handle:
  714. handle=handle+'@'+domain
  715. if not os.path.isdir(baseDir+'/accounts/'+handle):
  716. continue
  717. else:
  718. externalDomain=handle.split('@')[1]
  719. if not ('.' in externalDomain or externalDomain=='localhost'):
  720. continue
  721. mentionedNickname=handle.split('@')[0]
  722. if not validNickname(mentionedNickname):
  723. continue
  724. actor=httpPrefix+'://'+handle.split('@')[1]+'/users/'+mentionedNickname
  725. mentions.append(actor)
  726. return actor
  727. def createDirectMessagePost(baseDir: str,
  728. nickname: str, domain: str, port: int,httpPrefix: str, \
  729. content: str, followersOnly: bool, saveToFile: bool,
  730. clientToServer: bool,\
  731. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  732. inReplyTo=None, inReplyToAtomUri=None, subject=None) -> {}:
  733. """Direct Message post
  734. """
  735. mentionedPeople=getMentionedPeople(baseDir,httpPrefix,content,domain)
  736. postTo=None
  737. postCc=None
  738. if not mentionedPeople:
  739. return None
  740. if len(mentionedPeople)==1:
  741. postTo=mentionedPeople[0]
  742. if len(mentionedPeople)>1:
  743. postCc=mentionedPeople[1]
  744. return createPostBase(baseDir,nickname, domain, port, \
  745. postTo,postCc, \
  746. httpPrefix, content, followersOnly, saveToFile, \
  747. clientToServer, \
  748. attachImageFilename,imageDescription,useBlurhash, \
  749. False,inReplyTo, inReplyToAtomUri, subject)
  750. def createReportPost(baseDir: str,
  751. nickname: str, domain: str, port: int,httpPrefix: str, \
  752. content: str, followersOnly: bool, saveToFile: bool,
  753. clientToServer: bool,\
  754. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  755. debug: bool,subject=None) -> {}:
  756. """Send a report to moderators
  757. """
  758. domainFull=domain
  759. if port:
  760. if port!=80 and port!=443:
  761. if ':' not in domain:
  762. domainFull=domain+':'+str(port)
  763. # add a title to distinguish moderation reports from other posts
  764. reportTitle='Moderation Report'
  765. if not subject:
  766. subject=reportTitle
  767. else:
  768. if not subject.startswith(reportTitle):
  769. subject=reportTitle+': '+subject
  770. # create the list of moderators from the moderators file
  771. moderatorsList=[]
  772. moderatorsFile=baseDir+'/accounts/moderators.txt'
  773. if os.path.isfile(moderatorsFile):
  774. with open (moderatorsFile, "r") as fileHandler:
  775. for line in fileHandler:
  776. line=line.strip('\n')
  777. if line.startswith('#'):
  778. continue
  779. if line.startswith('/users/'):
  780. line=line.replace('users','')
  781. if line.startswith('@'):
  782. line=line[1:]
  783. if '@' in line:
  784. moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line.split('@')[0]
  785. if moderatorActor not in moderatorList:
  786. moderatorsList.append(moderatorActor)
  787. continue
  788. if line.startswith('http') or line.startswith('dat'):
  789. # must be a local address - no remote moderators
  790. if '://'+domainFull+'/' in line:
  791. if line not in moderatorsList:
  792. moderatorsList.append(line)
  793. else:
  794. if '/' not in line:
  795. moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line
  796. if moderatorActor not in moderatorsList:
  797. moderatorsList.append(moderatorActor)
  798. if len(moderatorsList)==0:
  799. # if there are no moderators then the admin becomes the moderator
  800. adminNickname=getConfigParam(baseDir,'admin')
  801. if adminNickname:
  802. moderatorsList.append(httpPrefix+'://'+domainFull+'/users/'+adminNickname)
  803. if not moderatorsList:
  804. return None
  805. if debug:
  806. print('DEBUG: Sending report to moderators')
  807. print(str(moderatorsList))
  808. postTo=moderatorsList
  809. postCc=None
  810. postJsonObject=None
  811. for toUrl in postTo:
  812. postJsonObject= \
  813. createPostBase(baseDir,nickname, domain, port, \
  814. toUrl,postCc, \
  815. httpPrefix, content, followersOnly, saveToFile, \
  816. clientToServer, \
  817. attachImageFilename,imageDescription,useBlurhash, \
  818. True,None, None, subject)
  819. return postJsonObject
  820. def threadSendPost(session,postJsonStr: str,federationList: [],\
  821. inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [],
  822. debug :bool) -> None:
  823. """Sends a post with exponential backoff
  824. """
  825. tries=0
  826. backoffTime=60
  827. for attempt in range(20):
  828. postResult = \
  829. postJsonString(session,postJsonStr,federationList, \
  830. inboxUrl,signatureHeaderJson, \
  831. "inbox:write",debug)
  832. if postResult:
  833. if debug:
  834. print('DEBUG: json post to '+inboxUrl+' succeeded')
  835. #if postJsonObject.get('published'):
  836. # postLog.append(postJsonObject['published']+' '+postResult+'\n')
  837. # keep the length of the log finite
  838. # Don't accumulate massive files on systems with limited resources
  839. while len(postLog)>64:
  840. postlog.pop(0)
  841. # save the log file
  842. filename=baseDir+'/post.log'
  843. with open(filename, "w") as logFile:
  844. for line in postLog:
  845. print(line, file=logFile)
  846. # our work here is done
  847. break
  848. if debug:
  849. print(postJsonStr)
  850. print('DEBUG: json post to '+inboxUrl+' failed. Waiting for '+ \
  851. str(backoffTime)+' seconds.')
  852. time.sleep(backoffTime)
  853. backoffTime *= 2
  854. def sendPost(projectVersion: str, \
  855. session,baseDir: str,nickname: str, domain: str, port: int, \
  856. toNickname: str, toDomain: str, toPort: int, cc: str, \
  857. httpPrefix: str, content: str, followersOnly: bool, \
  858. saveToFile: bool, clientToServer: bool, \
  859. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  860. federationList: [],\
  861. sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}, \
  862. debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int:
  863. """Post to another inbox
  864. """
  865. withDigest=True
  866. if toPort:
  867. if toPort!=80 and toPort!=443:
  868. if ':' not in toDomain:
  869. toDomain=toDomain+':'+str(toPort)
  870. handle=httpPrefix+'://'+toDomain+'/@'+toNickname
  871. # lookup the inbox for the To handle
  872. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  873. domain,projectVersion)
  874. if not wfRequest:
  875. return 1
  876. if not clientToServer:
  877. postToBox='inbox'
  878. else:
  879. postToBox='outbox'
  880. # get the actor inbox for the To handle
  881. inboxUrl,pubKeyId,pubKey,toPersonId,sharedInbox,capabilityAcquisition,avatarUrl,preferredName = \
  882. getPersonBox(session,wfRequest,personCache, \
  883. projectVersion,httpPrefix,domain,postToBox)
  884. # If there are more than one followers on the target domain
  885. # then send to the shared inbox indead of the individual inbox
  886. if nickname=='capabilities':
  887. inboxUrl=capabilityAcquisition
  888. if not capabilityAcquisition:
  889. return 2
  890. else:
  891. if noOfFollowersOnDomain(baseDir,handle,toDomain)>1 and sharedInbox:
  892. inboxUrl=sharedInbox
  893. if not inboxUrl:
  894. return 3
  895. if not pubKey:
  896. return 4
  897. if not toPersonId:
  898. return 5
  899. # sharedInbox and capabilities are optional
  900. postJsonObject = \
  901. createPostBase(baseDir,nickname,domain,port, \
  902. toPersonId,cc,httpPrefix,content, \
  903. followersOnly,saveToFile,clientToServer, \
  904. attachImageFilename,imageDescription,useBlurhash, \
  905. False,inReplyTo,inReplyToAtomUri,subject)
  906. # get the senders private key
  907. privateKeyPem=getPersonKey(nickname,domain,baseDir,'private')
  908. if len(privateKeyPem)==0:
  909. return 6
  910. if toDomain not in inboxUrl:
  911. return 7
  912. postPath=inboxUrl.split(toDomain)[1]
  913. # convert json to string so that there are no
  914. # subsequent conversions after creating message body digest
  915. postJsonStr=json.dumps(postJsonObject)
  916. # construct the http header, including the message body digest
  917. signatureHeaderJson = \
  918. createSignedHeader(privateKeyPem,nickname,domain,port, \
  919. toDomain,toPort, \
  920. postPath,httpPrefix,withDigest,postJsonStr)
  921. # Keep the number of threads being used small
  922. while len(sendThreads)>10:
  923. sendThreads[0].kill()
  924. sendThreads.pop(0)
  925. thr = threadWithTrace(target=threadSendPost,args=(session, \
  926. postJsonStr, \
  927. federationList, \
  928. inboxUrl,baseDir, \
  929. signatureHeaderJson.copy(), \
  930. postLog,
  931. debug),daemon=True)
  932. sendThreads.append(thr)
  933. thr.start()
  934. return 0
  935. def sendPostViaServer(projectVersion: str, \
  936. baseDir,session,fromNickname: str,password: str, \
  937. fromDomain: str, fromPort: int, \
  938. toNickname: str, toDomain: str, toPort: int, cc: str, \
  939. httpPrefix: str, content: str, followersOnly: bool, \
  940. attachImageFilename: str,imageDescription: str,useBlurhash: bool, \
  941. cachedWebfingers: {},personCache: {}, \
  942. debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int:
  943. """Send a post via a proxy (c2s)
  944. """
  945. if not session:
  946. print('WARN: No session for sendPostViaServer')
  947. return 6
  948. withDigest=True
  949. if toPort:
  950. if toPort!=80 and toPort!=443:
  951. if ':' not in fromDomain:
  952. fromDomain=fromDomain+':'+str(fromPort)
  953. handle=httpPrefix+'://'+fromDomain+'/@'+fromNickname
  954. # lookup the inbox for the To handle
  955. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  956. fromDomain,projectVersion)
  957. if not wfRequest:
  958. if debug:
  959. print('DEBUG: webfinger failed for '+handle)
  960. return 1
  961. postToBox='outbox'
  962. # get the actor inbox for the To handle
  963. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,preferredName = \
  964. getPersonBox(session,wfRequest,personCache, \
  965. projectVersion,httpPrefix,fromDomain,postToBox)
  966. if not inboxUrl:
  967. if debug:
  968. print('DEBUG: No '+postToBox+' was found for '+handle)
  969. return 3
  970. if not fromPersonId:
  971. if debug:
  972. print('DEBUG: No actor was found for '+handle)
  973. return 4
  974. # Get the json for the c2s post, not saving anything to file
  975. # Note that baseDir is set to None
  976. saveToFile=False
  977. clientToServer=True
  978. if toDomain.lower().endswith('public'):
  979. toPersonId='https://www.w3.org/ns/activitystreams#Public'
  980. fromDomainFull=fromDomain
  981. if fromPort:
  982. if fromPort!=80 and fromPort!=443:
  983. if ':' not in fromDomain:
  984. fromDomainFull=fromDomain+':'+str(fromPort)
  985. cc=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
  986. else:
  987. if toDomain.lower().endswith('followers') or \
  988. toDomain.lower().endswith('followersonly'):
  989. toPersonId=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
  990. else:
  991. toDomainFull=toDomain
  992. if toPort:
  993. if toPort!=80 and toPort!=443:
  994. if ':' not in toDomain:
  995. toDomainFull=toDomain+':'+str(toPort)
  996. toPersonId=httpPrefix+'://'+toDomainFull+'/users/'+toNickname
  997. postJsonObject = \
  998. createPostBase(baseDir, \
  999. fromNickname,fromDomain,fromPort, \
  1000. toPersonId,cc,httpPrefix,content, \
  1001. followersOnly,saveToFile,clientToServer, \
  1002. attachImageFilename,imageDescription,useBlurhash, \
  1003. False,inReplyTo,inReplyToAtomUri,subject)
  1004. authHeader=createBasicAuthHeader(fromNickname,password)
  1005. if attachImageFilename:
  1006. headers = {'host': fromDomain, \
  1007. 'Authorization': authHeader}
  1008. postResult = \
  1009. postImage(session,attachImageFilename,[],inboxUrl,headers,"inbox:write")
  1010. #if not postResult:
  1011. # if debug:
  1012. # print('DEBUG: Failed to upload image')
  1013. # return 9
  1014. headers = {'host': fromDomain, \
  1015. 'Content-type': 'application/json', \
  1016. 'Authorization': authHeader}
  1017. postResult = \
  1018. postJsonString(session,json.dumps(postJsonObject),[],inboxUrl,headers,"inbox:write",debug)
  1019. #if not postResult:
  1020. # if debug:
  1021. # print('DEBUG: POST failed for c2s to '+inboxUrl)
  1022. # return 5
  1023. if debug:
  1024. print('DEBUG: c2s POST success')
  1025. return 0
  1026. def groupFollowersByDomain(baseDir :str,nickname :str,domain :str) -> {}:
  1027. """Returns a dictionary with followers grouped by domain
  1028. """
  1029. handle=nickname+'@'+domain
  1030. followersFilename=baseDir+'/accounts/'+handle+'/followers.txt'
  1031. if not os.path.isfile(followersFilename):
  1032. return None
  1033. grouped={}
  1034. with open(followersFilename, "r") as f:
  1035. for followerHandle in f:
  1036. if '@' in followerHandle:
  1037. fHandle=followerHandle.strip().replace('\n','')
  1038. followerDomain=fHandle.split('@')[1]
  1039. if not grouped.get(followerDomain):
  1040. grouped[followerDomain]=[fHandle]
  1041. else:
  1042. grouped[followerDomain].append(fHandle)
  1043. return grouped
  1044. def sendSignedJson(postJsonObject: {},session,baseDir: str, \
  1045. nickname: str, domain: str, port: int, \
  1046. toNickname: str, toDomain: str, toPort: int, cc: str, \
  1047. httpPrefix: str, saveToFile: bool, clientToServer: bool, \
  1048. federationList: [], \
  1049. sendThreads: [], postLog: [], cachedWebfingers: {}, \
  1050. personCache: {}, debug: bool,projectVersion: str) -> int:
  1051. """Sends a signed json object to an inbox/outbox
  1052. """
  1053. if debug:
  1054. print('DEBUG: sendSignedJson start')
  1055. if not session:
  1056. print('WARN: No session specified for sendSignedJson')
  1057. return 8
  1058. withDigest=True
  1059. sharedInbox=False
  1060. if toNickname=='inbox':
  1061. sharedInbox=True
  1062. if toPort:
  1063. if toPort!=80 and toPort!=443:
  1064. if ':' not in toDomain:
  1065. toDomain=toDomain+':'+str(toPort)
  1066. handle=httpPrefix+'://'+toDomain+'/@'+toNickname
  1067. if debug:
  1068. print('DEBUG: handle - '+handle+' toPort '+str(toPort))
  1069. # lookup the inbox for the To handle
  1070. wfRequest=webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  1071. domain,projectVersion)
  1072. if not wfRequest:
  1073. if debug:
  1074. print('DEBUG: webfinger for '+handle+' failed')
  1075. return 1
  1076. if not clientToServer:
  1077. postToBox='inbox'
  1078. else:
  1079. postToBox='outbox'
  1080. # get the actor inbox/outbox/capabilities for the To handle
  1081. inboxUrl,pubKeyId,pubKey,toPersonId,sharedInboxUrl,capabilityAcquisition,avatarUrl,preferredName = \
  1082. getPersonBox(session,wfRequest,personCache, \
  1083. projectVersion,httpPrefix,domain,postToBox)
  1084. if nickname=='capabilities':
  1085. inboxUrl=capabilityAcquisition
  1086. if not capabilityAcquisition:
  1087. return 2
  1088. else:
  1089. if sharedInbox and sharedInboxUrl:
  1090. inboxUrl=sharedInboxUrl
  1091. if not inboxUrl:
  1092. if debug:
  1093. print('DEBUG: missing inboxUrl')
  1094. return 3
  1095. if debug:
  1096. print('DEBUG: Sending to endpoint '+inboxUrl)
  1097. if not pubKey:
  1098. if debug:
  1099. print('DEBUG: missing pubkey')
  1100. return 4
  1101. if not toPersonId:
  1102. if debug:
  1103. print('DEBUG: missing personId')
  1104. return 5
  1105. # sharedInbox and capabilities are optional
  1106. # get the senders private key
  1107. privateKeyPem=getPersonKey(nickname,domain,baseDir,'private',debug)
  1108. if len(privateKeyPem)==0:
  1109. if debug:
  1110. print('DEBUG: Private key not found for '+nickname+'@'+domain+' in '+baseDir+'/keys/private')
  1111. return 6
  1112. if toDomain not in inboxUrl:
  1113. if debug:
  1114. print('DEBUG: '+toDomain+' is not in '+inboxUrl)
  1115. return 7
  1116. postPath=inboxUrl.split(toDomain)[1]
  1117. # convert json to string so that there are no
  1118. # subsequent conversions after creating message body digest
  1119. postJsonStr=json.dumps(postJsonObject)
  1120. # construct the http header, including the message body digest
  1121. signatureHeaderJson = \
  1122. createSignedHeader(privateKeyPem,nickname,domain,port, \
  1123. toDomain,toPort, \
  1124. postPath,httpPrefix,withDigest,postJsonStr)
  1125. # Keep the number of threads being used small
  1126. while len(sendThreads)>10:
  1127. sendThreads[0].kill()
  1128. sendThreads.pop(0)
  1129. if debug:
  1130. print('DEBUG: starting thread to send post')
  1131. pprint(postJsonObject)
  1132. thr = threadWithTrace(target=threadSendPost, \
  1133. args=(session, \
  1134. postJsonStr, \
  1135. federationList, \
  1136. inboxUrl,baseDir, \
  1137. signatureHeaderJson.copy(), \
  1138. postLog,
  1139. debug),daemon=True)
  1140. sendThreads.append(thr)
  1141. thr.start()
  1142. return 0
  1143. def addToField(activityType: str,postJsonObject: {},debug: bool) -> ({},bool):
  1144. """The Follow activity doesn't have a 'to' field and so one
  1145. needs to be added so that activity distribution happens in a consistent way
  1146. Returns true if a 'to' field exists or was added
  1147. """
  1148. if postJsonObject.get('to'):
  1149. return postJsonObject,True
  1150. if debug:
  1151. pprint(postJsonObject)
  1152. print('DEBUG: no "to" field when sending to named addresses 2')
  1153. isSameType=False
  1154. toFieldAdded=False
  1155. if postJsonObject.get('object'):
  1156. if isinstance(postJsonObject['object'], str):
  1157. if postJsonObject.get('type'):
  1158. if postJsonObject['type']==activityType:
  1159. isSameType=True
  1160. if debug:
  1161. print('DEBUG: "to" field assigned to Follow')
  1162. toAddress=postJsonObject['object']
  1163. if '/statuses/' in toAddress:
  1164. toAddress=toAddress.split('/statuses/')[0]
  1165. postJsonObject['to']=[toAddress]
  1166. toFieldAdded=True
  1167. elif isinstance(postJsonObject['object'], dict):
  1168. if postJsonObject['object'].get('type'):
  1169. if postJsonObject['object']['type']==activityType:
  1170. isSameType=True
  1171. if isinstance(postJsonObject['object']['object'], str):
  1172. if debug:
  1173. print('DEBUG: "to" field assigned to Follow')
  1174. toAddress=postJsonObject['object']['object']
  1175. if '/statuses/' in toAddress:
  1176. toAddress=toAddress.split('/statuses/')[0]
  1177. postJsonObject['object']['to']=[toAddress]
  1178. postJsonObject['to']=[postJsonObject['object']['object']]
  1179. toFieldAdded=True
  1180. if not isSameType:
  1181. return postJsonObject,True
  1182. if toFieldAdded:
  1183. return postJsonObject,True
  1184. return postJsonObject,False
  1185. def sendToNamedAddresses(session,baseDir: str, \
  1186. nickname: str, domain: str, port: int, \
  1187. httpPrefix: str,federationList: [], \
  1188. sendThreads: [],postLog: [], \
  1189. cachedWebfingers: {},personCache: {}, \
  1190. postJsonObject: {},debug: bool, \
  1191. projectVersion: str) -> None:
  1192. """sends a post to the specific named addresses in to/cc
  1193. """
  1194. if not session:
  1195. print('WARN: No session for sendToNamedAddresses')
  1196. return
  1197. if not postJsonObject.get('object'):
  1198. return
  1199. if isinstance(postJsonObject['object'], dict):
  1200. if not postJsonObject['object'].get('to'):
  1201. if debug:
  1202. pprint(postJsonObject)
  1203. print('DEBUG: no "to" field when sending to named addresses')
  1204. if postJsonObject['object'].get('type'):
  1205. if postJsonObject['object']['type']=='Follow':
  1206. if isinstance(postJsonObject['object']['object'], str):
  1207. if debug:
  1208. print('DEBUG: "to" field assigned to Follow')
  1209. postJsonObject['object']['to']=[postJsonObject['object']['object']]
  1210. if not postJsonObject['object'].get('to'):
  1211. return
  1212. recipientsObject=postJsonObject['object']
  1213. else:
  1214. postJsonObject,fieldAdded=addToField('Follow',postJsonObject,debug)
  1215. if not fieldAdded:
  1216. return
  1217. postJsonObject,fieldAdded=addToField('Like',postJsonObject,debug)
  1218. if not fieldAdded:
  1219. return
  1220. recipientsObject=postJsonObject
  1221. recipients=[]
  1222. recipientType=['to','cc']
  1223. for rType in recipientType:
  1224. if not recipientsObject.get(rType):
  1225. continue
  1226. if isinstance(recipientsObject[rType], list):
  1227. if debug:
  1228. print('recipientsObject: '+str(recipientsObject[rType]))
  1229. for address in recipientsObject[rType]:
  1230. if not address:
  1231. continue
  1232. if '/' not in address:
  1233. continue
  1234. if address.endswith('#Public'):
  1235. continue
  1236. if address.endswith('/followers'):
  1237. continue
  1238. recipients.append(address)
  1239. elif isinstance(recipientsObject[rType], str):
  1240. address=recipientsObject[rType]
  1241. if address:
  1242. if '/' in address:
  1243. if address.endswith('#Public'):
  1244. continue
  1245. if address.endswith('/followers'):
  1246. continue
  1247. recipients.append(address)
  1248. if not recipients:
  1249. if debug:
  1250. print('DEBUG: no individual recipients')
  1251. return
  1252. if debug:
  1253. print('DEBUG: Sending individually addressed posts: '+str(recipients))
  1254. # this is after the message has arrived at the server
  1255. clientToServer=False
  1256. for address in recipients:
  1257. toNickname=getNicknameFromActor(address)
  1258. if not toNickname:
  1259. continue
  1260. toDomain,toPort=getDomainFromActor(address)
  1261. if not toDomain:
  1262. continue
  1263. if debug:
  1264. domainFull=domain
  1265. if port:
  1266. if port!=80 and port!=443:
  1267. if ':' not in domain:
  1268. domainFull=domain+':'+str(port)
  1269. toDomainFull=toDomain
  1270. if toPort:
  1271. if toPort!=80 and toPort!=443:
  1272. if ':' not in toDomain:
  1273. toDomainFull=toDomain+':'+str(toPort)
  1274. print('DEBUG: Post sending s2s: '+nickname+'@'+domainFull+' to '+toNickname+'@'+toDomainFull)
  1275. cc=[]
  1276. sendSignedJson(postJsonObject,session,baseDir, \
  1277. nickname,domain,port, \
  1278. toNickname,toDomain,toPort, \
  1279. cc,httpPrefix,True,clientToServer, \
  1280. federationList, \
  1281. sendThreads,postLog,cachedWebfingers, \
  1282. personCache,debug,projectVersion)
  1283. def sendToFollowers(session,baseDir: str, \
  1284. nickname: str, domain: str, port: int, \
  1285. httpPrefix: str,federationList: [], \
  1286. sendThreads: [],postLog: [], \
  1287. cachedWebfingers: {},personCache: {}, \
  1288. postJsonObject: {},debug: bool, \
  1289. projectVersion: str) -> None:
  1290. """sends a post to the followers of the given nickname
  1291. """
  1292. if not session:
  1293. print('WARN: No session for sendToFollowers')
  1294. return
  1295. if not postIsAddressedToFollowers(baseDir,nickname,domain, \
  1296. port,httpPrefix,postJsonObject):
  1297. if debug:
  1298. print('Post is not addressed to followers')
  1299. return
  1300. grouped=groupFollowersByDomain(baseDir,nickname,domain)
  1301. if not grouped:
  1302. if debug:
  1303. print('Post to followers did not resolve any domains')
  1304. return
  1305. # this is after the message has arrived at the server
  1306. clientToServer=False
  1307. # for each instance
  1308. for followerDomain,followerHandles in grouped.items():
  1309. if debug:
  1310. print('DEBUG: follower handles for '+followerDomain)
  1311. pprint(followerHandles)
  1312. toPort=port
  1313. index=0
  1314. toDomain=followerHandles[index].split('@')[1]
  1315. if ':' in toDomain:
  1316. toPort=toDomain.split(':')[1]
  1317. toDomain=toDomain.split(':')[0]
  1318. toNickname=followerHandles[index].split('@')[0]
  1319. cc=''
  1320. if len(followerHandles)>1:
  1321. nickname='inbox'
  1322. toNickname='inbox'
  1323. if debug:
  1324. print('DEBUG: Sending from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain)
  1325. sendSignedJson(postJsonObject,session,baseDir, \
  1326. nickname,domain,port, \
  1327. toNickname,toDomain,toPort, \
  1328. cc,httpPrefix,True,clientToServer, \
  1329. federationList, \
  1330. sendThreads,postLog,cachedWebfingers, \
  1331. personCache,debug,projectVersion)
  1332. if debug:
  1333. print('DEBUG: End of sendToFollowers')
  1334. def createInbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
  1335. itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
  1336. return createBoxBase(baseDir,'inbox',nickname,domain,port,httpPrefix, \
  1337. itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
  1338. def createOutbox(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
  1339. itemsPerPage: int,headerOnly: bool,authorized: bool,pageNumber=None) -> {}:
  1340. return createBoxBase(baseDir,'outbox',nickname,domain,port,httpPrefix, \
  1341. itemsPerPage,headerOnly,authorized,False,pageNumber)
  1342. def createModeration(baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
  1343. itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
  1344. boxDir = createPersonDir(nickname,domain,baseDir,'inbox')
  1345. boxname='moderation'
  1346. if port:
  1347. if port!=80 and port!=443:
  1348. if ':' not in domain:
  1349. domain=domain+':'+str(port)
  1350. if not pageNumber:
  1351. pageNumber=1
  1352. pageStr='?page='+str(pageNumber)
  1353. boxHeader = {'@context': 'https://www.w3.org/ns/activitystreams',
  1354. 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
  1355. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
  1356. 'last': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
  1357. 'totalItems': 0,
  1358. 'type': 'OrderedCollection'}
  1359. boxItems = {'@context': 'https://www.w3.org/ns/activitystreams',
  1360. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+pageStr,
  1361. 'orderedItems': [
  1362. ],
  1363. 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
  1364. 'type': 'OrderedCollectionPage'}
  1365. if isModerator(baseDir,nickname):
  1366. moderationIndexFile=baseDir+'/accounts/moderation.txt'
  1367. if os.path.isfile(moderationIndexFile):
  1368. with open(moderationIndexFile, "r") as f:
  1369. lines = f.readlines()
  1370. boxHeader['totalItems']=len(lines)
  1371. if headerOnly:
  1372. return boxHeader
  1373. pageLines=[]
  1374. if len(lines)>0:
  1375. endLineNumber=len(lines)-1-int(itemsPerPage*pageNumber)
  1376. if endLineNumber<0:
  1377. endLineNumber=0
  1378. startLineNumber=len(lines)-1-int(itemsPerPage*(pageNumber-1))
  1379. if startLineNumber<0:
  1380. startLineNumber=0
  1381. lineNumber=startLineNumber
  1382. while lineNumber>=endLineNumber:
  1383. pageLines.append(lines[lineNumber].strip('\n'))
  1384. lineNumber-=1
  1385. for postUrl in pageLines:
  1386. postFilename=boxDir+'/'+postUrl.replace('/','#')+'.json'
  1387. if os.path.isfile(postFilename):
  1388. with open(postFilename, 'r') as fp:
  1389. postJsonObject=commentjson.load(fp)
  1390. boxItems['orderedItems'].append(postJsonObject)
  1391. if headerOnly:
  1392. return boxHeader
  1393. return boxItems
  1394. def getStatusNumberFromPostFilename(filename) -> int:
  1395. """Gets the status number from a post filename
  1396. eg. https:##testdomain.com:8085#users#testuser567#statuses#1562958506952068.json
  1397. returns 156295850695206
  1398. """
  1399. if '#statuses#' not in filename:
  1400. return None
  1401. return int(filename.split('#')[-1].replace('.json',''))
  1402. def createBoxBase(baseDir: str,boxname: str, \
  1403. nickname: str,domain: str,port: int,httpPrefix: str, \
  1404. itemsPerPage: int,headerOnly: bool,authorized :bool, \
  1405. ocapAlways: bool,pageNumber=None) -> {}:
  1406. """Constructs the box feed for a person with the given nickname
  1407. """
  1408. if boxname!='inbox' and boxname!='outbox':
  1409. return None
  1410. boxDir = createPersonDir(nickname,domain,baseDir,boxname)
  1411. sharedBoxDir=None
  1412. if boxname=='inbox':
  1413. sharedBoxDir = createPersonDir('inbox',domain,baseDir,boxname)
  1414. if port:
  1415. if port!=80 and port!=443:
  1416. if ':' not in domain:
  1417. domain=domain+':'+str(port)
  1418. pageStr='?page=true'
  1419. if pageNumber:
  1420. try:
  1421. pageStr='?page='+str(pageNumber)
  1422. except:
  1423. pass
  1424. boxHeader = {'@context': 'https://www.w3.org/ns/activitystreams',
  1425. 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
  1426. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
  1427. 'last': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
  1428. 'totalItems': 0,
  1429. 'type': 'OrderedCollection'}
  1430. boxItems = {'@context': 'https://www.w3.org/ns/activitystreams',
  1431. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+pageStr,
  1432. 'orderedItems': [
  1433. ],
  1434. 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
  1435. 'type': 'OrderedCollectionPage'}
  1436. # counter for posts loop
  1437. postsOnPageCtr=0
  1438. # post filenames sorted in descending order
  1439. postsInBoxDict={}
  1440. postsCtr=0
  1441. postsInPersonInbox=os.listdir(boxDir)
  1442. for postFilename in postsInPersonInbox:
  1443. if not postFilename.endswith('.json'):
  1444. continue
  1445. # extract the status number
  1446. statusNumber=getStatusNumberFromPostFilename(postFilename)
  1447. if statusNumber:
  1448. postsInBoxDict[statusNumber]=os.path.join(boxDir, postFilename)
  1449. postsCtr+=1
  1450. # combine the inbox for the account with the shared inbox
  1451. if sharedBoxDir:
  1452. handle=nickname+'@'+domain
  1453. followingFilename=baseDir+'/accounts/'+handle+'/following.txt'
  1454. postsInSharedInbox=os.listdir(sharedBoxDir)
  1455. for postFilename in postsInSharedInbox:
  1456. statusNumber=getStatusNumberFromPostFilename(postFilename)
  1457. if statusNumber:
  1458. sharedInboxFilename=os.path.join(sharedBoxDir, postFilename)
  1459. # get the actor from the shared post
  1460. with open(sharedInboxFilename, 'r') as fp:
  1461. postJsonObject=commentjson.load(fp)
  1462. actorNickname=getNicknameFromActor(postJsonObject['actor'])
  1463. actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
  1464. if actorNickname and actorDomain:
  1465. # is the actor followed by this account?
  1466. if actorNickname+'@'+actorDomain in open(followingFilename).read():
  1467. if ocapAlways:
  1468. capsList=None
  1469. # Note: should this be in the Create or the object of a post?
  1470. if postJsonObject.get('capability'):
  1471. if isinstance(postJsonObject['capability'], list):
  1472. capsList=postJsonObject['capability']
  1473. # Have capabilities been granted for the sender?
  1474. ocapFilename=baseDir+'/accounts/'+handle+'/ocap/granted/'+postJsonObject['actor'].replace('/','#')+'.json'
  1475. if os.path.isfile(ocapFilename):
  1476. # read the capabilities id
  1477. with open(ocapFilename, 'r') as fp:
  1478. ocapJson=commentjson.load(fp)
  1479. if ocapJson.get('id'):
  1480. if ocapJson['id'] in capsList:
  1481. postsInBoxDict[statusNumber]=sharedInboxFilename
  1482. postsCtr+=1
  1483. else:
  1484. postsInBoxDict[statusNumber]=sharedInboxFilename
  1485. postsCtr+=1
  1486. # sort the list in descending order of date
  1487. postsInBox=OrderedDict(sorted(postsInBoxDict.items(),reverse=True))
  1488. # number of posts in box
  1489. boxHeader['totalItems']=postsCtr
  1490. prevPostFilename=None
  1491. if not pageNumber:
  1492. pageNumber=1
  1493. # Generate first and last entries within header
  1494. if postsCtr>0:
  1495. lastPage=int(postsCtr/itemsPerPage)
  1496. if lastPage<1:
  1497. lastPage=1
  1498. boxHeader['last']= \
  1499. httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page='+str(lastPage)
  1500. # Insert posts
  1501. currPage=1
  1502. postsCtr=0
  1503. for statusNumber,postFilename in postsInBox.items():
  1504. # Are we at the starting page yet?
  1505. if prevPostFilename and currPage==pageNumber and postsCtr==0:
  1506. # update the prev entry for the last message id
  1507. postId = prevPostFilename.split('#statuses#')[1].replace('#activity','')
  1508. boxHeader['prev']= \
  1509. httpPrefix+'://'+domain+'/users/'+nickname+'/'+ \
  1510. boxname+'?min_id='+postId+'&page=true'
  1511. # get the full path of the post file
  1512. filePath = postFilename
  1513. try:
  1514. if os.path.isfile(filePath):
  1515. if currPage == pageNumber and postsOnPageCtr <= itemsPerPage:
  1516. # get the post as json
  1517. with open(filePath, 'r') as fp:
  1518. p=commentjson.load(fp)
  1519. # remove any capability so that it's not displayed
  1520. if p.get('capability'):
  1521. del p['capability']
  1522. # Don't show likes or replies to unauthorized viewers
  1523. if not authorized:
  1524. if p.get('object'):
  1525. if isinstance(p['object'], dict):
  1526. if p['object'].get('likes'):
  1527. p['likes']={}
  1528. if p['object'].get('replies'):
  1529. p['replies']={}
  1530. # insert it into the box feed
  1531. if postsOnPageCtr < itemsPerPage:
  1532. if not headerOnly:
  1533. boxItems['orderedItems'].append(p)
  1534. elif postsOnPageCtr == itemsPerPage:
  1535. # if this is the last post update the next message ID
  1536. if '/statuses/' in p['id']:
  1537. postId = p['id'].split('/statuses/')[1].replace('/activity','')
  1538. boxHeader['next']= \
  1539. httpPrefix+'://'+domain+'/users/'+ \
  1540. nickname+'/'+boxname+'?max_id='+ \
  1541. postId+'&page=true'
  1542. postsOnPageCtr += 1
  1543. # remember the last post filename for use with prev
  1544. prevPostFilename = postFilename
  1545. if postsOnPageCtr > itemsPerPage:
  1546. break
  1547. # count the pages
  1548. postsCtr += 1
  1549. if postsCtr >= itemsPerPage:
  1550. postsCtr = 0
  1551. currPage += 1
  1552. except Exception as e:
  1553. print(e)
  1554. if headerOnly:
  1555. return boxHeader
  1556. return boxItems
  1557. def archivePosts(baseDir: str,httpPrefix: str,archiveDir: str,maxPostsInBox=256) -> None:
  1558. """Archives posts for all accounts
  1559. """
  1560. if archiveDir:
  1561. if not os.path.isdir(archiveDir):
  1562. os.mkdir(archiveDir)
  1563. if archiveDir:
  1564. if not os.path.isdir(archiveDir+'/accounts'):
  1565. os.mkdir(archiveDir+'/accounts')
  1566. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  1567. for handle in dirs:
  1568. if '@' in handle:
  1569. nickname=handle.split('@')[0]
  1570. domain=handle.split('@')[1]
  1571. archiveSubdir=None
  1572. if archiveDir:
  1573. if not os.path.isdir(archiveDir+'/accounts/'+handle):
  1574. os.mkdir(archiveDir+'/accounts/'+handle)
  1575. if not os.path.isdir(archiveDir+'/accounts/'+handle+'/inbox'):
  1576. os.mkdir(archiveDir+'/accounts/'+handle+'/inbox')
  1577. if not os.path.isdir(archiveDir+'/accounts/'+handle+'/outbox'):
  1578. os.mkdir(archiveDir+'/accounts/'+handle+'/outbox')
  1579. archiveSubdir=archiveDir+'/accounts/'+handle+'/inbox'
  1580. archivePostsForPerson(httpPrefix,nickname,domain,baseDir, \
  1581. 'inbox',archiveSubdir, \
  1582. maxPostsInBox)
  1583. if archiveDir:
  1584. archiveSubdir=archiveDir+'/accounts/'+handle+'/outbox'
  1585. archivePostsForPerson(httpPrefix,nickname,domain,baseDir, \
  1586. 'outbox',archiveSubdir, \
  1587. maxPostsInBox)
  1588. def archivePostsForPerson(httpPrefix: str,nickname: str,domain: str,baseDir: str, \
  1589. boxname: str,archiveDir: str,maxPostsInBox=256) -> None:
  1590. """Retain a maximum number of posts within the given box
  1591. Move any others to an archive directory
  1592. """
  1593. if boxname!='inbox' and boxname!='outbox':
  1594. return
  1595. if archiveDir:
  1596. if not os.path.isdir(archiveDir):
  1597. os.mkdir(archiveDir)
  1598. boxDir = createPersonDir(nickname,domain,baseDir,boxname)
  1599. postsInBox=sorted(os.listdir(boxDir), reverse=False)
  1600. noOfPosts=len(postsInBox)
  1601. if noOfPosts<=maxPostsInBox:
  1602. return
  1603. for postFilename in postsInBox:
  1604. filePath = os.path.join(boxDir, postFilename)
  1605. if os.path.isfile(filePath):
  1606. if archiveDir:
  1607. repliesPath=filePath.replace('.json','.replies')
  1608. archivePath = os.path.join(archiveDir, postFilename)
  1609. os.rename(filePath,archivePath)
  1610. if os.path.isfile(repliesPath):
  1611. os.rename(repliesPath,archivePath)
  1612. else:
  1613. deletePost(baseDir,httpPrefix,nickname,domain,filePath,False)
  1614. noOfPosts -= 1
  1615. if noOfPosts <= maxPostsInBox:
  1616. break
  1617. def getPublicPostsOfPerson(nickname: str,domain: str, \
  1618. raw: bool,simple: bool,useTor: bool, \
  1619. port: int,httpPrefix: str, \
  1620. debug: bool,projectVersion: str) -> None:
  1621. """ This is really just for test purposes
  1622. """
  1623. session = createSession(domain,port,useTor)
  1624. personCache={}
  1625. cachedWebfingers={}
  1626. federationList=[]
  1627. domainFull=domain
  1628. if port:
  1629. if port!=80 and port!=443:
  1630. if ':' not in domain:
  1631. domainFull=domain+':'+str(port)
  1632. handle=httpPrefix+"://"+domainFull+"/@"+nickname
  1633. wfRequest = \
  1634. webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  1635. domain,projectVersion)
  1636. if not wfRequest:
  1637. sys.exit()
  1638. personUrl,pubKeyId,pubKey,personId,shaedInbox,capabilityAcquisition,avatarUrl,preferredName= \
  1639. getPersonBox(session,wfRequest,personCache, \
  1640. projectVersion,httpPrefix,domain,'outbox')
  1641. wfResult = json.dumps(wfRequest, indent=4, sort_keys=True)
  1642. maxMentions=10
  1643. maxEmoji=10
  1644. maxAttachments=5
  1645. userPosts = getPosts(session,personUrl,30,maxMentions,maxEmoji, \
  1646. maxAttachments,federationList, \
  1647. personCache,raw,simple,debug, \
  1648. projectVersion,httpPrefix,domain)
  1649. #print(str(userPosts))
  1650. def sendCapabilitiesUpdate(session,baseDir: str,httpPrefix: str, \
  1651. nickname: str,domain: str,port: int, \
  1652. followerUrl,updateCaps: [], \
  1653. sendThreads: [],postLog: [], \
  1654. cachedWebfingers: {},personCache: {}, \
  1655. federationList :[],debug :bool, \
  1656. projectVersion: str) -> int:
  1657. """When the capabilities for a follower are changed this
  1658. sends out an update. followerUrl is the actor of the follower.
  1659. """
  1660. updateJson=capabilitiesUpdate(baseDir,httpPrefix, \
  1661. nickname,domain,port, \
  1662. followerUrl, \
  1663. updateCaps)
  1664. if not updateJson:
  1665. return 1
  1666. if debug:
  1667. pprint(updateJson)
  1668. print('DEBUG: sending capabilities update from '+ \
  1669. nickname+'@'+domain+' port '+ str(port) + \
  1670. ' to '+followerUrl)
  1671. clientToServer=False
  1672. followerNickname=getNicknameFromActor(followerUrl)
  1673. followerDomain,followerPort=getDomainFromActor(followerUrl)
  1674. return sendSignedJson(updateJson,session,baseDir, \
  1675. nickname,domain,port, \
  1676. followerNickname,followerDomain,followerPort, '', \
  1677. httpPrefix,True,clientToServer, \
  1678. federationList, \
  1679. sendThreads,postLog,cachedWebfingers, \
  1680. personCache,debug,projectVersion)
  1681. def populateRepliesJson(baseDir: str,nickname: str,domain: str,postRepliesFilename: str,authorized: bool,repliesJson: {}) -> None:
  1682. # populate the items list with replies
  1683. repliesBoxes=['outbox','inbox']
  1684. with open(postRepliesFilename,'r') as repliesFile:
  1685. for messageId in repliesFile:
  1686. replyFound=False
  1687. # examine inbox and outbox
  1688. for boxname in repliesBoxes:
  1689. searchFilename= \
  1690. baseDir+ \
  1691. '/accounts/'+nickname+'@'+ \
  1692. domain+'/'+ \
  1693. boxname+'/'+ \
  1694. messageId.replace('\n','').replace('/','#')+'.json'
  1695. if os.path.isfile(searchFilename):
  1696. if authorized or \
  1697. 'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
  1698. with open(searchFilename, 'r') as fp:
  1699. postJsonObject=commentjson.load(fp)
  1700. if postJsonObject['object'].get('cc'):
  1701. if authorized or \
  1702. ('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
  1703. 'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
  1704. repliesJson['orderedItems'].append(postJsonObject)
  1705. replyFound=True
  1706. else:
  1707. if authorized or \
  1708. 'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
  1709. repliesJson['orderedItems'].append(postJsonObject)
  1710. replyFound=True
  1711. break
  1712. # if not in either inbox or outbox then examine the shared inbox
  1713. if not replyFound:
  1714. searchFilename= \
  1715. baseDir+ \
  1716. '/accounts/inbox@'+ \
  1717. domain+'/inbox/'+ \
  1718. messageId.replace('\n','').replace('/','#')+'.json'
  1719. if os.path.isfile(searchFilename):
  1720. if authorized or \
  1721. 'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
  1722. # get the json of the reply and append it to the collection
  1723. with open(searchFilename, 'r') as fp:
  1724. postJsonObject=commentjson.load(fp)
  1725. if postJsonObject['object'].get('cc'):
  1726. if authorized or \
  1727. ('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
  1728. 'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
  1729. repliesJson['orderedItems'].append(postJsonObject)
  1730. else:
  1731. if authorized or \
  1732. 'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
  1733. repliesJson['orderedItems'].append(postJsonObject)