epicyon/webinterface.py

2299 lines
104 KiB
Python
Raw Normal View History

2019-07-20 21:13:36 +00:00
__filename__ = "webinterface.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2019-08-29 13:35:29 +00:00
__version__ = "1.0.0"
2019-07-20 21:13:36 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
2019-07-24 22:38:42 +00:00
import time
import os
2019-08-02 09:52:12 +00:00
import commentjson
2019-07-31 13:11:09 +00:00
from datetime import datetime
2019-08-21 12:47:41 +00:00
from dateutil.parser import parse
2019-07-24 22:38:42 +00:00
from shutil import copyfile
from pprint import pprint
2019-07-21 22:38:44 +00:00
from person import personBoxJson
2019-07-21 12:41:31 +00:00
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
2019-08-08 11:24:26 +00:00
from utils import noOfAccounts
2019-08-10 11:31:42 +00:00
from utils import isPublicPost
from utils import getDisplayName
2019-07-29 19:46:30 +00:00
from follow import isFollowingActor
2019-07-30 22:34:04 +00:00
from webfinger import webfingerHandle
2019-08-25 17:22:24 +00:00
from posts import isDM
from posts import getPersonBox
2019-07-30 22:34:04 +00:00
from posts import getUserUrl
from posts import parseUserFeed
from posts import populateRepliesJson
2019-08-12 13:22:17 +00:00
from posts import isModerator
2019-08-20 12:48:55 +00:00
from posts import outboxMessageCreateWrap
2019-07-30 22:34:04 +00:00
from session import getJson
2019-07-31 12:44:08 +00:00
from auth import createPassword
2019-08-01 09:05:09 +00:00
from like import likedByPerson
2019-08-30 20:48:52 +00:00
from like import noOfLikes
2019-08-01 12:18:22 +00:00
from announce import announcedByPerson
from blocking import isBlocked
2019-08-05 19:13:15 +00:00
from content import getMentionsFromHtml
2019-08-08 11:24:26 +00:00
from config import getConfigParam
2019-08-09 08:46:38 +00:00
from skills import getSkills
2019-08-18 13:35:33 +00:00
from cache import getPersonFromCache
2019-07-20 21:13:36 +00:00
2019-08-20 09:37:09 +00:00
def getPersonAvatarUrl(baseDir: str,personUrl: str,personCache: {}) -> str:
2019-08-18 13:30:40 +00:00
"""Returns the avatar url for the person
"""
2019-08-20 09:37:09 +00:00
personJson = getPersonFromCache(baseDir,personUrl,personCache)
2019-08-18 13:30:40 +00:00
if personJson:
if personJson.get('icon'):
if personJson['icon'].get('url'):
2019-08-18 13:45:24 +00:00
return personJson['icon']['url']
2019-08-18 13:30:40 +00:00
return None
2019-08-19 19:02:28 +00:00
def htmlSearchEmoji(baseDir: str,searchStr: str) -> str:
"""Search results for emoji
"""
2019-08-19 20:18:45 +00:00
if not os.path.isfile(baseDir+'/emoji/emoji.json'):
copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
2019-08-19 19:02:28 +00:00
searchStr=searchStr.lower().replace(':','').strip('\n')
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
emojiCSS=cssFile.read()
emojiLookupFilename=baseDir+'/emoji/emoji.json'
# create header
emojiForm=htmlHeader(emojiCSS)
emojiForm+='<center><h1>Emoji Search</h1></center>'
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm+='<center><h5>No results</h5></center>'
emojiForm+=htmlFooter()
return emojiForm
with open(emojiLookupFilename, 'r') as fp:
emojiJson=commentjson.load(fp)
results={}
2019-08-19 19:10:55 +00:00
for emojiName,filename in emojiJson.items():
if searchStr in emojiName:
results[emojiName] = filename+'.png'
for emojiName,filename in emojiJson.items():
if emojiName in searchStr:
results[emojiName] = filename+'.png'
2019-08-19 21:18:04 +00:00
headingShown=False
2019-08-19 19:02:28 +00:00
emojiForm+='<center>'
for emojiName,filename in results.items():
2019-08-19 21:18:04 +00:00
if os.path.isfile(baseDir+'/emoji/'+filename):
if not headingShown:
emojiForm+='<center><h5>Copy the text then paste it into your post</h5></center>'
headingShown=True
emojiForm+='<h3>:'+emojiName+':<img class="searchEmoji" src="/emoji/'+filename+'"/></h3>'
2019-08-19 19:02:28 +00:00
emojiForm+='</center>'
emojiForm+=htmlFooter()
return emojiForm
2019-08-25 21:59:42 +00:00
def htmlSearchSharedItems(baseDir: str,searchStr: str, \
pageNumber: int, \
resultsPerPage: int, \
httpPrefix: str,domainFull: str,actor: str) -> str:
2019-08-14 09:45:51 +00:00
"""Search results for shared items
"""
currPage=1
ctr=0
2019-08-13 21:32:18 +00:00
sharedItemsForm=''
2019-08-13 22:11:11 +00:00
searchStrLower=searchStr.replace('%2B','+').replace('%40','@').replace('%3A',':').replace('%23','#').lower().strip('\n')
searchStrLowerList=searchStrLower.split('+')
2019-08-13 21:32:18 +00:00
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
sharedItemsCSS=cssFile.read()
sharedItemsForm=htmlHeader(sharedItemsCSS)
sharedItemsForm+='<center><h1>Shared Items Search</h1></center>'
resultsExist=False
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
for handle in dirs:
if '@' not in handle:
continue
2019-08-25 21:48:51 +00:00
contactNickname=handle.split('@')[0]
2019-08-13 21:32:18 +00:00
sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
if not os.path.isfile(sharesFilename):
continue
with open(sharesFilename, 'r') as fp:
sharesJson=commentjson.load(fp)
for name,sharedItem in sharesJson.items():
2019-08-13 22:11:11 +00:00
matched=True
for searchSubstr in searchStrLowerList:
subStrMatched=False
searchSubstr=searchSubstr.strip()
if searchSubstr in sharedItem['location'].lower():
subStrMatched=True
elif searchSubstr in sharedItem['summary'].lower():
subStrMatched=True
elif searchSubstr in sharedItem['displayName'].lower():
subStrMatched=True
elif searchSubstr in sharedItem['category'].lower():
subStrMatched=True
if not subStrMatched:
matched=False
break
2019-08-13 21:32:18 +00:00
if matched:
2019-08-14 09:45:51 +00:00
if currPage==pageNumber:
sharedItemsForm+='<div class="container">'
sharedItemsForm+='<p class="share-title">'+sharedItem['displayName']+'</p>'
2019-08-25 21:16:38 +00:00
if sharedItem.get('imageUrl'):
2019-08-25 21:07:02 +00:00
sharedItemsForm+='<a href="'+sharedItem['imageUrl']+'">'
sharedItemsForm+='<img src="'+sharedItem['imageUrl']+'" alt="Item image"></a>'
2019-08-14 09:45:51 +00:00
sharedItemsForm+='<p>'+sharedItem['summary']+'</p>'
sharedItemsForm+='<p><b>Type:</b> '+sharedItem['itemType']+' '
sharedItemsForm+='<b>Category:</b> '+sharedItem['category']+' '
sharedItemsForm+='<b>Location:</b> '+sharedItem['location']+'</p>'
2019-08-25 21:59:42 +00:00
contactActor=httpPrefix+'://'+domainFull+'/users/'+contactNickname
sharedItemsForm+='<p><a href="'+actor+'?replydm=sharedesc:'+sharedItem['displayName']+'?mention='+contactActor+'"><button class="button">Contact</button></a>'
if actor.endswith('/users/'+contactNickname):
sharedItemsForm+=' <a href="'+actor+'?rmshare='+name+'"><button class="button">Remove</button></a>'
sharedItemsForm+='</p></div>'
2019-08-14 09:45:51 +00:00
if not resultsExist and currPage>1:
# previous page link, needs to be a POST
sharedItemsForm+= \
'<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber-1)+'">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
' <img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"/></a>' \
' </center>' \
'</form>'
resultsExist=True
ctr+=1
if ctr>=resultsPerPage:
currPage+=1
if currPage>pageNumber:
# next page link, needs to be a POST
sharedItemsForm+= \
'<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber+1)+'">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
' <img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"/></a>' \
' </center>' \
'</form>'
break
ctr=0
2019-08-13 21:32:18 +00:00
if not resultsExist:
sharedItemsForm+='<center><h5>No results</h5></center>'
sharedItemsForm+=htmlFooter()
return sharedItemsForm
2019-08-13 17:25:39 +00:00
def htmlModerationInfo(baseDir: str) -> str:
infoForm=''
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
infoCSS=cssFile.read()
infoForm=htmlHeader(infoCSS)
infoForm+='<center><h1>Moderation Information</h1></center>'
infoShown=False
suspendedFilename=baseDir+'/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
suspendedStr = f.read()
infoForm+= \
'<div class="container">' \
' <br><b>Suspended accounts</b>' \
' <br>These are currently suspended' \
' <textarea id="message" name="suspended" style="height:200px">'+suspendedStr+'</textarea>' \
'</div>'
infoShown=True
blockingFilename=baseDir+'/accounts/blocking.txt'
if os.path.isfile(blockingFilename):
with open(blockingFilename, "r") as f:
blockedStr = f.read()
infoForm+= \
'<div class="container">' \
2019-08-14 10:33:11 +00:00
' <br><b>Blocked accounts and hashtags</b>' \
2019-08-13 17:25:39 +00:00
' <br>These are globally blocked for all accounts on this instance' \
' <textarea id="message" name="blocked" style="height:200px">'+blockedStr+'</textarea>' \
'</div>'
infoShown=True
if not infoShown:
infoForm+='<center><p>Any blocks or suspensions made by moderators will be shown here.</p></center>'
infoForm+=htmlFooter()
return infoForm
2019-08-12 13:22:17 +00:00
2019-08-10 10:54:52 +00:00
def htmlHashtagSearch(baseDir: str,hashtag: str,pageNumber: int,postsPerPage: int,
2019-08-14 20:12:27 +00:00
session,wfRequest: {},personCache: {}, \
httpPrefix: str,projectVersion: str) -> str:
2019-08-10 10:54:52 +00:00
"""Show a page containing search results for a hashtag
"""
if hashtag.startswith('#'):
hashtag=hashtag[1:]
hashtagIndexFile=baseDir+'/tags/'+hashtag+'.txt'
if not os.path.isfile(hashtagIndexFile):
return None
# read the index
with open(hashtagIndexFile, "r") as f:
lines = f.readlines()
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
hashtagSearchCSS = cssFile.read()
startIndex=len(lines)-1-int(pageNumber*postsPerPage)
if startIndex<0:
startIndex=len(lines)-1
endIndex=startIndex-postsPerPage
if endIndex<0:
endIndex=0
hashtagSearchForm=htmlHeader(hashtagSearchCSS)
2019-08-10 16:37:14 +00:00
hashtagSearchForm+='<center><h1>#'+hashtag+'</h1></center>'
2019-08-10 10:54:52 +00:00
if startIndex!=len(lines)-1:
# previous page link
hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"></a></center>'
index=startIndex
while index>=endIndex:
postId=lines[index].strip('\n')
nickname=getNicknameFromActor(postId)
if not nickname:
index-=1
continue
domain,port=getDomainFromActor(postId)
if not domain:
index-=1
continue
postFilename=locatePost(baseDir,nickname,domain,postId)
if not postFilename:
index-=1
continue
with open(postFilename, 'r') as fp:
postJsonObject=commentjson.load(fp)
2019-08-10 11:31:42 +00:00
if not isPublicPost(postJsonObject):
2019-08-10 10:54:52 +00:00
index-=1
continue
2019-08-10 11:31:42 +00:00
hashtagSearchForm+= \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,postJsonObject, \
2019-08-14 20:12:27 +00:00
None,True,False, \
httpPrefix,projectVersion, \
2019-08-25 17:28:42 +00:00
False,False)
2019-08-10 10:54:52 +00:00
index-=1
if endIndex>0:
# next page link
2019-08-14 09:45:51 +00:00
hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"></a></center>'
2019-08-10 10:54:52 +00:00
hashtagSearchForm+=htmlFooter()
return hashtagSearchForm
def htmlSkillsSearch(baseDir: str,skillsearch: str,instanceOnly: bool,postsPerPage: int) -> str:
2019-08-27 22:50:40 +00:00
"""Show a page containing search results for a skill
"""
if skillsearch.startswith('*'):
skillsearch=skillsearch[1:].strip()
skillsearch=skillsearch.lower().strip('\n')
results=[]
# search instance accounts
for subdir, dirs, files in os.walk(baseDir+'/accounts/'):
for f in files:
if not f.endswith('.json'):
continue
if '@' not in f:
continue
if f.startswith('inbox@'):
continue
actorFilename = os.path.join(subdir, f)
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
2019-08-28 08:58:16 +00:00
if actorJson.get('id') and \
actorJson.get('skills') and \
actorJson.get('name') and \
actorJson.get('icon'):
actor=actorJson['id']
2019-08-27 22:50:40 +00:00
for skillName,skillLevel in actorJson['skills'].items():
2019-08-28 08:41:49 +00:00
skillName=skillName.lower()
if skillName in skillsearch or skillsearch in skillName:
2019-08-27 22:50:40 +00:00
skillLevelStr=str(skillLevel)
if skillLevel<100:
skillLevelStr='0'+skillLevelStr
if skillLevel<10:
skillLevelStr='0'+skillLevelStr
indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
if indexStr not in results:
2019-08-28 08:46:52 +00:00
results.append(indexStr)
if not instanceOnly:
# search actor cache
for subdir, dirs, files in os.walk(baseDir+'/cache/actors/'):
for f in files:
if not f.endswith('.json'):
continue
if '@' not in f:
continue
if f.startswith('inbox@'):
continue
actorFilename = os.path.join(subdir, f)
with open(actorFilename, 'r') as fp:
cachedActorJson=commentjson.load(fp)
if cachedActorJson.get('actor'):
actorJson=cachedActorJson['actor']
if actorJson.get('id') and \
actorJson.get('skills') and \
actorJson.get('name') and \
actorJson.get('icon'):
actor=actorJson['id']
for skillName,skillLevel in actorJson['skills'].items():
skillName=skillName.lower()
if skillName in skillsearch or skillsearch in skillName:
skillLevelStr=str(skillLevel)
if skillLevel<100:
skillLevelStr='0'+skillLevelStr
if skillLevel<10:
skillLevelStr='0'+skillLevelStr
indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
if indexStr not in results:
results.append(indexStr)
2019-08-27 22:50:40 +00:00
results.sort(reverse=True)
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
skillSearchCSS = cssFile.read()
skillSearchForm=htmlHeader(skillSearchCSS)
2019-08-27 23:04:02 +00:00
skillSearchForm+='<center><h1>Skills search: '+skillsearch+'</h1></center>'
2019-08-27 22:50:40 +00:00
if len(results)==0:
skillSearchForm+='<center><h5>No matches</h5></center>'
else:
skillSearchForm+='<center>'
ctr=0
for skillMatch in results:
skillMatchFields=skillMatch.split(';')
if len(skillMatchFields)==4:
actor=skillMatchFields[1]
actorName=skillMatchFields[2]
avatarUrl=skillMatchFields[3]
2019-08-28 10:19:48 +00:00
skillSearchForm+='<div class="search-result""><a href="'+actor+'/skills">'
2019-08-28 10:34:18 +00:00
skillSearchForm+='<img src="'+avatarUrl+'"/><span class="search-result-text">'+actorName+'</span></a></div>'
2019-08-27 22:50:40 +00:00
ctr+=1
if ctr>=postsPerPage:
break
skillSearchForm+='</center>'
skillSearchForm+=htmlFooter()
return skillSearchForm
2019-08-02 09:52:12 +00:00
def htmlEditProfile(baseDir: str,path: str,domain: str,port: int) -> str:
"""Shows the edit profile screen
"""
pathOriginal=path
path=path.replace('/inbox','').replace('/outbox','').replace('/shares','')
nickname=getNicknameFromActor(path)
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-08-02 09:52:12 +00:00
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return ''
2019-08-07 20:13:44 +00:00
isBot=''
displayNickname=nickname
2019-08-02 09:52:12 +00:00
bioStr=''
2019-08-07 11:42:06 +00:00
manuallyApprovesFollowers=''
2019-08-02 09:52:12 +00:00
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
if actorJson.get('name'):
displayNickname=actorJson['name']
2019-08-02 09:52:12 +00:00
if actorJson.get('summary'):
2019-08-20 21:41:58 +00:00
bioStr=actorJson['summary'].replace('<p>','').replace('</p>','')
2019-08-02 09:52:12 +00:00
if actorJson.get('manuallyApprovesFollowers'):
if actorJson['manuallyApprovesFollowers']:
manuallyApprovesFollowers='checked'
else:
manuallyApprovesFollowers=''
2019-08-07 20:13:44 +00:00
if actorJson.get('type'):
if actorJson['type']=='Service':
isBot='checked'
2019-08-02 11:43:14 +00:00
filterStr=''
filterFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/filters.txt'
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
filterStr=filterfile.read()
blockedStr=''
blockedFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
if os.path.isfile(blockedFilename):
with open(blockedFilename, 'r') as blockedfile:
blockedStr=blockedfile.read()
allowedInstancesStr=''
allowedInstancesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
if os.path.isfile(allowedInstancesFilename):
with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
allowedInstancesStr=allowedInstancesFile.read()
2019-08-09 08:46:38 +00:00
skills=getSkills(baseDir,nickname,domain)
skillsStr=''
skillCtr=1
if skills:
for skillDesc,skillValue in skills.items():
skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="'+skillDesc+'" style="width:40%">'
skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="'+str(skillValue)+'"></p>'
skillCtr+=1
skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="" style="width:40%">'
skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="50"></p>' \
2019-08-02 09:52:12 +00:00
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
2019-08-13 10:48:16 +00:00
editProfileCSS = cssFile.read()
2019-08-02 09:52:12 +00:00
2019-08-12 21:20:47 +00:00
moderatorsStr=''
adminNickname=getConfigParam(baseDir,'admin')
if path.startswith('/users/'+adminNickname+'/'):
moderators=''
moderatorsFile=baseDir+'/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
moderators = f.read()
moderatorsStr= \
'<div class="container">' \
' <b>Moderators</b><br>' \
' A list of moderator nicknames. One per line.' \
' <textarea id="message" name="moderators" placeholder="List of moderator nicknames..." style="height:200px">'+moderators+'</textarea>' \
'</div>'
2019-08-13 10:48:16 +00:00
editProfileForm=htmlHeader(editProfileCSS)
2019-08-02 09:52:12 +00:00
editProfileForm+= \
'<form enctype="multipart/form-data" method="POST" action="'+path+'/profiledata">' \
' <div class="vertical-center">' \
2019-08-07 19:22:56 +00:00
' <p class="new-post-text">Profile for '+nickname+'@'+domainFull+'</p>' \
' <div class="container">' \
' <input type="submit" name="submitProfile" value="Submit">' \
' <a href="'+pathOriginal+'"><button class="cancelbtn">Cancel</button></a>' \
' </div>'+ \
2019-08-02 09:52:12 +00:00
' <div class="container">' \
' <input type="text" placeholder="name" name="displayNickname" value="'+displayNickname+'">' \
2019-08-02 09:52:12 +00:00
' <textarea id="message" name="bio" placeholder="Your bio..." style="height:200px">'+bioStr+'</textarea>' \
' </div>' \
' <div class="container">' \
2019-08-26 21:50:03 +00:00
' The files attached below should be no larger than 10MB in total uploaded at once.<br>' \
2019-08-02 09:52:12 +00:00
' Avatar image' \
' <input type="file" id="avatar" name="avatar"' \
' accept=".png">' \
' <br>Background image' \
' <input type="file" id="image" name="image"' \
' accept=".png">' \
' <br>Timeline banner image' \
' <input type="file" id="banner" name="banner"' \
' accept=".png">' \
' </div>' \
' <div class="container">' \
2019-08-02 11:43:14 +00:00
' <input type="checkbox" class=profilecheckbox" name="approveFollowers" '+manuallyApprovesFollowers+'>Approve follower requests<br>' \
2019-08-07 20:13:44 +00:00
' <input type="checkbox" class=profilecheckbox" name="isBot" '+isBot+'>This is a bot account<br>' \
' <br><b>Filtered words</b>' \
' <br>One per line' \
' <textarea id="message" name="filteredWords" placeholder="" style="height:200px">'+filterStr+'</textarea>' \
' <br><b>Blocked accounts</b>' \
' <br>Blocked accounts, one per line, in the form <i>nickname@domain</i> or <i>*@blockeddomain</i>' \
' <textarea id="message" name="blocked" placeholder="" style="height:200px">'+blockedStr+'</textarea>' \
2019-08-02 12:12:12 +00:00
' <br><b>Federation list</b>' \
' <br>Federate only with a defined set of instances. One domain name per line.' \
' <textarea id="message" name="allowedInstances" placeholder="" style="height:200px">'+allowedInstancesStr+'</textarea>' \
2019-08-02 09:52:12 +00:00
' </div>' \
2019-08-09 08:46:38 +00:00
' <div class="container">' \
' <b>Skills</b><br>' \
' If you want to participate within organizations then you can indicate some skills that you have and approximate proficiency levels. This helps organizers to construct teams with an appropriate combination of skills.'+ \
2019-08-12 21:20:47 +00:00
skillsStr+moderatorsStr+ \
2019-08-09 08:46:38 +00:00
' </div>' \
2019-08-02 09:52:12 +00:00
' </div>' \
'</form>'
editProfileForm+=htmlFooter()
return editProfileForm
2019-08-08 13:38:33 +00:00
def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str,bool):
2019-07-25 10:56:24 +00:00
"""Receives login credentials via HTTPServer POST
2019-07-24 22:38:42 +00:00
"""
2019-07-25 10:56:24 +00:00
if not loginParams.startswith('username='):
2019-08-08 13:38:33 +00:00
return None,None,None
2019-07-24 22:38:42 +00:00
# minimum time between login attempts
currTime=int(time.time())
2019-08-08 13:38:33 +00:00
if currTime<lastLoginTime+10:
return None,None,None
2019-07-24 22:38:42 +00:00
if '&' not in loginParams:
2019-08-08 13:38:33 +00:00
return None,None,None
2019-07-24 22:38:42 +00:00
loginArgs=loginParams.split('&')
nickname=None
password=None
2019-08-08 13:38:33 +00:00
register=False
2019-07-24 22:38:42 +00:00
for arg in loginArgs:
if '=' in arg:
2019-07-25 10:56:24 +00:00
if arg.split('=',1)[0]=='username':
2019-07-24 22:38:42 +00:00
nickname=arg.split('=',1)[1]
elif arg.split('=',1)[0]=='password':
password=arg.split('=',1)[1]
2019-08-08 13:38:33 +00:00
elif arg.split('=',1)[0]=='register':
register=True
return nickname,password,register
2019-07-24 22:38:42 +00:00
def htmlLogin(baseDir: str) -> str:
2019-08-10 18:22:28 +00:00
"""Shows the login screen
"""
2019-08-08 11:24:26 +00:00
accounts=noOfAccounts(baseDir)
2019-07-24 22:38:42 +00:00
if not os.path.isfile(baseDir+'/accounts/login.png'):
copyfile(baseDir+'/img/login.png',baseDir+'/accounts/login.png')
2019-07-25 19:56:25 +00:00
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')
2019-07-25 19:22:19 +00:00
2019-08-08 11:24:26 +00:00
if accounts>0:
loginText='<p class="login-text">Welcome. Please enter your login details below.</p>'
else:
loginText='<p class="login-text">Please enter some credentials</p><p>You will become the admin of this site.</p>'
2019-07-25 19:22:19 +00:00
if os.path.isfile(baseDir+'/accounts/login.txt'):
2019-08-10 18:22:28 +00:00
# custom login message
2019-07-25 19:22:19 +00:00
with open(baseDir+'/accounts/login.txt', 'r') as file:
2019-07-25 19:56:25 +00:00
loginText = '<p class="login-text">'+file.read()+'</p>'
with open(baseDir+'/epicyon-login.css', 'r') as cssFile:
loginCSS = cssFile.read()
2019-08-08 11:24:26 +00:00
# show the register button
registerButtonStr=''
if getConfigParam(baseDir,'registration')=='open':
if int(getConfigParam(baseDir,'registrationsRemaining'))>0:
2019-08-08 13:38:33 +00:00
if accounts>0:
loginText='<p class="login-text">Welcome. Please login or register a new account.</p>'
2019-08-08 11:24:26 +00:00
registerButtonStr='<button type="submit" name="register">Register</button>'
2019-08-26 16:10:24 +00:00
TOSstr='<p class="login-text"><a href="/terms">Terms of Service</a></p>'
TOSstr+='<p class="login-text"><a href="/about">About this Instance</a></p>'
2019-08-08 13:38:33 +00:00
2019-08-08 11:24:26 +00:00
loginButtonStr=''
if accounts>0:
loginButtonStr='<button type="submit" name="submit">Login</button>'
2019-07-24 22:38:42 +00:00
loginForm=htmlHeader(loginCSS)
loginForm+= \
2019-07-25 21:39:09 +00:00
'<form method="POST" action="/login">' \
2019-07-24 22:38:42 +00:00
' <div class="imgcontainer">' \
2019-07-25 19:22:19 +00:00
' <img src="login.png" alt="login image" class="loginimage">'+ \
2019-08-08 13:38:33 +00:00
loginText+TOSstr+ \
2019-07-24 22:38:42 +00:00
' </div>' \
'' \
' <div class="container">' \
' <label for="nickname"><b>Nickname</b></label>' \
2019-08-27 14:40:00 +00:00
' <input type="text" placeholder="Enter Nickname" name="username" required autofocus>' \
2019-07-24 22:38:42 +00:00
'' \
' <label for="password"><b>Password</b></label>' \
2019-08-08 11:24:26 +00:00
' <input type="password" placeholder="Enter Password" name="password" required>'+ \
registerButtonStr+loginButtonStr+ \
2019-07-24 22:38:42 +00:00
' </div>' \
'</form>'
loginForm+=htmlFooter()
return loginForm
2019-08-10 15:33:18 +00:00
def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str:
2019-08-13 09:24:55 +00:00
"""Show the terms of service screen
"""
2019-08-10 15:33:18 +00:00
adminNickname = getConfigParam(baseDir,'admin')
2019-08-08 13:38:33 +00:00
if not os.path.isfile(baseDir+'/accounts/tos.txt'):
copyfile(baseDir+'/default_tos.txt',baseDir+'/accounts/tos.txt')
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')
TOSText='Terms of Service go here.'
if os.path.isfile(baseDir+'/accounts/tos.txt'):
with open(baseDir+'/accounts/tos.txt', 'r') as file:
TOSText = file.read()
TOSForm=''
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
termsCSS = cssFile.read()
TOSForm=htmlHeader(termsCSS)
TOSForm+='<div class="container">'+TOSText+'</div>'
2019-08-10 15:33:18 +00:00
if adminNickname:
adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
TOSForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
2019-08-08 13:38:33 +00:00
TOSForm+=htmlFooter()
return TOSForm
2019-08-26 16:02:47 +00:00
def htmlAbout(baseDir: str,httpPrefix: str,domainFull: str) -> str:
"""Show the about screen
"""
adminNickname = getConfigParam(baseDir,'admin')
if not os.path.isfile(baseDir+'/accounts/about.txt'):
copyfile(baseDir+'/default_about.txt',baseDir+'/accounts/about.txt')
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')
aboutText='Information about this instance goes here.'
if os.path.isfile(baseDir+'/accounts/about.txt'):
with open(baseDir+'/accounts/about.txt', 'r') as file:
aboutText = file.read()
aboutForm=''
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
termsCSS = cssFile.read()
aboutForm=htmlHeader(termsCSS)
aboutForm+='<div class="container">'+aboutText+'</div>'
if adminNickname:
adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
aboutForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
aboutForm+=htmlFooter()
return aboutForm
2019-08-14 10:32:15 +00:00
def htmlHashtagBlocked(baseDir: str) -> str:
"""Show the screen for a blocked hashtag
"""
blockedHashtagForm=''
with open(baseDir+'/epicyon-suspended.css', 'r') as cssFile:
blockedHashtagCSS=cssFile.read()
blockedHashtagForm=htmlHeader(blockedHashtagCSS)
blockedHashtagForm+='<div><center>'
blockedHashtagForm+=' <p class="screentitle">Hashtag Blocked</p>'
blockedHashtagForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
blockedHashtagForm+='</center></div>'
blockedHashtagForm+=htmlFooter()
return blockedHashtagForm
2019-08-13 09:24:55 +00:00
def htmlSuspended(baseDir: str) -> str:
"""Show the screen for suspended accounts
"""
suspendedForm=''
with open(baseDir+'/epicyon-suspended.css', 'r') as cssFile:
suspendedCSS=cssFile.read()
suspendedForm=htmlHeader(suspendedCSS)
suspendedForm+='<div><center>'
suspendedForm+=' <p class="screentitle">Account Suspended</p>'
suspendedForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
suspendedForm+='</center></div>'
suspendedForm+=htmlFooter()
return suspendedForm
2019-08-25 09:41:57 +00:00
def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: [],reportUrl: str) -> str:
2019-08-19 19:50:07 +00:00
"""New post screen
"""
2019-07-31 13:51:10 +00:00
replyStr=''
2019-07-28 11:35:57 +00:00
if not path.endswith('/newshare'):
2019-08-11 11:25:27 +00:00
if not path.endswith('/newreport'):
if not inReplyTo:
newPostText='<p class="new-post-text">Enter your post text below.</p>'
else:
newPostText='<p class="new-post-text">Enter your reply to <a href="'+inReplyTo+'">this post</a> below.</p>'
replyStr='<input type="hidden" name="replyTo" value="'+inReplyTo+'">'
2019-07-31 13:51:10 +00:00
else:
2019-08-11 11:25:27 +00:00
newPostText= \
'<p class="new-post-text">Enter your report below.</p>'
# custom report header with any additional instructions
if os.path.isfile(baseDir+'/accounts/report.txt'):
with open(baseDir+'/accounts/report.txt', 'r') as file:
customReportText=file.read()
if '</p>' not in customReportText:
customReportText='<p class="login-subtext">'+customReportText+'</p>'
customReportText=customReportText.replace('<p>','<p class="login-subtext">')
newPostText+=customReportText
newPostText+='<p class="new-post-subtext">This message <i>only goes to moderators</i>, even if it mentions other fediverse addresses.</p><p class="new-post-subtext">You can also refer to points within the <a href="/terms">Terms of Service</a> if necessary.</p>'
2019-07-28 11:35:57 +00:00
else:
newPostText='<p class="new-post-text">Enter the details for your shared item below.</p>'
2019-07-25 21:39:09 +00:00
if os.path.isfile(baseDir+'/accounts/newpost.txt'):
with open(baseDir+'/accounts/newpost.txt', 'r') as file:
newPostText = '<p class="new-post-text">'+file.read()+'</p>'
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
newPostCSS = cssFile.read()
if '?' in path:
path=path.split('?')[0]
2019-08-11 11:25:27 +00:00
pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','')
2019-07-26 12:26:41 +00:00
scopeIcon='scope_public.png'
scopeDescription='Public'
placeholderSubject='Subject or Content Warning (optional)...'
placeholderMessage='Write something...'
2019-07-26 12:59:30 +00:00
extraFields=''
2019-07-27 20:30:58 +00:00
endpoint='newpost'
2019-07-26 12:26:41 +00:00
if path.endswith('/newunlisted'):
scopeIcon='scope_unlisted.png'
scopeDescription='Unlisted'
2019-07-27 20:30:58 +00:00
endpoint='newunlisted'
2019-07-26 12:26:41 +00:00
if path.endswith('/newfollowers'):
scopeIcon='scope_followers.png'
2019-08-25 13:39:57 +00:00
scopeDescription='Followers'
2019-07-27 20:30:58 +00:00
endpoint='newfollowers'
2019-07-26 12:26:41 +00:00
if path.endswith('/newdm'):
scopeIcon='scope_dm.png'
2019-08-25 13:39:57 +00:00
scopeDescription='DM'
2019-07-27 20:30:58 +00:00
endpoint='newdm'
2019-08-11 11:25:27 +00:00
if path.endswith('/newreport'):
scopeIcon='scope_report.png'
scopeDescription='Report'
endpoint='newreport'
2019-07-26 12:26:41 +00:00
if path.endswith('/newshare'):
scopeIcon='scope_share.png'
scopeDescription='Shared Item'
placeholderSubject='Name of the shared item...'
placeholderMessage='Description of the item being shared...'
2019-07-27 20:30:58 +00:00
endpoint='newshare'
2019-07-26 12:59:30 +00:00
extraFields= \
2019-07-26 14:19:37 +00:00
'<div class="container">' \
2019-07-28 11:35:57 +00:00
' <input type="text" class="itemType" placeholder="Type of shared item. eg. hat" name="itemType">' \
' <input type="text" class="category" placeholder="Category of shared item. eg. clothing" name="category">' \
' <label class="labels">Duration of listing in days:</label> <input type="number" name="duration" min="1" max="365" step="1" value="14">' \
2019-07-26 14:19:37 +00:00
'</div>' \
2019-07-26 12:59:30 +00:00
'<input type="text" placeholder="City or location of the shared item" name="location">'
2019-07-25 21:39:09 +00:00
newPostForm=htmlHeader(newPostCSS)
# only show the share option if this is not a reply
shareOptionOnDropdown=''
if not replyStr:
shareOptionOnDropdown='<a href="'+pathBase+'/newshare"><img src="/icons/scope_share.png"/><b>Share</b><br>Describe a shared item</a>'
2019-08-05 19:13:15 +00:00
mentionsStr=''
for m in mentions:
mentionNickname=getNicknameFromActor(m)
if not mentionNickname:
continue
mentionDomain,mentionPort=getDomainFromActor(m)
if not mentionDomain:
continue
if mentionPort:
mentionsStr+='@'+mentionNickname+'@'+mentionDomain+':'+str(mentionPort)+' '
else:
mentionsStr+='@'+mentionNickname+'@'+mentionDomain+' '
dropDownContent=''
if not reportUrl:
dropDownContent= \
' <div id="myDropdown" class="dropdown-content">' \
' <a href="'+pathBase+'/newpost"><img src="/icons/scope_public.png"/><b>Public</b><br>Visible to anyone</a>' \
' <a href="'+pathBase+'/newunlisted"><img src="/icons/scope_unlisted.png"/><b>Unlisted</b><br>Not on public timeline</a>' \
2019-08-25 13:39:57 +00:00
' <a href="'+pathBase+'/newfollowers"><img src="/icons/scope_followers.png"/><b>Followers</b><br>Only to followers</a>' \
' <a href="'+pathBase+'/newdm"><img src="/icons/scope_dm.png"/><b>DM</b><br>Only to mentioned people</a>' \
' <a href="'+pathBase+'/newreport"><img src="/icons/scope_report.png"/><b>Report</b><br>Send to moderators</a>'+ \
shareOptionOnDropdown+ \
' </div>'
else:
mentionsStr='Re: '+reportUrl+'\n\n'+mentionsStr
2019-08-11 13:55:17 +00:00
2019-07-25 21:39:09 +00:00
newPostForm+= \
2019-07-27 20:30:58 +00:00
'<form enctype="multipart/form-data" method="POST" action="'+path+'?'+endpoint+'">' \
2019-07-25 21:39:09 +00:00
' <div class="vertical-center">' \
' <label for="nickname"><b>'+newPostText+'</b></label>' \
2019-07-26 10:30:13 +00:00
' <div class="container">' \
2019-08-25 13:10:58 +00:00
' <div class="dropbtn" onclick="dropdown()">' \
' <img src="/icons/'+scopeIcon+'"/><b class="scope-desc">'+scopeDescription+'</b>'+ \
dropDownContent+ \
2019-07-26 14:19:37 +00:00
' </div>' \
2019-07-28 15:16:14 +00:00
' <input type="submit" name="submitPost" value="Submit">' \
2019-08-25 10:12:29 +00:00
' <a href="'+pathBase+'/inbox"><button class="cancelbtn">Cancel</button></a>' \
2019-08-19 20:05:56 +00:00
' <a href="'+pathBase+'/searchemoji"><img src="/emoji/1F601.png" title="Search for emoji" alt="Search for emoji" class="right"/></a>'+ \
2019-07-31 13:51:10 +00:00
' </div>'+ \
replyStr+ \
2019-07-28 12:04:32 +00:00
' <input type="text" placeholder="'+placeholderSubject+'" name="subject">' \
'' \
2019-08-05 19:13:15 +00:00
' <textarea id="message" name="message" placeholder="'+placeholderMessage+'" style="height:200px" autofocus>'+mentionsStr+'</textarea>' \
2019-07-28 12:04:32 +00:00
''+extraFields+ \
' <div class="container">' \
' <input type="text" placeholder="Image description" name="imageDescription">' \
' <input type="file" id="attachpic" name="attachpic"' \
2019-08-30 14:37:24 +00:00
' accept=".png, .jpg, .jpeg, .gif, .mp4, .webm, .ogv, .mp3, .ogg">' \
2019-07-26 10:30:13 +00:00
' </div>' \
2019-07-25 21:39:09 +00:00
' </div>' \
'</form>'
2019-08-24 18:00:15 +00:00
if not reportUrl:
newPostForm+='<script>'+clickToDropDownScript()+'</script>'
2019-08-24 18:00:15 +00:00
2019-07-25 21:39:09 +00:00
newPostForm+=htmlFooter()
return newPostForm
2019-08-22 12:14:24 +00:00
def htmlHeader(css=None,refreshSec=0,lang='en') -> str:
if refreshSec==0:
meta=' <meta charset="utf-8">\n'
else:
meta=' <meta http-equiv="Refresh" content="'+str(refreshSec)+'" charset="utf-8">\n'
2019-07-21 18:18:58 +00:00
if not css:
htmlStr= \
'<!DOCTYPE html>\n' \
2019-08-22 12:14:24 +00:00
'<html lang="'+lang+'">\n'+ \
meta+ \
2019-07-21 18:18:58 +00:00
' <style>\n' \
2019-07-24 11:03:56 +00:00
' @import url("epicyon-profile.css");\n'+ \
2019-08-10 18:22:28 +00:00
' background-color: #282c37' \
2019-07-21 18:18:58 +00:00
' </style>\n' \
' <body>\n'
else:
htmlStr= \
'<!DOCTYPE html>\n' \
2019-08-22 12:14:24 +00:00
'<html lang="'+lang+'">\n'+ \
meta+ \
2019-07-21 18:18:58 +00:00
' <style>\n'+css+'</style>\n' \
' <body>\n'
2019-07-20 21:13:36 +00:00
return htmlStr
def htmlFooter() -> str:
htmlStr= \
' </body>\n' \
'</html>\n'
return htmlStr
2019-07-22 14:09:21 +00:00
def htmlProfilePosts(baseDir: str,httpPrefix: str, \
authorized: bool,ocapAlways: bool, \
nickname: str,domain: str,port: int, \
2019-08-14 20:12:27 +00:00
session,wfRequest: {},personCache: {}, \
projectVersion: str) -> str:
2019-07-22 09:38:02 +00:00
"""Shows posts on the profile screen
"""
profileStr=''
2019-07-22 14:09:21 +00:00
outboxFeed= \
personBoxJson(baseDir,domain, \
port,'/users/'+nickname+'/outbox?page=1', \
httpPrefix, \
4, 'outbox', \
authorized, \
ocapAlways)
2019-07-31 12:44:08 +00:00
profileStr+='<script>'+contentWarningScript()+'</script>'
2019-07-22 09:38:02 +00:00
for item in outboxFeed['orderedItems']:
2019-07-31 10:09:02 +00:00
if item['type']=='Create' or item['type']=='Announce':
2019-07-22 14:09:21 +00:00
profileStr+= \
2019-07-29 19:46:30 +00:00
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,item,None,True,False, \
httpPrefix,projectVersion, \
2019-08-25 17:28:42 +00:00
False,False)
2019-07-22 09:38:02 +00:00
return profileStr
2019-07-22 14:09:21 +00:00
def htmlProfileFollowing(baseDir: str,httpPrefix: str, \
authorized: bool,ocapAlways: bool, \
nickname: str,domain: str,port: int, \
session,wfRequest: {},personCache: {}, \
2019-08-14 20:12:27 +00:00
followingJson: {},projectVersion: str, \
2019-08-07 21:36:54 +00:00
buttons: []) -> str:
"""Shows following on the profile screen
"""
profileStr=''
for item in followingJson['orderedItems']:
2019-08-07 21:36:54 +00:00
profileStr+= \
2019-08-20 09:16:03 +00:00
individualFollowAsHtml(baseDir,session, \
wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
domain,item,authorized,nickname, \
httpPrefix,projectVersion, \
buttons)
return profileStr
2019-07-22 17:21:45 +00:00
def htmlProfileRoles(nickname: str,domain: str,rolesJson: {}) -> str:
"""Shows roles on the profile screen
"""
profileStr=''
for project,rolesList in rolesJson.items():
profileStr+='<div class="roles"><h2>'+project+'</h2><div class="roles-inner">'
for role in rolesList:
profileStr+='<h3>'+role+'</h3>'
profileStr+='</div></div>'
if len(profileStr)==0:
profileStr+='<p>@'+nickname+'@'+domain+' has no roles assigned</p>'
else:
profileStr='<div>'+profileStr+'</div>'
return profileStr
2019-07-22 20:01:46 +00:00
def htmlProfileSkills(nickname: str,domain: str,skillsJson: {}) -> str:
"""Shows skills on the profile screen
"""
profileStr=''
for skill,level in skillsJson.items():
profileStr+='<div>'+skill+'<br><div id="myProgress"><div id="myBar" style="width:'+str(level)+'%"></div></div></div><br>'
if len(profileStr)==0:
profileStr+='<p>@'+nickname+'@'+domain+' has no skills assigned</p>'
else:
profileStr='<center><div class="skill-title">'+profileStr+'</div></center>'
return profileStr
2019-07-23 12:33:09 +00:00
def htmlProfileShares(nickname: str,domain: str,sharesJson: {}) -> str:
"""Shows shares on the profile screen
"""
profileStr=''
for item in sharesJson['orderedItems']:
profileStr+='<div class="container">'
2019-07-24 09:53:07 +00:00
profileStr+='<p class="share-title">'+item['displayName']+'</p>'
2019-08-25 21:05:51 +00:00
if item.get('imageUrl'):
profileStr+='<a href="'+item['imageUrl']+'">'
profileStr+='<img src="'+item['imageUrl']+'" alt="Item image"></a>'
profileStr+='<p>'+item['summary']+'</p>'
2019-07-24 09:53:07 +00:00
profileStr+='<p><b>Type:</b> '+item['itemType']+' '
profileStr+='<b>Category:</b> '+item['category']+' '
profileStr+='<b>Location:</b> '+item['location']+'</p>'
profileStr+='</div>'
2019-07-23 12:33:09 +00:00
if len(profileStr)==0:
profileStr+='<p>@'+nickname+'@'+domain+' is not sharing any items</p>'
else:
profileStr='<div class="share-title">'+profileStr+'</div>'
2019-07-23 12:33:09 +00:00
return profileStr
2019-08-14 20:12:27 +00:00
def htmlProfile(projectVersion: str, \
baseDir: str,httpPrefix: str,authorized: bool, \
2019-07-22 14:09:21 +00:00
ocapAlways: bool,profileJson: {},selected: str, \
session,wfRequest: {},personCache: {}, \
extraJson=None) -> str:
2019-07-20 21:13:36 +00:00
"""Show the profile page as html
"""
2019-08-22 19:04:41 +00:00
nickname=profileJson['preferredUsername']
2019-07-21 18:18:58 +00:00
if not nickname:
return ""
displayName=profileJson['name']
2019-07-21 18:18:58 +00:00
domain,port=getDomainFromActor(profileJson['id'])
if not domain:
return ""
domainFull=domain
if port:
domainFull=domain+':'+str(port)
2019-07-31 12:44:08 +00:00
profileDescription=profileJson['summary']
2019-07-22 09:38:02 +00:00
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'
2019-07-28 15:52:59 +00:00
loginButton=''
2019-07-29 18:48:23 +00:00
followApprovalsSection=''
followApprovals=False
linkToTimelineStart=''
linkToTimelineEnd=''
2019-08-02 09:52:12 +00:00
editProfileStr=''
actor=profileJson['id']
2019-07-29 18:48:23 +00:00
2019-07-28 15:52:59 +00:00
if not authorized:
loginButton='<br><a href="/login"><button class="loginButton">Login</button></a>'
2019-07-29 18:48:23 +00:00
else:
2019-08-10 15:07:02 +00:00
editProfileStr='<a href="'+actor+'/editprofile"><button class="button"><span>Edit </span></button></a>'
2019-07-31 09:05:37 +00:00
linkToTimelineStart='<a href="/users/'+nickname+'/inbox" title="Switch to timeline view" alt="Switch to timeline view">'
linkToTimelineEnd='</a>'
2019-07-29 18:48:23 +00:00
# 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:
followApprovals=True
followersButton='buttonhighlighted'
if selected=='followers':
followersButton='buttonselectedhighlighted'
2019-07-29 18:48:23 +00:00
break
if selected=='followers':
if followApprovals:
2019-07-29 18:48:23 +00:00
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+='<div class="container">'
followApprovalsSection+='<a href="'+followerActor+'">'
followApprovalsSection+='<span class="followRequestHandle">'+followerHandle+'</span></a>'
followApprovalsSection+='<a href="'+basePath+'/followapprove='+followerHandle+'">'
followApprovalsSection+='<button class="followApprove">Approve</button></a>'
followApprovalsSection+='<a href="'+basePath+'/followdeny='+followerHandle+'">'
followApprovalsSection+='<button class="followDeny">Deny</button></a>'
followApprovalsSection+='</div>'
2019-07-21 18:18:58 +00:00
profileStr= \
linkToTimelineStart+ \
2019-07-21 18:18:58 +00:00
' <div class="hero-image">' \
2019-08-02 09:52:12 +00:00
' <div class="hero-text">'+ \
2019-07-21 20:36:58 +00:00
' <img src="'+profileJson['icon']['url']+'" alt="'+nickname+'@'+domainFull+'">' \
' <h1>'+displayName+'</h1>' \
2019-07-21 18:18:58 +00:00
' <p><b>@'+nickname+'@'+domainFull+'</b></p>' \
2019-07-28 15:52:59 +00:00
' <p>'+profileDescription+'</p>'+ \
loginButton+ \
2019-07-21 18:18:58 +00:00
' </div>' \
'</div>'+ \
linkToTimelineEnd+ \
2019-07-21 19:37:48 +00:00
'<div class="container">\n' \
' <center>' \
2019-07-22 10:01:10 +00:00
' <a href="'+actor+'"><button class="'+postsButton+'"><span>Posts </span></button></a>' \
' <a href="'+actor+'/following"><button class="'+followingButton+'"><span>Following </span></button></a>' \
' <a href="'+actor+'/followers"><button class="'+followersButton+'"><span>Followers </span></button></a>' \
2019-07-22 10:01:10 +00:00
' <a href="'+actor+'/roles"><button class="'+rolesButton+'"><span>Roles </span></button></a>' \
' <a href="'+actor+'/skills"><button class="'+skillsButton+'"><span>Skills </span></button></a>' \
2019-08-02 09:52:12 +00:00
' <a href="'+actor+'/shares"><button class="'+sharesButton+'"><span>Shares </span></button></a>'+ \
editProfileStr+ \
2019-07-21 19:37:48 +00:00
' </center>' \
2019-07-21 18:18:58 +00:00
'</div>'
2019-07-29 18:48:23 +00:00
profileStr+=followApprovalsSection
2019-07-22 17:42:39 +00:00
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
profileStyle = cssFile.read().replace('image.png',actor+'/image.png')
2019-07-21 22:38:44 +00:00
2019-07-22 17:42:39 +00:00
if selected=='posts':
profileStr+= \
htmlProfilePosts(baseDir,httpPrefix,authorized, \
ocapAlways,nickname,domain,port, \
2019-08-14 20:12:27 +00:00
session,wfRequest,personCache, \
projectVersion)
2019-08-07 21:36:54 +00:00
if selected=='following':
2019-07-22 17:42:39 +00:00
profileStr+= \
htmlProfileFollowing(baseDir,httpPrefix, \
authorized,ocapAlways,nickname, \
domain,port,session, \
2019-08-07 21:36:54 +00:00
wfRequest,personCache,extraJson, \
2019-08-14 20:12:27 +00:00
projectVersion, \
2019-08-07 21:36:54 +00:00
["unfollow"])
if selected=='followers':
profileStr+= \
htmlProfileFollowing(baseDir,httpPrefix, \
authorized,ocapAlways,nickname, \
domain,port,session, \
wfRequest,personCache,extraJson, \
2019-08-14 20:12:27 +00:00
projectVersion,
2019-08-07 21:36:54 +00:00
["block"])
2019-07-22 17:42:39 +00:00
if selected=='roles':
profileStr+= \
htmlProfileRoles(nickname,domainFull,extraJson)
2019-07-22 20:01:46 +00:00
if selected=='skills':
profileStr+= \
htmlProfileSkills(nickname,domainFull,extraJson)
2019-07-23 12:33:09 +00:00
if selected=='shares':
profileStr+= \
htmlProfileShares(nickname,domainFull,extraJson)
2019-07-22 17:42:39 +00:00
profileStr=htmlHeader(profileStyle)+profileStr+htmlFooter()
2019-07-21 18:18:58 +00:00
return profileStr
2019-07-20 21:13:36 +00:00
2019-08-20 09:16:03 +00:00
def individualFollowAsHtml(baseDir: str,session,wfRequest: {}, \
2019-07-22 14:09:21 +00:00
personCache: {},domain: str, \
2019-08-07 21:36:54 +00:00
followUrl: str, \
authorized: bool, \
2019-08-08 08:38:40 +00:00
actorNickname: str, \
2019-08-14 20:12:27 +00:00
httpPrefix: str, \
projectVersion: str, \
2019-08-07 21:36:54 +00:00
buttons=[]) -> str:
nickname=getNicknameFromActor(followUrl)
domain,port=getDomainFromActor(followUrl)
titleStr='@'+nickname+'@'+domain
2019-08-20 09:37:09 +00:00
avatarUrl=getPersonAvatarUrl(baseDir,followUrl,personCache)
2019-08-18 13:30:40 +00:00
if not avatarUrl:
avatarUrl=followUrl+'/avatar.png'
2019-07-22 14:09:21 +00:00
if domain not in followUrl:
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
projectVersion,httpPrefix,domain,'outbox')
2019-07-22 14:09:21 +00:00
if avatarUrl2:
avatarUrl=avatarUrl2
if displayName:
titleStr=displayName+' '+titleStr
2019-08-07 21:36:54 +00:00
buttonsStr=''
if authorized:
for b in buttons:
if b=='block':
2019-08-08 08:38:40 +00:00
buttonsStr+='<a href="/users/'+actorNickname+'?block='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">Block</button></a>'
2019-08-07 21:36:54 +00:00
if b=='unfollow':
2019-08-08 08:38:40 +00:00
buttonsStr+='<a href="/users/'+actorNickname+'?unfollow='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">Unfollow</button></a>'
2019-08-07 21:36:54 +00:00
return \
'<div class="container">\n' \
'<a href="'+followUrl+'">' \
2019-08-07 21:36:54 +00:00
'<p><img src="'+avatarUrl+'" alt="Avatar">\n'+ \
titleStr+'</a>'+buttonsStr+'</p>' \
'</div>\n'
2019-08-24 18:00:15 +00:00
def clickToDropDownScript() -> str:
"""Function run onclick to create a dropdown
"""
script= \
'function dropdown() {' \
' document.getElementById("myDropdown").classList.toggle("show");' \
'}'
2019-08-25 13:36:43 +00:00
#'window.onclick = function(event) {' \
#" if (!event.target.matches('.dropbtn')) {" \
#' var dropdowns = document.getElementsByClassName("dropdown-content");' \
#' var i;' \
#' for (i = 0; i < dropdowns.length; i++) {' \
#' var openDropdown = dropdowns[i];' \
#" if (openDropdown.classList.contains('show')) {" \
#" openDropdown.classList.remove('show');" \
#' }' \
#' }' \
#' }' \
#'}'
2019-08-24 18:00:15 +00:00
return script
2019-07-31 12:44:08 +00:00
def contentWarningScript() -> str:
"""Returns a script used for content warnings
"""
script= \
'function showContentWarning(postID) {' \
' var x = document.getElementById(postID);' \
' if (x.style.display === "none") {' \
' x.style.display = "block";' \
' } else {' \
' x.style.display = "none";' \
' }' \
'}'
return script
2019-08-19 12:59:57 +00:00
def htmlRemplaceEmojiFromTags(content: str,tag: {}) -> str:
"""Uses the tags to replace :emoji: with html image markup
"""
for tagItem in tag:
if not tagItem.get('type'):
continue
2019-08-19 13:35:55 +00:00
if tagItem['type']!='Emoji':
continue
2019-08-19 12:59:57 +00:00
if not tagItem.get('name'):
continue
2019-08-19 13:35:55 +00:00
if not tagItem.get('icon'):
continue
if not tagItem['icon'].get('url'):
2019-08-19 12:59:57 +00:00
continue
if tagItem['name'] not in content:
continue
2019-08-19 13:35:55 +00:00
emojiHtml="<img src=\""+tagItem['icon']['url']+"\" alt=\""+tagItem['name'].replace(':','')+"\" align=\"middle\" class=\"emoji\"/>"
2019-08-19 12:59:57 +00:00
content=content.replace(tagItem['name'],emojiHtml)
return content
2019-08-30 11:32:48 +00:00
def addEmbeddedAudio(content: str) -> str:
"""Adds embedded audio for mp3/ogg
"""
if not ('.mp3' in content or '.ogg' in content):
return content
2019-08-30 16:24:40 +00:00
if '<audio ' in content:
return content
2019-08-30 11:32:48 +00:00
extension='.mp3'
if '.ogg' in content:
extension='.ogg'
words=content.strip('\n').split(' ')
for w in words:
if extension not in w:
continue
2019-08-30 16:20:34 +00:00
w=w.replace('href="','').replace('">','')
2019-08-30 11:32:48 +00:00
if w.endswith('.'):
w=w[:-1]
2019-08-30 16:18:34 +00:00
if w.endswith('"'):
w=w[:-1]
2019-08-30 11:32:48 +00:00
if w.endswith(';'):
w=w[:-1]
if w.endswith(':'):
w=w[:-1]
2019-08-30 12:18:39 +00:00
if not w.endswith(extension):
2019-08-30 11:32:48 +00:00
continue
2019-08-30 16:18:34 +00:00
2019-08-30 11:38:57 +00:00
if not (w.startswith('http') or w.startswith('dat:') or '/' in w):
2019-08-30 11:32:48 +00:00
continue
url=w
2019-08-30 16:24:40 +00:00
content+='<center><audio controls>'
2019-08-30 11:32:48 +00:00
content+='<source src="'+url+'" type="audio/'+extension.replace('.','')+'">'
content+='Your browser does not support the audio element.'
2019-08-30 12:20:30 +00:00
content+='</audio></center>'
2019-08-30 11:32:48 +00:00
return content
2019-08-30 11:45:21 +00:00
def addEmbeddedVideo(content: str,width=400,height=300) -> str:
"""Adds embedded video for mp4/webm/ogv
"""
if not ('.mp4' in content or '.webm' in content or '.ogv' in content):
return content
2019-08-30 16:24:40 +00:00
if '<video ' in content:
return content
2019-08-30 11:45:21 +00:00
extension='.mp4'
if '.webm' in content:
extension='.webm'
elif '.ogv' in content:
extension='.ogv'
words=content.strip('\n').split(' ')
for w in words:
if extension not in w:
continue
2019-08-30 16:20:34 +00:00
w=w.replace('href="','').replace('">','')
2019-08-30 11:45:21 +00:00
if w.endswith('.'):
w=w[:-1]
2019-08-30 16:18:34 +00:00
if w.endswith('"'):
w=w[:-1]
2019-08-30 11:45:21 +00:00
if w.endswith(';'):
w=w[:-1]
if w.endswith(':'):
w=w[:-1]
2019-08-30 12:18:39 +00:00
if not w.endswith(extension):
2019-08-30 11:45:21 +00:00
continue
if not (w.startswith('http') or w.startswith('dat:') or '/' in w):
continue
url=w
2019-08-30 12:20:30 +00:00
content+='<center><video width="'+str(width)+'" height="'+str(height)+'" controls>'
2019-08-30 11:45:21 +00:00
content+='<source src="'+url+'" type="video/'+extension.replace('.','')+'">'
content+='Your browser does not support the video element.'
2019-08-30 12:20:30 +00:00
content+='</video></center>'
2019-08-30 11:45:21 +00:00
return content
2019-08-30 11:39:55 +00:00
def addEmbeddedVideoFromSites(content: str,width=400,height=300) -> str:
2019-08-20 16:40:39 +00:00
"""Adds embedded videos
"""
if '>vimeo.com/' in content:
url=content.split('>vimeo.com/')[1]
if '<' in url:
2019-08-20 16:48:37 +00:00
url=url.split('<')[0]
2019-08-20 18:18:04 +00:00
content=content+"<center><iframe src=\"https://player.vimeo.com/video/"+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
2019-08-20 17:27:23 +00:00
return content
2019-08-21 11:34:02 +00:00
videoSite='https://www.youtube.com'
if '"'+videoSite in content:
url=content.split('"'+videoSite)[1]
2019-08-20 17:27:23 +00:00
if '"' in url:
2019-08-21 11:34:02 +00:00
url=url.split('"')[0].replace('/watch?v=','/embed/')
content=content+"<center><iframe src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
return content
videoSite='https://media.ccc.de'
if '"'+videoSite in content:
url=content.split('"'+videoSite)[1]
if '"' in url:
url=url.split('"')[0]
2019-08-21 11:40:43 +00:00
if not url.endswith('/oembed'):
url=url+'/oembed'
2019-08-22 09:41:11 +00:00
content=content+"<center><iframe src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"fullscreen\" allowfullscreen></iframe></center>"
2019-08-20 17:27:23 +00:00
return content
2019-08-20 18:13:23 +00:00
# A selection of the current larger peertube sites, mostly French and German language
# These have been chosen based on reported numbers of users and the content of each has not been reviewed, so mileage could vary
peerTubeSites=['peertube.mastodon.host','open.tube','share.tube','tube.tr4sk.me','videos.elbinario.net','hkvideo.live','peertube.snargol.com','tube.22decembre.eu','tube.fabrigli.fr','libretube.net','libre.video','peertube.linuxrocks.online','spacepub.space','video.ploud.jp','video.omniatv.com','peertube.servebeer.com','tube.tchncs.de','tubee.fr','video.alternanet.fr','devtube.dev-wiki.de','video.samedi.pm','https://video.irem.univ-paris-diderot.fr','peertube.openstreetmap.fr','video.antopie.org','scitech.video','tube.4aem.com','video.ploud.fr','peervideo.net','video.valme.io','videos.pair2jeux.tube','vault.mle.party','hostyour.tv','diode.zone','visionon.tv','artitube.artifaille.fr','peertube.fr','peertube.live','tube.ac-lyon.fr','www.yiny.org','betamax.video','tube.piweb.be','pe.ertu.be','peertube.social','videos.lescommuns.org','peertube.nogafa.org','skeptikon.fr','video.tedomum.net','tube.p2p.legal','sikke.fi','exode.me','peertube.video']
for site in peerTubeSites:
if '"https://'+site in content:
url=content.split('"https://'+site)[1]
if '"' in url:
url=url.split('"')[0].replace('/watch/','/embed/')
2019-08-20 18:18:04 +00:00
content=content+"<center><iframe sandbox=\"allow-same-origin allow-scripts\" src=\"https://"+site+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
2019-08-20 18:13:23 +00:00
return content
2019-08-20 16:40:39 +00:00
return content
2019-08-30 11:47:30 +00:00
def addEmbeddedElements(content: str) -> str:
"""Adds embedded elements for various media types
"""
content=addEmbeddedVideoFromSites(content)
content=addEmbeddedAudio(content)
return addEmbeddedVideo(content)
2019-07-29 19:46:30 +00:00
def individualPostAsHtml(baseDir: str, \
session,wfRequest: {},personCache: {}, \
2019-07-28 19:54:05 +00:00
nickname: str,domain: str,port: int, \
2019-07-30 12:47:42 +00:00
postJsonObject: {}, \
2019-07-30 22:34:04 +00:00
avatarUrl: str, showAvatarDropdown: bool,
2019-08-14 20:12:27 +00:00
allowDeletion: bool, \
httpPrefix: str, projectVersion: str, \
2019-08-25 17:28:42 +00:00
showRepeats=True,
2019-08-14 20:12:27 +00:00
showIcons=False) -> str:
2019-07-31 10:09:02 +00:00
""" Shows a single post as html
"""
# If this is the inbox timeline then don't show the repeat icon on any DMs
2019-08-25 18:08:25 +00:00
showRepeatIcon=showRepeats
2019-08-25 17:53:20 +00:00
showDMicon=False
if showRepeats:
if isDM(postJsonObject):
2019-08-25 18:08:25 +00:00
showRepeatIcon=False
2019-08-25 17:53:20 +00:00
showDMicon=True
2019-07-31 10:09:02 +00:00
titleStr=''
2019-08-20 13:13:44 +00:00
isAnnounced=False
2019-07-31 10:09:02 +00:00
if postJsonObject['type']=='Announce':
if postJsonObject.get('object'):
if isinstance(postJsonObject['object'], str):
# get the announced post
2019-08-20 12:39:59 +00:00
announceCacheDir=baseDir+'/cache/announce/'+nickname
if not os.path.isdir(announceCacheDir):
os.mkdir(announceCacheDir)
announceFilename=announceCacheDir+'/'+postJsonObject['object'].replace('/','#')+'.json'
2019-08-20 13:37:18 +00:00
print('announceFilename: '+announceFilename)
2019-08-20 12:39:59 +00:00
if os.path.isfile(announceFilename):
print('Reading cached Announce content for '+postJsonObject['object'])
with open(announceFilename, 'r') as fp:
postJsonObject=commentjson.load(fp)
2019-08-20 13:13:44 +00:00
isAnnounced=True
2019-07-31 10:09:02 +00:00
else:
2019-08-20 12:39:59 +00:00
print('Downloading Announce content for '+postJsonObject['object'])
asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-08-20 13:30:27 +00:00
actorNickname=getNicknameFromActor(postJsonObject['actor'])
actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
2019-08-20 12:39:59 +00:00
announcedJson = getJson(session,postJsonObject['object'],asHeader,None,projectVersion,httpPrefix,domain)
if announcedJson:
if not announcedJson.get('type'):
2019-08-20 13:44:43 +00:00
pprint(announcedJson)
2019-08-20 12:39:59 +00:00
return ''
2019-08-20 13:13:44 +00:00
if announcedJson['type']!='Note':
2019-08-20 13:42:25 +00:00
pprint(announcedJson)
2019-08-20 13:13:44 +00:00
return ''
# wrap in create to be consistent with other posts
announcedJson= \
outboxMessageCreateWrap(httpPrefix, \
actorNickname,actorDomain,actorPort, \
announcedJson)
2019-08-20 12:39:59 +00:00
if announcedJson['type']!='Create':
2019-08-20 13:44:43 +00:00
pprint(announcedJson)
2019-08-20 12:39:59 +00:00
return ''
2019-08-20 14:06:42 +00:00
# set the id to the original status
announcedJson['id']=postJsonObject['object']
announcedJson['object']['id']=postJsonObject['object']
2019-08-20 12:39:59 +00:00
postJsonObject=announcedJson
with open(announceFilename, 'w') as fp:
2019-08-20 13:13:44 +00:00
commentjson.dump(postJsonObject, fp, indent=4, sort_keys=False)
isAnnounced=True
2019-08-20 12:39:59 +00:00
else:
return ''
2019-07-31 10:09:02 +00:00
else:
return ''
if not isinstance(postJsonObject['object'], dict):
return ''
isModerationPost=False
if postJsonObject['object'].get('moderationStatus'):
isModerationPost=True
2019-07-21 11:20:49 +00:00
avatarPosition=''
containerClass='container'
2019-07-30 12:47:42 +00:00
containerClassIcons='containericons'
2019-07-21 11:20:49 +00:00
timeClass='time-right'
2019-07-28 19:54:05 +00:00
actorNickname=getNicknameFromActor(postJsonObject['actor'])
actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
2019-08-02 20:21:25 +00:00
messageId=''
if postJsonObject.get('id'):
messageId=postJsonObject['id'].replace('/activity','')
2019-08-20 13:13:44 +00:00
displayName=getDisplayName(postJsonObject['actor'],personCache)
if displayName:
titleStr+='<a href="'+messageId+'">'+displayName+'</a>'
2019-08-22 12:41:16 +00:00
else:
titleStr+='<a href="'+messageId+'">@'+actorNickname+'@'+actorDomain+'</a>'
2019-08-25 17:22:24 +00:00
2019-08-25 17:53:20 +00:00
# Show a DM icon for DMs in the inbox timeline
if showDMicon:
2019-08-25 18:23:12 +00:00
titleStr=titleStr+' <img src="/icons/dm.png" class="DMicon"/>'
2019-08-25 17:53:20 +00:00
2019-08-25 18:08:25 +00:00
if showRepeatIcon:
2019-08-25 17:22:24 +00:00
if isAnnounced:
if postJsonObject['object'].get('attributedTo'):
announceNickname=getNicknameFromActor(postJsonObject['object']['attributedTo'])
announceDomain,announcePort=getDomainFromActor(postJsonObject['object']['attributedTo'])
announceDisplayName=getDisplayName(postJsonObject['object']['attributedTo'],personCache)
if announceDisplayName:
titleStr+=' <img src="/icons/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">'+announceDisplayName+'</a>'
else:
titleStr+=' <img src="/icons/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@'+announceNickname+'@'+announceDomain+'</a>'
2019-08-22 12:41:16 +00:00
else:
2019-08-25 17:22:24 +00:00
titleStr+=' <img src="/icons/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@unattributed</a>'
2019-07-21 13:03:57 +00:00
else:
2019-08-25 17:22:24 +00:00
if postJsonObject['object'].get('inReplyTo'):
containerClassIcons='containericons darker'
containerClass='container darker'
#avatarPosition=' class="right"'
if '/statuses/' in postJsonObject['object']['inReplyTo']:
replyNickname=getNicknameFromActor(postJsonObject['object']['inReplyTo'])
replyDomain,replyPort=getDomainFromActor(postJsonObject['object']['inReplyTo'])
if replyNickname and replyDomain:
replyDisplayName=getDisplayName(postJsonObject['object']['inReplyTo'],personCache)
if replyDisplayName:
titleStr+=' <img src="/icons/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+replyDisplayName+'</a>'
else:
titleStr+=' <img src="/icons/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">@'+replyNickname+'@'+replyDomain+'</a>'
else:
postDomain=postJsonObject['object']['inReplyTo'].replace('https://','').replace('http://','').replace('dat://','')
if '/' in postDomain:
postDomain=postDomain.split('/',1)[0]
titleStr+=' <img src="/icons/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+postDomain+'</a>'
2019-07-21 11:20:49 +00:00
attachmentStr=''
2019-08-30 17:39:59 +00:00
if postJsonObject['object'].get('attachment'):
2019-07-21 11:20:49 +00:00
if isinstance(postJsonObject['object']['attachment'], list):
attachmentCtr=0
2019-08-20 15:38:24 +00:00
attachmentStr+='<div class="media">'
2019-07-21 11:20:49 +00:00
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+='<br>'
attachmentStr+= \
2019-07-21 12:24:38 +00:00
'<a href="'+attach['url']+'">' \
'<img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n'
2019-08-30 12:11:58 +00:00
attachmentCtr+=1
2019-08-30 12:17:03 +00:00
elif mediaType=='video/mp4' or \
mediaType=='video/webm' or \
mediaType=='video/ogv':
2019-08-30 12:11:58 +00:00
extension='.mp4'
if attach['url'].endswith('.webm'):
extension='.webm'
elif attach['url'].endswith('.ogv'):
extension='.ogv'
if attach['url'].endswith(extension):
if attachmentCtr>0:
attachmentStr+='<br>'
attachmentStr+= \
2019-08-30 12:20:30 +00:00
'<center><video width="400" height="300" controls>' \
'<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="video/'+extension.replace('.','')+'">' \
2019-08-30 12:11:58 +00:00
'Your browser does not support the video tag.' \
2019-08-30 12:20:30 +00:00
'</video></center>'
2019-08-30 12:11:58 +00:00
attachmentCtr+=1
2019-08-30 18:44:26 +00:00
elif mediaType=='audio/mpeg' or \
2019-08-30 12:17:03 +00:00
mediaType=='audio/ogg':
2019-08-30 12:11:58 +00:00
extension='.mp3'
if attach['url'].endswith('.ogg'):
extension='.ogg'
if attach['url'].endswith(extension):
if attachmentCtr>0:
attachmentStr+='<br>'
attachmentStr+= \
2019-08-30 12:20:30 +00:00
'<center><audio controls>' \
'<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="audio/'+extension.replace('.','')+'">' \
2019-08-30 12:11:58 +00:00
'Your browser does not support the audio tag.' \
2019-08-30 12:20:30 +00:00
'</audio></center>'
2019-07-21 11:20:49 +00:00
attachmentCtr+=1
2019-08-20 15:38:24 +00:00
attachmentStr+='</div>'
2019-07-22 14:09:21 +00:00
2019-08-18 13:30:40 +00:00
if not avatarUrl:
2019-08-20 09:37:09 +00:00
avatarUrl=getPersonAvatarUrl(baseDir,postJsonObject['actor'],personCache)
2019-07-30 22:34:04 +00:00
if not avatarUrl:
avatarUrl=postJsonObject['actor']+'/avatar.png'
2019-07-28 19:54:05 +00:00
fullDomain=domain
2019-07-31 19:37:29 +00:00
if port:
if port!=80 and port!=443:
if ':' not in domain:
fullDomain=domain+':'+str(port)
2019-07-28 20:14:45 +00:00
2019-07-28 19:54:05 +00:00
if fullDomain not in postJsonObject['actor']:
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
projectVersion,httpPrefix,domain,'outbox')
2019-07-22 14:09:21 +00:00
if avatarUrl2:
avatarUrl=avatarUrl2
if displayName:
titleStr=displayName+' '+titleStr
2019-07-28 20:14:45 +00:00
2019-08-27 16:42:50 +00:00
avatarImageInPost= \
2019-08-27 16:48:34 +00:00
' <div class="timeline-avatar">' \
2019-07-28 20:14:45 +00:00
' <a href="'+postJsonObject['actor']+'">' \
2019-08-27 16:48:34 +00:00
' <img src="'+avatarUrl+'" title="Show profile" alt="Avatar"'+avatarPosition+'/></a>' \
' </div>'
2019-07-29 19:46:30 +00:00
2019-08-25 09:25:24 +00:00
messageIdStr=''
if messageId:
messageIdStr=';'+messageId
2019-07-30 22:36:26 +00:00
if showAvatarDropdown and fullDomain+'/users/'+nickname not in postJsonObject['actor']:
2019-08-27 16:42:50 +00:00
avatarImageInPost= \
2019-08-25 11:38:38 +00:00
' <div class="timeline-avatar">' \
2019-08-25 09:25:24 +00:00
' <a href="/users/'+nickname+'?options='+postJsonObject['actor']+';'+avatarUrl+messageIdStr+'">' \
' <img title="Show options for this person" src="'+avatarUrl+'" '+avatarPosition+'/></a>' \
' </div>'
2019-07-28 20:14:45 +00:00
2019-07-31 13:11:09 +00:00
publishedStr=postJsonObject['object']['published']
2019-08-20 12:54:52 +00:00
if '.' not in publishedStr:
2019-08-27 22:50:40 +00:00
if '+' not in publishedStr:
datetimeObject = datetime.strptime(publishedStr,"%Y-%m-%dT%H:%M:%SZ")
else:
2019-08-27 22:55:13 +00:00
datetimeObject = datetime.strptime(publishedStr.split('+')[0]+'Z',"%Y-%m-%dT%H:%M:%SZ")
2019-08-20 12:54:52 +00:00
else:
publishedStr=publishedStr.replace('T',' ').split('.')[0]
2019-08-21 12:47:41 +00:00
datetimeObject = parse(publishedStr)
publishedStr=datetimeObject.strftime("%a %b %d, %H:%M")
2019-07-31 13:11:09 +00:00
footerStr='<span class="'+timeClass+'">'+publishedStr+'</span>\n'
2019-07-31 19:37:29 +00:00
announceStr=''
2019-08-25 18:08:25 +00:00
if not isModerationPost and showRepeatIcon:
# don't allow announce/repeat of your own posts
announceIcon='repeat_inactive.png'
announceLink='repeat'
announceTitle='Repeat this post'
if announcedByPerson(postJsonObject,nickname,fullDomain):
announceIcon='repeat.png'
announceLink='unrepeat'
announceTitle='Undo the repeat this post'
announceStr= \
'<a href="/users/'+nickname+'?'+announceLink+'='+postJsonObject['object']['id']+'" title="'+announceTitle+'">' \
'<img src="/icons/'+announceIcon+'"/></a>'
likeStr=''
if not isModerationPost:
likeIcon='like_inactive.png'
likeLink='like'
likeTitle='Like this post'
2019-08-30 20:48:52 +00:00
if noOfLikes(postJsonObject)>0:
likeIcon='like.png'
likeLink='unlike'
likeTitle='Undo the like of this post'
likeStr= \
'<a href="/users/'+nickname+'?'+likeLink+'='+postJsonObject['object']['id']+'" title="'+likeTitle+'">' \
'<img src="/icons/'+likeIcon+'"/></a>'
deleteStr=''
2019-08-12 18:02:29 +00:00
if allowDeletion or \
('/'+fullDomain+'/' in postJsonObject['actor'] and \
postJsonObject['object']['id'].startswith(postJsonObject['actor'])):
if '/users/'+nickname+'/' in postJsonObject['object']['id']:
deleteStr= \
'<a href="/users/'+nickname+'?delete='+postJsonObject['object']['id']+'" title="Delete this post">' \
'<img src="/icons/delete.png"/></a>'
2019-07-31 19:37:29 +00:00
# change the background color for DMs in inbox timeline
if showDMicon:
containerClassIcons='containericons dm'
containerClass='container dm'
2019-07-30 12:47:42 +00:00
if showIcons:
2019-08-05 19:13:15 +00:00
replyToLink=postJsonObject['object']['id']
if postJsonObject['object'].get('attributedTo'):
replyToLink+='?mention='+postJsonObject['object']['attributedTo']
if postJsonObject['object'].get('content'):
mentionedActors=getMentionsFromHtml(postJsonObject['object']['content'])
if mentionedActors:
for actorUrl in mentionedActors:
if '?mention='+actorUrl not in replyToLink:
replyToLink+='?mention='+actorUrl
if len(replyToLink)>500:
break
2019-07-30 12:47:42 +00:00
footerStr='<div class="'+containerClassIcons+'">'
2019-08-25 18:08:25 +00:00
if not isModerationPost and showRepeatIcon:
footerStr+='<a href="/users/'+nickname+'?replyto='+replyToLink+'" title="Reply to this post">'
else:
footerStr+='<a href="/users/'+nickname+'?replydm='+replyToLink+'" title="Reply to this post">'
2019-07-30 22:34:04 +00:00
footerStr+='<img src="/icons/reply.png"/></a>'
2019-08-04 18:29:26 +00:00
footerStr+=announceStr+likeStr+deleteStr
2019-07-31 13:11:09 +00:00
footerStr+='<span class="'+timeClass+'">'+publishedStr+'</span>'
2019-07-30 12:47:42 +00:00
footerStr+='</div>'
2019-07-31 12:44:08 +00:00
if not postJsonObject['object']['sensitive']:
contentStr=postJsonObject['object']['content']+attachmentStr
2019-08-30 11:47:30 +00:00
contentStr=addEmbeddedElements(contentStr)
2019-07-31 12:44:08 +00:00
else:
postID='post'+str(createPassword(8))
contentStr=''
if postJsonObject['object'].get('summary'):
contentStr+='<b>'+postJsonObject['object']['summary']+'</b> '
if isModerationPost:
2019-08-11 20:38:10 +00:00
containerClass='container report'
2019-07-31 12:44:08 +00:00
else:
contentStr+='<b>Sensitive</b> '
contentStr+='<button class="cwButton" onclick="showContentWarning('+"'"+postID+"'"+')">SHOW MORE</button>'
contentStr+='<div class="cwText" id="'+postID+'">'
contentStr+=postJsonObject['object']['content']+attachmentStr
2019-08-30 11:47:30 +00:00
contentStr=addEmbeddedElements(contentStr)
2019-07-31 12:44:08 +00:00
contentStr+='</div>'
2019-08-19 12:59:57 +00:00
if postJsonObject['object'].get('tag'):
contentStr=htmlRemplaceEmojiFromTags(contentStr,postJsonObject['object']['tag'])
2019-08-21 13:17:30 +00:00
contentStr='<div class="message">'+contentStr+'</div>'
2019-07-21 09:09:28 +00:00
return \
2019-07-28 20:14:45 +00:00
'<div class="'+containerClass+'">\n'+ \
2019-08-27 16:42:50 +00:00
avatarImageInPost+ \
2019-07-21 13:03:57 +00:00
'<p class="post-title">'+titleStr+'</p>'+ \
2019-07-31 12:44:08 +00:00
contentStr+footerStr+ \
2019-07-21 11:52:13 +00:00
'</div>\n'
2019-07-21 09:09:28 +00:00
2019-07-31 20:37:19 +00:00
def htmlTimeline(pageNumber: int,itemsPerPage: int,session,baseDir: str, \
wfRequest: {},personCache: {}, \
2019-07-28 19:54:05 +00:00
nickname: str,domain: str,port: int,timelineJson: {}, \
2019-08-14 20:12:27 +00:00
boxName: str,allowDeletion: bool, \
httpPrefix: str,projectVersion: str) -> str:
2019-07-21 09:09:28 +00:00
"""Show the timeline as html
"""
2019-07-24 12:02:28 +00:00
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
profileStyle = \
cssFile.read().replace('banner.png', \
'/users/'+nickname+'/banner.png')
2019-08-12 13:22:17 +00:00
moderator=isModerator(baseDir,nickname)
2019-07-30 10:50:01 +00:00
inboxButton='button'
2019-08-25 14:29:47 +00:00
dmButton='button'
2019-07-30 10:50:01 +00:00
sentButton='button'
2019-08-12 13:22:17 +00:00
moderationButton='button'
2019-07-24 12:02:28 +00:00
if boxName=='inbox':
2019-07-30 10:50:01 +00:00
inboxButton='buttonselected'
2019-08-25 14:29:47 +00:00
elif boxName=='dm':
dmButton='buttonselected'
2019-07-24 12:02:28 +00:00
elif boxName=='outbox':
2019-07-30 10:50:01 +00:00
sentButton='buttonselected'
2019-08-12 13:22:17 +00:00
elif boxName=='moderation':
moderationButton='buttonselected'
2019-07-24 12:02:28 +00:00
actor='/users/'+nickname
2019-07-29 20:56:07 +00:00
2019-07-30 12:47:42 +00:00
showIndividualPostIcons=True
if boxName=='inbox':
showIndividualPostIcons=True
2019-07-29 20:56:07 +00:00
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:
2019-07-30 10:21:02 +00:00
# show follow approvals icon
2019-07-30 10:50:01 +00:00
followApprovals='<a href="'+actor+'/followers"><img class="right" alt="Approve follow requests" title="Approve follow requests" src="/icons/person.png"/></a>'
2019-07-29 20:56:07 +00:00
break
2019-08-12 13:22:17 +00:00
moderationButtonStr=''
if moderator:
2019-08-25 14:15:46 +00:00
moderationButtonStr='<a href="'+actor+'/moderation"><button class="'+moderationButton+'"><span>Mod </span></button></a>'
2019-08-22 12:14:24 +00:00
2019-07-24 12:02:28 +00:00
tlStr=htmlHeader(profileStyle)
2019-08-25 18:27:19 +00:00
if (boxName=='inbox' or boxName=='dm') and pageNumber==1:
# refresh if on the first page of the inbox and dm timeline
2019-08-22 12:14:24 +00:00
tlStr=htmlHeader(profileStyle,240)
2019-08-28 20:05:10 +00:00
if boxName!='dm':
newPostButtonStr='<a href="'+actor+'/newpost"><img src="/icons/newpost.png" title="Create a new post" alt="Create a new post" class="right"/></a>'
else:
newPostButtonStr='<a href="'+actor+'/newdm"><img src="/icons/newpost.png" title="Create a new DM" alt="Create a new DM" class="right"/></a>'
# banner and row of buttons
2019-07-24 12:02:28 +00:00
tlStr+= \
2019-07-31 09:05:37 +00:00
'<a href="/users/'+nickname+'" title="Switch to profile view" alt="Switch to profile view">' \
2019-07-24 12:02:28 +00:00
'<div class="timeline-banner">' \
'</div></a>' \
2019-07-30 10:21:02 +00:00
'<div class="container">\n'+ \
2019-08-25 14:26:33 +00:00
' <a href="'+actor+'/inbox"><button class="'+inboxButton+'"><span>Inbox</span></button></a>' \
' <a href="'+actor+'/dm"><button class="'+dmButton+'"><span>DM</span></button></a>' \
' <a href="'+actor+'/outbox"><button class="'+sentButton+'"><span>Outbox</span></button></a>'+ \
2019-08-28 20:05:10 +00:00
moderationButtonStr+newPostButtonStr+ \
' <a href="'+actor+'/search"><img src="/icons/search.png" title="Search and follow" alt="Search and follow" class="right"/></a>'+ \
2019-07-29 20:56:07 +00:00
followApprovals+ \
2019-07-24 12:02:28 +00:00
'</div>'
# second row of buttons for moderator actions
if moderator and boxName=='moderation':
tlStr+= \
2019-08-13 10:04:18 +00:00
'<form method="POST" action="/users/'+nickname+'/moderationaction">' \
'<div class="container">\n'+ \
2019-08-13 16:39:43 +00:00
' <input type="text" placeholder="Nickname or URL. Block using *@domain or nickname@domain" name="moderationAction" value="">' \
2019-08-13 10:04:18 +00:00
' <input type="submit" title="Remove the above item" name="submitRemove" value="Remove">' \
' <input type="submit" title="Suspend the above account nickname" name="submitSuspend" value="Suspend">' \
' <input type="submit" title="Remove a suspension for an account nickname" name="submitUnsuspend" value="Unsuspend">' \
' <input type="submit" title="Block an account on another instance" name="submitBlock" value="Block">' \
' <input type="submit" title="Unblock an account on another instance" name="submitUnblock" value="Unblock">' \
2019-08-13 17:25:39 +00:00
' <input type="submit" title="Information about current blocks/suspensions" name="submitInfo" value="Info">' \
2019-08-13 10:04:18 +00:00
'</div></form>'
# add the javascript for content warnings
2019-08-24 18:00:15 +00:00
tlStr+='<script>'+contentWarningScript()+clickToDropDownScript()+'</script>'
# page up arrow
2019-07-31 20:37:19 +00:00
if pageNumber>1:
tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"></a></center>'
# show the posts
2019-07-31 20:37:19 +00:00
itemCtr=0
2019-07-21 09:09:28 +00:00
for item in timelineJson['orderedItems']:
2019-07-31 10:09:02 +00:00
if item['type']=='Create' or item['type']=='Announce':
2019-07-31 20:37:19 +00:00
itemCtr+=1
2019-08-20 09:37:09 +00:00
avatarUrl=getPersonAvatarUrl(baseDir,item['actor'],personCache)
2019-07-29 19:46:30 +00:00
tlStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \
2019-08-18 13:30:40 +00:00
nickname,domain,port,item,avatarUrl,True, \
2019-08-14 20:12:27 +00:00
allowDeletion, \
httpPrefix,projectVersion,
2019-08-25 17:28:42 +00:00
boxName!='dm',
2019-08-14 20:12:27 +00:00
showIndividualPostIcons)
# page down arrow
2019-07-31 20:37:19 +00:00
if itemCtr>=itemsPerPage:
tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"></a></center>'
2019-07-21 09:09:28 +00:00
tlStr+=htmlFooter()
return tlStr
2019-07-31 20:37:19 +00:00
def htmlInbox(pageNumber: int,itemsPerPage: int, \
session,baseDir: str,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,inboxJson: {}, \
2019-08-14 20:12:27 +00:00
allowDeletion: bool, \
httpPrefix: str,projectVersion: str) -> str:
2019-07-20 21:13:36 +00:00
"""Show the inbox as html
"""
2019-07-31 20:37:19 +00:00
return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,inboxJson,'inbox',allowDeletion, \
httpPrefix,projectVersion)
2019-08-25 17:34:09 +00:00
def htmlInboxDMs(pageNumber: int,itemsPerPage: int, \
session,baseDir: str,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,inboxJson: {}, \
allowDeletion: bool, \
httpPrefix: str,projectVersion: str) -> str:
"""Show the DM timeline as html
"""
return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
nickname,domain,port,inboxJson,'dm',allowDeletion, \
httpPrefix,projectVersion)
2019-07-20 21:13:36 +00:00
2019-08-12 13:22:17 +00:00
def htmlModeration(pageNumber: int,itemsPerPage: int, \
session,baseDir: str,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,inboxJson: {}, \
2019-08-14 20:12:27 +00:00
allowDeletion: bool, \
httpPrefix: str,projectVersion: str) -> str:
2019-08-12 13:22:17 +00:00
"""Show the moderation feed as html
"""
return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,inboxJson,'moderation',allowDeletion, \
httpPrefix,projectVersion)
2019-08-12 13:22:17 +00:00
2019-07-31 20:37:19 +00:00
def htmlOutbox(pageNumber: int,itemsPerPage: int, \
session,baseDir: str,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,outboxJson: {}, \
2019-08-14 20:12:27 +00:00
allowDeletion: bool,
httpPrefix: str,projectVersion: str) -> str:
2019-07-20 21:13:36 +00:00
"""Show the Outbox as html
"""
2019-07-31 20:37:19 +00:00
return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,outboxJson,'outbox',allowDeletion, \
httpPrefix,projectVersion)
2019-07-20 21:13:36 +00:00
2019-07-29 19:46:30 +00:00
def htmlIndividualPost(baseDir: str,session,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,authorized: bool, \
2019-08-14 20:12:27 +00:00
postJsonObject: {},httpPrefix: str,projectVersion: str) -> str:
2019-07-20 21:13:36 +00:00
"""Show an individual post as html
"""
2019-08-02 20:21:25 +00:00
postStr='<script>'+contentWarningScript()+'</script>'
postStr+= \
2019-07-29 19:46:30 +00:00
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,postJsonObject,None,True,False, \
2019-08-25 17:28:42 +00:00
httpPrefix,projectVersion,False,False)
2019-08-02 19:47:30 +00:00
messageId=postJsonObject['id'].replace('/activity','')
# show the previous posts
while postJsonObject['object'].get('inReplyTo'):
postFilename=locatePost(baseDir,nickname,domain,postJsonObject['object']['inReplyTo'])
if not postFilename:
break
with open(postFilename, 'r') as fp:
postJsonObject=commentjson.load(fp)
postStr= \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,postJsonObject, \
2019-08-14 20:12:27 +00:00
None,True,False, \
httpPrefix,projectVersion, \
2019-08-25 17:28:42 +00:00
False,False)+postStr
2019-08-02 19:47:30 +00:00
# show the following posts
postFilename=locatePost(baseDir,nickname,domain,messageId)
if postFilename:
# is there a replies file for this post?
repliesFilename=postFilename.replace('.json','.replies')
if os.path.isfile(repliesFilename):
# get items from the replies file
repliesJson={'orderedItems': []}
populateRepliesJson(baseDir,nickname,domain,repliesFilename,authorized,repliesJson)
# add items to the html output
for item in repliesJson['orderedItems']:
postStr+= \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,item,None,True,False, \
2019-08-25 17:28:42 +00:00
httpPrefix,projectVersion,False,False)
2019-08-02 16:49:42 +00:00
return htmlHeader()+postStr+htmlFooter()
def htmlPostReplies(baseDir: str,session,wfRequest: {},personCache: {}, \
2019-08-14 20:12:27 +00:00
nickname: str,domain: str,port: int,repliesJson: {}, \
httpPrefix: str,projectVersion: str) -> str:
2019-07-20 21:13:36 +00:00
"""Show the replies to an individual post as html
"""
2019-08-02 16:49:42 +00:00
repliesStr=''
if repliesJson.get('orderedItems'):
for item in repliesJson['orderedItems']:
repliesStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \
2019-08-14 20:12:27 +00:00
nickname,domain,port,item,None,True,False, \
2019-08-25 17:28:42 +00:00
httpPrefix,projectVersion,False,False)
2019-08-02 16:49:42 +00:00
return htmlHeader()+repliesStr+htmlFooter()
2019-07-29 09:49:46 +00:00
def htmlRemoveSharedItem(baseDir: str,actor: str,shareName: str) -> str:
"""Shows a screen asking to confirm the removal of a shared item
"""
nickname=getNicknameFromActor(actor)
domain,port=getDomainFromActor(actor)
sharesFile=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json'
if not os.path.isfile(sharesFile):
return None
sharesJson=None
with open(sharesFile, 'r') as fp:
sharesJson=commentjson.load(fp)
if not sharesJson:
return None
if not sharesJson.get(shareName):
return None
sharedItemDisplayName=sharesJson[shareName]['displayName']
sharedItemImageUrl=None
if sharesJson[shareName].get('imageUrl'):
sharedItemImageUrl=sharesJson[shareName]['imageUrl']
if os.path.isfile(baseDir+'/img/shares-background.png'):
if not os.path.isfile(baseDir+'/accounts/shares-background.png'):
copyfile(baseDir+'/img/shares-background.png',baseDir+'/accounts/shares-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
sharesStr=htmlHeader(profileStyle)
sharesStr+='<div class="follow">'
sharesStr+=' <div class="followAvatar">'
sharesStr+=' <center>'
if sharedItemImageUrl:
sharesStr+=' <img src="'+sharedItemImageUrl+'"/>'
sharesStr+=' <p class="followText">Remove '+sharedItemDisplayName+' ?</p>'
sharesStr+= \
' <form method="POST" action="'+actor+'/rmshare">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="hidden" name="shareName" value="'+shareName+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+actor+'/inbox'+'"><button class="button">No</button></a>' \
' </form>'
sharesStr+=' </center>'
sharesStr+=' </div>'
sharesStr+='</div>'
sharesStr+=htmlFooter()
return sharesStr
2019-08-27 12:47:11 +00:00
def htmlDeletePost(session,baseDir: str,messageId: str, \
httpPrefix: str,projectVersion: str, \
wfRequest: {},personCache: {}) -> str:
"""Shows a screen asking to confirm the deletion of a post
"""
if '/statuses/' not in messageId:
return None
actor=messageId.split('/statuses/')[0]
nickname=getNicknameFromActor(actor)
domain,port=getDomainFromActor(actor)
postFilename=locatePost(baseDir,nickname,domain,messageId)
if not postFilename:
return None
with open(postFilename, 'r') as fp:
postJsonObject=commentjson.load(fp)
if os.path.isfile(baseDir+'/img/delete-background.png'):
if not os.path.isfile(baseDir+'/accounts/delete-background.png'):
copyfile(baseDir+'/img/delete-background.png',baseDir+'/accounts/delete-background.png')
deletePostStr=None
2019-08-27 13:06:51 +00:00
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
2019-08-27 12:47:11 +00:00
profileStyle = cssFile.read()
deletePostStr=htmlHeader(profileStyle)
deletePostStr+='<script>'+contentWarningScript()+'</script>'
deletePostStr+= \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,postJsonObject,None,True,False, \
httpPrefix,projectVersion,False,False)
deletePostStr+='<center>'
deletePostStr+=' <p class="followText">Delete this post?</p>'
deletePostStr+= \
' <form method="POST" action="'+actor+'/rmpost">' \
' <input type="hidden" name="messageId" value="'+messageId+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+actor+'/inbox'+'"><button class="button">No</button></a>' \
' </form>'
deletePostStr+='</center>'
deletePostStr+=htmlFooter()
2019-08-27 12:47:11 +00:00
return deletePostStr
2019-07-29 09:49:46 +00:00
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+='<div class="follow">'
followStr+=' <div class="followAvatar">'
followStr+=' <center>'
followStr+=' <a href="'+followActor+'">'
followStr+=' <img src="'+followProfileUrl+'"/></a>'
followStr+=' <p class="followText">Follow '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
followStr+= \
2019-07-29 16:13:48 +00:00
' <form method="POST" action="'+originPathStr+'/followconfirm">' \
2019-07-29 09:49:46 +00:00
' <input type="hidden" name="actor" value="'+followActor+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
' </form>'
followStr+='</center>'
followStr+='</div>'
followStr+='</div>'
followStr+=htmlFooter()
return followStr
2019-07-29 20:36:26 +00:00
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+='<div class="follow">'
followStr+=' <div class="followAvatar">'
followStr+=' <center>'
followStr+=' <a href="'+followActor+'">'
followStr+=' <img src="'+followProfileUrl+'"/></a>'
followStr+=' <p class="followText">Stop following '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
followStr+= \
' <form method="POST" action="'+originPathStr+'/unfollowconfirm">' \
' <input type="hidden" name="actor" value="'+followActor+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
' </form>'
followStr+='</center>'
followStr+='</div>'
followStr+='</div>'
followStr+=htmlFooter()
return followStr
2019-07-30 22:34:04 +00:00
2019-08-24 23:00:03 +00:00
def htmlPersonOptions(baseDir: str,domain: str,originPathStr: str,optionsActor: str,optionsProfileUrl: str,optionsLink: str) -> str:
2019-08-24 21:10:20 +00:00
"""Show options for a person: view/follow/block/report
"""
2019-08-24 23:00:03 +00:00
optionsDomain,optionsPort=getDomainFromActor(optionsActor)
2019-08-24 21:10:20 +00:00
if os.path.isfile(baseDir+'/img/options-background.png'):
if not os.path.isfile(baseDir+'/accounts/options-background.png'):
copyfile(baseDir+'/img/options-background.png',baseDir+'/accounts/options-background.png')
2019-08-24 23:00:03 +00:00
followStr='Follow'
blockStr='Block'
if originPathStr.startswith('/users/'):
nickname=originPathStr.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
if '?' in nickname:
nickname=nickname.split('?')[0]
followerDomain,followerPort=getDomainFromActor(optionsActor)
if isFollowingActor(baseDir,nickname,domain,optionsActor):
followStr='Unfollow'
optionsNickname=getNicknameFromActor(optionsActor)
optionsDomainFull=optionsDomain
if optionsPort:
if optionsPort!=80 and optionsPort!=443:
optionsDomainFull=optionsDomain+':'+str(optionsPort)
if isBlocked(baseDir,nickname,domain,optionsNickname,optionsDomainFull):
blockStr='Block'
optionsLinkStr=''
if optionsLink:
optionsLinkStr=' <input type="hidden" name="postUrl" value="'+optionsLink+'">'
2019-08-24 21:10:20 +00:00
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
optionsStr=htmlHeader(profileStyle)
optionsStr+='<div class="options">'
optionsStr+=' <div class="optionsAvatar">'
optionsStr+=' <center>'
optionsStr+=' <a href="'+optionsActor+'">'
optionsStr+=' <img src="'+optionsProfileUrl+'"/></a>'
2019-08-28 15:47:56 +00:00
optionsStr+=' <p class="optionsText">Options for @'+getNicknameFromActor(optionsActor)+'@'+optionsDomain+'</p>'
2019-08-24 21:10:20 +00:00
optionsStr+= \
2019-08-24 21:18:19 +00:00
' <form method="POST" action="'+originPathStr+'/personoptions">' \
2019-08-24 21:10:20 +00:00
' <input type="hidden" name="actor" value="'+optionsActor+'">' \
2019-08-24 23:00:03 +00:00
' <input type="hidden" name="avatarUrl" value="'+optionsProfileUrl+'">'+ \
optionsLinkStr+ \
2019-08-24 21:10:20 +00:00
' <button type="submit" class="button" name="submitView">View</button>' \
2019-08-24 23:00:03 +00:00
' <button type="submit" class="button" name="submit'+followStr+'">'+followStr+'</button>' \
' <button type="submit" class="button" name="submit'+blockStr+'">'+blockStr+'</button>' \
2019-08-25 10:30:39 +00:00
' <button type="submit" class="button" name="submitDM">DM</button>' \
2019-08-24 21:10:20 +00:00
' <button type="submit" class="button" name="submitReport">Report</button>' \
' </form>'
optionsStr+='</center>'
optionsStr+='</div>'
optionsStr+='</div>'
optionsStr+=htmlFooter()
return optionsStr
def htmlBlockConfirm(baseDir: str,originPathStr: str,blockActor: str,blockProfileUrl: str) -> str:
"""Asks to confirm a block
"""
blockDomain,port=getDomainFromActor(blockActor)
if os.path.isfile(baseDir+'/img/block-background.png'):
if not os.path.isfile(baseDir+'/accounts/block-background.png'):
copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
blockStr=htmlHeader(profileStyle)
blockStr+='<div class="block">'
blockStr+=' <div class="blockAvatar">'
blockStr+=' <center>'
blockStr+=' <a href="'+blockActor+'">'
blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
blockStr+=' <p class="blockText">Block '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
blockStr+= \
' <form method="POST" action="'+originPathStr+'/blockconfirm">' \
' <input type="hidden" name="actor" value="'+blockActor+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
' </form>'
blockStr+='</center>'
blockStr+='</div>'
blockStr+='</div>'
blockStr+=htmlFooter()
return blockStr
def htmlUnblockConfirm(baseDir: str,originPathStr: str,blockActor: str,blockProfileUrl: str) -> str:
"""Asks to confirm unblocking an actor
"""
blockDomain,port=getDomainFromActor(blockActor)
if os.path.isfile(baseDir+'/img/block-background.png'):
if not os.path.isfile(baseDir+'/accounts/block-background.png'):
copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
blockStr=htmlHeader(profileStyle)
blockStr+='<div class="block">'
blockStr+=' <div class="blockAvatar">'
blockStr+=' <center>'
blockStr+=' <a href="'+blockActor+'">'
blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
blockStr+=' <p class="blockText">Stop blocking '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
blockStr+= \
' <form method="POST" action="'+originPathStr+'/unblockconfirm">' \
' <input type="hidden" name="actor" value="'+blockActor+'">' \
' <button type="submit" class="button" name="submitYes">Yes</button>' \
' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
' </form>'
blockStr+='</center>'
blockStr+='</div>'
blockStr+='</div>'
blockStr+=htmlFooter()
return blockStr
2019-08-19 20:01:29 +00:00
def htmlSearchEmojiTextEntry(baseDir: str,path: str) -> str:
"""Search for an emoji by name
"""
2019-08-19 20:18:45 +00:00
if not os.path.isfile(baseDir+'/emoji/emoji.json'):
copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
2019-08-19 20:01:29 +00:00
actor=path.replace('/search','')
nickname=getNicknameFromActor(actor)
domain,port=getDomainFromActor(actor)
if os.path.isfile(baseDir+'/img/search-background.png'):
if not os.path.isfile(baseDir+'/accounts/search-background.png'):
copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
emojiStr=htmlHeader(profileStyle)
emojiStr+='<div class="follow">'
emojiStr+=' <div class="followAvatar">'
emojiStr+=' <center>'
emojiStr+=' <p class="followText">Enter an emoji name to search for</p>'
emojiStr+= \
' <form method="POST" action="'+actor+'/searchhandleemoji">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="text" name="searchtext" autofocus><br>' \
' <button type="submit" class="button" name="submitSearch">Submit</button>' \
' </form>'
emojiStr+=' </center>'
emojiStr+=' </div>'
emojiStr+='</div>'
emojiStr+=htmlFooter()
return emojiStr
2019-07-30 22:34:04 +00:00
def htmlSearch(baseDir: str,path: str) -> str:
"""Search called from the timeline icon
"""
actor=path.replace('/search','')
nickname=getNicknameFromActor(actor)
domain,port=getDomainFromActor(actor)
if os.path.isfile(baseDir+'/img/search-background.png'):
if not os.path.isfile(baseDir+'/accounts/search-background.png'):
copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
followStr=htmlHeader(profileStyle)
followStr+='<div class="follow">'
followStr+=' <div class="followAvatar">'
followStr+=' <center>'
2019-08-27 22:50:40 +00:00
followStr+=' <p class="followText">Enter an address, shared item, #hashtag, *skill or :emoji: to search for</p>'
2019-07-30 22:34:04 +00:00
followStr+= \
' <form method="POST" action="'+actor+'/searchhandle">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="text" name="searchtext" autofocus><br>' \
' <button type="submit" class="button" name="submitSearch">Submit</button>' \
' </form>'
followStr+=' </center>'
followStr+=' </div>'
followStr+='</div>'
followStr+=htmlFooter()
return followStr
def htmlProfileAfterSearch(baseDir: str,path: str,httpPrefix: str, \
nickname: str,domain: str,port: int, \
profileHandle: str, \
session,wfRequest: {},personCache: {},
2019-08-14 20:12:27 +00:00
debug: bool,projectVersion: str) -> str:
2019-07-30 22:34:04 +00:00
"""Show a profile page after a search for a fediverse address
"""
2019-08-21 16:15:35 +00:00
if '/users/' in profileHandle or '/@' in profileHandle:
2019-07-30 22:34:04 +00:00
searchNickname=getNicknameFromActor(profileHandle)
searchDomain,searchPort=getDomainFromActor(profileHandle)
else:
if '@' not in profileHandle:
if debug:
print('DEBUG: no @ in '+profileHandle)
return None
if profileHandle.startswith('@'):
profileHandle=profileHandle[1:]
if '@' not in profileHandle:
if debug:
print('DEBUG: no @ in '+profileHandle)
return None
searchNickname=profileHandle.split('@')[0]
searchDomain=profileHandle.split('@')[1]
searchPort=None
if ':' in searchDomain:
searchPort=int(searchDomain.split(':')[1])
searchDomain=searchDomain.split(':')[0]
if not searchNickname:
if debug:
print('DEBUG: No nickname found in '+profileHandle)
return None
if not searchDomain:
if debug:
print('DEBUG: No domain found in '+profileHandle)
return None
searchDomainFull=searchDomain
if searchPort:
if searchPort!=80 and searchPort!=443:
if ':' not in searchDomain:
searchDomainFull=searchDomain+':'+str(searchPort)
2019-07-30 22:34:04 +00:00
profileStr=''
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
2019-08-14 20:12:27 +00:00
wf = webfingerHandle(session,searchNickname+'@'+searchDomainFull,httpPrefix,wfRequest, \
2019-09-01 12:09:29 +00:00
domain,projectVersion)
2019-07-30 22:34:04 +00:00
if not wf:
if debug:
print('DEBUG: Unable to webfinger '+searchNickname+'@'+searchDomainFull)
2019-07-30 22:34:04 +00:00
return None
2019-09-01 11:52:46 +00:00
asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-07-30 22:34:04 +00:00
personUrl = getUserUrl(wf)
2019-09-01 12:09:29 +00:00
profileJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
2019-07-30 22:34:04 +00:00
if not profileJson:
if debug:
print('DEBUG: No actor returned from '+personUrl)
return None
avatarUrl=''
if profileJson.get('icon'):
if profileJson['icon'].get('url'):
avatarUrl=profileJson['icon']['url']
2019-08-18 13:30:40 +00:00
if not avatarUrl:
2019-08-20 09:37:09 +00:00
avatarUrl=getPersonAvatarUrl(baseDir,personUrl,personCache)
displayName=searchNickname
2019-08-22 18:37:22 +00:00
if profileJson.get('name'):
displayName=profileJson['name']
2019-07-30 22:34:04 +00:00
profileDescription=''
2019-07-31 12:44:08 +00:00
if profileJson.get('summary'):
profileDescription=profileJson['summary']
2019-07-30 22:34:04 +00:00
outboxUrl=None
if not profileJson.get('outbox'):
if debug:
pprint(profileJson)
print('DEBUG: No outbox found')
return None
outboxUrl=profileJson['outbox']
profileBackgroundImage=''
if profileJson.get('image'):
if profileJson['image'].get('url'):
profileBackgroundImage=profileJson['image']['url']
profileStyle = cssFile.read().replace('image.png',profileBackgroundImage)
# url to return to
backUrl=path
if not backUrl.endswith('/inbox'):
backUrl+='/inbox'
2019-07-30 22:34:04 +00:00
profileStr= \
' <div class="hero-image">' \
' <div class="hero-text">' \
' <img src="'+avatarUrl+'" alt="'+searchNickname+'@'+searchDomainFull+'">' \
' <h1>'+displayName+'</h1>' \
2019-07-30 22:34:04 +00:00
' <p><b>@'+searchNickname+'@'+searchDomainFull+'</b></p>' \
' <p>'+profileDescription+'</p>'+ \
' </div>' \
'</div>'+ \
'<div class="container">\n' \
' <form method="POST" action="'+backUrl+'/followconfirm">' \
' <center>' \
' <input type="hidden" name="actor" value="'+personUrl+'">' \
' <button type="submit" class="button" name="submitYes">Follow</button>' \
' <a href="'+backUrl+'"><button class="button">Go Back</button></a>' \
' </center>' \
' </form>' \
2019-07-30 22:34:04 +00:00
'</div>'
2019-07-31 12:44:08 +00:00
profileStr+='<script>'+contentWarningScript()+'</script>'
2019-07-30 22:34:04 +00:00
result = []
i = 0
2019-08-14 20:39:26 +00:00
for item in parseUserFeed(session,outboxUrl,asHeader, \
projectVersion,httpPrefix,domain):
2019-07-30 22:34:04 +00:00
if not item.get('type'):
continue
2019-07-31 10:09:02 +00:00
if item['type']!='Create' and item['type']!='Announce':
2019-07-30 22:34:04 +00:00
continue
if not item.get('object'):
continue
profileStr+= \
individualPostAsHtml(baseDir, \
session,wfRequest,personCache, \
nickname,domain,port, \
2019-08-14 20:12:27 +00:00
item,avatarUrl,False,False, \
2019-08-25 17:28:42 +00:00
httpPrefix,projectVersion, \
False,False)
2019-07-30 22:34:04 +00:00
i+=1
if i>=20:
break
return htmlHeader(profileStyle)+profileStr+htmlFooter()