person.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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. theme=getConfigParam(baseDir,'theme')
  297. defaultProfileImageFilename=baseDir+'/img/image.png'
  298. if theme:
  299. if os.path.isfile(baseDir+'/img/image_'+theme+'.png'):
  300. defaultBannerFilename=baseDir+'/img/image_'+theme+'.png'
  301. if os.path.isfile(defaultProfileImageFilename):
  302. copyfile(defaultProfileImageFilename,baseDir+'/accounts/'+nickname+'@'+domain+'/image.png')
  303. defaultBannerFilename=baseDir+'/img/banner.png'
  304. if theme:
  305. if os.path.isfile(baseDir+'/img/banner_'+theme+'.png'):
  306. defaultBannerFilename=baseDir+'/img/banner_'+theme+'.png'
  307. if os.path.isfile(defaultBannerFilename):
  308. copyfile(defaultBannerFilename,baseDir+'/accounts/'+nickname+'@'+domain+'/banner.png')
  309. if remainingConfigExists:
  310. registrationsRemaining-=1
  311. setConfigParam(baseDir,'registrationsRemaining',str(registrationsRemaining))
  312. return privateKeyPem,publicKeyPem,newPerson,webfingerEndpoint
  313. def createSharedInbox(baseDir: str,nickname: str,domain: str,port: int, \
  314. httpPrefix: str) -> (str,str,{},{}):
  315. """Generates the shared inbox
  316. """
  317. return createPersonBase(baseDir,nickname,domain,port,httpPrefix,True,None)
  318. def createCapabilitiesInbox(baseDir: str,nickname: str,domain: str,port: int, \
  319. httpPrefix: str) -> (str,str,{},{}):
  320. """Generates the capabilities inbox to sign requests
  321. """
  322. return createPersonBase(baseDir,nickname,domain,port,httpPrefix,True,None)
  323. def personLookup(domain: str,path: str,baseDir: str) -> {}:
  324. """Lookup the person for an given nickname
  325. """
  326. if path.endswith('#main-key'):
  327. path=path.replace('#main-key','')
  328. # is this a shared inbox lookup?
  329. isSharedInbox=False
  330. if path=='/inbox' or path=='/users/inbox' or path=='/sharedInbox':
  331. # shared inbox actor on @domain@domain
  332. path='/users/'+domain
  333. isSharedInbox=True
  334. else:
  335. notPersonLookup=['/inbox','/outbox','/outboxarchive', \
  336. '/followers','/following','/featured', \
  337. '.png','.jpg','.gif','.mpv']
  338. for ending in notPersonLookup:
  339. if path.endswith(ending):
  340. return None
  341. nickname=None
  342. if path.startswith('/users/'):
  343. nickname=path.replace('/users/','',1)
  344. if path.startswith('/@'):
  345. nickname=path.replace('/@','',1)
  346. if not nickname:
  347. return None
  348. if not isSharedInbox and not validNickname(domain,nickname):
  349. return None
  350. if ':' in domain:
  351. domain=domain.split(':')[0]
  352. handle=nickname+'@'+domain
  353. filename=baseDir+'/accounts/'+handle+'.json'
  354. if not os.path.isfile(filename):
  355. return None
  356. personJson={"user": "unknown"}
  357. try:
  358. with open(filename, 'r') as fp:
  359. personJson=commentjson.load(fp)
  360. except:
  361. print('WARN: Failed to load actor '+filename)
  362. return None
  363. return personJson
  364. def personBoxJson(baseDir: str,domain: str,port: int,path: str, \
  365. httpPrefix: str,noOfItems: int,boxname: str, \
  366. authorized: bool,ocapAlways: bool) -> []:
  367. """Obtain the inbox/outbox/moderation feed for the given person
  368. """
  369. if boxname!='inbox' and boxname!='dm' and \
  370. boxname!='outbox' and boxname!='moderation':
  371. return None
  372. if not '/'+boxname in path:
  373. return None
  374. # Only show the header by default
  375. headerOnly=True
  376. # handle page numbers
  377. pageNumber=None
  378. if '?page=' in path:
  379. pageNumber=path.split('?page=')[1]
  380. if pageNumber=='true':
  381. pageNumber=1
  382. else:
  383. try:
  384. pageNumber=int(pageNumber)
  385. except:
  386. pass
  387. path=path.split('?page=')[0]
  388. headerOnly=False
  389. if not path.endswith('/'+boxname):
  390. return None
  391. nickname=None
  392. if path.startswith('/users/'):
  393. nickname=path.replace('/users/','',1).replace('/'+boxname,'')
  394. if path.startswith('/@'):
  395. nickname=path.replace('/@','',1).replace('/'+boxname,'')
  396. if not nickname:
  397. return None
  398. if not validNickname(domain,nickname):
  399. return None
  400. if boxname=='inbox':
  401. return createInbox(baseDir,nickname,domain,port,httpPrefix, \
  402. noOfItems,headerOnly,ocapAlways,pageNumber)
  403. if boxname=='dm':
  404. return createDMTimeline(baseDir,nickname,domain,port,httpPrefix, \
  405. noOfItems,headerOnly,ocapAlways,pageNumber)
  406. elif boxname=='outbox':
  407. return createOutbox(baseDir,nickname,domain,port,httpPrefix, \
  408. noOfItems,headerOnly,authorized,pageNumber)
  409. elif boxname=='moderation':
  410. return createModeration(baseDir,nickname,domain,port,httpPrefix, \
  411. noOfItems,headerOnly,authorized,pageNumber)
  412. return None
  413. def personInboxJson(baseDir: str,domain: str,port: int,path: str, \
  414. httpPrefix: str,noOfItems: int,ocapAlways: bool) -> []:
  415. """Obtain the inbox feed for the given person
  416. Authentication is expected to have already happened
  417. """
  418. if not '/inbox' in path:
  419. return None
  420. # Only show the header by default
  421. headerOnly=True
  422. # handle page numbers
  423. pageNumber=None
  424. if '?page=' in path:
  425. pageNumber=path.split('?page=')[1]
  426. if pageNumber=='true':
  427. pageNumber=1
  428. else:
  429. try:
  430. pageNumber=int(pageNumber)
  431. except:
  432. pass
  433. path=path.split('?page=')[0]
  434. headerOnly=False
  435. if not path.endswith('/inbox'):
  436. return None
  437. nickname=None
  438. if path.startswith('/users/'):
  439. nickname=path.replace('/users/','',1).replace('/inbox','')
  440. if path.startswith('/@'):
  441. nickname=path.replace('/@','',1).replace('/inbox','')
  442. if not nickname:
  443. return None
  444. if not validNickname(domain,nickname):
  445. return None
  446. return createInbox(baseDir,nickname,domain,port,httpPrefix, \
  447. noOfItems,headerOnly,ocapAlways,pageNumber)
  448. def setDisplayNickname(baseDir: str,nickname: str, domain: str, \
  449. displayName: str) -> bool:
  450. if len(displayName)>32:
  451. return False
  452. handle=nickname.lower()+'@'+domain.lower()
  453. filename=baseDir+'/accounts/'+handle.lower()+'.json'
  454. if not os.path.isfile(filename):
  455. return False
  456. personJson=None
  457. with open(filename, 'r') as fp:
  458. personJson=commentjson.load(fp)
  459. if not personJson:
  460. return False
  461. personJson['name']=displayName
  462. with open(filename, 'w') as fp:
  463. commentjson.dump(personJson, fp, indent=4, sort_keys=False)
  464. return True
  465. def setBio(baseDir: str,nickname: str, domain: str, bio: str) -> bool:
  466. if len(bio)>32:
  467. return False
  468. handle=nickname.lower()+'@'+domain.lower()
  469. filename=baseDir+'/accounts/'+handle.lower()+'.json'
  470. if not os.path.isfile(filename):
  471. return False
  472. personJson=None
  473. with open(filename, 'r') as fp:
  474. personJson=commentjson.load(fp)
  475. if not personJson:
  476. return False
  477. if not personJson.get('summary'):
  478. return False
  479. personJson['summary']=bio
  480. with open(filename, 'w') as fp:
  481. commentjson.dump(personJson, fp, indent=4, sort_keys=False)
  482. return True
  483. def isSuspended(baseDir: str,nickname: str) -> bool:
  484. """Returns true if the given nickname is suspended
  485. """
  486. adminNickname=getConfigParam(baseDir,'admin')
  487. if nickname==adminNickname:
  488. return False
  489. suspendedFilename=baseDir+'/accounts/suspended.txt'
  490. if os.path.isfile(suspendedFilename):
  491. with open(suspendedFilename, "r") as f:
  492. lines = f.readlines()
  493. suspendedFile=open(suspendedFilename,"w+")
  494. for suspended in lines:
  495. if suspended.strip('\n')==nickname:
  496. return True
  497. return False
  498. def unsuspendAccount(baseDir: str,nickname: str) -> None:
  499. """Removes an account suspention
  500. """
  501. suspendedFilename=baseDir+'/accounts/suspended.txt'
  502. if os.path.isfile(suspendedFilename):
  503. with open(suspendedFilename, "r") as f:
  504. lines = f.readlines()
  505. suspendedFile=open(suspendedFilename,"w+")
  506. for suspended in lines:
  507. if suspended.strip('\n')!=nickname:
  508. suspendedFile.write(suspended)
  509. suspendedFile.close()
  510. def suspendAccount(baseDir: str,nickname: str,salts: {}) -> None:
  511. """Suspends the given account
  512. This also changes the salt used by the authentication token
  513. so that the person can't continue to use the system without
  514. going through the login screen
  515. """
  516. # Don't suspend the admin
  517. adminNickname=getConfigParam(baseDir,'admin')
  518. if nickname==adminNickname:
  519. return
  520. # Don't suspend moderators
  521. moderatorsFile=baseDir+'/accounts/moderators.txt'
  522. if os.path.isfile(moderatorsFile):
  523. with open(moderatorsFile, "r") as f:
  524. lines = f.readlines()
  525. for moderator in lines:
  526. if moderator.strip('\n')==nickname:
  527. return
  528. suspendedFilename=baseDir+'/accounts/suspended.txt'
  529. if os.path.isfile(suspendedFilename):
  530. with open(suspendedFilename, "r") as f:
  531. lines = f.readlines()
  532. for suspended in lines:
  533. if suspended.strip('\n')==nickname:
  534. return
  535. suspendedFile=open(suspendedFilename,'a+')
  536. if suspendedFile:
  537. suspendedFile.write(nickname+'\n')
  538. suspendedFile.close()
  539. salts[nickname]=createPassword(32)
  540. else:
  541. suspendedFile=open(suspendedFilename,'w+')
  542. if suspendedFile:
  543. suspendedFile.write(nickname+'\n')
  544. suspendedFile.close()
  545. salts[nickname]=createPassword(32)
  546. def canRemovePost(baseDir: str,nickname: str,domain: str,port: int,postId: str) -> bool:
  547. """Returns true if the given post can be removed
  548. """
  549. if '/statuses/' not in postId:
  550. return False
  551. domainFull=domain
  552. if port:
  553. if port!=80 and port!=443:
  554. if ':' not in domain:
  555. domainFull=domain+':'+str(port)
  556. # is the post by the admin?
  557. adminNickname=getConfigParam(baseDir,'admin')
  558. if domainFull+'/users/'+adminNickname+'/' in postId:
  559. return False
  560. # is the post by a moderator?
  561. moderatorsFile=baseDir+'/accounts/moderators.txt'
  562. if os.path.isfile(moderatorsFile):
  563. with open(moderatorsFile, "r") as f:
  564. lines = f.readlines()
  565. for moderator in lines:
  566. if domainFull+'/users/'+moderator.strip('\n')+'/' in postId:
  567. return False
  568. return True
  569. def removeTagsForNickname(baseDir: str,nickname: str,domain: str,port: int) -> None:
  570. """Removes tags for a nickname
  571. """
  572. if not os.path.isdir(baseDir+'/tags'):
  573. return
  574. domainFull=domain
  575. if port:
  576. if port!=80 and port!=443:
  577. if ':' not in domain:
  578. domainFull=domain+':'+str(port)
  579. matchStr=domainFull+'/users/'+nickname+'/'
  580. directory = os.fsencode(baseDir+'/tags/')
  581. for f in os.listdir(directory):
  582. filename = os.fsdecode(f)
  583. if not filename.endswith(".txt"):
  584. continue
  585. tagFilename=os.path.join(baseDir+'/accounts/',filename)
  586. if matchStr not in open(tagFilename).read():
  587. continue
  588. with open(tagFilename, "r") as f:
  589. lines = f.readlines()
  590. tagFile=open(tagFilename,"w+")
  591. if tagFile:
  592. for tagline in lines:
  593. if matchStr not in tagline:
  594. tagFile.write(tagline)
  595. tagFile.close()
  596. def removeAccount(baseDir: str,nickname: str,domain: str,port: int) -> bool:
  597. """Removes an account
  598. """
  599. # Don't remove the admin
  600. adminNickname=getConfigParam(baseDir,'admin')
  601. if nickname==adminNickname:
  602. return False
  603. # Don't remove moderators
  604. moderatorsFile=baseDir+'/accounts/moderators.txt'
  605. if os.path.isfile(moderatorsFile):
  606. with open(moderatorsFile, "r") as f:
  607. lines = f.readlines()
  608. for moderator in lines:
  609. if moderator.strip('\n')==nickname:
  610. return False
  611. unsuspendAccount(baseDir,nickname)
  612. handle=nickname+'@'+domain
  613. removePassword(baseDir,nickname)
  614. removeTagsForNickname(baseDir,nickname,domain,port)
  615. if os.path.isdir(baseDir+'/accounts/'+handle):
  616. shutil.rmtree(baseDir+'/accounts/'+handle)
  617. if os.path.isfile(baseDir+'/accounts/'+handle+'.json'):
  618. os.remove(baseDir+'/accounts/'+handle+'.json')
  619. if os.path.isfile(baseDir+'/wfendpoints/'+handle+'.json'):
  620. os.remove(baseDir+'/wfendpoints/'+handle+'.json')
  621. if os.path.isfile(baseDir+'/keys/private/'+handle+'.key'):
  622. os.remove(baseDir+'/keys/private/'+handle+'.key')
  623. if os.path.isfile(baseDir+'/keys/public/'+handle+'.pem'):
  624. os.remove(baseDir+'/keys/public/'+handle+'.pem')
  625. if os.path.isdir(baseDir+'/sharefiles/'+nickname):
  626. shutil.rmtree(baseDir+'/sharefiles/'+nickname)
  627. return True