Notes on skills

master
Bob Mottram 2019-07-19 11:01:24 +01:00
parent 12072b57e1
commit 770edd001d
7 changed files with 214 additions and 65 deletions

View File

@ -296,32 +296,6 @@ If you want old posts to be deleted for data minimization purposes then the arch
python3 epicyon.py --archive /dev/null --archiveweeks 4 --maxposts 256 python3 epicyon.py --archive /dev/null --archiveweeks 4 --maxposts 256
``` ```
## Roles and skills
To build crowdsourced organizations you might want to assign roles and skills to individuals. This can be done with some command options:
To assign a role to someone:
``` bash
python3 epicyon.py --nickname susan --domain somedomain.net --project "instance" --role "moderator"
```
Individuals can be assigned to multiple roles within multiple projects if needed.
You might also want to advertize that you have various skills. Skill levels are a percentage value.
``` bash
python3 epicyon.py --nickname jon --domain somedomain.net --skill "Dressmaking" --level 60
```
You can also set a busy status to define whether people are available for new tasks.
``` bash
python3 epicyon.py --nickname eva --domain somedomain.net --availability ready
```
With roles, skills and availability defined tasks may then be automatically assigned to the relevant people, or load balanced as volunteers come and go and complete pieces of work. Orgbots may collect that information and rapidly assemble a viable organization from predefined schemas. This is the way to produce effective non-hierarchical organizations which are also transient with no fixed bureaucracy.
## Blocking and unblocking ## Blocking and unblocking
Whether you are using the **--federate** option to define a set of allowed instances or not, you may want to block particular accounts even inside of the perimeter. To block an account: Whether you are using the **--federate** option to define a set of allowed instances or not, you may want to block particular accounts even inside of the perimeter. To block an account:
@ -403,7 +377,7 @@ python3 epicyon.py --nickname [admin nickname] --domain [mydomain] \
--password [c2s password] --password [c2s password]
``` ```
This extends the ActivityPub client-to-server protocol to include an activities called *Delegate* and *Role*. The json looks like: This extends the ActivityPub client-to-server protocol to include activities called *Delegate* and *Role*. The json looks like:
``` json ``` json
{ 'type': 'Delegate', { 'type': 'Delegate',
@ -421,6 +395,26 @@ This extends the ActivityPub client-to-server protocol to include an activities
Projects and roles are only scoped within a single instance. There presently are not enough security mechanisms to support multi-instance distributed organizations. 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
To help create organizations you can assign some skills to your account. Note that you can only assign skills to yourself and not to other people. The command is:
``` bash
python3 epicyon.py --nickname [nick] --domain [mydomain] \
--skill [tag] --level [0-100] \
--password [c2s password]
```
This extends the ActivityPub client-to-server protocol to include an activity called *Skill*. The json looks like:
``` json
{ 'type': 'Skill',
'actor': https://'+somedomain/users/somenickname,
'object': gardening;80,
'to': [],
'cc': []}
```
## Object Capabilities Security ## Object Capabilities Security
A description of the proposed object capabilities model [is here](ocaps.md). A description of the proposed object capabilities model [is here](ocaps.md).

View File

@ -42,6 +42,7 @@ from blocking import outboxBlock
from blocking import outboxUndoBlock from blocking import outboxUndoBlock
from config import setConfigParam from config import setConfigParam
from roles import outboxDelegate from roles import outboxDelegate
from skills import outboxSkills
import os import os
import sys import sys
@ -174,7 +175,7 @@ class PubServer(BaseHTTPRequestHandler):
permittedOutboxTypes=[ permittedOutboxTypes=[
'Create','Announce','Like','Follow','Undo', \ 'Create','Announce','Like','Follow','Undo', \
'Update','Add','Remove','Block','Delete', \ 'Update','Add','Remove','Block','Delete', \
'Delegate' 'Delegate','Skill'
] ]
if messageJson['type'] not in permittedOutboxTypes: if messageJson['type'] not in permittedOutboxTypes:
if self.server.debug: if self.server.debug:
@ -222,7 +223,10 @@ class PubServer(BaseHTTPRequestHandler):
outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug) outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
if self.server.debug: if self.server.debug:
print('DEBUG: handle delegation requests') print('DEBUG: handle delegation requests')
outboxDelegate(self.server.baseDir,messageJson,self.server.debug) outboxDelegate(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle skills changes requestsw')
outboxSkills(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
if self.server.debug: if self.server.debug:
print('DEBUG: handle any like requests') print('DEBUG: handle any like requests')
outboxLike(self.server.baseDir,self.server.httpPrefix, \ outboxLike(self.server.baseDir,self.server.httpPrefix, \

View File

@ -14,7 +14,7 @@ from person import setPreferredNickname
from person import setBio from person import setBio
from person import validNickname from person import validNickname
from person import setProfileImage from person import setProfileImage
from person import setSkillLevel from skills import setSkillLevel
from roles import setRole from roles import setRole
from person import setAvailability from person import setAvailability
from person import setOrganizationScheme from person import setOrganizationScheme
@ -68,6 +68,7 @@ from like import sendUndoLikeViaServer
from blocking import sendBlockViaServer from blocking import sendBlockViaServer
from blocking import sendUndoBlockViaServer from blocking import sendUndoBlockViaServer
from roles import sendRoleViaServer from roles import sendRoleViaServer
from skills import sendSkillViaServer
import argparse import argparse
def str2bool(v): def str2bool(v):
@ -753,14 +754,36 @@ if args.project:
sys.exit() sys.exit()
if args.skill: if args.skill:
if args.skillLevelPercent==0: if not nickname:
args.skillLevelPercent=None print('Specify a nickname with the --nickname option')
if args.skillLevelPercent: sys.exit()
if setSkillLevel(baseDir,nickname,domain,args.skill,args.skillLevelPercent):
print('Skill level for '+args.skill+' set to '+str(args.skillLevelPercent)+'%') if not args.password:
else: print('Specify a password with the --password option')
if setSkillLevel(baseDir,nickname,domain,args.skill,args.skillLevelPercent): sys.exit()
print('Skill '+args.skill+' removed')
if not args.skillLevelPercent:
print('Specify a skill level in the range 0-100')
sys.exit()
if int(args.skillLevelPercent)<0 or int(args.skillLevelPercent)>100:
print('Skill level should be a percentage in the range 0-100')
sys.exit()
session = createSession(domain,port,useTor)
personCache={}
cachedWebfingers={}
print('Sending '+args.skill+' skill level '+str(args.skillLevelPercent)+' for '+nickname)
sendSkillViaServer(session,nickname,args.password,
domain,port, \
httpPrefix, \
args.skill,args.skillLevelPercent, \
cachedWebfingers,personCache, \
True)
for i in range(10):
# TODO detect send success/fail
time.sleep(1)
sys.exit() sys.exit()
if federationList: if federationList:

View File

@ -87,28 +87,6 @@ def setProfileImage(baseDir: str,httpPrefix :str,nickname: str,domain: str, \
return True return True
return False return False
def setSkillLevel(baseDir: str,nickname: str,domain: str, \
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
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return False
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
if not actorJson.get('skills'):
actorJson['skills']={}
if skillLevelPercent>0:
actorJson['skills'][skill]=skillLevelPercent
else:
del actorJson['skills'][skill]
with open(actorFilename, 'w') as fp:
commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
return True
def setOrganizationScheme(baseDir: str,nickname: str,domain: str, \ def setOrganizationScheme(baseDir: str,nickname: str,domain: str, \
schema: str) -> bool: schema: str) -> bool:
"""Set the organization schema within which a person exists """Set the organization schema within which a person exists

View File

@ -61,7 +61,7 @@ def getRoles(baseDir: str,nickname: str,domain: str, \
return actorJson['roles'][project] return actorJson['roles'][project]
return None return None
def outboxDelegate(baseDir: str,messageJson: {},debug: bool) -> bool: def outboxDelegate(baseDir: str,authenticatedNickname: str,messageJson: {},debug: bool) -> bool:
"""Handles receiving a delegation request """Handles receiving a delegation request
""" """
if not messageJson.get('type'): if not messageJson.get('type'):
@ -87,6 +87,8 @@ def outboxDelegate(baseDir: str,messageJson: {},debug: bool) -> bool:
return False return False
delegatorNickname=getNicknameFromActor(messageJson['actor']) delegatorNickname=getNicknameFromActor(messageJson['actor'])
if delegatorNickname!=authenticatedNickname:
return
domain,port=getDomainFromActor(messageJson['actor']) domain,port=getDomainFromActor(messageJson['actor'])
project=messageJson['object']['object'].split(';')[0].strip() project=messageJson['object']['object'].split(';')[0].strip()

148
skills.py 100644
View File

@ -0,0 +1,148 @@
__filename__ = "skills.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "0.0.1"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
import commentjson
import os
from webfinger import webfingerHandle
from auth import createBasicAuthHeader
from posts import getPersonBox
from session import postJson
from utils import getNicknameFromActor
from utils import getDomainFromActor
def setSkillLevel(baseDir: str,nickname: str,domain: str, \
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
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return False
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
if not actorJson.get('skills'):
actorJson['skills']={}
if skillLevelPercent>0:
actorJson['skills'][skill]=skillLevelPercent
else:
del actorJson['skills'][skill]
with open(actorFilename, 'w') as fp:
commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
return True
def getSkills(baseDir: str,nickname: str,domain: str) -> []:
"""Returns the skills for a given person
"""
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return False
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
if not actorJson.get('skills'):
return None
return actorJson['skills']
return None
def outboxSkills(baseDir: str,nickname: str,messageJson: {},debug: bool) -> bool:
"""Handles receiving a skills update
"""
if not messageJson.get('type'):
return False
if not messageJson['type']=='Skill':
return False
if not messageJson.get('actor'):
return False
if not messageJson.get('object'):
return False
if not isinstance(messageJson['object'], str):
return False
actorNickname=getNicknameFromActor(messageJson['actor'])
if actorNickname!=nickname:
return False
domain,port=getDomainFromActor(messageJson['actor'])
skill=messageJson['object'].split(';')[0].strip()
skillLevelPercent=messageJson['object'].split(';')[1].strip()
return setSkillLevel(baseDir,nickname,domain, \
skill,skillLevelPercent)
def sendSkillViaServer(session,nickname: str,password: str,
domain: str,port: int, \
httpPrefix: str, \
skill: str,skillLevelPercent: int, \
cachedWebfingers: {},personCache: {}, \
debug: bool) -> {}:
"""Sets a skill for a person via c2s
"""
if not session:
print('WARN: No session for sendSkillViaServer')
return 6
domainFull=domain
if port!=80 and port!=443:
domainFull=domain+':'+str(port)
toUrl = httpPrefix+'://'+domainFull+'/users/'+nickname
ccUrl = httpPrefix+'://'+domainFull+'/users/'+Nickname+'/followers'
if skillLevelPercent:
skillStr=skill+';'+str(skillLevelPercent)
else:
skillStr=skill+';0'
newSkillJson = {
'type': 'Skill',
'actor': httpPrefix+'://'+domainFull+'/users/'+nickname,
'object': skill+';'+str(skillLevelPercent),
'to': [toUrl],
'cc': [ccUrl]
}
handle=httpPrefix+'://'+domainFull+'/@'+nickname
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers)
if not wfRequest:
if debug:
print('DEBUG: announce webfinger failed for '+handle)
return 1
postToBox='outbox'
# get the actor inbox for the To handle
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition = \
getPersonBox(session,wfRequest,personCache,postToBox)
if not inboxUrl:
if debug:
print('DEBUG: No '+postToBox+' was found for '+handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: No actor was found for '+handle)
return 4
authHeader=createBasicAuthHeader(Nickname,password)
headers = {'host': domain, \
'Content-type': 'application/json', \
'Authorization': authHeader}
postResult = \
postJson(session,newSkillJson,[],inboxUrl,headers,"inbox:write")
#if not postResult:
# if debug:
# print('DEBUG: POST announce failed for c2s to '+inboxUrl)
# return 5
if debug:
print('DEBUG: c2s POST skill success')
return newSkillJson

View File

@ -43,7 +43,7 @@ from follow import sendFollowRequest
from person import createPerson from person import createPerson
from person import setPreferredNickname from person import setPreferredNickname
from person import setBio from person import setBio
from person import setSkillLevel from skills import setSkillLevel
from roles import setRole from roles import setRole
from roles import getRoles from roles import getRoles
from roles import outboxDelegate from roles import outboxDelegate
@ -974,9 +974,9 @@ def testDelegateRoles():
'cc': [] 'cc': []
} }
assert outboxDelegate(baseDir,newRoleJson,False) assert outboxDelegate(baseDir,nickname,newRoleJson,False)
# second time delegation has already happened so should return false # second time delegation has already happened so should return false
assert outboxDelegate(baseDir,newRoleJson,False)==False assert outboxDelegate(baseDir,nickname,newRoleJson,False)==False
assert '"delegator"' in open(baseDir+'/accounts/'+nickname+'@'+domain+'.json').read() assert '"delegator"' in open(baseDir+'/accounts/'+nickname+'@'+domain+'.json').read()
assert '"delegator"' in open(baseDir+'/accounts/'+nicknameDelegated+'@'+domain+'.json').read() assert '"delegator"' in open(baseDir+'/accounts/'+nicknameDelegated+'@'+domain+'.json').read()
@ -996,7 +996,7 @@ def testDelegateRoles():
} }
# non-delegators cannot assign roles # non-delegators cannot assign roles
assert outboxDelegate(baseDir,newRoleJson,False)==False assert outboxDelegate(baseDir,nicknameDelegated,newRoleJson,False)==False
assert '"otherrole"' not in open(baseDir+'/accounts/'+nickname+'@'+domain+'.json').read() assert '"otherrole"' not in open(baseDir+'/accounts/'+nickname+'@'+domain+'.json').read()
os.chdir(currDir) os.chdir(currDir)