person.py 32 KB

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