person.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  1. __filename__ = "person.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import time
  9. import os
  10. import subprocess
  11. import shutil
  12. import pyqrcode
  13. from random import randint
  14. from pathlib import Path
  15. try:
  16. from Cryptodome.PublicKey import RSA
  17. except ImportError:
  18. from Crypto.PublicKey import RSA
  19. from shutil import copyfile
  20. from webfinger import createWebfingerEndpoint
  21. from webfinger import storeWebfingerEndpoint
  22. from posts import createDMTimeline
  23. from posts import createRepliesTimeline
  24. from posts import createMediaTimeline
  25. from posts import createBlogsTimeline
  26. from posts import createBookmarksTimeline
  27. from posts import createInbox
  28. from posts import createOutbox
  29. from posts import createModeration
  30. from auth import storeBasicCredentials
  31. from auth import removePassword
  32. from roles import setRole
  33. from media import removeMetaData
  34. from utils import validNickname
  35. from utils import noOfAccounts
  36. from utils import loadJson
  37. from utils import saveJson
  38. from config import setConfigParam
  39. from config import getConfigParam
  40. def generateRSAKey() -> (str, str):
  41. key = RSA.generate(2048)
  42. privateKeyPem = key.exportKey("PEM").decode("utf-8")
  43. publicKeyPem = key.publickey().exportKey("PEM").decode("utf-8")
  44. return privateKeyPem, publicKeyPem
  45. def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
  46. port: int, imageFilename: str, imageType: str,
  47. resolution: str) -> bool:
  48. """Saves the given image file as an avatar or background
  49. image for the given person
  50. """
  51. imageFilename = imageFilename.replace('\n', '').replace('\r', '')
  52. if not (imageFilename.endswith('.png') or
  53. imageFilename.endswith('.jpg') or
  54. imageFilename.endswith('.jpeg') or
  55. imageFilename.endswith('.gif')):
  56. print('Profile image must be png, jpg or gif format')
  57. return False
  58. if imageFilename.startswith('~/'):
  59. imageFilename = imageFilename.replace('~/', str(Path.home()) + '/')
  60. if ':' in domain:
  61. domain = domain.split(':')[0]
  62. fullDomain = domain
  63. if port:
  64. if port != 80 and port != 443:
  65. if ':' not in domain:
  66. fullDomain = domain + ':' + str(port)
  67. handle = nickname.lower() + '@' + domain.lower()
  68. personFilename = baseDir + '/accounts/' + handle + '.json'
  69. if not os.path.isfile(personFilename):
  70. print('person definition not found: ' + personFilename)
  71. return False
  72. if not os.path.isdir(baseDir + '/accounts/' + handle):
  73. print('Account not found: ' + baseDir + '/accounts/' + handle)
  74. return False
  75. iconFilenameBase = 'icon'
  76. if imageType == 'avatar' or imageType == 'icon':
  77. iconFilenameBase = 'icon'
  78. else:
  79. iconFilenameBase = 'image'
  80. mediaType = 'image/png'
  81. iconFilename = iconFilenameBase + '.png'
  82. if imageFilename.endswith('.jpg') or \
  83. imageFilename.endswith('.jpeg'):
  84. mediaType = 'image/jpeg'
  85. iconFilename = iconFilenameBase + '.jpg'
  86. if imageFilename.endswith('.gif'):
  87. mediaType = 'image/gif'
  88. iconFilename = iconFilenameBase + '.gif'
  89. profileFilename = baseDir + '/accounts/' + handle + '/' + iconFilename
  90. personJson = loadJson(personFilename)
  91. if personJson:
  92. personJson[iconFilenameBase]['mediaType'] = mediaType
  93. personJson[iconFilenameBase]['url'] = \
  94. httpPrefix + '://' + fullDomain + '/users/' + \
  95. nickname + '/'+iconFilename
  96. saveJson(personJson, personFilename)
  97. cmd = \
  98. '/usr/bin/convert ' + imageFilename + ' -size ' + \
  99. resolution + ' -quality 50 ' + profileFilename
  100. subprocess.call(cmd, shell=True)
  101. removeMetaData(profileFilename, profileFilename)
  102. return True
  103. return False
  104. def setOrganizationScheme(baseDir: str, nickname: str, domain: str,
  105. schema: str) -> bool:
  106. """Set the organization schema within which a person exists
  107. This will define how roles, skills and availability are assembled
  108. into organizations
  109. """
  110. # avoid giant strings
  111. if len(schema) > 256:
  112. return False
  113. actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
  114. if not os.path.isfile(actorFilename):
  115. return False
  116. actorJson = loadJson(actorFilename)
  117. if actorJson:
  118. actorJson['orgSchema'] = schema
  119. saveJson(actorJson, actorFilename)
  120. return True
  121. def accountExists(baseDir: str, nickname: str, domain: str) -> bool:
  122. """Returns true if the given account exists
  123. """
  124. if ':' in domain:
  125. domain = domain.split(':')[0]
  126. return os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain) or \
  127. os.path.isdir(baseDir + '/deactivated/' + nickname + '@' + domain)
  128. def randomizeActorImages(personJson: {}) -> None:
  129. """Randomizes the filenames for avatar image and background
  130. This causes other instances to update their cached avatar image
  131. """
  132. personId = personJson['id']
  133. lastPartOfFilename = personJson['icon']['url'].split('/')[-1]
  134. existingExtension = lastPartOfFilename.split('.')[1]
  135. # NOTE: these files don't need to have cryptographically
  136. # secure names
  137. randStr = str(randint(10000000000000, 99999999999999)) # nosec
  138. personJson['icon']['url'] = \
  139. personId + '/avatar' + randStr + '.' + existingExtension
  140. lastPartOfFilename = personJson['image']['url'].split('/')[-1]
  141. existingExtension = lastPartOfFilename.split('.')[1]
  142. randStr = str(randint(10000000000000, 99999999999999)) # nosec
  143. personJson['image']['url'] = \
  144. personId + '/image' + randStr + '.' + existingExtension
  145. def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
  146. httpPrefix: str, saveToFile: bool,
  147. password=None) -> (str, str, {}, {}):
  148. """Returns the private key, public key, actor and webfinger endpoint
  149. """
  150. privateKeyPem, publicKeyPem = generateRSAKey()
  151. webfingerEndpoint = \
  152. createWebfingerEndpoint(nickname, domain, port,
  153. httpPrefix, publicKeyPem)
  154. if saveToFile:
  155. storeWebfingerEndpoint(nickname, domain, port,
  156. baseDir, webfingerEndpoint)
  157. handle = nickname.lower() + '@' + domain.lower()
  158. originalDomain = domain
  159. if port:
  160. if port != 80 and port != 443:
  161. if ':' not in domain:
  162. domain = domain + ':' + str(port)
  163. personType = 'Person'
  164. approveFollowers = False
  165. personName = nickname
  166. personId = httpPrefix + '://' + domain + '/users/' + nickname
  167. inboxStr = personId + '/inbox'
  168. personUrl = httpPrefix + '://' + domain + '/@' + personName
  169. if nickname == 'inbox':
  170. # shared inbox
  171. inboxStr = httpPrefix + '://' + domain + '/actor/inbox'
  172. personId = httpPrefix + '://' + domain + '/actor'
  173. personUrl = httpPrefix + '://' + domain + \
  174. '/about/more?instance_actor=true'
  175. personName = originalDomain
  176. approveFollowers = True
  177. personType = 'Application'
  178. # NOTE: these image files don't need to have
  179. # cryptographically secure names
  180. imageUrl = \
  181. personId + '/image' + \
  182. str(randint(10000000000000, 99999999999999)) + '.png' # nosec
  183. iconUrl = \
  184. personId + '/avatar' + \
  185. str(randint(10000000000000, 99999999999999)) + '.png' # nosec
  186. contextDict = {
  187. 'Emoji': 'toot:Emoji',
  188. 'Hashtag': 'as:Hashtag',
  189. 'IdentityProof': 'toot:IdentityProof',
  190. 'PropertyValue': 'schema:PropertyValue',
  191. 'alsoKnownAs': {
  192. '@id': 'as:alsoKnownAs', '@type': '@id'
  193. },
  194. 'focalPoint': {
  195. '@container': '@list', '@id': 'toot:focalPoint'
  196. },
  197. 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
  198. 'movedTo': {
  199. '@id': 'as:movedTo', '@type': '@id'
  200. },
  201. 'schema': 'http://schema.org#',
  202. 'value': 'schema:value'
  203. }
  204. newPerson = {
  205. '@context': [
  206. 'https://www.w3.org/ns/activitystreams',
  207. 'https://w3id.org/security/v1',
  208. contextDict
  209. ],
  210. 'attachment': [],
  211. 'alsoKnownAs': [],
  212. 'discoverable': False,
  213. 'endpoints': {
  214. 'id': personId+'/endpoints',
  215. 'sharedInbox': httpPrefix+'://'+domain+'/inbox',
  216. },
  217. 'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
  218. 'followers': personId+'/followers',
  219. 'following': personId+'/following',
  220. 'shares': personId+'/shares',
  221. 'orgSchema': None,
  222. 'skills': {},
  223. 'roles': {},
  224. 'availability': None,
  225. 'icon': {
  226. 'mediaType': 'image/png',
  227. 'type': 'Image',
  228. 'url': iconUrl
  229. },
  230. 'id': personId,
  231. 'image': {
  232. 'mediaType': 'image/png',
  233. 'type': 'Image',
  234. 'url': imageUrl
  235. },
  236. 'inbox': inboxStr,
  237. 'manuallyApprovesFollowers': approveFollowers,
  238. 'name': personName,
  239. 'outbox': personId+'/outbox',
  240. 'preferredUsername': personName,
  241. 'summary': '',
  242. 'publicKey': {
  243. 'id': personId+'#main-key',
  244. 'owner': personId,
  245. 'publicKeyPem': publicKeyPem
  246. },
  247. 'tag': [],
  248. 'type': personType,
  249. 'url': personUrl,
  250. 'nomadicLocations': [{
  251. 'id': personId,
  252. 'type': 'nomadicLocation',
  253. 'locationAddress': 'acct:' + nickname + '@' + domain,
  254. 'locationPrimary': True,
  255. 'locationDeleted': False
  256. }]
  257. }
  258. if nickname == 'inbox':
  259. # fields not needed by the shared inbox
  260. del newPerson['outbox']
  261. del newPerson['icon']
  262. del newPerson['image']
  263. del newPerson['skills']
  264. del newPerson['shares']
  265. del newPerson['roles']
  266. del newPerson['tag']
  267. del newPerson['availability']
  268. del newPerson['followers']
  269. del newPerson['following']
  270. del newPerson['attachment']
  271. if saveToFile:
  272. # save person to file
  273. peopleSubdir = '/accounts'
  274. if not os.path.isdir(baseDir + peopleSubdir):
  275. os.mkdir(baseDir + peopleSubdir)
  276. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle):
  277. os.mkdir(baseDir + peopleSubdir + '/' + handle)
  278. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/inbox'):
  279. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/inbox')
  280. if not os.path.isdir(baseDir + peopleSubdir + '/' +
  281. handle + '/outbox'):
  282. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/outbox')
  283. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/ocap'):
  284. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/ocap')
  285. if not os.path.isdir(baseDir + peopleSubdir + '/' + handle + '/queue'):
  286. os.mkdir(baseDir + peopleSubdir + '/' + handle + '/queue')
  287. filename = baseDir + peopleSubdir + '/' + handle + '.json'
  288. saveJson(newPerson, filename)
  289. # save to cache
  290. if not os.path.isdir(baseDir + '/cache'):
  291. os.mkdir(baseDir + '/cache')
  292. if not os.path.isdir(baseDir + '/cache/actors'):
  293. os.mkdir(baseDir + '/cache/actors')
  294. cacheFilename = baseDir + '/cache/actors/' + \
  295. newPerson['id'].replace('/', '#') + '.json'
  296. saveJson(newPerson, cacheFilename)
  297. # save the private key
  298. privateKeysSubdir = '/keys/private'
  299. if not os.path.isdir(baseDir + '/keys'):
  300. os.mkdir(baseDir + '/keys')
  301. if not os.path.isdir(baseDir + privateKeysSubdir):
  302. os.mkdir(baseDir + privateKeysSubdir)
  303. filename = baseDir + privateKeysSubdir + '/' + handle + '.key'
  304. with open(filename, "w") as text_file:
  305. print(privateKeyPem, file=text_file)
  306. # save the public key
  307. publicKeysSubdir = '/keys/public'
  308. if not os.path.isdir(baseDir + publicKeysSubdir):
  309. os.mkdir(baseDir + publicKeysSubdir)
  310. filename = baseDir + publicKeysSubdir + '/' + handle + '.pem'
  311. with open(filename, "w") as text_file:
  312. print(publicKeyPem, file=text_file)
  313. if password:
  314. storeBasicCredentials(baseDir, nickname, password)
  315. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  316. def registerAccount(baseDir: str, httpPrefix: str, domain: str, port: int,
  317. nickname: str, password: str) -> bool:
  318. """Registers a new account from the web interface
  319. """
  320. if accountExists(baseDir, nickname, domain):
  321. return False
  322. if not validNickname(domain, nickname):
  323. print('REGISTER: Nickname ' + nickname + ' is invalid')
  324. return False
  325. if len(password) < 8:
  326. print('REGISTER: Password should be at least 8 characters')
  327. return False
  328. (privateKeyPem, publicKeyPem,
  329. newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
  330. domain, port,
  331. httpPrefix, True,
  332. password)
  333. if privateKeyPem:
  334. return True
  335. return False
  336. def createGroup(baseDir: str, nickname: str, domain: str, port: int,
  337. httpPrefix: str, saveToFile: bool,
  338. password=None) -> (str, str, {}, {}):
  339. """Returns a group
  340. """
  341. (privateKeyPem, publicKeyPem,
  342. newPerson, webfingerEndpoint) = createPerson(baseDir, nickname,
  343. domain, port,
  344. httpPrefix, saveToFile,
  345. password)
  346. newPerson['type'] = 'Group'
  347. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  348. def savePersonQrcode(baseDir: str,
  349. nickname: str, domain: str, port: int,
  350. scale=6) -> None:
  351. """Saves a qrcode image for the handle of the person
  352. This helps to transfer onion or i2p handles to a mobile device
  353. """
  354. qrcodeFilename = baseDir + '/accounts/' + \
  355. nickname + '@' + domain + '/qrcode.png'
  356. if os.path.isfile(qrcodeFilename):
  357. return
  358. handle = '@' + nickname + '@' + domain
  359. if port:
  360. if port != 80 and port != 443:
  361. handle = handle + ':' + str(port)
  362. url = pyqrcode.create(handle)
  363. url.png(qrcodeFilename, scale)
  364. def createPerson(baseDir: str, nickname: str, domain: str, port: int,
  365. httpPrefix: str, saveToFile: bool,
  366. password=None) -> (str, str, {}, {}):
  367. """Returns the private key, public key, actor and webfinger endpoint
  368. """
  369. if not validNickname(domain, nickname):
  370. return None, None, None, None
  371. # If a config.json file doesn't exist then don't decrement
  372. # remaining registrations counter
  373. remainingConfigExists = getConfigParam(baseDir, 'registrationsRemaining')
  374. if remainingConfigExists:
  375. registrationsRemaining = int(remainingConfigExists)
  376. if registrationsRemaining <= 0:
  377. return None, None, None, None
  378. (privateKeyPem, publicKeyPem,
  379. newPerson, webfingerEndpoint) = createPersonBase(baseDir, nickname,
  380. domain, port,
  381. httpPrefix,
  382. saveToFile, password)
  383. if noOfAccounts(baseDir) == 1:
  384. # print(nickname+' becomes the instance admin and a moderator')
  385. setRole(baseDir, nickname, domain, 'instance', 'admin')
  386. setRole(baseDir, nickname, domain, 'instance', 'moderator')
  387. setRole(baseDir, nickname, domain, 'instance', 'delegator')
  388. setConfigParam(baseDir, 'admin', nickname)
  389. if not os.path.isdir(baseDir + '/accounts'):
  390. os.mkdir(baseDir + '/accounts')
  391. if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain):
  392. os.mkdir(baseDir + '/accounts/' + nickname + '@' + domain)
  393. if os.path.isfile(baseDir + '/img/default-avatar.png'):
  394. copyfile(baseDir + '/img/default-avatar.png',
  395. baseDir + '/accounts/' + nickname + '@' + domain +
  396. '/avatar.png')
  397. theme = getConfigParam(baseDir, 'theme')
  398. defaultProfileImageFilename = baseDir + '/img/image.png'
  399. if theme:
  400. if os.path.isfile(baseDir + '/img/image_' + theme + '.png'):
  401. defaultBannerFilename = baseDir + '/img/image_' + theme + '.png'
  402. if os.path.isfile(defaultProfileImageFilename):
  403. copyfile(defaultProfileImageFilename, baseDir +
  404. '/accounts/' + nickname + '@' + domain + '/image.png')
  405. defaultBannerFilename = baseDir + '/img/banner.png'
  406. if theme:
  407. if os.path.isfile(baseDir + '/img/banner_' + theme + '.png'):
  408. defaultBannerFilename = baseDir + '/img/banner_' + theme + '.png'
  409. if os.path.isfile(defaultBannerFilename):
  410. copyfile(defaultBannerFilename, baseDir + '/accounts/' +
  411. nickname + '@' + domain + '/banner.png')
  412. if remainingConfigExists:
  413. registrationsRemaining -= 1
  414. setConfigParam(baseDir, 'registrationsRemaining',
  415. str(registrationsRemaining))
  416. savePersonQrcode(baseDir, nickname, domain, port)
  417. return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint
  418. def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int,
  419. httpPrefix: str) -> (str, str, {}, {}):
  420. """Generates the shared inbox
  421. """
  422. return createPersonBase(baseDir, nickname, domain, port, httpPrefix,
  423. True, None)
  424. def createCapabilitiesInbox(baseDir: str, nickname: str,
  425. domain: str, port: int,
  426. httpPrefix: str) -> (str, str, {}, {}):
  427. """Generates the capabilities inbox to sign requests
  428. """
  429. return createPersonBase(baseDir, nickname, domain, port,
  430. httpPrefix, True, None)
  431. def personUpgradeActor(baseDir: str, personJson: {},
  432. handle: str, filename: str) -> None:
  433. """Alter the actor to add any new properties
  434. """
  435. updateActor = False
  436. if not os.path.isfile(filename):
  437. print('WARN: actor file not found ' + filename)
  438. return
  439. if not personJson:
  440. personJson = loadJson(filename)
  441. if not personJson.get('nomadicLocations'):
  442. personJson['nomadicLocations'] = [{
  443. 'id': personJson['id'],
  444. 'type': 'nomadicLocation',
  445. 'locationAddress':'acct:'+handle,
  446. 'locationPrimary':True,
  447. 'locationDeleted':False
  448. }]
  449. print('Nomadic locations added to to actor ' + handle)
  450. updateActor = True
  451. if updateActor:
  452. saveJson(personJson, filename)
  453. # also update the actor within the cache
  454. actorCacheFilename = \
  455. baseDir + '/accounts/cache/actors/' + \
  456. personJson['id'].replace('/', '#') + '.json'
  457. if os.path.isfile(actorCacheFilename):
  458. saveJson(personJson, actorCacheFilename)
  459. # update domain/@nickname in actors cache
  460. actorCacheFilename = \
  461. baseDir + '/accounts/cache/actors/' + \
  462. personJson['id'].replace('/users/', '/@').replace('/', '#') + \
  463. '.json'
  464. if os.path.isfile(actorCacheFilename):
  465. saveJson(personJson, actorCacheFilename)
  466. def personLookup(domain: str, path: str, baseDir: str) -> {}:
  467. """Lookup the person for an given nickname
  468. """
  469. if path.endswith('#main-key'):
  470. path = path.replace('#main-key', '')
  471. # is this a shared inbox lookup?
  472. isSharedInbox = False
  473. if path == '/inbox' or path == '/users/inbox' or path == '/sharedInbox':
  474. # shared inbox actor on @domain@domain
  475. path = '/users/' + domain
  476. isSharedInbox = True
  477. else:
  478. notPersonLookup = ('/inbox', '/outbox', '/outboxarchive',
  479. '/followers', '/following', '/featured',
  480. '.png', '.jpg', '.gif', '.mpv')
  481. for ending in notPersonLookup:
  482. if path.endswith(ending):
  483. return None
  484. nickname = None
  485. if path.startswith('/users/'):
  486. nickname = path.replace('/users/', '', 1)
  487. if path.startswith('/@'):
  488. nickname = path.replace('/@', '', 1)
  489. if not nickname:
  490. return None
  491. if not isSharedInbox and not validNickname(domain, nickname):
  492. return None
  493. if ':' in domain:
  494. domain = domain.split(':')[0]
  495. handle = nickname + '@' + domain
  496. filename = baseDir + '/accounts/' + handle + '.json'
  497. if not os.path.isfile(filename):
  498. return None
  499. personJson = loadJson(filename)
  500. personUpgradeActor(baseDir, personJson, handle, filename)
  501. # if not personJson:
  502. # personJson={"user": "unknown"}
  503. return personJson
  504. def personBoxJson(recentPostsCache: {},
  505. session, baseDir: str, domain: str, port: int, path: str,
  506. httpPrefix: str, noOfItems: int, boxname: str,
  507. authorized: bool, ocapAlways: bool) -> {}:
  508. """Obtain the inbox/outbox/moderation feed for the given person
  509. """
  510. if boxname != 'inbox' and boxname != 'dm' and \
  511. boxname != 'tlreplies' and boxname != 'tlmedia' and \
  512. boxname != 'tlblogs' and \
  513. boxname != 'outbox' and boxname != 'moderation' and \
  514. boxname != 'tlbookmarks' and boxname != 'bookmarks':
  515. return None
  516. if not '/' + boxname in path:
  517. return None
  518. # Only show the header by default
  519. headerOnly = True
  520. # handle page numbers
  521. pageNumber = None
  522. if '?page=' in path:
  523. pageNumber = path.split('?page=')[1]
  524. if pageNumber == 'true':
  525. pageNumber = 1
  526. else:
  527. try:
  528. pageNumber = int(pageNumber)
  529. except BaseException:
  530. pass
  531. path = path.split('?page=')[0]
  532. headerOnly = False
  533. if not path.endswith('/' + boxname):
  534. return None
  535. nickname = None
  536. if path.startswith('/users/'):
  537. nickname = path.replace('/users/', '', 1).replace('/' + boxname, '')
  538. if path.startswith('/@'):
  539. nickname = path.replace('/@', '', 1).replace('/' + boxname, '')
  540. if not nickname:
  541. return None
  542. if not validNickname(domain, nickname):
  543. return None
  544. if boxname == 'inbox':
  545. return createInbox(recentPostsCache,
  546. session, baseDir, nickname, domain, port,
  547. httpPrefix,
  548. noOfItems, headerOnly, ocapAlways, pageNumber)
  549. elif boxname == 'dm':
  550. return createDMTimeline(session, baseDir, nickname, domain, port,
  551. httpPrefix,
  552. noOfItems, headerOnly, ocapAlways, pageNumber)
  553. elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
  554. return createBookmarksTimeline(session, baseDir, nickname, domain,
  555. port, httpPrefix,
  556. noOfItems, headerOnly, ocapAlways,
  557. pageNumber)
  558. elif boxname == 'tlreplies':
  559. return createRepliesTimeline(session, baseDir, nickname, domain,
  560. port, httpPrefix,
  561. noOfItems, headerOnly, ocapAlways,
  562. pageNumber)
  563. elif boxname == 'tlmedia':
  564. return createMediaTimeline(session, baseDir, nickname, domain, port,
  565. httpPrefix,
  566. noOfItems, headerOnly, ocapAlways,
  567. pageNumber)
  568. elif boxname == 'tlblogs':
  569. return createBlogsTimeline(session, baseDir, nickname, domain, port,
  570. httpPrefix,
  571. noOfItems, headerOnly, ocapAlways,
  572. pageNumber)
  573. elif boxname == 'outbox':
  574. return createOutbox(session, baseDir, nickname, domain, port,
  575. httpPrefix,
  576. noOfItems, headerOnly, authorized,
  577. pageNumber)
  578. elif boxname == 'moderation':
  579. return createModeration(baseDir, nickname, domain, port,
  580. httpPrefix,
  581. noOfItems, headerOnly, authorized,
  582. pageNumber)
  583. return None
  584. def personInboxJson(recentPostsCache: {},
  585. baseDir: str, domain: str, port: int, path: str,
  586. httpPrefix: str, noOfItems: int, ocapAlways: bool) -> []:
  587. """Obtain the inbox feed for the given person
  588. Authentication is expected to have already happened
  589. """
  590. if '/inbox' not in path:
  591. return None
  592. # Only show the header by default
  593. headerOnly = True
  594. # handle page numbers
  595. pageNumber = None
  596. if '?page=' in path:
  597. pageNumber = path.split('?page=')[1]
  598. if pageNumber == 'true':
  599. pageNumber = 1
  600. else:
  601. try:
  602. pageNumber = int(pageNumber)
  603. except BaseException:
  604. pass
  605. path = path.split('?page=')[0]
  606. headerOnly = False
  607. if not path.endswith('/inbox'):
  608. return None
  609. nickname = None
  610. if path.startswith('/users/'):
  611. nickname = path.replace('/users/', '', 1).replace('/inbox', '')
  612. if path.startswith('/@'):
  613. nickname = path.replace('/@', '', 1).replace('/inbox', '')
  614. if not nickname:
  615. return None
  616. if not validNickname(domain, nickname):
  617. return None
  618. return createInbox(recentPostsCache, baseDir, nickname,
  619. domain, port, httpPrefix,
  620. noOfItems, headerOnly, ocapAlways, pageNumber)
  621. def setDisplayNickname(baseDir: str, nickname: str, domain: str,
  622. displayName: str) -> bool:
  623. if len(displayName) > 32:
  624. return False
  625. handle = nickname.lower() + '@' + domain.lower()
  626. filename = baseDir + '/accounts/' + handle.lower() + '.json'
  627. if not os.path.isfile(filename):
  628. return False
  629. personJson = loadJson(filename)
  630. if not personJson:
  631. return False
  632. personJson['name'] = displayName
  633. saveJson(personJson, filename)
  634. return True
  635. def setBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool:
  636. if len(bio) > 32:
  637. return False
  638. handle = nickname.lower() + '@' + domain.lower()
  639. filename = baseDir + '/accounts/' + handle.lower() + '.json'
  640. if not os.path.isfile(filename):
  641. return False
  642. personJson = loadJson(filename)
  643. if not personJson:
  644. return False
  645. if not personJson.get('summary'):
  646. return False
  647. personJson['summary'] = bio
  648. saveJson(personJson, filename)
  649. return True
  650. def isSuspended(baseDir: str, nickname: str) -> bool:
  651. """Returns true if the given nickname is suspended
  652. """
  653. adminNickname = getConfigParam(baseDir, 'admin')
  654. if nickname == adminNickname:
  655. return False
  656. suspendedFilename = baseDir + '/accounts/suspended.txt'
  657. if os.path.isfile(suspendedFilename):
  658. with open(suspendedFilename, "r") as f:
  659. lines = f.readlines()
  660. for suspended in lines:
  661. if suspended.strip('\n').strip('\r') == nickname:
  662. return True
  663. return False
  664. def unsuspendAccount(baseDir: str, nickname: str) -> None:
  665. """Removes an account suspention
  666. """
  667. suspendedFilename = baseDir + '/accounts/suspended.txt'
  668. if os.path.isfile(suspendedFilename):
  669. with open(suspendedFilename, "r") as f:
  670. lines = f.readlines()
  671. suspendedFile = open(suspendedFilename, "w+")
  672. for suspended in lines:
  673. if suspended.strip('\n').strip('\r') != nickname:
  674. suspendedFile.write(suspended)
  675. suspendedFile.close()
  676. def suspendAccount(baseDir: str, nickname: str, domain: str) -> None:
  677. """Suspends the given account
  678. """
  679. # Don't suspend the admin
  680. adminNickname = getConfigParam(baseDir, 'admin')
  681. if nickname == adminNickname:
  682. return
  683. # Don't suspend moderators
  684. moderatorsFile = baseDir + '/accounts/moderators.txt'
  685. if os.path.isfile(moderatorsFile):
  686. with open(moderatorsFile, "r") as f:
  687. lines = f.readlines()
  688. for moderator in lines:
  689. if moderator.strip('\n').strip('\r') == nickname:
  690. return
  691. saltFilename = baseDir + '/accounts/' + \
  692. nickname + '@' + domain + '/.salt'
  693. if os.path.isfile(saltFilename):
  694. os.remove(saltFilename)
  695. tokenFilename = baseDir + '/accounts/' + \
  696. nickname + '@' + domain + '/.token'
  697. if os.path.isfile(tokenFilename):
  698. os.remove(tokenFilename)
  699. suspendedFilename = baseDir + '/accounts/suspended.txt'
  700. if os.path.isfile(suspendedFilename):
  701. with open(suspendedFilename, "r") as f:
  702. lines = f.readlines()
  703. for suspended in lines:
  704. if suspended.strip('\n').strip('\r') == nickname:
  705. return
  706. suspendedFile = open(suspendedFilename, 'a+')
  707. if suspendedFile:
  708. suspendedFile.write(nickname + '\n')
  709. suspendedFile.close()
  710. else:
  711. suspendedFile = open(suspendedFilename, 'w+')
  712. if suspendedFile:
  713. suspendedFile.write(nickname + '\n')
  714. suspendedFile.close()
  715. def canRemovePost(baseDir: str, nickname: str,
  716. domain: str, port: int, postId: str) -> bool:
  717. """Returns true if the given post can be removed
  718. """
  719. if '/statuses/' not in postId:
  720. return False
  721. domainFull = domain
  722. if port:
  723. if port != 80 and port != 443:
  724. if ':' not in domain:
  725. domainFull = domain + ':' + str(port)
  726. # is the post by the admin?
  727. adminNickname = getConfigParam(baseDir, 'admin')
  728. if domainFull + '/users/' + adminNickname + '/' in postId:
  729. return False
  730. # is the post by a moderator?
  731. moderatorsFile = baseDir + '/accounts/moderators.txt'
  732. if os.path.isfile(moderatorsFile):
  733. with open(moderatorsFile, "r") as f:
  734. lines = f.readlines()
  735. for moderator in lines:
  736. if domainFull + '/users/' + moderator.strip('\n') + '/' in postId:
  737. return False
  738. return True
  739. def removeTagsForNickname(baseDir: str, nickname: str,
  740. domain: str, port: int) -> None:
  741. """Removes tags for a nickname
  742. """
  743. if not os.path.isdir(baseDir + '/tags'):
  744. return
  745. domainFull = domain
  746. if port:
  747. if port != 80 and port != 443:
  748. if ':' not in domain:
  749. domainFull = domain + ':' + str(port)
  750. matchStr = domainFull + '/users/' + nickname + '/'
  751. directory = os.fsencode(baseDir + '/tags/')
  752. for f in os.scandir(directory):
  753. f = f.name
  754. filename = os.fsdecode(f)
  755. if not filename.endswith(".txt"):
  756. continue
  757. tagFilename = os.path.join(directory, filename)
  758. if not os.path.isfile(tagFilename):
  759. continue
  760. if matchStr not in open(tagFilename).read():
  761. continue
  762. with open(tagFilename, "r") as f:
  763. lines = f.readlines()
  764. tagFile = open(tagFilename, "w+")
  765. if tagFile:
  766. for tagline in lines:
  767. if matchStr not in tagline:
  768. tagFile.write(tagline)
  769. tagFile.close()
  770. def removeAccount(baseDir: str, nickname: str,
  771. domain: str, port: int) -> bool:
  772. """Removes an account
  773. """
  774. # Don't remove the admin
  775. adminNickname = getConfigParam(baseDir, 'admin')
  776. if nickname == adminNickname:
  777. return False
  778. # Don't remove moderators
  779. moderatorsFile = baseDir + '/accounts/moderators.txt'
  780. if os.path.isfile(moderatorsFile):
  781. with open(moderatorsFile, "r") as f:
  782. lines = f.readlines()
  783. for moderator in lines:
  784. if moderator.strip('\n') == nickname:
  785. return False
  786. unsuspendAccount(baseDir, nickname)
  787. handle = nickname + '@' + domain
  788. removePassword(baseDir, nickname)
  789. removeTagsForNickname(baseDir, nickname, domain, port)
  790. if os.path.isdir(baseDir + '/deactivated/' + handle):
  791. shutil.rmtree(baseDir + '/deactivated/' + handle)
  792. if os.path.isdir(baseDir + '/accounts/' + handle):
  793. shutil.rmtree(baseDir + '/accounts/' + handle)
  794. if os.path.isfile(baseDir + '/accounts/' + handle + '.json'):
  795. os.remove(baseDir + '/accounts/' + handle + '.json')
  796. if os.path.isfile(baseDir + '/wfendpoints/' + handle + '.json'):
  797. os.remove(baseDir + '/wfendpoints/' + handle + '.json')
  798. if os.path.isfile(baseDir + '/keys/private/' + handle + '.key'):
  799. os.remove(baseDir + '/keys/private/' + handle + '.key')
  800. if os.path.isfile(baseDir + '/keys/public/' + handle + '.pem'):
  801. os.remove(baseDir + '/keys/public/' + handle + '.pem')
  802. if os.path.isdir(baseDir + '/sharefiles/' + nickname):
  803. shutil.rmtree(baseDir + '/sharefiles/' + nickname)
  804. if os.path.isfile(baseDir + '/wfdeactivated/' + handle + '.json'):
  805. os.remove(baseDir + '/wfdeactivated/' + handle + '.json')
  806. if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname):
  807. shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname)
  808. return True
  809. def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool:
  810. """Makes an account temporarily unavailable
  811. """
  812. handle = nickname + '@' + domain
  813. accountDir = baseDir + '/accounts/' + handle
  814. if not os.path.isdir(accountDir):
  815. return False
  816. deactivatedDir = baseDir + '/deactivated'
  817. if not os.path.isdir(deactivatedDir):
  818. os.mkdir(deactivatedDir)
  819. shutil.move(accountDir, deactivatedDir + '/' + handle)
  820. if os.path.isfile(baseDir + '/wfendpoints/' + handle + '.json'):
  821. deactivatedWebfingerDir = baseDir + '/wfdeactivated'
  822. if not os.path.isdir(deactivatedWebfingerDir):
  823. os.mkdir(deactivatedWebfingerDir)
  824. shutil.move(baseDir + '/wfendpoints/' + handle + '.json',
  825. deactivatedWebfingerDir + '/' + handle + '.json')
  826. if os.path.isdir(baseDir + '/sharefiles/' + nickname):
  827. deactivatedSharefilesDir = baseDir + '/sharefilesdeactivated'
  828. if not os.path.isdir(deactivatedSharefilesDir):
  829. os.mkdir(deactivatedSharefilesDir)
  830. shutil.move(baseDir + '/sharefiles/' + nickname,
  831. deactivatedSharefilesDir + '/' + nickname)
  832. return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
  833. def activateAccount(baseDir: str, nickname: str, domain: str) -> None:
  834. """Makes a deactivated account available
  835. """
  836. handle = nickname + '@' + domain
  837. deactivatedDir = baseDir + '/deactivated'
  838. deactivatedAccountDir = deactivatedDir + '/' + handle
  839. if os.path.isdir(deactivatedAccountDir):
  840. accountDir = baseDir + '/accounts/' + handle
  841. if not os.path.isdir(accountDir):
  842. shutil.move(deactivatedAccountDir, accountDir)
  843. deactivatedWebfingerDir = baseDir + '/wfdeactivated'
  844. if os.path.isfile(deactivatedWebfingerDir + '/' + handle + '.json'):
  845. shutil.move(deactivatedWebfingerDir + '/' + handle + '.json',
  846. baseDir + '/wfendpoints/' + handle + '.json')
  847. deactivatedSharefilesDir = baseDir + '/sharefilesdeactivated'
  848. if os.path.isdir(deactivatedSharefilesDir + '/' + nickname):
  849. if not os.path.isdir(baseDir + '/sharefiles/' + nickname):
  850. shutil.move(deactivatedSharefilesDir + '/' + nickname,
  851. baseDir + '/sharefiles/' + nickname)
  852. def isPersonSnoozed(baseDir: str, nickname: str, domain: str,
  853. snoozeActor: str) -> bool:
  854. """Returns true if the given actor is snoozed
  855. """
  856. snoozedFilename = baseDir + '/accounts/' + \
  857. nickname + '@' + domain + '/snoozed.txt'
  858. if not os.path.isfile(snoozedFilename):
  859. return False
  860. if snoozeActor + ' ' not in open(snoozedFilename).read():
  861. return False
  862. # remove the snooze entry if it has timed out
  863. replaceStr = None
  864. with open(snoozedFilename, 'r') as snoozedFile:
  865. for line in snoozedFile:
  866. # is this the entry for the actor?
  867. if line.startswith(snoozeActor + ' '):
  868. snoozedTimeStr = \
  869. line.split(' ')[1].replace('\n', '').replace('\r', '')
  870. # is there a time appended?
  871. if snoozedTimeStr.isdigit():
  872. snoozedTime = int(snoozedTimeStr)
  873. currTime = int(time.time())
  874. # has the snooze timed out?
  875. if int(currTime - snoozedTime) > 60 * 60 * 24:
  876. replaceStr = line
  877. else:
  878. replaceStr = line
  879. break
  880. if replaceStr:
  881. content = None
  882. with open(snoozedFilename, 'r') as snoozedFile:
  883. content = snoozedFile.read().replace(replaceStr, '')
  884. if content:
  885. writeSnoozedFile = open(snoozedFilename, 'w')
  886. if writeSnoozedFile:
  887. writeSnoozedFile.write(content)
  888. writeSnoozedFile.close()
  889. if snoozeActor + ' ' in open(snoozedFilename).read():
  890. return True
  891. return False
  892. def personSnooze(baseDir: str, nickname: str, domain: str,
  893. snoozeActor: str) -> None:
  894. """Temporarily ignores the given actor
  895. """
  896. accountDir = baseDir + '/accounts/' + nickname + '@' + domain
  897. if not os.path.isdir(accountDir):
  898. print('ERROR: unknown account ' + accountDir)
  899. return
  900. snoozedFilename = accountDir + '/snoozed.txt'
  901. if os.path.isfile(snoozedFilename):
  902. if snoozeActor + ' ' in open(snoozedFilename).read():
  903. return
  904. snoozedFile = open(snoozedFilename, "a+")
  905. if snoozedFile:
  906. snoozedFile.write(snoozeActor + ' ' +
  907. str(int(time.time())) + '\n')
  908. snoozedFile.close()
  909. def personUnsnooze(baseDir: str, nickname: str, domain: str,
  910. snoozeActor: str) -> None:
  911. """Undoes a temporarily ignore of the given actor
  912. """
  913. accountDir = baseDir + '/accounts/' + nickname + '@' + domain
  914. if not os.path.isdir(accountDir):
  915. print('ERROR: unknown account ' + accountDir)
  916. return
  917. snoozedFilename = accountDir + '/snoozed.txt'
  918. if not os.path.isfile(snoozedFilename):
  919. return
  920. if snoozeActor + ' ' not in open(snoozedFilename).read():
  921. return
  922. replaceStr = None
  923. with open(snoozedFilename, 'r') as snoozedFile:
  924. for line in snoozedFile:
  925. if line.startswith(snoozeActor + ' '):
  926. replaceStr = line
  927. break
  928. if replaceStr:
  929. content = None
  930. with open(snoozedFilename, 'r') as snoozedFile:
  931. content = snoozedFile.read().replace(replaceStr, '')
  932. if content:
  933. writeSnoozedFile = open(snoozedFilename, 'w')
  934. if writeSnoozedFile:
  935. writeSnoozedFile.write(content)
  936. writeSnoozedFile.close()