person.py 24 KB

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