__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 posts import downloadAnnounce 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 content import replaceEmojiFromTags 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+='
'+sharedItem['summary']+'
' sharedItemsForm+=''+translate['Type']+': '+sharedItem['itemType']+' ' sharedItemsForm+=''+translate['Category']+': '+sharedItem['category']+' ' sharedItemsForm+=''+translate['Location']+': '+sharedItem['location']+'
' contactActor=httpPrefix+'://'+domainFull+'/users/'+contactNickname sharedItemsForm+='' if actor.endswith('/users/'+contactNickname): sharedItemsForm+=' ' sharedItemsForm+='
'+translate['Any blocks or suspensions made by moderators will be shown here.']+'
','').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['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.']+'
' registerButtonStr='' TOSstr=''+translate['Terms of Service']+'
' TOSstr+=''+translate['About this Instance']+'
' 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+='Administered by '+adminNickname+'
Administered by '+adminNickname+'
Hashtag Blocked
' blockedHashtagForm+='See Terms of Service
' blockedHashtagForm+='Account Suspended
' suspendedForm+='See Terms of Service
' suspendedForm+=''+translate['Write your post text below.']+'
' else: newPostText=''+translate['Write your reply to']+' '+translate['this post']+'
' replyStr='' else: newPostText= \ ''+translate['Write your report below.']+'
' # 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['Also see']+' '+translate['Terms of Service']+'
' else: newPostText=''+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= \ '@'+nickname+'@'+domain+' has no roles assigned
' else: profileStr=''+item['summary']+'
' profileStr+=''+translate['Type']+': '+item['itemType']+' ' profileStr+=''+translate['Category']+': '+item['category']+' ' profileStr+=''+translate['Location']+': '+item['location']+'
' profileStr+='@'+nickname+'@'+domainFull+'
' \ ''+profileDescription+'
'+ \ loginButton+ \ '\n'+ \
titleStr+'