person.py 32 KB

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