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..004b2b328 100644 --- a/city.py +++ b/city.py @@ -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 d72c34d61..1b616ddf6 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 @@ -119,6 +124,7 @@ from blocking import isBlockedHashtag from blocking import isBlockedDomain from blocking import getDomainBlocklist from roles import setRole +from roles import getRolesFromString from roles import clearModeratorStatus from roles import clearEditorStatus from roles import clearCounselorStatus @@ -4154,22 +4160,6 @@ class PubServer(BaseHTTPRequestHandler): # which isn't implemented in Epicyon actorJson['discoverable'] = True actorChanged = True - if not actorJson['@context'][2].get('orgSchema'): - actorJson['@context'][2]['orgSchema'] = \ - 'toot:orgSchema' - actorChanged = True - if not actorJson['@context'][2].get('skills'): - actorJson['@context'][2]['skills'] = 'toot:skills' - actorChanged = True - if not actorJson['@context'][2].get('shares'): - actorJson['@context'][2]['shares'] = 'toot:shares' - actorChanged = True - if not actorJson['@context'][2].get('roles'): - actorJson['@context'][2]['roles'] = 'toot:roles' - actorChanged = True - if not actorJson['@context'][2].get('availability'): - actorJson['@context'][2]['availaibility'] = \ - 'toot:availability' if actorJson.get('capabilityAcquisitionEndpoint'): del actorJson['capabilityAcquisitionEndpoint'] actorChanged = True @@ -4207,7 +4197,7 @@ class PubServer(BaseHTTPRequestHandler): # set skill levels skillCtr = 1 - newSkills = {} + actorSkillsCtr = noOfActorSkills(actorJson) while skillCtr < 10: skillName = \ fields.get('skillName' + str(skillCtr)) @@ -4222,21 +4212,21 @@ class PubServer(BaseHTTPRequestHandler): if not skillValue: skillCtr += 1 continue - if not actorJson['skills'].get(skillName): + if not actorHasSkill(actorJson, skillName): actorChanged = True else: - if actorJson['skills'][skillName] != \ + if actorSkillValue(actorJson, skillName) != \ int(skillValue): actorChanged = True - newSkills[skillName] = int(skillValue) + setActorSkillLevel(actorJson, + skillName, int(skillValue)) skillsStr = self.server.translate['Skills'] setHashtagCategory(baseDir, skillName, skillsStr.lower()) skillCtr += 1 - if len(actorJson['skills'].items()) != \ - len(newSkills.items()): + if noOfActorSkills(actorJson) != \ + actorSkillsCtr: actorChanged = True - actorJson['skills'] = newSkills # change password if fields.get('password'): @@ -4593,19 +4583,20 @@ class PubServer(BaseHTTPRequestHandler): # Other accounts (alsoKnownAs) occupationName = "" - if actorJson.get('occupationName'): - occupationName = actorJson['occupationName'] + if actorJson.get('hasOccupation'): + if actorJson['hasOccupation'].get('name'): + occupationName = actorJson['hasOccupation']['name'] if fields.get('occupationName'): fields['occupationName'] = \ removeHtml(fields['occupationName']) if occupationName != \ fields['occupationName']: - actorJson['occupationName'] = \ + actorJson['hasOccupation']['name'] = \ fields['occupationName'] actorChanged = True else: if occupationName: - actorJson['occupationName'] = '' + actorJson['hasOccupation']['name'] = '' actorChanged = True # Other accounts (alsoKnownAs) @@ -4742,7 +4733,7 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(modDir): setRole(baseDir, modNick, domain, - 'instance', 'moderator') + 'moderator') else: # nicknames on separate lines modFile = open(moderatorsFile, "w+") @@ -4767,7 +4758,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(modDir): setRole(baseDir, modNick, domain, - 'instance', 'moderator') # change site editors list @@ -4799,7 +4789,7 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'editor') + 'editor') else: # nicknames on separate lines edFile = open(editorsFile, "w+") @@ -4824,7 +4814,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'editor') # change site counselors list @@ -4856,7 +4845,7 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'counselor') + 'counselor') else: # nicknames on separate lines edFile = open(counselorsFile, "w+") @@ -4881,7 +4870,6 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isdir(edDir): setRole(baseDir, edNick, domain, - 'instance', 'counselor') # remove scheduled posts @@ -7386,7 +7374,7 @@ class PubServer(BaseHTTPRequestHandler): if not actorJson: return False - if actorJson.get('roles'): + if actorJson.get('affiliation'): if self._requestHTTP(): getPerson = \ personLookup(domain, path.replace('/roles', ''), @@ -7407,6 +7395,10 @@ class PubServer(BaseHTTPRequestHandler): if self.server.keyShortcuts.get(nickname): accessKeys = self.server.keyShortcuts[nickname] + rolesList = [] + if actorJson.get('affiliation'): + actorRolesStr = actorJson['affiliation']['roleName'] + rolesList = getRolesFromString(actorRolesStr) msg = \ htmlProfile(self.server.rssIconAtTop, self.server.cssCache, @@ -7430,8 +7422,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.allowLocalNetworkAccess, self.server.textModeBanner, self.server.debug, - accessKeys, - actorJson['roles'], + accessKeys, rolesList, None, None) msg = msg.encode('utf-8') msglen = len(msg) @@ -7443,7 +7434,12 @@ class PubServer(BaseHTTPRequestHandler): 'show roles') else: if self._fetchAuthenticated(): - msg = json.dumps(actorJson['roles'], + rolesList = [] + if actorJson.get('affiliation'): + actorRolesStr = actorJson['affiliation']['roleName'] + rolesList = getRolesFromString(actorRolesStr) + + msg = json.dumps(rolesList, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) @@ -7476,7 +7472,7 @@ class PubServer(BaseHTTPRequestHandler): if os.path.isfile(actorFilename): actorJson = loadJson(actorFilename) if actorJson: - if actorJson.get('skills'): + if noOfActorSkills(actorJson) > 0: if self._requestHTTP(): getPerson = \ personLookup(domain, @@ -7501,6 +7497,9 @@ class PubServer(BaseHTTPRequestHandler): if self.server.keyShortcuts.get(nickname): accessKeys = \ self.server.keyShortcuts[nickname] + actorSkillsStr = \ + actorJson['hasOccupation']['skills'] + skills = getSkillsFromString(actorSkillsStr) msg = \ htmlProfile(self.server.rssIconAtTop, self.server.cssCache, @@ -7524,8 +7523,7 @@ class PubServer(BaseHTTPRequestHandler): allowLocalNetworkAccess, self.server.textModeBanner, self.server.debug, - accessKeys, - actorJson['skills'], + accessKeys, skills, None, None) msg = msg.encode('utf-8') msglen = len(msg) @@ -7538,7 +7536,10 @@ class PubServer(BaseHTTPRequestHandler): 'show skills') else: if self._fetchAuthenticated(): - msg = json.dumps(actorJson['skills'], + actorSkillsStr = \ + actorJson['hasOccupation']['skills'] + skills = getSkillsFromString(actorSkillsStr) + msg = json.dumps(skills, ensure_ascii=False) msg = msg.encode('utf-8') msglen = len(msg) @@ -10571,7 +10572,11 @@ class PubServer(BaseHTTPRequestHandler): msg = \ htmlLogin(self.server.cssCache, self.server.translate, - self.server.baseDir, False).encode('utf-8') + self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.server.systemLanguage, + False).encode('utf-8') msglen = len(msg) self._logout_headers('text/html', msglen, callingDomain) self._write(msg) @@ -11201,13 +11206,15 @@ class PubServer(BaseHTTPRequestHandler): htmlAbout(self.server.cssCache, self.server.baseDir, 'http', self.server.onionDomain, - None, self.server.translate) + None, self.server.translate, + self.server.systemLanguage) elif callingDomain.endswith('.i2p'): msg = \ htmlAbout(self.server.cssCache, self.server.baseDir, 'http', self.server.i2pDomain, - None, self.server.translate) + None, self.server.translate, + self.server.systemLanguage) else: msg = \ htmlAbout(self.server.cssCache, @@ -11215,7 +11222,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.httpPrefix, self.server.domainFull, self.server.onionDomain, - self.server.translate) + self.server.translate, + self.server.systemLanguage) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, callingDomain) @@ -11676,7 +11684,10 @@ class PubServer(BaseHTTPRequestHandler): # request basic auth msg = htmlLogin(self.server.cssCache, self.server.translate, - self.server.baseDir).encode('utf-8') + self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.server.systemLanguage).encode('utf-8') msglen = len(msg) self._login_headers('text/html', msglen, callingDomain) self._write(msg) 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 389a36026..7195d70a9 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 """ @@ -219,7 +199,10 @@ def getDefaultPersonContext() -> str: 'schema': 'http://schema.org#', 'suspended': 'toot:suspended', 'toot': 'http://joinmastodon.org/ns#', - 'value': 'schema:value' + 'value': 'schema:value', + 'Occupation': 'schema:Occupation', + 'OrganizationRole': 'schema:OrganizationRole', + 'WebSite': 'schema:Project' } @@ -295,10 +278,20 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'following': personId + '/following', 'tts': personId + '/speaker', 'shares': personId + '/shares', - 'orgSchema': None, - 'occupation': "", - 'skills': {}, - 'roles': {}, + 'hasOccupation': { + '@type': 'Occupation', + 'name': "", + 'skills': "" + }, + "affiliation": { + "@type": "OrganizationRole", + "roleName": "", + "affiliation": { + "@type": "WebSite", + "url": httpPrefix + '://' + domain + }, + "startDate": published + }, 'availability': None, 'icon': { 'mediaType': 'image/png', @@ -333,9 +326,11 @@ 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'] + if newPerson.get('roles'): + del newPerson['roles'] del newPerson['tag'] del newPerson['availability'] del newPerson['followers'] @@ -479,10 +474,9 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, if nickname != 'news': # print(nickname+' becomes the instance admin and a moderator') setConfigParam(baseDir, 'admin', nickname) - setRole(baseDir, nickname, domain, 'instance', 'admin') - setRole(baseDir, nickname, domain, 'instance', 'moderator') - setRole(baseDir, nickname, domain, 'instance', 'editor') - setRole(baseDir, nickname, domain, 'instance', 'delegator') + setRole(baseDir, nickname, domain, 'admin') + setRole(baseDir, nickname, domain, 'moderator') + setRole(baseDir, nickname, domain, 'editor') if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') @@ -578,7 +572,69 @@ def personUpgradeActor(baseDir: str, personJson: {}, personJson['published'] = published updateActor = True + occupationName = '' + if personJson.get('occupationName'): + occupationName = personJson['occupationName'] + del personJson['occupationName'] + if personJson.get('occupation'): + occupationName = personJson['occupation'] + del personJson['occupation'] + + # if the older skills format is being used then switch + # to the new one + if not personJson.get('hasOccupation'): + personJson['hasOccupation'] = { + '@type': 'Occupation', + 'name': occupationName, + 'skills': "" + } + updateActor = True + + # remove the old skills format + if personJson.get('skills'): + del personJson['skills'] + updateActor = True + + # if the older roles format is being used then switch + # to the new one + if not personJson.get('affiliation'): + rolesStr = '' + adminName = getConfigParam(baseDir, 'admin') + if personJson['id'].endswith('/users/' + adminName): + rolesStr = 'admin, moderator, editor' + statusNumber, published = getStatusNumber() + personJson['affiliation'] = { + "@type": "OrganizationRole", + "roleName": rolesStr, + "affiliation": { + "@type": "WebSite", + "url": personJson['id'].split('/users/')[0] + }, + "startDate": published + } + updateActor = True + + # if no roles are defined then ensure that the admin + # roles are configured + if not personJson['affiliation']['roleName']: + adminName = getConfigParam(baseDir, 'admin') + if personJson['id'].endswith('/users/' + adminName): + personJson['affiliation']['roleName'] = \ + 'admin, moderator, editor' + updateActor = True + + # remove the old roles format + if personJson.get('roles'): + del personJson['roles'] + updateActor = True + if updateActor: + personJson['@context'] = [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + getDefaultPersonContext() + ], + saveJson(personJson, filename) # also update the actor within the cache diff --git a/roles.py b/roles.py index f198fe7f0..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 @@ -37,10 +30,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 +108,35 @@ def _removeRole(baseDir: str, nickname: str, roleFilename: str) -> None: f.write(roleNickname + '\n') +def setRolesFromList(actorJson: {}, rolesList: []) -> None: + """Sets roles from a list + """ + rolesStr = '' + for roleName in rolesList: + if rolesStr: + rolesStr += ', ' + rolesStr += roleName.lower() + if actorJson.get('affiliation'): + actorJson['affiliation']['roleName'] = rolesStr + + +def getRolesFromString(rolesStr: str) -> []: + """Returns a list of roles from a string + """ + rolesList = rolesStr.split(',') + rolesResult = [] + for roleName in rolesList: + rolesResult.append(roleName.strip().lower()) + return rolesResult + + def setRole(baseDir: str, nickname: str, domain: str, - project: str, role: str) -> bool: - """Set a person's role within a project + role: str) -> bool: + """Set a person's role Setting the role to an empty string or None will remove it """ # avoid giant strings - if len(role) > 128 or len(project) > 128: + if len(role) > 128: return False actorFilename = baseDir + '/accounts/' + \ nickname + '@' + domain + '.json' @@ -133,230 +151,28 @@ def setRole(baseDir: str, nickname: str, domain: str, actorJson = loadJson(actorFilename) if actorJson: + if not actorJson.get('affiliation'): + return False + rolesList = \ + getRolesFromString(actorJson['affiliation']['roleName']) + actorChanged = False if role: # add the role - if project == 'instance': - if roleFiles.get(role): - _addRole(baseDir, nickname, domain, roleFiles[role]) - if actorJson['roles'].get(project): - if role not in actorJson['roles'][project]: - actorJson['roles'][project].append(role) - else: - actorJson['roles'][project] = [role] + if 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/skills.py b/skills.py index 0609bfd5a..0c13d4165 100644 --- a/skills.py +++ b/skills.py @@ -15,7 +15,95 @@ from utils import getFullDomain from utils import getNicknameFromActor from utils import getDomainFromActor from utils import loadJson -from utils import saveJson + + +def setSkillsFromDict(actorJson: {}, skillsDict: {}) -> str: + """Converts a dict containing skills to a string + Returns the string version of the dictionary + """ + skillsStr = '' + for name, value in skillsDict.items(): + if skillsStr: + skillsStr += ', ' + skillsStr += name + ':' + str(value) + actorJson['hasOccupation']['skills'] = skillsStr + return skillsStr + + +def getSkillsFromString(skillsStr: str) -> {}: + """Returns a dict of skills from a string + """ + skillsList = skillsStr.split(',') + skillsDict = {} + for skill in skillsList: + if ':' not in skill: + continue + name = skill.split(':')[0].strip().lower() + valueStr = skill.split(':')[1] + if not valueStr.isdigit(): + continue + skillsDict[name] = int(valueStr) + return skillsDict + + +def actorHasSkill(actorJson: {}, skillName: str) -> bool: + """Returns true if the actor has the given skill + """ + skillsDict = \ + getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skillsDict: + return False + return skillsDict.get(skillName.lower()) + + +def actorSkillValue(actorJson: {}, skillName: str) -> int: + """Returns The skill level from an actor + """ + skillsDict = \ + getSkillsFromString(actorJson['hasOccupation']['skills']) + if not skillsDict: + return 0 + skillName = skillName.lower() + if skillsDict.get(skillName): + return skillsDict[skillName] + return 0 + + +def noOfActorSkills(actorJson: {}) -> int: + """Returns the number of skills that an actor has + """ + if actorJson.get('hasOccupation'): + skillsList = actorJson['hasOccupation']['skills'].split(',') + if skillsList: + return len(skillsList) + return 0 + + +def setActorSkillLevel(actorJson: {}, + skill: str, skillLevelPercent: int) -> bool: + """Set a skill level for a person + Setting skill level to zero removes it + """ + if skillLevelPercent < 0 or skillLevelPercent > 100: + return False + + if not actorJson: + return True + if not actorJson.get('hasOccupation'): + actorJson['hasOccupation'] = { + '@type': 'Occupation', + 'name': '', + 'skills': '' + } + skillsDict = \ + getSkillsFromString(actorJson['hasOccupation']['skills']) + if skillLevelPercent > 0: + skillsDict[skill] = skillLevelPercent + else: + if skillsDict.get(skill): + del skillsDict[skill] + setSkillsFromDict(actorJson, skillsDict) + return True def setSkillLevel(baseDir: str, nickname: str, domain: str, @@ -30,15 +118,8 @@ def setSkillLevel(baseDir: str, nickname: str, domain: str, return False actorJson = loadJson(actorFilename) - if actorJson: - if not actorJson.get('skills'): - actorJson['skills'] = {} - if skillLevelPercent > 0: - actorJson['skills'][skill] = skillLevelPercent - else: - del actorJson['skills'][skill] - saveJson(actorJson, actorFilename) - return True + return setActorSkillLevel(actorJson, + skill, skillLevelPercent) def getSkills(baseDir: str, nickname: str, domain: str) -> []: @@ -50,9 +131,9 @@ def getSkills(baseDir: str, nickname: str, domain: str) -> []: actorJson = loadJson(actorFilename) if actorJson: - if not actorJson.get('skills'): + if not actorJson.get('hasOccupation'): return None - return actorJson['skills'] + return getSkillsFromString(actorJson['hasOccupation']['skills']) return None @@ -112,7 +193,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, newSkillJson = { 'type': 'Skill', 'actor': actor, - 'object': '"'+skillStr+'"', + 'object': '"' + skillStr + '"', 'to': [toUrl], 'cc': [ccUrl] } diff --git a/tests.py b/tests.py index 541eb52af..1b53f0f2e 100644 --- a/tests.py +++ b/tests.py @@ -67,8 +67,11 @@ from person import setDisplayNickname from person import setBio # from person import generateRSAKey from skills import setSkillLevel +from skills import setSkillsFromDict +from skills import getSkillsFromString +from roles import setRolesFromList +from roles import getRolesFromString from roles import setRole -from roles import outboxDelegate from auth import constantTimeStringCheck from auth import createBasicAuthHeader from auth import authorizeBasic @@ -452,7 +455,7 @@ def createServerAlice(path: str, domain: str, port: int, deleteAllPosts(path, nickname, domain, 'inbox') deleteAllPosts(path, nickname, domain, 'outbox') assert setSkillLevel(path, nickname, domain, 'hacking', 90) - assert setRole(path, nickname, domain, 'someproject', 'guru') + assert setRole(path, nickname, domain, 'guru') if hasFollows: followPerson(path, nickname, domain, 'bob', bobAddress, federationList, False) @@ -556,8 +559,6 @@ def createServerBob(path: str, domain: str, port: int, False, password) deleteAllPosts(path, nickname, domain, 'inbox') deleteAllPosts(path, nickname, domain, 'outbox') - assert setRole(path, nickname, domain, 'bandname', 'bass player') - assert setRole(path, nickname, domain, 'bandname', 'publicist') if hasFollows: followPerson(path, nickname, domain, 'alice', aliceAddress, federationList, False) @@ -1409,80 +1410,6 @@ def testCreatePerson(): shutil.rmtree(baseDir) -def testDelegateRoles(): - print('testDelegateRoles') - currDir = os.getcwd() - nickname = 'test382' - nicknameDelegated = 'test383' - domain = 'badgerdomain.com' - password = 'mypass' - port = 80 - httpPrefix = 'https' - baseDir = currDir + '/.tests_delegaterole' - if os.path.isdir(baseDir): - shutil.rmtree(baseDir) - os.mkdir(baseDir) - os.chdir(baseDir) - - privateKeyPem, publicKeyPem, person, wfEndpoint = \ - createPerson(baseDir, nickname, domain, port, - httpPrefix, True, False, password) - privateKeyPem, publicKeyPem, person, wfEndpoint = \ - createPerson(baseDir, nicknameDelegated, domain, port, - httpPrefix, True, False, 'insecure') - - httpPrefix = 'http' - project = 'artechoke' - role = 'delegator' - actorDelegated = \ - httpPrefix + '://' + domain + '/users/' + nicknameDelegated - newRoleJson = { - 'type': 'Delegate', - 'actor': httpPrefix + '://' + domain + '/users/' + nickname, - 'object': { - 'type': 'Role', - 'actor': actorDelegated, - 'object': project + ';' + role, - 'to': [], - 'cc': [] - }, - 'to': [], - 'cc': [] - } - - assert outboxDelegate(baseDir, nickname, newRoleJson, False) - # second time delegation has already happened so should return false - assert outboxDelegate(baseDir, nickname, newRoleJson, False) is False - - assert '"delegator"' in open(baseDir + '/accounts/' + nickname + - '@' + domain + '.json').read() - assert '"delegator"' in open(baseDir + '/accounts/' + nicknameDelegated + - '@' + domain + '.json').read() - - newRoleJson = { - 'type': 'Delegate', - 'actor': httpPrefix + '://' + domain + '/users/' + nicknameDelegated, - 'object': { - 'type': 'Role', - 'actor': httpPrefix + '://' + domain + '/users/' + nickname, - 'object': 'otherproject;otherrole', - 'to': [], - 'cc': [] - }, - 'to': [], - 'cc': [] - } - - # non-delegators cannot assign roles - assert outboxDelegate(baseDir, nicknameDelegated, - newRoleJson, False) is False - assert '"otherrole"' not in open(baseDir + '/accounts/' + - nickname + '@' + domain + '.json').read() - - os.chdir(currDir) - shutil.rmtree(baseDir) - - def testAuthentication(): print('testAuthentication') currDir = os.getcwd() @@ -3731,9 +3658,54 @@ def testSpoofGeolocation() -> None: kmlFile.close() +def testSkills() -> None: + print('testSkills') + actorJson = { + 'hasOccupation': { + '@type': 'Occupation', + 'name': "", + 'skills': "" + } + } + skillsDict = { + 'bakery': 40, + 'gardening': 70 + } + setSkillsFromDict(actorJson, skillsDict) + assert actorJson['hasOccupation']['skills'] + skillsDict = getSkillsFromString(actorJson['hasOccupation']['skills']) + assert skillsDict.get('bakery') + assert skillsDict.get('gardening') + assert skillsDict['bakery'] == 40 + assert skillsDict['gardening'] == 70 + + +def testRoles() -> None: + print('testRoles') + actorJson = { + 'affiliation': { + "@type": "OrganizationRole", + "roleName": "", + "affiliation": { + "@type": "WebSite", + "url": "https://testinstance.org" + }, + "startDate": "date goes here" + } + } + testRolesList = ["admin", "moderator"] + setRolesFromList(actorJson, testRolesList) + assert actorJson['affiliation']['roleName'] + rolesList = getRolesFromString(actorJson['affiliation']['roleName']) + assert 'admin' in rolesList + assert 'moderator' in rolesList + + def runAllTests(): print('Running tests...') testFunctions() + testRoles() + testSkills() testSpoofGeolocation() testRemovePostInteractions() testExtractPGPPublicKey() @@ -3787,5 +3759,4 @@ def runAllTests(): testNoOfFollowersOnDomain() testFollows() testGroupFollowers() - testDelegateRoles() print('Tests succeeded\n') 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 += \ 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_profile.py b/webapp_profile.py index e5b569978..8b91ba5d6 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'] @@ -1026,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' @@ -1601,8 +1600,9 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str, editProfileForm += ' accept="' + imageFormats + '">\n' occupationName = '' - if actorJson.get('occupationName'): - occupationName = actorJson['occupationName'] + if actorJson.get('hasOccupation'): + if actorJson['hasOccupation'].get('name'): + occupationName = actorJson['hasOccupation']['name'] editProfileForm += '
\n' 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..6fdfbe1ea 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' + \ @@ -755,6 +748,45 @@ def htmlHeaderWithPersonMarkup(cssFilename: str, instanceTitle: str, return htmlStr +def htmlHeaderWithWebsiteMarkup(cssFilename: str, instanceTitle: str, + httpPrefix: str, domain: str, + systemLanguage: str) -> str: + """html header which includes website markup + https://schema.org/WebSite + """ + htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, + systemLanguage) + + licenseUrl = 'https://www.gnu.org/licenses/agpl-3.0.en.html' + websiteMarkup = \ + ' \n' + htmlStr = htmlStr.replace('\n', '\n' + websiteMarkup) + return htmlStr + + def htmlFooter() -> str: htmlStr = ' \n' htmlStr += '\n'