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') postBytesStr = postBytes.decode('utf-8')
redirectPath = '' redirectPath = ''
checkNameAndBio = False checkNameAndBio = False
onFinalWelcomeScreen = False
if 'name="previewAvatar"' in postBytesStr: if 'name="previewAvatar"' in postBytesStr:
redirectPath = '/welcome_profile' redirectPath = '/welcome_profile'
elif 'name="initialWelcomeScreen"' in postBytesStr: elif 'name="initialWelcomeScreen"' in postBytesStr:
@ -4061,6 +4062,7 @@ class PubServer(BaseHTTPRequestHandler):
redirectPath = '/' + self.server.defaultTimeline redirectPath = '/' + self.server.defaultTimeline
welcomeScreenIsComplete(self.server.baseDir, nickname, welcomeScreenIsComplete(self.server.baseDir, nickname,
self.server.domain) self.server.domain)
onFinalWelcomeScreen = True
# extract all of the text fields into a dict # extract all of the text fields into a dict
fields = \ fields = \
@ -4717,15 +4719,20 @@ class PubServer(BaseHTTPRequestHandler):
nickname, domain) nickname, domain)
# approve followers # approve followers
approveFollowers = False if onFinalWelcomeScreen:
if fields.get('approveFollowers'): # Default setting created via the welcome screen
if fields['approveFollowers'] == 'on': actorJson['manuallyApprovesFollowers'] = True
approveFollowers = True
if approveFollowers != \
actorJson['manuallyApprovesFollowers']:
actorJson['manuallyApprovesFollowers'] = \
approveFollowers
actorChanged = 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 # remove a custom font
if fields.get('removeCustomFont'): if fields.get('removeCustomFont'):
@ -4773,15 +4780,22 @@ class PubServer(BaseHTTPRequestHandler):
baseDir + '/accounts/' + \ baseDir + '/accounts/' + \
nickname + '@' + domain + \ nickname + '@' + domain + \
'/.followDMs' '/.followDMs'
followDMsActive = False if onFinalWelcomeScreen:
if fields.get('followDMs'): # initial default setting created via
if fields['followDMs'] == 'on': # the welcome screen
followDMsActive = True with open(followDMsFilename, 'w+') as fFile:
with open(followDMsFilename, 'w+') as fFile: fFile.write('\n')
fFile.write('\n') actorChanged = True
if not followDMsActive: else:
if os.path.isfile(followDMsFilename): followDMsActive = False
os.remove(followDMsFilename) 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 # remove Twitter retweets
removeTwitterFilename = \ removeTwitterFilename = \
@ -4822,16 +4836,22 @@ class PubServer(BaseHTTPRequestHandler):
os.remove(hideLikeButtonFile) os.remove(hideLikeButtonFile)
# notify about new Likes # notify about new Likes
notifyLikesActive = False if onFinalWelcomeScreen:
if fields.get('notifyLikes'): # default setting from welcome screen
if fields['notifyLikes'] == 'on' and \ with open(notifyLikesFilename, 'w+') as rFile:
not hideLikeButtonActive: rFile.write('\n')
notifyLikesActive = True actorChanged = True
with open(notifyLikesFilename, 'w+') as rFile: else:
rFile.write('\n') notifyLikesActive = False
if not notifyLikesActive: if fields.get('notifyLikes'):
if os.path.isfile(notifyLikesFilename): if fields['notifyLikes'] == 'on' and \
os.remove(notifyLikesFilename) 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 # this account is a bot
if fields.get('isBot'): if fields.get('isBot'):
@ -5751,6 +5771,52 @@ class PubServer(BaseHTTPRequestHandler):
return return
self._404() 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, def _showCachedAvatar(self, callingDomain: str, path: str,
baseDir: str, baseDir: str,
GETstartTime, GETtimings: {}) -> None: GETstartTime, GETtimings: {}) -> None:
@ -9669,7 +9735,7 @@ class PubServer(BaseHTTPRequestHandler):
""" """
imageExtensions = getImageExtensions() imageExtensions = getImageExtensions()
for ext in imageExtensions: for ext in imageExtensions:
for bg in ('follow', 'options', 'login'): for bg in ('follow', 'options', 'login', 'welcome'):
# follow screen background image # follow screen background image
if path.endswith('/' + bg + '-background.' + ext): if path.endswith('/' + bg + '-background.' + ext):
bgFilename = \ bgFilename = \
@ -9714,41 +9780,45 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {}) -> bool: GETstartTime, GETtimings: {}) -> bool:
"""Show a shared item image """Show a shared item image
""" """
if self._pathIsImage(path): if not self._pathIsImage(path):
mediaStr = path.split('/sharefiles/')[1] self._404()
mediaFilename = \ return True
baseDir + '/sharefiles/' + mediaStr
if os.path.isfile(mediaFilename):
if self._etag_exists(mediaFilename):
# The file has not changed
self._304()
return True
mediaFileType = 'png' mediaStr = path.split('/sharefiles/')[1]
if mediaFilename.endswith('.png'): mediaFilename = \
mediaFileType = 'png' baseDir + '/sharefiles/' + mediaStr
elif mediaFilename.endswith('.jpg'): if not os.path.isfile(mediaFilename):
mediaFileType = 'jpeg' self._404()
elif mediaFilename.endswith('.webp'): return True
mediaFileType = 'webp'
elif mediaFilename.endswith('.avif'): if self._etag_exists(mediaFilename):
mediaFileType = 'avif' # The file has not changed
elif mediaFilename.endswith('.svg'): self._304()
mediaFileType = 'svg+xml' return True
else:
mediaFileType = 'gif' mediaFileType = 'png'
with open(mediaFilename, 'rb') as avFile: if mediaFilename.endswith('.png'):
mediaBinary = avFile.read() mediaFileType = 'png'
self._set_headers_etag(mediaFilename, elif mediaFilename.endswith('.jpg'):
'image/' + mediaFileType, mediaFileType = 'jpeg'
mediaBinary, None, elif mediaFilename.endswith('.webp'):
self.server.domainFull) mediaFileType = 'webp'
self._write(mediaBinary) elif mediaFilename.endswith('.avif'):
self._benchmarkGETtimings(GETstartTime, GETtimings, mediaFileType = 'avif'
'show media done', elif mediaFilename.endswith('.svg'):
'share files shown') mediaFileType = 'svg+xml'
return True else:
self._404() 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 return True
def _showAvatarOrBanner(self, callingDomain: str, path: str, def _showAvatarOrBanner(self, callingDomain: str, path: str,
@ -9756,59 +9826,62 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {}) -> bool: GETstartTime, GETtimings: {}) -> bool:
"""Shows an avatar or banner or profile background image """Shows an avatar or banner or profile background image
""" """
if '/users/' in path: if '/users/' not in path:
if self._pathIsImage(path): return False
avatarStr = path.split('/users/')[1] if not self._pathIsImage(path):
if '/' in avatarStr and '.temp.' not in path: return False
avatarNickname = avatarStr.split('/')[0] avatarStr = path.split('/users/')[1]
avatarFile = avatarStr.split('/')[1] if not ('/' in avatarStr and '.temp.' not in path):
avatarFileExt = avatarFile.split('.')[-1] return False
# remove any numbers, eg. avatar123.png becomes avatar.png avatarNickname = avatarStr.split('/')[0]
if avatarFile.startswith('avatar'): avatarFile = avatarStr.split('/')[1]
avatarFile = 'avatar.' + avatarFileExt avatarFileExt = avatarFile.split('.')[-1]
elif avatarFile.startswith('banner'): # remove any numbers, eg. avatar123.png becomes avatar.png
avatarFile = 'banner.' + avatarFileExt if avatarFile.startswith('avatar'):
elif avatarFile.startswith('search_banner'): avatarFile = 'avatar.' + avatarFileExt
avatarFile = 'search_banner.' + avatarFileExt elif avatarFile.startswith('banner'):
elif avatarFile.startswith('image'): avatarFile = 'banner.' + avatarFileExt
avatarFile = 'image.' + avatarFileExt elif avatarFile.startswith('search_banner'):
elif avatarFile.startswith('left_col_image'): avatarFile = 'search_banner.' + avatarFileExt
avatarFile = 'left_col_image.' + avatarFileExt elif avatarFile.startswith('image'):
elif avatarFile.startswith('right_col_image'): avatarFile = 'image.' + avatarFileExt
avatarFile = 'right_col_image.' + avatarFileExt elif avatarFile.startswith('left_col_image'):
avatarFilename = \ avatarFile = 'left_col_image.' + avatarFileExt
baseDir + '/accounts/' + \ elif avatarFile.startswith('right_col_image'):
avatarNickname + '@' + domain + '/' + avatarFile avatarFile = 'right_col_image.' + avatarFileExt
if os.path.isfile(avatarFilename): avatarFilename = \
if self._etag_exists(avatarFilename): baseDir + '/accounts/' + \
# The file has not changed avatarNickname + '@' + domain + '/' + avatarFile
self._304() if not os.path.isfile(avatarFilename):
return True return False
mediaImageType = 'png' if self._etag_exists(avatarFilename):
if avatarFile.endswith('.png'): # The file has not changed
mediaImageType = 'png' self._304()
elif avatarFile.endswith('.jpg'): return True
mediaImageType = 'jpeg' mediaImageType = 'png'
elif avatarFile.endswith('.gif'): if avatarFile.endswith('.png'):
mediaImageType = 'gif' mediaImageType = 'png'
elif avatarFile.endswith('.avif'): elif avatarFile.endswith('.jpg'):
mediaImageType = 'avif' mediaImageType = 'jpeg'
elif avatarFile.endswith('.svg'): elif avatarFile.endswith('.gif'):
mediaImageType = 'svg+xml' mediaImageType = 'gif'
else: elif avatarFile.endswith('.avif'):
mediaImageType = 'webp' mediaImageType = 'avif'
with open(avatarFilename, 'rb') as avFile: elif avatarFile.endswith('.svg'):
mediaBinary = avFile.read() mediaImageType = 'svg+xml'
self._set_headers_etag(avatarFilename, else:
'image/' + mediaImageType, mediaImageType = 'webp'
mediaBinary, None, with open(avatarFilename, 'rb') as avFile:
self.server.domainFull) mediaBinary = avFile.read()
self._write(mediaBinary) self._set_headers_etag(avatarFilename,
self._benchmarkGETtimings(GETstartTime, GETtimings, 'image/' + mediaImageType,
'icon shown done', mediaBinary, None,
'avatar background shown') self.server.domainFull)
return True self._write(mediaBinary)
return False self._benchmarkGETtimings(GETstartTime, GETtimings,
'icon shown done',
'avatar background shown')
return True
def _confirmDeleteEvent(self, callingDomain: str, path: str, def _confirmDeleteEvent(self, callingDomain: str, path: str,
baseDir: str, httpPrefix: str, cookie: str, baseDir: str, httpPrefix: str, cookie: str,
@ -11042,6 +11115,14 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings) GETstartTime, GETtimings)
return 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, self._benchmarkGETtimings(GETstartTime, GETtimings,
'show files done', 'show files done',
'icon shown 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 ### مرحبًا بكم في INSTANCE
هذا خادم ActivityPub مصمم للاستضافة الذاتية السهلة لعدد قليل من الأشخاص على أنظمة منخفضة الطاقة ، مثل أجهزة الكمبيوتر ذات اللوحة الواحدة أو أجهزة الكمبيوتر المحمولة القديمة. هذا خادم ActivityPub مصمم للاستضافة الذاتية السهلة لعدد قليل من الأشخاص على أنظمة منخفضة الطاقة ، مثل أجهزة الكمبيوتر ذات اللوحة الواحدة أو أجهزة الكمبيوتر المحمولة القديمة.

View File

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

View File

@ -1,3 +1,4 @@
![Immagine di benvenuto](/helpimages/welcome.jpg)
### Benvenuto in INSTANCE ### 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. 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へようこそ ### INSTANCEへようこそ
これは、シングルボードコンピューターや古いラップトップなどの低電力システムで数人を簡単にセルフホスティングするために設計されたActivityPubサーバーです。 これは、シングルボードコンピューターや古いラップトップなどの低電力システムで数人を簡単にセルフホスティングするために設計された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. 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. 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. 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. 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 ### Добро пожаловать в INSTANCE
Это сервер ActivityPub, предназначенный для простого самостоятельного размещения нескольких человек в системах с низким энергопотреблением, таких как одноплатные компьютеры или старые ноутбуки. Это сервер ActivityPub, предназначенный для простого самостоятельного размещения нескольких человек в системах с низким энергопотреблением, таких как одноплатные компьютеры или старые ноутбуки.

View File

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

View File

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

View File

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

View File

@ -3288,6 +3288,18 @@ def testMarkdownToHtml():
markdown = 'This is just plain text' markdown = 'This is just plain text'
assert markdownToHtml(markdown) == markdown 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**' markdown = 'This is **bold**'
assert markdownToHtml(markdown) == 'This is <b>bold</b>' assert markdownToHtml(markdown) == 'This is <b>bold</b>'
@ -3306,14 +3318,16 @@ def testMarkdownToHtml():
markdown = \ markdown = \
'This is [a link](https://something.somewhere) to something.\n' + \ '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) == \ assert markdownToHtml(markdown) == \
'This is <a href="https://something.somewhere" ' + \ 'This is <a href="https://something.somewhere" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \
'a link</a> to something.<br>' + \ 'a link</a> to something.<br>' + \
'And <a href="https://cat.pic" ' + \ 'And <a href="https://cat.pic" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \
'something else</a>.' 'something else</a>.<br>' + \
'Or <img class="markdownImage" src="/cat.jpg" alt="pounce" />.'
def runAllTests(): 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', 'accounts', 'channels', 'profile', 'u',
'updates', 'repeat', 'announce', 'updates', 'repeat', 'announce',
'shares', 'fonts', 'icons', 'avatars', 'shares', 'fonts', 'icons', 'avatars',
'welcome') 'welcome', 'helpimages')
if nickname in reservedNames: if nickname in reservedNames:
return True return True
return False return False

