__filename__ = "webinterface.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "0.0.1" __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" import json import time import os from shutil import copyfile from pprint import pprint from person import personBoxJson from utils import getNicknameFromActor from utils import getDomainFromActor from posts import getPersonBox from follow import isFollowingActor def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str): """Receives login credentials via HTTPServer POST """ if not loginParams.startswith('username='): return None,None # minimum time between login attempts currTime=int(time.time()) if currTime str: if not os.path.isfile(baseDir+'/accounts/login.png'): copyfile(baseDir+'/img/login.png',baseDir+'/accounts/login.png') if os.path.isfile(baseDir+'/img/login-background.png'): if not os.path.isfile(baseDir+'/accounts/login-background.png'): copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png') loginText='

Welcome. Please enter your login details below.

' if os.path.isfile(baseDir+'/accounts/login.txt'): with open(baseDir+'/accounts/login.txt', 'r') as file: loginText = '

'+file.read()+'

' with open(baseDir+'/epicyon-login.css', 'r') as cssFile: loginCSS = cssFile.read() loginForm=htmlHeader(loginCSS) loginForm+= \ '
' \ '
' \ ' login image'+ \ loginText+ \ '
' \ '' \ '
' \ ' ' \ ' ' \ '' \ ' ' \ ' ' \ '' \ ' ' \ '
' \ '
' loginForm+=htmlFooter() return loginForm def htmlNewPost(baseDir: str,path: str) -> str: if not path.endswith('/newshare'): newPostText='

Enter your post text below.

' else: newPostText='

Enter the details for your shared item below.

' if os.path.isfile(baseDir+'/accounts/newpost.txt'): with open(baseDir+'/accounts/newpost.txt', 'r') as file: newPostText = '

'+file.read()+'

' with open(baseDir+'/epicyon-profile.css', 'r') as cssFile: newPostCSS = cssFile.read() pathBase=path.replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','') scopeIcon='scope_public.png' scopeDescription='Public' placeholderSubject='Subject or Content Warning (optional)...' placeholderMessage='Write something...' extraFields='' endpoint='newpost' if path.endswith('/newunlisted'): scopeIcon='scope_unlisted.png' scopeDescription='Unlisted' endpoint='newunlisted' if path.endswith('/newfollowers'): scopeIcon='scope_followers.png' scopeDescription='Followers Only' endpoint='newfollowers' if path.endswith('/newdm'): scopeIcon='scope_dm.png' scopeDescription='Direct Message' endpoint='newdm' if path.endswith('/newshare'): scopeIcon='scope_share.png' scopeDescription='Shared Item' placeholderSubject='Name of the shared item...' placeholderMessage='Description of the item being shared...' endpoint='newshare' extraFields= \ '
' \ ' ' \ ' ' \ ' ' \ '
' \ '' newPostForm=htmlHeader(newPostCSS) newPostForm+= \ '
' \ '
' \ ' ' \ ' ' \ ' ' \ '' \ ' ' \ ''+extraFields+ \ '
' \ ' ' \ ' ' \ '
' \ '
' \ '
' newPostForm+=htmlFooter() return newPostForm def htmlHeader(css=None,lang='en') -> str: if not css: htmlStr= \ '\n' \ '\n' \ ' \n' \ ' \n' \ ' \n' else: htmlStr= \ '\n' \ '\n' \ ' \n' \ ' \n' \ ' \n' return htmlStr def htmlFooter() -> str: htmlStr= \ ' \n' \ '\n' return htmlStr def htmlProfilePosts(baseDir: str,httpPrefix: str, \ authorized: bool,ocapAlways: bool, \ nickname: str,domain: str,port: int, \ session,wfRequest: {},personCache: {}) -> str: """Shows posts on the profile screen """ profileStr='' outboxFeed= \ personBoxJson(baseDir,domain, \ port,'/users/'+nickname+'/outbox?page=1', \ httpPrefix, \ 4, 'outbox', \ authorized, \ ocapAlways) for item in outboxFeed['orderedItems']: if item['type']=='Create': profileStr+= \ individualPostAsHtml(baseDir,session,wfRequest,personCache, \ nickname,domain,port,item,False) return profileStr def htmlProfileFollowing(baseDir: str,httpPrefix: str, \ authorized: bool,ocapAlways: bool, \ nickname: str,domain: str,port: int, \ session,wfRequest: {},personCache: {}, \ followingJson: {}) -> str: """Shows following on the profile screen """ profileStr='' for item in followingJson['orderedItems']: profileStr+=individualFollowAsHtml(session,wfRequest,personCache,domain,item) return profileStr def htmlProfileRoles(nickname: str,domain: str,rolesJson: {}) -> str: """Shows roles on the profile screen """ profileStr='' for project,rolesList in rolesJson.items(): profileStr+='

