person.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  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 commentjson
  10. import os
  11. import fileinput
  12. import subprocess
  13. import shutil
  14. from pprint import pprint
  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 createInbox
  22. from posts import createOutbox
  23. from posts import createModeration
  24. from auth import storeBasicCredentials
  25. from auth import removePassword
  26. from roles import setRole
  27. from media import removeMetaData
  28. from utils import validNickname
  29. from utils import noOfAccounts
  30. from auth import createPassword
  31. from config import setConfigParam
  32. from config import getConfigParam
  33. def generateRSAKey() -> (str,str):
  34. key = RSA.generate(2048)
  35. privateKeyPem = key.exportKey("PEM").decode("utf-8")
  36. publicKeyPem = key.publickey().exportKey("PEM").decode("utf-8")
  37. return privateKeyPem,publicKeyPem
  38. def setProfileImage(baseDir: str,httpPrefix :str,nickname: str,domain: str, \
  39. port :int,imageFilename: str,imageType :str,resolution :str) -> bool:
  40. """Saves the given image file as an avatar or background
  41. image for the given person
  42. """
  43. imageFilename=imageFilename.replace('\n','')
  44. if not (imageFilename.endswith('.png') or \
  45. imageFilename.endswith('.jpg') or \
  46. imageFilename.endswith('.jpeg') or \
  47. imageFilename.endswith('.gif')):
  48. print('Profile image must be png, jpg or gif format')
  49. return False
  50. if imageFilename.startswith('~/'):
  51. imageFilename=imageFilename.replace('~/',str(Path.home())+'/')
  52. if ':' in domain:
  53. domain=domain.split(':')[0]
  54. fullDomain=domain
  55. if port:
  56. if port!=80 and port!=443:
  57. if ':' not in domain:
  58. fullDomain=domain+':'+str(port)
  59. handle=nickname.lower()+'@'+domain.lower()
  60. personFilename=baseDir+'/accounts/'+handle+'.json'
  61. if not os.path.isfile(personFilename):
  62. print('person definition not found: '+personFilename)
  63. return False
  64. if not os.path.isdir(baseDir+'/accounts/'+handle):
  65. print('Account not found: '+baseDir+'/accounts/'+handle)
  66. return False
  67. iconFilenameBase='icon'
  68. if imageType=='avatar' or imageType=='icon':
  69. iconFilenameBase='icon'
  70. else:
  71. iconFilenameBase='image'
  72. mediaType='image/png'
  73. iconFilename=iconFilenameBase+'.png'
  74. if imageFilename.endswith('.jpg') or \
  75. imageFilename.endswith('.jpeg'):
  76. mediaType='image/jpeg'
  77. iconFilename=iconFilenameBase+'.jpg'
  78. if imageFilename.endswith('.gif'):
  79. mediaType='image/gif'
  80. iconFilename=iconFilenameBase+'.gif'
  81. profileFilename=baseDir+'/accounts/'+handle+'/'+iconFilename
  82. with open(personFilename, 'r') as fp:
  83. personJson=commentjson.load(fp)
  84. personJson[iconFilenameBase]['mediaType']=mediaType
  85. personJson[iconFilenameBase]['url']=httpPrefix+'://'+fullDomain+'/users/'+nickname+'/'+iconFilename
  86. with open(personFilename, 'w') as fp:
  87. commentjson.dump(personJson, fp, indent=4, sort_keys=False)
  88. cmd = '/usr/bin/convert '+imageFilename+' -size '+resolution+' -quality 50 '+profileFilename
  89. subprocess.call(cmd, shell=True)
  90. removeMetaData(profileFilename,profileFilename)
  91. return True
  92. return False
  93. def setOrganizationScheme(baseDir: str,nickname: str,domain: str, \
  94. schema: str) -> bool:
  95. """Set the organization schema within which a person exists
  96. This will define how roles, skills and availability are assembled
  97. into organizations
  98. """
  99. # avoid giant strings
  100. if len(schema)>256:
  101. return False
  102. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  103. if not os.path.isfile(actorFilename):
  104. return False
  105. with open(actorFilename, 'r') as fp:
  106. actorJson=commentjson.load(fp)
  107. actorJson['orgSchema']=schema
  108. with open(actorFilename, 'w') as fp:
  109. commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
  110. return True
  111. def accountExists(baseDir: str,nickname: str,domain: str) -> bool:
  112. """Returns true if the given account exists
  113. """
  114. if ':' in domain:
  115. domain=domain.split(':')[0]
  116. return os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain)
  117. def createPersonBase(baseDir: str,nickname: str,domain: str,port: int, \
  118. httpPrefix: str, saveToFile: bool,password=None) -> (str,str,{},{}):
  119. """Returns the private key, public key, actor and webfinger endpoint
  120. """
  121. privateKeyPem,publicKeyPem=generateRSAKey()
  122. webfingerEndpoint= \
  123. createWebfingerEndpoint(nickname,domain,port,httpPrefix,publicKeyPem)
  124. if saveToFile:
  125. storeWebfingerEndpoint(nickname,domain,port,baseDir,webfingerEndpoint)
  126. handle=nickname.lower()+'@'+domain.lower()
  127. originalDomain=domain
  128. if port:
  129. if port!=80 and port!=443:
  130. if ':' not in domain:
  131. domain=domain+':'+str(port)
  132. personType='Person'
  133. approveFollowers=False
  134. personName=nickname
  135. personId=httpPrefix+'://'+domain+'/users/'+nickname
  136. inboxStr=personId+'/inbox'
  137. personUrl=httpPrefix+'://'+domain+'/@'+personName
  138. if nickname=='inbox':
  139. # shared inbox
  140. inboxStr=httpPrefix+'://'+domain+'/actor/inbox'
  141. personId=httpPrefix+'://'+domain+'/actor'
  142. personUrl=httpPrefix+'://'+domain+'/about/more?instance_actor=true'
  143. personName=originalDomain
  144. approveFollowers=True
  145. personType='Application'
  146. newPerson = {'@context': ['https://www.w3.org/ns/activitystreams',
  147. 'https://w3id.org/security/v1',
  148. {'Emoji': 'toot:Emoji',
  149. 'Hashtag': 'as:Hashtag',
  150. 'IdentityProof': 'toot:IdentityProof',
  151. 'PropertyValue': 'schema:PropertyValue',
  152. 'alsoKnownAs': {'@id': 'as:alsoKnownAs', '@type': '@id'},
  153. 'focalPoint': {'@container': '@list', '@id': 'toot:focalPoint'},
  154. 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
  155. 'movedTo': {'@id': 'as:movedTo', '@type': '@id'},
  156. 'schema': 'http://schema.org#',
  157. 'value': 'schema:value'}],
  158. 'attachment': [],
  159. 'endpoints': {
  160. 'id': personId+'/endpoints',
  161. 'sharedInbox': httpPrefix+'://'+domain+'/inbox',
  162. },
  163. 'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new',
  164. 'followers': personId+'/followers',
  165. 'following': personId+'/following',
  166. 'shares': personId+'/shares',
  167. 'orgSchema': None,
  168. 'skills': {},
  169. 'roles': {},
  170. 'availability': None,
  171. 'icon': {'mediaType': 'image/png',
  172. 'type': 'Image',
  173. 'url': personId+'/avatar.png'},
  174. 'id': personId,
  175. 'image': {'mediaType': 'image/png',
  176. 'type': 'Image',
  177. 'url': personId+'/image.png'},
  178. 'inbox': inboxStr,
  179. 'manuallyApprovesFollowers': approveFollowers,
  180. 'name': personName,
  181. 'outbox': personId+'/outbox',
  182. 'preferredUsername': personName,
  183. 'summary': '',
  184. 'publicKey': {
  185. 'id': personId+'#main-key',
  186. 'owner': personId,
  187. 'publicKeyPem': publicKeyPem
  188. },
  189. 'tag': [],
  190. 'type': personType,
  191. 'url': personUrl
  192. }
  193. if nickname=='inbox':
  194. # fields not needed by the shared inbox
  195. del newPerson['outbox']
  196. del newPerson['icon']
  197. del newPerson['image']
  198. del newPerson['skills']
  199. del newPerson['shares']
  200. del newPerson['roles']
  201. del newPerson['tag']
  202. del newPerson['availability']
  203. del newPerson['followers']
  204. del newPerson['following']
  205. del newPerson['attachment']
  206. if saveToFile:
  207. # save person to file
  208. peopleSubdir='/accounts'
  209. if not os.path.isdir(baseDir+peopleSubdir):
  210. os.mkdir(baseDir+peopleSubdir)
  211. if not os.path.isdir(baseDir+peopleSubdir+'/'+handle):
  212. os.mkdir(baseDir+peopleSubdir+'/'+handle)
  213. if not os.path.isdir(baseDir+peopleSubdir+'/'+handle+'/inbox'):
  214. os.mkdir(baseDir+peopleSubdir+'/'+handle+'/inbox')
  215. if not os.path.isdir(baseDir+peopleSubdir+'/'+handle+'/outbox'):
  216. os.mkdir(baseDir+peopleSubdir+'/'+handle+'/outbox')
  217. if not os.path.isdir(baseDir+peopleSubdir+'/'+handle+'/ocap'):
  218. os.mkdir(baseDir+peopleSubdir+'/'+handle+'/ocap')
  219. if not os.path.isdir(baseDir+peopleSubdir+'/'+handle+'/queue'):
  220. os.mkdir(baseDir+peopleSubdir+'/'+handle+'/queue')
  221. filename=baseDir+peopleSubdir+'/'+handle+'.json'
  222. with open(filename, 'w') as fp:
  223. commentjson.dump(newPerson, fp, indent=4, sort_keys=False)
  224. # save to cache
  225. if not os.path.isdir(baseDir+'/cache'):
  226. os.mkdir(baseDir+'/cache')
  227. if not os.path.isdir(baseDir+'/cache/actors'):
  228. os.mkdir(baseDir+'/cache/actors')
  229. cacheFilename=baseDir+'/cache/actors/'+newPerson['id'].replace('/','#')+'.json'
  230. with open(cacheFilename, 'w') as fp:
  231. commentjson.dump(newPerson, fp, indent=4, sort_keys=False)
  232. # save the private key
  233. privateKeysSubdir='/keys/private'
  234. if not os.path.isdir(baseDir+'/keys'):
  235. os.mkdir(baseDir+'/keys')
  236. if not os.path.isdir(baseDir+privateKeysSubdir):
  237. os.mkdir(baseDir+privateKeysSubdir)
  238. filename=baseDir+privateKeysSubdir+'/'+handle+'.key'
  239. with open(filename, "w") as text_file:
  240. print(privateKeyPem, file=text_file)
  241. # save the public key
  242. publicKeysSubdir='/keys/public'
  243. if not os.path.isdir(baseDir+publicKeysSubdir):
  244. os.mkdir(baseDir+publicKeysSubdir)
  245. filename=baseDir+publicKeysSubdir+'/'+handle+'.pem'
  246. with open(filename, "w") as text_file:
  247. print(publicKeyPem, file=text_file)
  248. if password:
  249. storeBasicCredentials(baseDir,nickname,password)
  250. return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
  251. def registerAccount(baseDir: str,httpPrefix: str,domain: str,port: int, \
  252. nickname: str,password: str) -> bool:
  253. """Registers a new account from the web interface
  254. """
  255. if accountExists(baseDir,nickname,domain):
  256. return False
  257. if not validNickname(domain,nickname):
  258. print('REGISTER: Nickname '+nickname+' is invalid')
  259. return False
  260. if len(password)<8:
  261. print('REGISTER: Password should be at least 8 characters')
  262. return False
  263. privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint= \
  264. createPerson(baseDir,nickname,domain,port, \
  265. httpPrefix,True,password)
  266. if privateKeyPem:
  267. return True
  268. return False
  269. def createPerson(baseDir: str,nickname: str,domain: str,port: int, \
  270. httpPrefix: str, saveToFile: bool,password=None) -> (str,str,{},{}):
  271. """Returns the private key, public key, actor and webfinger endpoint
  272. """
  273. if not validNickname(domain,nickname):
  274. return None,None,None,None
  275. # If a config.json file doesn't exist then don't decrement
  276. # remaining registrations counter
  277. remainingConfigExists=getConfigParam(baseDir,'registrationsRemaining')
  278. if remainingConfigExists:
  279. registrationsRemaining=int(remainingConfigExists)
  280. if registrationsRemaining<=0:
  281. return None,None,None,None
  282. privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint = \
  283. createPersonBase(baseDir,nickname,domain,port,httpPrefix,saveToFile,password)
  284. if noOfAccounts(baseDir)==1:
  285. #print(nickname+' becomes the instance admin and a moderator')
  286. setRole(baseDir,nickname,domain,'instance','admin')
  287. setRole(baseDir,nickname,domain,'instance','moderator')
  288. setRole(baseDir,nickname,domain,'instance','delegator')
  289. setConfigParam(baseDir,'admin',nickname)
  290. if not os.path.isdir(baseDir+'/accounts'):
  291. os.mkdir(baseDir+'/accounts')
  292. if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
  293. os.mkdir(baseDir+'/accounts/'+nickname+'@'+domain)
  294. if os.path.isfile(baseDir+'/img/default-avatar.png'):
  295. copyfile(baseDir+'/img/default-avatar.png',baseDir+'/accounts/'+nickname+'@'+domain+'/avatar.png')
  296. if os.path.isfile(baseDir+'/img/image.png'):
  297. copyfile(baseDir+'/img/image.png',baseDir+'/accounts/'+nickname+'@'+domain+'/image.png')
  298. if os.path.isfile(baseDir+'/img/banner.png'):
  299. copyfile(baseDir+'/img/banner.png',baseDir+'/accounts/'+nickname+'@'+domain+'/banner.png')
  300. if remainingConfigExists:
  301. registrationsRemaining-=1
  302. setConfigParam(baseDir,'registrationsRemaining',str(registrationsRemaining))
  303. return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
  304. def createSharedInbox(baseDir: str,nickname: str,domain: str,port: int, \
  305. httpPrefix: str) -> (str,str,{},{}):
  306. """Generates the shared inbox
  307. """
  308. return createPersonBase(baseDir,nickname,domain,port,httpPrefix,True,None)
  309. def createCapabilitiesInbox(baseDir: str,nickname: str,domain: str,port: int, \
  310. httpPrefix: str) -> (str,str,{},{}):
  311. """Generates the capabilities inbox to sign requests
  312. """
  313. return createPersonBase(baseDir,nickname,domain,port,httpPrefix,True,None)
  314. def personLookup(domain: str,path: str,baseDir: str) -> {}:
  315. """Lookup the person for an given nickname
  316. """
  317. if path.endswith('#main-key'):
  318. path=path.replace('#main-key','')
  319. # is this a shared inbox lookup?
  320. isSharedInbox=False
  321. if path=='/inbox' or path=='/users/inbox' or path=='/sharedInbox':
  322. # shared inbox actor on @domain@domain
  323. path='/users/'+domain
  324. isSharedInbox=True
  325. else:
  326. notPersonLookup=['/inbox','/outbox','/outboxarchive', \
  327. '/followers','/following','/featured', \
  328. '.png','.jpg','.gif','.mpv']
  329. for ending in notPersonLookup:
  330. if path.endswith(ending):
  331. return None
  332. nickname=None
  333. if path.startswith('/users/'):
  334. nickname=path.replace('/users/','',1)
  335. if path.startswith('/@'):
  336. nickname=path.replace('/@','',1)
  337. if not nickname:
  338. return None
  339. if not isSharedInbox and not validNickname(domain,nickname):
  340. return None
  341. if ':' in domain:
  342. domain=domain.split(':')[0]
  343. handle=nickname+'@'+domain
  344. filename=baseDir+'/accounts/'+handle+'.json'
  345. if not os.path.isfile(filename):
  346. return None
  347. personJson={"user": "unknown"}
  348. try:
  349. with open(filename, 'r') as fp:
  350. personJson=commentjson.load(fp)
  351. except:
  352. print('WARN: Failed to load actor '+filename)
  353. return None
  354. return personJson
  355. def personBoxJson(baseDir: str,domain: str,port: int,path: str, \
  356. httpPrefix: str,noOfItems: int,boxname: str, \
  357. authorized: bool,ocapAlways: bool) -> []:
  358. """Obtain the inbox/outbox/moderation feed for the given person
  359. """
  360. if boxname!='inbox' and boxname!='dm' and \
  361. boxname!='outbox' and boxname!='moderation':
  362. return None
  363. if not '/'+boxname in path:
  364. return None
  365. # Only show the header by default
  366. headerOnly=True
  367. # handle page numbers
  368. pageNumber=None
  369. if '?page=' in path:
  370. pageNumber=path.split('?page=')[1]
  371. if pageNumber=='true':
  372. pageNumber=1
  373. else:
  374. try:
  375. pageNumber=int(pageNumber)
  376. except:
  377. pass
  378. path=path.split('?page=')[0]
  379. headerOnly=False
  380. if not path.endswith('/'+boxname):
  381. return None
  382. nickname=None
  383. if path.startswith('/users/'):
  384. nickname=path.replace('/users/','',1).replace('/'+boxname,'')
  385. if path.startswith('/@'):
  386. nickname=path.replace('/@','',1).replace('/'+boxname,'')
  387. if not nickname:
  388. return None
  389. if not validNickname(domain,nickname):
  390. return None
  391. if boxname=='inbox':
  392. return createInbox(baseDir,nickname,domain,port,httpPrefix, \
  393. noOfItems,headerOnly,ocapAlways,pageNumber)
  394. if boxname=='dm':
  395. return createDMTimeline(baseDir,nickname,domain,port,httpPrefix, \
  396. noOfItems,headerOnly,ocapAlways,pageNumber)
  397. elif boxname=='outbox':
  398. return createOutbox(baseDir,nickname,domain,port,httpPrefix, \
  399. noOfItems,headerOnly,authorized,pageNumber)
  400. elif boxname=='moderation':
  401. return createModeration(baseDir,nickname,domain,port,httpPrefix, \
  402. noOfItems,headerOnly,authorized,pageNumber)
  403. return None
  404. def personInboxJson(baseDir: str,domain: str,port: int,path: str, \
  405. httpPrefix: str,noOfItems: int,ocapAlways: bool) -> []:
  406. """Obtain the inbox feed for the given person
  407. Authentication is expected to have already happened
  408. """
  409. if not '/inbox' in path:
  410. return None
  411. # Only show the header by default
  412. headerOnly=True
  413. # handle page numbers
  414. pageNumber=None
  415. if '?page=' in path:
  416. pageNumber=path.split('?page=')[1]
  417. if pageNumber=='true':
  418. pageNumber=1
  419. else:
  420. try:
  421. pageNumber=int(pageNumber)
  422. except:
  423. pass
  424. path=path.split('?page=')[0]
  425. headerOnly=False
  426. if not path.endswith('/inbox'):
  427. return None
  428. nickname=None
  429. if path.startswith('/users/'):
  430. nickname=path.replace('/users/','',1).replace('/inbox','')
  431. if path.startswith('/@'):
  432. nickname=path.replace('/@','',1).replace('/inbox','')
  433. if not nickname:
  434. return None
  435. if not validNickname(domain,nickname):
  436. return None
  437. return createInbox(baseDir,nickname,domain,port,httpPrefix, \
  438. noOfItems,headerOnly,ocapAlways,pageNumber)
  439. def setDisplayNickname(baseDir: str,nickname: str, domain: str, \
  440. displayName: str) -> bool:
  441. if len(displayName)>32:
  442. return False
  443. handle=nickname.lower()+'@'+domain.lower()
  444. filename=baseDir+'/accounts/'+handle.lower()+'.json'
  445. if not os.path.isfile(filename):
  446. return False
  447. personJson=None
  448. with open(filename, 'r') as fp:
  449. personJson=commentjson.load(fp)
  450. if not personJson:
  451. return False
  452. personJson['name']=displayName
  453. with open(filename, 'w') as fp:
  454. commentjson.dump(personJson, fp, indent=4, sort_keys=False)
  455. return True
  456. def setBio(baseDir: str,nickname: str, domain: str, bio: str) -> bool:
  457. if len(bio)>32:
  458. return False
  459. handle=nickname.lower()+'@'+domain.lower()
  460. filename=baseDir+'/accounts/'+handle.lower()+'.json'
  461. if not os.path.isfile(filename):
  462. return False
  463. personJson=None
  464. with open(filename, 'r') as fp:
  465. personJson=commentjson.load(fp)
  466. if not personJson:
  467. return False
  468. if not personJson.get('summary'):
  469. return False
  470. personJson['summary']=bio
  471. with open(filename, 'w') as fp:
  472. commentjson.dump(personJson, fp, indent=4, sort_keys=False)
  473. return True
  474. def isSuspended(baseDir: str,nickname: str) -> bool:
  475. """Returns true if the given nickname is suspended
  476. """
  477. adminNickname=getConfigParam(baseDir,'admin')
  478. if nickname==adminNickname:
  479. return False
  480. suspendedFilename=baseDir+'/accounts/suspended.txt'
  481. if os.path.isfile(suspendedFilename):
  482. with open(suspendedFilename, "r") as f:
  483. lines = f.readlines()
  484. suspendedFile=open(suspendedFilename,"w+")
  485. for suspended in lines:
  486. if suspended.strip('\n')==nickname:
  487. return True
  488. return False
  489. def unsuspendAccount(baseDir: str,nickname: str) -> None:
  490. """Removes an account suspention
  491. """
  492. suspendedFilename=baseDir+'/accounts/suspended.txt'
  493. if os.path.isfile(suspendedFilename):
  494. with open(suspendedFilename, "r") as f:
  495. lines = f.readlines()
  496. suspendedFile=open(suspendedFilename,"w+")
  497. for suspended in lines:
  498. if suspended.strip('\n')!=nickname:
  499. suspendedFile.write(suspended)
  500. suspendedFile.close()
  501. def suspendAccount(baseDir: str,nickname: str,salts: {}) -> None:
  502. """Suspends the given account
  503. This also changes the salt used by the authentication token
  504. so that the person can't continue to use the system without
  505. going through the login screen
  506. """
  507. # Don't suspend the admin
  508. adminNickname=getConfigParam(baseDir,'admin')
  509. if nickname==adminNickname:
  510. return
  511. # Don't suspend moderators
  512. moderatorsFile=baseDir+'/accounts/moderators.txt'
  513. if os.path.isfile(moderatorsFile):
  514. with open(moderatorsFile, "r") as f:
  515. lines = f.readlines()
  516. for moderator in lines:
  517. if moderator.strip('\n')==nickname:
  518. return
  519. suspendedFilename=baseDir+'/accounts/suspended.txt'
  520. if os.path.isfile(suspendedFilename):
  521. with open(suspendedFilename, "r") as f:
  522. lines = f.readlines()
  523. for suspended in lines:
  524. if suspended.strip('\n')==nickname:
  525. return
  526. suspendedFile=open(suspendedFilename,'a+')
  527. if suspendedFile:
  528. suspendedFile.write(nickname+'\n')
  529. suspendedFile.close()
  530. salts[nickname]=createPassword(32)
  531. else:
  532. suspendedFile=open(suspendedFilename,'w+')
  533. if suspendedFile:
  534. suspendedFile.write(nickname+'\n')
  535. suspendedFile.close()
  536. salts[nickname]=createPassword(32)
  537. def canRemovePost(baseDir: str,nickname: str,domain: str,port: int,postId: str) -> bool:
  538. """Returns true if the given post can be removed
  539. """
  540. if '/statuses/' not in postId:
  541. return False
  542. domainFull=domain
  543. if port:
  544. if port!=80 and port!=443:
  545. if ':' not in domain:
  546. domainFull=domain+':'+str(port)
  547. # is the post by the admin?
  548. adminNickname=getConfigParam(baseDir,'admin')
  549. if domainFull+'/users/'+adminNickname+'/' in postId:
  550. return False
  551. # is the post by a moderator?
  552. moderatorsFile=baseDir+'/accounts/moderators.txt'
  553. if os.path.isfile(moderatorsFile):
  554. with open(moderatorsFile, "r") as f:
  555. lines = f.readlines()
  556. for moderator in lines:
  557. if domainFull+'/users/'+moderator.strip('\n')+'/' in postId:
  558. return False
  559. return True
  560. def removeTagsForNickname(baseDir: str,nickname: str,domain: str,port: int) -> None:
  561. """Removes tags for a nickname
  562. """
  563. if not os.path.isdir(baseDir+'/tags'):
  564. return
  565. domainFull=domain
  566. if port:
  567. if port!=80 and port!=443:
  568. if ':' not in domain:
  569. domainFull=domain+':'+str(port)
  570. matchStr=domainFull+'/users/'+nickname+'/'
  571. directory = os.fsencode(baseDir+'/tags/')
  572. for f in os.listdir(directory):
  573. filename = os.fsdecode(f)
  574. if not filename.endswith(".txt"):
  575. continue
  576. tagFilename=os.path.join(baseDir+'/accounts/',filename)
  577. if matchStr not in open(tagFilename).read():
  578. continue
  579. with open(tagFilename, "r") as f:
  580. lines = f.readlines()
  581. tagFile=open(tagFilename,"w+")
  582. if tagFile:
  583. for tagline in lines:
  584. if matchStr not in tagline:
  585. tagFile.write(tagline)
  586. tagFile.close()
  587. def removeAccount(baseDir: str,nickname: str,domain: str,port: int) -> bool:
  588. """Removes an account
  589. """
  590. # Don't remove the admin
  591. adminNickname=getConfigParam(baseDir,'admin')
  592. if nickname==adminNickname:
  593. return False
  594. # Don't remove moderators
  595. moderatorsFile=baseDir+'/accounts/moderators.txt'
  596. if os.path.isfile(moderatorsFile):
  597. with open(moderatorsFile, "r") as f:
  598. lines = f.readlines()
  599. for moderator in lines:
  600. if moderator.strip('\n')==nickname:
  601. return False
  602. unsuspendAccount(baseDir,nickname)
  603. handle=nickname+'@'+domain
  604. removePassword(baseDir,nickname)
  605. removeTagsForNickname(baseDir,nickname,domain,port)
  606. if os.path.isdir(baseDir+'/accounts/'+handle):
  607. shutil.rmtree(baseDir+'/accounts/'+handle)
  608. if os.path.isfile(baseDir+'/accounts/'+handle+'.json'):
  609. os.remove(baseDir+'/accounts/'+handle+'.json')
  610. if os.path.isfile(baseDir+'/wfendpoints/'+handle+'.json'):
  611. os.remove(baseDir+'/wfendpoints/'+handle+'.json')
  612. if os.path.isfile(baseDir+'/keys/private/'+handle+'.key'):
  613. os.remove(baseDir+'/keys/private/'+handle+'.key')
  614. if os.path.isfile(baseDir+'/keys/public/'+handle+'.pem'):
  615. os.remove(baseDir+'/keys/public/'+handle+'.pem')
  616. if os.path.isdir(baseDir+'/sharefiles/'+nickname):
  617. shutil.rmtree(baseDir+'/sharefiles/'+nickname)
  618. return True