View File

@ -8,12 +8,15 @@ __status__ = "Production"
import os import os
import time import time
from shutil import copyfile
from utils import dangerousMarkup
from utils import getConfigParam from utils import getConfigParam
from utils import getFullDomain from utils import getFullDomain
from utils import isEditor from utils import isEditor
from utils import removeIdEnding from utils import removeIdEnding
from follow import followerApprovalActive from follow import followerApprovalActive
from person import isPersonSnoozed from person import isPersonSnoozed
from webapp_utils import markdownToHtml
from webapp_utils import htmlKeyboardNavigation from webapp_utils import htmlKeyboardNavigation
from webapp_utils import htmlHideFromScreenReader from webapp_utils import htmlHideFromScreenReader
from webapp_utils import htmlPostSeparator from webapp_utils import htmlPostSeparator
@ -42,6 +45,42 @@ def _logTimelineTiming(enableTimingLog: bool, timelineStartTime,
boxName + ' ' + debugId + ' = ' + str(timeDiff)) 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, def htmlTimeline(cssCache: {}, defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int, recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, translate: {}, pageNumber: int,
@ -698,6 +737,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
translate['Page down'] + '"></a>\n' + \ translate['Page down'] + '"></a>\n' + \
' </center>\n' ' </center>\n'
tlStr += textModeSeparator tlStr += textModeSeparator
elif itemCtr == 0:
tlStr += _getHelpForTimeline(baseDir, boxName)
# end of timeline-posts # end of timeline-posts
tlStr += ' </div>\n' tlStr += ' </div>\n'
@ -788,6 +829,7 @@ def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
' </center>\n' ' </center>\n'
separatorStr = htmlPostSeparator(baseDir, None) separatorStr = htmlPostSeparator(baseDir, None)
ctr = 0
for published, item in sharesJson.items(): for published, item in sharesJson.items():
showContactButton = False showContactButton = False
if item['actor'] != actor: if item['actor'] != actor:
@ -799,6 +841,10 @@ def _htmlSharesTimeline(translate: {}, pageNumber: int, itemsPerPage: int,
htmlIndividualShare(actor, item, translate, htmlIndividualShare(actor, item, translate,
showContactButton, showRemoveButton) showContactButton, showRemoveButton)
timelineStr += separatorStr timelineStr += separatorStr
ctr += 1
if ctr == 0:
timelineStr += _getHelpForTimeline(baseDir, 'tlshares')
if not lastPage: if not lastPage:
timelineStr += \ timelineStr += \

View File

@ -66,31 +66,88 @@ def _markdownEmphasisHtml(markdown: str) -> str:
return markdown return markdown
def markdownToHtml(markdown: str) -> str: def _markdownReplaceQuotes(markdown: str) -> str:
"""Converts markdown formatted text to html """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 = {} replaceLinks = {}
text = markdown text = markdown
while '[' in text: startChars = '['
if images:
startChars = '!['
while startChars in text:
if ')' not in text: if ')' not in text:
break break
text = text.split('[', 1)[1] text = text.split(startChars, 1)[1]
markdownLink = '[' + text.split(')')[0] + ')' markdownLink = startChars + text.split(')')[0] + ')'
if ']' not in markdownLink or \ if ']' not in markdownLink or \
'(' not in markdownLink: '(' not in markdownLink:
text = text.split(')', 1)[1] text = text.split(')', 1)[1]
continue continue
replaceLinks[markdownLink] = \ if not images:
'<a href="' + \ replaceLinks[markdownLink] = \
markdownLink.split('(')[1].split(')')[0] + \ '<a href="' + \
'" target="_blank" rel="nofollow noopener noreferrer">' + \ markdownLink.split('(')[1].split(')')[0] + \
markdownLink.split('[')[1].split(']')[0] + \ '" target="_blank" rel="nofollow noopener noreferrer">' + \
'</a>' 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] text = text.split(')', 1)[1]
for mdLink, htmlLink in replaceLinks.items(): for mdLink, htmlLink in replaceLinks.items():
markdown = markdown.replace(mdLink, htmlLink) 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 # replace headers
linesList = markdown.split('\n') linesList = markdown.split('\n')