From 0f0818994ea4b869f609388ac88b3feab66739b6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 12:14:14 +0100 Subject: [PATCH 01/18] More standard occupation property --- daemon.py | 9 +++++---- person.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/daemon.py b/daemon.py index d72c34d61..7f820f302 100644 --- a/daemon.py +++ b/daemon.py @@ -4593,19 +4593,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) diff --git a/person.py b/person.py index 389a36026..4a7ac3c24 100644 --- a/person.py +++ b/person.py @@ -219,7 +219,8 @@ def getDefaultPersonContext() -> str: 'schema': 'http://schema.org#', 'suspended': 'toot:suspended', 'toot': 'http://joinmastodon.org/ns#', - 'value': 'schema:value' + 'value': 'schema:value', + 'Occupation': 'schema:Occupation' } @@ -296,7 +297,11 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'tts': personId + '/speaker', 'shares': personId + '/shares', 'orgSchema': None, - 'occupation': "", + 'hasOccupation': { + '@type': 'Occupation', + 'name': "", + 'skills': "", + }, 'skills': {}, 'roles': {}, 'availability': None, @@ -578,7 +583,26 @@ def personUpgradeActor(baseDir: str, personJson: {}, personJson['published'] = published updateActor = True + occupationName = '' + if personJson.get('occupation'): + occupationName = personJson['occupation'] + del personJson['occupation'] + + if not personJson.get('hasOccupation'): + personJson['hasOccupation'] = { + '@type': 'Occupation', + 'name': occupationName, + 'skills': "", + }, + 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 From 31bed2bdc54ce4b976b4233b07969d5621f528e1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 12:19:31 +0100 Subject: [PATCH 02/18] Tidying --- daemon.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/daemon.py b/daemon.py index 7f820f302..a91879a98 100644 --- a/daemon.py +++ b/daemon.py @@ -4154,22 +4154,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 From e126d1ab1f3b38cb4ed2a8d657d323b59ecc1567 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 12:26:05 +0100 Subject: [PATCH 03/18] Stray comma --- person.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/person.py b/person.py index 4a7ac3c24..786dc570b 100644 --- a/person.py +++ b/person.py @@ -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 """ @@ -296,7 +276,6 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'following': personId + '/following', 'tts': personId + '/speaker', 'shares': personId + '/shares', - 'orgSchema': None, 'hasOccupation': { '@type': 'Occupation', 'name': "", @@ -593,7 +572,7 @@ def personUpgradeActor(baseDir: str, personJson: {}, '@type': 'Occupation', 'name': occupationName, 'skills': "", - }, + } updateActor = True if updateActor: From 62d90006710783770e5e4d5f4bf9e24496f24f5f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 12:27:29 +0100 Subject: [PATCH 04/18] Remove occupation name --- person.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/person.py b/person.py index 786dc570b..60c064ae6 100644 --- a/person.py +++ b/person.py @@ -563,6 +563,9 @@ def personUpgradeActor(baseDir: str, personJson: {}, updateActor = True occupationName = '' + if personJson.get('occupationName'): + occupationName = personJson['occupationName'] + del personJson['occupationName'] if personJson.get('occupation'): occupationName = personJson['occupation'] del personJson['occupation'] From ccd02acfeb9a691267879c2620aa1d4f3a4ed261 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 12:35:36 +0100 Subject: [PATCH 05/18] Show occupation name --- webapp_profile.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/webapp_profile.py b/webapp_profile.py index e5b569978..ae42753a0 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -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'] @@ -1601,8 +1602,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 += '
\n' From 0cc86dc1311196289b3ef3d3c0b11abe74849ea0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 14:27:35 +0100 Subject: [PATCH 06/18] More standards compliant representation of skills --- daemon.py | 31 +++++++++------ person.py | 12 ++++-- skills.py | 101 +++++++++++++++++++++++++++++++++++++++++------ webapp_search.py | 16 +++++--- webapp_utils.py | 17 +++----- 5 files changed, 132 insertions(+), 45 deletions(-) diff --git a/daemon.py b/daemon.py index a91879a98..05c9eb297 100644 --- a/daemon.py +++ b/daemon.py @@ -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 @@ -4191,7 +4196,7 @@ class PubServer(BaseHTTPRequestHandler): # set skill levels skillCtr = 1 - newSkills = {} + actorSkillsCtr = noOfActorSkills(actorJson) while skillCtr < 10: skillName = \ fields.get('skillName' + str(skillCtr)) @@ -4206,21 +4211,20 @@ 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, 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'): @@ -7461,7 +7465,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, @@ -7486,6 +7490,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, @@ -7509,8 +7516,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) @@ -7523,7 +7529,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) diff --git a/person.py b/person.py index 60c064ae6..c029a81ec 100644 --- a/person.py +++ b/person.py @@ -279,9 +279,8 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'hasOccupation': { '@type': 'Occupation', 'name': "", - 'skills': "", + 'skills': "" }, - 'skills': {}, 'roles': {}, 'availability': None, 'icon': { @@ -317,7 +316,8 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, del newPerson['outbox'] del newPerson['icon'] del newPerson['image'] - del newPerson['skills'] + if newPerson.get('skills'): + del newPerson['skills'] del newPerson['shares'] del newPerson['roles'] del newPerson['tag'] @@ -574,10 +574,14 @@ def personUpgradeActor(baseDir: str, personJson: {}, personJson['hasOccupation'] = { '@type': 'Occupation', 'name': occupationName, - 'skills': "", + 'skills': "" } updateActor = True + if personJson.get('skills'): + del personJson['skills'] + updateActor = True + if updateActor: personJson['@context'] = [ 'https://www.w3.org/ns/activitystreams', diff --git a/skills.py b/skills.py index 0609bfd5a..00d258d09 100644 --- a/skills.py +++ b/skills.py @@ -15,7 +15,89 @@ from utils import getFullDomain from utils import getNicknameFromActor from utils import getDomainFromActor from utils import loadJson -from utils import saveJson + + +def _setSkillsFromDict(actorJson: {}, skills: {}) -> None: + """Converts a dict containing skills to a string + """ + skillsStr = '' + for name, value in skills.items(): + if skillsStr: + skillsStr += ', ' + skillsStr += name + ':' + str(value) + return skillsStr + + +def getSkillsFromString(skillsStr: str) -> {}: + """Returns a dict of skills from a string + """ + skillsList = skillsStr.split(',') + skills = {} + for skill in skillsList: + if ':' not in skill: + continue + name = skill.split(':')[0].strip().lower() + valueStr = skill.split(':')[1] + if not valueStr.isdigit(): + continue + skills[name] = int(valueStr) + return skills + + +def actorHasSkill(actorJson: {}, skillName: str) -> bool: + """Returns true if the actor has the given skill + """ + skills = getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skills: + return False + return skills.get(skillName.lower()) + + +def actorSkillValue(actorJson: {}, skillName: str) -> int: + """Returns The skill level from an actor + """ + skills = getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skills: + return 0 + skillName = skillName.lower() + if skills.get(skillName): + return skills[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 int(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 actorJson: + if not actorJson.get('hasOccupation'): + actorJson['hasOccupation'] = { + '@type': 'Occupation', + 'name': '', + 'skills': '' + } + skills = getSkillsFromString(actorJson['hasOccupation']['skills']) + if skillLevelPercent > 0: + skills[skill] = skillLevelPercent + else: + if skills.get(skill): + del skills[skill] + _setSkillsFromDict(actorJson, skills) + return True def setSkillLevel(baseDir: str, nickname: str, domain: str, @@ -30,15 +112,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 +125,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 +187,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, newSkillJson = { 'type': 'Skill', 'actor': actor, - 'object': '"'+skillStr+'"', + 'object': '"' + skillStr + '"', 'to': [toUrl], 'cc': [ccUrl] } diff --git a/webapp_search.py b/webapp_search.py index 7148e57d2..131d019a3 100644 --- a/webapp_search.py +++ b/webapp_search.py @@ -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): diff --git a/webapp_utils.py b/webapp_utils.py index 65c68b87f..ba812dccb 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -720,18 +720,11 @@ 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'] - occupationStr = ' "name": "' + occupationName + '",\n' + if actorJson.get('hasOccupation'): + skillsStr = actorJson['hasOccupation']['skills'] + if actorJson['hasOccupation'].get('name'): + occupationName = actorJson['hasOccupation']['name'] + occupationStr = ' "name": "' + occupationName + '",\n' skillsMarkup = \ ' "hasOccupation": {\n' + \ ' "@type": "Occupation",\n' + \ From 94675dd67355f9dbeb265dca61efde1b3cde9933 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 14:29:27 +0100 Subject: [PATCH 07/18] Length --- skills.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills.py b/skills.py index 00d258d09..5a6ac7870 100644 --- a/skills.py +++ b/skills.py @@ -71,7 +71,7 @@ def noOfActorSkills(actorJson: {}) -> int: if actorJson.get('hasOccupation'): skillsList = actorJson['hasOccupation']['skills'].split(',') if skillsList: - return int(skillsList) + return len(skillsList) return 0 From 40ed35c239223f7a2edca1dd8f0fe37571d7acc0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 14:31:42 +0100 Subject: [PATCH 08/18] Skill value --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 05c9eb297..9f4fdf87a 100644 --- a/daemon.py +++ b/daemon.py @@ -4217,7 +4217,7 @@ class PubServer(BaseHTTPRequestHandler): if actorSkillValue(actorJson, skillName) != \ int(skillValue): actorChanged = True - setActorSkillLevel(actorJson, skillName, skillValue) + setActorSkillLevel(actorJson, skillName, int(skillValue)) skillsStr = self.server.translate['Skills'] setHashtagCategory(baseDir, skillName, skillsStr.lower()) From 57015d6000303053b94272816e2db498dc22b61e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 15:13:27 +0100 Subject: [PATCH 09/18] Unit tests for skills functions --- daemon.py | 3 ++- skills.py | 58 ++++++++++++++++++++++++++++++------------------------- tests.py | 25 ++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/daemon.py b/daemon.py index 9f4fdf87a..82ada2b1f 100644 --- a/daemon.py +++ b/daemon.py @@ -4217,7 +4217,8 @@ class PubServer(BaseHTTPRequestHandler): if actorSkillValue(actorJson, skillName) != \ int(skillValue): actorChanged = True - setActorSkillLevel(actorJson, skillName, int(skillValue)) + setActorSkillLevel(actorJson, + skillName, int(skillValue)) skillsStr = self.server.translate['Skills'] setHashtagCategory(baseDir, skillName, skillsStr.lower()) diff --git a/skills.py b/skills.py index 5a6ac7870..0c13d4165 100644 --- a/skills.py +++ b/skills.py @@ -17,14 +17,16 @@ from utils import getDomainFromActor from utils import loadJson -def _setSkillsFromDict(actorJson: {}, skills: {}) -> None: +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 skills.items(): + for name, value in skillsDict.items(): if skillsStr: skillsStr += ', ' skillsStr += name + ':' + str(value) + actorJson['hasOccupation']['skills'] = skillsStr return skillsStr @@ -32,7 +34,7 @@ def getSkillsFromString(skillsStr: str) -> {}: """Returns a dict of skills from a string """ skillsList = skillsStr.split(',') - skills = {} + skillsDict = {} for skill in skillsList: if ':' not in skill: continue @@ -40,28 +42,30 @@ def getSkillsFromString(skillsStr: str) -> {}: valueStr = skill.split(':')[1] if not valueStr.isdigit(): continue - skills[name] = int(valueStr) - return skills + skillsDict[name] = int(valueStr) + return skillsDict def actorHasSkill(actorJson: {}, skillName: str) -> bool: """Returns true if the actor has the given skill """ - skills = getSkillsFromString(actorJson['hasOccupation']['skills']) - if not skills: + skillsDict = \ + getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skillsDict: return False - return skills.get(skillName.lower()) + return skillsDict.get(skillName.lower()) def actorSkillValue(actorJson: {}, skillName: str) -> int: """Returns The skill level from an actor """ - skills = getSkillsFromString(actorJson['hasOccupation']['skills']) - if not skills: + skillsDict = \ + getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skillsDict: return 0 skillName = skillName.lower() - if skills.get(skillName): - return skills[skillName] + if skillsDict.get(skillName): + return skillsDict[skillName] return 0 @@ -83,20 +87,22 @@ def setActorSkillLevel(actorJson: {}, if skillLevelPercent < 0 or skillLevelPercent > 100: return False - if actorJson: - if not actorJson.get('hasOccupation'): - actorJson['hasOccupation'] = { - '@type': 'Occupation', - 'name': '', - 'skills': '' - } - skills = getSkillsFromString(actorJson['hasOccupation']['skills']) - if skillLevelPercent > 0: - skills[skill] = skillLevelPercent - else: - if skills.get(skill): - del skills[skill] - _setSkillsFromDict(actorJson, skills) + 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 diff --git a/tests.py b/tests.py index 541eb52af..e4b2300d0 100644 --- a/tests.py +++ b/tests.py @@ -67,6 +67,8 @@ 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 setRole from roles import outboxDelegate from auth import constantTimeStringCheck @@ -3731,9 +3733,32 @@ 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 runAllTests(): print('Running tests...') testFunctions() + testSkills() testSpoofGeolocation() testRemovePostInteractions() testExtractPGPPublicKey() From 7e48beb0fe813f04b8829964a0b31eb7c2b4b1be Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 20:58:16 +0100 Subject: [PATCH 10/18] Remove role delegation The keeps the handling of roles very simple --- README_commandline.md | 46 ------- city.py | 5 +- daemon.py | 26 ++-- epicyon.py | 111 +---------------- outbox.py | 7 +- person.py | 40 ++++-- roles.py | 281 ++++++++---------------------------------- tests.py | 104 ++++------------ webapp_profile.py | 20 ++- 9 files changed, 140 insertions(+), 500 deletions(-) diff --git a/README_commandline.md b/README_commandline.md index af39f363d..9529671b9 100644 --- a/README_commandline.md +++ b/README_commandline.md @@ -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 diff --git a/city.py b/city.py index d34142908..3ea9983b7 100644 --- a/city.py +++ b/city.py @@ -128,7 +128,7 @@ def _getCityPulse(currTimeOfDay, decoySeed: int) -> (float, float): def spoofGeolocation(baseDir: str, city: str, currTime, decoySeed: int, - citiesList: []) -> (float, float, str, str, + citiesList: []) -> (float, float, str, str, \ str, str, int): """Given a city and the current time spoofs the location for an image @@ -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() diff --git a/daemon.py b/daemon.py index 82ada2b1f..023790a9c 100644 --- a/daemon.py +++ b/daemon.py @@ -124,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 @@ -4732,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+") @@ -4757,7 +4758,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(modDir): setRole(baseDir, modNick, domain, - 'instance', 'moderator') # change site editors list @@ -4789,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+") @@ -4814,7 +4814,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'editor') # change site counselors list @@ -4846,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+") @@ -4871,7 +4870,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'counselor') # remove scheduled posts @@ -7376,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', ''), @@ -7397,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, @@ -7420,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) @@ -7433,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) diff --git a/epicyon.py b/epicyon.py index c7c6a4438..ab5997893 100644 --- a/epicyon.py +++ b/epicyon.py @@ -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, diff --git a/outbox.py b/outbox.py index 8da8fa6d2..8f5562d62 100644 --- a/outbox.py +++ b/outbox.py @@ -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) diff --git a/person.py b/person.py index c029a81ec..c6ca823e3 100644 --- a/person.py +++ b/person.py @@ -200,7 +200,9 @@ def getDefaultPersonContext() -> str: 'suspended': 'toot:suspended', 'toot': 'http://joinmastodon.org/ns#', 'value': 'schema:value', - 'Occupation': 'schema:Occupation' + 'Occupation': 'schema:Occupation', + 'OrganizationRole': 'schema:OrganizationRole', + 'WebSite': 'schema:Project' } @@ -281,7 +283,15 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'name': "", 'skills': "" }, - 'roles': {}, + "affiliation": { + "@type": "OrganizationRole", + "roleName": "", + "affiliation": { + "@type": "WebSite", + "url": httpPrefix + '://' + domain + }, + "startDate": published + }, 'availability': None, 'icon': { 'mediaType': 'image/png', @@ -319,7 +329,8 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, if newPerson.get('skills'): del newPerson['skills'] del newPerson['shares'] - del newPerson['roles'] + if newPerson.get('roles'): + del newPerson['roles'] del newPerson['tag'] del newPerson['availability'] del newPerson['followers'] @@ -463,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') @@ -582,6 +592,22 @@ def personUpgradeActor(baseDir: str, personJson: {}, del personJson['skills'] updateActor = True + if not personJson.get('affiliation'): + personJson['affiliation'] = { + "@type": "OrganizationRole", + "roleName": "", + "affiliation": { + "@type": "WebSite", + "url": personJson['id'].split('/users/')[0] + }, + "startDate": published + } + updateActor = True + + if personJson.get('roles'): + del personJson['roles'] + updateActor = True + if updateActor: personJson['@context'] = [ 'https://www.w3.org/ns/activitystreams', diff --git a/roles.py b/roles.py index f198fe7f0..0ce857d49 100644 --- a/roles.py +++ b/roles.py @@ -37,10 +37,13 @@ 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) - saveJson(actorJson, filename) + if not actorJson.get('affiliation'): + continue + rolesList = \ + getRolesFromString(actorJson['affiliation']['roleName']) + if role in rolesList: + rolesList.remove(role) + saveJson(actorJson, filename) def clearEditorStatus(baseDir: str) -> None: @@ -112,13 +115,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 +158,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 roleFiles.get(role): + _addRole(baseDir, nickname, domain, roleFiles[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] - saveJson(actorJson, actorFilename) + if roleFiles.get(role): + _removeRole(baseDir, nickname, roleFiles[role]) + 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 diff --git a/tests.py b/tests.py index e4b2300d0..1b53f0f2e 100644 --- a/tests.py +++ b/tests.py @@ -69,8 +69,9 @@ from person import setBio 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 @@ -454,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) @@ -558,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) @@ -1411,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() @@ -3755,9 +3680,31 @@ def testSkills() -> None: 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() @@ -3812,5 +3759,4 @@ def runAllTests(): testNoOfFollowersOnDomain() testFollows() testGroupFollowers() - testDelegateRoles() print('Tests succeeded\n') diff --git a/webapp_profile.py b/webapp_profile.py index ae42753a0..8b91ba5d6 100644 --- a/webapp_profile.py +++ b/webapp_profile.py @@ -1027,20 +1027,18 @@ 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 += \ - '
\n

