forked from indymedia/epicyon
Notes on skills
parent
12072b57e1
commit
770edd001d
48
README.md
48
README.md
|
@ -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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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]
|
||||
```
|
||||
|
||||
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
|
||||
{ '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.
|
||||
|
||||
## 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
|
||||
|
||||
A description of the proposed object capabilities model [is here](ocaps.md).
|
|
@ -42,6 +42,7 @@ from blocking import outboxBlock
|
|||
from blocking import outboxUndoBlock
|
||||
from config import setConfigParam
|
||||
from roles import outboxDelegate
|
||||
from skills import outboxSkills
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
@ -174,7 +175,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
permittedOutboxTypes=[
|
||||
'Create','Announce','Like','Follow','Undo', \
|
||||
'Update','Add','Remove','Block','Delete', \
|
||||
'Delegate'
|
||||
'Delegate','Skill'
|
||||
]
|
||||
if messageJson['type'] not in permittedOutboxTypes:
|
||||
if self.server.debug:
|
||||
|
@ -222,7 +223,10 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
|
||||
if self.server.debug:
|
||||
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:
|
||||
print('DEBUG: handle any like requests')
|
||||
outboxLike(self.server.baseDir,self.server.httpPrefix, \
|
||||
|
|
41
epicyon.py
41
epicyon.py
|
@ -14,7 +14,7 @@ from person import setPreferredNickname
|
|||
from person import setBio
|
||||
from person import validNickname
|
||||
from person import setProfileImage
|
||||
from person import setSkillLevel
|
||||
from skills import setSkillLevel
|
||||
from roles import setRole
|
||||
from person import setAvailability
|
||||
from person import setOrganizationScheme
|
||||
|
@ -68,6 +68,7 @@ from like import sendUndoLikeViaServer
|
|||
from blocking import sendBlockViaServer
|
||||
from blocking import sendUndoBlockViaServer
|
||||
from roles import sendRoleViaServer
|
||||
from skills import sendSkillViaServer
|
||||
import argparse
|
||||
|
||||
def str2bool(v):
|
||||
|
@ -753,14 +754,36 @@ if args.project:
|
|||
sys.exit()
|
||||
|
||||
if args.skill:
|
||||
if args.skillLevelPercent==0:
|
||||
args.skillLevelPercent=None
|
||||
if args.skillLevelPercent:
|
||||
if setSkillLevel(baseDir,nickname,domain,args.skill,args.skillLevelPercent):
|
||||
print('Skill level for '+args.skill+' set to '+str(args.skillLevelPercent)+'%')
|
||||
else:
|
||||
if setSkillLevel(baseDir,nickname,domain,args.skill,args.skillLevelPercent):
|
||||
print('Skill '+args.skill+' removed')
|
||||
if not nickname:
|
||||
print('Specify a nickname with the --nickname option')
|
||||
sys.exit()
|
||||
|
||||
if not args.password:
|
||||
print('Specify a password with the --password option')
|
||||
sys.exit()
|
||||
|
||||
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()
|
||||
|
||||
if federationList:
|
||||
|
|
22
person.py
22
person.py
|
@ -87,28 +87,6 @@ def setProfileImage(baseDir: str,httpPrefix :str,nickname: str,domain: str, \
|
|||
return True
|
||||
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, \
|
||||
schema: str) -> bool:
|
||||
"""Set the organization schema within which a person exists
|
||||
|
|
4
roles.py
4
roles.py
|
@ -61,7 +61,7 @@ def getRoles(baseDir: str,nickname: str,domain: str, \
|
|||
return actorJson['roles'][project]
|
||||
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
|
||||
"""
|
||||
if not messageJson.get('type'):
|
||||
|
@ -87,6 +87,8 @@ def outboxDelegate(baseDir: str,messageJson: {},debug: bool) -> bool:
|
|||
return False
|
||||
|
||||
delegatorNickname=getNicknameFromActor(messageJson['actor'])
|
||||
if delegatorNickname!=authenticatedNickname:
|
||||
return
|
||||
domain,port=getDomainFromActor(messageJson['actor'])
|
||||
project=messageJson['object']['object'].split(';')[0].strip()
|
||||
|
||||
|
|
|
@ -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
|
8
tests.py
8
tests.py
|
@ -43,7 +43,7 @@ from follow import sendFollowRequest
|
|||
from person import createPerson
|
||||
from person import setPreferredNickname
|
||||
from person import setBio
|
||||
from person import setSkillLevel
|
||||
from skills import setSkillLevel
|
||||
from roles import setRole
|
||||
from roles import getRoles
|
||||
from roles import outboxDelegate
|
||||
|
@ -974,9 +974,9 @@ def testDelegateRoles():
|
|||
'cc': []
|
||||
}
|
||||
|
||||
assert outboxDelegate(baseDir,newRoleJson,False)
|
||||
assert outboxDelegate(baseDir,nickname,newRoleJson,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/'+nicknameDelegated+'@'+domain+'.json').read()
|
||||
|
@ -996,7 +996,7 @@ def testDelegateRoles():
|
|||
}
|
||||
|
||||
# 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()
|
||||
|
||||
os.chdir(currDir)
|
||||
|
|
Loading…
Reference in New Issue