Modern ActivityPub compliant server, designed for simplicity and accessibility. Includes calendar and sharing economy features to empower your federated community. https://code.freedombone.net/bashrc/epicyon Docs: https://epicyon.net/#install
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

person.py 49KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322
  1. __filename__ = "person.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.2.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import time
  9. import os
  10. import subprocess
  11. import shutil
  12. import pyqrcode
  13. from random import randint
  14. from pathlib import Path
  15. from cryptography.hazmat.backends import default_backend
  16. from cryptography.hazmat.primitives.asymmetric import rsa
  17. from cryptography.hazmat.primitives import serialization
  18. from shutil import copyfile
  19. from webfinger import createWebfingerEndpoint
  20. from webfinger import storeWebfingerEndpoint
  21. from posts import getUserUrl
  22. from posts import createDMTimeline
  23. from posts import createRepliesTimeline
  24. from posts import createMediaTimeline
  25. from posts import createNewsTimeline
  26. from posts import createBlogsTimeline
  27. from posts import createFeaturesTimeline
  28. from posts import createBookmarksTimeline
  29. from posts import createEventsTimeline
  30. from posts import createInbox
  31. from posts import createOutbox
  32. from posts import createModeration
  33. from auth import storeBasicCredentials
  34. from auth import removePassword
  35. from roles import setRole
  36. from media import processMetaData
  37. from utils import getStatusNumber
  38. from utils import getFullDomain
  39. from utils import validNickname
  40. from utils import loadJson
  41. from utils import saveJson
  42. from utils import setConfigParam
  43. from utils import getConfigParam
  44. from utils import refreshNewswire
  45. from utils import getProtocolPrefixes
  46. from utils import hasUsersPath
  47. from session import createSession
  48. from session import getJson
  49. from webfinger import webfingerHandle
  50. from pprint import pprint
  51. def generateRSAKey() -> (str, str):
  52. key = rsa.generate_private_key(
  53. public_exponent=65537,
  54. key_size=2048,
  55. backend=default_backend()
  56. )
  57. privateKeyPem = key.private_bytes(
  58. encoding=serialization.Encoding.PEM,
  59. format=serialization.PrivateFormat.TraditionalOpenSSL,
  60. encryption_algorithm=serialization.NoEncryption()
  61. )
  62. pubkey = key.public_key()
  63. publicKeyPem = pubkey.public_bytes(
  64. encoding=serialization.Encoding.PEM,
  65. format=serialization.PublicFormat.SubjectPublicKeyInfo,
  66. )
  67. privateKeyPem = privateKeyPem.decode("utf-8")
  68. publicKeyPem = publicKeyPem.decode("utf-8")
  69. return privateKeyPem, publicKeyPem
  70. def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
  71. port: int, imageFilename: str, imageType: str,
  72. resolution: str, city: str) -> bool:
  73. """Saves the given image file as an avatar or background
  74. image for the given person
  75. """
  76. imageFilename = imageFilename.replace('\n', '').replace('\r', '')
  77. if not (imageFilename.endswith('.png') or
  78. imageFilename.endswith('.jpg') or
  79. imageFilename.endswith('.jpeg') or
  80. imageFilename.endswith('.svg') or
  81. imageFilename.endswith('.gif')):
  82. print('Profile image must be png, jpg, gif or svg format')
  83. return False
  84. if imageFilename.startswith('~/'):
  85. imageFilename = imageFilename.replace('~/', str(Path.home()) + '/')
  86. if ':' in domain:
  87. domain = domain.split(':')[0]
  88. fullDomain = getFullDomain(domain, port)
  89. handle = nickname + '@' + domain
  90. personFilename = baseDir + '/accounts/' + handle + '.json'
  91. if not os.path.isfile(personFilename):
  92. print('person definition not found: ' + personFilename)
  93. return False
  94. if not os.path.isdir(baseDir + '/accounts/' + handle):
  95. print('Account not found: ' + baseDir + '/accounts/' + handle)
  96. return False
  97. iconFilenameBase = 'icon'
  98. if imageType == 'avatar' or imageType == 'icon':
  99. iconFilenameBase = 'icon'
  100. else:
  101. iconFilenameBase = 'image'
  102. mediaType = 'image/png'
  103. iconFilename = iconFilenameBase + '.png'
  104. if imageFilename.endswith('.jpg') or \
  105. imageFilename.endswith('.jpeg'):
  106. mediaType = 'image/jpeg'
  107. iconFilename = iconFilenameBase + '.jpg'
  108. if imageFilename.endswith('.gif'):
  109. mediaType = 'image/gif'
  110. iconFilename = iconFilenameBase + '.gif'
  111. if imageFilename.endswith('.svg'):
  112. mediaType = 'image/svg+xml'
  113. iconFilename = iconFilenameBase + '.svg'
  114. profileFilename = baseDir + '/accounts/' + handle + '/' + iconFilename
  115. personJson = loadJson(personFilename)
  116. if personJson:
  117. personJson[iconFilenameBase]['mediaType'] = mediaType
  118. personJson[iconFilenameBase]['url'] = \
  119. httpPrefix + '://' + fullDomain + '/users/' + \
  120. nickname + '/'+iconFilename
  121. saveJson(personJson, personFilename)
  122. cmd = \
  123. '/usr/bin/convert ' + imageFilename + ' -size ' + \
  124. resolution + ' -quality 50 ' + profileFilename
  125. subprocess.call(cmd, shell=True)
  126. processMetaData(baseDir, nickname, domain,
  127. profileFilename, profileFilename, city)
  128. return True
  129. return False
  130. def _accountExists(baseDir: str, nickname: str, domain: str) -> bool:
  131. """Returns true if the given account exists
  132. """
  133. if ':' in domain:
  134. domain = domain.split(':')[0]
  135. return os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain) or \
  136. os.path.isdir(baseDir + '/deactivated/' + nickname + '@' + domain)
  137. def randomizeActorImages(personJson: {}) -> None:
  138. """Randomizes the filenames for avatar image and background
  139. This causes other instances to update their cached avatar image
  140. """
  141. personId = personJson['id']
  142. lastPartOfFilename = personJson['icon']['url'].split('/')[-1]
  143. existingExtension = lastPartOfFilename.split('.')[1]
  144. # NOTE: these files don't need to have cryptographically
  145. # secure names
  146. randStr = str(randint(10000000000000, 99999999999999)) # nosec
  147. personJson['icon']['url'] = \
  148. personId + '/avatar' + randStr + '.' + existingExtension
  149. lastPartOfFilename = personJson['image']['url'].split('/')[-1]
  150. existingExtension = lastPartOfFilename.split('.')[1]
  151. randStr = str(randint(10000000000000, 99999999999999)) # nosec
  152. personJson['image']['url'] = \
  153. personId + '/image' + randStr + '.' + existingExtension
  154. def getDefaultPersonContext() -> str:
  155. """Gets the default actor context
  156. """
  157. return {
  158. 'Curve25519Key': 'toot:Curve25519Key',
  159. 'Device': 'toot:Device',
  160. 'Ed25519Key': 'toot:Ed25519Key',
  161. 'Ed25519Signature': 'toot:Ed25519Signature',
  162. 'EncryptedMessage': 'toot:EncryptedMessage',
  163. 'IdentityProof': 'toot:IdentityProof',
  164. 'PropertyValue': 'schema:PropertyValue',
  165. 'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
  166. 'cipherText': 'toot:cipherText',
  167. 'claim': {'@id': 'toot:claim', '@type': '@id'},
  168. 'deviceId': 'toot:deviceId',
  169. 'devices': {'@id': 'toot:devices', '@type': '@id'},
  170. 'discoverable': 'toot:discoverable',
  171. 'featured': {'@id': 'toot:featured', '@type': '@id'},
  172. 'featuredTags': {'@id': 'toot:featuredTags', '@type': '@id'},
  173. 'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
  174. 'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
  175. 'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
  176. 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
  177. 'messageFranking': 'toot:messageFranking',
  178. 'messageType': 'toot:messageType',
  179. 'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
  180. 'publicKeyBase64': 'toot:publicKeyBase64',
  181. 'schema': 'http://schema.org#',
  182. 'suspended': 'toot:suspended',
  183. 'toot': 'http://joinmastodon.org/ns#',
  184. 'value': 'schema:value',
  185. 'Occupation': 'schema:Occupation',
  186. 'OrganizationRole': 'schema:OrganizationRole',
  187. 'WebSite': 'schema:Project'
  188. }
  189. def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
  190. httpPrefix: str, saveToFile: bool,
  191. manualFollowerApproval: bool,
  192. password=None) -> (str, str, {}, {}):
  193. """Returns the private key, public key, actor and webfinger endpoint
  194. """
  195. privateKeyPem, publicKeyPem = generateRSAKey()
  196. webfingerEndpoint = \
  197. createWebfingerEndpoint(nickname, domain, port,
  198. httpPrefix, publicKeyPem)
  199. if saveToFile:
  200. storeWebfingerEndpoint(nickname, domain, port,
  201. baseDir, webfingerEndpoint)
  202. handle = nickname + '@' + domain
  203. originalDomain = domain
  204. domain = getFullDomain(domain, port)
  205. personType = 'Person'
  206. # Enable follower approval by default
  207. approveFollowers = manualFollowerApproval
  208. personName = nickname
  209. personId = httpPrefix + '://' + domain + '/users/' + nickname
  210. inboxStr = personId + '/inbox'
  211. personUrl = httpPrefix + '://' + domain + '/@' + personName
  212. if nickname == 'inbox':
  213. # shared inbox
  214. inboxStr = httpPrefix + '://' + domain + '/actor/inbox'
  215. personId = httpPrefix + '://' + domain + '/actor'
  216. personUrl = httpPrefix + '://' + domain + \
  217. '/about/more?instance_actor=true'
  218. personName = originalDomain
  219. approveFollowers = True
  220. personType = 'Application'
  221. elif nickname == 'news':
  222. personUrl = httpPrefix + '://' + domain + \
  223. '/about/more?news_actor=true'
  224. approveFollowers = True
  225. personType = 'Application'
  226. # NOTE: these image files don't need to have
  227. # cryptographically secure names
  228. imageUrl = \
  229. personId + '/image' + \
  230. str(randint(10000000000000, 99999999999999)) + '.png' # nosec
  231. iconUrl = \
  232. personId + '/avatar' + \
  233. str(randint(10000000000000, 99999999999999)) + '.png' # nosec
  234. statusNumber, published = getStatusNumber()
  235. newPerson = {
  236. '@context': [
  237. 'https://www.w3.org/ns/activitystreams',
  238. 'https://w3id.org/security/v1',
  239. getDefaultPersonContext()
  240. ],
  241. 'published': published,
  242. 'alsoKnownAs': [],
  243. 'attachment': [],
  244. 'devices': personId + '/collections/devices',
  245. 'endpoints': {
  246. 'id': personId + '/endpoints',
  247. 'sharedInbox': httpPrefix + '://' + domain + '/inbox',
  248. },
  249. 'featured': personId + '/collections/featured',
  250. 'featuredTags': personId + '/collections/tags',
  251. 'followers': personId + '/followers',
  252. 'following': personId + '/following',
  253. 'tts': personId + '/speaker',
  254. 'shares': personId + '/shares',
  255. 'hasOccupation': {
  256. '@type': 'Occupation',
  257. 'name': "",
  258. 'skills': ""
  259. },
  260. "affiliation": {
  261. "@type": "OrganizationRole",
  262. "roleName": "",
  263. "affiliation": {
  264. "@type": "WebSite",
  265. "url": httpPrefix + '://' + domain
  266. },
  267. "startDate": published
  268. },
  269. 'availability': None,
  270. 'icon': {
  271. 'mediaType': 'image/png',
  272. 'type': 'Image',
  273. 'url': iconUrl
  274. },
  275. 'id': personId,
  276. 'image': {
  277. 'mediaType': 'image/png',
  278. 'type': 'Image',
  279. 'url': imageUrl
  280. },
  281. 'inbox': inboxStr,
  282. 'manuallyApprovesFollowers': approveFollowers,
  283. 'discoverable': True,
  284. 'name': personName,
  285. 'outbox': personId + '/outbox',
  286. 'preferredUsername': personName,
  287. 'summary': '',
  288. 'publicKey': {
  289. 'id': personId + '#main-key',
  290. 'owner': personId,
  291. 'publicKeyPem': publicKeyPem
  292. },
  293. 'tag': [],
  294. 'type': personType,
  295. 'url': personUrl
  296. }
  297. if nickname == 'inbox':
  298. # fields not needed by the shared inbox
  299. del newPerson['outbox']
  300. del newPerson['icon']
  301. del newPerson['image']
  302. if newPerson.get('skills'):
  303. del newPerson['skills']
  304. del newPerson['shares']
  305. if newPerson.get('roles'):
  306. del newPerson['roles']
  307. del newPerson['tag']
  308. del newPerson['availability']
  309. del newPerson['followers']
  310. del newPerson['following']
  311. del newPerson['attachment']
  312. if saveToFile:
  313. # save person to file
  314. peopleSubdir = '/accounts'
  315. if not os.path.isdir(baseDir + peopleSubdir):
  316. os.mkdir(baseDir + peopleSubdir)
  317. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle):
  318. os.mkdir(baseDir + peopleSubdir + '/' + handle)
  319. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/inbox'):
  320. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/inbox')
  321. if not os.path.isdir(baseDir + peopleSubdir + '/' +
  322. handle + '/outbox'):
  323. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox')
  324. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'):
  325. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
  326. filename = baseDir + peopleSubdir + '/' + handle + '.json'
  327. saveJson(newPerson, filename)
  328. # save to cache
  329. if not os.path.isdir(baseDir + '/cache'):
  330. os.mkdir(baseDir + '/cache')
  331. if not os.path.isdir(baseDir + '/cache/actors'):
  332. os.mkdir(baseDir + '/cache/actors')
  333. cacheFilename = baseDir + '/cache/actors/' + \
  334. newPerson['id'].replace('/', '#') + '.json'
  335. saveJson(newPerson, cacheFilename)
  336. # save the private key
  337. privateKeysSubdir = '/keys/private'
  338. if not os.path.isdir(baseDir + '/keys'):
  339. os.mkdir(baseDir + '/keys')
  340. if not os.path.isdir(baseDir + privateKeysSubdir):
  341. os.mkdir(baseDir + privateKeysSubdir)
  342. filename = baseDir + privateKeysSubdir + '/' + handle + '.key'
  343. with open(filename, 'w+') as text_file:
  344. print(privateKeyPem, file=text_file)
  345. # save the public key
  346. publicKeysSubdir = '/keys/public'
  347. if not os.path.isdir(baseDir + publicKeysSubdir):
  348. os.mkdir(baseDir + publicKeysSubdir)
  349. filename = baseDir + publicKeysSubdir + '/' + handle + '.pem'
  350. with open(filename, 'w+') as text_file:
  351. print(publicKeyPem, file=text_file)
  352. if password:
  353. storeBasicCredentials(baseDir, nickname, password)
  354. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  355. def registerAccount(baseDir: str, httpPrefix: str, domain: str, port: int,
  356. nickname: str, password: str,
  357. manualFollowerApproval: bool) -> bool:
  358. """Registers a new account from the web interface
  359. """
  360. if _accountExists(baseDir, nickname, domain):
  361. return False
  362. if not validNickname(domain, nickname):
  363. print('REGISTER: Nickname ' + nickname + ' is invalid')
  364. return False
  365. if len(password) < 8:
  366. print('REGISTER: Password should be at least 8 characters')
  367. return False
  368. (privateKeyPem, publicKeyPem,
  369. newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
  370. domain, port,
  371. httpPrefix, True,
  372. manualFollowerApproval,
  373. password)
  374. if privateKeyPem:
  375. return True
  376. return False
  377. def createGroup(baseDir: str, nickname: str, domain: str, port: int,
  378. httpPrefix: str, saveToFile: bool,
  379. password=None) -> (str, str, {}, {}):
  380. """Returns a group
  381. """
  382. (privateKeyPem, publicKeyPem,
  383. newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
  384. domain, port,
  385. httpPrefix, saveToFile,
  386. False, password)
  387. newPerson['type'] = 'Group'
  388. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  389. def savePersonQrcode(baseDir: str,
  390. nickname: str, domain: str, port: int,
  391. scale=6) -> None:
  392. """Saves a qrcode image for the handle of the person
  393. This helps to transfer onion or i2p handles to a mobile device
  394. """
  395. qrcodeFilename = baseDir + '/accounts/' + \
  396. nickname + '@' + domain + '/qrcode.png'
  397. if os.path.isfile(qrcodeFilename):
  398. return
  399. handle = getFullDomain('@' + nickname + '@' + domain, port)
  400. url = pyqrcode.create(handle)
  401. url.png(qrcodeFilename, scale)
  402. def createPerson(baseDir: str, nickname: str, domain: str, port: int,
  403. httpPrefix: str, saveToFile: bool,
  404. manualFollowerApproval: bool,
  405. password=None) -> (str, str, {}, {}):
  406. """Returns the private key, public key, actor and webfinger endpoint
  407. """
  408. if not validNickname(domain, nickname):
  409. return None, None, None, None
  410. # If a config.json file doesn't exist then don't decrement
  411. # remaining registrations counter
  412. if nickname != 'news':
  413. remainingConfigExists = \
  414. getConfigParam(baseDir, 'registrationsRemaining')
  415. if remainingConfigExists:
  416. registrationsRemaining = int(remainingConfigExists)
  417. if registrationsRemaining <= 0:
  418. return None, None, None, None
  419. else:
  420. if os.path.isdir(baseDir + '/accounts/news@' + domain):
  421. # news account already exists
  422. return None, None, None, None
  423. (privateKeyPem, publicKeyPem,
  424. newPerson, webfingerEndpoint) = _createPersonBase(baseDir, nickname,
  425. domain, port,
  426. httpPrefix,
  427. saveToFile,
  428. manualFollowerApproval,
  429. password)
  430. if not getConfigParam(baseDir, 'admin'):
  431. if nickname != 'news':
  432. # print(nickname+' becomes the instance admin and a moderator')
  433. setConfigParam(baseDir, 'admin', nickname)
  434. setRole(baseDir, nickname, domain, 'admin')
  435. setRole(baseDir, nickname, domain, 'moderator')
  436. setRole(baseDir, nickname, domain, 'editor')
  437. if not os.path.isdir(baseDir + '/accounts'):
  438. os.mkdir(baseDir + '/accounts')
  439. if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain):
  440. os.mkdir(baseDir + '/accounts/' + nickname + '@' + domain)
  441. if manualFollowerApproval:
  442. followDMsFilename = baseDir + '/accounts/' + \
  443. nickname + '@' + domain + '/.followDMs'
  444. with open(followDMsFilename, 'w+') as fFile:
  445. fFile.write('\n')
  446. # notify when posts are liked
  447. if nickname != 'news':
  448. notifyLikesFilename = baseDir + '/accounts/' + \
  449. nickname + '@' + domain + '/.notifyLikes'
  450. with open(notifyLikesFilename, 'w+') as nFile:
  451. nFile.write('\n')
  452. theme = getConfigParam(baseDir, 'theme')
  453. if not theme:
  454. theme = 'default'
  455. if nickname != 'news':
  456. if os.path.isfile(baseDir + '/img/default-avatar.png'):
  457. copyfile(baseDir + '/img/default-avatar.png',
  458. baseDir + '/accounts/' + nickname + '@' + domain +
  459. '/avatar.png')
  460. else:
  461. newsAvatar = baseDir + '/theme/' + theme + '/icons/avatar_news.png'
  462. if os.path.isfile(newsAvatar):
  463. copyfile(newsAvatar,
  464. baseDir + '/accounts/' + nickname + '@' + domain +
  465. '/avatar.png')
  466. defaultProfileImageFilename = baseDir + '/theme/default/image.png'
  467. if theme:
  468. if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'):
  469. defaultProfileImageFilename = \
  470. baseDir + '/theme/' + theme + '/image.png'
  471. if os.path.isfile(defaultProfileImageFilename):
  472. copyfile(defaultProfileImageFilename, baseDir +
  473. '/accounts/' + nickname + '@' + domain + '/image.png')
  474. defaultBannerFilename = baseDir + '/theme/default/banner.png'
  475. if theme:
  476. if os.path.isfile(baseDir + '/theme/' + theme + '/banner.png'):
  477. defaultBannerFilename = baseDir + '/theme/' + theme + '/banner.png'
  478. if os.path.isfile(defaultBannerFilename):
  479. copyfile(defaultBannerFilename, baseDir + '/accounts/' +
  480. nickname + '@' + domain + '/banner.png')
  481. if nickname != 'news' and remainingConfigExists:
  482. registrationsRemaining -= 1
  483. setConfigParam(baseDir, 'registrationsRemaining',
  484. str(registrationsRemaining))
  485. savePersonQrcode(baseDir, nickname, domain, port)
  486. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  487. def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int,
  488. httpPrefix: str) -> (str, str, {}, {}):
  489. """Generates the shared inbox
  490. """
  491. return _createPersonBase(baseDir, nickname, domain, port, httpPrefix,
  492. True, True, None)
  493. def createNewsInbox(baseDir: str, domain: str, port: int,
  494. httpPrefix: str) -> (str, str, {}, {}):
  495. """Generates the news inbox
  496. """
  497. return createPerson(baseDir, 'news', domain, port,
  498. httpPrefix, True, True, None)
  499. def personUpgradeActor(baseDir: str, personJson: {},
  500. handle: str, filename: str) -> None:
  501. """Alter the actor to add any new properties
  502. """
  503. updateActor = False
  504. if not os.path.isfile(filename):
  505. print('WARN: actor file not found ' + filename)
  506. return
  507. if not personJson:
  508. personJson = loadJson(filename)
  509. # add a speaker endpoint
  510. if not personJson.get('tts'):
  511. personJson['tts'] = personJson['id'] + '/speaker'
  512. updateActor = True
  513. if not personJson.get('published'):
  514. statusNumber, published = getStatusNumber()
  515. personJson['published'] = published
  516. updateActor = True
  517. occupationName = ''
  518. if personJson.get('occupationName'):
  519. occupationName = personJson['occupationName']
  520. del personJson['occupationName']
  521. if personJson.get('occupation'):
  522. occupationName = personJson['occupation']
  523. del personJson['occupation']
  524. # if the older skills format is being used then switch
  525. # to the new one
  526. if not personJson.get('hasOccupation'):
  527. personJson['hasOccupation'] = {
  528. '@type': 'Occupation',
  529. 'name': occupationName,
  530. 'skills': ""
  531. }
  532. updateActor = True
  533. # remove the old skills format
  534. if personJson.get('skills'):
  535. del personJson['skills']
  536. updateActor = True
  537. # if the older roles format is being used then switch
  538. # to the new one
  539. if not personJson.get('affiliation'):
  540. rolesStr = ''
  541. adminName = getConfigParam(baseDir, 'admin')
  542. if personJson['id'].endswith('/users/' + adminName):
  543. rolesStr = 'admin, moderator, editor'
  544. statusNumber, published = getStatusNumber()
  545. personJson['affiliation'] = {
  546. "@type": "OrganizationRole",
  547. "roleName": rolesStr,
  548. "affiliation": {
  549. "@type": "WebSite",
  550. "url": personJson['id'].split('/users/')[0]
  551. },
  552. "startDate": published
  553. }
  554. updateActor = True
  555. # if no roles are defined then ensure that the admin
  556. # roles are configured
  557. if not personJson['affiliation']['roleName']:
  558. adminName = getConfigParam(baseDir, 'admin')
  559. if personJson['id'].endswith('/users/' + adminName):
  560. personJson['affiliation']['roleName'] = \
  561. 'admin, moderator, editor'
  562. updateActor = True
  563. # remove the old roles format
  564. if personJson.get('roles'):
  565. del personJson['roles']
  566. updateActor = True
  567. if updateActor:
  568. personJson['@context'] = [
  569. 'https://www.w3.org/ns/activitystreams',
  570. 'https://w3id.org/security/v1',
  571. getDefaultPersonContext()
  572. ],
  573. saveJson(personJson, filename)
  574. # also update the actor within the cache
  575. actorCacheFilename = \
  576. baseDir + '/accounts/cache/actors/' + \
  577. personJson['id'].replace('/', '#') + '.json'
  578. if os.path.isfile(actorCacheFilename):
  579. saveJson(personJson, actorCacheFilename)
  580. # update domain/@nickname in actors cache
  581. actorCacheFilename = \
  582. baseDir + '/accounts/cache/actors/' + \
  583. personJson['id'].replace('/users/', '/@').replace('/', '#') + \
  584. '.json'
  585. if os.path.isfile(actorCacheFilename):
  586. saveJson(personJson, actorCacheFilename)
  587. def personLookup(domain: str, path: str, baseDir: str) -> {}:
  588. """Lookup the person for an given nickname
  589. """
  590. if path.endswith('#main-key'):
  591. path = path.replace('#main-key', '')
  592. # is this a shared inbox lookup?
  593. isSharedInbox = False
  594. if path == '/inbox' or path == '/users/inbox' or path == '/sharedInbox':
  595. # shared inbox actor on @domain@domain
  596. path = '/users/' + domain
  597. isSharedInbox = True
  598. else:
  599. notPersonLookup = ('/inbox', '/outbox', '/outboxarchive',
  600. '/followers', '/following', '/featured',
  601. '.png', '.jpg', '.gif', '.svg', '.mpv')
  602. for ending in notPersonLookup:
  603. if path.endswith(ending):
  604. return None
  605. nickname = None
  606. if path.startswith('/users/'):
  607. nickname = path.replace('/users/', '', 1)
  608. if path.startswith('/@'):
  609. nickname = path.replace('/@', '', 1)
  610. if not nickname:
  611. return None
  612. if not isSharedInbox and not validNickname(domain, nickname):
  613. return None
  614. if ':' in domain:
  615. domain = domain.split(':')[0]
  616. handle = nickname + '@' + domain
  617. filename = baseDir + '/accounts/' + handle + '.json'
  618. if not os.path.isfile(filename):
  619. return None
  620. personJson = loadJson(filename)
  621. personUpgradeActor(baseDir, personJson, handle, filename)
  622. # if not personJson:
  623. # personJson={"user": "unknown"}
  624. return personJson
  625. def personBoxJson(recentPostsCache: {},
  626. session, baseDir: str, domain: str, port: int, path: str,
  627. httpPrefix: str, noOfItems: int, boxname: str,
  628. authorized: bool,
  629. newswireVotesThreshold: int, positiveVoting: bool,
  630. votingTimeMins: int) -> {}:
  631. """Obtain the inbox/outbox/moderation feed for the given person
  632. """
  633. if boxname != 'inbox' and boxname != 'dm' and \
  634. boxname != 'tlreplies' and boxname != 'tlmedia' and \
  635. boxname != 'tlblogs' and boxname != 'tlnews' and \
  636. boxname != 'tlfeatures' and \
  637. boxname != 'outbox' and boxname != 'moderation' and \
  638. boxname != 'tlbookmarks' and boxname != 'bookmarks' and \
  639. boxname != 'tlevents':
  640. return None
  641. if not '/' + boxname in path:
  642. return None
  643. # Only show the header by default
  644. headerOnly = True
  645. # handle page numbers
  646. pageNumber = None
  647. if '?page=' in path:
  648. pageNumber = path.split('?page=')[1]
  649. if pageNumber == 'true':
  650. pageNumber = 1
  651. else:
  652. try:
  653. pageNumber = int(pageNumber)
  654. except BaseException:
  655. pass
  656. path = path.split('?page=')[0]
  657. headerOnly = False
  658. if not path.endswith('/' + boxname):
  659. return None
  660. nickname = None
  661. if path.startswith('/users/'):
  662. nickname = path.replace('/users/', '', 1).replace('/' + boxname, '')
  663. if path.startswith('/@'):
  664. nickname = path.replace('/@', '', 1).replace('/' + boxname, '')
  665. if not nickname:
  666. return None
  667. if not validNickname(domain, nickname):
  668. return None
  669. if boxname == 'inbox':
  670. return createInbox(recentPostsCache,
  671. session, baseDir, nickname, domain, port,
  672. httpPrefix,
  673. noOfItems, headerOnly, pageNumber)
  674. elif boxname == 'dm':
  675. return createDMTimeline(recentPostsCache,
  676. session, baseDir, nickname, domain, port,
  677. httpPrefix,
  678. noOfItems, headerOnly, pageNumber)
  679. elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
  680. return createBookmarksTimeline(session, baseDir, nickname, domain,
  681. port, httpPrefix,
  682. noOfItems, headerOnly,
  683. pageNumber)
  684. elif boxname == 'tlevents':
  685. return createEventsTimeline(recentPostsCache,
  686. session, baseDir, nickname, domain,
  687. port, httpPrefix,
  688. noOfItems, headerOnly,
  689. pageNumber)
  690. elif boxname == 'tlreplies':
  691. return createRepliesTimeline(recentPostsCache,
  692. session, baseDir, nickname, domain,
  693. port, httpPrefix,
  694. noOfItems, headerOnly,
  695. pageNumber)
  696. elif boxname == 'tlmedia':
  697. return createMediaTimeline(session, baseDir, nickname, domain, port,
  698. httpPrefix, noOfItems, headerOnly,
  699. pageNumber)
  700. elif boxname == 'tlnews':
  701. return createNewsTimeline(session, baseDir, nickname, domain, port,
  702. httpPrefix, noOfItems, headerOnly,
  703. newswireVotesThreshold, positiveVoting,
  704. votingTimeMins, pageNumber)
  705. elif boxname == 'tlfeatures':
  706. return createFeaturesTimeline(session, baseDir, nickname, domain, port,
  707. httpPrefix, noOfItems, headerOnly,
  708. pageNumber)
  709. elif boxname == 'tlblogs':
  710. return createBlogsTimeline(session, baseDir, nickname, domain, port,
  711. httpPrefix, noOfItems, headerOnly,
  712. pageNumber)
  713. elif boxname == 'outbox':
  714. return createOutbox(session, baseDir, nickname, domain, port,
  715. httpPrefix,
  716. noOfItems, headerOnly, authorized,
  717. pageNumber)
  718. elif boxname == 'moderation':
  719. return createModeration(baseDir, nickname, domain, port,
  720. httpPrefix,
  721. noOfItems, headerOnly,
  722. pageNumber)
  723. return None
  724. def setDisplayNickname(baseDir: str, nickname: str, domain: str,
  725. displayName: str) -> bool:
  726. if len(displayName) > 32:
  727. return False
  728. handle = nickname + '@' + domain
  729. filename = baseDir + '/accounts/' + handle + '.json'
  730. if not os.path.isfile(filename):
  731. return False
  732. personJson = loadJson(filename)
  733. if not personJson:
  734. return False
  735. personJson['name'] = displayName
  736. saveJson(personJson, filename)
  737. return True
  738. def setBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool:
  739. if len(bio) > 32:
  740. return False
  741. handle = nickname + '@' + domain
  742. filename = baseDir + '/accounts/' + handle + '.json'
  743. if not os.path.isfile(filename):
  744. return False
  745. personJson = loadJson(filename)
  746. if not personJson:
  747. return False
  748. if not personJson.get('summary'):
  749. return False
  750. personJson['summary'] = bio
  751. saveJson(personJson, filename)
  752. return True
  753. def reenableAccount(baseDir: str, nickname: str) -> None:
  754. """Removes an account suspention
  755. """
  756. suspendedFilename = baseDir + '/accounts/suspended.txt'
  757. if os.path.isfile(suspendedFilename):
  758. with open(suspendedFilename, "r") as f:
  759. lines = f.readlines()
  760. suspendedFile = open(suspendedFilename, "w+")
  761. for suspended in lines:
  762. if suspended.strip('\n').strip('\r') != nickname:
  763. suspendedFile.write(suspended)
  764. suspendedFile.close()
  765. def suspendAccount(baseDir: str, nickname: str, domain: str) -> None:
  766. """Suspends the given account
  767. """
  768. # Don't suspend the admin
  769. adminNickname = getConfigParam(baseDir, 'admin')
  770. if not adminNickname:
  771. return
  772. if nickname == adminNickname:
  773. return
  774. # Don't suspend moderators
  775. moderatorsFile = baseDir + '/accounts/moderators.txt'
  776. if os.path.isfile(moderatorsFile):
  777. with open(moderatorsFile, "r") as f:
  778. lines = f.readlines()
  779. for moderator in lines:
  780. if moderator.strip('\n').strip('\r') == nickname:
  781. return
  782. saltFilename = baseDir + '/accounts/' + \
  783. nickname + '@' + domain + '/.salt'
  784. if os.path.isfile(saltFilename):
  785. os.remove(saltFilename)
  786. tokenFilename = baseDir + '/accounts/' + \
  787. nickname + '@' + domain + '/.token'
  788. if os.path.isfile(tokenFilename):
  789. os.remove(tokenFilename)
  790. suspendedFilename = baseDir + '/accounts/suspended.txt'
  791. if os.path.isfile(suspendedFilename):
  792. with open(suspendedFilename, "r") as f:
  793. lines = f.readlines()
  794. for suspended in lines:
  795. if suspended.strip('\n').strip('\r') == nickname:
  796. return
  797. suspendedFile = open(suspendedFilename, 'a+')
  798. if suspendedFile:
  799. suspendedFile.write(nickname + '\n')
  800. suspendedFile.close()
  801. else:
  802. suspendedFile = open(suspendedFilename, 'w+')
  803. if suspendedFile:
  804. suspendedFile.write(nickname + '\n')
  805. suspendedFile.close()
  806. def canRemovePost(baseDir: str, nickname: str,
  807. domain: str, port: int, postId: str) -> bool:
  808. """Returns true if the given post can be removed
  809. """
  810. if '/statuses/' not in postId:
  811. return False
  812. domainFull = getFullDomain(domain, port)
  813. # is the post by the admin?
  814. adminNickname = getConfigParam(baseDir, 'admin')
  815. if not adminNickname:
  816. return False
  817. if domainFull + '/users/' + adminNickname + '/' in postId:
  818. return False
  819. # is the post by a moderator?
  820. moderatorsFile = baseDir + '/accounts/moderators.txt'
  821. if os.path.isfile(moderatorsFile):
  822. with open(moderatorsFile, "r") as f:
  823. lines = f.readlines()
  824. for moderator in lines:
  825. if domainFull + '/users/' + moderator.strip('\n') + '/' in postId:
  826. return False
  827. return True
  828. def _removeTagsForNickname(baseDir: str, nickname: str,
  829. domain: str, port: int) -> None:
  830. """Removes tags for a nickname
  831. """
  832. if not os.path.isdir(baseDir + '/tags'):
  833. return
  834. domainFull = getFullDomain(domain, port)
  835. matchStr = domainFull + '/users/' + nickname + '/'
  836. directory = os.fsencode(baseDir + '/tags/')
  837. for f in os.scandir(directory):
  838. f = f.name
  839. filename = os.fsdecode(f)
  840. if not filename.endswith(".txt"):
  841. continue
  842. try:
  843. tagFilename = os.path.join(directory, filename)
  844. except BaseException:
  845. continue
  846. if not os.path.isfile(tagFilename):
  847. continue
  848. if matchStr not in open(tagFilename).read():
  849. continue
  850. with open(tagFilename, "r") as f:
  851. lines = f.readlines()
  852. tagFile = open(tagFilename, "w+")
  853. if tagFile:
  854. for tagline in lines:
  855. if matchStr not in tagline:
  856. tagFile.write(tagline)
  857. tagFile.close()
  858. def removeAccount(baseDir: str, nickname: str,
  859. domain: str, port: int) -> bool:
  860. """Removes an account
  861. """
  862. # Don't remove the admin
  863. adminNickname = getConfigParam(baseDir, 'admin')
  864. if not adminNickname:
  865. return False
  866. if nickname == adminNickname:
  867. return False
  868. # Don't remove moderators
  869. moderatorsFile = baseDir + '/accounts/moderators.txt'
  870. if os.path.isfile(moderatorsFile):
  871. with open(moderatorsFile, "r") as f:
  872. lines = f.readlines()
  873. for moderator in lines:
  874. if moderator.strip('\n') == nickname:
  875. return False
  876. reenableAccount(baseDir, nickname)
  877. handle = nickname + '@' + domain
  878. removePassword(baseDir, nickname)
  879. _removeTagsForNickname(baseDir, nickname, domain, port)
  880. if os.path.isdir(baseDir + '/deactivated/' + handle):
  881. shutil.rmtree(baseDir + '/deactivated/' + handle)
  882. if os.path.isdir(baseDir + '/accounts/' + handle):
  883. shutil.rmtree(baseDir + '/accounts/' + handle)
  884. if os.path.isfile(baseDir + '/accounts/' + handle + '.json'):
  885. os.remove(baseDir + '/accounts/' + handle + '.json')
  886. if os.path.isfile(baseDir + '/wfendpoints/' + handle + '.json'):
  887. os.remove(baseDir + '/wfendpoints/' + handle + '.json')
  888. if os.path.isfile(baseDir + '/keys/private/' + handle + '.key'):
  889. os.remove(baseDir + '/keys/private/' + handle + '.key')
  890. if os.path.isfile(baseDir + '/keys/public/' + handle + '.pem'):
  891. os.remove(baseDir + '/keys/public/' + handle + '.pem')
  892. if os.path.isdir(baseDir + '/sharefiles/' + nickname):
  893. shutil.rmtree(baseDir + '/sharefiles/' + nickname)
  894. if os.path.isfile(baseDir + '/wfdeactivated/' + handle + '.json'):
  895. os.remove(baseDir + '/wfdeactivated/' + handle + '.json')
  896. if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname):
  897. shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname)
  898. refreshNewswire(baseDir)
  899. return True
  900. def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool:
  901. """Makes an account temporarily unavailable
  902. """
  903. handle = nickname + '@' + domain
  904. accountDir = baseDir + '/accounts/' + handle
  905. if not os.path.isdir(accountDir):
  906. return False
  907. deactivatedDir = baseDir + '/deactivated'
  908. if not os.path.isdir(deactivatedDir):
  909. os.mkdir(deactivatedDir)
  910. shutil.move(accountDir, deactivatedDir + '/' + handle)
  911. if os.path.isfile(baseDir + '/wfendpoints/' + handle + '.json'):
  912. deactivatedWebfingerDir = baseDir + '/wfdeactivated'
  913. if not os.path.isdir(deactivatedWebfingerDir):
  914. os.mkdir(deactivatedWebfingerDir)
  915. shutil.move(baseDir + '/wfendpoints/' + handle + '.json',
  916. deactivatedWebfingerDir + '/' + handle + '.json')
  917. if os.path.isdir(baseDir + '/sharefiles/' + nickname):
  918. deactivatedSharefilesDir = baseDir + '/sharefilesdeactivated'
  919. if not os.path.isdir(deactivatedSharefilesDir):
  920. os.mkdir(deactivatedSharefilesDir)
  921. shutil.move(baseDir + '/sharefiles/' + nickname,
  922. deactivatedSharefilesDir + '/' + nickname)
  923. refreshNewswire(baseDir)
  924. return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
  925. def activateAccount(baseDir: str, nickname: str, domain: str) -> None:
  926. """Makes a deactivated account available
  927. """
  928. handle = nickname + '@' + domain
  929. deactivatedDir = baseDir + '/deactivated'
  930. deactivatedAccountDir = deactivatedDir + '/' + handle
  931. if os.path.isdir(deactivatedAccountDir):
  932. accountDir = baseDir + '/accounts/' + handle
  933. if not os.path.isdir(accountDir):
  934. shutil.move(deactivatedAccountDir, accountDir)
  935. deactivatedWebfingerDir = baseDir + '/wfdeactivated'
  936. if os.path.isfile(deactivatedWebfingerDir + '/' + handle + '.json'):
  937. shutil.move(deactivatedWebfingerDir + '/' + handle + '.json',
  938. baseDir + '/wfendpoints/' + handle + '.json')
  939. deactivatedSharefilesDir = baseDir + '/sharefilesdeactivated'
  940. if os.path.isdir(deactivatedSharefilesDir + '/' + nickname):
  941. if not os.path.isdir(baseDir + '/sharefiles/' + nickname):
  942. shutil.move(deactivatedSharefilesDir + '/' + nickname,
  943. baseDir + '/sharefiles/' + nickname)
  944. refreshNewswire(baseDir)
  945. def isPersonSnoozed(baseDir: str, nickname: str, domain: str,
  946. snoozeActor: str) -> bool:
  947. """Returns true if the given actor is snoozed
  948. """
  949. snoozedFilename = baseDir + '/accounts/' + \
  950. nickname + '@' + domain + '/snoozed.txt'
  951. if not os.path.isfile(snoozedFilename):
  952. return False
  953. if snoozeActor + ' ' not in open(snoozedFilename).read():
  954. return False
  955. # remove the snooze entry if it has timed out
  956. replaceStr = None
  957. with open(snoozedFilename, 'r') as snoozedFile:
  958. for line in snoozedFile:
  959. # is this the entry for the actor?
  960. if line.startswith(snoozeActor + ' '):
  961. snoozedTimeStr = \
  962. line.split(' ')[1].replace('\n', '').replace('\r', '')
  963. # is there a time appended?
  964. if snoozedTimeStr.isdigit():
  965. snoozedTime = int(snoozedTimeStr)
  966. currTime = int(time.time())
  967. # has the snooze timed out?
  968. if int(currTime - snoozedTime) > 60 * 60 * 24:
  969. replaceStr = line
  970. else:
  971. replaceStr = line
  972. break
  973. if replaceStr:
  974. content = None
  975. with open(snoozedFilename, 'r') as snoozedFile:
  976. content = snoozedFile.read().replace(replaceStr, '')
  977. if content:
  978. writeSnoozedFile = open(snoozedFilename, 'w+')
  979. if writeSnoozedFile:
  980. writeSnoozedFile.write(content)
  981. writeSnoozedFile.close()
  982. if snoozeActor + ' ' in open(snoozedFilename).read():
  983. return True
  984. return False
  985. def personSnooze(baseDir: str, nickname: str, domain: str,
  986. snoozeActor: str) -> None:
  987. """Temporarily ignores the given actor
  988. """
  989. accountDir = baseDir + '/accounts/' + nickname + '@' + domain
  990. if not os.path.isdir(accountDir):
  991. print('ERROR: unknown account ' + accountDir)
  992. return
  993. snoozedFilename = accountDir + '/snoozed.txt'
  994. if os.path.isfile(snoozedFilename):
  995. if snoozeActor + ' ' in open(snoozedFilename).read():
  996. return
  997. snoozedFile = open(snoozedFilename, "a+")
  998. if snoozedFile:
  999. snoozedFile.write(snoozeActor + ' ' +
  1000. str(int(time.time())) + '\n')
  1001. snoozedFile.close()
  1002. def personUnsnooze(baseDir: str, nickname: str, domain: str,
  1003. snoozeActor: str) -> None:
  1004. """Undoes a temporarily ignore of the given actor
  1005. """
  1006. accountDir = baseDir + '/accounts/' + nickname + '@' + domain
  1007. if not os.path.isdir(accountDir):
  1008. print('ERROR: unknown account ' + accountDir)
  1009. return
  1010. snoozedFilename = accountDir + '/snoozed.txt'
  1011. if not os.path.isfile(snoozedFilename):
  1012. return
  1013. if snoozeActor + ' ' not in open(snoozedFilename).read():
  1014. return
  1015. replaceStr = None
  1016. with open(snoozedFilename, 'r') as snoozedFile:
  1017. for line in snoozedFile:
  1018. if line.startswith(snoozeActor + ' '):
  1019. replaceStr = line
  1020. break
  1021. if replaceStr:
  1022. content = None
  1023. with open(snoozedFilename, 'r') as snoozedFile:
  1024. content = snoozedFile.read().replace(replaceStr, '')
  1025. if content:
  1026. writeSnoozedFile = open(snoozedFilename, 'w+')
  1027. if writeSnoozedFile:
  1028. writeSnoozedFile.write(content)
  1029. writeSnoozedFile.close()
  1030. def setPersonNotes(baseDir: str, nickname: str, domain: str,
  1031. handle: str, notes: str) -> bool:
  1032. """Adds notes about a person
  1033. """
  1034. if '@' not in handle:
  1035. return False
  1036. if handle.startswith('@'):
  1037. handle = handle[1:]
  1038. notesDir = baseDir + '/accounts/' + \
  1039. nickname + '@' + domain + '/notes'
  1040. if not os.path.isdir(notesDir):
  1041. os.mkdir(notesDir)
  1042. notesFilename = notesDir + '/' + handle + '.txt'
  1043. with open(notesFilename, 'w+') as notesFile:
  1044. notesFile.write(notes)
  1045. return True
  1046. def getActorJson(handle: str, http: bool, gnunet: bool,
  1047. debug: bool, quiet=False) -> {}:
  1048. """Returns the actor json
  1049. """
  1050. if debug:
  1051. print('getActorJson for ' + handle)
  1052. originalActor = handle
  1053. if '/@' in handle or \
  1054. '/users/' in handle or \
  1055. handle.startswith('http') or \
  1056. handle.startswith('dat'):
  1057. # format: https://domain/@nick
  1058. prefixes = getProtocolPrefixes()
  1059. for prefix in prefixes:
  1060. handle = handle.replace(prefix, '')
  1061. handle = handle.replace('/@', '/users/')
  1062. if not hasUsersPath(handle):
  1063. if not quiet or debug:
  1064. print('getActorJson: Expected actor format: ' +
  1065. 'https://domain/@nick or https://domain/users/nick')
  1066. return None
  1067. if '/users/' in handle:
  1068. nickname = handle.split('/users/')[1]
  1069. nickname = nickname.replace('\n', '').replace('\r', '')
  1070. domain = handle.split('/users/')[0]
  1071. elif '/profile/' in handle:
  1072. nickname = handle.split('/profile/')[1]
  1073. nickname = nickname.replace('\n', '').replace('\r', '')
  1074. domain = handle.split('/profile/')[0]
  1075. elif '/channel/' in handle:
  1076. nickname = handle.split('/channel/')[1]
  1077. nickname = nickname.replace('\n', '').replace('\r', '')
  1078. domain = handle.split('/channel/')[0]
  1079. elif '/accounts/' in handle:
  1080. nickname = handle.split('/accounts/')[1]
  1081. nickname = nickname.replace('\n', '').replace('\r', '')
  1082. domain = handle.split('/accounts/')[0]
  1083. elif '/u/' in handle:
  1084. nickname = handle.split('/u/')[1]
  1085. nickname = nickname.replace('\n', '').replace('\r', '')
  1086. domain = handle.split('/u/')[0]
  1087. else:
  1088. # format: @nick@domain
  1089. if '@' not in handle:
  1090. if not quiet:
  1091. print('getActorJson Syntax: --actor nickname@domain')
  1092. return None
  1093. if handle.startswith('@'):
  1094. handle = handle[1:]
  1095. if '@' not in handle:
  1096. if not quiet:
  1097. print('getActorJsonSyntax: --actor nickname@domain')
  1098. return None
  1099. nickname = handle.split('@')[0]
  1100. domain = handle.split('@')[1]
  1101. domain = domain.replace('\n', '').replace('\r', '')
  1102. cachedWebfingers = {}
  1103. proxyType = None
  1104. if http or domain.endswith('.onion'):
  1105. httpPrefix = 'http'
  1106. proxyType = 'tor'
  1107. elif domain.endswith('.i2p'):
  1108. httpPrefix = 'http'
  1109. proxyType = 'i2p'
  1110. elif gnunet:
  1111. httpPrefix = 'gnunet'
  1112. proxyType = 'gnunet'
  1113. else:
  1114. if '127.0.' not in domain and '192.168.' not in domain:
  1115. httpPrefix = 'https'
  1116. else:
  1117. httpPrefix = 'http'
  1118. session = createSession(proxyType)
  1119. if nickname == 'inbox':
  1120. nickname = domain
  1121. handle = nickname + '@' + domain
  1122. wfRequest = webfingerHandle(session, handle,
  1123. httpPrefix, cachedWebfingers,
  1124. None, __version__, debug)
  1125. if not wfRequest:
  1126. if not quiet:
  1127. print('getActorJson Unable to webfinger ' + handle)
  1128. return None
  1129. if not isinstance(wfRequest, dict):
  1130. if not quiet:
  1131. print('getActorJson Webfinger for ' + handle +
  1132. ' did not return a dict. ' + str(wfRequest))
  1133. return None
  1134. if not quiet:
  1135. pprint(wfRequest)
  1136. personUrl = None
  1137. if wfRequest.get('errors'):
  1138. if not quiet or debug:
  1139. print('getActorJson wfRequest error: ' + str(wfRequest['errors']))
  1140. if hasUsersPath(handle):
  1141. personUrl = originalActor
  1142. else:
  1143. if debug:
  1144. print('No users path in ' + handle)
  1145. return None
  1146. profileStr = 'https://www.w3.org/ns/activitystreams'
  1147. asHeader = {
  1148. 'Accept': 'application/activity+json; profile="' + profileStr + '"'
  1149. }
  1150. if not personUrl:
  1151. personUrl = getUserUrl(wfRequest, 0, debug)
  1152. if nickname == domain:
  1153. personUrl = personUrl.replace('/users/', '/actor/')
  1154. personUrl = personUrl.replace('/accounts/', '/actor/')
  1155. personUrl = personUrl.replace('/channel/', '/actor/')
  1156. personUrl = personUrl.replace('/profile/', '/actor/')
  1157. personUrl = personUrl.replace('/u/', '/actor/')
  1158. if not personUrl:
  1159. # try single user instance
  1160. personUrl = httpPrefix + '://' + domain
  1161. profileStr = 'https://www.w3.org/ns/activitystreams'
  1162. asHeader = {
  1163. 'Accept': 'application/ld+json; profile="' + profileStr + '"'
  1164. }
  1165. if '/channel/' in personUrl or '/accounts/' in personUrl:
  1166. profileStr = 'https://www.w3.org/ns/activitystreams'
  1167. asHeader = {
  1168. 'Accept': 'application/ld+json; profile="' + profileStr + '"'
  1169. }
  1170. personJson = \
  1171. getJson(session, personUrl, asHeader, None,
  1172. debug, __version__, httpPrefix, None, 20, quiet)
  1173. if personJson:
  1174. if not quiet:
  1175. pprint(personJson)
  1176. return personJson
  1177. else:
  1178. profileStr = 'https://www.w3.org/ns/activitystreams'
  1179. asHeader = {
  1180. 'Accept': 'application/jrd+json; profile="' + profileStr + '"'
  1181. }
  1182. personJson = \
  1183. getJson(session, personUrl, asHeader, None,
  1184. debug, __version__, httpPrefix, None)
  1185. if not quiet:
  1186. if personJson:
  1187. print('getActorJson returned actor')
  1188. pprint(personJson)
  1189. else:
  1190. print('Failed to get ' + personUrl)
  1191. return personJson