' + project + \ - '

\n
\n' - for role in rolesList: - if translate.get(role): - profileStr += '

' + translate[role] + '

\n' - else: - profileStr += '

' + role + '

\n' - profileStr += '
\n' + profileStr += \ + '
\n
\n' + for role in rolesList: + if translate.get(role): + profileStr += '

' + translate[role] + '

\n' + else: + profileStr += '

' + role + '

\n' + profileStr += '
\n' if len(profileStr) == 0: profileStr += \ '

@' + nickname + '@' + domain + ' has no roles assigned

\n' From e58e7ce05945e4e870577e82215b3792fbb23143 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 21:00:40 +0100 Subject: [PATCH 11/18] Published date --- person.py | 1 + 1 file changed, 1 insertion(+) diff --git a/person.py b/person.py index c6ca823e3..2dc42214c 100644 --- a/person.py +++ b/person.py @@ -593,6 +593,7 @@ def personUpgradeActor(baseDir: str, personJson: {}, updateActor = True if not personJson.get('affiliation'): + statusNumber, published = getStatusNumber() personJson['affiliation'] = { "@type": "OrganizationRole", "roleName": "", From 0b19087c88400fc2acab8df57608dbfd225ed997 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 21:16:43 +0100 Subject: [PATCH 12/18] Fix unit tests --- city.py | 2 +- person.py | 6 +++++- roles.py | 7 ------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/city.py b/city.py index 3ea9983b7..004b2b328 100644 --- a/city.py +++ b/city.py @@ -128,7 +128,7 @@ def _getCityPulse(currTimeOfDay, decoySeed: int) -> (float, float): def spoofGeolocation(baseDir: str, city: str, currTime, decoySeed: int, - citiesList: []) -> (float, float, str, str, \ + citiesList: []) -> (float, float, str, str, str, str, int): """Given a city and the current time spoofs the location for an image diff --git a/person.py b/person.py index 2dc42214c..6c28c13a5 100644 --- a/person.py +++ b/person.py @@ -593,10 +593,14 @@ def personUpgradeActor(baseDir: str, personJson: {}, updateActor = True if not personJson.get('affiliation'): + rolesStr = '' + adminName = getConfigParam(baseDir, 'admin') + if personJson['id'].endswith('/users/' + adminName): + rolesStr = 'admin' statusNumber, published = getStatusNumber() personJson['affiliation'] = { "@type": "OrganizationRole", - "roleName": "", + "roleName": rolesStr, "affiliation": { "@type": "WebSite", "url": personJson['id'].split('/users/')[0] diff --git a/roles.py b/roles.py index 0ce857d49..1eba0fa10 100644 --- a/roles.py +++ b/roles.py @@ -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 From ba0ec266d7d5fa4fcafee48a62ed0c2e3e6af0db Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 13 May 2021 21:21:37 +0100 Subject: [PATCH 13/18] Comments --- person.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/person.py b/person.py index 6c28c13a5..7195d70a9 100644 --- a/person.py +++ b/person.py @@ -580,6 +580,8 @@ def personUpgradeActor(baseDir: str, personJson: {}, 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', @@ -588,15 +590,18 @@ def personUpgradeActor(baseDir: str, personJson: {}, } 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' + rolesStr = 'admin, moderator, editor' statusNumber, published = getStatusNumber() personJson['affiliation'] = { "@type": "OrganizationRole", @@ -609,6 +614,16 @@ def personUpgradeActor(baseDir: str, personJson: {}, } 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 From 5458aca79412a85958badbc4c7abcf3df41ec9e4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 14 May 2021 12:27:08 +0100 Subject: [PATCH 14/18] Accessibility metadata on login screen --- daemon.py | 11 +++++++++-- webapp_login.py | 12 +++++++++--- webapp_utils.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/daemon.py b/daemon.py index 023790a9c..3ef9c713c 100644 --- a/daemon.py +++ b/daemon.py @@ -10572,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) @@ -11677,7 +11681,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) diff --git a/webapp_login.py b/webapp_login.py index 9e5fa6206..c549d75f4 100644 --- a/webapp_login.py +++ b/webapp_login.py @@ -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 += '
\n' loginForm += '
\n' loginForm += '
\n' diff --git a/webapp_utils.py b/webapp_utils.py index ba812dccb..5c6c257cb 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -748,6 +748,44 @@ 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 = \ + ' \n' + htmlStr = htmlStr.replace('\n', '\n' + websiteMarkup) + return htmlStr + + def htmlFooter() -> str: htmlStr = ' \n' htmlStr += '\n' From 02d78be5f995c1dae18dded022b7f91d726793a1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 14 May 2021 12:29:20 +0100 Subject: [PATCH 15/18] Extra comma --- webapp_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp_utils.py b/webapp_utils.py index 5c6c257cb..a7fc83301 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -779,7 +779,7 @@ def htmlHeaderWithWebsiteMarkup(cssFilename: str, instanceTitle: str, ' "encodingFormat" : [\n' + \ ' "text/html", "image/png", "image/webp",\n' + \ ' "image/jpeg", "image/gif", "text/css"\n' + \ - ' ],\n' + \ + ' ]\n' + \ ' }\n' + \ ' \n' htmlStr = htmlStr.replace('\n', '\n' + websiteMarkup) From 91736b547f4c30050b6017854cc5f6439e80ac45 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 14 May 2021 12:30:05 +0100 Subject: [PATCH 16/18] Remove spaces --- webapp_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp_utils.py b/webapp_utils.py index a7fc83301..04d19962e 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -770,7 +770,7 @@ def htmlHeaderWithWebsiteMarkup(cssFilename: str, instanceTitle: str, ' "genre": "https://en.wikipedia.org/wiki/Fediverse",\n' + \ ' "accessMode": ["textual", "visual"],\n' + \ ' "accessModeSufficient": ["textual"],\n' + \ - ' "accessibilityAPI" : [ "ARIA" ],\n' + \ + ' "accessibilityAPI" : ["ARIA"],\n' + \ ' "accessibilityControl" : [\n' + \ ' "fullKeyboardControl",\n' + \ ' "fullTouchControl",\n' + \ From 80203466b66b2066a368954859d6833552577c92 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 14 May 2021 12:56:23 +0100 Subject: [PATCH 17/18] Include metadata on about screen --- daemon.py | 9 ++++++--- webapp_about.py | 10 +++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/daemon.py b/daemon.py index 3ef9c713c..1b616ddf6 100644 --- a/daemon.py +++ b/daemon.py @@ -11206,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, @@ -11220,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) diff --git a/webapp_about.py b/webapp_about.py index 4f9b5f8bb..3a617232a 100644 --- a/webapp_about.py +++ b/webapp_about.py @@ -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 += '
' + aboutText + '
' if onionDomain: aboutForm += \ From 76ea1f6a29be3a52baedbb3f150813e1ff2b1b55 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Fri, 14 May 2021 13:24:21 +0100 Subject: [PATCH 18/18] Add website url --- webapp_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp_utils.py b/webapp_utils.py index 04d19962e..6fdfbe1ea 100644 --- a/webapp_utils.py +++ b/webapp_utils.py @@ -764,6 +764,7 @@ def htmlHeaderWithWebsiteMarkup(cssFilename: str, instanceTitle: str, ' "@context" : "http://schema.org",\n' + \ ' "@type" : "WebSite",\n' + \ ' "name": "' + instanceTitle + '",\n' + \ + ' "url": "' + httpPrefix + '://' + domain + '",\n' + \ ' "license": "' + licenseUrl + '",\n' + \ ' "inLanguage": "' + systemLanguage + '",\n' + \ ' "isAccessibleForFree": true,\n' + \