epicyon/webinterface.py

6049 lines
251 KiB
Python
Raw Normal View History

2020-04-05 09:17:19 +00:00
__filename__ = "webinterface.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
2019-07-24 22:38:42 +00:00
import time
import os
2020-04-15 09:39:30 +00:00
import urllib.parse
2019-11-02 14:19:51 +00:00
from collections import OrderedDict
2019-07-31 13:11:09 +00:00
from datetime import datetime
2019-10-10 18:25:42 +00:00
from datetime import date
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-11-06 11:39:41 +00:00
from person import isPersonSnoozed
from pgp import getEmailAddress
from pgp import getPGPpubKey
2019-12-17 14:57:16 +00:00
from xmpp import getXmppAddress
2020-02-26 14:35:17 +00:00
from ssb import getSSBAddress
2020-03-22 14:42:26 +00:00
from tox import getToxAddress
2019-12-17 15:25:34 +00:00
from matrix import getMatrixAddress
from donate import getDonationUrl
2020-05-04 18:24:30 +00:00
from utils import getFileCaseInsensitive
2020-04-11 12:37:20 +00:00
from utils import searchBoxPosts
2020-02-24 23:14:49 +00:00
from utils import isBlogPost
from utils import updateRecentPostsCache
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 isPublicPostFromUrl
from utils import getDisplayName
2019-10-19 17:50:05 +00:00
from utils import getCachedPostDirectory
from utils import getCachedPostFilename
2019-10-22 11:55:06 +00:00
from utils import loadJson
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-09-28 16:10:45 +00:00
from posts import downloadAnnounce
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-11-17 14:01:49 +00:00
from bookmarks import bookmarkedByPerson
2019-08-01 12:18:22 +00:00
from announce import announcedByPerson
from blocking import isBlocked
2019-12-13 09:46:46 +00:00
from blocking import isBlockedHashtag
2020-02-19 18:51:08 +00:00
from content import switchWords
2019-08-05 19:13:15 +00:00
from content import getMentionsFromHtml
2019-09-23 10:05:16 +00:00
from content import addHtmlTags
2019-09-29 16:28:02 +00:00
from content import replaceEmojiFromTags
2019-11-04 20:39:14 +00:00
from content import removeLongWords
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-09-14 18:17:09 +00:00
from cache import storePersonInCache
2019-11-03 09:48:01 +00:00
from shares import getValidSharedItemID
from happening import todaysEventsCheck
from happening import thisWeeksEventsCheck
from happening import getCalendarEvents
2020-02-23 10:20:10 +00:00
from happening import getTodaysEvents
2020-05-02 19:24:17 +00:00
from git import isGitPatch
2020-05-28 09:11:21 +00:00
from theme import getThemesList
2019-07-20 21:13:36 +00:00
2020-04-05 09:17:19 +00:00
def getBlogAddress(actorJson: {}) -> str:
"""Returns blog address for the given actor
"""
if not actorJson.get('attachment'):
return ''
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue['name'].lower().startswith('blog'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue.get('value'):
continue
if propertyValue['type'] != 'PropertyValue':
continue
propertyValue['value'] = propertyValue['value'].strip()
if not (propertyValue['value'].startswith('https://') or
propertyValue['value'].startswith('http://') or
propertyValue['value'].startswith('dat://') or
2020-05-17 09:37:59 +00:00
propertyValue['value'].startswith('hyper://') or
propertyValue['value'].startswith('i2p://')):
continue
if '.' not in propertyValue['value']:
continue
if ' ' in propertyValue['value']:
continue
if ',' in propertyValue['value']:
continue
return propertyValue['value']
return ''
def setBlogAddress(actorJson: {}, blogAddress: str) -> None:
"""Sets an blog address for the given actor
"""
if not actorJson.get('attachment'):
actorJson['attachment'] = []
# remove any existing value
propertyFound = None
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue['name'].lower().startswith('blog'):
continue
propertyFound = propertyValue
break
if propertyFound:
actorJson['attachment'].remove(propertyFound)
if not (blogAddress.startswith('https://') or
blogAddress.startswith('http://') or
blogAddress.startswith('dat://') or
2020-05-17 09:37:59 +00:00
blogAddress.startswith('hyper://') or
blogAddress.startswith('i2p://')):
return
if '.' not in blogAddress:
return
if ' ' in blogAddress:
return
if ',' in blogAddress:
return
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue['name'].lower().startswith('blog'):
continue
if propertyValue['type'] != 'PropertyValue':
continue
propertyValue['value'] = blogAddress
return
newBlogAddress = {
"name": "Blog",
"type": "PropertyValue",
"value": blogAddress
}
actorJson['attachment'].append(newBlogAddress)
2020-04-05 09:17:19 +00:00
def updateAvatarImageCache(session, baseDir: str, httpPrefix: str,
actor: str, avatarUrl: str,
personCache: {}, force=False) -> str:
2019-09-14 17:12:03 +00:00
"""Updates the cached avatar for the given actor
"""
if not avatarUrl:
return None
2020-04-05 09:17:19 +00:00
actorStr = actor.replace('/', '-')
avatarImagePath = baseDir + '/cache/avatars/' + actorStr
if avatarUrl.endswith('.png') or \
'.png?' in avatarUrl:
sessionHeaders = {
2020-03-22 20:36:19 +00:00
'Accept': 'image/png'
}
2020-04-05 09:17:19 +00:00
avatarImageFilename = avatarImagePath + '.png'
elif (avatarUrl.endswith('.jpg') or
avatarUrl.endswith('.jpeg') or
'.jpg?' in avatarUrl or
'.jpeg?' in avatarUrl):
sessionHeaders = {
2020-03-22 20:36:19 +00:00
'Accept': 'image/jpeg'
}
2020-04-05 09:17:19 +00:00
avatarImageFilename = avatarImagePath + '.jpg'
elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl:
2020-04-05 09:17:19 +00:00
sessionHeaders = {
2020-03-22 20:36:19 +00:00
'Accept': 'image/gif'
}
2020-04-05 09:17:19 +00:00
avatarImageFilename = avatarImagePath + '.gif'
2019-11-14 15:11:20 +00:00
elif avatarUrl.endswith('.webp') or '.webp?' in avatarUrl:
2020-04-05 09:17:19 +00:00
sessionHeaders = {
2020-03-22 20:36:19 +00:00
'Accept': 'image/webp'
}
2020-04-05 09:17:19 +00:00
avatarImageFilename = avatarImagePath + '.webp'
2019-09-14 17:16:03 +00:00
else:
return None
2019-09-14 17:12:03 +00:00
if not os.path.isfile(avatarImageFilename) or force:
try:
2020-04-05 09:17:19 +00:00
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 ' +
2020-02-23 15:32:47 +00:00
str(result.status_code))
2019-09-14 19:39:51 +00:00
# remove partial download
if os.path.isfile(avatarImageFilename):
os.remove(avatarImageFilename)
else:
with open(avatarImageFilename, 'wb') as f:
f.write(result.content)
2020-04-05 09:17:19 +00:00
print('avatar image downloaded for ' + actor)
return avatarImageFilename.replace(baseDir + '/cache', '')
2020-03-22 21:16:02 +00:00
except Exception as e:
2020-04-05 09:17:19 +00:00
print('Failed to download avatar image: ' + str(avatarUrl))
2019-09-14 17:12:03 +00:00
print(e)
2020-04-05 09:17:19 +00:00
prof = 'https://www.w3.org/ns/activitystreams'
2019-10-18 12:51:37 +00:00
if '/channel/' not in actor:
2020-04-05 09:17:19 +00:00
sessionHeaders = {
'Accept': 'application/activity+json; profile="' + prof + '"'
2020-02-23 15:32:47 +00:00
}
2019-10-18 12:51:37 +00:00
else:
2020-04-05 09:17:19 +00:00
sessionHeaders = {
'Accept': 'application/ld+json; profile="' + prof + '"'
2020-02-23 15:32:47 +00:00
}
2020-04-05 09:17:19 +00:00
personJson = \
getJson(session, actor, sessionHeaders, None, __version__,
httpPrefix, None)
2019-09-14 17:12:03 +00:00
if personJson:
2019-09-14 19:06:08 +00:00
if not personJson.get('id'):
return None
if not personJson.get('publicKey'):
return None
if not personJson['publicKey'].get('publicKeyPem'):
return None
2020-04-05 09:17:19 +00:00
if personJson['id'] != actor:
2019-09-14 19:06:08 +00:00
return None
if not personCache.get(actor):
return None
2020-04-05 09:17:19 +00:00
if personCache[actor]['actor']['publicKey']['publicKeyPem'] != \
2020-02-23 15:32:47 +00:00
personJson['publicKey']['publicKeyPem']:
2020-04-05 09:17:19 +00:00
print("ERROR: " +
"public keys don't match when downloading actor for " +
actor)
2019-09-14 19:06:08 +00:00
return None
2020-04-05 09:17:19 +00:00
storePersonInCache(baseDir, actor, personJson, personCache)
return getPersonAvatarUrl(baseDir, actor, personCache)
2019-09-14 17:52:55 +00:00
return None
2020-04-05 09:17:19 +00:00
return avatarImageFilename.replace(baseDir + '/cache', '')
2019-09-14 17:12:03 +00:00
2020-04-05 09:17:19 +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
"""
2020-04-05 09:17:19 +00:00
personJson = getPersonFromCache(baseDir, personUrl, personCache)
2019-10-31 20:46:37 +00:00
if not personJson:
return None
# get from locally stored image
2020-04-05 09:17:19 +00:00
actorStr = personJson['id'].replace('/', '-')
2020-05-04 18:33:13 +00:00
avatarImagePath = baseDir + '/cache/avatars/' + actorStr
2020-05-04 18:34:38 +00:00
if os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.png')):
2020-04-05 09:17:19 +00:00
return '/avatars/' + actorStr + '.png'
2020-05-04 18:33:13 +00:00
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.jpg')):
2020-04-05 09:17:19 +00:00
return '/avatars/' + actorStr + '.jpg'
2020-05-04 18:33:13 +00:00
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.gif')):
2020-04-05 09:17:19 +00:00
return '/avatars/' + actorStr + '.gif'
2020-05-04 18:33:13 +00:00
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath + '.webp')):
2020-04-05 09:17:19 +00:00
return '/avatars/' + actorStr + '.webp'
2020-05-04 18:33:13 +00:00
elif os.path.isfile(getFileCaseInsensitive(avatarImagePath)):
2020-04-05 09:17:19 +00:00
return '/avatars/' + actorStr
2020-03-22 21:16:02 +00:00
2019-10-31 20:46:37 +00:00
if personJson.get('icon'):
if personJson['icon'].get('url'):
return personJson['icon']['url']
2019-08-18 13:30:40 +00:00
return None
2020-04-05 09:17:19 +00:00
def htmlSearchEmoji(translate: {}, baseDir: str, httpPrefix: str,
2020-02-23 15:32:47 +00:00
searchStr: str) -> str:
2019-08-19 19:02:28 +00:00
"""Search results for emoji
"""
2019-11-03 14:46:30 +00:00
# emoji.json is generated so that it can be customized and the changes
2020-03-22 21:16:02 +00:00
# will be retained even if default_emoji.json is subsequently updated
2020-04-05 09:17:19 +00:00
if not os.path.isfile(baseDir + '/emoji/emoji.json'):
copyfile(baseDir + '/emoji/default_emoji.json',
baseDir + '/emoji/emoji.json')
2020-05-22 11:32:38 +00:00
searchStr = searchStr.lower().replace(':', '').strip('\n').strip('\r')
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
emojiCSS = cssFile.read()
if httpPrefix != 'https':
emojiCSS = emojiCSS.replace('https://',
httpPrefix + '://')
emojiLookupFilename = baseDir + '/emoji/emoji.json'
2019-08-19 19:02:28 +00:00
# create header
2020-04-05 09:17:19 +00:00
emojiForm = htmlHeader(cssFilename, emojiCSS)
emojiForm += '<center><h1>' + \
translate['Emoji Search'] + \
'</h1></center>'
2019-08-19 19:02:28 +00:00
# does the lookup file exist?
if not os.path.isfile(emojiLookupFilename):
2020-04-05 09:17:19 +00:00
emojiForm += '<center><h5>' + \
translate['No results'] + '</h5></center>'
emojiForm += htmlFooter()
2019-08-19 19:02:28 +00:00
return emojiForm
2019-10-22 11:55:06 +00:00
2020-04-05 09:17:19 +00:00
emojiJson = loadJson(emojiLookupFilename)
2019-10-22 11:55:06 +00:00
if emojiJson:
2020-04-05 09:17:19 +00:00
results = {}
for emojiName, filename in emojiJson.items():
2019-08-19 19:10:55 +00:00
if searchStr in emojiName:
2020-04-05 09:17:19 +00:00
results[emojiName] = filename + '.png'
for emojiName, filename in emojiJson.items():
2019-08-19 19:10:55 +00:00
if emojiName in searchStr:
2020-04-05 09:17:19 +00:00
results[emojiName] = filename + '.png'
headingShown = False
emojiForm += '<center>'
msgStr1 = translate['Copy the text then paste it into your post']
msgStr2 = ':<img loading="lazy" class="searchEmoji" src="/emoji/'
for emojiName, filename in results.items():
if os.path.isfile(baseDir + '/emoji/' + filename):
2019-08-19 21:18:04 +00:00
if not headingShown:
2020-04-05 09:17:19 +00:00
emojiForm += \
'<center><h5>' + msgStr1 + \
2020-02-23 15:32:47 +00:00
'</h5></center>'
2020-04-05 09:17:19 +00:00
headingShown = True
emojiForm += \
'<h3>:' + emojiName + msgStr2 + \
filename + '"/></h3>'
emojiForm += '</center>'
emojiForm += htmlFooter()
2019-08-19 19:02:28 +00:00
return emojiForm
2020-04-05 09:17:19 +00:00
def getIconsDir(baseDir: str) -> str:
"""Returns the directory where icons exist
"""
2020-04-05 09:17:19 +00:00
iconsDir = 'icons'
theme = getConfigParam(baseDir, 'theme')
if theme:
2020-04-05 09:17:19 +00:00
if os.path.isdir(baseDir + '/img/icons/' + theme):
iconsDir = 'icons/' + theme
return iconsDir
2020-04-05 09:17:19 +00:00
def htmlSearchSharedItems(translate: {},
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
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
currPage = 1
ctr = 0
sharedItemsForm = ''
2020-04-15 11:10:30 +00:00
searchStrLower = urllib.parse.unquote(searchStr)
2020-05-22 11:32:38 +00:00
searchStrLower = searchStrLower.lower().strip('\n').strip('\r')
2020-04-05 09:17:19 +00:00
searchStrLowerList = searchStrLower.split('+')
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-12-10 14:48:08 +00:00
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
sharedItemsCSS = cssFile.read()
if httpPrefix != 'https':
sharedItemsCSS = \
sharedItemsCSS.replace('https://',
httpPrefix + '://')
sharedItemsForm = htmlHeader(cssFilename, sharedItemsCSS)
sharedItemsForm += \
'<center><h1>' + translate['Shared Items Search'] + \
'</h1></center>'
resultsExist = False
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
2019-08-13 21:32:18 +00:00
for handle in dirs:
if '@' not in handle:
continue
2020-04-05 09:17:19 +00:00
contactNickname = handle.split('@')[0]
sharesFilename = baseDir + '/accounts/' + handle + \
'/shares.json'
2019-08-13 21:32:18 +00:00
if not os.path.isfile(sharesFilename):
continue
2020-04-05 09:17:19 +00:00
sharesJson = loadJson(sharesFilename)
if not sharesJson:
continue
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
for name, sharedItem in sharesJson.items():
matched = True
2019-08-13 22:11:11 +00:00
for searchSubstr in searchStrLowerList:
2020-04-05 09:17:19 +00:00
subStrMatched = False
searchSubstr = searchSubstr.strip()
2019-08-13 22:11:11 +00:00
if searchSubstr in sharedItem['location'].lower():
2020-04-05 09:17:19 +00:00
subStrMatched = True
2019-08-13 22:11:11 +00:00
elif searchSubstr in sharedItem['summary'].lower():
2020-04-05 09:17:19 +00:00
subStrMatched = True
2019-08-13 22:11:11 +00:00
elif searchSubstr in sharedItem['displayName'].lower():
2020-04-05 09:17:19 +00:00
subStrMatched = True
2019-08-13 22:11:11 +00:00
elif searchSubstr in sharedItem['category'].lower():
2020-04-05 09:17:19 +00:00
subStrMatched = True
2019-08-13 22:11:11 +00:00
if not subStrMatched:
2020-04-05 09:17:19 +00:00
matched = False
2019-08-13 22:11:11 +00:00
break
2019-08-13 21:32:18 +00:00
if matched:
2020-04-05 09:17:19 +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'):
2020-04-05 09:17:19 +00:00
sharedItemsForm += \
'<a href="' + \
sharedItem['imageUrl'] + '">'
sharedItemsForm += \
'<img loading="lazy" src="' + \
sharedItem['imageUrl'] + \
2020-02-23 15:32:47 +00:00
'" alt="Item image"></a>'
2020-04-05 09:17:19 +00:00
sharedItemsForm += \
'<p>' + sharedItem['summary'] + '</p>'
sharedItemsForm += \
'<p><b>' + translate['Type'] + \
':</b> ' + sharedItem['itemType'] + ' '
sharedItemsForm += \
'<b>' + translate['Category'] + \
':</b> ' + sharedItem['category'] + ' '
sharedItemsForm += \
'<b>' + translate['Location'] + \
':</b> ' + sharedItem['location'] + '</p>'
contactActor = \
httpPrefix + '://' + domainFull + \
'/users/' + contactNickname
sharedItemsForm += \
'<p><a href="' + actor + \
'?replydm=sharedesc:' + \
sharedItem['displayName'] + \
'?mention=' + contactActor + \
'"><button class="button">' + \
translate['Contact'] + '</button></a>'
if actor.endswith('/users/' + contactNickname):
sharedItemsForm += \
' <a href="' + actor + '?rmshare=' + \
name + '"><button class="button">' + \
translate['Remove'] + '</button></a>'
sharedItemsForm += '</p></div>'
if not resultsExist and currPage > 1:
2019-08-14 09:45:51 +00:00
# previous page link, needs to be a POST
2020-04-05 09:17:19 +00:00
sharedItemsForm += \
'<form method="POST" action="' + actor + \
'/searchhandle?page=' + \
str(pageNumber - 1) + '">'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="searchtext" value="' + \
searchStrLower + '"><br>'
sharedItemsForm += \
' <center><a href="' + actor + \
'" type="submit" name="submitSearch">'
sharedItemsForm += \
' <img loading="lazy" ' + \
'class="pageicon" src="/' + iconsDir + \
'/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"/></a>'
sharedItemsForm += ' </center>'
sharedItemsForm += '</form>'
resultsExist = True
ctr += 1
if ctr >= resultsPerPage:
currPage += 1
if currPage > pageNumber:
2019-08-14 09:45:51 +00:00
# next page link, needs to be a POST
2020-04-05 09:17:19 +00:00
sharedItemsForm += \
'<form method="POST" action="' + actor + \
'/searchhandle?page=' + \
str(pageNumber + 1) + '">'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="actor" value="' + actor + '">'
sharedItemsForm += \
' <input type="hidden" ' + \
'name="searchtext" value="' + \
searchStrLower + '"><br>'
sharedItemsForm += \
' <center><a href="' + actor + \
'" type="submit" name="submitSearch">'
sharedItemsForm += \
' <img loading="lazy" ' + \
'class="pageicon" src="/' + iconsDir + \
'/pagedown.png" title="' + \
translate['Page down'] + \
'" alt="' + translate['Page down'] + \
'"/></a>'
sharedItemsForm += ' </center>'
sharedItemsForm += '</form>'
2019-08-14 09:45:51 +00:00
break
2020-04-05 09:17:19 +00:00
ctr = 0
2019-08-13 21:32:18 +00:00
if not resultsExist:
2020-04-05 09:17:19 +00:00
sharedItemsForm += \
'<center><h5>' + translate['No results'] + '</h5></center>'
sharedItemsForm += htmlFooter()
2020-03-22 21:16:02 +00:00
return sharedItemsForm
2019-08-13 21:32:18 +00:00
2019-08-13 17:25:39 +00:00
2020-04-05 09:17:19 +00:00
def htmlModerationInfo(translate: {}, baseDir: str, httpPrefix: str) -> str:
msgStr1 = \
'These are globally blocked for all accounts on this instance'
msgStr2 = \
'Any blocks or suspensions made by moderators will be shown here.'
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()
if httpPrefix != 'https':
infoCSS = infoCSS.replace('https://',
httpPrefix + '://')
infoForm = htmlHeader(cssFilename, infoCSS)
infoForm += \
'<center><h1>' + \
translate['Moderation Information'] + \
'</h1></center>'
infoShown = False
suspendedFilename = baseDir + '/accounts/suspended.txt'
2019-08-13 17:25:39 +00:00
if os.path.isfile(suspendedFilename):
with open(suspendedFilename, "r") as f:
2020-04-05 09:17:19 +00:00
suspendedStr = f.read()
infoForm += '<div class="container">'
infoForm += ' <br><b>' + \
translate['Suspended accounts'] + '</b>'
infoForm += ' <br>' + \
translate['These are currently suspended']
infoForm += \
' <textarea id="message" ' + \
'name="suspended" style="height:200px">' + \
suspendedStr + '</textarea>'
infoForm += '</div>'
infoShown = True
blockingFilename = baseDir + '/accounts/blocking.txt'
2019-08-13 17:25:39 +00:00
if os.path.isfile(blockingFilename):
with open(blockingFilename, "r") as f:
2020-04-05 09:17:19 +00:00
blockedStr = f.read()
infoForm += '<div class="container">'
infoForm += \
' <br><b>' + \
translate['Blocked accounts and hashtags'] + '</b>'
infoForm += \
' <br>' + \
translate[msgStr1]
infoForm += \
' <textarea id="message" ' + \
'name="blocked" style="height:400px">' + \
blockedStr + '</textarea>'
infoForm += '</div>'
infoShown = True
2019-08-13 17:25:39 +00:00
if not infoShown:
2020-04-05 09:17:19 +00:00
infoForm += \
'<center><p>' + \
translate[msgStr2] + \
2020-02-23 15:32:47 +00:00
'</p></center>'
2020-04-05 09:17:19 +00:00
infoForm += htmlFooter()
2020-03-22 21:16:02 +00:00
return infoForm
2019-08-12 13:22:17 +00:00
2020-04-05 09:17:19 +00:00
def htmlHashtagSearch(nickname: str, domain: str, port: int,
recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, hashtag: str, pageNumber: int,
postsPerPage: int,
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('#'):
2020-04-05 09:17:19 +00:00
hashtag = hashtag[1:]
2020-04-15 09:39:30 +00:00
hashtag = urllib.parse.unquote(hashtag)
2020-04-05 09:17:19 +00:00
hashtagIndexFile = baseDir + '/tags/' + hashtag + '.txt'
2019-08-10 10:54:52 +00:00
if not os.path.isfile(hashtagIndexFile):
2020-04-15 09:23:44 +00:00
print('WARN: hashtag file not found ' + hashtagIndexFile)
2019-08-10 10:54:52 +00:00
return None
2020-04-15 09:23:44 +00:00
iconsDir = getIconsDir(baseDir)
2019-12-13 10:33:33 +00:00
# check that the directory for the nickname exists
if nickname:
2020-04-05 09:17:19 +00:00
if not os.path.isdir(baseDir + '/accounts/' +
nickname + '@' + domain):
nickname = None
2019-12-13 10:33:33 +00:00
2019-08-10 10:54:52 +00:00
# read the index
with open(hashtagIndexFile, "r") as f:
2020-04-05 09:17:19 +00:00
lines = f.readlines()
2019-12-17 09:58:22 +00:00
# read the css
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
hashtagSearchCSS = cssFile.read()
if httpPrefix != 'https':
hashtagSearchCSS = \
hashtagSearchCSS.replace('https://',
httpPrefix + '://')
2019-08-10 10:54:52 +00:00
2019-12-17 09:58:22 +00:00
# ensure that the page number is in bounds
if not pageNumber:
2020-04-05 09:17:19 +00:00
pageNumber = 1
elif pageNumber < 1:
pageNumber = 1
2019-12-17 09:58:22 +00:00
# get the start end end within the index file
2020-04-05 09:17:19 +00:00
startIndex = int((pageNumber - 1) * postsPerPage)
endIndex = startIndex + postsPerPage
noOfLines = len(lines)
if endIndex >= noOfLines and noOfLines > 0:
endIndex = noOfLines - 1
2019-12-17 09:58:22 +00:00
# add the page title
2020-04-05 09:17:19 +00:00
hashtagSearchForm = htmlHeader(cssFilename, hashtagSearchCSS)
hashtagSearchForm += '<script>' + contentWarningScript() + '</script>'
hashtagSearchForm += '<center><h1>#' + hashtag + '</h1></center>'
2019-12-17 09:58:22 +00:00
2020-04-05 09:17:19 +00:00
if startIndex > 0:
2019-08-10 10:54:52 +00:00
# previous page link
2020-04-05 09:17:19 +00:00
hashtagSearchForm += \
'<center><a href="/tags/' + hashtag + '?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + \
'" alt="' + translate['Page up'] + \
'"></a></center>'
index = startIndex
while index <= endIndex:
2020-05-22 11:32:38 +00:00
postId = lines[index].strip('\n').strip('\r')
2019-12-12 18:56:30 +00:00
if ' ' not in postId:
2020-04-05 09:17:19 +00:00
nickname = getNicknameFromActor(postId)
2019-12-12 18:56:30 +00:00
if not nickname:
2020-04-05 09:17:19 +00:00
index += 1
2019-12-12 18:56:30 +00:00
continue
else:
2020-04-05 09:17:19 +00:00
postFields = postId.split(' ')
if len(postFields) != 3:
index = +1
2019-12-12 19:24:18 +00:00
continue
2020-04-05 09:17:19 +00:00
nickname = postFields[1]
postId = postFields[2]
postFilename = locatePost(baseDir, nickname, domain, postId)
2019-08-10 10:54:52 +00:00
if not postFilename:
2020-04-05 09:17:19 +00:00
index += 1
2019-08-10 10:54:52 +00:00
continue
2020-04-05 09:17:19 +00:00
postJsonObject = loadJson(postFilename)
2019-10-22 11:55:06 +00:00
if postJsonObject:
2019-08-10 11:31:42 +00:00
if not isPublicPost(postJsonObject):
2020-04-05 09:17:19 +00:00
index += 1
2020-03-22 21:16:02 +00:00
continue
2020-04-05 09:17:19 +00:00
showIndividualPostIcons = False
2019-12-13 10:33:33 +00:00
if nickname:
2020-04-05 09:17:19 +00:00
showIndividualPostIcons = True
allowDeletion = False
hashtagSearchForm += \
individualPostAsHtml(recentPostsCache,
maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
postJsonObject,
None, True, allowDeletion,
httpPrefix, projectVersion,
'search',
showIndividualPostIcons,
showIndividualPostIcons,
False, False, False)
index += 1
if endIndex < noOfLines - 1:
2019-08-10 10:54:52 +00:00
# next page link
2020-04-05 09:17:19 +00:00
hashtagSearchForm += \
'<center><a href="/tags/' + hashtag + \
'?page=' + str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + iconsDir + \
'/pagedown.png" title="' + translate['Page down'] + \
'" alt="' + translate['Page down'] + '"></a></center>'
hashtagSearchForm += htmlFooter()
2019-08-10 10:54:52 +00:00
return hashtagSearchForm
2020-04-05 09:17:19 +00:00
def htmlSkillsSearch(translate: {}, baseDir: str,
httpPrefix: str,
skillsearch: str, instanceOnly: bool,
2019-09-07 10:08:18 +00:00
postsPerPage: int) -> str:
2019-08-27 22:50:40 +00:00
"""Show a page containing search results for a skill
"""
if skillsearch.startswith('*'):
2020-04-05 09:17:19 +00:00
skillsearch = skillsearch[1:].strip()
2019-08-27 22:50:40 +00:00
2020-05-22 11:32:38 +00:00
skillsearch = skillsearch.lower().strip('\n').strip('\r')
2019-08-27 22:50:40 +00:00
2020-04-05 09:17:19 +00:00
results = []
2019-08-27 22:50:40 +00:00
# search instance accounts
2020-04-05 09:17:19 +00:00
for subdir, dirs, files in os.walk(baseDir + '/accounts/'):
2019-08-27 22:50:40 +00:00
for f in files:
if not f.endswith('.json'):
continue
if '@' not in f:
continue
if f.startswith('inbox@'):
continue
2020-04-05 09:17:19 +00:00
actorFilename = os.path.join(subdir, f)
actorJson = loadJson(actorFilename)
2019-10-22 11:55:06 +00:00
if actorJson:
2019-08-28 08:58:16 +00:00
if actorJson.get('id') and \
actorJson.get('skills') and \
actorJson.get('name') and \
actorJson.get('icon'):
2020-04-05 09:17:19 +00:00
actor = actorJson['id']
for skillName, skillLevel in actorJson['skills'].items():
skillName = skillName.lower()
if not (skillName in skillsearch or
2020-03-22 20:36:19 +00:00
skillsearch in skillName):
continue
2020-04-05 09:17:19 +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']
2020-03-22 20:36:19 +00:00
if indexStr not in results:
results.append(indexStr)
if not instanceOnly:
# search actor cache
2020-04-05 09:17:19 +00:00
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
2020-04-05 09:17:19 +00:00
actorFilename = os.path.join(subdir, f)
cachedActorJson = loadJson(actorFilename)
2019-10-22 11:55:06 +00:00
if cachedActorJson:
if cachedActorJson.get('actor'):
2020-04-05 09:17:19 +00:00
actorJson = cachedActorJson['actor']
if actorJson.get('id') and \
actorJson.get('skills') and \
actorJson.get('name') and \
actorJson.get('icon'):
2020-04-05 09:17:19 +00:00
actor = actorJson['id']
for skillName, skillLevel in \
actorJson['skills'].items():
skillName = skillName.lower()
if not (skillName in skillsearch or
2020-03-22 20:36:19 +00:00
skillsearch in skillName):
continue
2020-04-05 09:17:19 +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']
2020-03-22 20:36:19 +00:00
if indexStr not in results:
results.append(indexStr)
2019-08-27 22:50:40 +00:00
results.sort(reverse=True)
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
skillSearchCSS = cssFile.read()
if httpPrefix != 'https':
skillSearchCSS = \
skillSearchCSS.replace('https://',
httpPrefix + '://')
skillSearchForm = htmlHeader(cssFilename, skillSearchCSS)
skillSearchForm += \
'<center><h1>' + translate['Skills search'] + ': ' + \
skillsearch + '</h1></center>'
if len(results) == 0:
skillSearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
2019-08-27 22:50:40 +00:00
else:
2020-04-05 09:17:19 +00:00
skillSearchForm += '<center>'
ctr = 0
2019-08-27 22:50:40 +00:00
for skillMatch in results:
2020-04-05 09:17:19 +00:00
skillMatchFields = skillMatch.split(';')
if len(skillMatchFields) != 4:
2020-03-22 20:36:19 +00:00
continue
2020-04-05 09:17:19 +00:00
actor = skillMatchFields[1]
actorName = skillMatchFields[2]
avatarUrl = skillMatchFields[3]
skillSearchForm += \
'<div class="search-result""><a href="' + \
actor + '/skills">'
skillSearchForm += \
'<img loading="lazy" src="' + avatarUrl + \
'"/><span class="search-result-text">' + actorName + \
2020-03-22 20:36:19 +00:00
'</span></a></div>'
2020-04-05 09:17:19 +00:00
ctr += 1
if ctr >= postsPerPage:
2020-03-22 20:36:19 +00:00
break
2020-04-05 09:17:19 +00:00
skillSearchForm += '</center>'
skillSearchForm += htmlFooter()
2019-08-27 22:50:40 +00:00
return skillSearchForm
2020-04-05 09:17:19 +00:00
2020-04-11 12:37:20 +00:00
def htmlHistorySearch(translate: {}, baseDir: str,
httpPrefix: str,
nickname: str, domain: str,
historysearch: str,
postsPerPage: int, pageNumber: int,
projectVersion: str,
recentPostsCache: {},
maxRecentPosts: int,
session,
wfRequest,
personCache: {},
port: int) -> str:
"""Show a page containing search results for your post history
"""
if historysearch.startswith('!'):
historysearch = historysearch[1:].strip()
2020-05-22 11:32:38 +00:00
historysearch = historysearch.lower().strip('\n').strip('\r')
2020-04-11 12:37:20 +00:00
2020-04-11 12:51:05 +00:00
boxFilenames = \
2020-04-11 12:37:20 +00:00
searchBoxPosts(baseDir, nickname, domain,
historysearch, postsPerPage)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
with open(cssFilename, 'r') as cssFile:
historySearchCSS = cssFile.read()
if httpPrefix != 'https':
historySearchCSS = \
historySearchCSS.replace('https://',
httpPrefix + '://')
historySearchForm = htmlHeader(cssFilename, historySearchCSS)
2020-04-11 13:20:52 +00:00
historySearchForm += '<script>' + contentWarningScript() + '</script>'
2020-04-11 12:37:20 +00:00
# add the page title
historySearchForm += \
'<center><h1>' + translate['Your Posts'] + '</h1></center>'
2020-04-11 12:51:05 +00:00
if len(boxFilenames) == 0:
2020-04-11 12:37:20 +00:00
historySearchForm += \
'<center><h5>' + translate['No results'] + \
'</h5></center>'
return historySearchForm
iconsDir = getIconsDir(baseDir)
# ensure that the page number is in bounds
if not pageNumber:
pageNumber = 1
elif pageNumber < 1:
pageNumber = 1
# get the start end end within the index file
startIndex = int((pageNumber - 1) * postsPerPage)
endIndex = startIndex + postsPerPage
2020-04-11 12:51:05 +00:00
noOfBoxFilenames = len(boxFilenames)
if endIndex >= noOfBoxFilenames and noOfBoxFilenames > 0:
endIndex = noOfBoxFilenames - 1
2020-04-11 12:37:20 +00:00
index = startIndex
while index <= endIndex:
2020-04-11 12:51:05 +00:00
postFilename = boxFilenames[index]
2020-04-11 12:37:20 +00:00
if not postFilename:
index += 1
continue
postJsonObject = loadJson(postFilename)
if not postJsonObject:
2020-04-11 12:51:05 +00:00
index += 1
2020-04-11 12:37:20 +00:00
continue
showIndividualPostIcons = True
allowDeletion = False
historySearchForm += \
individualPostAsHtml(recentPostsCache,
maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
postJsonObject,
None, True, allowDeletion,
httpPrefix, projectVersion,
'search',
showIndividualPostIcons,
showIndividualPostIcons,
False, False, False)
index += 1
historySearchForm += htmlFooter()
return historySearchForm
2020-04-05 09:17:19 +00:00
def scheduledPostsExist(baseDir: str, nickname: str, domain: str) -> bool:
2020-01-14 10:23:17 +00:00
"""Returns true if there are posts scheduled to be delivered
"""
2020-04-05 09:17:19 +00:00
scheduleIndexFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/schedule.index'
2020-01-14 10:23:17 +00:00
if not os.path.isfile(scheduleIndexFilename):
return False
if '#users#' in open(scheduleIndexFilename).read():
return True
return False
2020-04-05 09:17:19 +00:00
def htmlEditProfile(translate: {}, baseDir: str, path: str,
domain: str, port: int, httpPrefix: str) -> str:
2019-08-02 09:52:12 +00:00
"""Shows the edit profile screen
"""
2020-04-05 09:17:19 +00:00
imageFormats = '.png, .jpg, .jpeg, .gif, .webp'
pathOriginal = path
path = path.replace('/inbox', '').replace('/outbox', '')
path = path.replace('/shares', '')
nickname = getNicknameFromActor(path)
2019-09-02 09:43:43 +00:00
if not nickname:
return ''
2020-04-05 09:17:19 +00:00
domainFull = domain
2019-08-02 09:52:12 +00:00
if port:
2020-04-05 09:17:19 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-05 09:17:19 +00:00
domainFull = domain + ':' + str(port)
2019-08-02 09:52:12 +00:00
2020-04-05 09:17:19 +00:00
actorFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '.json'
2019-08-02 09:52:12 +00:00
if not os.path.isfile(actorFilename):
return ''
2020-04-05 09:17:19 +00:00
isBot = ''
isGroup = ''
followDMs = ''
removeTwitter = ''
mediaInstanceStr = ''
displayNickname = nickname
bioStr = ''
donateUrl = ''
emailAddress = ''
PGPpubKey = ''
xmppAddress = ''
matrixAddress = ''
ssbAddress = ''
2020-05-04 11:28:43 +00:00
blogAddress = ''
2020-04-05 09:17:19 +00:00
toxAddress = ''
manuallyApprovesFollowers = ''
actorJson = loadJson(actorFilename)
2019-10-22 11:55:06 +00:00
if actorJson:
2020-04-05 09:17:19 +00:00
donateUrl = getDonationUrl(actorJson)
xmppAddress = getXmppAddress(actorJson)
matrixAddress = getMatrixAddress(actorJson)
ssbAddress = getSSBAddress(actorJson)
2020-05-04 11:28:43 +00:00
blogAddress = getBlogAddress(actorJson)
2020-04-05 09:17:19 +00:00
toxAddress = getToxAddress(actorJson)
emailAddress = getEmailAddress(actorJson)
PGPpubKey = getPGPpubKey(actorJson)
if actorJson.get('name'):
2020-04-05 09:17:19 +00:00
displayNickname = actorJson['name']
2019-08-02 09:52:12 +00:00
if actorJson.get('summary'):
2020-04-05 09:17:19 +00:00
bioStr = \
actorJson['summary'].replace('<p>', '').replace('</p>', '')
2019-08-02 09:52:12 +00:00
if actorJson.get('manuallyApprovesFollowers'):
if actorJson['manuallyApprovesFollowers']:
2020-04-05 09:17:19 +00:00
manuallyApprovesFollowers = 'checked'
2019-08-02 09:52:12 +00:00
else:
2020-04-05 09:17:19 +00:00
manuallyApprovesFollowers = ''
2019-08-07 20:13:44 +00:00
if actorJson.get('type'):
2020-04-05 09:17:19 +00:00
if actorJson['type'] == 'Service':
isBot = 'checked'
isGroup = ''
elif actorJson['type'] == 'Group':
isGroup = 'checked'
isBot = ''
if os.path.isfile(baseDir + '/accounts/' +
nickname + '@' + domain + '/.followDMs'):
followDMs = 'checked'
if os.path.isfile(baseDir + '/accounts/' +
nickname + '@' + domain + '/.removeTwitter'):
removeTwitter = 'checked'
mediaInstance = getConfigParam(baseDir, "mediaInstance")
2019-11-28 17:03:57 +00:00
if mediaInstance:
2020-04-05 09:17:19 +00:00
if mediaInstance is True:
mediaInstanceStr = 'checked'
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
filterStr = ''
filterFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/filters.txt'
2019-08-02 11:43:14 +00:00
if os.path.isfile(filterFilename):
with open(filterFilename, 'r') as filterfile:
2020-04-05 09:17:19 +00:00
filterStr = filterfile.read()
2019-08-02 11:43:14 +00:00
2020-04-05 09:17:19 +00:00
switchStr = ''
switchFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/replacewords.txt'
2020-02-19 19:06:23 +00:00
if os.path.isfile(switchFilename):
with open(switchFilename, 'r') as switchfile:
2020-04-05 09:17:19 +00:00
switchStr = switchfile.read()
2020-02-19 19:06:23 +00:00
2020-04-05 09:17:19 +00:00
blockedStr = ''
blockedFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/blocking.txt'
2019-08-02 11:43:14 +00:00
if os.path.isfile(blockedFilename):
with open(blockedFilename, 'r') as blockedfile:
2020-04-05 09:17:19 +00:00
blockedStr = blockedfile.read()
2019-08-02 11:43:14 +00:00
2020-04-05 09:17:19 +00:00
allowedInstancesStr = ''
allowedInstancesFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/allowedinstances.txt'
if os.path.isfile(allowedInstancesFilename):
with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
2020-04-05 09:17:19 +00:00
allowedInstancesStr = allowedInstancesFile.read()
gitProjectsStr = ''
gitProjectsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/gitprojects.txt'
if os.path.isfile(gitProjectsFilename):
with open(gitProjectsFilename, 'r') as gitProjectsFile:
gitProjectsStr = gitProjectsFile.read()
2020-04-05 09:17:19 +00:00
skills = getSkills(baseDir, nickname, domain)
skillsStr = ''
skillCtr = 1
2019-08-09 08:46:38 +00:00
if skills:
2020-04-05 09:17:19 +00:00
for skillDesc, skillValue in skills.items():
skillsStr += \
'<p><input type="text" placeholder="' + translate['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>'
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
editProfileCSS = cssFile.read()
if httpPrefix != 'https':
editProfileCSS = \
editProfileCSS.replace('https://', httpPrefix + '://')
instanceStr = ''
moderatorsStr = ''
themesDropdown = ''
adminNickname = getConfigParam(baseDir, 'admin')
if path.startswith('/users/' + adminNickname + '/'):
instanceDescription = \
getConfigParam(baseDir, 'instanceDescription')
instanceDescriptionShort = \
getConfigParam(baseDir, 'instanceDescriptionShort')
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
instanceStr = '<div class="container">'
instanceStr += \
' <label class="labels">' + \
translate['Instance Title'] + '</label>'
2020-05-27 10:30:40 +00:00
if instanceTitle:
instanceStr += \
' <input type="text" name="instanceTitle" value="' + \
instanceTitle + '"><br>'
else:
instanceStr += \
' <input type="text" name="instanceTitle" value=""><br>'
2020-04-05 09:17:19 +00:00
instanceStr += \
' <label class="labels">' + \
translate['Instance Short Description'] + '</label>'
2020-05-27 10:30:40 +00:00
if instanceDescriptionShort:
instanceStr += \
' <input type="text" ' + \
'name="instanceDescriptionShort" value="' + \
instanceDescriptionShort + '"><br>'
else:
instanceStr += \
' <input type="text" ' + \
'name="instanceDescriptionShort" value=""><br>'
2020-04-05 09:17:19 +00:00
instanceStr += \
' <label class="labels">' + \
translate['Instance Description'] + '</label>'
2020-05-27 10:30:40 +00:00
if instanceDescription:
instanceStr += \
' <textarea id="message" name="instanceDescription" ' + \
'style="height:200px">' + \
instanceDescription + '</textarea>'
else:
instanceStr += \
' <textarea id="message" name="instanceDescription" ' + \
'style="height:200px"></textarea>'
2020-04-05 09:17:19 +00:00
instanceStr += \
' <label class="labels">' + \
translate['Instance Logo'] + '</label>'
instanceStr += \
2020-02-23 15:32:47 +00:00
' <input type="file" id="instanceLogo" name="instanceLogo"'
2020-04-05 09:17:19 +00:00
instanceStr += ' accept="' + imageFormats + '">'
instanceStr += '</div>'
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
moderators = ''
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-12 21:20:47 +00:00
if os.path.isfile(moderatorsFile):
with open(moderatorsFile, "r") as f:
2020-04-05 09:17:19 +00:00
moderators = f.read()
moderatorsStr = '<div class="container">'
moderatorsStr += ' <b>' + translate['Moderators'] + '</b><br>'
moderatorsStr += ' ' + \
translate['A list of moderator nicknames. One per line.']
moderatorsStr += \
' <textarea id="message" name="moderators" placeholder="' + \
translate['List of moderator nicknames'] + \
'..." style="height:200px">' + moderators + '</textarea>'
moderatorsStr += '</div>'
2020-05-28 09:11:21 +00:00
themes = getThemesList()
2020-04-05 09:17:19 +00:00
themesDropdown = '<div class="container">'
themesDropdown += ' <b>' + translate['Theme'] + '</b><br>'
themesDropdown += ' <select id="themeDropdown" ' + \
'name="themeDropdown" class="theme">'
2020-05-28 09:11:21 +00:00
for themeName in themes:
themesDropdown += ' <option value="' + \
themeName.lower() + '">' + \
translate[themeName] + '</option>'
2020-04-05 09:17:19 +00:00
themesDropdown += ' </select><br>'
2020-05-26 20:47:52 +00:00
if os.path.isfile(baseDir + '/fonts/custom.woff') or \
os.path.isfile(baseDir + '/fonts/custom.woff2') or \
os.path.isfile(baseDir + '/fonts/custom.otf') or \
os.path.isfile(baseDir + '/fonts/custom.ttf'):
themesDropdown += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeCustomFont">' + \
translate['Remove the custom font'] + '<br>'
2020-04-05 09:17:19 +00:00
themesDropdown += '</div>'
themeName = getConfigParam(baseDir, 'theme')
themesDropdown = \
themesDropdown.replace('<option value="' + themeName + '">',
'<option value="' + themeName +
'" selected>')
editProfileForm = htmlHeader(cssFilename, editProfileCSS)
editProfileForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + path + '/profiledata">'
editProfileForm += ' <div class="vertical-center">'
editProfileForm += \
' <p class="new-post-text">' + translate['Profile for'] + \
' ' + nickname + '@' + domainFull + '</p>'
editProfileForm += ' <div class="container">'
editProfileForm += \
' <input type="submit" name="submitProfile" value="' + \
translate['Submit'] + '">'
editProfileForm += \
' <a href="' + pathOriginal + '"><button class="cancelbtn">' + \
translate['Cancel'] + '</button></a>'
editProfileForm += ' </div>'
if scheduledPostsExist(baseDir, nickname, domain):
editProfileForm += ' <div class="container">'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeScheduledPosts">' + \
translate['Remove scheduled posts'] + '<br>'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += ' <label class="labels">' + \
translate['Nickname'] + '</label>'
editProfileForm += \
' <input type="text" name="displayNickname" value="' + \
displayNickname + '"><br>'
editProfileForm += \
' <label class="labels">' + translate['Your bio'] + '</label>'
editProfileForm += \
' <textarea id="message" name="bio" style="height:200px">' + \
bioStr + '</textarea>'
editProfileForm += '<label class="labels">' + \
translate['Donations link'] + '</label><br>'
editProfileForm += \
' <input type="text" placeholder="https://..." ' + \
'name="donateUrl" value="' + donateUrl + '">'
editProfileForm += \
'<label class="labels">' + translate['XMPP'] + '</label><br>'
editProfileForm += \
' <input type="text" name="xmppAddress" value="' + \
xmppAddress + '">'
editProfileForm += '<label class="labels">' + \
translate['Matrix'] + '</label><br>'
editProfileForm += \
' <input type="text" name="matrixAddress" value="' + \
matrixAddress+'">'
2020-05-04 11:28:43 +00:00
2020-04-05 09:17:19 +00:00
editProfileForm += '<label class="labels">SSB</label><br>'
editProfileForm += \
' <input type="text" name="ssbAddress" value="' + \
ssbAddress + '">'
2020-05-04 11:28:43 +00:00
editProfileForm += '<label class="labels">Blog</label><br>'
editProfileForm += \
' <input type="text" name="blogAddress" value="' + \
blogAddress + '">'
2020-04-05 09:17:19 +00:00
editProfileForm += '<label class="labels">Tox</label><br>'
editProfileForm += \
' <input type="text" name="toxAddress" value="' + \
toxAddress + '">'
editProfileForm += '<label class="labels">' + \
translate['Email'] + '</label><br>'
editProfileForm += \
' <input type="text" name="email" value="' + emailAddress + '">'
editProfileForm += \
'<label class="labels">' + translate['PGP'] + '</label><br>'
editProfileForm += \
' <textarea id="message" placeholder=' + \
'"-----BEGIN PGP PUBLIC KEY BLOCK-----" name="pgp" ' + \
'style="height:100px">' + PGPpubKey + '</textarea>'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
idx = 'The files attached below should be no larger than ' + \
'10MB in total uploaded at once.'
editProfileForm += \
' <label class="labels">' + translate[idx] + '</label><br><br>'
editProfileForm += \
' <label class="labels">' + translate['Avatar image'] + '</label>'
editProfileForm += \
' <input type="file" id="avatar" name="avatar"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += \
' <br><label class="labels">' + \
translate['Background image'] + '</label>'
editProfileForm += ' <input type="file" id="image" name="image"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' <br><label class="labels">' + \
translate['Timeline banner image'] + '</label>'
editProfileForm += ' <input type="file" id="banner" name="banner"'
editProfileForm += ' accept="' + imageFormats + '">'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += \
'<label class="labels">' + translate['Change Password'] + \
'</label><br>'
editProfileForm += ' <input type="text" name="password" value=""><br>'
editProfileForm += \
'<label class="labels">' + translate['Confirm Password'] + \
'</label><br>'
editProfileForm += \
' <input type="text" name="passwordconfirm" value="">'
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="approveFollowers" ' + manuallyApprovesFollowers + \
'>' + translate['Approve follower requests'] + '<br>'
editProfileForm += \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="isBot" ' + \
isBot + '>' + translate['This is a bot account'] + '<br>'
editProfileForm += \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="isGroup" ' + isGroup + '>' + \
translate['This is a group account'] + '<br>'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="followDMs" ' + followDMs + '>' + \
translate['Only people I follow can send me DMs'] + '<br>'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="removeTwitter" ' + removeTwitter + '>' + \
translate['Remove Twitter posts'] + '<br>'
if path.startswith('/users/' + adminNickname + '/'):
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="mediaInstance" ' + mediaInstanceStr + '>' + \
translate['This is a media instance'] + '<br>'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Filtered words'] + '</label></b>'
editProfileForm += ' <br><label class="labels">' + \
translate['One per line'] + '</label>'
editProfileForm += ' <textarea id="message" ' + \
'name="filteredWords" style="height:200px">' + \
filterStr + '</textarea>'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Word Replacements'] + '</label></b>'
editProfileForm += ' <br><label class="labels">A -> B</label>'
editProfileForm += \
' <textarea id="message" name="switchWords" ' + \
'style="height:200px">' + switchStr + '</textarea>'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Blocked accounts'] + '</label></b>'
idx = 'Blocked accounts, one per line, in the form ' + \
'nickname@domain or *@blockeddomain'
editProfileForm += \
' <br><label class="labels">' + translate[idx] + '</label>'
editProfileForm += \
' <textarea id="message" name="blocked" style="height:200px">' + \
blockedStr + '</textarea>'
2020-04-05 09:17:19 +00:00
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Federation list'] + '</label></b>'
idx = 'Federate only with a defined set of instances. ' + \
'One domain name per line.'
editProfileForm += \
' <br><label class="labels">' + \
translate[idx] + '</label>'
editProfileForm += \
' <textarea id="message" name="allowedInstances" ' + \
'style="height:200px">' + allowedInstancesStr + '</textarea>'
editProfileForm += \
' <br><b><label class="labels">' + \
translate['Git Projects'] + '</label></b>'
idx = 'List of project names that you wish to receive git patches for'
editProfileForm += \
' <br><label class="labels">' + \
translate[idx] + '</label>'
editProfileForm += \
' <textarea id="message" name="gitProjects" ' + \
'style="height:100px">' + gitProjectsStr + '</textarea>'
2020-04-05 09:17:19 +00:00
editProfileForm += ' </div>'
editProfileForm += ' <div class="container">'
editProfileForm += \
' <b><label class="labels">' + \
translate['Skills'] + '</label></b><br>'
idx = '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.'
editProfileForm += ' <label class="labels">' + \
translate[idx] + '</label>'
editProfileForm += skillsStr + themesDropdown + moderatorsStr
editProfileForm += ' </div>' + instanceStr
editProfileForm += ' <div class="container">'
editProfileForm += ' <b><label class="labels">' + \
translate['Danger Zone'] + '</label></b><br>'
editProfileForm += \
' <input type="checkbox" class=dangercheckbox" ' + \
'name="deactivateThisAccount">' + \
translate['Deactivate this account'] + '<br>'
editProfileForm += ' </div>'
editProfileForm += ' </div>'
editProfileForm += '</form>'
editProfileForm += htmlFooter()
2019-08-02 09:52:12 +00:00
return editProfileForm
2020-04-05 09:17:19 +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='):
2020-04-05 09:17:19 +00:00
return None, None, None
2019-07-24 22:38:42 +00:00
# minimum time between login attempts
2020-04-05 09:17:19 +00:00
currTime = int(time.time())
if currTime < lastLoginTime+10:
return None, None, None
2019-07-24 22:38:42 +00:00
if '&' not in loginParams:
2020-04-05 09:17:19 +00:00
return None, None, None
loginArgs = loginParams.split('&')
nickname = None
password = None
register = False
2019-07-24 22:38:42 +00:00
for arg in loginArgs:
if '=' in arg:
2020-04-05 09:17:19 +00:00
if arg.split('=', 1)[0] == 'username':
nickname = arg.split('=', 1)[1]
elif arg.split('=', 1)[0] == 'password':
password = arg.split('=', 1)[1]
elif arg.split('=', 1)[0] == 'register':
register = True
return nickname, password, register
def htmlLogin(translate: {}, baseDir: str, autocomplete=True) -> str:
2019-08-10 18:22:28 +00:00
"""Shows the login screen
"""
2020-04-05 09:17:19 +00:00
accounts = noOfAccounts(baseDir)
loginImage = 'login.png'
loginImageFilename = None
if os.path.isfile(baseDir + '/accounts/' + loginImage):
loginImageFilename = baseDir + '/accounts/' + loginImage
2020-04-15 12:30:41 +00:00
elif os.path.isfile(baseDir + '/accounts/login.jpg'):
2020-04-05 09:17:19 +00:00
loginImage = 'login.jpg'
loginImageFilename = baseDir + '/accounts/' + loginImage
2020-04-15 12:30:41 +00:00
elif os.path.isfile(baseDir + '/accounts/login.jpeg'):
2020-04-05 09:17:19 +00:00
loginImage = 'login.jpeg'
loginImageFilename = baseDir + '/accounts/' + loginImage
2020-04-15 12:30:41 +00:00
elif os.path.isfile(baseDir + '/accounts/login.gif'):
2020-04-05 09:17:19 +00:00
loginImage = 'login.gif'
loginImageFilename = baseDir + '/accounts/' + loginImage
2020-04-15 12:30:41 +00:00
elif os.path.isfile(baseDir + '/accounts/login.webp'):
2020-04-05 09:17:19 +00:00
loginImage = 'login.webp'
loginImageFilename = baseDir + '/accounts/' + loginImage
2019-11-14 13:30:54 +00:00
if not loginImageFilename:
2020-04-05 09:17:19 +00:00
loginImageFilename = baseDir + '/accounts/' + loginImage
copyfile(baseDir + '/img/login.png', loginImageFilename)
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 = \
'<p class="login-text">' + \
translate['Welcome. Please enter your login details below.'] + \
'</p>'
2019-08-08 11:24:26 +00:00
else:
2020-04-05 09:17:19 +00:00
loginText = \
'<p class="login-text">' + \
translate['Please enter some credentials'] + '</p>'
loginText += \
'<p class="login-text">' + \
translate['You will become the admin of this site.'] + \
'</p>'
if os.path.isfile(baseDir + '/accounts/login.txt'):
2019-08-10 18:22:28 +00:00
# custom login message
2020-04-05 09:17:19 +00:00
with open(baseDir + '/accounts/login.txt', 'r') as file:
loginText = '<p class="login-text">' + file.read() + '</p>'
2019-07-25 19:56:25 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-login.css'
if os.path.isfile(baseDir + '/login.css'):
cssFilename = baseDir + '/login.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
loginCSS = cssFile.read()
2019-07-25 19:56:25 +00:00
2019-08-08 11:24:26 +00:00
# show the register button
2020-04-05 09:17:19 +00:00
registerButtonStr = ''
if getConfigParam(baseDir, 'registration') == 'open':
if int(getConfigParam(baseDir, 'registrationsRemaining')) > 0:
if accounts > 0:
idx = 'Welcome. Please login or register a new account.'
loginText = \
'<p class="login-text">' + \
translate[idx] + \
'</p>'
registerButtonStr = \
2020-02-23 15:32:47 +00:00
'<button type="submit" name="register">Register</button>'
2020-04-05 09:17:19 +00:00
TOSstr = \
'<p class="login-text"><a href="/terms">' + \
translate['Terms of Service'] + '</a></p>'
TOSstr += \
'<p class="login-text"><a href="/about">' + \
translate['About this Instance'] + '</a></p>'
2019-08-08 13:38:33 +00:00
2020-04-05 09:17:19 +00:00
loginButtonStr = ''
if accounts > 0:
loginButtonStr = \
'<button type="submit" name="submit">' + \
translate['Login'] + '</button>'
2020-04-05 09:17:19 +00:00
autocompleteStr = ''
if not autocomplete:
2020-04-05 09:17:19 +00:00
autocompleteStr = 'autocomplete="off" value=""'
2020-04-05 09:17:19 +00:00
loginForm = htmlHeader(cssFilename, loginCSS)
loginForm += '<form method="POST" action="/login">'
loginForm += ' <div class="imgcontainer">'
loginForm += \
' <img loading="lazy" src="' + loginImage + \
2020-02-23 15:32:47 +00:00
'" alt="login image" class="loginimage">'
2020-04-05 09:17:19 +00:00
loginForm += loginText + TOSstr
loginForm += ' </div>'
loginForm += ''
loginForm += ' <div class="container">'
loginForm += ' <label for="nickname"><b>' + \
translate['Nickname'] + '</b></label>'
loginForm += \
' <input type="text" ' + autocompleteStr + ' placeholder="' + \
translate['Enter Nickname'] + '" name="username" required autofocus>'
loginForm += ''
loginForm += ' <label for="password"><b>' + \
translate['Password'] + '</b></label>'
loginForm += \
' <input type="password" ' + autocompleteStr + \
' placeholder="' + translate['Enter Password'] + \
'" name="password" required>'
loginForm += registerButtonStr + loginButtonStr
loginForm += ' </div>'
loginForm += '</form>'
loginForm += \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
'<img loading="lazy" class="license" title="' + \
translate['Get the source code'] + '" alt="' + \
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'
loginForm += htmlFooter()
2019-07-24 22:38:42 +00:00
return loginForm
2020-04-05 09:17:19 +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
"""
2020-04-05 09:17:19 +00:00
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'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
termsCSS = cssFile.read()
if httpPrefix != 'https':
termsCSS = termsCSS.replace('https://', httpPrefix+'://')
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
TOSForm = htmlHeader(cssFilename, termsCSS)
TOSForm += '<div class="container">' + TOSText + '</div>'
2019-08-10 15:33:18 +00:00
if adminNickname:
2020-04-05 09:17:19 +00:00
adminActor = httpPrefix + '://' + domainFull + \
'/users/' + adminNickname
TOSForm += \
'<div class="container"><center>' + \
'<p class="administeredby">Administered by <a href="' + \
adminActor + '">' + adminNickname + '</a></p></center></div>'
TOSForm += htmlFooter()
2019-08-08 13:38:33 +00:00
return TOSForm
2020-04-05 09:17:19 +00:00
2020-04-17 16:30:06 +00:00
def htmlAbout(baseDir: str, httpPrefix: str,
domainFull: str, onionDomain: str) -> str:
2019-08-26 16:02:47 +00:00
"""Show the about screen
"""
2020-04-05 09:17:19 +00:00
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'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
termsCSS = cssFile.read()
if httpPrefix != 'http':
termsCSS = termsCSS.replace('https://',
httpPrefix + '://')
2019-12-10 14:48:08 +00:00
2020-04-05 09:17:19 +00:00
aboutForm = htmlHeader(cssFilename, termsCSS)
aboutForm += '<div class="container">' + aboutText + '</div>'
2020-04-17 16:30:06 +00:00
if onionDomain:
aboutForm += \
2020-04-17 16:38:25 +00:00
'<div class="container"><center><p class="administeredby">' + \
'http://' + onionDomain + '</p></center></div>'
2019-08-26 16:02:47 +00:00
if adminNickname:
2020-04-05 09:17:19 +00:00
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()
2019-08-26 16:02:47 +00:00
return aboutForm
2020-04-05 09:17:19 +00:00
2019-08-14 10:32:15 +00:00
def htmlHashtagBlocked(baseDir: str) -> str:
"""Show the screen for a blocked hashtag
"""
2020-04-05 09:17:19 +00:00
blockedHashtagForm = ''
cssFilename = baseDir + '/epicyon-suspended.css'
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
blockedHashtagCSS = cssFile.read()
blockedHashtagForm = htmlHeader(cssFilename,
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()
2019-08-14 10:32:15 +00:00
return blockedHashtagForm
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
2019-08-13 09:24:55 +00:00
def htmlSuspended(baseDir: str) -> str:
"""Show the screen for suspended accounts
"""
2020-04-05 09:17:19 +00:00
suspendedForm = ''
cssFilename = baseDir + '/epicyon-suspended.css'
if os.path.isfile(baseDir + '/suspended.css'):
cssFilename = baseDir + '/suspended.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
suspendedCSS = cssFile.read()
suspendedForm = htmlHeader(cssFilename, 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()
2019-08-13 09:24:55 +00:00
return suspendedForm
2020-04-05 09:17:19 +00:00
def htmlNewPost(mediaInstance: bool, translate: {},
baseDir: str, httpPrefix: str,
path: str, inReplyTo: str,
mentions: [],
reportUrl: str, pageNumber: int,
nickname: str, domain: str) -> str:
2019-08-19 19:50:07 +00:00
"""New post screen
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
replyStr = ''
2020-04-05 09:17:19 +00:00
showPublicOnDropdown = True
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:
2020-04-05 09:17:19 +00:00
newPostText = '<p class="new-post-text">' + \
translate['Write your post text below.'] + '</p>'
2019-08-11 11:25:27 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostText = \
'<p class="new-post-text">' + \
translate['Write your reply to'] + \
' <a href="' + inReplyTo + '">' + \
translate['this post'] + '</a></p>'
replyStr = '<input type="hidden" ' + \
'name="replyTo" value="' + inReplyTo + '">'
# if replying to a non-public post then also make
# this post non-public
if not isPublicPostFromUrl(baseDir, nickname, domain,
inReplyTo):
newPostPath = path
if '?' in newPostPath:
2020-04-05 09:17:19 +00:00
newPostPath = newPostPath.split('?')[0]
if newPostPath.endswith('/newpost'):
2020-04-05 09:17:19 +00:00
path = path.replace('/newpost', '/newfollowers')
elif newPostPath.endswith('/newunlisted'):
2020-04-05 09:17:19 +00:00
path = path.replace('/newunlisted', '/newfollowers')
showPublicOnDropdown = False
2019-07-31 13:51:10 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostText = \
'<p class="new-post-text">' + \
translate['Write your report below.'] + '</p>'
# custom report header with any additional instructions
2020-04-05 09:17:19 +00:00
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:
2020-04-05 09:17:19 +00:00
customReportText = \
'<p class="login-subtext">' + \
customReportText + '</p>'
repStr = '<p class="login-subtext">'
customReportText = \
customReportText.replace('<p>', repStr)
newPostText += customReportText
idx = 'This message only goes to moderators, even if it ' + \
'mentions other fediverse addresses.'
newPostText += \
'<p class="new-post-subtext">' + translate[idx] + \
'</p><p class="new-post-subtext">' + translate['Also see'] + \
' <a href="/terms">' + \
translate['Terms of Service'] + '</a></p>'
2019-07-28 11:35:57 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostText = \
'<p class="new-post-text">' + \
translate['Enter the details for your shared item below.'] + '</p>'
2019-11-25 20:46:52 +00:00
2019-11-28 12:38:37 +00:00
if path.endswith('/newquestion'):
2020-04-05 09:17:19 +00:00
newPostText = \
'<p class="new-post-text">' + \
translate['Enter the choices for your question below.'] + '</p>'
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>'
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
newPostCSS = cssFile.read()
if httpPrefix != 'https':
newPostCSS = newPostCSS.replace('https://',
httpPrefix + '://')
2019-07-25 21:39:09 +00:00
if '?' in path:
2020-04-05 09:17:19 +00:00
path = path.split('?')[0]
pathBase = path.replace('/newreport', '').replace('/newpost', '')
pathBase = pathBase.replace('/newblog', '').replace('/newshare', '')
pathBase = pathBase.replace('/newunlisted', '')
pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '')
newPostImageSection = ' <div class="container">'
newPostImageSection += \
' <label class="labels">' + translate['Image description'] + \
'</label>'
newPostImageSection += ' <input type="text" name="imageDescription">'
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, ' + \
'.webm, .ogv, .mp3, .ogg">'
newPostImageSection += ' </div>'
scopeIcon = 'scope_public.png'
scopeDescription = translate['Public']
placeholderSubject = \
translate['Subject or Content Warning (optional)'] + '...'
placeholderMessage = translate['Write something'] + '...'
extraFields = ''
endpoint = 'newpost'
2020-02-24 13:32:19 +00:00
if path.endswith('/newblog'):
2020-04-05 09:17:19 +00:00
placeholderSubject = translate['Title']
scopeIcon = 'scope_blog.png'
scopeDescription = translate['Blog']
endpoint = 'newblog'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newunlisted'):
2020-04-05 09:17:19 +00:00
scopeIcon = 'scope_unlisted.png'
scopeDescription = translate['Unlisted']
endpoint = 'newunlisted'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newfollowers'):
2020-04-05 09:17:19 +00:00
scopeIcon = 'scope_followers.png'
scopeDescription = translate['Followers']
endpoint = 'newfollowers'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newdm'):
2020-04-05 09:17:19 +00:00
scopeIcon = 'scope_dm.png'
scopeDescription = translate['DM']
endpoint = 'newdm'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newreport'):
2020-04-05 09:17:19 +00:00
scopeIcon = 'scope_report.png'
scopeDescription = translate['Report']
endpoint = 'newreport'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newquestion'):
2020-04-05 09:17:19 +00:00
scopeIcon = 'scope_question.png'
scopeDescription = translate['Question']
placeholderMessage = translate['Enter your question'] + '...'
endpoint = 'newquestion'
extraFields = '<div class="container">'
extraFields += ' <label class="labels">' + \
translate['Possible answers'] + ':</label><br>'
2019-11-26 12:17:52 +00:00
for questionCtr in range(8):
2020-04-05 09:17:19 +00:00
extraFields += \
' <input type="text" class="questionOption" placeholder="' + \
str(questionCtr + 1) + \
'" name="questionOption' + str(questionCtr) + '"><br>'
extraFields += \
' <label class="labels">' + \
translate['Duration of listing in days'] + \
':</label> <input type="number" name="duration" ' + \
'min="1" max="365" step="1" value="14"><br>'
extraFields += '</div>'
2020-02-24 13:32:19 +00:00
elif path.endswith('/newshare'):
2020-04-05 09:17:19 +00:00
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 = '<div class="container">'
extraFields += \
' <label class="labels">' + \
translate['Type of shared item. eg. hat'] + ':</label>'
extraFields += ' <input type="text" class="itemType" name="itemType">'
extraFields += \
' <br><label class="labels">' + \
translate['Category of shared item. eg. clothing'] + ':</label>'
extraFields += ' <input type="text" class="category" name="category">'
extraFields += \
' <br><label class="labels">' + \
translate['Duration of listing in days'] + ':</label>'
extraFields += ' <input type="number" name="duration" ' + \
'min="1" max="365" step="1" value="14">'
extraFields += '</div>'
extraFields += '<div class="container">'
extraFields += \
'<label class="labels">' + \
translate['City or location of the shared item'] + ':</label>'
extraFields += '<input type="text" name="location">'
extraFields += '</div>'
dateAndLocation = ''
if endpoint != 'newshare' and \
endpoint != 'newreport' and \
endpoint != 'newquestion':
dateAndLocation = '<div class="container">'
2020-01-12 13:05:36 +00:00
if not inReplyTo:
2020-04-05 09:17:19 +00:00
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="schedulePost"><label class="labels">' + \
translate['This is a scheduled post.'] + '</label></p>'
dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>'
dateAndLocation += '<label class="labels">' + \
translate['Date'] + ': </label>'
dateAndLocation += '<input type="date" name="eventDate">'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += '<input type="time" name="eventTime"></label></p>'
dateAndLocation += '</div>'
dateAndLocation += '<div class="container">'
dateAndLocation += '<br><label class="labels">' + \
translate['Location'] + ': </label>'
dateAndLocation += '<input type="text" name="location">'
dateAndLocation += '</div>'
newPostForm = htmlHeader(cssFilename, newPostCSS)
# only show the share option if this is not a reply
2020-04-05 09:17:19 +00:00
shareOptionOnDropdown = ''
questionOptionOnDropdown = ''
if not replyStr:
2020-04-05 09:17:19 +00:00
shareOptionOnDropdown = \
'<a href="' + pathBase + \
'/newshare"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_share.png"/><b>' + translate['Shares'] + \
'</b><br>' + translate['Describe a shared item'] + '</a>'
questionOptionOnDropdown = \
'<a href="' + pathBase + \
'/newquestion"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_question.png"/><b>' + translate['Question'] + \
'</b><br>' + translate['Ask a question'] + '</a>'
mentionsStr = ''
2019-08-05 19:13:15 +00:00
for m in mentions:
2020-04-05 09:17:19 +00:00
mentionNickname = getNicknameFromActor(m)
2019-08-05 19:13:15 +00:00
if not mentionNickname:
continue
2020-04-05 09:17:19 +00:00
mentionDomain, mentionPort = getDomainFromActor(m)
2019-08-05 19:13:15 +00:00
if not mentionDomain:
continue
2019-09-22 17:54:33 +00:00
if mentionPort:
2020-04-05 09:17:19 +00:00
mentionsHandle = \
'@' + mentionNickname + '@' + \
mentionDomain + ':' + str(mentionPort)
2019-08-05 19:13:15 +00:00
else:
2020-04-05 09:17:19 +00:00
mentionsHandle = '@' + mentionNickname + '@' + mentionDomain
2019-09-22 17:54:33 +00:00
if mentionsHandle not in mentionsStr:
2020-04-05 09:17:19 +00:00
mentionsStr += mentionsHandle + ' '
2019-08-05 19:13:15 +00:00
2020-02-23 15:32:47 +00:00
# build suffixes so that any replies or mentions are
# preserved when switching between scopes
2020-04-05 09:17:19 +00:00
dropdownNewPostSuffix = '/newpost'
dropdownNewBlogSuffix = '/newblog'
dropdownUnlistedSuffix = '/newunlisted'
dropdownFollowersSuffix = '/newfollowers'
dropdownDMSuffix = '/newdm'
dropdownReportSuffix = '/newreport'
if inReplyTo or mentions:
2020-04-05 09:17:19 +00:00
dropdownNewPostSuffix = ''
dropdownNewBlogSuffix = ''
dropdownUnlistedSuffix = ''
dropdownFollowersSuffix = ''
dropdownDMSuffix = ''
dropdownReportSuffix = ''
if inReplyTo:
2020-04-05 09:17:19 +00:00
dropdownNewPostSuffix += '?replyto=' + inReplyTo
dropdownNewBlogSuffix += '?replyto=' + inReplyTo
dropdownUnlistedSuffix += '?replyto=' + inReplyTo
dropdownFollowersSuffix += '?replyfollowers=' + inReplyTo
dropdownDMSuffix += '?replydm=' + inReplyTo
for mentionedActor in mentions:
2020-04-05 09:17:19 +00:00
dropdownNewPostSuffix += '?mention=' + mentionedActor
dropdownNewBlogSuffix += '?mention=' + mentionedActor
dropdownUnlistedSuffix += '?mention=' + mentionedActor
dropdownFollowersSuffix += '?mention=' + mentionedActor
dropdownDMSuffix += '?mention=' + mentionedActor
dropdownReportSuffix += '?mention=' + mentionedActor
dropDownContent = ''
if not reportUrl:
2020-04-05 09:17:19 +00:00
dropDownContent += \
' <div id="myDropdown" class="dropdown-content">'
if showPublicOnDropdown:
2020-04-05 09:17:19 +00:00
dropDownContent += \
' <a href="' + pathBase+dropdownNewPostSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_public.png"/><b>' + \
translate['Public'] + '</b><br>' + \
translate['Visible to anyone'] + '</a>'
dropDownContent += \
' <a href="' + pathBase+dropdownNewBlogSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/edit.png"/><b>' + \
translate['Blog'] + '</b><br>' + \
translate['Publicly visible post'] + '</a>'
dropDownContent += \
' <a href="' + pathBase+dropdownUnlistedSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir+'/scope_unlisted.png"/><b>' + \
translate['Unlisted'] + '</b><br>' + \
translate['Not on public timeline'] + '</a>'
dropDownContent += \
' <a href="' + pathBase+dropdownFollowersSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_followers.png"/><b>' + \
translate['Followers'] + '</b><br>' + \
translate['Only to followers'] + '</a>'
dropDownContent += \
' <a href="' + \
pathBase + dropdownDMSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_dm.png"/><b>' + translate['DM'] + \
'</b><br>' + translate['Only to mentioned people'] + '</a>'
dropDownContent += \
' <a href="' + pathBase + dropdownReportSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + iconsDir + \
'/scope_report.png"/><b>' + translate['Report'] + \
'</b><br>' + translate['Send to moderators'] + '</a>'
dropDownContent += questionOptionOnDropdown + shareOptionOnDropdown
dropDownContent += ' </div>'
else:
2020-04-05 09:17:19 +00:00
mentionsStr = 'Re: ' + reportUrl + '\n\n' + mentionsStr
newPostForm += \
'<form enctype="multipart/form-data" method="POST" ' + \
'accept-charset="UTF-8" action="' + \
path + '?' + endpoint + '?page=' + str(pageNumber) + '">'
newPostForm += ' <div class="vertical-center">'
newPostForm += \
' <label for="nickname"><b>' + newPostText + '</b></label>'
newPostForm += ' <div class="container">'
newPostForm += ' <div class="dropbtn" onclick="dropdown()">'
newPostForm += \
' <img loading="lazy" alt="" title="" src="/' + iconsDir + \
'/' + scopeIcon + '"/><b class="scope-desc">' + \
scopeDescription + '</b>'
newPostForm += dropDownContent
newPostForm += ' </div>'
newPostForm += \
' <a href="' + pathBase + \
'/searchemoji"><img loading="lazy" class="emojisearch" ' + \
'src="/emoji/1F601.png" title="' + \
translate['Search for emoji'] + '" alt="' + \
translate['Search for emoji'] + '"/></a>'
newPostForm += ' </div>'
newPostForm += ' <div class="container"><center>'
newPostForm += \
' <a href="' + pathBase + \
'/inbox"><button class="cancelbtn">' + \
translate['Cancel'] + '</button></a>'
newPostForm += \
' <input type="submit" name="submitPost" value="' + \
translate['Submit'] + '">'
newPostForm += ' </center></div>'
newPostForm += replyStr
if mediaInstance and not replyStr:
2020-04-05 09:17:19 +00:00
newPostForm += newPostImageSection
newPostForm += \
' <label class="labels">' + placeholderSubject + '</label><br>'
newPostForm += ' <input type="text" name="subject">'
newPostForm += ''
newPostForm += \
' <br><label class="labels">' + placeholderMessage + '</label>'
messageBoxHeight = 400
if mediaInstance:
2020-04-05 09:17:19 +00:00
messageBoxHeight = 200
2020-02-24 13:35:20 +00:00
2020-04-05 09:17:19 +00:00
if endpoint == 'newquestion':
messageBoxHeight = 100
elif endpoint == 'newblog':
messageBoxHeight = 800
2020-02-24 13:35:20 +00:00
2020-04-05 09:17:19 +00:00
newPostForm += \
' <textarea id="message" name="message" style="height:' + \
str(messageBoxHeight) + 'px">' + mentionsStr + '</textarea>'
newPostForm += extraFields+dateAndLocation
if not mediaInstance or replyStr:
2020-04-05 09:17:19 +00:00
newPostForm += newPostImageSection
newPostForm += ' </div>'
newPostForm += '</form>'
2019-08-24 18:00:15 +00:00
if not reportUrl:
2020-04-05 09:17:19 +00:00
newPostForm += \
'<script>' + clickToDropDownScript() + \
cursorToEndOfMessageScript() + '</script>'
newPostForm = \
newPostForm.replace('<body>', '<body onload="focusOnMessage()">')
2019-08-24 18:00:15 +00:00
2020-04-05 09:17:19 +00:00
newPostForm += htmlFooter()
2019-07-25 21:39:09 +00:00
return newPostForm
2020-04-05 09:17:19 +00:00
def htmlHeader(cssFilename: str, css=None, refreshSec=0, lang='en') -> str:
if refreshSec == 0:
meta = ' <meta charset="utf-8">\n'
2019-08-22 12:14:24 +00:00
else:
2020-04-05 09:17:19 +00:00
meta = \
' <meta http-equiv="Refresh" content="' + \
str(refreshSec) + '" charset="utf-8">\n'
2019-08-22 12:14:24 +00:00
2019-09-11 09:58:43 +00:00
if not css:
if '/' in cssFilename:
2020-04-05 09:17:19 +00:00
cssFilename = cssFilename.split('/')[-1]
htmlStr = '<!DOCTYPE html>\n'
htmlStr += '<html lang="' + lang + '">\n'
htmlStr += meta
htmlStr += ' <style>\n'
htmlStr += ' @import url("' + cssFilename + '");\n'
htmlStr += ' background-color: #282c37'
htmlStr += ' </style>\n'
htmlStr += ' <body>\n'
2019-07-21 18:18:58 +00:00
else:
2020-04-05 09:17:19 +00:00
htmlStr = '<!DOCTYPE html>\n'
htmlStr += '<html lang="' + lang + '">\n'
htmlStr += meta
htmlStr += ' <style>\n' + css + '</style>\n'
htmlStr += ' <body>\n'
2019-07-20 21:13:36 +00:00
return htmlStr
2020-04-05 09:17:19 +00:00
2019-07-20 21:13:36 +00:00
def htmlFooter() -> str:
2020-04-05 09:17:19 +00:00
htmlStr = ' </body>\n'
htmlStr += '</html>\n'
2019-07-20 21:13:36 +00:00
return htmlStr
2020-04-05 09:17:19 +00:00
def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
translate: {},
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
projectVersion: str) -> str:
2019-07-22 09:38:02 +00:00
"""Shows posts on the profile screen
2019-09-02 11:59:15 +00:00
These should only be public posts
2019-07-22 09:38:02 +00:00
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
profileStr = ''
maxItems = 4
profileStr += '<script>' + contentWarningScript() + '</script>'
ctr = 0
currPage = 1
while ctr < maxItems and currPage < 4:
outboxFeed = \
personBoxJson({}, session, baseDir, domain,
port,
'/users/' + nickname + '/outbox?page=' +
str(currPage),
httpPrefix,
10, 'outbox',
authorized,
2019-09-02 18:58:20 +00:00
ocapAlways)
if not outboxFeed:
break
2020-04-05 09:17:19 +00:00
if len(outboxFeed['orderedItems']) == 0:
2019-09-02 18:58:20 +00:00
break
for item in outboxFeed['orderedItems']:
2020-04-05 09:17:19 +00:00
if item['type'] == 'Create':
postStr = \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
False, False, False, True, False)
2019-09-02 18:58:20 +00:00
if postStr:
2020-04-05 09:17:19 +00:00
profileStr += postStr
ctr += 1
if ctr >= maxItems:
2019-09-02 18:58:20 +00:00
break
2020-04-05 09:17:19 +00:00
currPage += 1
2019-07-22 09:38:02 +00:00
return profileStr
2020-04-05 09:17:19 +00:00
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
"""
2020-04-05 09:17:19 +00:00
profileStr = ''
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
if authorized and pageNumber:
2020-04-05 09:17:19 +00:00
if authorized and pageNumber > 1:
# page up arrow
2020-04-05 09:17:19 +00:00
profileStr += \
'<center><a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a></center>'
2020-02-23 15:32:47 +00:00
for item in followingJson['orderedItems']:
2020-04-05 09:17:19 +00:00
profileStr += \
individualFollowAsHtml(translate, baseDir, session,
wfRequest, personCache,
domain, item, authorized, nickname,
httpPrefix, projectVersion,
2019-08-14 20:12:27 +00:00
buttons)
if authorized and maxItemsPerPage and pageNumber:
2020-04-05 09:17:19 +00:00
if len(followingJson['orderedItems']) >= maxItemsPerPage:
# page down arrow
2020-04-05 09:17:19 +00:00
profileStr += \
'<center><a href="' + actor + '/' + feedName + \
'?page=' + str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a></center>'
return profileStr
2020-04-05 09:17:19 +00:00
def htmlProfileRoles(translate: {}, nickname: str, domain: str,
rolesJson: {}) -> str:
2019-07-22 17:21:45 +00:00
"""Shows roles on the profile screen
"""
2020-04-05 09:17:19 +00:00
profileStr = ''
for project, rolesList in rolesJson.items():
profileStr += \
'<div class="roles"><h2>' + project + \
'</h2><div class="roles-inner">'
2019-07-22 17:21:45 +00:00
for role in rolesList:
2020-04-05 09:17:19 +00:00
profileStr += '<h3>' + role + '</h3>'
profileStr += '</div></div>'
if len(profileStr) == 0:
profileStr += \
'<p>@' + nickname + '@' + domain + ' has no roles assigned</p>'
2019-07-22 17:21:45 +00:00
else:
2020-04-05 09:17:19 +00:00
profileStr = '<div>' + profileStr + '</div>'
2019-07-22 17:21:45 +00:00
return profileStr
2020-04-05 09:17:19 +00:00
def htmlProfileSkills(translate: {}, nickname: str, domain: str,
2020-02-23 15:32:47 +00:00
skillsJson: {}) -> str:
2019-07-22 20:01:46 +00:00
"""Shows skills on the profile screen
"""
2020-04-05 09:17:19 +00:00
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 = '<center><div class="skill-title">' + \
profileStr + '</div></center>'
2019-07-22 20:01:46 +00:00
return profileStr
2020-04-05 09:17:19 +00:00
def htmlIndividualShare(actor: str, item: {}, translate: {},
showContact: bool, removeButton: bool) -> str:
2019-11-02 14:19:51 +00:00
"""Returns an individual shared item as html
"""
2020-04-05 09:17:19 +00:00
profileStr = '<div class="container">'
profileStr += '<p class="share-title">' + item['displayName'] + '</p>'
2019-11-02 14:19:51 +00:00
if item.get('imageUrl'):
2020-04-05 09:17:19 +00:00
profileStr += '<a href="' + item['imageUrl'] + '">'
profileStr += \
'<img loading="lazy" src="' + item['imageUrl'] + \
'" alt="' + translate['Item image'] + '"></a>'
profileStr += '<p>' + item['summary'] + '</p>'
profileStr += \
'<p><b>' + translate['Type'] + ':</b> ' + item['itemType'] + ' '
profileStr += \
'<b>' + translate['Category'] + ':</b> ' + item['category'] + ' '
profileStr += \
'<b>' + translate['Location'] + ':</b> ' + item['location'] + '</p>'
2019-11-02 14:19:51 +00:00
if showContact:
2020-04-05 09:17:19 +00:00
contactActor = item['actor']
profileStr += \
'<p><a href="' + actor + \
'?replydm=sharedesc:' + item['displayName'] + \
'?mention=' + contactActor + '"><button class="button">' + \
translate['Contact'] + '</button></a>'
if removeButton:
2020-04-05 09:17:19 +00:00
profileStr += \
' <a href="' + actor + '?rmshare=' + item['displayName'] + \
'"><button class="button">' + \
translate['Remove'] + '</button></a>'
profileStr += '</div>'
2019-11-02 14:19:51 +00:00
return profileStr
2020-04-05 09:17:19 +00:00
def htmlProfileShares(actor: str, translate: {},
nickname: str, domain: str, sharesJson: {}) -> str:
2019-07-23 12:33:09 +00:00
"""Shows shares on the profile screen
"""
2020-04-05 09:17:19 +00:00
profileStr = ''
2019-07-23 12:33:09 +00:00
for item in sharesJson['orderedItems']:
2020-04-05 09:17:19 +00:00
profileStr += htmlIndividualShare(actor, item, translate, False, False)
if len(profileStr) > 0:
profileStr = '<div class="share-title">' + profileStr + '</div>'
2019-07-23 12:33:09 +00:00
return profileStr
2020-04-05 09:17:19 +00:00
def sharesTimelineJson(actor: str, pageNumber: int, itemsPerPage: int,
baseDir: str, maxSharesPerAccount: int) -> ({}, bool):
2019-11-02 14:19:51 +00:00
"""Get a page on the shared items timeline as json
maxSharesPerAccount helps to avoid one person dominating the timeline
by sharing a large number of things
"""
2020-04-05 09:17:19 +00:00
allSharesJson = {}
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
2019-11-02 14:19:51 +00:00
for handle in dirs:
if '@' in handle:
2020-04-05 09:17:19 +00:00
accountDir = baseDir + '/accounts/' + handle
sharesFilename = accountDir + '/shares.json'
2019-11-02 14:19:51 +00:00
if os.path.isfile(sharesFilename):
2020-04-05 09:17:19 +00:00
sharesJson = loadJson(sharesFilename)
2019-11-02 14:19:51 +00:00
if not sharesJson:
continue
2020-04-05 09:17:19 +00:00
nickname = handle.split('@')[0]
2019-11-03 09:36:04 +00:00
# actor who owns this share
2020-04-05 09:17:19 +00:00
owner = actor.split('/users/')[0] + '/users/' + nickname
ctr = 0
for itemID, item in sharesJson.items():
2019-11-03 09:36:04 +00:00
# assign owner to the item
2020-04-05 09:17:19 +00:00
item['actor'] = owner
allSharesJson[str(item['published'])] = item
ctr += 1
if ctr >= maxSharesPerAccount:
2019-11-02 14:19:51 +00:00
break
# sort the shared items in descending order of publication date
2020-04-05 09:17:19 +00:00
sharesJson = OrderedDict(sorted(allSharesJson.items(), reverse=True))
lastPage = False
startIndex = itemsPerPage*pageNumber
maxIndex = len(sharesJson.items())
if maxIndex < itemsPerPage:
lastPage = True
if startIndex >= maxIndex - itemsPerPage:
lastPage = True
startIndex = maxIndex - itemsPerPage
if startIndex < 0:
startIndex = 0
ctr = 0
resultJson = {}
for published, item in sharesJson.items():
if ctr >= startIndex + itemsPerPage:
2020-03-22 21:16:02 +00:00
break
2020-04-05 09:17:19 +00:00
if ctr < startIndex:
ctr += 1
2019-11-02 14:19:51 +00:00
continue
2020-04-05 09:17:19 +00:00
resultJson[published] = item
ctr += 1
return resultJson, lastPage
def htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
baseDir: str, actor: str,
nickname: str, domain: str, port: int,
maxSharesPerAccount: int, httpPrefix: str) -> str:
2019-11-02 14:19:51 +00:00
"""Show shared items timeline as html
"""
2020-04-05 09:17:19 +00:00
sharesJson, lastPage = \
sharesTimelineJson(actor, pageNumber, itemsPerPage,
baseDir, maxSharesPerAccount)
domainFull = domain
if port != 80 and port != 443:
2019-11-02 14:19:51 +00:00
if ':' not in domain:
2020-04-05 09:17:19 +00:00
domainFull = domain + ':' + str(port)
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
timelineStr = ''
if pageNumber > 1:
iconsDir = getIconsDir(baseDir)
timelineStr += \
'<center><a href="' + actor + '/tlshares?page=' + \
str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + translate['Page up'] + \
'" alt="' + translate['Page up'] + '"></a></center>'
for published, item in sharesJson.items():
showContactButton = False
if item['actor'] != actor:
showContactButton = True
showRemoveButton = False
if item['actor'] == actor:
showRemoveButton = True
timelineStr += \
htmlIndividualShare(actor, item, translate,
showContactButton, showRemoveButton)
2019-11-02 14:19:51 +00:00
if not lastPage:
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
timelineStr += \
'<center><a href="' + actor + '/tlshares?page=' + \
str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + translate['Page down'] + \
'" alt="' + translate['Page down'] + '"></a></center>'
2020-03-22 21:16:02 +00:00
2019-11-02 14:19:51 +00:00
return timelineStr
2020-04-05 09:17:19 +00:00
def htmlProfile(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, projectVersion: str,
baseDir: str, httpPrefix: str, authorized: bool,
ocapAlways: bool, profileJson: {}, selected: str,
session, wfRequest: {}, personCache: {},
extraJson=None,
pageNumber=None, maxItemsPerPage=None) -> str:
2019-07-20 21:13:36 +00:00
"""Show the profile page as html
"""
2020-04-05 09:17:19 +00:00
nickname = profileJson['preferredUsername']
2019-07-21 18:18:58 +00:00
if not nickname:
return ""
2020-04-05 09:17:19 +00:00
domain, port = getDomainFromActor(profileJson['id'])
2019-07-21 18:18:58 +00:00
if not domain:
return ""
2020-04-05 09:17:19 +00:00
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
profileJson['name'], True)
domainFull = domain
2019-07-21 18:18:58 +00:00
if port:
2020-04-05 09:17:19 +00:00
domainFull = domain + ':' + str(port)
profileDescription = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
profileJson['summary'], False)
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 = ''
logoutStr = ''
actor = profileJson['id']
usersPath = '/users/' + actor.split('/users/')[1]
donateSection = ''
donateUrl = getDonationUrl(profileJson)
PGPpubKey = getPGPpubKey(profileJson)
emailAddress = getEmailAddress(profileJson)
xmppAddress = getXmppAddress(profileJson)
matrixAddress = getMatrixAddress(profileJson)
ssbAddress = getSSBAddress(profileJson)
toxAddress = getToxAddress(profileJson)
2020-02-23 15:32:47 +00:00
if donateUrl or xmppAddress or matrixAddress or \
2020-03-22 14:42:26 +00:00
ssbAddress or toxAddress or PGPpubKey or emailAddress:
2020-04-05 09:17:19 +00:00
donateSection = '<div class="container">\n'
donateSection += ' <center>\n'
2019-12-17 14:57:16 +00:00
if donateUrl:
2020-04-05 09:17:19 +00:00
donateSection += \
' <p><a href="' + donateUrl + \
'"><button class="donateButton">' + translate['Donate'] + \
2020-02-23 15:32:47 +00:00
'</button></a></p>\n'
if emailAddress:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p>' + translate['Email'] + ': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>\n'
2019-12-17 14:57:16 +00:00
if xmppAddress:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
xmppAddress + '">'+xmppAddress + '</a></p>\n'
2019-12-17 15:25:34 +00:00
if matrixAddress:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p>' + translate['Matrix'] + ': ' + matrixAddress + '</p>\n'
2020-02-26 14:35:17 +00:00
if ssbAddress:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p>SSB: <label class="ssbaddr">' + \
ssbAddress + '</label></p>\n'
2020-03-22 14:42:26 +00:00
if toxAddress:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p>Tox: <label class="ssbaddr">' + \
toxAddress + '</label></p>\n'
if PGPpubKey:
2020-04-05 09:17:19 +00:00
donateSection += \
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
donateSection += ' </center>\n'
donateSection += '</div>\n'
2019-11-07 09:52:00 +00:00
2019-07-28 15:52:59 +00:00
if not authorized:
2020-04-05 09:17:19 +00:00
loginButton = \
'<br><a href="/login"><button class="loginButton">' + \
translate['Login'] + '</button></a>'
2019-07-29 18:48:23 +00:00
else:
2020-04-05 09:17:19 +00:00
editProfileStr = \
'<a href="' + usersPath + \
'/editprofile"><button class="button"><span>' + \
translate['Edit'] + ' </span></button></a>'
logoutStr = \
'<a href="/logout"><button class="button"><span>' + \
translate['Logout'] + ' </span></button></a>'
linkToTimelineStart = \
'<a href="/users/' + nickname + '/' + defaultTimeline + \
'"><label class="transparent">' + \
translate['Switch to timeline view'] + '</label></a>'
linkToTimelineStart += \
'<a href="/users/' + nickname + '/' + defaultTimeline + \
'" title="' + translate['Switch to timeline view'] + \
'" alt="' + translate['Switch to timeline view'] + '">'
linkToTimelineEnd = '</a>'
2019-07-29 18:48:23 +00:00
# are there any follow requests?
2020-04-05 09:17:19 +00:00
followRequestsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/followrequests.txt'
2019-07-29 18:48:23 +00:00
if os.path.isfile(followRequestsFilename):
2020-04-05 09:17:19 +00:00
with open(followRequestsFilename, 'r') as f:
2019-07-29 18:48:23 +00:00
for line in f:
2020-04-05 09:17:19 +00:00
if len(line) > 0:
followApprovals = True
followersButton = 'buttonhighlighted'
if selected == 'followers':
followersButton = 'buttonselectedhighlighted'
2019-07-29 18:48:23 +00:00
break
2020-04-05 09:17:19 +00:00
if selected == 'followers':
if followApprovals:
2020-04-05 09:17:19 +00:00
with open(followRequestsFilename, 'r') as f:
2019-07-29 18:48:23 +00:00
for followerHandle in f:
2020-04-05 09:17:19 +00:00
if len(line) > 0:
2019-07-29 18:48:23 +00:00
if '://' in followerHandle:
2020-04-05 09:17:19 +00:00
followerActor = followerHandle
2019-07-29 18:48:23 +00:00
else:
2020-04-05 09:17:19 +00:00
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">' + \
translate['Approve'] + '</button></a>'
followApprovalsSection += \
'<a href="' + basePath + \
'/followdeny=' + followerHandle + '">'
followApprovalsSection += \
'<button class="followDeny">' + \
translate['Deny'] + '</button></a>'
followApprovalsSection += '</div>'
profileDescriptionShort = profileDescription
2019-10-23 14:27:43 +00:00
if '\n' in profileDescription:
2020-04-05 09:17:19 +00:00
if len(profileDescription.split('\n')) > 2:
profileDescriptionShort = ''
2019-10-23 14:27:43 +00:00
else:
if '<br>' in profileDescription:
2020-04-05 09:17:19 +00:00
if len(profileDescription.split('<br>')) > 2:
profileDescriptionShort = ''
profileDescription = profileDescription.replace('<br>', '\n')
2019-10-23 15:09:20 +00:00
# keep the profile description short
2020-04-05 09:17:19 +00:00
if len(profileDescriptionShort) > 256:
profileDescriptionShort = ''
2019-10-23 15:09:20 +00:00
# remove formatting from profile description used on title
2020-04-05 09:17:19 +00:00
avatarDescription = ''
if profileJson.get('summary'):
2020-04-05 09:17:19 +00:00
avatarDescription = profileJson['summary'].replace('<br>', '\n')
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
profileHeaderStr = '<div class="hero-image">'
profileHeaderStr += ' <div class="hero-text">'
profileHeaderStr += \
' <img loading="lazy" src="' + profileJson['icon']['url'] + \
'" title="' + avatarDescription + '" alt="' + \
avatarDescription + '" class="title">'
profileHeaderStr += ' <h1>' + displayName + '</h1>'
profileHeaderStr += \
' <p><b>@' + nickname + '@' + domainFull + '</b></p>'
profileHeaderStr += ' <p>' + profileDescriptionShort + '</p>'
profileHeaderStr += loginButton
profileHeaderStr += ' </div>'
profileHeaderStr += '</div>'
profileStr = \
2020-02-23 15:32:47 +00:00
linkToTimelineStart + profileHeaderStr + \
linkToTimelineEnd + donateSection
2020-04-05 09:17:19 +00:00
profileStr += '<div class="container">\n'
profileStr += ' <center>'
profileStr += \
' <a href="' + usersPath + '"><button class="' + postsButton + \
'"><span>' + translate['Posts'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/following"><button class="' + \
followingButton + '"><span>' + translate['Following'] + \
2020-02-23 15:32:47 +00:00
' </span></button></a>'
2020-04-05 09:17:19 +00:00
profileStr += \
' <a href="' + usersPath + '/followers"><button class="' + \
followersButton + '"><span>' + translate['Followers'] + \
2020-02-23 15:32:47 +00:00
' </span></button></a>'
2020-04-05 09:17:19 +00:00
profileStr += \
' <a href="' + usersPath + '/roles"><button class="' + \
rolesButton + '"><span>' + translate['Roles'] + ' </span></button></a>'
profileStr += \
' <a href="' + usersPath + '/skills"><button class="' + \
skillsButton + '"><span>' + translate['Skills'] + \
2020-02-23 15:32:47 +00:00
' </span></button></a>'
2020-04-05 09:17:19 +00:00
profileStr += \
' <a href="' + usersPath + '/shares"><button class="' + \
sharesButton + '"><span>' + translate['Shares'] + \
2020-02-23 15:32:47 +00:00
' </span></button></a>'
2020-04-05 09:17:19 +00:00
profileStr += editProfileStr + logoutStr
profileStr += ' </center>'
profileStr += '</div>'
2019-07-21 18:18:58 +00:00
2020-04-05 09:17:19 +00:00
profileStr += followApprovalsSection
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = \
cssFile.read().replace('image.png',
2020-02-23 15:32:47 +00:00
profileJson['image']['url'])
2019-07-21 22:38:44 +00:00
2020-04-05 09:17:19 +00:00
licenseStr = \
'<a href="https://gitlab.com/bashrc2/epicyon">' + \
'<img loading="lazy" class="license" alt="' + \
translate['Get the source code'] + '" title="' + \
translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'
if selected == 'posts':
profileStr += \
htmlProfilePosts(recentPostsCache, maxRecentPosts,
translate,
baseDir, httpPrefix, authorized,
ocapAlways, nickname, domain, port,
session, wfRequest, personCache,
projectVersion) + licenseStr
if selected == 'following':
profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname,
domain, port, session,
wfRequest, personCache, extraJson,
projectVersion, ["unfollow"], selected,
actor, pageNumber, maxItemsPerPage)
if selected == 'followers':
profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix,
authorized, ocapAlways, nickname,
domain, port, session,
wfRequest, personCache, extraJson,
projectVersion, ["block"],
selected, actor, pageNumber,
maxItemsPerPage)
if selected == 'roles':
profileStr += \
htmlProfileRoles(translate, nickname, domainFull, extraJson)
if selected == 'skills':
profileStr += \
htmlProfileSkills(translate, nickname, domainFull, extraJson)
if selected == 'shares':
profileStr += \
htmlProfileShares(actor, translate,
nickname, domainFull,
extraJson) + licenseStr
profileStr = \
htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()
2019-07-21 18:18:58 +00:00
return profileStr
2019-07-20 21:13:36 +00:00
2020-04-05 09:17:19 +00:00
def individualFollowAsHtml(translate: {},
baseDir: str, session, wfRequest: {},
personCache: {}, domain: str,
followUrl: str,
authorized: bool,
actorNickname: str,
httpPrefix: str,
projectVersion: str,
2019-08-07 21:36:54 +00:00
buttons=[]) -> str:
2020-05-04 18:08:35 +00:00
"""An individual follow entry on the profile screen
"""
2020-04-05 09:17:19 +00:00
nickname = getNicknameFromActor(followUrl)
domain, port = getDomainFromActor(followUrl)
titleStr = '@' + nickname + '@' + domain
avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache)
2019-08-18 13:30:40 +00:00
if not avatarUrl:
2020-04-05 09:17:19 +00:00
avatarUrl = followUrl + '/avatar.png'
2019-07-22 14:09:21 +00:00
if domain not in followUrl:
2020-04-05 09:17:19 +00:00
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache, projectVersion,
httpPrefix, nickname,
domain, 'outbox')
2019-07-22 14:09:21 +00:00
if avatarUrl2:
2020-04-05 09:17:19 +00:00
avatarUrl = avatarUrl2
if displayName:
2020-04-05 09:17:19 +00:00
titleStr = displayName + ' ' + titleStr
2019-08-07 21:36:54 +00:00
2020-04-05 09:17:19 +00:00
buttonsStr = ''
2019-08-07 21:36:54 +00:00
if authorized:
for b in buttons:
2020-04-05 09:17:19 +00:00
if b == 'block':
buttonsStr += \
'<a href="/users/' + actorNickname + \
'?options=' + followUrl + \
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
translate['Block'] + '</button></a>'
if b == 'unfollow':
buttonsStr += \
'<a href="/users/' + actorNickname + \
'?options=' + followUrl + \
';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
translate['Unfollow'] + '</button></a>'
resultStr = '<div class="container">\n'
resultStr += \
'<a href="/users/' + actorNickname + '?options=' + \
followUrl + ';1;' + avatarUrl + '">'
resultStr += '<p><img loading="lazy" src="' + avatarUrl + '" alt=" ">\n'
resultStr += titleStr + '</a>' + buttonsStr + '</p>'
resultStr += '</div>\n'
return resultStr
2020-04-05 09:17:19 +00:00
2019-08-24 18:00:15 +00:00
def clickToDropDownScript() -> str:
"""Function run onclick to create a dropdown
"""
2020-04-05 09:17:19 +00:00
script = 'function dropdown() {\n'
script += ' document.getElementById("myDropdown")' + \
'.classList.toggle("show");\n'
script += '}\n'
2019-08-24 18:00:15 +00:00
return script
2020-04-05 09:17:19 +00:00
2019-09-22 09:26:52 +00:00
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
"""
2020-04-05 09:17:19 +00:00
script = 'function focusOnMessage() {\n'
script += " var replyTextArea=document.getElementById('message');\n"
script += ' val=replyTextArea.value;\n'
script += ' if ((val.length>0) && (val.charAt(val.length-1) != " ")) {\n'
script += ' val += " ";\n'
script += ' }\n'
script += ' replyTextArea.focus();\n'
script += ' replyTextArea.value="";\n'
script += ' replyTextArea.value=val;\n'
script += '}\n'
script += "var replyTextArea=document.getElementById('message')\n"
script += 'replyTextArea.onFocus=function() {\n'
script += ' focusOnMessage();'
script += '}\n'
return script
2020-04-05 09:17:19 +00:00
2019-07-31 12:44:08 +00:00
def contentWarningScript() -> str:
"""Returns a script used for content warnings
"""
2020-04-05 09:17:19 +00:00
script = 'function showContentWarning(postID) {\n'
script += ' var x=document.getElementById(postID);\n'
script += ' if (x.style.display !== "block") {\n'
script += ' x.style.display="block";\n'
script += ' } else {\n'
script += ' x.style.display="none";\n'
script += ' }\n'
script += '}\n'
2019-07-31 12:44:08 +00:00
return script
2020-04-05 09:17:19 +00:00
def contentWarningScriptOpen() -> str:
"""Returns a script used for content warnings
The warning is open by default. This is used on blog replies.
"""
2020-04-05 09:17:19 +00:00
script = 'function showContentWarning(postID) {\n'
script += ' var x=document.getElementById(postID);\n'
script += ' x.style.display="block";\n'
script += '}\n'
return script
2020-04-05 09:17:19 +00:00
def addEmbeddedAudio(translate: {}, content: str) -> str:
2019-08-30 11:32:48 +00:00
"""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
2020-04-05 09:17:19 +00:00
extension = '.mp3'
2019-08-30 11:32:48 +00:00
if '.ogg' in content:
2020-04-05 09:17:19 +00:00
extension = '.ogg'
2019-08-30 11:32:48 +00:00
2020-04-05 09:17:19 +00:00
words = content.strip('\n').split(' ')
2019-08-30 11:32:48 +00:00
for w in words:
if extension not in w:
continue
2020-04-05 09:17:19 +00:00
w = w.replace('href="', '').replace('">', '')
2019-08-30 11:32:48 +00:00
if w.endswith('.'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 16:18:34 +00:00
if w.endswith('"'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 11:32:48 +00:00
if w.endswith(';'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 11:32:48 +00:00
if w.endswith(':'):
2020-04-05 09:17:19 +00:00
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
2020-03-22 20:53:47 +00:00
2020-04-05 09:17:19 +00:00
if not (w.startswith('http') or w.startswith('dat:') or
2020-05-17 09:37:59 +00:00
w.startswith('hyper:') or w.startswith('i2p:') or
'/' in w):
2019-08-30 11:32:48 +00:00
continue
2020-04-05 09:17:19 +00:00
url = w
content += '<center><audio controls>'
content += \
'<source src="' + url + '" type="audio/' + \
extension.replace('.', '') + '">'
content += \
translate['Your browser does not support the audio element.']
content += '</audio></center>'
2019-08-30 11:32:48 +00:00
return content
2020-04-05 09:17:19 +00:00
def addEmbeddedVideo(translate: {}, content: str,
width=400, height=300) -> str:
2019-08-30 11:45:21 +00:00
"""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
2020-04-05 09:17:19 +00:00
extension = '.mp4'
2019-08-30 11:45:21 +00:00
if '.webm' in content:
2020-04-05 09:17:19 +00:00
extension = '.webm'
2019-08-30 11:45:21 +00:00
elif '.ogv' in content:
2020-04-05 09:17:19 +00:00
extension = '.ogv'
2019-08-30 11:45:21 +00:00
2020-04-05 09:17:19 +00:00
words = content.strip('\n').split(' ')
2019-08-30 11:45:21 +00:00
for w in words:
if extension not in w:
continue
2020-04-05 09:17:19 +00:00
w = w.replace('href="', '').replace('">', '')
2019-08-30 11:45:21 +00:00
if w.endswith('.'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 16:18:34 +00:00
if w.endswith('"'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 11:45:21 +00:00
if w.endswith(';'):
2020-04-05 09:17:19 +00:00
w = w[:-1]
2019-08-30 11:45:21 +00:00
if w.endswith(':'):
2020-04-05 09:17:19 +00:00
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
2020-04-05 09:17:19 +00:00
if not (w.startswith('http') or w.startswith('dat:') or
2020-05-17 09:37:59 +00:00
w.startswith('hyper:') or w.startswith('i2p:') or
'/' in w):
2019-08-30 11:45:21 +00:00
continue
2020-04-05 09:17:19 +00:00
url = w
content += \
'<center><video width="' + str(width) + '" height="' + \
str(height) + '" controls>'
content += \
'<source src="' + url + '" type="video/' + \
extension.replace('.', '') + '">'
content += \
translate['Your browser does not support the video element.']
content += '</video></center>'
2019-08-30 11:45:21 +00:00
return content
2020-04-05 09:17:19 +00:00
def addEmbeddedVideoFromSites(translate: {}, content: str,
width=400, height=300) -> str:
2019-08-20 16:40:39 +00:00
"""Adds embedded videos
"""
if '>vimeo.com/' in content:
2020-04-05 09:17:19 +00:00
url = content.split('>vimeo.com/')[1]
2019-08-20 16:40:39 +00:00
if '<' in url:
2020-04-05 09:17:19 +00:00
url = url.split('<')[0]
content = \
content + "<center><iframe loading=\"lazy\" " + \
"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
2020-04-05 09:17:19 +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:
2020-04-05 09:17:19 +00:00
url = url.split('"')[0].replace('/watch?v=', '/embed/')
2019-10-22 08:44:33 +00:00
if '&' in url:
2020-04-05 09:17:19 +00:00
url = url.split('&')[0]
content = \
content + "<center><iframe loading=\"lazy\" src=\"" + \
videoSite + url + "\" width=\"" + str(width) + \
"\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe></center>"
2019-08-21 11:34:02 +00:00
return content
2020-04-05 09:50:45 +00:00
invidiousSites = ('https://invidio.us',
'axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
'bzzsg2ii4fv2iid.onion')
2019-10-01 11:31:29 +00:00
for videoSite in invidiousSites:
2020-04-05 09:17:19 +00:00
if '"' + videoSite in content:
url = content.split('"' + videoSite)[1]
2019-10-01 11:31:29 +00:00
if '"' in url:
2020-04-05 09:17:19 +00:00
url = url.split('"')[0].replace('/watch?v=', '/embed/')
2019-10-22 08:44:33 +00:00
if '&' in url:
2020-04-05 09:17:19 +00:00
url = url.split('&')[0]
content = \
content + "<center><iframe loading=\"lazy\" src=\"" + \
videoSite + url + "\" width=\"" + \
str(width) + "\" height=\"" + str(height) + \
"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" " + \
"allowfullscreen></iframe></center>"
2019-10-01 11:31:29 +00:00
return content
2019-10-01 11:25:10 +00:00
2020-04-05 09:17:19 +00:00
videoSite = 'https://media.ccc.de'
if '"' + videoSite in content:
url = content.split('"' + videoSite)[1]
2019-08-21 11:34:02 +00:00
if '"' in url:
2020-04-05 09:17:19 +00:00
url = url.split('"')[0]
2019-08-21 11:40:43 +00:00
if not url.endswith('/oembed'):
2020-04-05 09:17:19 +00:00
url = url + '/oembed'
content = \
content + "<center><iframe loading=\"lazy\" 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
2019-10-18 16:57:33 +00:00
if '"https://' in content:
2020-04-05 09:17:19 +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',
'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')
2019-10-18 16:57:33 +00:00
for site in peerTubeSites:
2020-04-05 09:17:19 +00:00
if '"https://' + site in content:
url = content.split('"https://' + site)[1]
2019-10-18 16:57:33 +00:00
if '"' in url:
2020-04-05 09:17:19 +00:00
url = url.split('"')[0].replace('/watch/', '/embed/')
content = \
content + "<center><iframe loading=\"lazy\" " + \
"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-10-18 16:57:33 +00:00
return content
2019-08-20 16:40:39 +00:00
return content
2020-04-05 09:17:19 +00:00
def addEmbeddedElements(translate: {}, content: str) -> str:
2019-08-30 11:47:30 +00:00
"""Adds embedded elements for various media types
"""
2020-04-05 09:17:19 +00:00
content = addEmbeddedVideoFromSites(translate, content)
content = addEmbeddedAudio(translate, content)
return addEmbeddedVideo(translate, content)
2020-04-05 09:17:19 +00:00
def followerApprovalActive(baseDir: str, nickname: str, domain: str) -> bool:
"""Returns true if the given account requires follower approval
"""
2020-04-05 09:17:19 +00:00
manuallyApprovesFollowers = False
actorFilename = baseDir + '/accounts/' + nickname + '@' + domain + '.json'
if os.path.isfile(actorFilename):
2020-04-05 09:17:19 +00:00
actorJson = loadJson(actorFilename)
2019-10-22 11:55:06 +00:00
if actorJson:
if actorJson.get('manuallyApprovesFollowers'):
2020-04-05 09:17:19 +00:00
manuallyApprovesFollowers = \
actorJson['manuallyApprovesFollowers']
return manuallyApprovesFollowers
2020-04-05 09:17:19 +00:00
def insertQuestion(baseDir: str, translate: {},
nickname: str, domain: str, port: int,
content: str,
postJsonObject: {}, pageNumber: int) -> str:
2019-09-06 15:08:32 +00:00
""" Inserts question selection into a post
"""
if not isQuestion(postJsonObject):
return content
2020-04-05 09:17:19 +00:00
if len(postJsonObject['object']['oneOf']) == 0:
2019-09-06 15:08:32 +00:00
return content
2020-04-05 09:17:19 +00:00
messageId = postJsonObject['id'].replace('/activity', '')
2019-11-25 12:33:05 +00:00
if '#' in messageId:
2020-04-05 09:17:19 +00:00
messageId = messageId.split('#', 1)[0]
pageNumberStr = ''
2019-09-06 16:37:33 +00:00
if pageNumber:
2020-04-05 09:17:19 +00:00
pageNumberStr = '?page=' + str(pageNumber)
2019-11-25 13:34:44 +00:00
2020-04-05 09:17:19 +00:00
votesFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + '/questions.txt'
2019-11-26 20:22:52 +00:00
2020-04-05 09:17:19 +00:00
showQuestionResults = False
2019-11-26 20:22:52 +00:00
if os.path.isfile(votesFilename):
2019-12-10 19:31:45 +00:00
if messageId in open(votesFilename).read():
2020-04-05 09:17:19 +00:00
showQuestionResults = True
2019-11-26 20:22:52 +00:00
2019-12-10 19:31:45 +00:00
if not showQuestionResults:
2019-11-25 13:34:44 +00:00
# show the question options
2020-04-05 09:17:19 +00:00
content += '<div class="question">'
content += \
'<form method="POST" action="/users/' + \
nickname + '/question' + pageNumberStr + '">'
content += \
'<input type="hidden" name="messageId" value="' + \
messageId + '"><br>'
2019-11-25 13:34:44 +00:00
for choice in postJsonObject['object']['oneOf']:
if not choice.get('type'):
continue
if not choice.get('name'):
continue
2020-04-05 09:17:19 +00:00
content += \
'<input type="radio" name="answer" value="' + \
choice['name'] + '"> ' + choice['name'] + '<br><br>'
content += \
'<input type="submit" value="' + \
translate['Vote'] + '" class="vote"><br><br>'
content += '</form></div>'
2019-11-25 13:34:44 +00:00
else:
# show the responses to a question
2020-04-05 09:17:19 +00:00
content += '<div class="questionresult">'
2019-11-25 13:34:44 +00:00
# get the maximum number of votes
2020-04-05 09:17:19 +00:00
maxVotes = 1
2019-11-25 13:34:44 +00:00
for questionOption in postJsonObject['object']['oneOf']:
if not questionOption.get('name'):
continue
if not questionOption.get('replies'):
continue
2020-04-05 09:17:19 +00:00
votes = 0
2019-12-10 18:05:38 +00:00
try:
2020-04-05 09:17:19 +00:00
votes = int(questionOption['replies']['totalItems'])
except BaseException:
2019-12-10 18:05:38 +00:00
pass
2020-04-05 09:17:19 +00:00
if votes > maxVotes:
maxVotes = int(votes+1)
2019-11-25 13:34:44 +00:00
# show the votes as sliders
2020-04-05 09:17:19 +00:00
questionCtr = 1
2019-11-25 13:34:44 +00:00
for questionOption in postJsonObject['object']['oneOf']:
if not questionOption.get('name'):
continue
if not questionOption.get('replies'):
continue
2020-04-05 09:17:19 +00:00
votes = 0
2019-12-10 18:05:38 +00:00
try:
2020-04-05 09:17:19 +00:00
votes = int(questionOption['replies']['totalItems'])
except BaseException:
2019-12-10 18:05:38 +00:00
pass
2020-04-05 09:17:19 +00:00
votesPercent = str(int(votes * 100 / maxVotes))
content += \
'<p><input type="text" title="' + str(votes) + \
'" name="skillName' + str(questionCtr) + \
'" value="' + questionOption['name'] + \
' (' + str(votes) + ')" style="width:40%">'
content += \
'<input type="range" min="1" max="100" ' + \
'class="slider" title="' + \
str(votes) + '" name="skillValue' + str(questionCtr) + \
'" value="' + votesPercent + '"></p>'
questionCtr += 1
content += '</div>'
2019-09-06 15:08:32 +00:00
return content
2020-04-05 09:17:19 +00:00
def addEmojiToDisplayName(baseDir: str, httpPrefix: str,
nickname: str, domain: str,
displayName: str, inProfileName: bool) -> str:
2019-09-23 11:44:43 +00:00
"""Adds emoji icons to display names on individual posts
"""
2019-10-18 12:46:35 +00:00
if ':' not in displayName:
return displayName
2020-04-05 09:17:19 +00:00
displayName = displayName.replace('<p>', '').replace('</p>', '')
emojiTags = {}
print('TAG: displayName before tags: ' + displayName)
displayName = \
addHtmlTags(baseDir, httpPrefix,
nickname, domain, displayName, [], emojiTags)
displayName = displayName.replace('<p>', '').replace('</p>', '')
print('TAG: displayName after tags: ' + displayName)
2019-10-18 12:46:35 +00:00
# convert the emoji dictionary to a list
2020-04-05 09:17:19 +00:00
emojiTagsList = []
for tagName, tag in emojiTags.items():
2019-10-18 12:46:35 +00:00
emojiTagsList.append(tag)
2020-04-05 09:17:19 +00:00
print('TAG: emoji tags list: ' + str(emojiTagsList))
2019-10-18 12:46:35 +00:00
if not inProfileName:
2020-04-05 09:17:19 +00:00
displayName = \
replaceEmojiFromTags(displayName, emojiTagsList, 'post header')
2019-10-18 12:46:35 +00:00
else:
2020-04-05 09:17:19 +00:00
displayName = \
replaceEmojiFromTags(displayName, emojiTagsList, 'profile')
print('TAG: displayName after tags 2: ' + displayName)
2019-10-18 12:46:35 +00:00
# remove any stray emoji
while ':' in displayName:
if '://' in displayName:
break
2020-04-05 09:17:19 +00:00
emojiStr = displayName.split(':')[1]
prevDisplayName = displayName
displayName = displayName.replace(':' + emojiStr + ':', '').strip()
if prevDisplayName == displayName:
2019-10-18 12:46:35 +00:00
break
2020-04-05 09:17:19 +00:00
print('TAG: displayName after tags 3: ' + displayName)
print('TAG: displayName after tag replacements: ' + displayName)
2019-10-09 17:03:07 +00:00
2019-09-23 11:44:43 +00:00
return displayName
2020-04-05 09:17:19 +00:00
2019-10-18 12:00:14 +00:00
def postContainsPublic(postJsonObject: {}) -> bool:
"""Does the given post contain #Public
"""
2020-04-05 09:17:19 +00:00
containsPublic = False
2019-10-18 12:00:14 +00:00
if not postJsonObject['object'].get('to'):
return containsPublic
2020-03-22 21:16:02 +00:00
2019-10-18 12:00:14 +00:00
for toAddress in postJsonObject['object']['to']:
if toAddress.endswith('#Public'):
2020-04-05 09:17:19 +00:00
containsPublic = True
2019-10-18 12:00:14 +00:00
break
if not containsPublic:
if postJsonObject['object'].get('cc'):
for toAddress in postJsonObject['object']['cc']:
if toAddress.endswith('#Public'):
2020-04-05 09:17:19 +00:00
containsPublic = True
2019-10-18 12:00:14 +00:00
break
return containsPublic
2020-04-05 09:17:19 +00:00
def loadIndividualPostAsHtmlFromCache(baseDir: str,
nickname: str, domain: str,
2019-10-19 11:53:57 +00:00
postJsonObject: {}) -> str:
"""If a cached html version of the given post exists then load it and
return the html text
2019-10-19 12:08:18 +00:00
This is much quicker than generating the html from the json object
2019-10-19 11:53:57 +00:00
"""
2020-04-05 09:17:19 +00:00
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
2019-10-19 12:07:12 +00:00
2020-04-05 09:17:19 +00:00
postHtml = ''
2019-11-29 23:04:37 +00:00
if not cachedPostFilename:
return postHtml
2020-03-22 21:16:02 +00:00
2019-10-19 11:53:57 +00:00
if not os.path.isfile(cachedPostFilename):
return postHtml
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
tries = 0
while tries < 3:
2019-10-19 11:53:57 +00:00
try:
with open(cachedPostFilename, 'r') as file:
2020-04-05 09:17:19 +00:00
postHtml = file.read()
2019-10-19 11:53:57 +00:00
break
except Exception as e:
print(e)
# no sleep
2020-04-05 09:17:19 +00:00
tries += 1
2019-10-19 11:53:57 +00:00
if postHtml:
return postHtml
2019-10-18 12:00:14 +00:00
2020-04-05 09:17:19 +00:00
def saveIndividualPostAsHtmlToCache(baseDir: str,
nickname: str, domain: str,
postJsonObject: {},
postHtml: str) -> bool:
2019-10-19 11:53:57 +00:00
"""Saves the given html for a post to a cache file
2020-04-05 09:17:19 +00:00
This is so that it can be quickly reloaded on subsequent
refresh of the timeline
2019-10-19 11:53:57 +00:00
"""
2020-04-05 09:17:19 +00:00
htmlPostCacheDir = \
getCachedPostDirectory(baseDir, nickname, domain)
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
2019-10-19 11:53:57 +00:00
# create the cache directory if needed
if not os.path.isdir(htmlPostCacheDir):
os.mkdir(htmlPostCacheDir)
try:
with open(cachedPostFilename, 'w') as fp:
fp.write(postHtml)
return True
except Exception as e:
2020-04-05 09:17:19 +00:00
print('ERROR: saving post to cache ' + str(e))
2019-10-19 11:53:57 +00:00
return False
2020-04-05 09:17:19 +00:00
def preparePostFromHtmlCache(postHtml: str, boxName: str,
pageNumber: int) -> str:
2019-11-25 10:29:09 +00:00
"""Sets the page number on a cached html post
"""
# if on the bookmarks timeline then remain there
2020-05-21 20:15:24 +00:00
if boxName == 'tlbookmarks' or boxName == 'bookmarks':
2020-04-05 09:17:19 +00:00
postHtml = postHtml.replace('?tl=inbox', '?tl=tlbookmarks')
2020-05-22 12:48:58 +00:00
if '?page=' in postHtml:
pageNumberStr = postHtml.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr = pageNumberStr.split('?')[0]
postHtml = postHtml.replace('?page=' + pageNumberStr, '?page=-999')
2020-04-05 09:17:19 +00:00
withPageNumber = postHtml.replace(';-999;', ';' + str(pageNumber) + ';')
withPageNumber = withPageNumber.replace('?page=-999',
'?page=' + str(pageNumber))
return withPageNumber
2020-04-05 09:17:19 +00:00
def postIsMuted(baseDir: str, nickname: str, domain: str,
postJsonObject: {}, messageId: str) -> bool:
2019-12-01 13:45:30 +00:00
""" Returns true if the given post is muted
"""
2020-04-05 09:17:19 +00:00
isMuted = postJsonObject.get('muted')
if isMuted is True or isMuted is False:
2019-12-01 16:15:41 +00:00
return isMuted
2020-04-05 09:17:19 +00:00
postDir = baseDir + '/accounts/' + nickname + '@' + domain
muteFilename = \
postDir + '/inbox/' + messageId.replace('/', '#') + '.json.muted'
2019-12-01 13:45:30 +00:00
if os.path.isfile(muteFilename):
return True
2020-04-05 09:17:19 +00:00
muteFilename = \
postDir + '/outbox/' + messageId.replace('/', '#') + '.json.muted'
2019-12-01 14:09:45 +00:00
if os.path.isfile(muteFilename):
return True
2020-04-05 09:17:19 +00:00
muteFilename = \
baseDir + '/accounts/cache/announce/' + nickname + \
'/' + messageId.replace('/', '#') + '.json.muted'
2019-12-01 13:45:30 +00:00
if os.path.isfile(muteFilename):
return True
return False
2020-04-05 09:17:19 +00:00
def getPostAttachmentsAsHtml(postJsonObject: {}, boxName: str, translate: {},
isMuted: bool, avatarLink: str,
replyStr: str, announceStr: str, likeStr: str,
bookmarkStr: str, deleteStr: str,
muteStr: str) -> (str, str):
"""Returns a string representing any attachments
"""
2020-04-05 09:17:19 +00:00
attachmentStr = ''
galleryStr = ''
if not postJsonObject['object'].get('attachment'):
2020-04-05 09:17:19 +00:00
return attachmentStr, galleryStr
if not isinstance(postJsonObject['object']['attachment'], list):
2020-04-05 09:17:19 +00:00
return attachmentStr, galleryStr
2020-04-05 09:17:19 +00:00
attachmentCtr = 0
attachmentStr += '<div class="media">'
for attach in postJsonObject['object']['attachment']:
if not (attach.get('mediaType') and attach.get('url')):
continue
2020-04-05 09:17:19 +00:00
mediaType = attach['mediaType']
imageDescription = ''
if attach.get('name'):
2020-04-05 09:17:19 +00:00
imageDescription = attach['name'].replace('"', "'")
if mediaType == 'image/png' or \
mediaType == 'image/jpeg' or \
mediaType == 'image/webp' or \
mediaType == 'image/gif':
if attach['url'].endswith('.png') or \
attach['url'].endswith('.jpg') or \
attach['url'].endswith('.jpeg') or \
attach['url'].endswith('.webp') or \
attach['url'].endswith('.gif'):
2020-04-05 09:17:19 +00:00
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += \
' <img loading="lazy" src="' + \
attach['url'] + '" alt="" title="">\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
2020-04-05 09:17:19 +00:00
imagePostUrl = postJsonObject['object']['url']
else:
2020-04-05 09:17:19 +00:00
imagePostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += \
' <a href="' + imagePostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
2020-04-05 09:17:19 +00:00
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr+announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<a href="' + attach['url'] + '">'
attachmentStr += \
'<img loading="lazy" src="' + attach['url'] + \
'" alt="' + imageDescription + '" title="' + \
imageDescription + '" class="attachment"></a>\n'
attachmentCtr += 1
elif (mediaType == 'video/mp4' or
mediaType == 'video/webm' or
mediaType == 'video/ogv'):
extension = '.mp4'
if attach['url'].endswith('.webm'):
2020-04-05 09:17:19 +00:00
extension = '.webm'
elif attach['url'].endswith('.ogv'):
2020-04-05 09:17:19 +00:00
extension = '.ogv'
if attach['url'].endswith(extension):
2020-04-05 09:17:19 +00:00
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += \
' <video width="600" height="400" controls>\n'
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="video/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the video tag.'
galleryStr += translate[idx]
galleryStr += ' </video>\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
2020-04-05 09:17:19 +00:00
videoPostUrl = postJsonObject['object']['url']
else:
2020-04-05 09:17:19 +00:00
videoPostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += \
' <a href="' + videoPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
2020-04-05 09:17:19 +00:00
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + likeStr + \
bookmarkStr + deleteStr + muteStr + '\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += \
'<center><video width="400" height="300" controls>'
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="video/' + \
extension.replace('.', '') + '">'
attachmentStr += \
translate['Your browser does not support the video tag.']
attachmentStr += '</video></center>'
attachmentCtr += 1
elif (mediaType == 'audio/mpeg' or
mediaType == 'audio/ogg'):
extension = '.mp3'
if attach['url'].endswith('.ogg'):
2020-04-05 09:17:19 +00:00
extension = '.ogg'
if attach['url'].endswith(extension):
2020-04-05 09:17:19 +00:00
if attachmentCtr > 0:
attachmentStr += '<br>'
if boxName == 'tlmedia':
galleryStr += '<div class="gallery">\n'
if not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += ' <a href="' + attach['url'] + '">\n'
galleryStr += ' <audio controls>\n'
galleryStr += \
' <source src="' + attach['url'] + \
'" alt="' + imageDescription + \
'" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
idx = 'Your browser does not support the audio tag.'
galleryStr += translate[idx]
galleryStr += ' </audio>\n'
galleryStr += ' </a>\n'
if postJsonObject['object'].get('url'):
2020-04-05 09:17:19 +00:00
audioPostUrl = postJsonObject['object']['url']
else:
2020-04-05 09:17:19 +00:00
audioPostUrl = postJsonObject['object']['id']
if imageDescription and not isMuted:
2020-04-05 09:17:19 +00:00
galleryStr += \
' <a href="' + audioPostUrl + \
'" class="gallerytext"><div ' + \
'class="gallerytext">' + \
imageDescription + '</div></a>\n'
else:
2020-04-05 09:17:19 +00:00
galleryStr += \
'<label class="transparent">---</label><br>'
galleryStr += ' <div class="mediaicons">\n'
galleryStr += \
' ' + replyStr + announceStr + \
likeStr + bookmarkStr + \
deleteStr + muteStr+'\n'
galleryStr += ' </div>\n'
galleryStr += ' <div class="mediaavatar">\n'
galleryStr += ' ' + avatarLink + '\n'
galleryStr += ' </div>\n'
galleryStr += '</div>\n'
attachmentStr += '<center><audio controls>'
attachmentStr += \
'<source src="' + attach['url'] + '" alt="' + \
imageDescription + '" title="' + imageDescription + \
'" class="attachment" type="audio/' + \
extension.replace('.', '') + '">'
attachmentStr += \
translate['Your browser does not support the audio tag.']
attachmentStr += '</audio></center>'
attachmentCtr += 1
attachmentStr += '</div>'
return attachmentStr, galleryStr
def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
iconsDir: str, translate: {},
pageNumber: int, baseDir: str,
session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int,
postJsonObject: {},
avatarUrl: str, showAvatarOptions: bool,
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
boxName: str, showRepeats=True,
showIcons=False,
manuallyApprovesFollowers=False,
showPublicOnly=False,
2019-10-19 11:42:41 +00:00
storeToCache=True) -> str:
2019-07-31 10:09:02 +00:00
""" Shows a single post as html
"""
2020-04-05 09:17:19 +00:00
postActor = postJsonObject['actor']
2019-11-03 16:11:52 +00:00
2019-11-06 11:39:41 +00:00
# ZZZzzz
2020-04-05 09:17:19 +00:00
if isPersonSnoozed(baseDir, nickname, domain, postActor):
2019-11-06 11:39:41 +00:00
return ''
2020-04-05 09:17:19 +00:00
avatarPosition = ''
messageId = ''
2019-11-28 13:41:11 +00:00
if postJsonObject.get('id'):
2020-04-05 09:17:19 +00:00
messageId = postJsonObject['id'].replace('/activity', '')
2019-11-28 13:41:11 +00:00
2020-04-05 09:17:19 +00:00
messageIdStr = ''
2019-11-28 13:39:03 +00:00
if messageId:
2020-04-05 09:17:19 +00:00
messageIdStr = ';' + messageId
2019-11-28 13:39:03 +00:00
2020-04-05 09:17:19 +00:00
fullDomain = domain
2019-11-28 13:39:03 +00:00
if port:
2020-04-05 09:17:19 +00:00
if port != 80 and port != 443:
2019-11-28 13:39:03 +00:00
if ':' not in domain:
2020-04-05 09:17:19 +00:00
fullDomain = domain + ':' + str(port)
2019-11-28 13:39:03 +00:00
2020-04-05 09:17:19 +00:00
pageNumberParam = ''
2019-11-28 13:39:03 +00:00
if pageNumber:
2020-04-05 09:17:19 +00:00
pageNumberParam = '?page=' + str(pageNumber)
2019-11-28 13:39:03 +00:00
if (not showPublicOnly and
(storeToCache or boxName == 'bookmarks' or
boxName == 'tlbookmarks') and
boxName != 'tlmedia'):
2019-11-03 16:11:52 +00:00
# update avatar if needed
if not avatarUrl:
2020-04-05 09:17:19 +00:00
avatarUrl = \
getPersonAvatarUrl(baseDir, postActor, personCache)
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache)
2019-11-03 16:11:52 +00:00
2020-04-05 09:17:19 +00:00
postHtml = \
loadIndividualPostAsHtmlFromCache(baseDir, nickname, domain,
2019-10-19 11:53:57 +00:00
postJsonObject)
if postHtml:
2020-04-05 09:17:19 +00:00
postHtml = preparePostFromHtmlCache(postHtml, boxName, pageNumber)
updateRecentPostsCache(recentPostsCache, maxRecentPosts,
postJsonObject, postHtml)
return postHtml
2019-10-19 10:34:06 +00:00
2019-11-28 13:39:03 +00:00
if not avatarUrl:
2020-04-05 09:17:19 +00:00
avatarUrl = \
getPersonAvatarUrl(baseDir, postActor, personCache)
avatarUrl = \
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache)
2019-11-28 13:39:03 +00:00
else:
2020-04-05 09:17:19 +00:00
updateAvatarImageCache(session, baseDir, httpPrefix,
postActor, avatarUrl, personCache)
2019-11-28 13:39:03 +00:00
if not avatarUrl:
2020-04-05 09:17:19 +00:00
avatarUrl = postActor + '/avatar.png'
2019-11-28 13:39:03 +00:00
if fullDomain not in postActor:
2020-04-05 09:17:19 +00:00
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl2, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, 'outbox')
2019-11-28 13:39:03 +00:00
if avatarUrl2:
2020-04-05 09:17:19 +00:00
avatarUrl = avatarUrl2
2019-11-28 13:39:03 +00:00
if displayName:
if ':' in displayName:
2020-04-05 09:17:19 +00:00
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
displayName, False)
avatarLink = ' <a href="' + postActor + '">'
avatarLink += \
' <img loading="lazy" src="' + avatarUrl + '" title="' + \
translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>'
if showAvatarOptions and \
fullDomain + '/users/' + nickname not in postActor:
avatarLink = \
' <a href="/users/' + nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">'
avatarLink += \
' <img loading="lazy" title="' + \
translate['Show options for this person'] + \
'" src="' + avatarUrl + '" ' + avatarPosition + '/></a>'
avatarImageInPost = \
' <div class="timeline-avatar">' + avatarLink + '</div>'
2019-11-28 13:39:03 +00:00
# don't create new html within the bookmarks timeline
# it should already have been created for the inbox
2020-05-21 20:15:24 +00:00
if boxName == 'tlbookmarks' or boxName == 'bookmarks':
return ''
2020-04-05 09:17:19 +00:00
timelinePostBookmark = postJsonObject['id'].replace('/activity', '')
timelinePostBookmark = timelinePostBookmark.replace('://', '-')
timelinePostBookmark = timelinePostBookmark.replace('/', '-')
2019-11-19 15:27:43 +00:00
# If this is the inbox timeline then don't show the repeat icon on any DMs
2020-04-05 09:17:19 +00:00
showRepeatIcon = showRepeats
isPublicRepeat = False
showDMicon = False
if showRepeats:
if isDM(postJsonObject):
2020-04-05 09:17:19 +00:00
showDMicon = True
showRepeatIcon = False
2020-02-14 17:30:09 +00:00
else:
if not isPublicPost(postJsonObject):
2020-04-05 09:17:19 +00:00
isPublicRepeat = True
titleStr = ''
galleryStr = ''
isAnnounced = False
if postJsonObject['type'] == 'Announce':
postJsonAnnounce = \
downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, postJsonObject,
projectVersion)
2019-09-28 16:05:01 +00:00
if not postJsonAnnounce:
return ''
2020-04-05 09:17:19 +00:00
postJsonObject = postJsonAnnounce
isAnnounced = True
2019-09-28 16:05:01 +00:00
2019-07-31 10:09:02 +00:00
if not isinstance(postJsonObject['object'], dict):
return ''
2019-09-02 11:59:15 +00:00
# if this post should be public then check its recipients
if showPublicOnly:
2019-10-18 12:00:14 +00:00
if not postContainsPublic(postJsonObject):
return ''
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
isModerationPost = False
if postJsonObject['object'].get('moderationStatus'):
2020-04-05 09:17:19 +00:00
isModerationPost = True
containerClass = 'container'
containerClassIcons = 'containericons'
timeClass = 'time-right'
actorNickname = getNicknameFromActor(postActor)
if not actorNickname:
# single user instance
2020-04-05 09:17:19 +00:00
actorNickname = 'dev'
actorDomain, actorPort = getDomainFromActor(postActor)
2019-08-20 13:13:44 +00:00
2020-04-05 09:17:19 +00:00
displayName = getDisplayName(baseDir, postActor, personCache)
if displayName:
2019-10-18 17:07:45 +00:00
if ':' in displayName:
2020-04-05 09:17:19 +00:00
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
displayName, False)
titleStr += \
'<a href="/users/' + nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
'">' + displayName + '</a>'
2019-08-22 12:41:16 +00:00
else:
2019-10-21 10:04:05 +00:00
if not messageId:
2020-04-05 09:17:19 +00:00
# pprint(postJsonObject)
2019-10-21 10:04:05 +00:00
print('ERROR: no messageId')
if not actorNickname:
2020-04-05 09:17:19 +00:00
# pprint(postJsonObject)
2019-10-21 10:04:05 +00:00
print('ERROR: no actorNickname')
if not actorDomain:
2020-04-05 09:17:19 +00:00
# pprint(postJsonObject)
2019-10-21 10:04:05 +00:00
print('ERROR: no actorDomain')
2020-04-05 09:17:19 +00:00
titleStr += \
'<a href="/users/' + nickname + '?options=' + postActor + \
';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + \
'">@' + 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:
2020-04-05 09:17:19 +00:00
titleStr = \
titleStr + ' <img loading="lazy" src="/' + \
iconsDir + '/dm.png" class="DMicon"/>'
2019-08-25 17:53:20 +00:00
2020-04-05 09:17:19 +00:00
replyStr = ''
2019-11-28 12:34:02 +00:00
if showIcons:
2020-04-05 09:17:19 +00:00
replyToLink = postJsonObject['object']['id']
2019-11-28 12:34:02 +00:00
if postJsonObject['object'].get('attributedTo'):
2020-04-05 09:17:19 +00:00
replyToLink += \
'?mention=' + postJsonObject['object']['attributedTo']
2019-11-28 12:34:02 +00:00
if postJsonObject['object'].get('content'):
2020-04-05 09:17:19 +00:00
mentionedActors = \
2020-02-23 15:32:47 +00:00
getMentionsFromHtml(postJsonObject['object']['content'])
2019-11-28 12:34:02 +00:00
if mentionedActors:
for actorUrl in mentionedActors:
2020-04-05 09:17:19 +00:00
if '?mention=' + actorUrl not in replyToLink:
replyToLink += '?mention=' + actorUrl
if len(replyToLink) > 500:
2019-11-28 12:34:02 +00:00
break
2020-04-05 09:17:19 +00:00
replyToLink += pageNumberParam
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
replyStr = ''
2020-02-14 17:16:01 +00:00
if isPublicRepeat:
2020-04-05 09:17:19 +00:00
replyStr += \
'<a href="/users/' + nickname + '?replyto=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">'
2020-02-11 10:24:22 +00:00
else:
if isDM(postJsonObject):
2020-04-05 09:17:19 +00:00
replyStr += \
'<a href="/users/' + nickname + \
'?replydm=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">'
2019-11-28 12:34:02 +00:00
else:
2020-04-05 09:17:19 +00:00
replyStr += \
'<a href="/users/' + nickname + \
'?replyfollowers=' + replyToLink + \
'?actor=' + postJsonObject['actor'] + \
'" title="' + translate['Reply to this post'] + '">'
replyStr += \
'<img loading="lazy" title="' + \
translate['Reply to this post'] + ' |" alt="' + \
translate['Reply to this post'] + \
' |" src="/' + iconsDir + '/reply.png"/></a>'
editStr = ''
if fullDomain + '/users/' + nickname in postJsonObject['actor']:
2020-02-29 23:59:48 +00:00
if isBlogPost(postJsonObject):
2020-03-01 12:33:20 +00:00
if '/statuses/' in postJsonObject['object']['id']:
2020-04-05 09:17:19 +00:00
editStr += \
'<a href="/users/' + nickname + \
'/tlblogs?editblogpost=' + \
postJsonObject['object']['id'].split('/statuses/')[1] + \
'?actor=' + actorNickname + \
'" title="' + translate['Edit blog post'] + '">' + \
'<img loading="lazy" title="' + \
translate['Edit blog post'] + ' |" alt="' + \
translate['Edit blog post'] + \
' |" src="/' + iconsDir + '/edit.png"/></a>'
announceStr = ''
2019-11-28 12:00:01 +00:00
if not isModerationPost and showRepeatIcon:
# don't allow announce/repeat of your own posts
2020-04-05 09:17:19 +00:00
announceIcon = 'repeat_inactive.png'
announceLink = 'repeat'
2020-02-14 17:16:01 +00:00
if not isPublicRepeat:
2020-04-05 09:17:19 +00:00
announceLink = 'repeatprivate'
announceTitle = translate['Repeat this post']
if announcedByPerson(postJsonObject, nickname, fullDomain):
announceIcon = 'repeat.png'
2020-02-14 17:16:01 +00:00
if not isPublicRepeat:
2020-04-05 09:17:19 +00:00
announceLink = 'unrepeatprivate'
announceTitle = translate['Undo the repeat']
announceStr = \
'<a href="/users/' + nickname + '?' + announceLink + \
'=' + postJsonObject['object']['id'] + pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
'?bm=' + timelinePostBookmark + \
'?tl=' + boxName + '" title="' + announceTitle + '">'
announceStr += \
'<img loading="lazy" title="' + translate['Repeat this post'] + \
' |" alt="' + translate['Repeat this post'] + \
' |" src="/' + iconsDir + '/' + announceIcon + '"/></a>'
likeStr = ''
2019-11-28 12:00:01 +00:00
if not isModerationPost:
2020-04-05 09:17:19 +00:00
likeIcon = 'like_inactive.png'
likeLink = 'like'
likeTitle = translate['Like this post']
if noOfLikes(postJsonObject) > 0:
likeIcon = 'like.png'
if likedByPerson(postJsonObject, nickname, fullDomain):
likeLink = 'unlike'
likeTitle = translate['Undo the like']
likeStr = \
2019-11-28 12:00:01 +00:00
'<a href="/users/' + nickname + '?' + \
2020-04-05 09:17:19 +00:00
likeLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
'?bm=' + timelinePostBookmark + \
'?tl=' + boxName + '" title="' + likeTitle + '">'
likeStr += \
'<img loading="lazy" title="' + likeTitle + \
' |" alt="' + likeTitle + \
' |" src="/' + iconsDir + '/' + likeIcon + '"/></a>'
bookmarkStr = ''
2019-11-28 12:00:01 +00:00
if not isModerationPost:
2020-04-05 09:17:19 +00:00
bookmarkIcon = 'bookmark_inactive.png'
bookmarkLink = 'bookmark'
bookmarkTitle = translate['Bookmark this post']
if bookmarkedByPerson(postJsonObject, nickname, fullDomain):
bookmarkIcon = 'bookmark.png'
bookmarkLink = 'unbookmark'
bookmarkTitle = translate['Undo the bookmark']
bookmarkStr = \
2019-11-28 12:00:01 +00:00
'<a href="/users/' + nickname + '?' + \
2020-04-05 09:17:19 +00:00
bookmarkLink + '=' + postJsonObject['object']['id'] + \
pageNumberParam + \
'?actor=' + postJsonObject['actor'] + \
'?bm=' + timelinePostBookmark + \
'?tl=' + boxName + '" title="' + bookmarkTitle + '">'
bookmarkStr += \
'<img loading="lazy" title="' + bookmarkTitle + ' |" alt="' + \
bookmarkTitle + ' |" src="/' + iconsDir + \
'/' + bookmarkIcon + '"/></a>'
isMuted = postIsMuted(baseDir, nickname, domain, postJsonObject, messageId)
deleteStr = ''
muteStr = ''
if (allowDeletion or
('/' + fullDomain + '/' in postActor and
messageId.startswith(postActor))):
if '/users/' + nickname + '/' in messageId:
deleteStr = \
'<a href="/users/' + nickname + \
'?delete=' + messageId + pageNumberParam + \
'" title="' + translate['Delete this post'] + '">'
deleteStr += \
'<img loading="lazy" alt="' + translate['Delete this post'] + \
' |" title="' + translate['Delete this post'] + \
' |" src="/' + iconsDir + '/delete.png"/></a>'
2019-12-01 13:45:30 +00:00
else:
if not isMuted:
2020-04-05 09:17:19 +00:00
muteStr = \
'<a href="/users/' + nickname + \
'?mute=' + messageId + pageNumberParam + '?tl=' + boxName + \
'?bm=' + timelinePostBookmark + \
'" title="' + translate['Mute this post'] + '">'
muteStr += \
'<img loading="lazy" alt="' + \
translate['Mute this post'] + \
' |" title="' + translate['Mute this post'] + \
' |" src="/' + iconsDir + '/mute.png"/></a>'
2019-12-01 13:45:30 +00:00
else:
2020-04-05 09:17:19 +00:00
muteStr = \
'<a href="/users/' + nickname + '?unmute=' + messageId + \
pageNumberParam + '?tl=' + boxName + '?bm=' + \
timelinePostBookmark + '" title="' + \
translate['Undo mute'] + '">'
muteStr += \
'<img loading="lazy" alt="' + translate['Undo mute'] + \
' |" title="' + translate['Undo mute'] + \
' |" src="/' + iconsDir+'/unmute.png"/></a>'
replyAvatarImageInPost = ''
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'):
2020-04-05 09:17:19 +00:00
attributedTo = postJsonObject['object']['attributedTo']
if attributedTo.startswith(postActor):
titleStr += \
' <img loading="lazy" title="' + \
translate['announces'] + \
'" alt="' + translate['announces'] + \
'" src="/' + iconsDir + \
2020-02-23 15:32:47 +00:00
'/repeat_inactive.png" class="announceOrReply"/>'
2019-08-25 17:22:24 +00:00
else:
2020-04-05 09:17:19 +00:00
announceNickname = \
getNicknameFromActor(attributedTo)
2019-09-25 12:48:12 +00:00
if announceNickname:
2020-04-05 09:17:19 +00:00
announceDomain, announcePort = \
getDomainFromActor(attributedTo)
getPersonFromCache(baseDir, attributedTo, personCache)
announceDisplayName = \
getDisplayName(baseDir, attributedTo, personCache)
2019-09-25 12:48:12 +00:00
if announceDisplayName:
2019-10-18 12:55:53 +00:00
if ':' in announceDisplayName:
2020-04-05 09:17:19 +00:00
announceDisplayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
nickname, domain,
announceDisplayName,
False)
titleStr += \
' <img loading="lazy" title="' + \
translate['announces'] + '" alt="' + \
translate['announces'] + '" src="/' + \
iconsDir + '/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
postJsonObject['object']['id'] + '">' + \
announceDisplayName + '</a>'
2019-09-25 12:48:12 +00:00
# show avatar of person replied to
2020-04-05 09:17:19 +00:00
announceActor = \
postJsonObject['object']['attributedTo']
announceAvatarUrl = \
getPersonAvatarUrl(baseDir, announceActor,
personCache)
2019-09-25 12:48:12 +00:00
if announceAvatarUrl:
2020-04-05 09:17:19 +00:00
idx = 'Show options for this person'
replyAvatarImageInPost = \
2019-09-25 12:48:12 +00:00
'<div class="timeline-avatar-reply">' \
2020-04-05 09:17:19 +00:00
'<a href="/users/' + nickname + \
'?options=' + \
announceActor + ';' + str(pageNumber) + \
';' + announceAvatarUrl + \
messageIdStr + '">' \
'<img loading="lazy" src="' + \
announceAvatarUrl + '" ' \
'title="' + translate[idx] + \
'" alt=" "' + avatarPosition + \
'/></a></div>'
2019-09-25 12:48:12 +00:00
else:
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + \
translate['announces'] + \
'" alt="' + translate['announces'] + \
'" src="/' + iconsDir + \
'/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
postJsonObject['object']['id'] + '">@' + \
announceNickname + '@' + \
announceDomain + '</a>'
2019-09-25 12:48:12 +00:00
else:
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + \
translate['announces'] + '" alt="' + \
translate['announces'] + '" src="/' + iconsDir + \
'/repeat_inactive.png" ' + \
'class="announceOrReply"/> <a href="' + \
postJsonObject['object']['id'] + \
'">@unattributed</a>'
2019-08-22 12:41:16 +00:00
else:
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + translate['announces'] + \
'" alt="' + translate['announces'] + \
'" src="/' + iconsDir + \
'/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'):
2020-04-05 09:17:19 +00:00
containerClassIcons = 'containericons darker'
containerClass = 'container darker'
2019-10-31 09:59:06 +00:00
if postJsonObject['object']['inReplyTo'].startswith(postActor):
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + \
translate['replying to themselves'] + \
'" alt="' + translate['replying to themselves'] + \
'" src="/' + iconsDir + \
'/reply.png" class="announceOrReply"/>'
2019-08-25 17:22:24 +00:00
else:
2019-09-24 10:09:35 +00:00
if '/statuses/' in postJsonObject['object']['inReplyTo']:
2020-04-05 09:17:19 +00:00
inReplyTo = postJsonObject['object']['inReplyTo']
replyActor = inReplyTo.split('/statuses/')[0]
replyNickname = getNicknameFromActor(replyActor)
2019-09-24 10:09:35 +00:00
if replyNickname:
2020-04-05 09:17:19 +00:00
replyDomain, replyPort = \
getDomainFromActor(replyActor)
2019-09-24 10:09:35 +00:00
if replyNickname and replyDomain:
2020-04-05 09:17:19 +00:00
getPersonFromCache(baseDir, replyActor,
personCache)
replyDisplayName = \
getDisplayName(baseDir, replyActor,
personCache)
2019-09-24 10:09:35 +00:00
if replyDisplayName:
2019-10-18 12:55:53 +00:00
if ':' in replyDisplayName:
2020-04-05 09:17:19 +00:00
repDisp = replyDisplayName
replyDisplayName = \
addEmojiToDisplayName(baseDir,
httpPrefix,
nickname,
domain,
repDisp,
False)
titleStr += \
' <img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + \
translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> ' + \
'<a href="' + inReplyTo + \
'">' + replyDisplayName + '</a>'
2019-09-24 11:05:47 +00:00
2019-09-24 11:11:45 +00:00
# show avatar of person replied to
2020-04-05 09:17:19 +00:00
replyAvatarUrl = \
getPersonAvatarUrl(baseDir,
replyActor,
personCache)
2019-09-24 11:05:47 +00:00
if replyAvatarUrl:
2020-04-05 09:17:19 +00:00
replyAvatarImageInPost = \
'<div class=' + \
'"timeline-avatar-reply">'
replyAvatarImageInPost += \
'<a href="/users/' + nickname + \
'?options=' + replyActor + \
';' + str(pageNumber) + ';' + \
replyAvatarUrl + \
messageIdStr + '">'
replyAvatarImageInPost += \
'<img loading="lazy" src="' + \
replyAvatarUrl + '" '
replyAvatarImageInPost += \
'title="' + \
translate['Show profile']
replyAvatarImageInPost += \
'" alt=" "' + \
avatarPosition + '/></a></div>'
2019-09-24 10:09:35 +00:00
else:
2020-04-05 09:17:19 +00:00
inReplyTo = \
postJsonObject['object']['inReplyTo']
titleStr += \
' <img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + \
translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> ' + \
'<a href="' + \
inReplyTo + '">@' + \
replyNickname + '@' + \
replyDomain + '</a>'
2019-09-24 10:09:35 +00:00
else:
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + \
translate['replying to'] + \
'" src="/' + \
iconsDir + \
'/reply.png" class="announceOrReply"/> ' + \
'<a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">@unknown</a>'
2019-09-24 10:09:35 +00:00
else:
2020-04-05 09:17:19 +00:00
postDomain = \
postJsonObject['object']['inReplyTo']
postDomain = postDomain.replace('https://', '')
postDomain = postDomain.replace('http://', '')
2020-05-17 09:37:59 +00:00
postDomain = postDomain.replace('hyper://', '')
2020-04-05 09:17:19 +00:00
postDomain = postDomain.replace('dat://', '')
postDomain = postDomain.replace('i2p://', '')
2019-09-24 10:09:35 +00:00
if '/' in postDomain:
2020-04-05 09:17:19 +00:00
postDomain = postDomain.split('/', 1)[0]
2019-09-24 10:09:35 +00:00
if postDomain:
2020-04-05 09:17:19 +00:00
titleStr += \
' <img loading="lazy" title="' + \
translate['replying to'] + \
'" alt="' + translate['replying to'] + \
'" src="/' + \
iconsDir + '/reply.png" ' + \
'class="announceOrReply"/> <a href="' + \
postJsonObject['object']['inReplyTo'] + \
'">' + postDomain + '</a>'
attachmentStr, galleryStr = \
getPostAttachmentsAsHtml(postJsonObject, boxName, translate,
isMuted, avatarLink,
replyStr, announceStr, likeStr,
bookmarkStr, deleteStr, muteStr)
publishedStr = ''
2019-11-29 22:45:56 +00:00
if postJsonObject['object'].get('published'):
2020-04-05 09:17:19 +00:00
publishedStr = postJsonObject['object']['published']
2019-11-29 22:45:56 +00:00
if '.' not in publishedStr:
if '+' not in publishedStr:
2020-04-05 09:17:19 +00:00
datetimeObject = \
datetime.strptime(publishedStr, "%Y-%m-%dT%H:%M:%SZ")
2019-11-29 22:45:56 +00:00
else:
2020-04-05 09:17:19 +00:00
datetimeObject = \
datetime.strptime(publishedStr.split('+')[0] + 'Z',
2020-02-23 15:32:47 +00:00
"%Y-%m-%dT%H:%M:%SZ")
2019-08-27 22:50:40 +00:00
else:
2020-04-05 09:17:19 +00:00
publishedStr = \
publishedStr.replace('T', ' ').split('.')[0]
datetimeObject = parse(publishedStr)
publishedStr = datetimeObject.strftime("%a %b %d, %H:%M")
2020-02-24 23:14:49 +00:00
2020-04-05 09:17:19 +00:00
publishedLink = messageId
2020-02-24 23:14:49 +00:00
# blog posts should have no /statuses/ in their link
if isBlogPost(postJsonObject):
# is this a post to the local domain?
2020-04-05 09:17:19 +00:00
if '://' + domain in messageId:
publishedLink = messageId.replace('/statuses/', '/')
2020-03-28 17:50:55 +00:00
# if this is a local link then make it relative so that it works
# on clearnet or onion address
2020-04-05 09:17:19 +00:00
if domain + '/users/' in publishedLink or \
domain + ':' + str(port) + '/users/' in publishedLink:
publishedLink = '/users/' + publishedLink.split('/users/')[1]
2020-02-24 23:14:49 +00:00
2020-04-05 09:17:19 +00:00
footerStr = '<a href="' + publishedLink + \
'" class="' + timeClass + '">' + publishedStr + '</a>\n'
2019-07-31 19:37:29 +00:00
# change the background color for DMs in inbox timeline
if showDMicon:
2020-04-05 09:17:19 +00:00
containerClassIcons = 'containericons dm'
containerClass = 'container dm'
2019-07-30 12:47:42 +00:00
if showIcons:
2020-04-05 09:17:19 +00:00
footerStr = '<div class="' + containerClassIcons + '">'
footerStr += replyStr + announceStr + likeStr + bookmarkStr + \
deleteStr + muteStr + editStr
footerStr += '<a href="' + publishedLink + '" class="' + \
timeClass + '">' + publishedStr + '</a>\n'
footerStr += '</div>'
postIsSensitive = False
2020-02-18 10:04:31 +00:00
if postJsonObject['object'].get('sensitive'):
# sensitive posts should have a summary
if postJsonObject['object'].get('summary'):
2020-04-05 09:17:19 +00:00
postIsSensitive = postJsonObject['object']['sensitive']
else:
# add a generic summary if none is provided
2020-04-05 09:17:19 +00:00
postJsonObject['object']['summary'] = translate['Sensitive']
2020-02-23 15:32:47 +00:00
# add an extra line if there is a content warning,
# for better vertical spacing on mobile
2020-02-18 10:04:31 +00:00
if postIsSensitive:
2020-04-05 09:17:19 +00:00
footerStr = '<br>' + footerStr
2020-01-03 09:37:30 +00:00
2019-10-17 22:45:02 +00:00
if not postJsonObject['object'].get('summary'):
2020-04-05 09:17:19 +00:00
postJsonObject['object']['summary'] = ''
2019-11-04 20:39:14 +00:00
2019-11-29 22:49:17 +00:00
if not postJsonObject['object'].get('content'):
return ''
isPatch = isGitPatch(baseDir, nickname, domain,
2020-05-03 13:12:52 +00:00
postJsonObject['object']['type'],
postJsonObject['object']['summary'],
postJsonObject['object']['content'])
if not isPatch:
objectContent = \
removeLongWords(postJsonObject['object']['content'], 40, [])
objectContent = \
switchWords(baseDir, nickname, domain, objectContent)
2020-05-02 20:06:01 +00:00
else:
objectContent = postJsonObject['object']['content']
2020-02-18 10:04:31 +00:00
if not postIsSensitive:
2020-04-05 09:17:19 +00:00
contentStr = objectContent + attachmentStr
contentStr = addEmbeddedElements(translate, contentStr)
contentStr = insertQuestion(baseDir, translate,
nickname, domain, port,
contentStr, postJsonObject,
pageNumber)
2019-07-31 12:44:08 +00:00
else:
2020-04-05 09:17:19 +00:00
postID = 'post' + str(createPassword(8))
contentStr = ''
2019-07-31 12:44:08 +00:00
if postJsonObject['object'].get('summary'):
2020-04-05 09:17:19 +00:00
contentStr += \
'<b>' + postJsonObject['object']['summary'] + '</b> '
if isModerationPost:
2020-04-05 09:17:19 +00:00
containerClass = 'container report'
contentStr += \
'<button class="cwButton" onclick="showContentWarning(' + \
"'" + postID + "'" + ')">' + translate['SHOW MORE'] + '</button>'
contentStr += '<div class="cwText" id="' + postID + '">'
contentStr += objectContent + attachmentStr
if not isPatch:
contentStr = addEmbeddedElements(translate, contentStr)
contentStr = \
insertQuestion(baseDir, translate, nickname, domain, port,
contentStr, postJsonObject, pageNumber)
2020-04-05 09:17:19 +00:00
contentStr += '</div>'
2019-07-31 12:44:08 +00:00
if postJsonObject['object'].get('tag') and not isPatch:
2020-04-05 09:17:19 +00:00
contentStr = \
replaceEmojiFromTags(contentStr,
postJsonObject['object']['tag'],
'content')
2019-08-19 12:59:57 +00:00
2019-12-01 13:45:30 +00:00
if isMuted:
2020-04-05 09:17:19 +00:00
contentStr = ''
2019-12-01 13:45:30 +00:00
else:
if not isPatch:
2020-05-02 19:24:17 +00:00
contentStr = '<div class="message">' + contentStr + '</div>'
else:
2020-05-02 19:39:09 +00:00
contentStr = \
2020-05-02 21:12:06 +00:00
'<div class="gitpatch"><pre><code>' + contentStr + \
'</code></pre></div>'
2020-04-05 09:17:19 +00:00
postHtml = ''
if boxName != 'tlmedia':
postHtml = '<div id="' + timelinePostBookmark + \
'" class="' + containerClass + '">\n'
postHtml += avatarImageInPost
postHtml += '<p class="post-title">' + titleStr + \
replyAvatarImageInPost + '</p>'
postHtml += contentStr + footerStr
postHtml += '</div>\n'
2019-09-28 11:29:42 +00:00
else:
2020-04-05 09:17:19 +00:00
postHtml = galleryStr
2019-10-19 09:13:08 +00:00
if not showPublicOnly and storeToCache and \
2020-05-21 20:15:24 +00:00
boxName != 'tlmedia' and boxName != 'tlbookmarks' and \
boxName != 'bookmarks':
2020-04-05 09:17:19 +00:00
saveIndividualPostAsHtmlToCache(baseDir, nickname, domain,
postJsonObject, postHtml)
updateRecentPostsCache(recentPostsCache, maxRecentPosts,
postJsonObject, postHtml)
2019-10-19 09:13:08 +00:00
2019-10-19 11:53:57 +00:00
return postHtml
2019-07-21 09:09:28 +00:00
2020-04-05 09:17:19 +00:00
2019-09-06 15:08:32 +00:00
def isQuestion(postObjectJson: {}) -> bool:
""" is the given post a question?
"""
2020-04-05 09:17:19 +00:00
if postObjectJson['type'] != 'Create' and \
postObjectJson['type'] != 'Update':
2019-11-25 10:10:59 +00:00
return False
if not isinstance(postObjectJson['object'], dict):
return False
if not postObjectJson['object'].get('type'):
return False
2020-04-05 09:17:19 +00:00
if postObjectJson['object']['type'] != 'Question':
2019-11-25 10:10:59 +00:00
return False
if not postObjectJson['object'].get('oneOf'):
return False
if not isinstance(postObjectJson['object']['oneOf'], list):
return False
return True
2019-09-06 15:08:32 +00:00
2020-04-05 09:17:19 +00:00
def htmlTimeline(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int,
itemsPerPage: int, session, baseDir: str,
wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, timelineJson: {},
boxName: str, allowDeletion: bool,
httpPrefix: str, projectVersion: str,
2020-05-23 14:23:56 +00:00
manuallyApproveFollowers: bool,
minimal: bool) -> str:
2019-07-21 09:09:28 +00:00
"""Show the timeline as html
"""
2020-04-05 09:17:19 +00:00
accountDir = baseDir + '/accounts/' + nickname + '@' + domain
# should the calendar icon be highlighted?
2020-04-05 09:17:19 +00:00
calendarImage = 'calendar.png'
calendarPath = '/calendar'
calendarFile = accountDir + '/.newCalendar'
if os.path.isfile(calendarFile):
2020-04-05 09:17:19 +00:00
calendarImage = 'calendar_notify.png'
with open(calendarFile, 'r') as calfile:
2020-05-22 11:32:38 +00:00
calendarPath = calfile.read().replace('##sent##', '')
calendarPath = calendarPath.replace('\n', '').replace('\r', '')
# should the DM button be highlighted?
2020-04-05 09:17:19 +00:00
newDM = False
dmFile = accountDir + '/.newDM'
if os.path.isfile(dmFile):
2020-04-05 09:17:19 +00:00
newDM = True
if boxName == 'dm':
os.remove(dmFile)
# should the Replies button be highlighted?
2020-04-05 09:17:19 +00:00
newReply = False
replyFile = accountDir + '/.newReply'
if os.path.isfile(replyFile):
2020-04-05 09:17:19 +00:00
newReply = True
if boxName == 'tlreplies':
os.remove(replyFile)
2019-11-02 11:31:25 +00:00
# should the Shares button be highlighted?
2020-04-05 09:17:19 +00:00
newShare = False
newShareFile = accountDir + '/.newShare'
2019-11-02 11:31:25 +00:00
if os.path.isfile(newShareFile):
2020-04-05 09:17:19 +00:00
newShare = True
if boxName == 'tlshares':
2019-11-02 11:31:25 +00:00
os.remove(newShareFile)
# should the Moderation button be highlighted?
2020-04-05 09:17:19 +00:00
newReport = False
newReportFile = accountDir + '/.newReport'
if os.path.isfile(newReportFile):
2020-04-05 09:17:19 +00:00
newReport = True
if boxName == 'moderation':
os.remove(newReportFile)
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-11-14 14:52:35 +00:00
2020-04-05 09:17:19 +00:00
bannerFile = 'banner.png'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
2019-11-14 14:52:35 +00:00
if not os.path.isfile(bannerFilename):
2020-04-05 09:17:19 +00:00
bannerFile = 'banner.jpg'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
2019-11-14 14:52:35 +00:00
if not os.path.isfile(bannerFilename):
2020-04-05 09:17:19 +00:00
bannerFile = 'banner.gif'
bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile
2019-11-14 14:52:35 +00:00
if not os.path.isfile(bannerFilename):
2020-04-05 09:17:19 +00:00
bannerFile = 'banner.webp'
2020-03-22 21:16:02 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = \
cssFile.read().replace('banner.png',
'/users/' + nickname + '/' + bannerFile)
if httpPrefix != 'https':
profileStyle = \
profileStyle.replace('https://',
httpPrefix + '://')
moderator = isModerator(baseDir, nickname)
inboxButton = 'button'
blogsButton = 'button'
dmButton = 'button'
if newDM:
2020-04-05 09:17:19 +00:00
dmButton = 'buttonhighlighted'
repliesButton = 'button'
if newReply:
2020-04-05 09:17:19 +00:00
repliesButton = 'buttonhighlighted'
mediaButton = 'button'
bookmarksButton = 'button'
sentButton = 'button'
sharesButton = 'button'
2019-11-16 16:08:13 +00:00
if newShare:
2020-04-05 09:17:19 +00:00
sharesButton = 'buttonhighlighted'
moderationButton = 'button'
2019-11-16 16:08:13 +00:00
if newReport:
2020-04-05 09:17:19 +00:00
moderationButton = 'buttonhighlighted'
if boxName == 'inbox':
inboxButton = 'buttonselected'
elif boxName == 'tlblogs':
blogsButton = 'buttonselected'
elif boxName == 'dm':
dmButton = 'buttonselected'
if newDM:
2020-04-05 09:17:19 +00:00
dmButton = 'buttonselectedhighlighted'
elif boxName == 'tlreplies':
repliesButton = 'buttonselected'
if newReply:
2020-04-05 09:17:19 +00:00
repliesButton = 'buttonselectedhighlighted'
elif boxName == 'tlmedia':
mediaButton = 'buttonselected'
elif boxName == 'outbox':
sentButton = 'buttonselected'
elif boxName == 'moderation':
moderationButton = 'buttonselected'
if newReport:
2020-04-05 09:17:19 +00:00
moderationButton = 'buttonselectedhighlighted'
elif boxName == 'tlshares':
sharesButton = 'buttonselected'
2019-11-02 11:31:25 +00:00
if newShare:
2020-04-05 09:17:19 +00:00
sharesButton = 'buttonselectedhighlighted'
2020-05-21 20:15:24 +00:00
elif boxName == 'tlbookmarks' or boxName == 'bookmarks':
2020-04-05 09:17:19 +00:00
bookmarksButton = 'buttonselected'
2019-11-02 14:53:30 +00:00
2020-04-05 09:17:19 +00:00
fullDomain = domain
if port != 80 and port != 443:
2019-11-02 14:53:30 +00:00
if ':' not in domain:
2020-04-05 09:17:19 +00:00
fullDomain = domain + ':' + str(port)
usersPath = '/users/' + nickname
actor = httpPrefix + '://' + fullDomain + usersPath
2019-07-29 20:56:07 +00:00
2020-04-05 09:17:19 +00:00
showIndividualPostIcons = True
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
followApprovals = ''
followRequestsFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/followrequests.txt'
2019-07-29 20:56:07 +00:00
if os.path.isfile(followRequestsFilename):
2020-04-05 09:17:19 +00:00
with open(followRequestsFilename, 'r') as f:
2019-07-29 20:56:07 +00:00
for line in f:
2020-04-05 09:17:19 +00:00
if len(line) > 0:
2019-07-30 10:21:02 +00:00
# show follow approvals icon
2020-04-05 09:17:19 +00:00
followApprovals = \
'<a href="' + usersPath + \
'/followers"><img loading="lazy" ' + \
'class="timelineicon" alt="' + \
translate['Approve follow requests'] + \
'" title="' + translate['Approve follow requests'] + \
'" src="/' + iconsDir + '/person.png"/></a>'
2019-07-29 20:56:07 +00:00
break
2020-04-05 09:17:19 +00:00
moderationButtonStr = ''
2020-05-23 14:23:56 +00:00
if moderator and not minimal:
2020-04-05 09:17:19 +00:00
moderationButtonStr = \
'<a href="' + usersPath + \
'/moderation"><button class="' + \
moderationButton + '"><span>' + \
translate['Mod'] + ' </span></button></a>'
2020-05-23 14:23:56 +00:00
sharesButtonStr = ''
bookmarksButtonStr = ''
if not minimal:
sharesButtonStr = \
'<a href="' + usersPath + '/tlshares"><button class="' + \
sharesButton + '"><span>' + translate['Shares'] + \
' </span></button></a>'
2019-11-02 11:31:25 +00:00
2020-05-23 14:23:56 +00:00
bookmarksButtonStr = \
'<a href="' + usersPath + '/tlbookmarks"><button class="' + \
bookmarksButton + '"><span>' + translate['Bookmarks'] + \
' </span></button></a>'
2019-11-17 14:01:49 +00:00
2020-04-05 09:17:19 +00:00
tlStr = htmlHeader(cssFilename, profileStyle)
2020-04-05 09:17:19 +00:00
if boxName != 'dm':
if boxName != 'tlblogs':
2020-02-24 14:54:42 +00:00
if not manuallyApproveFollowers:
2020-04-05 09:17:19 +00:00
newPostButtonStr = \
'<a href="' + usersPath + \
'/newpost"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="' + \
translate['Create a new post'] + \
'" class="timelineicon"/></a>'
2020-02-24 14:54:42 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostButtonStr = \
'<a href="' + usersPath + \
'/newfollowers"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + \
'" alt="' + translate['Create a new post'] + \
'" class="timelineicon"/></a>'
2020-02-24 14:54:42 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostButtonStr = \
'<a href="' + usersPath + \
'/newblog"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="' + \
translate['Create a new post'] + \
'" class="timelineicon"/></a>'
2019-08-28 20:05:10 +00:00
else:
2020-04-05 09:17:19 +00:00
newPostButtonStr = \
'<a href="' + usersPath + \
'/newdm"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new DM'] + \
'" alt="' + translate['Create a new DM'] + \
'" class="timelineicon"/></a>'
# This creates a link to the profile page when viewed
# in lynx, but should be invisible in a graphical web browser
tlStr += \
'<a href="/users/' + nickname + '"><label class="transparent">' + \
translate['Switch to profile view'] + '</label></a>'
2020-03-22 21:16:02 +00:00
# banner and row of buttons
2020-04-05 09:17:19 +00:00
tlStr += \
'<a href="/users/' + nickname + '" title="' + \
translate['Switch to profile view'] + '" alt="' + \
translate['Switch to profile view'] + '">'
tlStr += '<div class="timeline-banner">'
tlStr += '</div></a>'
tlStr += '<div class="container">\n'
2020-02-24 14:39:25 +00:00
# first button
2020-04-05 09:17:19 +00:00
if defaultTimeline == 'tlmedia':
tlStr += \
' <a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
2020-02-23 15:32:47 +00:00
'</span></button></a>'
2020-04-05 09:17:19 +00:00
elif defaultTimeline == 'tlblogs':
tlStr += \
' <a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + translate['Blogs'] + \
2020-02-24 14:39:25 +00:00
'</span></button></a>'
else:
2020-04-05 09:17:19 +00:00
tlStr += \
' <a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + \
translate['Inbox'] + '</span></button></a>'
tlStr += \
' <a href="' + usersPath + '/dm"><button class="' + dmButton + \
'"><span>' + translate['DM'] + '</span></button></a>'
tlStr += \
' <a href="' + usersPath + '/tlreplies"><button class="' + \
repliesButton + '"><span>' + translate['Replies'] + \
2020-02-23 15:32:47 +00:00
'</span></button></a>'
2020-02-24 14:39:25 +00:00
# typically the media button
2020-04-05 09:17:19 +00:00
if defaultTimeline != 'tlmedia':
2020-05-23 14:23:56 +00:00
if not minimal:
tlStr += \
' <a href="' + usersPath + \
'/tlmedia"><button class="' + \
mediaButton + '"><span>' + translate['Media'] + \
'</span></button></a>'
2019-11-28 16:16:43 +00:00
else:
2020-05-23 14:23:56 +00:00
if not minimal:
tlStr += \
' <a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton+'"><span>' + translate['Inbox'] + \
'</span></button></a>'
2020-02-24 14:39:25 +00:00
# typically the blogs button
2020-04-05 09:17:19 +00:00
if defaultTimeline != 'tlblogs':
2020-05-23 14:23:56 +00:00
if not minimal:
tlStr += \
' <a href="' + usersPath + \
'/tlblogs"><button class="' + \
blogsButton + '"><span>' + translate['Blogs'] + \
'</span></button></a>'
2020-02-24 14:39:25 +00:00
else:
2020-05-23 14:23:56 +00:00
if not minimal:
tlStr += \
' <a href="' + usersPath + \
'/inbox"><button class="' + \
inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>'
2020-02-24 14:39:25 +00:00
2020-04-05 09:17:19 +00:00
tlStr += \
' <a href="' + usersPath + \
'/outbox"><button class="' + \
sentButton+'"><span>' + translate['Outbox'] + \
2020-02-23 15:32:47 +00:00
'</span></button></a>'
2020-04-05 09:17:19 +00:00
tlStr += \
sharesButtonStr + bookmarksButtonStr + \
moderationButtonStr + newPostButtonStr
tlStr += \
' <a href="' + usersPath + \
'/search"><img loading="lazy" src="/' + \
iconsDir + '/search.png" title="' + \
translate['Search and follow'] + '" alt="' + \
translate['Search and follow'] + '" class="timelineicon"/></a>'
tlStr += \
' <a href="' + usersPath + calendarPath + \
'"><img loading="lazy" src="/' + iconsDir + '/' + \
calendarImage + '" title="' + translate['Calendar'] + \
'" alt="' + translate['Calendar'] + \
'" class="timelineicon"/></a>'
tlStr += \
2020-05-23 14:23:56 +00:00
' <a href="' + usersPath + '/minimal' + \
2020-04-05 09:17:19 +00:00
'"><img loading="lazy" src="/' + iconsDir + \
2020-05-23 19:31:06 +00:00
'/showhide.png" title="' + translate['Show/Hide Buttons'] + \
2020-05-23 14:23:56 +00:00
'" alt="' + translate['Show/Hide Buttons'] + \
2020-04-05 09:17:19 +00:00
'" class="timelineicon"/></a>'
tlStr += followApprovals
tlStr += '</div>'
# second row of buttons for moderator actions
2020-04-05 09:17:19 +00:00
if moderator and boxName == 'moderation':
tlStr += \
'<form method="POST" action="/users/' + \
nickname + '/moderationaction">'
tlStr += '<div class="container">\n'
idx = 'Nickname or URL. Block using *@domain or nickname@domain'
tlStr += \
' <b>' + translate[idx] + '</b><br>\n'
tlStr += ' <input type="text" ' + \
'name="moderationAction" value="" autofocus><br>\n'
tlStr += \
' <input type="submit" title="' + \
translate['Remove the above item'] + \
'" name="submitRemove" value="' + \
translate['Remove'] + '">'
tlStr += \
' <input type="submit" title="' + \
translate['Suspend the above account nickname'] + \
'" name="submitSuspend" value="' + translate['Suspend'] + '">'
tlStr += \
' <input type="submit" title="' + \
translate['Remove a suspension for an account nickname'] + \
'" name="submitUnsuspend" value="' + \
translate['Unsuspend'] + '">'
tlStr += \
' <input type="submit" title="' + \
translate['Block an account on another instance'] + \
'" name="submitBlock" value="' + translate['Block'] + '">'
tlStr += \
' <input type="submit" title="' + \
translate['Unblock an account on another instance'] + \
'" name="submitUnblock" value="' + translate['Unblock'] + '">'
tlStr += \
' <input type="submit" title="' + \
translate['Information about current blocks/suspensions'] + \
'" name="submitInfo" value="' + translate['Info'] + '">'
tlStr += '</div></form>'
if boxName == 'tlshares':
maxSharesPerAccount = itemsPerPage
return (tlStr +
htmlSharesTimeline(translate, pageNumber, itemsPerPage,
baseDir, actor, nickname, domain, port,
maxSharesPerAccount, httpPrefix) +
htmlFooter())
2019-11-02 14:19:51 +00:00
# add the javascript for content warnings
2020-04-05 09:17:19 +00:00
tlStr += '<script>' + contentWarningScript() + '</script>'
2020-02-22 22:39:13 +00:00
# show todays events buttons on the first inbox page
2020-04-05 09:17:19 +00:00
if boxName == 'inbox' and pageNumber == 1:
if todaysEventsCheck(baseDir, nickname, domain):
now = datetime.now()
tlStr += \
'<center><a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '"><button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>'
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'<a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a>'
tlStr += '</center>'
2020-02-22 18:35:50 +00:00
else:
2020-04-05 09:17:19 +00:00
if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \
'<center><a href="' + usersPath + \
'/calendar"><button class="buttonevent">' + \
translate['Happening This Week'] + '</button></a></center>'
# page up arrow
2020-04-05 09:17:19 +00:00
if pageNumber > 1:
tlStr += \
'<center><a href="' + usersPath + '/' + boxName + \
'?page=' + str(pageNumber - 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pageup.png" title="' + \
translate['Page up'] + '" alt="' + \
translate['Page up'] + '"></a></center>'
# show the posts
2020-04-05 09:17:19 +00:00
itemCtr = 0
2019-09-23 21:05:31 +00:00
if timelineJson:
2020-04-05 09:17:19 +00:00
if boxName == 'tlmedia':
if pageNumber > 1:
tlStr += '<br>'
tlStr += '<div class="galleryContainer">\n'
2019-09-23 21:05:31 +00:00
for item in timelineJson['orderedItems']:
2020-04-05 09:17:19 +00:00
if item['type'] == 'Create' or \
item['type'] == 'Announce' or \
item['type'] == 'Update':
2019-11-25 09:37:01 +00:00
# is the actor who sent this post snoozed?
2020-04-05 09:17:19 +00:00
if isPersonSnoozed(baseDir, nickname, domain, item['actor']):
2019-11-25 09:37:01 +00:00
continue
# is the post in the memory cache of recent ones?
2020-04-05 09:17:19 +00:00
currTlStr = None
if boxName != 'tlmedia' and \
recentPostsCache.get('index'):
postId = \
item['id'].replace('/activity', '').replace('/', '#')
if postId in recentPostsCache['index']:
if not item.get('muted'):
2020-03-22 21:16:02 +00:00
if recentPostsCache['html'].get(postId):
2020-04-05 09:17:19 +00:00
currTlStr = recentPostsCache['html'][postId]
currTlStr = \
preparePostFromHtmlCache(currTlStr,
boxName,
pageNumber)
if not currTlStr:
2019-11-25 09:37:01 +00:00
# read the post from disk
2020-04-05 09:17:19 +00:00
currTlStr = \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, pageNumber,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
item, None, True,
allowDeletion,
httpPrefix, projectVersion,
boxName,
boxName != 'dm',
showIndividualPostIcons,
manuallyApproveFollowers,
False, True)
2019-09-30 16:26:59 +00:00
if currTlStr:
2020-04-05 09:17:19 +00:00
itemCtr += 1
tlStr += currTlStr
if boxName == 'tlmedia':
tlStr += '</div>\n'
# page down arrow
2020-04-05 09:17:19 +00:00
if itemCtr > 2:
tlStr += \
'<center><a href="' + usersPath + '/' + boxName + '?page=' + \
str(pageNumber + 1) + \
'"><img loading="lazy" class="pageicon" src="/' + \
iconsDir + '/pagedown.png" title="' + \
translate['Page down'] + '" alt="' + \
translate['Page down'] + '"></a></center>'
tlStr += htmlFooter()
2019-07-21 09:09:28 +00:00
return tlStr
2020-04-05 09:17:19 +00:00
def htmlShares(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int,
allowDeletion: bool,
httpPrefix: str, projectVersion: str) -> str:
2019-11-02 14:31:39 +00:00
"""Show the shares timeline as html
"""
2020-04-05 09:17:19 +00:00
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, None,
'tlshares', allowDeletion,
2020-05-23 14:23:56 +00:00
httpPrefix, projectVersion, manuallyApproveFollowers,
False)
2020-04-05 09:17:19 +00:00
def htmlInbox(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-07-20 21:13:36 +00:00
"""Show the inbox as html
"""
2020-04-05 09:17:19 +00:00
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson,
'inbox', allowDeletion,
2020-05-23 14:23:56 +00:00
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal)
2020-04-05 09:17:19 +00:00
def htmlBookmarks(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, bookmarksJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-11-17 14:01:49 +00:00
"""Show the bookmarks as html
"""
2020-04-05 09:17:19 +00:00
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, bookmarksJson,
'tlbookmarks', allowDeletion,
2020-05-23 14:23:56 +00:00
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal)
2020-04-05 09:17:19 +00:00
def htmlInboxDMs(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-08-25 17:34:09 +00:00
"""Show the DM timeline as html
"""
2020-04-05 09:17:19 +00:00
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'dm', allowDeletion,
2020-05-23 14:23:56 +00:00
httpPrefix, projectVersion, False, minimal)
2020-04-05 09:17:19 +00:00
def htmlInboxReplies(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-09-23 20:09:11 +00:00
"""Show the replies timeline as html
"""
2020-04-05 09:17:19 +00:00
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlreplies',
2020-05-23 14:23:56 +00:00
allowDeletion, httpPrefix, projectVersion, False,
minimal)
2020-04-05 09:17:19 +00:00
def htmlInboxMedia(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-09-28 11:29:42 +00:00
"""Show the media timeline as html
"""
2020-04-05 09:17:19 +00:00
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlmedia',
2020-05-23 14:23:56 +00:00
allowDeletion, httpPrefix, projectVersion, False,
minimal)
2020-04-05 09:17:19 +00:00
def htmlInboxBlogs(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2020-02-24 14:39:25 +00:00
"""Show the blogs timeline as html
"""
2020-04-05 09:17:19 +00:00
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlblogs',
2020-05-23 14:23:56 +00:00
allowDeletion, httpPrefix, projectVersion, False,
minimal)
2020-04-05 09:17:19 +00:00
def htmlModeration(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str) -> str:
2019-08-12 13:22:17 +00:00
"""Show the moderation feed as html
"""
2020-04-05 09:17:19 +00:00
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'moderation',
2020-05-23 14:23:56 +00:00
allowDeletion, httpPrefix, projectVersion, True, False)
2020-04-05 09:17:19 +00:00
def htmlOutbox(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, 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,
2020-05-23 14:23:56 +00:00
httpPrefix: str, projectVersion: str,
minimal: bool) -> str:
2019-07-20 21:13:36 +00:00
"""Show the Outbox as html
"""
2020-04-05 09:17:19 +00:00
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, outboxJson, 'outbox',
allowDeletion, httpPrefix, projectVersion,
2020-05-23 14:23:56 +00:00
manuallyApproveFollowers, minimal)
2020-04-05 09:17:19 +00:00
def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, authorized: bool,
postJsonObject: {}, httpPrefix: str,
projectVersion: str) -> str:
2019-07-20 21:13:36 +00:00
"""Show an individual post as html
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
postStr = '<script>' + contentWarningScript() + '</script>'
postStr += \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest, personCache,
nickname, domain, port, postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'inbox',
False, authorized, False, False, False)
messageId = postJsonObject['id'].replace('/activity', '')
2019-08-02 19:47:30 +00:00
# show the previous posts
2020-02-05 18:50:53 +00:00
if isinstance(postJsonObject['object'], dict):
while postJsonObject['object'].get('inReplyTo'):
2020-04-05 09:17:19 +00:00
postFilename = \
locatePost(baseDir, nickname, domain,
2020-02-23 14:24:11 +00:00
postJsonObject['object']['inReplyTo'])
2020-02-05 18:50:53 +00:00
if not postFilename:
break
2020-04-05 09:17:19 +00:00
postJsonObject = loadJson(postFilename)
2020-02-05 18:50:53 +00:00
if postJsonObject:
2020-04-05 09:17:19 +00:00
postStr = \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port,
postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'inbox',
False, authorized,
False, False, False) + postStr
2019-08-02 19:47:30 +00:00
# show the following posts
2020-04-05 09:17:19 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
if postFilename:
# is there a replies file for this post?
2020-04-05 09:17:19 +00:00
repliesFilename = postFilename.replace('.json', '.replies')
if os.path.isfile(repliesFilename):
# get items from the replies file
2020-04-05 09:17:19 +00:00
repliesJson = {
'orderedItems': []
}
populateRepliesJson(baseDir, nickname, domain,
repliesFilename, authorized, repliesJson)
# add items to the html output
for item in repliesJson['orderedItems']:
2020-04-05 09:17:19 +00:00
postStr += \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest,
personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
False, authorized,
False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-18 10:01:31 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
postsCSS = cssFile.read()
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + postStr + htmlFooter()
def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int,
translate: {}, baseDir: str,
session, wfRequest: {}, personCache: {},
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
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
repliesStr = ''
2019-08-02 16:49:42 +00:00
if repliesJson.get('orderedItems'):
for item in repliesJson['orderedItems']:
2020-04-05 09:17:19 +00:00
repliesStr += \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None,
baseDir, session, wfRequest, personCache,
nickname, domain, port, item,
None, True, False,
httpPrefix, projectVersion, 'inbox',
False, False, False, False, False)
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-18 10:01:31 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
postsCSS = cssFile.read()
if httpPrefix != 'https':
postsCSS = postsCSS.replace('https://',
httpPrefix + '://')
return htmlHeader(cssFilename, postsCSS) + repliesStr + htmlFooter()
2019-07-29 09:49:46 +00:00
2020-04-05 09:17:19 +00:00
def htmlRemoveSharedItem(translate: {}, baseDir: str,
actor: str, shareName: str) -> str:
"""Shows a screen asking to confirm the removal of a shared item
"""
2020-04-05 09:17:19 +00:00
itemID = getValidSharedItemID(shareName)
nickname = getNicknameFromActor(actor)
domain, port = getDomainFromActor(actor)
sharesFile = baseDir + '/accounts/' + \
nickname + '@' + domain + '/shares.json'
if not os.path.isfile(sharesFile):
2020-04-05 09:17:19 +00:00
print('ERROR: no shares file ' + sharesFile)
return None
2020-04-05 09:17:19 +00:00
sharesJson = loadJson(sharesFile)
if not sharesJson:
2019-11-03 09:36:04 +00:00
print('ERROR: unable to load shares.json')
return None
2019-11-03 09:48:01 +00:00
if not sharesJson.get(itemID):
2020-04-05 09:17:19 +00:00
print('ERROR: share named "' + itemID + '" is not in ' + sharesFile)
return None
2020-04-05 09:17:19 +00:00
sharedItemDisplayName = sharesJson[itemID]['displayName']
sharedItemImageUrl = None
2019-11-03 09:48:01 +00:00
if sharesJson[itemID].get('imageUrl'):
2020-04-05 09:17:19 +00:00
sharedItemImageUrl = sharesJson[itemID]['imageUrl']
2020-04-05 09:17:19 +00:00
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')
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
sharesStr = htmlHeader(cssFilename, profileStyle)
sharesStr += '<div class="follow">'
sharesStr += ' <div class="followAvatar">'
sharesStr += ' <center>'
if sharedItemImageUrl:
2020-04-05 09:17:19 +00:00
sharesStr += ' <img loading="lazy" src="' + \
sharedItemImageUrl + '"/>'
sharesStr += \
' <p class="followText">' + translate['Remove'] + \
' ' + sharedItemDisplayName + ' ?</p>'
sharesStr += ' <form method="POST" action="' + actor + '/rmshare">'
sharesStr += ' <input type="hidden" name="actor" value="' + actor + '">'
sharesStr += ' <input type="hidden" name="shareName" value="' + \
shareName + '">'
sharesStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
sharesStr += \
' <a href="' + actor + '/inbox' + '"><button class="button">' + \
translate['No'] + '</button></a>'
sharesStr += ' </form>'
sharesStr += ' </center>'
sharesStr += ' </div>'
sharesStr += '</div>'
sharesStr += htmlFooter()
return sharesStr
2019-08-27 12:47:11 +00:00
2020-04-05 09:17:19 +00:00
def htmlDeletePost(recentPostsCache: {}, maxRecentPosts: int,
translate, pageNumber: int,
session, baseDir: str, messageId: str,
httpPrefix: str, projectVersion: str,
wfRequest: {}, personCache: {}) -> str:
2019-08-27 12:47:11 +00:00
"""Shows a screen asking to confirm the deletion of a post
"""
if '/statuses/' not in messageId:
return None
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
actor = messageId.split('/statuses/')[0]
nickname = getNicknameFromActor(actor)
domain, port = getDomainFromActor(actor)
2019-08-27 12:47:11 +00:00
2020-04-05 09:17:19 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
2019-08-27 12:47:11 +00:00
if not postFilename:
return None
2020-04-05 09:17:19 +00:00
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return None
2019-08-27 12:47:11 +00:00
2020-04-05 09:17:19 +00:00
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')
2019-08-27 12:47:11 +00:00
2020-04-05 09:17:19 +00:00
deletePostStr = None
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
if httpPrefix != 'https':
profileStyle = profileStyle.replace('https://',
httpPrefix + '://')
deletePostStr = htmlHeader(cssFilename, profileStyle)
deletePostStr += '<script>' + contentWarningScript() + '</script>'
deletePostStr += \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, pageNumber,
baseDir, session, wfRequest, personCache,
nickname, domain, port, postJsonObject,
None, True, False,
httpPrefix, projectVersion, 'outbox',
False, False, False, False, False)
deletePostStr += '<center>'
deletePostStr += \
' <p class="followText">' + \
translate['Delete this post?'] + '</p>'
deletePostStr += ' <form method="POST" action="' + actor + '/rmpost">'
deletePostStr += \
' <input type="hidden" name="pageNumber" value="' + \
str(pageNumber) + '">'
deletePostStr += \
' <input type="hidden" name="messageId" value="' + \
messageId + '">'
deletePostStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
deletePostStr += \
' <a href="' + actor + '/inbox"><button class="button">' + \
translate['No'] + '</button></a>'
deletePostStr += ' </form>'
deletePostStr += '</center>'
deletePostStr += htmlFooter()
2019-08-27 12:47:11 +00:00
return deletePostStr
2020-04-05 09:17:19 +00:00
def htmlCalendarDeleteConfirm(translate: {}, baseDir: str,
path: str, httpPrefix: str,
domainFull: str, postId: str, postTime: str,
year: int, monthNumber: int,
dayNumber: int) -> str:
2020-02-23 12:20:54 +00:00
"""Shows a screen asking to confirm the deletion of a calendar event
"""
2020-04-05 09:17:19 +00:00
nickname = getNicknameFromActor(path)
actor = httpPrefix + '://' + domainFull + '/users/' + nickname
domain, port = getDomainFromActor(actor)
messageId = actor + '/statuses/' + postId
2020-02-23 12:20:54 +00:00
2020-04-05 09:17:19 +00:00
postFilename = locatePost(baseDir, nickname, domain, messageId)
2020-02-23 12:20:54 +00:00
if not postFilename:
return None
2020-04-05 09:17:19 +00:00
postJsonObject = loadJson(postFilename)
2020-02-23 12:20:54 +00:00
if not postJsonObject:
return None
2020-04-05 09:17:19 +00:00
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')
2020-02-23 12:20:54 +00:00
2020-04-05 09:17:19 +00:00
deletePostStr = None
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2020-02-23 12:20:54 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
if httpPrefix != 'https':
profileStyle = profileStyle.replace('https://',
httpPrefix + '://')
deletePostStr = htmlHeader(cssFilename, profileStyle)
deletePostStr += \
'<center><h1>' + postTime + ' ' + str(year) + '/' + \
str(monthNumber) + \
'/' + str(dayNumber) + '</h1></center>'
deletePostStr += '<center>'
deletePostStr += ' <p class="followText">' + \
translate['Delete this event'] + '</p>'
deletePostStr += ' <form method="POST" action="' + actor + '/rmpost">'
deletePostStr += ' <input type="hidden" name="year" value="' + \
str(year) + '">'
deletePostStr += ' <input type="hidden" name="month" value="' + \
str(monthNumber) + '">'
deletePostStr += ' <input type="hidden" name="day" value="' + \
str(dayNumber) + '">'
deletePostStr += \
' <input type="hidden" name="pageNumber" value="1">'
deletePostStr += \
' <input type="hidden" name="messageId" value="' + \
messageId + '">'
deletePostStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
deletePostStr += \
' <a href="' + actor + '/calendar?year=' + \
str(year) + '?month=' + \
str(monthNumber) + '"><button class="button">' + \
translate['No'] + '</button></a>'
deletePostStr += ' </form>'
deletePostStr += '</center>'
deletePostStr += htmlFooter()
2020-02-23 12:20:54 +00:00
return deletePostStr
2020-04-05 09:17:19 +00:00
def htmlFollowConfirm(translate: {}, baseDir: str,
originPathStr: str,
followActor: str,
2019-09-07 08:57:52 +00:00
followProfileUrl: str) -> str:
2019-07-29 09:49:46 +00:00
"""Asks to confirm a follow
"""
2020-04-05 09:17:19 +00:00
followDomain, port = getDomainFromActor(followActor)
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
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')
2019-07-29 09:49:46 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
followStr += '<div class="follow">'
followStr += ' <div class="followAvatar">'
followStr += ' <center>'
followStr += ' <a href="' + followActor + '">'
followStr += ' <img loading="lazy" src="' + followProfileUrl + '"/></a>'
followStr += \
' <p class="followText">' + translate['Follow'] + ' ' + \
getNicknameFromActor(followActor) + '@' + followDomain + ' ?</p>'
followStr += ' <form method="POST" action="' + \
originPathStr + '/followconfirm">'
followStr += ' <input type="hidden" name="actor" value="' + \
followActor + '">'
followStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
followStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>'
followStr += ' </form>'
followStr += '</center>'
followStr += '</div>'
followStr += '</div>'
followStr += htmlFooter()
2019-07-29 09:49:46 +00:00
return followStr
2019-07-29 20:36:26 +00:00
2020-04-05 09:17:19 +00:00
def htmlUnfollowConfirm(translate: {}, baseDir: str,
originPathStr: str,
followActor: str,
2019-09-07 08:57:52 +00:00
followProfileUrl: str) -> str:
2019-07-29 20:36:26 +00:00
"""Asks to confirm unfollowing an actor
"""
2020-04-05 09:17:19 +00:00
followDomain, port = getDomainFromActor(followActor)
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
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')
2019-07-29 20:36:26 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
followStr += '<div class="follow">'
followStr += ' <div class="followAvatar">'
followStr += ' <center>'
followStr += ' <a href="' + followActor + '">'
followStr += ' <img loading="lazy" src="' + followProfileUrl + '"/></a>'
followStr += \
' <p class="followText">' + translate['Stop following'] + \
' ' + getNicknameFromActor(followActor) + '@' + followDomain + ' ?</p>'
followStr += ' <form method="POST" action="' + \
originPathStr + '/unfollowconfirm">'
followStr += ' <input type="hidden" name="actor" value="' + \
followActor + '">'
followStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
followStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>'
followStr += ' </form>'
followStr += '</center>'
followStr += '</div>'
followStr += '</div>'
followStr += htmlFooter()
2019-07-29 20:36:26 +00:00
return followStr
2019-07-30 22:34:04 +00:00
2020-04-05 09:17:19 +00:00
def htmlPersonOptions(translate: {}, baseDir: str,
domain: str, originPathStr: str,
optionsActor: str,
optionsProfileUrl: str,
optionsLink: str,
pageNumber: int,
donateUrl: str,
xmppAddress: str,
matrixAddress: str,
ssbAddress: str,
2020-05-04 11:28:43 +00:00
blogAddress: str,
2020-04-05 09:17:19 +00:00
toxAddress: str,
PGPpubKey: str,
emailAddress) -> str:
2019-08-24 21:10:20 +00:00
"""Show options for a person: view/follow/block/report
"""
2020-04-05 09:17:19 +00:00
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +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 21:10:20 +00:00
2020-04-05 09:17:19 +00:00
followStr = 'Follow'
blockStr = 'Block'
nickname = None
2019-08-24 23:00:03 +00:00
if originPathStr.startswith('/users/'):
2020-04-05 09:17:19 +00:00
nickname = originPathStr.split('/users/')[1]
2019-08-24 23:00:03 +00:00
if '/' in nickname:
2020-04-05 09:17:19 +00:00
nickname = nickname.split('/')[0]
2019-08-24 23:00:03 +00:00
if '?' in nickname:
2020-04-05 09:17:19 +00:00
nickname = nickname.split('?')[0]
followerDomain, followerPort = getDomainFromActor(optionsActor)
if isFollowingActor(baseDir, nickname, domain, optionsActor):
followStr = 'Unfollow'
2019-08-24 23:00:03 +00:00
2020-04-05 09:17:19 +00:00
optionsNickname = getNicknameFromActor(optionsActor)
optionsDomainFull = optionsDomain
2019-08-24 23:00:03 +00:00
if optionsPort:
2020-04-05 09:17:19 +00:00
if optionsPort != 80 and optionsPort != 443:
optionsDomainFull = optionsDomain + ':' + str(optionsPort)
if isBlocked(baseDir, nickname, domain,
optionsNickname, optionsDomainFull):
blockStr = 'Block'
2019-08-24 23:00:03 +00:00
2020-04-05 09:17:19 +00:00
optionsLinkStr = ''
2019-08-24 23:00:03 +00:00
if optionsLink:
2020-04-05 09:17:19 +00:00
optionsLinkStr = \
' <input type="hidden" name="postUrl" value="' + \
optionsLink + '">'
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
2019-11-06 11:39:41 +00:00
# To snooze, or not to snooze? That is the question
2020-04-05 09:17:19 +00:00
snoozeButtonStr = 'Snooze'
2019-11-06 11:39:41 +00:00
if nickname:
2020-04-05 09:17:19 +00:00
if isPersonSnoozed(baseDir, nickname, domain, optionsActor):
snoozeButtonStr = 'Unsnooze'
2019-11-06 11:39:41 +00:00
2020-04-05 09:17:19 +00:00
donateStr = ''
2019-11-06 23:20:00 +00:00
if donateUrl:
2020-04-05 09:17:19 +00:00
donateStr = \
' <a href="' + donateUrl + \
'"><button class="button" name="submitDonate">' + \
translate['Donate'] + '</button></a>'
optionsStr = htmlHeader(cssFilename, profileStyle)
optionsStr += '<div class="options">'
optionsStr += ' <div class="optionsAvatar">'
optionsStr += ' <center>'
optionsStr += ' <a href="' + optionsActor + '">'
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + '"/></a>'
optionsStr += \
' <p class="optionsText">' + translate['Options for'] + \
' @' + getNicknameFromActor(optionsActor) + '@' + \
optionsDomain + '</p>'
if emailAddress:
2020-04-05 09:17:19 +00:00
optionsStr += \
'<p class="imText">' + translate['Email'] + \
': <a href="mailto:' + \
emailAddress + '">' + emailAddress + '</a></p>'
2019-12-17 14:57:16 +00:00
if xmppAddress:
2020-04-05 09:17:19 +00:00
optionsStr += \
'<p class="imText">' + translate['XMPP'] + \
': <a href="xmpp:' + xmppAddress + '">' + xmppAddress + '</a></p>'
2019-12-17 15:25:34 +00:00
if matrixAddress:
2020-04-05 09:17:19 +00:00
optionsStr += \
'<p class="imText">' + translate['Matrix'] + ': ' + \
matrixAddress + '</p>'
2020-02-26 14:35:17 +00:00
if ssbAddress:
2020-04-05 09:17:19 +00:00
optionsStr += \
'<p class="imText">SSB: ' + ssbAddress + '</p>'
2020-05-04 11:28:43 +00:00
if blogAddress:
optionsStr += \
'<p class="imText">Blog: <a href="' + blogAddress + '">' + \
blogAddress + '</a></p>'
2020-03-22 14:42:26 +00:00
if toxAddress:
2020-04-05 09:17:19 +00:00
optionsStr += \
'<p class="imText">Tox: ' + toxAddress + '</p>'
if PGPpubKey:
2020-04-05 09:17:19 +00:00
optionsStr += '<p class="pgp">' + \
PGPpubKey.replace('\n', '<br>') + '</p>'
optionsStr += ' <form method="POST" action="' + \
originPathStr + '/personoptions">'
optionsStr += ' <input type="hidden" name="pageNumber" value="' + \
str(pageNumber) + '">'
optionsStr += ' <input type="hidden" name="actor" value="' + \
optionsActor + '">'
optionsStr += ' <input type="hidden" name="avatarUrl" value="' + \
optionsProfileUrl + '">'
optionsStr += optionsLinkStr
optionsStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>'
optionsStr += donateStr
optionsStr += \
' <button type="submit" class="button" name="submit' + \
followStr + '">' + translate[followStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
blockStr + '">' + translate[blockStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitDM">' + \
translate['DM'] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submit' + \
snoozeButtonStr + '">' + translate[snoozeButtonStr] + '</button>'
optionsStr += \
' <button type="submit" class="button" name="submitReport">' + \
translate['Report'] + '</button>'
optionsStr += ' </form>'
optionsStr += '</center>'
optionsStr += '</div>'
optionsStr += '</div>'
optionsStr += htmlFooter()
2019-08-24 21:10:20 +00:00
return optionsStr
2020-04-05 09:17:19 +00:00
def htmlUnblockConfirm(translate: {}, baseDir: str,
originPathStr: str,
blockActor: str,
2019-09-07 08:57:52 +00:00
blockProfileUrl: str) -> str:
"""Asks to confirm unblocking an actor
"""
2020-04-05 09:17:19 +00:00
blockDomain, port = getDomainFromActor(blockActor)
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
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')
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
blockStr = htmlHeader(cssFilename, profileStyle)
blockStr += '<div class="block">'
blockStr += ' <div class="blockAvatar">'
blockStr += ' <center>'
blockStr += ' <a href="' + blockActor + '">'
blockStr += ' <img loading="lazy" src="' + blockProfileUrl + '"/></a>'
blockStr += \
' <p class="blockText">' + translate['Stop blocking'] + ' ' + \
getNicknameFromActor(blockActor) + '@' + blockDomain + ' ?</p>'
blockStr += ' <form method="POST" action="' + \
originPathStr + '/unblockconfirm">'
blockStr += ' <input type="hidden" name="actor" value="' + \
blockActor + '">'
blockStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Yes'] + '</button>'
blockStr += \
' <a href="' + originPathStr + '"><button class="button">' + \
translate['No'] + '</button></a>'
blockStr += ' </form>'
blockStr += '</center>'
blockStr += '</div>'
blockStr += '</div>'
blockStr += htmlFooter()
return blockStr
2020-04-05 09:17:19 +00:00
def htmlSearchEmojiTextEntry(translate: {},
baseDir: str, path: str) -> str:
2019-08-19 20:01:29 +00:00
"""Search for an emoji by name
"""
2019-11-03 14:46:30 +00:00
# emoji.json is generated so that it can be customized and the changes
2020-03-22 21:16:02 +00:00
# will be retained even if default_emoji.json is subsequently updated
2020-04-05 09:17:19 +00:00
if not os.path.isfile(baseDir + '/emoji/emoji.json'):
copyfile(baseDir + '/emoji/default_emoji.json',
baseDir + '/emoji/emoji.json')
actor = path.replace('/search', '')
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')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
emojiStr = htmlHeader(cssFilename, profileStyle)
emojiStr += '<div class="follow">'
emojiStr += ' <div class="followAvatar">'
emojiStr += ' <center>'
emojiStr += \
' <p class="followText">' + \
translate['Enter an emoji name to search for'] + '</p>'
emojiStr += ' <form method="POST" action="' + \
actor + '/searchhandleemoji">'
emojiStr += ' <input type="hidden" name="actor" value="' + \
actor + '">'
emojiStr += ' <input type="text" name="searchtext" autofocus><br>'
emojiStr += \
' <button type="submit" class="button" name="submitSearch">' + \
translate['Submit'] + '</button>'
emojiStr += ' </form>'
emojiStr += ' </center>'
emojiStr += ' </div>'
emojiStr += '</div>'
emojiStr += htmlFooter()
2019-08-19 20:01:29 +00:00
return emojiStr
2020-04-05 09:17:19 +00:00
def weekDayOfMonthStart(monthNumber: int, year: int) -> int:
2019-10-10 18:25:42 +00:00
"""Gets the day number of the first day of the month
1=sun, 7=sat
"""
2020-04-05 09:17:19 +00:00
firstDayOfMonth = datetime(year, monthNumber, 1, 0, 0)
return int(firstDayOfMonth.strftime("%w")) + 1
2019-10-10 18:25:42 +00:00
2020-02-22 16:00:27 +00:00
2020-04-05 09:17:19 +00:00
def htmlCalendarDay(translate: {},
baseDir: str, path: str,
year: int, monthNumber: int, dayNumber: int,
nickname: str, domain: str, dayEvents: [],
2019-10-12 16:18:24 +00:00
monthName: str, actor: str) -> str:
2019-10-11 16:00:54 +00:00
"""Show a day within the calendar
"""
2020-04-05 09:17:19 +00:00
accountDir = baseDir + '/accounts/' + nickname + '@' + domain
calendarFile = accountDir + '/.newCalendar'
if os.path.isfile(calendarFile):
os.remove(calendarFile)
2020-02-23 10:23:12 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-calendar.css'
if os.path.isfile(baseDir + '/calendar.css'):
cssFilename = baseDir + '/calendar.css'
2019-10-11 16:00:54 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
calendarStyle = cssFile.read()
calendarStr = htmlHeader(cssFilename, calendarStyle)
calendarStr += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \
' <a href="' + actor + '/calendar?year=' + str(year) + \
'?month=' + str(monthNumber) + '">'
calendarStr += \
' <h1>' + str(dayNumber) + ' ' + monthName + \
'</h1></a><br><span class="year">' + str(year) + '</span>\n'
calendarStr += '</caption>\n'
calendarStr += '<tbody>\n'
iconsDir = getIconsDir(baseDir)
2020-02-23 11:20:02 +00:00
2020-02-23 10:23:12 +00:00
if dayEvents:
for eventPost in dayEvents:
2020-04-05 09:17:19 +00:00
eventTime = None
eventDescription = None
eventPlace = None
postId = None
2020-02-23 11:20:02 +00:00
# get the time place and description
2020-02-23 10:23:12 +00:00
for ev in eventPost:
2020-04-05 09:17:19 +00:00
if ev['type'] == 'Event':
2020-02-23 11:20:02 +00:00
if ev.get('postId'):
2020-04-05 09:17:19 +00:00
postId = ev['postId']
2020-02-23 10:23:12 +00:00
if ev.get('startTime'):
2020-04-05 09:17:19 +00:00
eventDate = \
datetime.strptime(ev['startTime'],
2020-03-22 21:16:02 +00:00
"%Y-%m-%dT%H:%M:%S%z")
2020-04-05 09:17:19 +00:00
eventTime = eventDate.strftime("%H:%M").strip()
2020-02-23 10:23:12 +00:00
if ev.get('name'):
2020-04-05 09:17:19 +00:00
eventDescription = ev['name'].strip()
elif ev['type'] == 'Place':
2020-02-23 10:23:12 +00:00
if ev.get('name'):
2020-04-05 09:17:19 +00:00
eventPlace = ev['name']
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
deleteButtonStr = ''
2020-02-23 11:20:02 +00:00
if postId:
2020-04-05 09:17:19 +00:00
deleteButtonStr = \
'<td class="calendar__day__icons"><a href="' + actor + \
'/eventdelete?id=' + postId + '?year=' + str(year) + \
'?month=' + str(monthNumber) + '?day=' + str(dayNumber) + \
'?time=' + eventTime + \
'"><img class="calendardayicon" loading="lazy" alt="' + \
translate['Delete this event'] + ' |" title="' + \
translate['Delete this event'] + ' |" src="/' + \
iconsDir + '/delete.png" /></a></td>'
2020-02-23 11:20:02 +00:00
2020-02-23 10:23:12 +00:00
if eventTime and eventDescription and eventPlace:
2020-04-05 09:17:19 +00:00
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
'<span class="place">' + \
eventPlace + '</span><br>' + eventDescription + \
'</td>' + deleteButtonStr + '</tr>\n'
2020-02-23 10:23:12 +00:00
elif eventTime and eventDescription and not eventPlace:
2020-04-05 09:17:19 +00:00
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
eventDescription + '</td>' + deleteButtonStr + '</tr>\n'
2020-02-23 10:23:12 +00:00
elif not eventTime and eventDescription and not eventPlace:
2020-04-05 09:17:19 +00:00
calendarStr += \
'<tr><td class="calendar__day__time">' + \
'</td><td class="calendar__day__event">' + \
eventDescription + '</td>' + deleteButtonStr + '</tr>\n'
2020-02-23 10:23:12 +00:00
elif not eventTime and eventDescription and eventPlace:
2020-04-05 09:17:19 +00:00
calendarStr += \
'<tr><td class="calendar__day__time"></td>' + \
'<td class="calendar__day__event"><span class="place">' + \
eventPlace + '</span><br>' + eventDescription + \
'</td>' + deleteButtonStr + '</tr>\n'
2020-02-23 10:23:12 +00:00
elif eventTime and not eventDescription and eventPlace:
2020-04-05 09:17:19 +00:00
calendarStr += \
'<tr><td class="calendar__day__time"><b>' + eventTime + \
'</b></td><td class="calendar__day__event">' + \
'<span class="place">' + \
eventPlace + '</span></td>' + \
deleteButtonStr + '</tr>\n'
2020-02-23 10:23:12 +00:00
2020-04-05 09:17:19 +00:00
calendarStr += '</tbody>\n'
calendarStr += '</table></main>\n'
calendarStr += htmlFooter()
2019-10-11 16:00:54 +00:00
return calendarStr
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
def htmlCalendar(translate: {},
baseDir: str, path: str,
httpPrefix: str, domainFull: str) -> str:
2019-10-10 14:43:21 +00:00
"""Show the calendar for a person
"""
2020-04-05 09:17:19 +00:00
iconsDir = getIconsDir(baseDir)
domain = domainFull
2019-10-11 19:25:36 +00:00
if ':' in domainFull:
2020-04-05 09:17:19 +00:00
domain = domainFull.split(':')[0]
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
monthNumber = 0
dayNumber = None
year = 1970
actor = httpPrefix + '://' + domainFull + path.replace('/calendar', '')
2019-10-10 18:25:42 +00:00
if '?' in actor:
2020-04-05 09:17:19 +00:00
first = True
2019-10-10 18:25:42 +00:00
for p in actor.split('?'):
if not first:
if '=' in p:
2020-04-05 09:17:19 +00:00
if p.split('=')[0] == 'year':
numStr = p.split('=')[1]
2019-10-10 18:25:42 +00:00
if numStr.isdigit():
2020-04-05 09:17:19 +00:00
year = int(numStr)
elif p.split('=')[0] == 'month':
numStr = p.split('=')[1]
2019-10-10 18:25:42 +00:00
if numStr.isdigit():
2020-04-05 09:17:19 +00:00
monthNumber = int(numStr)
elif p.split('=')[0] == 'day':
numStr = p.split('=')[1]
2019-10-11 16:00:54 +00:00
if numStr.isdigit():
2020-04-05 09:17:19 +00:00
dayNumber = int(numStr)
first = False
actor = actor.split('?')[0]
2019-10-10 18:25:42 +00:00
2020-04-05 09:17:19 +00:00
currDate = datetime.now()
if year == 1970 and monthNumber == 0:
year = currDate.year
monthNumber = currDate.month
2019-10-10 18:25:42 +00:00
2020-04-05 09:17:19 +00:00
nickname = getNicknameFromActor(actor)
2019-10-11 16:00:54 +00:00
2020-04-05 09:17:19 +00:00
if os.path.isfile(baseDir + '/img/calendar-background.png'):
if not os.path.isfile(baseDir + '/accounts/calendar-background.png'):
copyfile(baseDir + '/img/calendar-background.png',
baseDir + '/accounts/calendar-background.png')
2019-10-11 16:00:54 +00:00
2020-04-05 09:17:19 +00:00
months = ('January', 'February', 'March', 'April',
'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December')
monthName = translate[months[monthNumber - 1]]
2020-02-23 10:20:10 +00:00
2019-10-11 16:00:54 +00:00
if dayNumber:
2020-04-05 09:17:19 +00:00
dayEvents = None
events = \
getTodaysEvents(baseDir, nickname, domain,
year, monthNumber, dayNumber)
2020-02-23 10:23:12 +00:00
if events:
if events.get(str(dayNumber)):
2020-04-05 09:17:19 +00:00
dayEvents = events[str(dayNumber)]
return htmlCalendarDay(translate, baseDir, path,
year, monthNumber, dayNumber,
nickname, domain, dayEvents,
monthName, actor)
events = \
getCalendarEvents(baseDir, nickname, domain, year, monthNumber)
prevYear = year
prevMonthNumber = monthNumber - 1
if prevMonthNumber < 1:
prevMonthNumber = 12
prevYear = year - 1
nextYear = year
nextMonthNumber = monthNumber + 1
if nextMonthNumber > 12:
nextMonthNumber = 1
nextYear = year + 1
print('Calendar year=' + str(year) + ' month=' + str(monthNumber) +
' ' + str(weekDayOfMonthStart(monthNumber, year)))
if monthNumber < 12:
daysInMonth = \
(date(year, monthNumber + 1, 1) - date(year, monthNumber, 1)).days
2019-10-10 18:25:42 +00:00
else:
2020-04-05 09:17:19 +00:00
daysInMonth = \
(date(year + 1, 1, 1) - date(year, monthNumber, 1)).days
2019-10-10 14:43:21 +00:00
2020-04-05 09:17:19 +00:00
cssFilename = baseDir + '/epicyon-calendar.css'
if os.path.isfile(baseDir + '/calendar.css'):
cssFilename = baseDir + '/calendar.css'
2019-10-10 14:43:21 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
calendarStyle = cssFile.read()
calendarStr = htmlHeader(cssFilename, calendarStyle)
calendarStr += '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \
' <a href="' + actor + '/calendar?year=' + str(prevYear) + \
'?month=' + str(prevMonthNumber) + '">'
calendarStr += \
' <img loading="lazy" alt="' + translate['Previous month'] + \
'" title="' + translate['Previous month'] + '" src="/' + iconsDir + \
2020-02-23 14:24:11 +00:00
'/prev.png" class="buttonprev"/></a>\n'
2020-04-05 09:17:19 +00:00
calendarStr += ' <a href="' + actor + '/inbox">'
calendarStr += ' <h1>' + monthName + '</h1></a>\n'
calendarStr += \
' <a href="' + actor + '/calendar?year=' + str(nextYear) + \
'?month=' + str(nextMonthNumber) + '">'
calendarStr += \
' <img loading="lazy" alt="' + translate['Next month'] + \
'" title="' + translate['Next month'] + '" src="/' + iconsDir + \
2020-02-23 14:24:11 +00:00
'/prev.png" class="buttonnext"/></a>\n'
2020-04-05 09:17:19 +00:00
calendarStr += '</caption>\n'
calendarStr += '<thead>\n'
calendarStr += '<tr>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Sun'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Mon'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Tue'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Wed'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Thu'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Fri'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \
translate['Sat'] + '</th>\n'
calendarStr += '</tr>\n'
calendarStr += '</thead>\n'
calendarStr += '<tbody>\n'
dayOfMonth = 0
dow = weekDayOfMonthStart(monthNumber, year)
for weekOfMonth in range(1, 6):
calendarStr += ' <tr>\n'
for dayNumber in range(1, 8):
if (weekOfMonth > 1 and dayOfMonth < daysInMonth) or \
(weekOfMonth == 1 and dayNumber >= dow):
dayOfMonth += 1
isToday = False
if year == currDate.year:
if currDate.month == monthNumber:
if dayOfMonth == currDate.day:
isToday = True
2019-10-11 10:28:28 +00:00
if events.get(str(dayOfMonth)):
2020-04-05 09:17:19 +00:00
url = actor + '/calendar?year=' + str(year) + '?month=' + \
str(monthNumber) + '?day=' + str(dayOfMonth)
dayLink = '<a href="' + url + '">' + \
str(dayOfMonth) + '</a>'
2019-10-11 10:28:28 +00:00
# there are events for this day
if not isToday:
2020-04-05 09:17:19 +00:00
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-event="">' + \
dayLink + '</td>\n'
2019-10-11 10:28:28 +00:00
else:
2020-04-05 09:17:19 +00:00
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-today-event="">' + \
dayLink + '</td>\n'
2019-10-10 20:51:36 +00:00
else:
2019-10-11 10:28:28 +00:00
# No events today
if not isToday:
2020-04-05 09:17:19 +00:00
calendarStr += \
' <td class="calendar__day__cell">' + \
str(dayOfMonth) + '</td>\n'
2019-10-11 10:28:28 +00:00
else:
2020-04-05 09:17:19 +00:00
calendarStr += \
' <td class="calendar__day__cell" ' + \
'data-today="">' + str(dayOfMonth) + '</td>\n'
2019-10-10 18:25:42 +00:00
else:
2020-04-05 09:17:19 +00:00
calendarStr += ' <td class="calendar__day__cell"></td>\n'
calendarStr += ' </tr>\n'
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
calendarStr += '</tbody>\n'
calendarStr += '</table></main>\n'
calendarStr += htmlFooter()
2019-10-10 14:43:21 +00:00
return calendarStr
2020-04-05 09:17:19 +00:00
def htmlHashTagSwarm(baseDir: str, actor: str) -> str:
2019-12-13 17:57:15 +00:00
"""Returns a tag swarm of today's hashtags
2019-12-12 20:39:49 +00:00
"""
2020-04-05 09:17:19 +00:00
daysSinceEpoch = (datetime.utcnow() - datetime(1970, 1, 1)).days
daysSinceEpochStr = str(daysSinceEpoch) + ' '
tagSwarm = []
tagSwarmCtr = []
for subdir, dirs, files in os.walk(baseDir + '/tags'):
2019-12-12 20:39:49 +00:00
for f in files:
2020-04-05 09:17:19 +00:00
tagsFilename = os.path.join(baseDir + '/tags', f)
2019-12-12 20:39:49 +00:00
if not os.path.isfile(tagsFilename):
continue
2020-04-05 09:17:19 +00:00
hashTagName = f.split('.')[0]
if isBlockedHashtag(baseDir, hashTagName):
2019-12-13 09:46:46 +00:00
continue
2019-12-12 20:45:55 +00:00
if daysSinceEpochStr not in open(tagsFilename).read():
2019-12-12 20:39:49 +00:00
continue
with open(tagsFilename, 'r') as tagsFile:
line = tagsFile.readline()
lineCtr = 1
2020-04-05 09:17:19 +00:00
tagCtr = 0
while line:
if ' ' not in line:
2020-04-15 16:25:13 +00:00
line = tagsFile.readline()
lineCtr += 1
2020-04-15 16:25:55 +00:00
# don't read too many lines
if lineCtr > 4:
break
2019-12-12 20:39:49 +00:00
continue
postDaysSinceEpochStr = line.split(' ')[0]
2019-12-12 20:39:49 +00:00
if not postDaysSinceEpochStr.isdigit():
2020-04-15 16:25:13 +00:00
line = tagsFile.readline()
lineCtr += 1
2020-04-15 16:25:55 +00:00
# don't read too many lines
if lineCtr > 4:
break
2019-12-12 20:39:49 +00:00
continue
2020-04-05 09:17:19 +00:00
postDaysSinceEpoch = int(postDaysSinceEpochStr)
if postDaysSinceEpoch < daysSinceEpoch:
2019-12-12 20:39:49 +00:00
break
2020-04-05 09:17:19 +00:00
if postDaysSinceEpoch == daysSinceEpoch:
if tagCtr == 0:
2020-02-28 20:50:56 +00:00
tagSwarm.append(hashTagName)
2020-04-05 09:17:19 +00:00
tagCtr += 1
if tagCtr > 3:
2020-02-28 20:50:56 +00:00
break
line = tagsFile.readline()
lineCtr += 1
# don't read too many lines
if lineCtr > 4:
break
2020-04-05 09:17:19 +00:00
if tagCtr > 0:
2020-02-28 20:53:51 +00:00
tagSwarmCtr.append(tagCtr)
2019-12-13 17:57:15 +00:00
if not tagSwarm:
2019-12-12 20:39:49 +00:00
return ''
2019-12-13 17:57:15 +00:00
tagSwarm.sort()
2020-04-05 09:17:19 +00:00
tagSwarmStr = ''
ctr = 0
2019-12-13 17:57:15 +00:00
for tagName in tagSwarm:
2020-04-05 09:17:19 +00:00
if tagSwarmCtr[ctr] < 4:
tagSwarmStr += \
'<a href="' + actor + '/tags/' + tagName + \
'" class="hashtagswarm">' + tagName + '</a> '
2020-02-28 20:50:56 +00:00
else:
2020-04-05 09:17:19 +00:00
tagSwarmStr += \
'<a href="' + actor + '/tags/' + tagName + \
'" class="hashtagswarm2">' + tagName + '</a> '
ctr += 1
tagSwarmHtml = tagSwarmStr.strip() + '\n'
2019-12-13 17:57:15 +00:00
return tagSwarmHtml
2019-12-12 20:39:49 +00:00
2020-04-05 09:17:19 +00:00
def htmlSearch(translate: {},
baseDir: str, path: str) -> str:
2019-07-30 22:34:04 +00:00
"""Search called from the timeline icon
"""
2020-04-05 09:17:19 +00:00
actor = path.replace('/search', '')
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')
cssFilename = baseDir + '/epicyon-follow.css'
if os.path.isfile(baseDir + '/follow.css'):
cssFilename = baseDir + '/follow.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read()
followStr = htmlHeader(cssFilename, profileStyle)
followStr += '<div class="follow">'
followStr += ' <div class="followAvatar">'
followStr += ' <center>'
2020-04-11 12:37:20 +00:00
idx = 'Enter an address, shared item, !history, #hashtag, ' + \
2020-04-05 09:17:19 +00:00
'*skill or :emoji: to search for'
followStr += \
' <p class="followText">' + translate[idx] + '</p>'
followStr += ' <form method="POST" ' + \
'accept-charset="UTF-8" action="' + actor + '/searchhandle">'
followStr += ' <input type="hidden" name="actor" value="' + actor + '">'
followStr += ' <input type="text" name="searchtext" autofocus><br>'
followStr += ' <button type="submit" class="button" ' + \
'name="submitSearch">' + translate['Submit'] + '</button>'
followStr += ' <button type="submit" class="button" ' + \
'name="submitBack">' + translate['Go Back'] + '</button>'
followStr += ' </form>'
followStr += ' <p class="hashtagswarm">' + \
htmlHashTagSwarm(baseDir, actor) + '</p>'
followStr += ' </center>'
followStr += ' </div>'
followStr += '</div>'
followStr += htmlFooter()
2019-07-30 22:34:04 +00:00
return followStr
2020-04-05 09:17:19 +00:00
def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
translate: {},
baseDir: str, path: str, httpPrefix: str,
nickname: str, domain: str, port: int,
profileHandle: str,
2020-05-07 15:15:44 +00:00
session, cachedWebfingers: {}, personCache: {},
2020-04-05 09:17:19 +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-10-17 22:35:56 +00:00
if '/users/' in profileHandle or \
'/channel/' in profileHandle or \
'/profile/' in profileHandle or \
'/@' in profileHandle:
2020-04-05 09:17:19 +00:00
searchNickname = getNicknameFromActor(profileHandle)
searchDomain, searchPort = getDomainFromActor(profileHandle)
2019-07-30 22:34:04 +00:00
else:
if '@' not in profileHandle:
2020-05-07 14:45:54 +00:00
print('DEBUG: no @ in ' + profileHandle)
2019-07-30 22:34:04 +00:00
return None
if profileHandle.startswith('@'):
2020-04-05 09:17:19 +00:00
profileHandle = profileHandle[1:]
2019-07-30 22:34:04 +00:00
if '@' not in profileHandle:
2020-05-07 14:45:54 +00:00
print('DEBUG: no @ in ' + profileHandle)
2019-07-30 22:34:04 +00:00
return None
2020-04-05 09:17:19 +00:00
searchNickname = profileHandle.split('@')[0]
searchDomain = profileHandle.split('@')[1]
searchPort = None
2019-07-30 22:34:04 +00:00
if ':' in searchDomain:
2020-04-05 09:17:19 +00:00
searchPortStr = searchDomain.split(':')[1]
2020-03-01 10:18:08 +00:00
if searchPortStr.isdigit():
2020-04-05 09:17:19 +00:00
searchPort = int(searchPortStr)
searchDomain = searchDomain.split(':')[0]
2020-05-07 15:55:25 +00:00
print('DEBUG: Search for handle ' +
str(searchNickname) + '@' + str(searchDomain) + ':' +
str(searchPort))
2019-07-30 22:34:04 +00:00
if not searchNickname:
2020-05-07 14:45:54 +00:00
print('DEBUG: No nickname found in ' + profileHandle)
2019-07-30 22:34:04 +00:00
return None
if not searchDomain:
2020-05-07 14:45:54 +00:00
print('DEBUG: No domain found in ' + profileHandle)
2019-07-30 22:34:04 +00:00
return None
2020-05-07 14:45:54 +00:00
2020-04-05 09:17:19 +00:00
searchDomainFull = searchDomain
2019-07-30 22:34:04 +00:00
if searchPort:
2020-04-05 09:17:19 +00:00
if searchPort != 80 and searchPort != 443:
if ':' not in searchDomain:
2020-04-05 09:17:19 +00:00
searchDomainFull = searchDomain + ':' + str(searchPort)
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
profileStr = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2019-09-11 09:58:43 +00:00
with open(cssFilename, 'r') as cssFile:
2020-04-05 09:17:19 +00:00
wf = \
webfingerHandle(session,
searchNickname + '@' + searchDomainFull,
2020-05-07 15:15:44 +00:00
httpPrefix, cachedWebfingers,
2020-05-07 15:49:48 +00:00
domain, projectVersion)
2019-07-30 22:34:04 +00:00
if not wf:
print('DEBUG: Unable to webfinger ' +
searchNickname + '@' + searchDomainFull)
2020-05-07 15:15:44 +00:00
print('DEBUG: cachedWebfingers ' + str(cachedWebfingers))
2020-05-07 14:45:54 +00:00
print('DEBUG: httpPrefix ' + httpPrefix)
print('DEBUG: domain ' + domain)
2019-07-30 22:34:04 +00:00
return None
2020-05-07 14:45:54 +00:00
2020-04-05 09:17:19 +00:00
personUrl = None
2019-10-17 15:18:43 +00:00
if wf.get('errors'):
2020-04-05 09:17:19 +00:00
personUrl = httpPrefix + '://' + \
searchDomainFull + '/users/' + searchNickname
2020-03-22 21:16:02 +00:00
2020-04-05 09:17:19 +00:00
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
2020-02-23 15:32:47 +00:00
}
2019-10-17 15:18:43 +00:00
if not personUrl:
2020-04-05 09:17:19 +00:00
personUrl = getUserUrl(wf)
if not personUrl:
2019-10-21 09:57:42 +00:00
# try single user instance
2020-04-05 09:17:19 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2020-04-05 09:17:19 +00:00
personUrl = httpPrefix + '://' + searchDomainFull
profileJson = \
getJson(session, personUrl, asHeader, None,
projectVersion, httpPrefix, domain)
2019-10-17 22:35:56 +00:00
if not profileJson:
2020-04-05 09:17:19 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-02-23 15:32:47 +00:00
}
2020-04-05 09:17:19 +00:00
profileJson = \
getJson(session, personUrl, asHeader, None,
projectVersion, httpPrefix, domain)
2019-07-30 22:34:04 +00:00
if not profileJson:
print('DEBUG: No actor returned from ' + personUrl)
2019-07-30 22:34:04 +00:00
return None
2020-04-05 09:17:19 +00:00
avatarUrl = ''
2019-07-30 22:34:04 +00:00
if profileJson.get('icon'):
if profileJson['icon'].get('url'):
2020-04-05 09:17:19 +00:00
avatarUrl = profileJson['icon']['url']
2019-08-18 13:30:40 +00:00
if not avatarUrl:
2020-04-05 09:17:19 +00:00
avatarUrl = getPersonAvatarUrl(baseDir, personUrl, personCache)
displayName = searchNickname
2019-08-22 18:37:22 +00:00
if profileJson.get('name'):
2020-04-05 09:17:19 +00:00
displayName = profileJson['name']
profileDescription = ''
2019-07-31 12:44:08 +00:00
if profileJson.get('summary'):
2020-04-05 09:17:19 +00:00
profileDescription = profileJson['summary']
outboxUrl = None
2019-07-30 22:34:04 +00:00
if not profileJson.get('outbox'):
if debug:
pprint(profileJson)
print('DEBUG: No outbox found')
return None
2020-04-05 09:17:19 +00:00
outboxUrl = profileJson['outbox']
profileBackgroundImage = ''
2019-07-30 22:34:04 +00:00
if profileJson.get('image'):
if profileJson['image'].get('url'):
2020-04-05 09:17:19 +00:00
profileBackgroundImage = profileJson['image']['url']
2019-07-30 22:34:04 +00:00
2020-04-05 09:17:19 +00:00
profileStyle = cssFile.read().replace('image.png',
profileBackgroundImage)
if httpPrefix != 'https':
profileStyle = profileStyle.replace('https://',
httpPrefix + '://')
# url to return to
2020-04-05 09:17:19 +00:00
backUrl = path
if not backUrl.endswith('/inbox'):
2020-04-05 09:17:19 +00:00
backUrl += '/inbox'
2020-04-05 09:17:19 +00:00
profileDescriptionShort = profileDescription
2019-10-23 14:27:43 +00:00
if '\n' in profileDescription:
2020-04-05 09:17:19 +00:00
if len(profileDescription.split('\n')) > 2:
profileDescriptionShort = ''
2019-10-23 14:27:43 +00:00
else:
if '<br>' in profileDescription:
2020-04-05 09:17:19 +00:00
if len(profileDescription.split('<br>')) > 2:
profileDescriptionShort = ''
2019-10-23 15:09:20 +00:00
# keep the profile description short
2020-04-05 09:17:19 +00:00
if len(profileDescriptionShort) > 256:
profileDescriptionShort = ''
2019-10-23 15:09:20 +00:00
# remove formatting from profile description used on title
2020-04-05 09:17:19 +00:00
avatarDescription = ''
if profileJson.get('summary'):
2020-04-05 09:17:19 +00:00
avatarDescription = profileJson['summary'].replace('<br>', '\n')
avatarDescription = avatarDescription.replace('<p>', '')
avatarDescription = avatarDescription.replace('</p>', '')
profileStr = ' <div class="hero-image">'
profileStr += ' <div class="hero-text">'
profileStr += \
' <img loading="lazy" src="' + avatarUrl + \
'" alt="' + avatarDescription + '" title="' + \
avatarDescription + '">'
profileStr += ' <h1>' + displayName + '</h1>'
profileStr += ' <p><b>@' + searchNickname + '@' + \
searchDomainFull + '</b></p>'
profileStr += ' <p>' + profileDescriptionShort + '</p>'
profileStr += ' </div>'
profileStr += '</div>'
profileStr += '<div class="container">\n'
profileStr += ' <form method="POST" action="' + \
backUrl + '/followconfirm">'
profileStr += ' <center>'
profileStr += \
' <input type="hidden" name="actor" value="' + \
personUrl + '">'
profileStr += \
' <button type="submit" class="button" name="submitYes">' + \
translate['Follow'] + '</button>'
profileStr += \
' <button type="submit" class="button" name="submitView">' + \
translate['View'] + '</button>'
profileStr += \
' <a href="' + backUrl + '"><button class="button">' + \
translate['Go Back'] + '</button></a>'
profileStr += ' </center>'
profileStr += ' </form>'
profileStr += '</div>'
profileStr += '<script>' + contentWarningScript() + '</script>'
iconsDir = getIconsDir(baseDir)
i = 0
for item in parseUserFeed(session, outboxUrl, asHeader,
projectVersion, httpPrefix, domain):
2019-07-30 22:34:04 +00:00
if not item.get('type'):
continue
2020-04-05 09:17:19 +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
2020-04-05 09:17:19 +00:00
profileStr += \
individualPostAsHtml(recentPostsCache, maxRecentPosts,
iconsDir, translate, None, baseDir,
2020-05-07 15:15:44 +00:00
session, cachedWebfingers, personCache,
2020-04-05 09:17:19 +00:00
nickname, domain, port,
item, avatarUrl, False, False,
httpPrefix, projectVersion, 'inbox',
False, False, False, False, False)
i += 1
if i >= 20:
2019-07-30 22:34:04 +00:00
break
2020-04-05 09:17:19 +00:00
return htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter()