Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon

main
Bob Mottram 2021-05-14 13:37:28 +01:00
commit a6419a5647
14 changed files with 430 additions and 606 deletions

View File

@ -310,52 +310,6 @@ python3 epicyon.py --domainmax 1000 --accountmax 200
With these settings you're going to be receiving no more than 200 messages for any given account within a day.
## Delegated roles
Within an organization you may want to define different roles and for some projects to be delegated. By default the first account added to the system will be the admin, and be assigned *moderator* and *delegator* roles under a project called *instance*. The admin can then delegate a person to other projects with:
``` bash
python3 epicyon.py --nickname [admin nickname] --domain [mydomain] \
--delegate [person nickname] \
--project [project name] --role [title] \
--password [c2s password]
```
The other person could also be made a delegator, but they will only be able to delegate further within projects which they're assigned to. By design, this creates a restricted organizational hierarchy. For example:
``` bash
python3 epicyon.py --nickname [admin nickname] --domain [mydomain] \
--delegate [person nickname] \
--project [project name] --role delegator \
--password [c2s password]
```
A delegated role can also be removed.
``` bash
python3 epicyon.py --nickname [admin nickname] --domain [mydomain] \
--undelegate [person nickname] \
--project [project name] \
--password [c2s password]
```
This extends the ActivityPub client-to-server protocol to include activities called *Delegate* and *Role*. The JSON looks like:
``` json
{ 'type': 'Delegate',
'actor': https://somedomain/users/admin,
'object': {
'type': 'Role',
'actor': https://'+somedomain+'/users/'+other,
'object': 'otherproject;otherrole',
'to': [],
'cc': []
},
'to': [],
'cc': []}
```
Projects and roles are only scoped within a single instance. There presently are not enough security mechanisms to support multi-instance distributed organizations.
## Assigning skills

View File

@ -150,7 +150,8 @@ def spoofGeolocation(baseDir: str,
else:
if not os.path.isfile(locationsFilename):
return (default_latitude, default_longitude,
default_latdirection, default_longdirection)
default_latdirection, default_longdirection,
"", "", 0)
cities = []
with open(locationsFilename, "r") as f:
cities = f.readlines()

103
daemon.py
View File

@ -98,6 +98,11 @@ from follow import getFollowingFeed
from follow import sendFollowRequest
from follow import unfollowAccount
from follow import createInitialLastSeen
from skills import getSkillsFromString
from skills import noOfActorSkills
from skills import actorHasSkill
from skills import actorSkillValue
from skills import setActorSkillLevel
from auth import authorize
from auth import createPassword
from auth import createBasicAuthHeader
@ -119,6 +124,7 @@ from blocking import isBlockedHashtag
from blocking import isBlockedDomain
from blocking import getDomainBlocklist
from roles import setRole
from roles import getRolesFromString
from roles import clearModeratorStatus
from roles import clearEditorStatus
from roles import clearCounselorStatus
@ -4154,22 +4160,6 @@ class PubServer(BaseHTTPRequestHandler):
# which isn't implemented in Epicyon
actorJson['discoverable'] = True
actorChanged = True
if not actorJson['@context'][2].get('orgSchema'):
actorJson['@context'][2]['orgSchema'] = \
'toot:orgSchema'
actorChanged = True
if not actorJson['@context'][2].get('skills'):
actorJson['@context'][2]['skills'] = 'toot:skills'
actorChanged = True
if not actorJson['@context'][2].get('shares'):
actorJson['@context'][2]['shares'] = 'toot:shares'
actorChanged = True
if not actorJson['@context'][2].get('roles'):
actorJson['@context'][2]['roles'] = 'toot:roles'
actorChanged = True
if not actorJson['@context'][2].get('availability'):
actorJson['@context'][2]['availaibility'] = \
'toot:availability'
if actorJson.get('capabilityAcquisitionEndpoint'):
del actorJson['capabilityAcquisitionEndpoint']
actorChanged = True
@ -4207,7 +4197,7 @@ class PubServer(BaseHTTPRequestHandler):
# set skill levels
skillCtr = 1
newSkills = {}
actorSkillsCtr = noOfActorSkills(actorJson)
while skillCtr < 10:
skillName = \
fields.get('skillName' + str(skillCtr))
@ -4222,21 +4212,21 @@ class PubServer(BaseHTTPRequestHandler):
if not skillValue:
skillCtr += 1
continue
if not actorJson['skills'].get(skillName):
if not actorHasSkill(actorJson, skillName):
actorChanged = True
else:
if actorJson['skills'][skillName] != \
if actorSkillValue(actorJson, skillName) != \
int(skillValue):
actorChanged = True
newSkills[skillName] = int(skillValue)
setActorSkillLevel(actorJson,
skillName, int(skillValue))
skillsStr = self.server.translate['Skills']
setHashtagCategory(baseDir, skillName,
skillsStr.lower())
skillCtr += 1
if len(actorJson['skills'].items()) != \
len(newSkills.items()):
if noOfActorSkills(actorJson) != \
actorSkillsCtr:
actorChanged = True
actorJson['skills'] = newSkills
# change password
if fields.get('password'):
@ -4593,19 +4583,20 @@ class PubServer(BaseHTTPRequestHandler):
# Other accounts (alsoKnownAs)
occupationName = ""
if actorJson.get('occupationName'):
occupationName = actorJson['occupationName']
if actorJson.get('hasOccupation'):
if actorJson['hasOccupation'].get('name'):
occupationName = actorJson['hasOccupation']['name']
if fields.get('occupationName'):
fields['occupationName'] = \
removeHtml(fields['occupationName'])
if occupationName != \
fields['occupationName']:
actorJson['occupationName'] = \
actorJson['hasOccupation']['name'] = \
fields['occupationName']
actorChanged = True
else:
if occupationName:
actorJson['occupationName'] = ''
actorJson['hasOccupation']['name'] = ''
actorChanged = True
# Other accounts (alsoKnownAs)
@ -4742,7 +4733,7 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(modDir):
setRole(baseDir,
modNick, domain,
'instance', 'moderator')
'moderator')
else:
# nicknames on separate lines
modFile = open(moderatorsFile, "w+")
@ -4767,7 +4758,6 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(modDir):
setRole(baseDir,
modNick, domain,
'instance',
'moderator')
# change site editors list
@ -4799,7 +4789,7 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(edDir):
setRole(baseDir,
edNick, domain,
'instance', 'editor')
'editor')
else:
# nicknames on separate lines
edFile = open(editorsFile, "w+")
@ -4824,7 +4814,6 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(edDir):
setRole(baseDir,
edNick, domain,
'instance',
'editor')
# change site counselors list
@ -4856,7 +4845,7 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(edDir):
setRole(baseDir,
edNick, domain,
'instance', 'counselor')
'counselor')
else:
# nicknames on separate lines
edFile = open(counselorsFile, "w+")
@ -4881,7 +4870,6 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isdir(edDir):
setRole(baseDir,
edNick, domain,
'instance',
'counselor')
# remove scheduled posts
@ -7386,7 +7374,7 @@ class PubServer(BaseHTTPRequestHandler):
if not actorJson:
return False
if actorJson.get('roles'):
if actorJson.get('affiliation'):
if self._requestHTTP():
getPerson = \
personLookup(domain, path.replace('/roles', ''),
@ -7407,6 +7395,10 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.keyShortcuts.get(nickname):
accessKeys = self.server.keyShortcuts[nickname]
rolesList = []
if actorJson.get('affiliation'):
actorRolesStr = actorJson['affiliation']['roleName']
rolesList = getRolesFromString(actorRolesStr)
msg = \
htmlProfile(self.server.rssIconAtTop,
self.server.cssCache,
@ -7430,8 +7422,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowLocalNetworkAccess,
self.server.textModeBanner,
self.server.debug,
accessKeys,
actorJson['roles'],
accessKeys, rolesList,
None, None)
msg = msg.encode('utf-8')
msglen = len(msg)
@ -7443,7 +7434,12 @@ class PubServer(BaseHTTPRequestHandler):
'show roles')
else:
if self._fetchAuthenticated():
msg = json.dumps(actorJson['roles'],
rolesList = []
if actorJson.get('affiliation'):
actorRolesStr = actorJson['affiliation']['roleName']
rolesList = getRolesFromString(actorRolesStr)
msg = json.dumps(rolesList,
ensure_ascii=False)
msg = msg.encode('utf-8')
msglen = len(msg)
@ -7476,7 +7472,7 @@ class PubServer(BaseHTTPRequestHandler):
if os.path.isfile(actorFilename):
actorJson = loadJson(actorFilename)
if actorJson:
if actorJson.get('skills'):
if noOfActorSkills(actorJson) > 0:
if self._requestHTTP():
getPerson = \
personLookup(domain,
@ -7501,6 +7497,9 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.keyShortcuts.get(nickname):
accessKeys = \
self.server.keyShortcuts[nickname]
actorSkillsStr = \
actorJson['hasOccupation']['skills']
skills = getSkillsFromString(actorSkillsStr)
msg = \
htmlProfile(self.server.rssIconAtTop,
self.server.cssCache,
@ -7524,8 +7523,7 @@ class PubServer(BaseHTTPRequestHandler):
allowLocalNetworkAccess,
self.server.textModeBanner,
self.server.debug,
accessKeys,
actorJson['skills'],
accessKeys, skills,
None, None)
msg = msg.encode('utf-8')
msglen = len(msg)
@ -7538,7 +7536,10 @@ class PubServer(BaseHTTPRequestHandler):
'show skills')
else:
if self._fetchAuthenticated():
msg = json.dumps(actorJson['skills'],
actorSkillsStr = \
actorJson['hasOccupation']['skills']
skills = getSkillsFromString(actorSkillsStr)
msg = json.dumps(skills,
ensure_ascii=False)
msg = msg.encode('utf-8')
msglen = len(msg)
@ -10571,7 +10572,11 @@ class PubServer(BaseHTTPRequestHandler):
msg = \
htmlLogin(self.server.cssCache,
self.server.translate,
self.server.baseDir, False).encode('utf-8')
self.server.baseDir,
self.server.httpPrefix,
self.server.domainFull,
self.server.systemLanguage,
False).encode('utf-8')
msglen = len(msg)
self._logout_headers('text/html', msglen, callingDomain)
self._write(msg)
@ -11201,13 +11206,15 @@ class PubServer(BaseHTTPRequestHandler):
htmlAbout(self.server.cssCache,
self.server.baseDir, 'http',
self.server.onionDomain,
None, self.server.translate)
None, self.server.translate,
self.server.systemLanguage)
elif callingDomain.endswith('.i2p'):
msg = \
htmlAbout(self.server.cssCache,
self.server.baseDir, 'http',
self.server.i2pDomain,
None, self.server.translate)
None, self.server.translate,
self.server.systemLanguage)
else:
msg = \
htmlAbout(self.server.cssCache,
@ -11215,7 +11222,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.httpPrefix,
self.server.domainFull,
self.server.onionDomain,
self.server.translate)
self.server.translate,
self.server.systemLanguage)
msg = msg.encode('utf-8')
msglen = len(msg)
self._login_headers('text/html', msglen, callingDomain)
@ -11676,7 +11684,10 @@ class PubServer(BaseHTTPRequestHandler):
# request basic auth
msg = htmlLogin(self.server.cssCache,
self.server.translate,
self.server.baseDir).encode('utf-8')
self.server.baseDir,
self.server.httpPrefix,
self.server.domainFull,
self.server.systemLanguage).encode('utf-8')
msglen = len(msg)
self._login_headers('text/html', msglen, callingDomain)
self._write(msg)

View File

@ -74,7 +74,6 @@ from media import getAttachmentMediaType
from delete import sendDeleteViaServer
from like import sendLikeViaServer
from like import sendUndoLikeViaServer
from roles import sendRoleViaServer
from skills import sendSkillViaServer
from availability import setAvailability
from availability import sendAvailabilityViaServer
@ -495,9 +494,6 @@ parser.add_argument('--maxEmoji', '--maxemoji', dest='maxEmoji',
help='Maximum number of emoji within a post')
parser.add_argument('--role', dest='role', type=str, default=None,
help='Set a role for a person')
parser.add_argument('--organization', '--project', dest='project',
type=str, default=None,
help='Set a project for a person')
parser.add_argument('--skill', dest='skill', type=str, default=None,
help='Set a skill for a person')
parser.add_argument('--level', dest='skillLevelPercent', type=int,
@ -518,11 +514,6 @@ parser.add_argument('--mute', dest='mute', type=str, default=None,
help='Mute a particular post URL')
parser.add_argument('--unmute', dest='unmute', type=str, default=None,
help='Unmute a particular post URL')
parser.add_argument('--delegate', dest='delegate', type=str, default=None,
help='Address of an account to delegate a role to')
parser.add_argument('--undodelegate', '--undelegate', dest='undelegate',
type=str, default=None,
help='Removes a delegated role for the given address')
parser.add_argument('--filter', dest='filterStr', type=str, default=None,
help='Adds a word or phrase which if present will ' +
'cause a message to be ignored')
@ -1987,24 +1978,6 @@ if args.backgroundImage:
print('Background image was not added for ' + args.nickname)
sys.exit()
if args.project:
if not args.delegate and not args.undelegate:
if not nickname:
print('No nickname given')
sys.exit()
if args.role.lower() == 'none' or \
args.role.lower() == 'remove' or \
args.role.lower() == 'delete':
args.role = None
if args.role:
if setRole(baseDir, nickname, domain, args.project, args.role):
print('Role within ' + args.project + ' set to ' + args.role)
else:
if setRole(baseDir, nickname, domain, args.project, None):
print('Left ' + args.project)
sys.exit()
if args.skill:
if not nickname:
print('Specify a nickname with the --nickname option')
@ -2218,86 +2191,6 @@ if args.unmute:
time.sleep(1)
sys.exit()
if args.delegate:
if not nickname:
print('Specify a nickname with the --nickname option')
sys.exit()
if not args.password:
args.password = getpass.getpass('Password: ')
if not args.password:
print('Specify a password with the --password option')
sys.exit()
args.password = args.password.replace('\n', '')
if not args.project:
print('Specify a project with the --project option')
sys.exit()
if not args.role:
print('Specify a role with the --role option')
sys.exit()
if '@' in args.delegate:
delegatedNickname = args.delegate.split('@')[0]
args.delegate = blockedActor
session = createSession(proxyType)
personCache = {}
cachedWebfingers = {}
print('Sending delegation for ' + args.delegate +
' with role ' + args.role + ' in project ' + args.project)
sendRoleViaServer(baseDir, session,
nickname, args.password,
domain, port,
httpPrefix, args.delegate,
args.project, args.role,
cachedWebfingers, personCache,
True, __version__)
for i in range(10):
# TODO detect send success/fail
time.sleep(1)
sys.exit()
if args.undelegate:
if not nickname:
print('Specify a nickname with the --nickname option')
sys.exit()
if not args.password:
args.password = getpass.getpass('Password: ')
if not args.password:
print('Specify a password with the --password option')
sys.exit()
args.password = args.password.replace('\n', '')
if not args.project:
print('Specify a project with the --project option')
sys.exit()
if '@' in args.undelegate:
delegatedNickname = args.undelegate.split('@')[0]
args.undelegate = blockedActor
session = createSession(proxyType)
personCache = {}
cachedWebfingers = {}
print('Sending delegation removal for ' + args.undelegate +
' from role ' + args.role + ' in project ' + args.project)
sendRoleViaServer(baseDir, session,
nickname, args.password,
domain, port,
httpPrefix, args.delegate,
args.project, None,
cachedWebfingers, personCache,
True, __version__)
for i in range(10):
# TODO detect send success/fail
time.sleep(1)
sys.exit()
if args.unblock:
if not nickname:
print('Specify a nickname with the --nickname option')
@ -2388,9 +2281,7 @@ if args.testdata:
True, False, 'likewhateveryouwantscoob')
setSkillLevel(baseDir, nickname, domain, 'testing', 60)
setSkillLevel(baseDir, nickname, domain, 'typing', 50)
setRole(baseDir, nickname, domain, 'instance', 'admin')
setRole(baseDir, nickname, domain, 'epicyon', 'hacker')
setRole(baseDir, nickname, domain, 'someproject', 'assistant')
setRole(baseDir, nickname, domain, 'admin')
setAvailability(baseDir, nickname, domain, 'busy')
addShare(baseDir,

View File

@ -35,7 +35,6 @@ from inbox import inboxUpdateIndex
from announce import outboxAnnounce
from announce import outboxUndoAnnounce
from follow import outboxUndoFollow
from roles import outboxDelegate
from skills import outboxSkills
from availability import outboxAvailability
from like import outboxLike
@ -313,7 +312,7 @@ def postMessageToOutbox(session, translate: {},
permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo',
'Update', 'Add', 'Remove', 'Block', 'Delete',
'Delegate', 'Skill', 'Add', 'Remove', 'Event',
'Skill', 'Add', 'Remove', 'Event',
'Ignore')
if messageJson['type'] not in permittedOutboxTypes:
if debug:
@ -461,10 +460,6 @@ def postMessageToOutbox(session, translate: {},
print('DEBUG: handle any unfollow requests')
outboxUndoFollow(baseDir, messageJson, debug)
if debug:
print('DEBUG: handle delegation requests')
outboxDelegate(baseDir, postToNickname, messageJson, debug)
if debug:
print('DEBUG: handle skills changes requests')
outboxSkills(baseDir, postToNickname, messageJson, debug)

114
person.py
View File

@ -141,26 +141,6 @@ def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
return False
def setOrganizationScheme(baseDir: str, nickname: str, domain: str,
schema: str) -> bool:
"""Set the organization schema within which a person exists
This will define how roles, skills and availability are assembled
into organizations
"""
# avoid giant strings
if len(schema) > 256:
return False
actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
if not os.path.isfile(actorFilename):
return False
actorJson = loadJson(actorFilename)
if actorJson:
actorJson['orgSchema'] = schema
saveJson(actorJson, actorFilename)
return True
def _accountExists(baseDir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account exists
"""
@ -219,7 +199,10 @@ def getDefaultPersonContext() -> str:
'schema': 'http://schema.org#',
'suspended': 'toot:suspended',
'toot': 'http://joinmastodon.org/ns#',
'value': 'schema:value'
'value': 'schema:value',
'Occupation': 'schema:Occupation',
'OrganizationRole': 'schema:OrganizationRole',
'WebSite': 'schema:Project'
}
@ -295,10 +278,20 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
'following': personId + '/following',
'tts': personId + '/speaker',
'shares': personId + '/shares',
'orgSchema': None,
'occupation': "",
'skills': {},
'roles': {},
'hasOccupation': {
'@type': 'Occupation',
'name': "",
'skills': ""
},
"affiliation": {
"@type": "OrganizationRole",
"roleName": "",
"affiliation": {
"@type": "WebSite",
"url": httpPrefix + '://' + domain
},
"startDate": published
},
'availability': None,
'icon': {
'mediaType': 'image/png',
@ -333,8 +326,10 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
del newPerson['outbox']
del newPerson['icon']
del newPerson['image']
if newPerson.get('skills'):
del newPerson['skills']
del newPerson['shares']
if newPerson.get('roles'):
del newPerson['roles']
del newPerson['tag']
del newPerson['availability']
@ -479,10 +474,9 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
if nickname != 'news':
# print(nickname+' becomes the instance admin and a moderator')
setConfigParam(baseDir, 'admin', nickname)
setRole(baseDir, nickname, domain, 'instance', 'admin')
setRole(baseDir, nickname, domain, 'instance', 'moderator')
setRole(baseDir, nickname, domain, 'instance', 'editor')
setRole(baseDir, nickname, domain, 'instance', 'delegator')
setRole(baseDir, nickname, domain, 'admin')
setRole(baseDir, nickname, domain, 'moderator')
setRole(baseDir, nickname, domain, 'editor')
if not os.path.isdir(baseDir + '/accounts'):
os.mkdir(baseDir + '/accounts')
@ -578,7 +572,69 @@ def personUpgradeActor(baseDir: str, personJson: {},
personJson['published'] = published
updateActor = True
occupationName = ''
if personJson.get('occupationName'):
occupationName = personJson['occupationName']
del personJson['occupationName']
if personJson.get('occupation'):
occupationName = personJson['occupation']
del personJson['occupation']
# if the older skills format is being used then switch
# to the new one
if not personJson.get('hasOccupation'):
personJson['hasOccupation'] = {
'@type': 'Occupation',
'name': occupationName,
'skills': ""
}
updateActor = True
# remove the old skills format
if personJson.get('skills'):
del personJson['skills']
updateActor = True
# if the older roles format is being used then switch
# to the new one
if not personJson.get('affiliation'):
rolesStr = ''
adminName = getConfigParam(baseDir, 'admin')
if personJson['id'].endswith('/users/' + adminName):
rolesStr = 'admin, moderator, editor'
statusNumber, published = getStatusNumber()
personJson['affiliation'] = {
"@type": "OrganizationRole",
"roleName": rolesStr,
"affiliation": {
"@type": "WebSite",
"url": personJson['id'].split('/users/')[0]
},
"startDate": published
}
updateActor = True
# if no roles are defined then ensure that the admin
# roles are configured
if not personJson['affiliation']['roleName']:
adminName = getConfigParam(baseDir, 'admin')
if personJson['id'].endswith('/users/' + adminName):
personJson['affiliation']['roleName'] = \
'admin, moderator, editor'
updateActor = True
# remove the old roles format
if personJson.get('roles'):
del personJson['roles']
updateActor = True
if updateActor:
personJson['@context'] = [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
getDefaultPersonContext()
],
saveJson(personJson, filename)
# also update the actor within the cache

276
roles.py
View File

@ -7,13 +7,6 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from webfinger import webfingerHandle
from auth import createBasicAuthHeader
from posts import getPersonBox
from session import postJson
from utils import getFullDomain
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import loadJson
from utils import saveJson
@ -37,9 +30,12 @@ def _clearRoleStatus(baseDir: str, role: str) -> None:
actorJson = loadJson(filename)
if not actorJson:
continue
if actorJson['roles'].get('instance'):
if role in actorJson['roles']['instance']:
actorJson['roles']['instance'].remove(role)
if not actorJson.get('affiliation'):
continue
rolesList = \
getRolesFromString(actorJson['affiliation']['roleName'])
if role in rolesList:
rolesList.remove(role)
saveJson(actorJson, filename)
@ -112,13 +108,35 @@ def _removeRole(baseDir: str, nickname: str, roleFilename: str) -> None:
f.write(roleNickname + '\n')
def setRolesFromList(actorJson: {}, rolesList: []) -> None:
"""Sets roles from a list
"""
rolesStr = ''
for roleName in rolesList:
if rolesStr:
rolesStr += ', '
rolesStr += roleName.lower()
if actorJson.get('affiliation'):
actorJson['affiliation']['roleName'] = rolesStr
def getRolesFromString(rolesStr: str) -> []:
"""Returns a list of roles from a string
"""
rolesList = rolesStr.split(',')
rolesResult = []
for roleName in rolesList:
rolesResult.append(roleName.strip().lower())
return rolesResult
def setRole(baseDir: str, nickname: str, domain: str,
project: str, role: str) -> bool:
"""Set a person's role within a project
role: str) -> bool:
"""Set a person's role
Setting the role to an empty string or None will remove it
"""
# avoid giant strings
if len(role) > 128 or len(project) > 128:
if len(role) > 128:
return False
actorFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '.json'
@ -133,230 +151,28 @@ def setRole(baseDir: str, nickname: str, domain: str,
actorJson = loadJson(actorFilename)
if actorJson:
if not actorJson.get('affiliation'):
return False
rolesList = \
getRolesFromString(actorJson['affiliation']['roleName'])
actorChanged = False
if role:
# add the role
if project == 'instance':
if roleFiles.get(role):
_addRole(baseDir, nickname, domain, roleFiles[role])
if actorJson['roles'].get(project):
if role not in actorJson['roles'][project]:
actorJson['roles'][project].append(role)
else:
actorJson['roles'][project] = [role]
if role not in rolesList:
rolesList.append(role)
rolesList.sort()
setRolesFromList(actorJson, rolesList)
actorChanged = True
else:
# remove the role
if project == 'instance':
if roleFiles.get(role):
_removeRole(baseDir, nickname, roleFiles[role])
if actorJson['roles'].get(project):
actorJson['roles'][project].remove(role)
# if the project contains no roles then remove it
if len(actorJson['roles'][project]) == 0:
del actorJson['roles'][project]
if role in rolesList:
rolesList.remove(role)
setRolesFromList(actorJson, rolesList)
actorChanged = True
if actorChanged:
saveJson(actorJson, actorFilename)
return True
def _getRoles(baseDir: str, nickname: str, domain: str,
project: str) -> []:
"""Returns the roles for a given person on a given project
"""
actorFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '.json'
if not os.path.isfile(actorFilename):
return False
actorJson = loadJson(actorFilename)
if actorJson:
if not actorJson.get('roles'):
return None
if not actorJson['roles'].get(project):
return None
return actorJson['roles'][project]
return None
def outboxDelegate(baseDir: str, authenticatedNickname: str,
messageJson: {}, debug: bool) -> bool:
"""Handles receiving a delegation request
"""
if not messageJson.get('type'):
return False
if not messageJson['type'] == 'Delegate':
return False
if not messageJson.get('object'):
return False
if not isinstance(messageJson['object'], dict):
return False
if not messageJson['object'].get('type'):
return False
if not messageJson['object']['type'] == 'Role':
return False
if not messageJson['object'].get('object'):
return False
if not messageJson['object'].get('actor'):
return False
if not isinstance(messageJson['object']['object'], str):
return False
if ';' not in messageJson['object']['object']:
print('WARN: No ; separator between project and role')
return False
delegatorNickname = getNicknameFromActor(messageJson['actor'])
if delegatorNickname != authenticatedNickname:
return
domain, port = getDomainFromActor(messageJson['actor'])
project = messageJson['object']['object'].split(';')[0].strip()
# instance delegators can delagate to other projects
# than their own
canDelegate = False
delegatorRoles = _getRoles(baseDir, delegatorNickname,
domain, 'instance')
if delegatorRoles:
if 'delegator' in delegatorRoles:
canDelegate = True
if not canDelegate:
canDelegate = True
# non-instance delegators can only delegate within their project
delegatorRoles = _getRoles(baseDir, delegatorNickname,
domain, project)
if delegatorRoles:
if 'delegator' not in delegatorRoles:
return False
else:
return False
if not canDelegate:
return False
nickname = getNicknameFromActor(messageJson['object']['actor'])
if not nickname:
print('WARN: unable to find nickname in ' +
messageJson['object']['actor'])
return False
role = \
messageJson['object']['object'].split(';')[1].strip().lower()
if not role:
setRole(baseDir, nickname, domain, project, None)
return True
# what roles is this person already assigned to?
existingRoles = _getRoles(baseDir, nickname, domain, project)
if existingRoles:
if role in existingRoles:
if debug:
print(nickname + '@' + domain +
' is already assigned to the role ' +
role + ' within the project ' + project)
return False
setRole(baseDir, nickname, domain, project, role)
if debug:
print(nickname + '@' + domain +
' assigned to the role ' + role +
' within the project ' + project)
return True
def sendRoleViaServer(baseDir: str, session,
delegatorNickname: str, password: str,
delegatorDomain: str, delegatorPort: int,
httpPrefix: str, nickname: str,
project: str, role: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str) -> {}:
"""A delegator creates a role for a person via c2s
Setting role to an empty string or None removes the role
"""
if not session:
print('WARN: No session for sendRoleViaServer')
return 6
delegatorDomainFull = getFullDomain(delegatorDomain, delegatorPort)
toUrl = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
ccUrl = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + \
delegatorNickname + '/followers'
if role:
roleStr = project.lower() + ';' + role.lower()
else:
roleStr = project.lower() + ';'
actor = \
httpPrefix + '://' + delegatorDomainFull + \
'/users/' + delegatorNickname
delegateActor = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
newRoleJson = {
'type': 'Delegate',
'actor': actor,
'object': {
'type': 'Role',
'actor': delegateActor,
'object': roleStr,
'to': [toUrl],
'cc': [ccUrl]
},
'to': [toUrl],
'cc': [ccUrl]
}
handle = \
httpPrefix + '://' + delegatorDomainFull + '/@' + delegatorNickname
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
delegatorDomain, projectVersion, debug)
if not wfRequest:
if debug:
print('DEBUG: role webfinger failed for ' + handle)
return 1
if not isinstance(wfRequest, dict):
print('WARN: role webfinger for ' + handle +
' did not return a dict. ' + str(wfRequest))
return 1
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
avatarUrl, displayName) = getPersonBox(baseDir, session,
wfRequest, personCache,
projectVersion, httpPrefix,
delegatorNickname,
delegatorDomain, postToBox,
765672)
if not inboxUrl:
if debug:
print('DEBUG: role no ' + postToBox +
' was found for ' + handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: role no actor was found for ' + handle)
return 4
authHeader = createBasicAuthHeader(delegatorNickname, password)
headers = {
'host': delegatorDomain,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = \
postJson(session, newRoleJson, [], inboxUrl, headers, 30, True)
if not postResult:
if debug:
print('DEBUG: POST role failed for c2s to ' + inboxUrl)
# return 5
if debug:
print('DEBUG: c2s POST role success')
return newRoleJson

107
skills.py
View File

@ -15,7 +15,95 @@ from utils import getFullDomain
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import loadJson
from utils import saveJson
def setSkillsFromDict(actorJson: {}, skillsDict: {}) -> str:
"""Converts a dict containing skills to a string
Returns the string version of the dictionary
"""
skillsStr = ''
for name, value in skillsDict.items():
if skillsStr:
skillsStr += ', '
skillsStr += name + ':' + str(value)
actorJson['hasOccupation']['skills'] = skillsStr
return skillsStr
def getSkillsFromString(skillsStr: str) -> {}:
"""Returns a dict of skills from a string
"""
skillsList = skillsStr.split(',')
skillsDict = {}
for skill in skillsList:
if ':' not in skill:
continue
name = skill.split(':')[0].strip().lower()
valueStr = skill.split(':')[1]
if not valueStr.isdigit():
continue
skillsDict[name] = int(valueStr)
return skillsDict
def actorHasSkill(actorJson: {}, skillName: str) -> bool:
"""Returns true if the actor has the given skill
"""
skillsDict = \
getSkillsFromString(actorJson['hasOccupation']['skills'])
if not skillsDict:
return False
return skillsDict.get(skillName.lower())
def actorSkillValue(actorJson: {}, skillName: str) -> int:
"""Returns The skill level from an actor
"""
skillsDict = \
getSkillsFromString(actorJson['hasOccupation']['skills'])
if not skillsDict:
return 0
skillName = skillName.lower()
if skillsDict.get(skillName):
return skillsDict[skillName]
return 0
def noOfActorSkills(actorJson: {}) -> int:
"""Returns the number of skills that an actor has
"""
if actorJson.get('hasOccupation'):
skillsList = actorJson['hasOccupation']['skills'].split(',')
if skillsList:
return len(skillsList)
return 0
def setActorSkillLevel(actorJson: {},
skill: str, skillLevelPercent: int) -> bool:
"""Set a skill level for a person
Setting skill level to zero removes it
"""
if skillLevelPercent < 0 or skillLevelPercent > 100:
return False
if not actorJson:
return True
if not actorJson.get('hasOccupation'):
actorJson['hasOccupation'] = {
'@type': 'Occupation',
'name': '',
'skills': ''
}
skillsDict = \
getSkillsFromString(actorJson['hasOccupation']['skills'])
if skillLevelPercent > 0:
skillsDict[skill] = skillLevelPercent
else:
if skillsDict.get(skill):
del skillsDict[skill]
setSkillsFromDict(actorJson, skillsDict)
return True
def setSkillLevel(baseDir: str, nickname: str, domain: str,
@ -30,15 +118,8 @@ def setSkillLevel(baseDir: str, nickname: str, domain: str,
return False
actorJson = loadJson(actorFilename)
if actorJson:
if not actorJson.get('skills'):
actorJson['skills'] = {}
if skillLevelPercent > 0:
actorJson['skills'][skill] = skillLevelPercent
else:
del actorJson['skills'][skill]
saveJson(actorJson, actorFilename)
return True
return setActorSkillLevel(actorJson,
skill, skillLevelPercent)
def getSkills(baseDir: str, nickname: str, domain: str) -> []:
@ -50,9 +131,9 @@ def getSkills(baseDir: str, nickname: str, domain: str) -> []:
actorJson = loadJson(actorFilename)
if actorJson:
if not actorJson.get('skills'):
if not actorJson.get('hasOccupation'):
return None
return actorJson['skills']
return getSkillsFromString(actorJson['hasOccupation']['skills'])
return None
@ -112,7 +193,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str,
newSkillJson = {
'type': 'Skill',
'actor': actor,
'object': '"'+skillStr+'"',
'object': '"' + skillStr + '"',
'to': [toUrl],
'cc': [ccUrl]
}

129
tests.py
View File

@ -67,8 +67,11 @@ from person import setDisplayNickname
from person import setBio
# from person import generateRSAKey
from skills import setSkillLevel
from skills import setSkillsFromDict
from skills import getSkillsFromString
from roles import setRolesFromList
from roles import getRolesFromString
from roles import setRole
from roles import outboxDelegate
from auth import constantTimeStringCheck
from auth import createBasicAuthHeader
from auth import authorizeBasic
@ -452,7 +455,7 @@ def createServerAlice(path: str, domain: str, port: int,
deleteAllPosts(path, nickname, domain, 'inbox')
deleteAllPosts(path, nickname, domain, 'outbox')
assert setSkillLevel(path, nickname, domain, 'hacking', 90)
assert setRole(path, nickname, domain, 'someproject', 'guru')
assert setRole(path, nickname, domain, 'guru')
if hasFollows:
followPerson(path, nickname, domain, 'bob', bobAddress,
federationList, False)
@ -556,8 +559,6 @@ def createServerBob(path: str, domain: str, port: int,
False, password)
deleteAllPosts(path, nickname, domain, 'inbox')
deleteAllPosts(path, nickname, domain, 'outbox')
assert setRole(path, nickname, domain, 'bandname', 'bass player')
assert setRole(path, nickname, domain, 'bandname', 'publicist')
if hasFollows:
followPerson(path, nickname, domain,
'alice', aliceAddress, federationList, False)
@ -1409,80 +1410,6 @@ def testCreatePerson():
shutil.rmtree(baseDir)
def testDelegateRoles():
print('testDelegateRoles')
currDir = os.getcwd()
nickname = 'test382'
nicknameDelegated = 'test383'
domain = 'badgerdomain.com'
password = 'mypass'
port = 80
httpPrefix = 'https'
baseDir = currDir + '/.tests_delegaterole'
if os.path.isdir(baseDir):
shutil.rmtree(baseDir)
os.mkdir(baseDir)
os.chdir(baseDir)
privateKeyPem, publicKeyPem, person, wfEndpoint = \
createPerson(baseDir, nickname, domain, port,
httpPrefix, True, False, password)
privateKeyPem, publicKeyPem, person, wfEndpoint = \
createPerson(baseDir, nicknameDelegated, domain, port,
httpPrefix, True, False, 'insecure')
httpPrefix = 'http'
project = 'artechoke'
role = 'delegator'
actorDelegated = \
httpPrefix + '://' + domain + '/users/' + nicknameDelegated
newRoleJson = {
'type': 'Delegate',
'actor': httpPrefix + '://' + domain + '/users/' + nickname,
'object': {
'type': 'Role',
'actor': actorDelegated,
'object': project + ';' + role,
'to': [],
'cc': []
},
'to': [],
'cc': []
}
assert outboxDelegate(baseDir, nickname, newRoleJson, False)
# second time delegation has already happened so should return false
assert outboxDelegate(baseDir, nickname, newRoleJson, False) is False
assert '"delegator"' in open(baseDir + '/accounts/' + nickname +
'@' + domain + '.json').read()
assert '"delegator"' in open(baseDir + '/accounts/' + nicknameDelegated +
'@' + domain + '.json').read()
newRoleJson = {
'type': 'Delegate',
'actor': httpPrefix + '://' + domain + '/users/' + nicknameDelegated,
'object': {
'type': 'Role',
'actor': httpPrefix + '://' + domain + '/users/' + nickname,
'object': 'otherproject;otherrole',
'to': [],
'cc': []
},
'to': [],
'cc': []
}
# non-delegators cannot assign roles
assert outboxDelegate(baseDir, nicknameDelegated,
newRoleJson, False) is False
assert '"otherrole"' not in open(baseDir + '/accounts/' +
nickname + '@' + domain + '.json').read()
os.chdir(currDir)
shutil.rmtree(baseDir)
def testAuthentication():
print('testAuthentication')
currDir = os.getcwd()
@ -3731,9 +3658,54 @@ def testSpoofGeolocation() -> None:
kmlFile.close()
def testSkills() -> None:
print('testSkills')
actorJson = {
'hasOccupation': {
'@type': 'Occupation',
'name': "",
'skills': ""
}
}
skillsDict = {
'bakery': 40,
'gardening': 70
}
setSkillsFromDict(actorJson, skillsDict)
assert actorJson['hasOccupation']['skills']
skillsDict = getSkillsFromString(actorJson['hasOccupation']['skills'])
assert skillsDict.get('bakery')
assert skillsDict.get('gardening')
assert skillsDict['bakery'] == 40
assert skillsDict['gardening'] == 70
def testRoles() -> None:
print('testRoles')
actorJson = {
'affiliation': {
"@type": "OrganizationRole",
"roleName": "",
"affiliation": {
"@type": "WebSite",
"url": "https://testinstance.org"
},
"startDate": "date goes here"
}
}
testRolesList = ["admin", "moderator"]
setRolesFromList(actorJson, testRolesList)
assert actorJson['affiliation']['roleName']
rolesList = getRolesFromString(actorJson['affiliation']['roleName'])
assert 'admin' in rolesList
assert 'moderator' in rolesList
def runAllTests():
print('Running tests...')
testFunctions()
testRoles()
testSkills()
testSpoofGeolocation()
testRemovePostInteractions()
testExtractPGPPublicKey()
@ -3787,5 +3759,4 @@ def runAllTests():
testNoOfFollowersOnDomain()
testFollows()
testGroupFollowers()
testDelegateRoles()
print('Tests succeeded\n')

View File

@ -9,13 +9,14 @@ __status__ = "Production"
import os
from shutil import copyfile
from utils import getConfigParam
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlHeaderWithWebsiteMarkup
from webapp_utils import htmlFooter
from webapp_utils import markdownToHtml
def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str,
domainFull: str, onionDomain: str, translate: {}) -> str:
domainFull: str, onionDomain: str, translate: {},
systemLanguage: str) -> str:
"""Show the about screen
"""
adminNickname = getConfigParam(baseDir, 'admin')
@ -40,7 +41,10 @@ def htmlAbout(cssCache: {}, baseDir: str, httpPrefix: str,
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
aboutForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
aboutForm = \
htmlHeaderWithWebsiteMarkup(cssFilename, instanceTitle,
httpPrefix, domainFull,
systemLanguage)
aboutForm += '<div class="container">' + aboutText + '</div>'
if onionDomain:
aboutForm += \

View File

@ -11,7 +11,7 @@ import time
from shutil import copyfile
from utils import getConfigParam
from utils import noOfAccounts
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlHeaderWithWebsiteMarkup
from webapp_utils import htmlFooter
from webapp_utils import htmlKeyboardNavigation
from theme import getTextModeLogo
@ -51,7 +51,10 @@ def htmlGetLoginCredentials(loginParams: str,
def htmlLogin(cssCache: {}, translate: {},
baseDir: str, autocomplete=True) -> str:
baseDir: str,
httpPrefix: str, domain: str,
systemLanguage: str,
autocomplete=True) -> str:
"""Shows the login screen
"""
accounts = noOfAccounts(baseDir)
@ -145,7 +148,10 @@ def htmlLogin(cssCache: {}, translate: {},
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
loginForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
loginForm = \
htmlHeaderWithWebsiteMarkup(cssFilename, instanceTitle,
httpPrefix, domain,
systemLanguage)
loginForm += '<br>\n'
loginForm += '<form method="POST" action="/login">\n'
loginForm += ' <div class="imgcontainer">\n'

View File

@ -740,8 +740,9 @@ def htmlProfile(rssIconAtTop: bool,
if 'T' in profileJson['published']:
joinedDate = profileJson['published']
occupationName = None
if profileJson.get('occupationName'):
occupationName = profileJson['occupationName']
if profileJson.get('hasOccupation'):
if profileJson['hasOccupation'].get('name'):
occupationName = profileJson['hasOccupation']['name']
avatarUrl = profileJson['icon']['url']
@ -1026,14 +1027,12 @@ def _htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
def _htmlProfileRoles(translate: {}, nickname: str, domain: str,
rolesJson: {}) -> str:
rolesList: []) -> str:
"""Shows roles on the profile screen
"""
profileStr = ''
for project, rolesList in rolesJson.items():
profileStr += \
'<div class="roles">\n<h2>' + project + \
'</h2>\n<div class="roles-inner">\n'
'<div class="roles">\n<div class="roles-inner">\n'
for role in rolesList:
if translate.get(role):
profileStr += '<h3>' + translate[role] + '</h3>\n'
@ -1601,8 +1600,9 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
editProfileForm += ' accept="' + imageFormats + '">\n'
occupationName = ''
if actorJson.get('occupationName'):
occupationName = actorJson['occupationName']
if actorJson.get('hasOccupation'):
if actorJson['hasOccupation'].get('name'):
occupationName = actorJson['hasOccupation']['name']
editProfileForm += '<label class="labels">' + \
translate['Occupation'] + ':</label><br>\n'

View File

@ -20,6 +20,8 @@ from utils import locatePost
from utils import isPublicPost
from utils import firstParagraphFromString
from utils import searchBoxPosts
from skills import noOfActorSkills
from skills import getSkillsFromString
from categories import getHashtagCategory
from feeds import rss2TagHeader
from feeds import rss2TagFooter
@ -414,11 +416,13 @@ def htmlSkillsSearch(actor: str,
actorJson = loadJson(actorFilename)
if actorJson:
if actorJson.get('id') and \
actorJson.get('skills') and \
noOfActorSkills(actorJson) > 0 and \
actorJson.get('name') and \
actorJson.get('icon'):
actor = actorJson['id']
for skillName, skillLevel in actorJson['skills'].items():
actorSkillsStr = actorJson['hasOccupation']['skills']
skills = getSkillsFromString(actorSkillsStr)
for skillName, skillLevel in skills.items():
skillName = skillName.lower()
if not (skillName in skillsearch or
skillsearch in skillName):
@ -453,12 +457,14 @@ def htmlSkillsSearch(actor: str,
if cachedActorJson.get('actor'):
actorJson = cachedActorJson['actor']
if actorJson.get('id') and \
actorJson.get('skills') and \
noOfActorSkills(actorJson) > 0 and \
actorJson.get('name') and \
actorJson.get('icon'):
actor = actorJson['id']
for skillName, skillLevel in \
actorJson['skills'].items():
actorSkillsStr = \
actorJson['hasOccupation']['skills']
skills = getSkillsFromString(actorSkillsStr)
for skillName, skillLevel in skills.items():
skillName = skillName.lower()
if not (skillName in skillsearch or
skillsearch in skillName):

View File

@ -720,17 +720,10 @@ def htmlHeaderWithPersonMarkup(cssFilename: str, instanceTitle: str,
return htmlStr
skillsMarkup = ''
if actorJson.get('skills'):
skillsStr = ''
for skillName, skillValue in actorJson['skills'].items():
if skillsStr:
skillsStr += ', ' + skillName
else:
skillsStr += skillName
if skillsStr:
occupationStr = ''
if actorJson.get('occupationName'):
occupationName = actorJson['occupationName']
if actorJson.get('hasOccupation'):
skillsStr = actorJson['hasOccupation']['skills']
if actorJson['hasOccupation'].get('name'):
occupationName = actorJson['hasOccupation']['name']
occupationStr = ' "name": "' + occupationName + '",\n'
skillsMarkup = \
' "hasOccupation": {\n' + \
@ -755,6 +748,45 @@ def htmlHeaderWithPersonMarkup(cssFilename: str, instanceTitle: str,
return htmlStr
def htmlHeaderWithWebsiteMarkup(cssFilename: str, instanceTitle: str,
httpPrefix: str, domain: str,
systemLanguage: str) -> str:
"""html header which includes website markup
https://schema.org/WebSite
"""
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle,
systemLanguage)
licenseUrl = 'https://www.gnu.org/licenses/agpl-3.0.en.html'
websiteMarkup = \
' <script type="application/ld+json">\n' + \
' {\n' + \
' "@context" : "http://schema.org",\n' + \
' "@type" : "WebSite",\n' + \
' "name": "' + instanceTitle + '",\n' + \
' "url": "' + httpPrefix + '://' + domain + '",\n' + \
' "license": "' + licenseUrl + '",\n' + \
' "inLanguage": "' + systemLanguage + '",\n' + \
' "isAccessibleForFree": true,\n' + \
' "genre": "https://en.wikipedia.org/wiki/Fediverse",\n' + \
' "accessMode": ["textual", "visual"],\n' + \
' "accessModeSufficient": ["textual"],\n' + \
' "accessibilityAPI" : ["ARIA"],\n' + \
' "accessibilityControl" : [\n' + \
' "fullKeyboardControl",\n' + \
' "fullTouchControl",\n' + \
' "fullMouseControl"\n' + \
' ],\n' + \
' "encodingFormat" : [\n' + \
' "text/html", "image/png", "image/webp",\n' + \
' "image/jpeg", "image/gif", "text/css"\n' + \
' ]\n' + \
' }\n' + \
' </script>\n'
htmlStr = htmlStr.replace('<head>\n', '<head>\n' + websiteMarkup)
return htmlStr
def htmlFooter() -> str:
htmlStr = ' </body>\n'
htmlStr += '</html>\n'