__filename__ = "webinterface.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.0.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import json
import time
import os
import commentjson
from datetime import datetime
from dateutil.parser import parse
from shutil import copyfile
from shutil import copyfileobj
from pprint import pprint
from person import personBoxJson
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
from utils import noOfAccounts
from utils import isPublicPost
from utils import getDisplayName
from follow import isFollowingActor
from webfinger import webfingerHandle
from posts import isDM
from posts import getPersonBox
from posts import getUserUrl
from posts import parseUserFeed
from posts import populateRepliesJson
from posts import isModerator
from posts import outboxMessageCreateWrap
from session import getJson
from auth import createPassword
from like import likedByPerson
from like import noOfLikes
from announce import announcedByPerson
from blocking import isBlocked
from content import getMentionsFromHtml
from content import addHtmlTags
from config import getConfigParam
from skills import getSkills
from cache import getPersonFromCache
from cache import storePersonInCache
def updateAvatarImageCache(session,baseDir: str,httpPrefix: str,actor: str,avatarUrl: str,personCache: {},force=False) -> str:
"""Updates the cached avatar for the given actor
"""
if not avatarUrl:
return None
if avatarUrl.endswith('.png') or '.png?' in avatarUrl:
sessionHeaders = {'Accept': 'image/png'}
avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.png'
elif avatarUrl.endswith('.jpg') or avatarUrl.endswith('.jpeg') or \
'.jpg?' in avatarUrl or '.jpeg?' in avatarUrl:
sessionHeaders = {'Accept': 'image/jpeg'}
avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.jpg'
elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl:
sessionHeaders = {'Accept': 'image/gif'}
avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.gif'
else:
return None
if not os.path.isfile(avatarImageFilename) or force:
try:
print('avatar image url: '+avatarUrl)
result=session.get(avatarUrl, headers=sessionHeaders, params=None)
if result.status_code<200 or result.status_code>202:
print('Avatar image download failed with status '+str(result.status_code))
# remove partial download
if os.path.isfile(avatarImageFilename):
os.remove(avatarImageFilename)
else:
with open(avatarImageFilename, 'wb') as f:
f.write(result.content)
print('avatar image downloaded for '+actor)
return avatarImageFilename.replace(baseDir+'/cache','')
except Exception as e:
print('Failed to download avatar image: '+str(avatarUrl))
print(e)
sessionHeaders = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
personJson = getJson(session,actor,sessionHeaders,None,__version__,httpPrefix,None)
if personJson:
if not personJson.get('id'):
return None
if not personJson.get('publicKey'):
return None
if not personJson['publicKey'].get('publicKeyPem'):
return None
if personJson['id']!=actor:
return None
if not personCache.get(actor):
return None
if personCache[actor]['actor']['publicKey']['publicKeyPem']!=personJson['publicKey']['publicKeyPem']:
print("ERROR: public keys don't match when downloading actor for "+actor)
return None
storePersonInCache(baseDir,actor,personJson,personCache)
return getPersonAvatarUrl(baseDir,actor,personCache)
return None
return avatarImageFilename.replace(baseDir+'/cache','')
def getPersonAvatarUrl(baseDir: str,personUrl: str,personCache: {}) -> str:
"""Returns the avatar url for the person
"""
personJson = getPersonFromCache(baseDir,personUrl,personCache)
if personJson:
actorStr=personJson['id'].replace('/','-')
avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.png'
if os.path.isfile(avatarImageFilename):
return '/avatars/'+actorStr+'.png'
avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.jpg'
if os.path.isfile(avatarImageFilename):
return '/avatars/'+actorStr+'.jpg'
avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.gif'
if os.path.isfile(avatarImageFilename):
return '/avatars/'+actorStr+'.gif'
if personJson.get('icon'):
if personJson['icon'].get('url'):
return personJson['icon']['url']
return None
def htmlSearchEmoji(translate: {},baseDir: str,searchStr: str) -> str:
"""Search results for emoji
"""
if not os.path.isfile(baseDir+'/emoji/emoji.json'):
copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
searchStr=searchStr.lower().replace(':','').strip('\n')
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
emojiCSS=cssFile.read()
emojiLookupFilename=baseDir+'/emoji/emoji.json'
# create header
emojiForm=htmlHeader(cssFilename,emojiCSS)
emojiForm+='
'+translate['Emoji Search']+'
'
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
emojiForm+='
'+translate['No results']+'
'
emojiForm+=htmlFooter()
return emojiForm
loadedEmoji=False
try:
with open(emojiLookupFilename, 'r') as fp:
emojiJson=commentjson.load(fp)
loadedEmoji=True
except Exception as e:
print(e)
if loadedEmoji:
results={}
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'
headingShown=False
emojiForm+='
'
for emojiName,filename in results.items():
if os.path.isfile(baseDir+'/emoji/'+filename):
if not headingShown:
emojiForm+='
'+translate['Copy the text then paste it into your post']+'
'
headingShown=True
emojiForm+='
:'+emojiName+':
'
emojiForm+='
'
emojiForm+=htmlFooter()
return emojiForm
def getIconsDir(baseDir: str) -> str:
"""Returns the directory where icons exist
"""
iconsDir='icons'
theme=getConfigParam(baseDir,'theme')
if theme:
if os.path.isdir(baseDir+'/img/icons/'+theme):
iconsDir='icons/'+theme
return iconsDir
def htmlSearchSharedItems(translate: {}, \
baseDir: str,searchStr: str, \
pageNumber: int, \
resultsPerPage: int, \
httpPrefix: str, \
domainFull: str,actor: str) -> str:
"""Search results for shared items
"""
iconsDir=getIconsDir(baseDir)
currPage=1
ctr=0
sharedItemsForm=''
searchStrLower=searchStr.replace('%2B','+').replace('%40','@').replace('%3A',':').replace('%23','#').lower().strip('\n')
searchStrLowerList=searchStrLower.split('+')
cssFilename=baseDir+'/epicyon.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
sharedItemsCSS=cssFile.read()
sharedItemsForm=htmlHeader(cssFilename,sharedItemsCSS)
sharedItemsForm+='
'+translate['Shared Items Search']+'
'
resultsExist=False
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
for handle in dirs:
if '@' not in handle:
continue
contactNickname=handle.split('@')[0]
sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
if not os.path.isfile(sharesFilename):
continue
sharesJson=None
try:
with open(sharesFilename, 'r') as fp:
sharesJson=commentjson.load(fp)
except Exception as e:
print(e)
if not sharesJson:
continue
for name,sharedItem in sharesJson.items():
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
if matched:
if currPage==pageNumber:
sharedItemsForm+='
'
sharedItemsForm+='
'+sharedItem['displayName']+'
'
if sharedItem.get('imageUrl'):
sharedItemsForm+=''
sharedItemsForm+=''
sharedItemsForm+='
'
if actor.endswith('/users/'+contactNickname):
sharedItemsForm+=' '
sharedItemsForm+='
'
if not resultsExist and currPage>1:
# previous page link, needs to be a POST
sharedItemsForm+= \
''
resultsExist=True
ctr+=1
if ctr>=resultsPerPage:
currPage+=1
if currPage>pageNumber:
# next page link, needs to be a POST
sharedItemsForm+= \
''
break
ctr=0
if not resultsExist:
sharedItemsForm+='
'+translate['No results']+'
'
sharedItemsForm+=htmlFooter()
return sharedItemsForm
def htmlModerationInfo(translate: {},baseDir: str) -> str:
infoForm=''
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
infoCSS=cssFile.read()
infoForm=htmlHeader(cssFilename,infoCSS)
infoForm+='
'+translate['Moderation Information']+'
'
infoShown=False
suspendedFilename=baseDir+'/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
suspendedStr = f.read()
infoForm+= \
'
' \
' '+translate['Suspended accounts']+'' \
' '+translate['These are currently suspended']+ \
' ' \
'
'
infoShown=True
blockingFilename=baseDir+'/accounts/blocking.txt'
if os.path.isfile(blockingFilename):
with open(blockingFilename, "r") as f:
blockedStr = f.read()
infoForm+= \
'
' \
' '+translate['Blocked accounts and hashtags']+'' \
' '+translate['These are globally blocked for all accounts on this instance']+ \
' ' \
'
'
infoShown=True
if not infoShown:
infoForm+='
'+translate['Any blocks or suspensions made by moderators will be shown here.']+'
'
infoForm+=htmlFooter()
return infoForm
def htmlHashtagSearch(translate: {}, \
baseDir: str,hashtag: str,pageNumber: int,postsPerPage: int, \
session,wfRequest: {},personCache: {}, \
httpPrefix: str,projectVersion: str) -> str:
"""Show a page containing search results for a hashtag
"""
iconsDir=getIconsDir(baseDir)
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()
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, '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(cssFilename,hashtagSearchCSS)
hashtagSearchForm+='
#'+hashtag+'
'
if startIndex!=len(lines)-1:
# previous page link
hashtagSearchForm+='
'
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
loadedPost=False
try:
with open(postFilename, 'r') as fp:
postJsonObject=commentjson.load(fp)
loadedPost=True
except Exception as e:
print(e)
if loadedPost:
if not isPublicPost(postJsonObject):
index-=1
continue
hashtagSearchForm+= \
individualPostAsHtml(iconsDir,translate,None, \
baseDir,session,wfRequest,personCache, \
nickname,domain,port,postJsonObject, \
None,True,False, \
httpPrefix,projectVersion, \
False,False,False,False)
index-=1
if endIndex>0:
# next page link
hashtagSearchForm+='
'
hashtagSearchForm+=htmlFooter()
return hashtagSearchForm
def htmlSkillsSearch(translate: {},baseDir: str, \
skillsearch: str,instanceOnly: bool, \
postsPerPage: int) -> str:
"""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)
loadedActor=False
try:
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
loadedActor=True
except Exception as e:
print(e)
if loadedActor:
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)
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)
loadedActor=False
try:
with open(actorFilename, 'r') as fp:
cachedActorJson=commentjson.load(fp)
loadedActor=True
except Exception as e:
print(e)
if loadedActor:
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)
results.sort(reverse=True)
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
skillSearchCSS = cssFile.read()
skillSearchForm=htmlHeader(cssFilename,skillSearchCSS)
skillSearchForm+='
'+translate['Skills search']+': '+skillsearch+'
'
if len(results)==0:
skillSearchForm+='
'+translate['No results']+'
'
else:
skillSearchForm+='
'
ctr=0
for skillMatch in results:
skillMatchFields=skillMatch.split(';')
if len(skillMatchFields)==4:
actor=skillMatchFields[1]
actorName=skillMatchFields[2]
avatarUrl=skillMatchFields[3]
skillSearchForm+='
'
ctr+=1
if ctr>=postsPerPage:
break
skillSearchForm+='
'
skillSearchForm+=htmlFooter()
return skillSearchForm
def htmlEditProfile(translate: {},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)
if not nickname:
return ''
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
if not os.path.isfile(actorFilename):
return ''
isBot=''
displayNickname=nickname
bioStr=''
manuallyApprovesFollowers=''
loadedActor=False
try:
with open(actorFilename, 'r') as fp:
actorJson=commentjson.load(fp)
loadedActor=True
except Exception as e:
print(e)
if loadedActor:
if actorJson.get('name'):
displayNickname=actorJson['name']
if actorJson.get('summary'):
bioStr=actorJson['summary'].replace('
','').replace('
','')
if actorJson.get('manuallyApprovesFollowers'):
if actorJson['manuallyApprovesFollowers']:
manuallyApprovesFollowers='checked'
else:
manuallyApprovesFollowers=''
if actorJson.get('type'):
if actorJson['type']=='Service':
isBot='checked'
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()
skills=getSkills(baseDir,nickname,domain)
skillsStr=''
skillCtr=1
if skills:
for skillDesc,skillValue in skills.items():
skillsStr+='
'
skillsStr+='
'
skillCtr+=1
skillsStr+='
'
skillsStr+='
' \
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
editProfileCSS = cssFile.read()
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= \
'
' \
' '+translate['Moderators']+' ' \
' '+translate['A list of moderator nicknames. One per line.']+ \
' ' \
'
'
editProfileForm=htmlHeader(cssFilename,editProfileCSS)
editProfileForm+= \
''
editProfileForm+=htmlFooter()
return editProfileForm
def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str,bool):
"""Receives login credentials via HTTPServer POST
"""
if not loginParams.startswith('username='):
return None,None,None
# minimum time between login attempts
currTime=int(time.time())
if currTime str:
"""Shows the login screen
"""
accounts=noOfAccounts(baseDir)
if not os.path.isfile(baseDir+'/accounts/login.png'):
copyfile(baseDir+'/img/login.png',baseDir+'/accounts/login.png')
if os.path.isfile(baseDir+'/img/login-background.png'):
if not os.path.isfile(baseDir+'/accounts/login-background.png'):
copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
if accounts>0:
loginText='
'+translate['Welcome. Please enter your login details below.']+'
'
else:
loginText='
'+translate['Please enter some credentials']+'
'
loginText+='
'+translate['You will become the admin of this site.']+'
'
if os.path.isfile(baseDir+'/accounts/login.txt'):
# custom login message
with open(baseDir+'/accounts/login.txt', 'r') as file:
loginText = '
'+file.read()+'
'
cssFilename=baseDir+'/epicyon-login.css'
if os.path.isfile(baseDir+'/login.css'):
cssFilename=baseDir+'/login.css'
with open(cssFilename, 'r') as cssFile:
loginCSS = cssFile.read()
# show the register button
registerButtonStr=''
if getConfigParam(baseDir,'registration')=='open':
if int(getConfigParam(baseDir,'registrationsRemaining'))>0:
if accounts>0:
loginText='
'+translate['Welcome. Please login or register a new account.']+'
'
loginButtonStr=''
if accounts>0:
loginButtonStr=''
loginForm=htmlHeader(cssFilename,loginCSS)
loginForm+= \
''
loginForm+=htmlFooter()
return loginForm
def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str:
"""Show the terms of service screen
"""
adminNickname = getConfigParam(baseDir,'admin')
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=''
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
termsCSS = cssFile.read()
TOSForm=htmlHeader(cssFilename,termsCSS)
TOSForm+='
'+TOSText+'
'
if adminNickname:
adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
TOSForm+='
'
TOSForm+=htmlFooter()
return TOSForm
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=''
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
termsCSS = cssFile.read()
aboutForm=htmlHeader(cssFilename,termsCSS)
aboutForm+='
'+aboutText+'
'
if adminNickname:
adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
aboutForm+='
'
aboutForm+=htmlFooter()
return aboutForm
def htmlHashtagBlocked(baseDir: str) -> str:
"""Show the screen for a blocked hashtag
"""
blockedHashtagForm=''
cssFilename=baseDir+'/epicyon-suspended.css'
if os.path.isfile(baseDir+'/suspended.css'):
cssFilename=baseDir+'/suspended.css'
with open(cssFilename, 'r') as cssFile:
blockedHashtagCSS=cssFile.read()
blockedHashtagForm=htmlHeader(cssFilename,blockedHashtagCSS)
blockedHashtagForm+='
'
# 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 '' not in customReportText:
customReportText='
'+customReportText+'
'
customReportText=customReportText.replace('
','
')
newPostText+=customReportText
newPostText+='
'+translate['This message only goes to moderators, even if it mentions other fediverse addresses.']+'
'+translate['Enter the details for your shared item below.']+'
'
if os.path.isfile(baseDir+'/accounts/newpost.txt'):
with open(baseDir+'/accounts/newpost.txt', 'r') as file:
newPostText = '
'+file.read()+'
'
cssFilename=baseDir+'/epicyon-profile.css'
if os.path.isfile(baseDir+'/epicyon.css'):
cssFilename=baseDir+'/epicyon.css'
with open(cssFilename, 'r') as cssFile:
newPostCSS = cssFile.read()
if '?' in path:
path=path.split('?')[0]
pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','')
scopeIcon='scope_public.png'
scopeDescription=translate['Public']
placeholderSubject=translate['Subject or Content Warning (optional)']+'...'
placeholderMessage=translate['Write something']+'...'
extraFields=''
endpoint='newpost'
if path.endswith('/newunlisted'):
scopeIcon='scope_unlisted.png'
scopeDescription=translate['Unlisted']
endpoint='newunlisted'
if path.endswith('/newfollowers'):
scopeIcon='scope_followers.png'
scopeDescription=translate['Followers']
endpoint='newfollowers'
if path.endswith('/newdm'):
scopeIcon='scope_dm.png'
scopeDescription=translate['DM']
endpoint='newdm'
if path.endswith('/newreport'):
scopeIcon='scope_report.png'
scopeDescription=translate['Report']
endpoint='newreport'
if path.endswith('/newshare'):
scopeIcon='scope_share.png'
scopeDescription=translate['Shared Item']
placeholderSubject=translate['Name of the shared item']+'...'
placeholderMessage=translate['Description of the item being shared']+'...'
endpoint='newshare'
extraFields= \
'
' \
' ' \
' ' \
' ' \
'
' \
''
newPostForm=htmlHeader(cssFilename,newPostCSS)
# only show the share option if this is not a reply
shareOptionOnDropdown=''
if not replyStr:
shareOptionOnDropdown='Share '+translate['Describe a shared item']+''
mentionsStr=''
for m in mentions:
mentionNickname=getNicknameFromActor(m)
if not mentionNickname:
continue
mentionDomain,mentionPort=getDomainFromActor(m)
if not mentionDomain:
continue
if mentionPort:
mentionsHandle='@'+mentionNickname+'@'+mentionDomain+':'+str(mentionPort)
else:
mentionsHandle='@'+mentionNickname+'@'+mentionDomain
if mentionsHandle not in mentionsStr:
mentionsStr+=mentionsHandle+' '
# build suffixes so that any replies or mentions are preserved when switching between scopes
dropdownNewPostSuffix='/newpost'
dropdownUnlistedSuffix='/newunlisted'
dropdownFollowersSuffix='/newfollowers'
dropdownDMSuffix='/newdm'
dropdownReportSuffix='/newreport'
if inReplyTo or mentions:
dropdownNewPostSuffix=''
dropdownUnlistedSuffix=''
dropdownFollowersSuffix=''
dropdownDMSuffix=''
dropdownReportSuffix=''
if inReplyTo:
dropdownNewPostSuffix+='?replyto='+inReplyTo
dropdownUnlistedSuffix+='?replyto='+inReplyTo
dropdownFollowersSuffix+='?replyfollowers='+inReplyTo
dropdownDMSuffix+='?replydm='+inReplyTo
for mentionedActor in mentions:
dropdownNewPostSuffix+='?mention='+mentionedActor
dropdownUnlistedSuffix+='?mention='+mentionedActor
dropdownFollowersSuffix+='?mention='+mentionedActor
dropdownDMSuffix+='?mention='+mentionedActor
dropdownReportSuffix+='?mention='+mentionedActor
dropDownContent=''
if not reportUrl:
dropDownContent= \
'
'
else:
mentionsStr='Re: '+reportUrl+'\n\n'+mentionsStr
newPostForm+= \
''
if not reportUrl:
newPostForm+=''
newPostForm=newPostForm.replace('','')
newPostForm+=htmlFooter()
return newPostForm
def htmlHeader(cssFilename: str,css=None,refreshSec=0,lang='en') -> str:
if refreshSec==0:
meta=' \n'
else:
meta=' \n'
if not css:
if '/' in cssFilename:
cssFilename=cssFilename.split('/')[-1]
htmlStr= \
'\n' \
'\n'+ \
meta+ \
' \n' \
' \n'
else:
htmlStr= \
'\n' \
'\n'+ \
meta+ \
' \n' \
' \n'
return htmlStr
def htmlFooter() -> str:
htmlStr= \
' \n' \
'\n'
return htmlStr
def htmlProfilePosts(translate: {}, \
baseDir: str,httpPrefix: str, \
authorized: bool,ocapAlways: bool, \
nickname: str,domain: str,port: int, \
session,wfRequest: {},personCache: {}, \
projectVersion: str) -> str:
"""Shows posts on the profile screen
These should only be public posts
"""
iconsDir=getIconsDir(baseDir)
profileStr=''
maxItems=4
profileStr+=''
ctr=0
currPage=1
while ctr=maxItems:
break
currPage+=1
return profileStr
def htmlProfileFollowing(translate: {},baseDir: str,httpPrefix: str, \
authorized: bool,ocapAlways: bool, \
nickname: str,domain: str,port: int, \
session,wfRequest: {},personCache: {}, \
followingJson: {},projectVersion: str, \
buttons: [], \
feedName: str,actor: str, \
pageNumber: int, \
maxItemsPerPage: int) -> str:
"""Shows following on the profile screen
"""
profileStr=''
iconsDir=getIconsDir(baseDir)
if authorized and pageNumber:
if authorized and pageNumber>1:
# page up arrow
profileStr+= \
'
'
for item in followingJson['orderedItems']:
profileStr+= \
individualFollowAsHtml(translate,baseDir,session, \
wfRequest,personCache, \
domain,item,authorized,nickname, \
httpPrefix,projectVersion, \
buttons)
if authorized and maxItemsPerPage and pageNumber:
if len(followingJson['orderedItems'])>=maxItemsPerPage:
# page down arrow
profileStr+= \
'
'
return profileStr
def htmlProfileRoles(translate: {},nickname: str,domain: str,rolesJson: {}) -> str:
"""Shows roles on the profile screen
"""
profileStr=''
for project,rolesList in rolesJson.items():
profileStr+='
'+project+'
'
for role in rolesList:
profileStr+='
'+role+'
'
profileStr+='
'
if len(profileStr)==0:
profileStr+='
@'+nickname+'@'+domain+' has no roles assigned
'
else:
profileStr='
'+profileStr+'
'
return profileStr
def htmlProfileSkills(translate: {},nickname: str,domain: str,skillsJson: {}) -> str:
"""Shows skills on the profile screen
"""
profileStr=''
for skill,level in skillsJson.items():
profileStr+='
'+skill+'
'
if len(profileStr)>0:
profileStr='
'+profileStr+'
'
return profileStr
def htmlProfileShares(translate: {},nickname: str,domain: str,sharesJson: {}) -> str:
"""Shows shares on the profile screen
"""
profileStr=''
for item in sharesJson['orderedItems']:
profileStr+='
'
profileStr+='
'+item['displayName']+'
'
if item.get('imageUrl'):
profileStr+=''
profileStr+=''
profileStr+='
'
return profileStr
def htmlProfile(translate: {},projectVersion: str, \
baseDir: str,httpPrefix: str,authorized: bool, \
ocapAlways: bool,profileJson: {},selected: str, \
session,wfRequest: {},personCache: {}, \
extraJson=None, \
pageNumber=None,maxItemsPerPage=None) -> str:
"""Show the profile page as html
"""
nickname=profileJson['preferredUsername']
if not nickname:
return ""
displayName=profileJson['name']
domain,port=getDomainFromActor(profileJson['id'])
if not domain:
return ""
domainFull=domain
if port:
domainFull=domain+':'+str(port)
profileDescription=profileJson['summary']
postsButton='button'
followingButton='button'
followersButton='button'
rolesButton='button'
skillsButton='button'
sharesButton='button'
if selected=='posts':
postsButton='buttonselected'
elif selected=='following':
followingButton='buttonselected'
elif selected=='followers':
followersButton='buttonselected'
elif selected=='roles':
rolesButton='buttonselected'
elif selected=='skills':
skillsButton='buttonselected'
elif selected=='shares':
sharesButton='buttonselected'
loginButton=''
followApprovalsSection=''
followApprovals=False
linkToTimelineStart=''
linkToTimelineEnd=''
editProfileStr=''
actor=profileJson['id']
if not authorized:
loginButton=' '
else:
editProfileStr=''
linkToTimelineStart=''
linkToTimelineEnd=''
# are there any follow requests?
followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
if os.path.isfile(followRequestsFilename):
with open(followRequestsFilename,'r') as f:
for line in f:
if len(line)>0:
followApprovals=True
followersButton='buttonhighlighted'
if selected=='followers':
followersButton='buttonselectedhighlighted'
break
if selected=='followers':
if followApprovals:
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+='
\n'
def clickToDropDownScript() -> str:
"""Function run onclick to create a dropdown
"""
script= \
'function dropdown() {\n' \
' document.getElementById("myDropdown").classList.toggle("show");\n' \
'}\n'
#'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');" \
#' }' \
#' }' \
#' }' \
#'}'
return script
def cursorToEndOfMessageScript() -> str:
"""Moves the cursor to the end of the text in a textarea
This avoids the cursor being in the wrong position when replying
"""
script = \
'function focusOnMessage() {\n' \
" var replyTextArea = document.getElementById('message');\n" \
' val = replyTextArea.value;\n' \
' if ((val.length>0) && (val.charAt(val.length-1) != " ")) {\n' \
' val += " ";\n' \
' }\n' \
' replyTextArea.focus();\n' \
' replyTextArea.value="";\n' \
' replyTextArea.value=val;\n' \
'}\n' \
"var replyTextArea = document.getElementById('message')\n" \
'replyTextArea.onFocus = function() {\n' \
' focusOnMessage();' \
'}\n'
return script
def contentWarningScript() -> str:
"""Returns a script used for content warnings
"""
script= \
'function showContentWarning(postID) {\n' \
' var x = document.getElementById(postID);\n' \
' if (x.style.display !== "block") {\n' \
' x.style.display = "block";\n' \
' } else {\n' \
' x.style.display = "none";\n' \
' }\n' \
'}\n'
return script
def htmlReplaceEmojiFromTags(content: str,tag: []) -> str:
"""Uses the tags to replace :emoji: with html image markup
"""
for tagItem in tag:
if not tagItem.get('type'):
continue
if tagItem['type']!='Emoji':
continue
if not tagItem.get('name'):
continue
if not tagItem.get('icon'):
continue
if not tagItem['icon'].get('url'):
continue
if tagItem['name'] not in content:
continue
emojiHtml=""
content=content.replace(tagItem['name'],emojiHtml)
return content
def addEmbeddedAudio(translate: {},content: str) -> str:
"""Adds embedded audio for mp3/ogg
"""
if not ('.mp3' in content or '.ogg' in content):
return content
if '