main
Bob Mottram 2021-02-26 23:37:26 +00:00
commit 256c05ad69
39 changed files with 387 additions and 135 deletions

311
daemon.py
View File

@ -4050,6 +4050,7 @@ class PubServer(BaseHTTPRequestHandler):
postBytesStr = postBytes.decode('utf-8')
redirectPath = ''
checkNameAndBio = False
onFinalWelcomeScreen = False
if 'name="previewAvatar"' in postBytesStr:
redirectPath = '/welcome_profile'
elif 'name="initialWelcomeScreen"' in postBytesStr:
@ -4061,6 +4062,7 @@ class PubServer(BaseHTTPRequestHandler):
redirectPath = '/' + self.server.defaultTimeline
welcomeScreenIsComplete(self.server.baseDir, nickname,
self.server.domain)
onFinalWelcomeScreen = True
# extract all of the text fields into a dict
fields = \
@ -4717,15 +4719,20 @@ class PubServer(BaseHTTPRequestHandler):
nickname, domain)
# approve followers
approveFollowers = False
if fields.get('approveFollowers'):
if fields['approveFollowers'] == 'on':
approveFollowers = True
if approveFollowers != \
actorJson['manuallyApprovesFollowers']:
actorJson['manuallyApprovesFollowers'] = \
approveFollowers
if onFinalWelcomeScreen:
# Default setting created via the welcome screen
actorJson['manuallyApprovesFollowers'] = True
actorChanged = True
else:
approveFollowers = False
if fields.get('approveFollowers'):
if fields['approveFollowers'] == 'on':
approveFollowers = True
if approveFollowers != \
actorJson['manuallyApprovesFollowers']:
actorJson['manuallyApprovesFollowers'] = \
approveFollowers
actorChanged = True
# remove a custom font
if fields.get('removeCustomFont'):
@ -4773,15 +4780,22 @@ class PubServer(BaseHTTPRequestHandler):
baseDir + '/accounts/' + \
nickname + '@' + domain + \
'/.followDMs'
followDMsActive = False
if fields.get('followDMs'):
if fields['followDMs'] == 'on':
followDMsActive = True
with open(followDMsFilename, 'w+') as fFile:
fFile.write('\n')
if not followDMsActive:
if os.path.isfile(followDMsFilename):
os.remove(followDMsFilename)
if onFinalWelcomeScreen:
# initial default setting created via
# the welcome screen
with open(followDMsFilename, 'w+') as fFile:
fFile.write('\n')
actorChanged = True
else:
followDMsActive = False
if fields.get('followDMs'):
if fields['followDMs'] == 'on':
followDMsActive = True
with open(followDMsFilename, 'w+') as fFile:
fFile.write('\n')
if not followDMsActive:
if os.path.isfile(followDMsFilename):
os.remove(followDMsFilename)
# remove Twitter retweets
removeTwitterFilename = \
@ -4822,16 +4836,22 @@ class PubServer(BaseHTTPRequestHandler):
os.remove(hideLikeButtonFile)
# notify about new Likes
notifyLikesActive = False
if fields.get('notifyLikes'):
if fields['notifyLikes'] == 'on' and \
not hideLikeButtonActive:
notifyLikesActive = True
with open(notifyLikesFilename, 'w+') as rFile:
rFile.write('\n')
if not notifyLikesActive:
if os.path.isfile(notifyLikesFilename):
os.remove(notifyLikesFilename)
if onFinalWelcomeScreen:
# default setting from welcome screen
with open(notifyLikesFilename, 'w+') as rFile:
rFile.write('\n')
actorChanged = True
else:
notifyLikesActive = False
if fields.get('notifyLikes'):
if fields['notifyLikes'] == 'on' and \
not hideLikeButtonActive:
notifyLikesActive = True
with open(notifyLikesFilename, 'w+') as rFile:
rFile.write('\n')
if not notifyLikesActive:
if os.path.isfile(notifyLikesFilename):
os.remove(notifyLikesFilename)
# this account is a bot
if fields.get('isBot'):
@ -5751,6 +5771,52 @@ class PubServer(BaseHTTPRequestHandler):
return
self._404()
def _showHelpScreenImage(self, callingDomain: str, path: str,
baseDir: str,
GETstartTime, GETtimings: {}) -> None:
"""Shows a help screen image
"""
if not path.endswith('.jpg') and \
not path.endswith('.png') and \
not path.endswith('.webp') and \
not path.endswith('.avif') and \
not path.endswith('.gif'):
return
mediaStr = path.split('/helpimages/')[1]
if '/' not in mediaStr:
if not self.server.themeName:
theme = 'default'
else:
theme = self.server.themeName
iconFilename = mediaStr
else:
theme = mediaStr.split('/')[0]
iconFilename = mediaStr.split('/')[1]
mediaFilename = \
baseDir + '/theme/' + theme + '/helpimages/' + iconFilename
# if there is no theme-specific help image then use the default one
if not os.path.isfile(mediaFilename):
mediaFilename = \
baseDir + '/theme/default/helpimages/' + iconFilename
if self._etag_exists(mediaFilename):
# The file has not changed
self._304()
return
if os.path.isfile(mediaFilename):
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
mimeType = mediaFileMimeType(mediaFilename)
self._set_headers_etag(mediaFilename,
mimeType,
mediaBinary, None,
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show files done',
'help image shown')
return
self._404()
def _showCachedAvatar(self, callingDomain: str, path: str,
baseDir: str,
GETstartTime, GETtimings: {}) -> None:
@ -9669,7 +9735,7 @@ class PubServer(BaseHTTPRequestHandler):
"""
imageExtensions = getImageExtensions()
for ext in imageExtensions:
for bg in ('follow', 'options', 'login'):
for bg in ('follow', 'options', 'login', 'welcome'):
# follow screen background image
if path.endswith('/' + bg + '-background.' + ext):
bgFilename = \
@ -9714,41 +9780,45 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {}) -> bool:
"""Show a shared item image
"""
if self._pathIsImage(path):
mediaStr = path.split('/sharefiles/')[1]
mediaFilename = \
baseDir + '/sharefiles/' + mediaStr
if os.path.isfile(mediaFilename):
if self._etag_exists(mediaFilename):
# The file has not changed
self._304()
return True
if not self._pathIsImage(path):
self._404()
return True
mediaFileType = 'png'
if mediaFilename.endswith('.png'):
mediaFileType = 'png'
elif mediaFilename.endswith('.jpg'):
mediaFileType = 'jpeg'
elif mediaFilename.endswith('.webp'):
mediaFileType = 'webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'avif'
elif mediaFilename.endswith('.svg'):
mediaFileType = 'svg+xml'
else:
mediaFileType = 'gif'
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers_etag(mediaFilename,
'image/' + mediaFileType,
mediaBinary, None,
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show media done',
'share files shown')
return True
self._404()
mediaStr = path.split('/sharefiles/')[1]
mediaFilename = \
baseDir + '/sharefiles/' + mediaStr
if not os.path.isfile(mediaFilename):
self._404()
return True
if self._etag_exists(mediaFilename):
# The file has not changed
self._304()
return True
mediaFileType = 'png'
if mediaFilename.endswith('.png'):
mediaFileType = 'png'
elif mediaFilename.endswith('.jpg'):
mediaFileType = 'jpeg'
elif mediaFilename.endswith('.webp'):
mediaFileType = 'webp'
elif mediaFilename.endswith('.avif'):
mediaFileType = 'avif'
elif mediaFilename.endswith('.svg'):
mediaFileType = 'svg+xml'
else:
mediaFileType = 'gif'
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers_etag(mediaFilename,
'image/' + mediaFileType,
mediaBinary, None,
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show media done',
'share files shown')
return True
def _showAvatarOrBanner(self, callingDomain: str, path: str,
@ -9756,59 +9826,62 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {}) -> bool:
"""Shows an avatar or banner or profile background image
"""
if '/users/' in path:
if self._pathIsImage(path):
avatarStr = path.split('/users/')[1]
if '/' in avatarStr and '.temp.' not in path:
avatarNickname = avatarStr.split('/')[0]
avatarFile = avatarStr.split('/')[1]
avatarFileExt = avatarFile.split('.')[-1]
# remove any numbers, eg. avatar123.png becomes avatar.png
if avatarFile.startswith('avatar'):
avatarFile = 'avatar.' + avatarFileExt
elif avatarFile.startswith('banner'):
avatarFile = 'banner.' + avatarFileExt
elif avatarFile.startswith('search_banner'):
avatarFile = 'search_banner.' + avatarFileExt
elif avatarFile.startswith('image'):
avatarFile = 'image.' + avatarFileExt
elif avatarFile.startswith('left_col_image'):
avatarFile = 'left_col_image.' + avatarFileExt
elif avatarFile.startswith('right_col_image'):
avatarFile = 'right_col_image.' + avatarFileExt
avatarFilename = \
baseDir + '/accounts/' + \
avatarNickname + '@' + domain + '/' + avatarFile
if os.path.isfile(avatarFilename):
if self._etag_exists(avatarFilename):
# The file has not changed
self._304()
return True
mediaImageType = 'png'
if avatarFile.endswith('.png'):
mediaImageType = 'png'
elif avatarFile.endswith('.jpg'):
mediaImageType = 'jpeg'
elif avatarFile.endswith('.gif'):
mediaImageType = 'gif'
elif avatarFile.endswith('.avif'):
mediaImageType = 'avif'
elif avatarFile.endswith('.svg'):
mediaImageType = 'svg+xml'
else:
mediaImageType = 'webp'
with open(avatarFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers_etag(avatarFilename,
'image/' + mediaImageType,
mediaBinary, None,
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
'avatar background shown')
return True
return False
if '/users/' not in path:
return False
if not self._pathIsImage(path):
return False
avatarStr = path.split('/users/')[1]
if not ('/' in avatarStr and '.temp.' not in path):
return False
avatarNickname = avatarStr.split('/')[0]
avatarFile = avatarStr.split('/')[1]
avatarFileExt = avatarFile.split('.')[-1]
# remove any numbers, eg. avatar123.png becomes avatar.png
if avatarFile.startswith('avatar'):
avatarFile = 'avatar.' + avatarFileExt
elif avatarFile.startswith('banner'):
avatarFile = 'banner.' + avatarFileExt
elif avatarFile.startswith('search_banner'):
avatarFile = 'search_banner.' + avatarFileExt
elif avatarFile.startswith('image'):
avatarFile = 'image.' + avatarFileExt
elif avatarFile.startswith('left_col_image'):
avatarFile = 'left_col_image.' + avatarFileExt
elif avatarFile.startswith('right_col_image'):
avatarFile = 'right_col_image.' + avatarFileExt
avatarFilename = \
baseDir + '/accounts/' + \
avatarNickname + '@' + domain + '/' + avatarFile
if not os.path.isfile(avatarFilename):
return False
if self._etag_exists(avatarFilename):
# The file has not changed
self._304()
return True
mediaImageType = 'png'
if avatarFile.endswith('.png'):
mediaImageType = 'png'
elif avatarFile.endswith('.jpg'):
mediaImageType = 'jpeg'
elif avatarFile.endswith('.gif'):
mediaImageType = 'gif'
elif avatarFile.endswith('.avif'):
mediaImageType = 'avif'
elif avatarFile.endswith('.svg'):
mediaImageType = 'svg+xml'
else:
mediaImageType = 'webp'
with open(avatarFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers_etag(avatarFilename,
'image/' + mediaImageType,
mediaBinary, None,
self.server.domainFull)
self._write(mediaBinary)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
'avatar background shown')
return True
def _confirmDeleteEvent(self, callingDomain: str, path: str,
baseDir: str, httpPrefix: str, cookie: str,
@ -11042,6 +11115,14 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings)
return
# help screen images
# Note that this comes before the busy flag to avoid conflicts
if self.path.startswith('/helpimages/'):
self._showHelpScreenImage(callingDomain, self.path,
self.server.baseDir,
GETstartTime, GETtimings)
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show files done',
'icon shown done')

View File

@ -0,0 +1,3 @@
Direct messages will appear here, as a chronological timeline.
To avoid spam and improve security, by default you will only be able to receive direct messages *from people that you're following*. You can turn this off within your profile settings if you need to, by selecting the top **banner** and then the **edit** icon.

View File

@ -0,0 +1,19 @@
Incoming posts will appear here, as a chronological timeline. If you send any posts they will also appear here.
### The top banner
At the top of the screen you can select the **banner** to switch to your profile, and edit it or log out.
### Timeline buttons and icons
The **buttons** below the top banner allow you to select different timelines. There are also **icons** on the right to **search**, view your **calendar** or create **new posts**.
The **show/hide** icon allows more timeline buttons to be shown, along with moderator controls.
### Left column
Here you can add **useful links**. This only appears on desktop displays or devices with larger screens. It is similar to a *blogroll*. You can only add or edit links if you have an **administrator** or **editor** role.
If you are on mobile then use the **links icon** at the top to read news.
### Right column
RSS feeds can be added in the right column, known as the *newswire*. This only appears on desktop displays or devices with larger screens. You can only add or edit feeds if you have an **administrator** or **editor** role, and incoming feed items can also be moderated.
If you are on mobile then use the **newswire icon** at the top to read news.

View File

@ -0,0 +1 @@
Your sent posts will appear here, as a cronological timeline.

View File

@ -0,0 +1 @@
Any bookmarked posts appear here.

View File

@ -0,0 +1 @@
Any incoming posts which contain **images**, **video** or **audio** files will appear here, together with their descriptions.

View File

@ -0,0 +1,6 @@
### Shared items
These are typically physical objects or local services, exchanged or given away without use of money.
For example, you may want to share **equipment** between members of a sports team on the same instance, share any surplus **clothing**, share **gadgets** which you are no longer using, or share plants and gardening **tools** between people using the same growing space.
To avoid spam, shared items are not federated via ActivityPub and are local to members on the same instance.

View File

@ -1,3 +1,4 @@
![مرحبا الصورة](/helpimages/welcome.jpg)
### مرحبًا بكم في INSTANCE
هذا خادم ActivityPub مصمم للاستضافة الذاتية السهلة لعدد قليل من الأشخاص على أنظمة منخفضة الطاقة ، مثل أجهزة الكمبيوتر ذات اللوحة الواحدة أو أجهزة الكمبيوتر المحمولة القديمة.

View File

@ -1,3 +1,4 @@
![Imatge de benvinguda](/helpimages/welcome.jpg)
### Benvingut a INSTANCE
Es tracta dun servidor ActivityPub dissenyat per allotjar fàcilment algunes persones en sistemes de poca potència, com ara ordinadors de placa única o portàtils antics.

View File

@ -1,3 +1,4 @@
![Delwedd groeso](/helpimages/welcome.jpg)
### Croeso i INSTANCE
Gweinydd ActivityPub yw hwn sydd wedi'i gynllunio ar gyfer hunan-letya ychydig o bobl ar systemau pŵer isel yn hawdd, fel cyfrifiaduron bwrdd sengl neu hen gliniaduron.

View File

@ -1,3 +1,4 @@
![Willkommensbild](/helpimages/welcome.jpg)
### Willkommen bei INSTANCE
Dies ist ein ActivityPub-Server, der für das einfache Selbsthosting einiger weniger Personen auf Systemen mit geringem Stromverbrauch wie Single-Board-Computern oder alten Laptops entwickelt wurde.

View File

@ -1,3 +1,4 @@
![Welcome image](/helpimages/welcome.jpg)
### Welcome to INSTANCE
This is an ActivityPub server designed for easy self-hosting of a few people on low power systems, such as single board computers or old laptops.

View File

@ -1,3 +1,4 @@
![Imagen de bienvenida](/helpimages/welcome.jpg)
### Bienvenido a INSTANCE
Este es un servidor ActivityPub diseñado para el autohospedaje sencillo de algunas personas en sistemas de bajo consumo de energía, como computadoras de placa única o laptops antiguas.

View File

@ -1,3 +1,4 @@
![Image de bienvenue](/helpimages/welcome.jpg)
### Bienvenue à INSTANCE
Il s'agit d'un serveur ActivityPub conçu pour l'auto-hébergement facile de quelques personnes sur des systèmes à faible consommation d'énergie, tels que des ordinateurs monocarte ou d'anciens ordinateurs portables.

View File

@ -1,3 +1,4 @@
![Íomhá fáilte](/helpimages/welcome.jpg)
### Fáilte go INSTANCE
Is freastalaí ActivityPub é seo atá deartha chun féin-óstáil éasca a dhéanamh ar chúpla duine ar chórais ísealchumhachta, mar ríomhairí boird aonair nó sean ríomhairí glúine.

View File

@ -1,3 +1,4 @@
![स्वागत है छवि](/helpimages/welcome.jpg)
### INSTANCE पर आपका स्वागत है
यह एक एक्टिविटीपब सर्वर है जो कम पावर सिस्टम पर सिंगल बोर्ड कंप्यूटर या पुराने लैपटॉप जैसे कुछ लोगों की आसान सेल्फ-होस्टिंग के लिए बनाया गया है।

View File

@ -1,3 +1,4 @@
![Immagine di benvenuto](/helpimages/welcome.jpg)
### Benvenuto in INSTANCE
Questo è un server ActivityPub progettato per un facile self-hosting di poche persone su sistemi a basso consumo, come computer a scheda singola o vecchi laptop.

View File

@ -1,3 +1,4 @@
![ウェルカムイメージ](/helpimages/welcome.jpg)
### INSTANCEへようこそ
これは、シングルボードコンピューターや古いラップトップなどの低電力システムで数人を簡単にセルフホスティングするために設計されたActivityPubサーバーです。

View File

@ -1,4 +1,5 @@
# Welcome
![Welcome image](/helpimages/welcome.jpg)
### Welcome
Epicyon is an ActivityPub server designed for easy self-hosting of a few people on low power systems, such as single board computers or old laptops.
Run your own social network presence the way you want to, and say goodbye to Big Tech.

View File

@ -1,4 +1,5 @@
# Bem-vindo a INSTANCE
![Imagem de boas-vindas](/helpimages/welcome.jpg)
### Bem-vindo a INSTANCE
Este é um servidor ActivityPub projetado para fácil auto-hospedagem de algumas pessoas em sistemas de baixo consumo de energia, como computadores de placa única ou laptops antigos.
Administre sua própria presença na rede social do jeito que você quiser e diga adeus à Big Tech.

View File

@ -1,3 +1,4 @@
![Приветственное изображение](/helpimages/welcome.jpg)
### Добро пожаловать в INSTANCE
Это сервер ActivityPub, предназначенный для простого самостоятельного размещения нескольких человек в системах с низким энергопотреблением, таких как одноплатные компьютеры или старые ноутбуки.

View File

@ -1,3 +1,4 @@
![欢迎图片](/helpimages/welcome.jpg)
### 欢迎来到INSTANCE
这是一个ActivityPub服务器设计用于在低功耗系统例如单板计算机或旧笔记本电脑上轻松实现一些人的自我托管。

View File

@ -426,6 +426,10 @@ a:focus {
background-color: var(--timeline-posts-background-color);
}
.container img.markdownImage {
width: 100%;
}
.container img.timelineicon:hover {
filter: brightness(var(--icon-brightness-change));
}

View File

@ -149,6 +149,10 @@ img.avatar {
width: var(--welcome-avatar-width);
}
.container img.markdownImage {
width: 100%;
}
.container.next {
float: right;
}
@ -192,7 +196,7 @@ span.psw {
margin: 8px 0;
border: none;
cursor: pointer;
width: var(--welcome-button-width);
width: var(--welcome-button-width);
font-size: var(--welcome-font-size);
font-family: Arial, Helvetica, sans-serif;
}
@ -232,7 +236,7 @@ span.psw {
margin: 8px 0;
border: none;
cursor: pointer;
width: var(--welcome-button-width);
width: var(--welcome-button-width);
font-size: var(--welcome-font-size-mobile);
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -3288,6 +3288,18 @@ def testMarkdownToHtml():
markdown = 'This is just plain text'
assert markdownToHtml(markdown) == markdown
markdown = 'This is a quotation:\n' + \
'> Some quote or other'
assert markdownToHtml(markdown) == 'This is a quotation:<br>' + \
'<blockquote><i>Some quote or other</i></blockquote>'
markdown = 'This is a multi-line quotation:\n' + \
'> The first line\n' + \
'> The second line'
assert markdownToHtml(markdown) == \
'This is a multi-line quotation:<br>' + \
'<blockquote><i>The first line The second line</i></blockquote>'
markdown = 'This is **bold**'
assert markdownToHtml(markdown) == 'This is <b>bold</b>'
@ -3306,14 +3318,16 @@ def testMarkdownToHtml():
markdown = \
'This is [a link](https://something.somewhere) to something.\n' + \
'And [something else](https://cat.pic).'
'And [something else](https://cat.pic).\n' + \
'Or ![pounce](/cat.jpg).'
assert markdownToHtml(markdown) == \
'This is <a href="https://something.somewhere" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
'a link</a> to something.<br>' + \
'And <a href="https://cat.pic" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
'something else</a>.'
'something else</a>.<br>' + \
'Or <img class="markdownImage" src="/cat.jpg" alt="pounce" />.'
def runAllTests():

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1274,7 +1274,7 @@ def _isReservedName(nickname: str) -> bool:
'accounts', 'channels', 'profile', 'u',
'updates', 'repeat', 'announce',
'shares', 'fonts', 'icons', 'avatars',
'welcome')
'welcome', 'helpimages')
if nickname in reservedNames:
return True
return False

View File

@ -8,12 +8,15 @@ __status__ = "Production"
import os
import time
from shutil import copyfile
from utils import dangerousMarkup
from utils import getConfigParam
from utils import getFullDomain
from utils import isEditor
from utils import removeIdEnding
from follow import followerApprovalActive
from person import isPersonSnoozed
from webapp_utils import markdownToHtml
from webapp_utils import htmlKeyboardNavigation
from webapp_utils import htmlHideFromScreenReader
from webapp_utils import htmlPostSeparator
@ -42,6 +45,42 @@ def _logTimelineTiming(enableTimingLog: bool, timelineStartTime,
boxName + ' ' + debugId + ' = ' + str(timeDiff))
def _getHelpForTimeline(baseDir: str, boxName: str) -> str:
"""Shows help text for the given timeline
"""
# get the filename for help for this timeline
helpFilename = baseDir + '/accounts/help_' + boxName + '.md'
if not os.path.isfile(helpFilename):
language = \
getConfigParam(baseDir, 'language')
if not language:
language = 'en'
defaultFilename = \
baseDir + '/defaultwelcome/' + \
'help_' + boxName + '_' + language + '.md'
if not os.path.isfile(defaultFilename):
defaultFilename = \
baseDir + '/defaultwelcome/help_' + boxName + '_en.md'
if os.path.isfile(defaultFilename):
copyfile(defaultFilename, helpFilename)
# show help text
if os.path.isfile(helpFilename):
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
if not instanceTitle:
instanceTitle = 'Epicyon'
with open(helpFilename, 'r') as helpFile:
helpText = helpFile.read()
if dangerousMarkup(helpText, False):
return ''
helpText = helpText.replace('INSTANCE', instanceTitle)
return '<div class="container">\n' + \
markdownToHtml(helpText) + '\n' + \
'</div>\n'
return ''
def htmlTimeline(cssCache: {}, defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int,
@ -698,6 +737,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
translate['Page down'] + '"></a>\n' + \
' </center>\n'
tlStr += textModeSeparator
elif itemCtr == 0:
tlStr += _getHelpForTimeline(baseDir, boxName)
# end of timeline-posts
tlStr += ' </div>\n'
@ -788,6 +829,7 @@ def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
' </center>\n'
separatorStr = htmlPostSeparator(baseDir, None)
ctr = 0
for published, item in sharesJson.items():
showContactButton = False
if item['actor'] != actor:
@ -799,6 +841,10 @@ def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
htmlIndividualShare(actor, item, translate,
showContactButton, showRemoveButton)
timelineStr += separatorStr
ctr += 1
if ctr == 0:
timelineStr += _getHelpForTimeline(baseDir, 'tlshares')
if not lastPage:
timelineStr += \

View File

@ -66,31 +66,88 @@ def _markdownEmphasisHtml(markdown: str) -> str:
return markdown
def markdownToHtml(markdown: str) -> str:
"""Converts markdown formatted text to html
def _markdownReplaceQuotes(markdown: str) -> str:
"""Replaces > quotes with html blockquote
"""
if '> ' not in markdown:
return markdown
lines = markdown.split('\n')
result = ''
prevQuoteLine = None
for line in lines:
if '> ' not in line:
result += line + '\n'
prevQuoteLine = None
continue
lineStr = line.strip()
if not lineStr.startswith('> '):
result += line + '\n'
prevQuoteLine = None
continue
lineStr = lineStr.replace('> ', '', 1).strip()
if prevQuoteLine:
newPrevLine = prevQuoteLine.replace('</i></blockquote>\n', '')
result = result.replace(prevQuoteLine, newPrevLine) + ' '
lineStr += '</i></blockquote>\n'
else:
lineStr = '<blockquote><i>' + lineStr + '</i></blockquote>\n'
result += lineStr
prevQuoteLine = lineStr
if '</blockquote>\n' in result:
result = result.replace('</blockquote>\n', '</blockquote>')
if result.endswith('\n') and \
not markdown.endswith('\n'):
result = result[:len(result) - 1]
return result
def _markdownReplaceLinks(markdown: str, images=False) -> str:
"""Replaces markdown links with html
Optionally replace image links
"""
markdown = _markdownEmphasisHtml(markdown)
# replace markdown style links with html links
replaceLinks = {}
text = markdown
while '[' in text:
startChars = '['
if images:
startChars = '!['
while startChars in text:
if ')' not in text:
break
text = text.split('[', 1)[1]
markdownLink = '[' + text.split(')')[0] + ')'
text = text.split(startChars, 1)[1]
markdownLink = startChars + text.split(')')[0] + ')'
if ']' not in markdownLink or \
'(' not in markdownLink:
text = text.split(')', 1)[1]
continue
replaceLinks[markdownLink] = \
'<a href="' + \
markdownLink.split('(')[1].split(')')[0] + \
'" target="_blank" rel="nofollow noopener noreferrer">' + \
markdownLink.split('[')[1].split(']')[0] + \
'</a>'
if not images:
replaceLinks[markdownLink] = \
'<a href="' + \
markdownLink.split('(')[1].split(')')[0] + \
'" target="_blank" rel="nofollow noopener noreferrer">' + \
markdownLink.split(startChars)[1].split(']')[0] + \
'</a>'
else:
replaceLinks[markdownLink] = \
'<img class="markdownImage" src="' + \
markdownLink.split('(')[1].split(')')[0] + \
'" alt="' + \
markdownLink.split(startChars)[1].split(']')[0] + \
'" />'
text = text.split(')', 1)[1]
for mdLink, htmlLink in replaceLinks.items():
markdown = markdown.replace(mdLink, htmlLink)
return markdown
def markdownToHtml(markdown: str) -> str:
"""Converts markdown formatted text to html
"""
markdown = _markdownReplaceQuotes(markdown)
markdown = _markdownEmphasisHtml(markdown)
markdown = _markdownReplaceLinks(markdown, True)
markdown = _markdownReplaceLinks(markdown)
# replace headers
linesList = markdown.split('\n')