'+project+'

' for role in rolesList: profileStr+='

'+role+'

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

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

' else: profileStr='
'+profileStr+'
' return profileStr def htmlProfileSkills(nickname: str,domain: str,skillsJson: {}) -> str: """Shows skills on the profile screen """ profileStr='' for skill,level in skillsJson.items(): profileStr+='
'+skill+'

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

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

' else: profileStr='
'+profileStr+'
' return profileStr def htmlProfileShares(nickname: str,domain: str,sharesJson: {}) -> str: """Shows shares on the profile screen """ profileStr='' for item in sharesJson['orderedItems']: profileStr+='
' profileStr+='' profileStr+='' profileStr+='Item image' profileStr+='

'+item['summary']+'

' profileStr+='

Type: '+item['itemType']+' ' profileStr+='Category: '+item['category']+' ' profileStr+='Location: '+item['location']+'

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

@'+nickname+'@'+domain+' is not sharing any items

' else: profileStr='
'+profileStr+'
' return profileStr def htmlProfile(baseDir: str,httpPrefix: str,authorized: bool, \ ocapAlways: bool,profileJson: {},selected: str, \ session,wfRequest: {},personCache: {}, \ extraJson=None) -> str: """Show the profile page as html """ nickname=profileJson['name'] if not nickname: return "" preferredName=profileJson['preferredUsername'] domain,port=getDomainFromActor(profileJson['id']) if not domain: return "" domainFull=domain if port: domainFull=domain+':'+str(port) profileDescription=profileJson['publicKey']['summary'] profileDescription='A test description' postsButton='button' followingButton='button' followersButton='button' rolesButton='button' skillsButton='button' sharesButton='button' if selected=='posts': postsButton='buttonselected' elif selected=='following': followingButton='buttonselected' elif selected=='followers': followersButton='buttonselected' elif selected=='roles': rolesButton='buttonselected' elif selected=='skills': skillsButton='buttonselected' elif selected=='shares': sharesButton='buttonselected' loginButton='' followApprovalsSection='' followApprovals='' linkToTimelineStart='' linkToTimelineEnd='' if not authorized: loginButton='
' else: linkToTimelineStart='' linkToTimelineEnd='' # are there any follow requests? followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename,'r') as f: for line in f: if len(line)>0: # show a star on the followers tab followApprovals='' break if selected=='followers': if len(followApprovals)>0: with open(followRequestsFilename,'r') as f: for followerHandle in f: if len(line)>0: if '://' in followerHandle: followerActor=followerHandle else: followerActor=httpPrefix+'://'+followerHandle.split('@')[1]+'/users/'+followerHandle.split('@')[0] basePath=httpPrefix+'://'+domainFull+'/users/'+nickname followApprovalsSection+='
' followApprovalsSection+='' followApprovalsSection+=''+followerHandle+'' followApprovalsSection+='' followApprovalsSection+='' followApprovalsSection+='' followApprovalsSection+='' followApprovalsSection+='
' actor=profileJson['id'] profileStr= \ linkToTimelineStart+ \ '
' \ '
' \ ' '+nickname+'@'+domainFull+'' \ '

'+preferredName+'

' \ '

@'+nickname+'@'+domainFull+'

' \ '

'+profileDescription+'

'+ \ loginButton+ \ '
' \ '
'+ \ linkToTimelineEnd+ \ '
\n' \ '
' \ ' ' \ ' ' \ ' ' \ ' ' \ ' ' \ ' ' \ '
' \ '
' profileStr+=followApprovalsSection with open(baseDir+'/epicyon-profile.css', 'r') as cssFile: profileStyle = cssFile.read().replace('image.png',actor+'/image.png') if selected=='posts': profileStr+= \ htmlProfilePosts(baseDir,httpPrefix,authorized, \ ocapAlways,nickname,domain,port, \ session,wfRequest,personCache) if selected=='following' or selected=='followers': profileStr+= \ htmlProfileFollowing(baseDir,httpPrefix, \ authorized,ocapAlways,nickname, \ domain,port,session, \ wfRequest,personCache,extraJson) if selected=='roles': profileStr+= \ htmlProfileRoles(nickname,domainFull,extraJson) if selected=='skills': profileStr+= \ htmlProfileSkills(nickname,domainFull,extraJson) if selected=='shares': profileStr+= \ htmlProfileShares(nickname,domainFull,extraJson) profileStr=htmlHeader(profileStyle)+profileStr+htmlFooter() return profileStr def individualFollowAsHtml(session,wfRequest: {}, \ personCache: {},domain: str, \ followUrl: str) -> str: nickname=getNicknameFromActor(followUrl) domain,port=getDomainFromActor(followUrl) titleStr='@'+nickname+'@'+domain avatarUrl=followUrl+'/avatar.png' if domain not in followUrl: inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,preferredName = \ getPersonBox(session,wfRequest,personCache,'outbox') if avatarUrl2: avatarUrl=avatarUrl2 if preferredName: titleStr=preferredName+' '+titleStr return \ '
\n' \ '' \ 'Avatar\n'+ \ '

'+titleStr+'

'+ \ '
\n' def individualPostAsHtml(baseDir: str, \ session,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int, \ postJsonObject: {}, \ showIcons=False) -> str: avatarPosition='' containerClass='container' containerClassIcons='containericons' timeClass='time-right' actorNickname=getNicknameFromActor(postJsonObject['actor']) actorDomain,actorPort=getDomainFromActor(postJsonObject['actor']) titleStr='@'+actorNickname+'@'+actorDomain if postJsonObject['object']['inReplyTo']: containerClassIcons='containericons darker' containerClass='container darker' avatarPosition=' class="right"' timeClass='time-left' if '/statuses/' in postJsonObject['object']['inReplyTo']: replyNickname=getNicknameFromActor(postJsonObject['object']['inReplyTo']) replyDomain,replyPort=getDomainFromActor(postJsonObject['object']['inReplyTo']) if replyNickname and replyDomain: titleStr+=' replying to @'+replyNickname+'@'+replyDomain+'' else: titleStr+=' replying to '+postJsonObject['object']['inReplyTo'] attachmentStr='' if postJsonObject['object']['attachment']: if isinstance(postJsonObject['object']['attachment'], list): attachmentCtr=0 for attach in postJsonObject['object']['attachment']: if attach.get('mediaType') and attach.get('url'): mediaType=attach['mediaType'] imageDescription='' if attach.get('name'): imageDescription=attach['name'] if mediaType=='image/png' or \ mediaType=='image/jpeg' or \ mediaType=='image/gif': if attach['url'].endswith('.png') or \ attach['url'].endswith('.jpg') or \ attach['url'].endswith('.jpeg') or \ attach['url'].endswith('.gif'): if attachmentCtr>0: attachmentStr+='
' attachmentStr+= \ '' \ ''+imageDescription+'\n' attachmentCtr+=1 avatarUrl=postJsonObject['actor']+'/avatar.png' fullDomain=domain if port!=80 and port!=443: fullDomain=domain+':'+str(port) if fullDomain not in postJsonObject['actor']: inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,preferredName = \ getPersonBox(session,wfRequest,personCache,'outbox') if avatarUrl2: avatarUrl=avatarUrl2 if preferredName: titleStr=preferredName+' '+titleStr avatarDropdown= \ ' ' \ ' Avatar' if fullDomain+'/users/'+nickname not in postJsonObject['actor']: # if not following then show "Follow" in the dropdown followUnfollowStr='Follow' # if following then show "Unfollow" in the dropdown if isFollowingActor(baseDir,nickname,domain,postJsonObject['actor']): followUnfollowStr='Unfollow' avatarDropdown= \ ' ' footerStr=''+postJsonObject['object']['published']+'\n' if showIcons: footerStr='
' footerStr+='' footerStr+='' footerStr+='' footerStr+=''+postJsonObject['object']['published']+'' footerStr+='
' return \ '
\n'+ \ avatarDropdown+ \ '

'+titleStr+'

'+ \ postJsonObject['object']['content']+'\n'+ \ attachmentStr+footerStr+ \ '
\n' def htmlTimeline(session,baseDir: str,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,timelineJson: {}, \ boxName: str) -> str: """Show the timeline as html """ with open(baseDir+'/epicyon-profile.css', 'r') as cssFile: profileStyle = \ cssFile.read().replace('banner.png', \ '/users/'+nickname+'/banner.png') inboxButton='button' sentButton='button' if boxName=='inbox': inboxButton='buttonselected' elif boxName=='outbox': sentButton='buttonselected' actor='/users/'+nickname showIndividualPostIcons=True if boxName=='inbox': showIndividualPostIcons=True followApprovals='' followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt' if os.path.isfile(followRequestsFilename): with open(followRequestsFilename,'r') as f: for line in f: if len(line)>0: # show follow approvals icon followApprovals='Approve follow requests' break tlStr=htmlHeader(profileStyle) tlStr+= \ '' \ '
' \ '
' \ '
\n'+ \ ' ' \ ' ' \ ' Create a new post'+ \ ' Search and follow'+ \ followApprovals+ \ '
' for item in timelineJson['orderedItems']: if item['type']=='Create': tlStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \ nickname,domain,port,item,showIndividualPostIcons) tlStr+=htmlFooter() return tlStr def htmlInbox(session,baseDir: str,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,inboxJson: {}) -> str: """Show the inbox as html """ return htmlTimeline(session,baseDir,wfRequest,personCache, \ nickname,domain,port,inboxJson,'inbox') def htmlOutbox(session,baseDir: str,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,outboxJson: {}) -> str: """Show the Outbox as html """ return htmlTimeline(session,baseDir,wfRequest,personCache, \ nickname,domain,port,outboxJson,'outbox') def htmlIndividualPost(baseDir: str,session,wfRequest: {},personCache: {}, \ nickname: str,domain: str,port: int,postJsonObject: {}) -> str: """Show an individual post as html """ return htmlHeader()+ \ individualPostAsHtml(baseDir,session,wfRequest,personCache, \ nickname,domain,port,postJsonObject,False)+ \ htmlFooter() def htmlPostReplies(postJsonObject: {}) -> str: """Show the replies to an individual post as html """ return htmlHeader()+"

Replies

"+htmlFooter() def htmlFollowConfirm(baseDir: str,originPathStr: str,followActor: str,followProfileUrl: str) -> str: """Asks to confirm a follow """ followDomain,port=getDomainFromActor(followActor) if os.path.isfile(baseDir+'/img/follow-background.png'): if not os.path.isfile(baseDir+'/accounts/follow-background.png'): copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png') with open(baseDir+'/epicyon-follow.css', 'r') as cssFile: profileStyle = cssFile.read() followStr=htmlHeader(profileStyle) followStr+='
' followStr+='
' followStr+='
' followStr+=' ' followStr+=' ' followStr+='

Follow '+getNicknameFromActor(followActor)+'@'+followDomain+' ?

' followStr+= \ '
' \ ' ' \ ' ' \ ' ' \ '
' followStr+='
' followStr+='
' followStr+='
' followStr+=htmlFooter() return followStr def htmlUnfollowConfirm(baseDir: str,originPathStr: str,followActor: str,followProfileUrl: str) -> str: """Asks to confirm unfollowing an actor """ followDomain,port=getDomainFromActor(followActor) if os.path.isfile(baseDir+'/img/follow-background.png'): if not os.path.isfile(baseDir+'/accounts/follow-background.png'): copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png') with open(baseDir+'/epicyon-follow.css', 'r') as cssFile: profileStyle = cssFile.read() followStr=htmlHeader(profileStyle) followStr+='
' followStr+='
' followStr+='
' followStr+=' ' followStr+=' ' followStr+='

Stop following '+getNicknameFromActor(followActor)+'@'+followDomain+' ?

' followStr+= \ '
' \ ' ' \ ' ' \ ' ' \ '
' followStr+='
' followStr+='
' followStr+='
' followStr+=htmlFooter() return followStr