Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
1
Makefile
|
|
@ -23,3 +23,4 @@ clean:
|
|||
rm -f deploy/*~
|
||||
rm -f translations/*~
|
||||
rm -rf __pycache__
|
||||
rm calendar.css blog.css epicyon.css follow.css login.css options.css search.css suspended.css
|
||||
|
|
|
|||
37
content.py
|
|
@ -14,6 +14,41 @@ from utils import fileLastModified
|
|||
from utils import getLinkPrefixes
|
||||
|
||||
|
||||
def htmlReplaceEmailQuote(content: str) -> str:
|
||||
"""Replaces an email style quote "> Some quote" with html blockquote
|
||||
"""
|
||||
# replace quote paragraph
|
||||
if '<p>"' in content:
|
||||
if '"</p>' in content:
|
||||
content = content.replace('<p>"', '<p><blockquote>')
|
||||
content = content.replace('"</p>', '</blockquote></p>')
|
||||
if '>\u201c' in content:
|
||||
if '\u201d<' in content:
|
||||
content = content.replace('>\u201c', '><blockquote>')
|
||||
content = content.replace('\u201d<', '</blockquote><')
|
||||
# replace email style quote
|
||||
if '>> ' not in content:
|
||||
return content
|
||||
contentStr = content.replace('<p>', '')
|
||||
contentLines = contentStr.split('</p>')
|
||||
newContent = ''
|
||||
for lineStr in contentLines:
|
||||
if not lineStr:
|
||||
continue
|
||||
if '>> ' not in lineStr:
|
||||
if lineStr.startswith('> '):
|
||||
lineStr = lineStr.replace('> ', '<blockquote>')
|
||||
lineStr = lineStr.replace('>', '<br>')
|
||||
newContent += '<p>' + lineStr + '</blockquote></p>'
|
||||
else:
|
||||
newContent += '<p>' + lineStr + '</p>'
|
||||
else:
|
||||
lineStr = lineStr.replace('>> ', '><blockquote>')
|
||||
lineStr = lineStr.replace('>', '<br>')
|
||||
newContent += '<p>' + lineStr + '</blockquote></p>'
|
||||
return newContent
|
||||
|
||||
|
||||
def htmlReplaceQuoteMarks(content: str) -> str:
|
||||
"""Replaces quotes with html formatting
|
||||
"hello" becomes <q>hello</q>
|
||||
|
|
@ -612,6 +647,7 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
|||
by matching against known following accounts
|
||||
"""
|
||||
if content.startswith('<p>'):
|
||||
content = htmlReplaceEmailQuote(content)
|
||||
return htmlReplaceQuoteMarks(content)
|
||||
maxWordLength = 40
|
||||
content = content.replace('\r', '')
|
||||
|
|
@ -718,6 +754,7 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
|||
if longWordsList:
|
||||
content = removeLongWords(content, maxWordLength, longWordsList)
|
||||
content = content.replace(' --linebreak-- ', '</p><p>')
|
||||
content = htmlReplaceEmailQuote(content)
|
||||
return '<p>' + htmlReplaceQuoteMarks(content) + '</p>'
|
||||
|
||||
|
||||
|
|
|
|||
24
daemon.py
|
|
@ -3351,6 +3351,30 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if os.path.isfile(switchFilename):
|
||||
os.remove(switchFilename)
|
||||
|
||||
# autogenerated tags
|
||||
autoTagsFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + \
|
||||
'/autotags.txt'
|
||||
if fields.get('autoTags'):
|
||||
with open(autoTagsFilename, 'w+') as autoTagsFile:
|
||||
autoTagsFile.write(fields['autoTags'])
|
||||
else:
|
||||
if os.path.isfile(autoTagsFilename):
|
||||
os.remove(autoTagsFilename)
|
||||
|
||||
# autogenerated content warnings
|
||||
autoCWFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + \
|
||||
'/autocw.txt'
|
||||
if fields.get('autoCW'):
|
||||
with open(autoCWFilename, 'w+') as autoCWFile:
|
||||
autoCWFile.write(fields['autoCW'])
|
||||
else:
|
||||
if os.path.isfile(autoCWFilename):
|
||||
os.remove(autoCWFilename)
|
||||
|
||||
# save blocked accounts list
|
||||
blockedFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
|
|
|
|||
|
|
@ -767,5 +767,6 @@
|
|||
"parrot": "1F99C",
|
||||
"budgie": "1F424",
|
||||
"canary": "1F424",
|
||||
"linux": "1F427"
|
||||
"linux": "1F427",
|
||||
"valid": "valid"
|
||||
}
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -43,8 +43,16 @@
|
|||
--button-corner-radius: 15px;
|
||||
--timeline-border-radius: 30px;
|
||||
--focus-color: white;
|
||||
--line-spacing: 130%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
@ -63,6 +71,7 @@ body, html {
|
|||
min-width: 950px;
|
||||
margin: 0 auto;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
|
||||
a, u {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@
|
|||
--focus-color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@
|
|||
--focus-color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@
|
|||
--button-selected: #666;
|
||||
--form-border-radius: 30px;
|
||||
--focus-color: white;
|
||||
--line-spacing: 130%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
@ -46,6 +54,7 @@ body, html {
|
|||
min-width: 600px;
|
||||
margin: 0 auto;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
|
||||
a, u {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@
|
|||
--focus-color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
@ -153,6 +160,19 @@ a:focus {
|
|||
cursor: pointer;
|
||||
margin: 30px;
|
||||
}
|
||||
.buttonIcon {
|
||||
border-radius: 4px;
|
||||
background-color: var(--button-background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
border: none;
|
||||
color: var(--button-text);
|
||||
text-align: center;
|
||||
padding: 10px 65px;
|
||||
font-size: 24px;
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.buttonsmall {
|
||||
border-radius: 4px;
|
||||
background-color: var(--button-small-background);
|
||||
|
|
@ -215,6 +235,19 @@ a:focus {
|
|||
cursor: pointer;
|
||||
margin: 30px;
|
||||
}
|
||||
.buttonIcon {
|
||||
border-radius: 4px;
|
||||
background-color: var(--button-background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
border: none;
|
||||
color: var(--button-text);
|
||||
text-align: center;
|
||||
padding: 6px 80px;
|
||||
font-size: 40px;
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.buttonsmall {
|
||||
border-radius: 4px;
|
||||
background-color: var(--button-small-background);
|
||||
|
|
|
|||
|
|
@ -57,8 +57,19 @@
|
|||
--icons-side: right;
|
||||
--title-color: #999;
|
||||
--focus-color: white;
|
||||
--quote-right-margin: 0.1em;
|
||||
--quote-font-weight: normal;
|
||||
--quote-font-size: 120%;
|
||||
--line-spacing: 130%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
@ -77,6 +88,34 @@ body, html {
|
|||
min-width: 950px;
|
||||
margin: 0 auto;
|
||||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 10px;
|
||||
margin: 1.5em 10px;
|
||||
padding: 0.5em 10px;
|
||||
font-weight: var(--quote-font-weight);
|
||||
font-style: italic;
|
||||
font-size: var(--quote-font-size);
|
||||
quotes: "\201C""\201D""\2018""\2019";
|
||||
}
|
||||
blockquote:before {
|
||||
content: open-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-right: 0.25em;
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
blockquote:after {
|
||||
content: close-quote;
|
||||
font-size: 2em;
|
||||
line-height: 0.1em;
|
||||
margin-left: var(--quote-right-margin);
|
||||
vertical-align: -0.4em;
|
||||
}
|
||||
blockquote p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.imageAnchor:focus img{
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@
|
|||
--focus-color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@
|
|||
--focus-color: white;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
font-display: block;
|
||||
src: url('./fonts/bedstead.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Bedstead';
|
||||
font-style: normal;
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ def clearFollows(baseDir: str, nickname: str, domain: str,
|
|||
followFile='following.txt') -> None:
|
||||
"""Removes all follows
|
||||
"""
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
handle = nickname + '@' + domain
|
||||
if not os.path.isdir(baseDir + '/accounts'):
|
||||
os.mkdir(baseDir + '/accounts')
|
||||
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
||||
|
|
@ -269,7 +269,7 @@ def getNoOfFollows(baseDir: str, nickname: str, domain: str,
|
|||
# account holders
|
||||
# if not authenticated:
|
||||
# return 9999
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
handle = nickname + '@' + domain
|
||||
filename = baseDir + '/accounts/' + handle + '/' + followFile
|
||||
if not os.path.isfile(filename):
|
||||
return 0
|
||||
|
|
@ -378,7 +378,7 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
|
|||
handleDomain = domain
|
||||
if ':' in handleDomain:
|
||||
handleDomain = domain.split(':')[0]
|
||||
handle = nickname.lower() + '@' + handleDomain.lower()
|
||||
handle = nickname + '@' + handleDomain
|
||||
filename = baseDir + '/accounts/' + handle + '/' + followFile + '.txt'
|
||||
if not os.path.isfile(filename):
|
||||
return following
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Epicyon is an AGPL licensed ActivityPub protocol compliant federated social netw
|
|||
|
||||
## An Internet of People, Not Corporate Agendas
|
||||
|
||||
Epicyon is written in Python with a HTML+CSS web interface and uses no javascript which makes display in a web browser very lightweight.
|
||||
Epicyon is written in Python with a HTML+CSS web interface and uses no javascript which makes display in a web browser very lightweight. It can run as a Progressive Web App on mobile. Just say "no" to boring social media sites packed with generic adverts and zombified corporate influencers.
|
||||
|
||||
Emojis, hashtags, photos, video and audio attachments, instance and account level blocking controls, moderation functions and reports are all supported. Build the community you want and avoid the stuff you don't. No ads. No blockchains or other Silicon Valley garbage.
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ Enable the site:
|
|||
|
||||
Forward port 443 from your internet router to your server. If you have dynamic DNS make sure its configured. Add a TLS certificate:
|
||||
|
||||
certbot certonly -n --server https://acme-v01.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
|
||||
certbot certonly -n --server https://acme-v02.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
|
||||
|
||||
Restart your web server:
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 112 KiB |
12
person.py
|
|
@ -73,7 +73,7 @@ def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str,
|
|||
if ':' not in domain:
|
||||
fullDomain = domain + ':' + str(port)
|
||||
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
handle = nickname + '@' + domain
|
||||
personFilename = baseDir + '/accounts/' + handle + '.json'
|
||||
if not os.path.isfile(personFilename):
|
||||
print('person definition not found: ' + personFilename)
|
||||
|
|
@ -210,7 +210,7 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
storeWebfingerEndpoint(nickname, domain, port,
|
||||
baseDir, webfingerEndpoint)
|
||||
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
handle = nickname + '@' + domain
|
||||
originalDomain = domain
|
||||
if port:
|
||||
if port != 80 and port != 443:
|
||||
|
|
@ -736,8 +736,8 @@ def setDisplayNickname(baseDir: str, nickname: str, domain: str,
|
|||
displayName: str) -> bool:
|
||||
if len(displayName) > 32:
|
||||
return False
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
filename = baseDir + '/accounts/' + handle.lower() + '.json'
|
||||
handle = nickname + '@' + domain
|
||||
filename = baseDir + '/accounts/' + handle + '.json'
|
||||
if not os.path.isfile(filename):
|
||||
return False
|
||||
|
||||
|
|
@ -752,8 +752,8 @@ def setDisplayNickname(baseDir: str, nickname: str, domain: str,
|
|||
def setBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool:
|
||||
if len(bio) > 32:
|
||||
return False
|
||||
handle = nickname.lower() + '@' + domain.lower()
|
||||
filename = baseDir + '/accounts/' + handle.lower() + '.json'
|
||||
handle = nickname + '@' + domain
|
||||
filename = baseDir + '/accounts/' + handle + '.json'
|
||||
if not os.path.isfile(filename):
|
||||
return False
|
||||
|
||||
|
|
|
|||
37
posts.py
|
|
@ -668,6 +668,41 @@ def validContentWarning(cw: str) -> str:
|
|||
return cw
|
||||
|
||||
|
||||
def loadAutoCW(baseDir: str, nickname: str, domain: str) -> []:
|
||||
"""Loads automatic CWs file and returns a list containing
|
||||
the lines of the file
|
||||
"""
|
||||
filename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/autocw.txt'
|
||||
if not os.path.isfile(filename):
|
||||
return []
|
||||
with open(filename, "r") as f:
|
||||
return f.readlines()
|
||||
return []
|
||||
|
||||
|
||||
def addAutoCW(baseDir: str, nickname: str, domain: str,
|
||||
subject: str, content: str) -> str:
|
||||
"""Appends any automatic CW to the subject line
|
||||
and returns the new subject line
|
||||
"""
|
||||
newSubject = subject
|
||||
autoCWList = loadAutoCW(baseDir, nickname, domain)
|
||||
for cwRule in autoCWList:
|
||||
if '->' not in cwRule:
|
||||
continue
|
||||
match = cwRule.split('->')[0].strip()
|
||||
if match not in content:
|
||||
continue
|
||||
cwStr = cwRule.split('->')[1].strip()
|
||||
if newSubject:
|
||||
if cwStr not in newSubject:
|
||||
newSubject += ', ' + cwStr
|
||||
else:
|
||||
newSubject = cwStr
|
||||
return newSubject
|
||||
|
||||
|
||||
def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||
toUrl: str, ccUrl: str, httpPrefix: str, content: str,
|
||||
followersOnly: bool, saveToFile: bool, clientToServer: bool,
|
||||
|
|
@ -687,6 +722,8 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
eventStatus=None, ticketUrl=None) -> {}:
|
||||
"""Creates a message
|
||||
"""
|
||||
subject = addAutoCW(baseDir, nickname, domain, subject, content)
|
||||
|
||||
mentionedRecipients = \
|
||||
getMentionedPeople(baseDir, httpPrefix, content, domain, False)
|
||||
|
||||
|
|
|
|||
41
tests.py
|
|
@ -68,6 +68,7 @@ from delete import sendDeleteViaServer
|
|||
from inbox import jsonPostAllowsComments
|
||||
from inbox import validInbox
|
||||
from inbox import validInboxFilenames
|
||||
from content import htmlReplaceEmailQuote
|
||||
from content import htmlReplaceQuoteMarks
|
||||
from content import dangerousMarkup
|
||||
from content import removeHtml
|
||||
|
|
@ -2124,8 +2125,48 @@ def testConstantTimeStringCheck():
|
|||
assert timeDiffMicroseconds < 10
|
||||
|
||||
|
||||
def testReplaceEmailQuote():
|
||||
print('testReplaceEmailQuote')
|
||||
testStr = '<p>This content has no quote.</p>'
|
||||
assert htmlReplaceEmailQuote(testStr) == testStr
|
||||
|
||||
testStr = '<p>This content has no quote.</p>' + \
|
||||
'<p>With multiple</p><p>lines</p>'
|
||||
assert htmlReplaceEmailQuote(testStr) == testStr
|
||||
|
||||
testStr = '<p>"This is a quoted paragraph."</p>'
|
||||
assert htmlReplaceEmailQuote(testStr) == \
|
||||
'<p><blockquote>This is a quoted paragraph.</blockquote></p>'
|
||||
|
||||
testStr = "<p><span class=\"h-card\">" + \
|
||||
"<a href=\"https://somewebsite/@nickname\" " + \
|
||||
"class=\"u-url mention\">@<span>nickname</span></a></span> " + \
|
||||
"<br />> This is a quote</p><p>Some other text.</p>"
|
||||
expectedStr = "<p><span class=\"h-card\">" + \
|
||||
"<a href=\"https://somewebsite/@nickname\" " + \
|
||||
"class=\"u-url mention\">@<span>nickname</span></a></span> " + \
|
||||
"<br /><blockquote>This is a quote</blockquote></p>" + \
|
||||
"<p>Some other text.</p>"
|
||||
resultStr = htmlReplaceEmailQuote(testStr)
|
||||
if resultStr != expectedStr:
|
||||
print('Result: ' + resultStr)
|
||||
print('Expect: ' + expectedStr)
|
||||
assert resultStr == expectedStr
|
||||
|
||||
testStr = "<p>Some text:</p><p>> first line->second line</p>" + \
|
||||
"<p>Some question?</p>"
|
||||
expectedStr = "<p>Some text:</p><p><blockquote>first line-<br>" + \
|
||||
"second line</blockquote></p><p>Some question?</p>"
|
||||
resultStr = htmlReplaceEmailQuote(testStr)
|
||||
if resultStr != expectedStr:
|
||||
print('Result: ' + resultStr)
|
||||
print('Expect: ' + expectedStr)
|
||||
assert resultStr == expectedStr
|
||||
|
||||
|
||||
def runAllTests():
|
||||
print('Running tests...')
|
||||
testReplaceEmailQuote()
|
||||
testConstantTimeStringCheck()
|
||||
testTranslations()
|
||||
testValidContentWarning()
|
||||
|
|
|
|||
99
theme.py
|
|
@ -25,8 +25,8 @@ def getThemesList() -> []:
|
|||
and to lookup function names
|
||||
"""
|
||||
return ('Default', 'Blue', 'Hacker', 'Henge', 'HighVis',
|
||||
'LCD', 'Light', 'Night', 'Purple', 'Solidaric',
|
||||
'Starlight', 'Zen')
|
||||
'Indymedia', 'LCD', 'Light', 'Night', 'Purple',
|
||||
'Solidaric', 'Starlight', 'Zen')
|
||||
|
||||
|
||||
def setThemeInConfig(baseDir: str, name: str) -> bool:
|
||||
|
|
@ -67,13 +67,21 @@ def setCSSparam(css: str, param: str, value: str) -> str:
|
|||
if param.startswith('rgba('):
|
||||
return css.replace(param, value)
|
||||
# if the parameter begins with * then don't prepend --
|
||||
onceOnly = False
|
||||
if param.startswith('*'):
|
||||
searchStr = param.replace('*', '') + ':'
|
||||
if param.startswith('**'):
|
||||
onceOnly = True
|
||||
searchStr = param.replace('**', '') + ':'
|
||||
else:
|
||||
searchStr = param.replace('*', '') + ':'
|
||||
else:
|
||||
searchStr = '--' + param + ':'
|
||||
if searchStr not in css:
|
||||
return css
|
||||
s = css.split(searchStr)
|
||||
if onceOnly:
|
||||
s = css.split(searchStr, 1)
|
||||
else:
|
||||
s = css.split(searchStr)
|
||||
newcss = ''
|
||||
for sectionStr in s:
|
||||
if not newcss:
|
||||
|
|
@ -235,6 +243,59 @@ def setThemeDefault(baseDir: str):
|
|||
setThemeFromDict(baseDir, name, themeParams, bgParams)
|
||||
|
||||
|
||||
def setThemeIndymedia(baseDir: str):
|
||||
name = 'indymedia'
|
||||
removeTheme(baseDir)
|
||||
setThemeInConfig(baseDir, name)
|
||||
bgParams = {
|
||||
"login": "jpg",
|
||||
"follow": "jpg",
|
||||
"options": "jpg",
|
||||
"search": "jpg"
|
||||
}
|
||||
themeParams = {
|
||||
"button-corner-radius": "5px",
|
||||
"timeline-border-radius": "5px",
|
||||
"focus-color": "blue",
|
||||
"font-size-button-mobile": "36px",
|
||||
"font-size": "32px",
|
||||
"font-size2": "26px",
|
||||
"font-size3": "40px",
|
||||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "black",
|
||||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "black",
|
||||
"main-link-color": "#ff9900",
|
||||
"main-link-color-hover": "#d09338",
|
||||
"main-visited-color": "#ffb900",
|
||||
"main-fg-color": "white",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#003366",
|
||||
"border-width": "0",
|
||||
"main-bg-color-reply": "#0f0d10",
|
||||
"main-bg-color-report": "#0f0d10",
|
||||
"hashtag-vertical-spacing3": "100px",
|
||||
"hashtag-vertical-spacing4": "150px",
|
||||
"button-background-hover": "darkblue",
|
||||
"button-background": "#003366",
|
||||
"button-selected": "blue",
|
||||
"calendar-bg-color": "#0f0d10",
|
||||
"event-background": "#555",
|
||||
"border-color": "#003366",
|
||||
"lines-color": "#ff9900",
|
||||
"day-number": "lightblue",
|
||||
"day-number2": "white",
|
||||
"time-color": "#003366",
|
||||
"place-color": "#003366",
|
||||
"event-color": "#003366",
|
||||
"title-text": "white",
|
||||
"title-background": "#003366",
|
||||
"quote-right-margin": "0.1em",
|
||||
}
|
||||
setThemeFromDict(baseDir, name, themeParams, bgParams)
|
||||
|
||||
|
||||
def setThemeBlue(baseDir: str):
|
||||
name = 'blue'
|
||||
removeTheme(baseDir)
|
||||
|
|
@ -276,6 +337,8 @@ def setThemeNight(baseDir: str):
|
|||
setThemeInConfig(baseDir, name)
|
||||
fontStr = \
|
||||
"url('./fonts/solidaric.woff2') format('woff2')"
|
||||
fontStrItalic = \
|
||||
"url('./fonts/solidaric-italic.woff2') format('woff2')"
|
||||
themeParams = {
|
||||
"focus-color": "blue",
|
||||
"font-size-button-mobile": "36px",
|
||||
|
|
@ -287,9 +350,11 @@ def setThemeNight(baseDir: str):
|
|||
"main-bg-color": "#0f0d10",
|
||||
"text-entry-background": "#0f0d10",
|
||||
"link-bg-color": "#0f0d10",
|
||||
"main-fg-color": "#7961ab",
|
||||
"main-link-color": "ff9900",
|
||||
"main-link-color-hover": "#d09338",
|
||||
"main-fg-color": "#a961ab",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#7961ab",
|
||||
"border-color": "#606984",
|
||||
"main-bg-color-reply": "#0f0d10",
|
||||
"main-bg-color-report": "#0f0d10",
|
||||
"hashtag-vertical-spacing3": "100px",
|
||||
|
|
@ -298,15 +363,18 @@ def setThemeNight(baseDir: str):
|
|||
"button-background": "#a961ab",
|
||||
"button-selected": "#86579d",
|
||||
"calendar-bg-color": "#0f0d10",
|
||||
"lines-color": "#7961ab",
|
||||
"day-number": "#7961ab",
|
||||
"lines-color": "#a961ab",
|
||||
"day-number": "#a961ab",
|
||||
"day-number2": "#555",
|
||||
"time-color": "#7961ab",
|
||||
"place-color": "#7961ab",
|
||||
"event-color": "#7961ab",
|
||||
"time-color": "#a961ab",
|
||||
"place-color": "#a961ab",
|
||||
"event-color": "#a961ab",
|
||||
"event-background": "#333",
|
||||
"quote-right-margin": "0",
|
||||
"line-spacing": "150%",
|
||||
"*font-family": "'solidaric'",
|
||||
"*src": fontStr
|
||||
"*src": fontStr,
|
||||
"**src": fontStrItalic
|
||||
}
|
||||
bgParams = {
|
||||
"login": "jpg",
|
||||
|
|
@ -364,6 +432,7 @@ def setThemeStarlight(baseDir: str):
|
|||
"place-color": "#ffc4bc",
|
||||
"event-color": "#ffc4bc",
|
||||
"image-corners": "2%",
|
||||
"quote-right-margin": "0.1em",
|
||||
"*font-family": "'bgrove'",
|
||||
"*src": "url('fonts/bgrove.woff2') format('woff2')"
|
||||
}
|
||||
|
|
@ -418,6 +487,7 @@ def setThemeHenge(baseDir: str):
|
|||
"event-background": "#333",
|
||||
"timeline-border-radius": "20px",
|
||||
"image-corners": "8%",
|
||||
"quote-right-margin": "0.1em",
|
||||
"*font-family": "'bgrove'",
|
||||
"*src": "url('fonts/bgrove.woff2') format('woff2')"
|
||||
}
|
||||
|
|
@ -766,8 +836,11 @@ def setThemeSolidaric(baseDir: str):
|
|||
"title-text": "#282c37",
|
||||
"title-background": "#ccc",
|
||||
"gallery-text-color": "black",
|
||||
"quote-right-margin": "0",
|
||||
"line-spacing": "150%",
|
||||
"*font-family": "'solidaric'",
|
||||
"*src": "url('./fonts/solidaric.woff2') format('woff2')"
|
||||
"*src": "url('./fonts/solidaric.woff2') format('woff2')",
|
||||
"**src": "url('./fonts/solidaric-italic.woff2') format('woff2')"
|
||||
}
|
||||
bgParams = {
|
||||
"login": "jpg",
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "سياسة الوسطية أو قواعد السلوك",
|
||||
"Edit event": "تحرير الحدث",
|
||||
"Notify when posts are liked": "يخطر عندما يتم اعجاب المشاركات",
|
||||
"Don't show the Like button": "لا تظهر زر أعجبني"
|
||||
"Don't show the Like button": "لا تظهر زر أعجبني",
|
||||
"Autogenerated Hashtags": "علامات التجزئة المُنشأة تلقائيًا",
|
||||
"Autogenerated Content Warnings": "تحذيرات المحتوى المُنشأ تلقائيًا",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Política de moderació o codi de conducta",
|
||||
"Edit event": "Edita l’esdeveniment",
|
||||
"Notify when posts are liked": "Notifiqueu-ho quan us agradin les publicacions",
|
||||
"Don't show the Like button": "No mostreu el botó M'agrada"
|
||||
"Don't show the Like button": "No mostreu el botó M'agrada",
|
||||
"Autogenerated Hashtags": "Hashtags autogenerats",
|
||||
"Autogenerated Content Warnings": "Advertiments de contingut autogenerats",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Polisi cymedroli neu god ymddygiad",
|
||||
"Edit event": "Golygu digwyddiad",
|
||||
"Notify when posts are liked": "Hysbysu pryd mae swyddi'n cael eu hoffi",
|
||||
"Don't show the Like button": "Peidiwch â dangos y botwm Hoffi"
|
||||
"Don't show the Like button": "Peidiwch â dangos y botwm Hoffi",
|
||||
"Autogenerated Hashtags": "Hashtags awtogeneiddiedig",
|
||||
"Autogenerated Content Warnings": "Rhybuddion Cynnwys Autogenerated",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Moderationsrichtlinie oder Verhaltenskodex",
|
||||
"Edit event": "Ereignis bearbeiten",
|
||||
"Notify when posts are liked": "Benachrichtigen, wenn Beiträge gefallen",
|
||||
"Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an"
|
||||
"Don't show the Like button": "Zeigen Sie nicht die Schaltfläche \"Gefällt mir\" an",
|
||||
"Autogenerated Hashtags": "Automatisch generierte Hashtags",
|
||||
"Autogenerated Content Warnings": "Warnungen vor automatisch generierten Inhalten",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Moderation policy or code of conduct",
|
||||
"Edit event": "Edit event",
|
||||
"Notify when posts are liked": "Notify when posts are liked",
|
||||
"Don't show the Like button": "Don't show the Like button"
|
||||
"Don't show the Like button": "Don't show the Like button",
|
||||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Política de moderación o código de conducta",
|
||||
"Edit event": "Editar evento",
|
||||
"Notify when posts are liked": "Notificar cuando les gusten las publicaciones",
|
||||
"Don't show the Like button": "No mostrar el botón Me gusta"
|
||||
"Don't show the Like button": "No mostrar el botón Me gusta",
|
||||
"Autogenerated Hashtags": "Hashtags autogenerados",
|
||||
"Autogenerated Content Warnings": "Advertencias de contenido generado automáticamente",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Politique de modération ou code de conduite",
|
||||
"Edit event": "Modifier l'événement",
|
||||
"Notify when posts are liked": "Notifier lorsque les messages sont aimés",
|
||||
"Don't show the Like button": "Ne pas afficher le bouton J'aime"
|
||||
"Don't show the Like button": "Ne pas afficher le bouton J'aime",
|
||||
"Autogenerated Hashtags": "Hashtags générés automatiquement",
|
||||
"Autogenerated Content Warnings": "Avertissements de contenu générés automatiquement",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Beartas modhnóireachta nó cód iompair",
|
||||
"Edit event": "Cuir imeacht in eagar",
|
||||
"Notify when posts are liked": "Cuir in iúl cathain is maith poist",
|
||||
"Don't show the Like button": "Ná taispeáin an cnaipe Cosúil"
|
||||
"Don't show the Like button": "Ná taispeáin an cnaipe Cosúil",
|
||||
"Autogenerated Hashtags": "Hashtags uathghinte",
|
||||
"Autogenerated Content Warnings": "Rabhaidh Ábhar Uathghinte",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "मॉडरेशन पॉलिसी या आचार संहिता",
|
||||
"Edit event": "घटना संपादित करें",
|
||||
"Notify when posts are liked": "पोस्ट पसंद आने पर सूचित करें",
|
||||
"Don't show the Like button": "लाइक बटन न दिखाएं"
|
||||
"Don't show the Like button": "लाइक बटन न दिखाएं",
|
||||
"Autogenerated Hashtags": "ऑटोजेनरेटेड हैशटैग",
|
||||
"Autogenerated Content Warnings": "स्वतः प्राप्त सामग्री चेतावनी",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Politica di moderazione o codice di condotta",
|
||||
"Edit event": "Modifica evento",
|
||||
"Notify when posts are liked": "Avvisa quando i post sono piaciuti",
|
||||
"Don't show the Like button": "Non mostrare il pulsante Mi piace"
|
||||
"Don't show the Like button": "Non mostrare il pulsante Mi piace",
|
||||
"Autogenerated Hashtags": "Hashtag generati automaticamente",
|
||||
"Autogenerated Content Warnings": "Avvisi sui contenuti generati automaticamente",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "モデレートポリシーまたは行動規範",
|
||||
"Edit event": "イベントを編集",
|
||||
"Notify when posts are liked": "投稿が高く評価されたときに通知する",
|
||||
"Don't show the Like button": "「いいね!」ボタンを表示しない"
|
||||
"Don't show the Like button": "「いいね!」ボタンを表示しない",
|
||||
"Autogenerated Hashtags": "自動生成されたハッシュタグ",
|
||||
"Autogenerated Content Warnings": "自動生成されたコンテンツの警告",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,5 +279,8 @@
|
|||
"Moderation policy or code of conduct": "Moderation policy or code of conduct",
|
||||
"Edit event": "Edit event",
|
||||
"Notify when posts are liked": "Notify when posts are liked",
|
||||
"Don't show the Like button": "Don't show the Like button"
|
||||
"Don't show the Like button": "Don't show the Like button",
|
||||
"Autogenerated Hashtags": "Autogenerated Hashtags",
|
||||
"Autogenerated Content Warnings": "Autogenerated Content Warnings",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Política de moderação ou código de conduta",
|
||||
"Edit event": "Editar evento",
|
||||
"Notify when posts are liked": "Notificar quando as postagens forem curtidas",
|
||||
"Don't show the Like button": "Não mostrar o botão Curtir"
|
||||
"Don't show the Like button": "Não mostrar o botão Curtir",
|
||||
"Autogenerated Hashtags": "Hashtags autogeradas",
|
||||
"Autogenerated Content Warnings": "Avisos de conteúdo gerado automaticamente",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "Политика модерации или кодекс поведения",
|
||||
"Edit event": "Изменить мероприятие",
|
||||
"Notify when posts are liked": "Уведомлять, когда публикации нравятся",
|
||||
"Don't show the Like button": "Не показывать кнопку \"Нравится\""
|
||||
"Don't show the Like button": "Не показывать кнопку \"Нравится\"",
|
||||
"Autogenerated Hashtags": "Автоматически сгенерированные хештеги",
|
||||
"Autogenerated Content Warnings": "Автоматические предупреждения о содержании",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,5 +283,8 @@
|
|||
"Moderation policy or code of conduct": "审核政策或行为准则",
|
||||
"Edit event": "编辑活动",
|
||||
"Notify when posts are liked": "通知喜欢的帖子",
|
||||
"Don't show the Like button": "不显示“赞”按钮"
|
||||
"Don't show the Like button": "不显示“赞”按钮",
|
||||
"Autogenerated Hashtags": "自动生成的标签",
|
||||
"Autogenerated Content Warnings": "自动生成的内容警告",
|
||||
"Indymedia": "Indymedia"
|
||||
}
|
||||
|
|
|
|||
4
utils.py
|
|
@ -327,9 +327,9 @@ def followPerson(baseDir: str, nickname: str, domain: str,
|
|||
print('DEBUG: follow of domain ' + followDomain)
|
||||
|
||||
if ':' in domain:
|
||||
handle = nickname + '@' + domain.split(':')[0].lower()
|
||||
handle = nickname + '@' + domain.split(':')[0]
|
||||
else:
|
||||
handle = nickname + '@' + domain.lower()
|
||||
handle = nickname + '@' + domain
|
||||
|
||||
if not os.path.isdir(baseDir + '/accounts/' + handle):
|
||||
print('WARN: account for ' + handle + ' does not exist')
|
||||
|
|
|
|||
10
webfinger.py
|
|
@ -121,11 +121,11 @@ def storeWebfingerEndpoint(nickname: str, domain: str, port: int,
|
|||
wfSubdir = '/wfendpoints'
|
||||
if not os.path.isdir(baseDir + wfSubdir):
|
||||
os.mkdir(baseDir + wfSubdir)
|
||||
filename = baseDir + wfSubdir + '/' + handle.lower() + '.json'
|
||||
filename = baseDir + wfSubdir + '/' + handle + '.json'
|
||||
saveJson(wfJson, filename)
|
||||
if nickname == 'inbox':
|
||||
handle = originalDomain + '@' + domain
|
||||
filename = baseDir + wfSubdir + '/' + handle.lower() + '.json'
|
||||
filename = baseDir + wfSubdir + '/' + handle + '.json'
|
||||
saveJson(wfJson, filename)
|
||||
return True
|
||||
|
||||
|
|
@ -261,7 +261,7 @@ def webfingerLookup(path: str, baseDir: str,
|
|||
if onionDomain in handle:
|
||||
handle = handle.replace(onionDomain, domain)
|
||||
onionify = True
|
||||
filename = baseDir + '/wfendpoints/' + handle.lower() + '.json'
|
||||
filename = baseDir + '/wfendpoints/' + handle + '.json'
|
||||
if debug:
|
||||
print('DEBUG: WEBFINGER filename ' + filename)
|
||||
if not os.path.isfile(filename):
|
||||
|
|
@ -339,7 +339,7 @@ def webfingerUpdate(baseDir: str, nickname: str, domain: str,
|
|||
if not os.path.isdir(baseDir + wfSubdir):
|
||||
return
|
||||
|
||||
filename = baseDir + wfSubdir + '/' + handle.lower() + '.json'
|
||||
filename = baseDir + wfSubdir + '/' + handle + '.json'
|
||||
onionify = False
|
||||
if onionDomain:
|
||||
if onionDomain in handle:
|
||||
|
|
@ -352,7 +352,7 @@ def webfingerUpdate(baseDir: str, nickname: str, domain: str,
|
|||
if not wfJson:
|
||||
return
|
||||
|
||||
actorFilename = baseDir + '/accounts/' + handle.lower() + '.json'
|
||||
actorFilename = baseDir + '/accounts/' + handle + '.json'
|
||||
actorJson = loadJson(actorFilename)
|
||||
if not actorJson:
|
||||
return
|
||||
|
|
|
|||
104
webinterface.py
|
|
@ -58,6 +58,7 @@ from bookmarks import bookmarkedByPerson
|
|||
from announce import announcedByPerson
|
||||
from blocking import isBlocked
|
||||
from blocking import isBlockedHashtag
|
||||
from content import htmlReplaceEmailQuote
|
||||
from content import htmlReplaceQuoteMarks
|
||||
from content import removeTextFormatting
|
||||
from content import switchWords
|
||||
|
|
@ -523,20 +524,20 @@ def htmlSearchSharedItems(translate: {},
|
|||
break
|
||||
if matched:
|
||||
if currPage == pageNumber:
|
||||
sharedItemsForm += '<div class="container">'
|
||||
sharedItemsForm += '<div class="container">\n'
|
||||
sharedItemsForm += \
|
||||
'<p class="share-title">' + \
|
||||
sharedItem['displayName'] + '</p>'
|
||||
sharedItem['displayName'] + '</p>\n'
|
||||
if sharedItem.get('imageUrl'):
|
||||
sharedItemsForm += \
|
||||
'<a href="' + \
|
||||
sharedItem['imageUrl'] + '">'
|
||||
sharedItem['imageUrl'] + '">\n'
|
||||
sharedItemsForm += \
|
||||
'<img loading="lazy" src="' + \
|
||||
sharedItem['imageUrl'] + \
|
||||
'" alt="Item image"></a>'
|
||||
'" alt="Item image"></a>\n'
|
||||
sharedItemsForm += \
|
||||
'<p>' + sharedItem['summary'] + '</p>'
|
||||
'<p>' + sharedItem['summary'] + '</p>\n'
|
||||
sharedItemsForm += \
|
||||
'<p><b>' + translate['Type'] + \
|
||||
':</b> ' + sharedItem['itemType'] + ' '
|
||||
|
|
@ -545,7 +546,7 @@ def htmlSearchSharedItems(translate: {},
|
|||
':</b> ' + sharedItem['category'] + ' '
|
||||
sharedItemsForm += \
|
||||
'<b>' + translate['Location'] + \
|
||||
':</b> ' + sharedItem['location'] + '</p>'
|
||||
':</b> ' + sharedItem['location'] + '</p>\n'
|
||||
contactActor = \
|
||||
httpPrefix + '://' + domainFull + \
|
||||
'/users/' + contactNickname
|
||||
|
|
@ -555,13 +556,13 @@ def htmlSearchSharedItems(translate: {},
|
|||
sharedItem['displayName'] + \
|
||||
'?mention=' + contactActor + \
|
||||
'"><button class="button">' + \
|
||||
translate['Contact'] + '</button></a>'
|
||||
translate['Contact'] + '</button></a>\n'
|
||||
if actor.endswith('/users/' + contactNickname):
|
||||
sharedItemsForm += \
|
||||
' <a href="' + actor + '?rmshare=' + \
|
||||
name + '"><button class="button">' + \
|
||||
translate['Remove'] + '</button></a>'
|
||||
sharedItemsForm += '</p></div>'
|
||||
translate['Remove'] + '</button></a>\n'
|
||||
sharedItemsForm += '</p></div>\n'
|
||||
if not resultsExist and currPage > 1:
|
||||
postActor = \
|
||||
getAltPath(actor, domainFull,
|
||||
|
|
@ -571,26 +572,26 @@ def htmlSearchSharedItems(translate: {},
|
|||
'<form method="POST" action="' + \
|
||||
postActor + \
|
||||
'/searchhandle?page=' + \
|
||||
str(pageNumber - 1) + '">'
|
||||
str(pageNumber - 1) + '">\n'
|
||||
sharedItemsForm += \
|
||||
' <input type="hidden" ' + \
|
||||
'name="actor" value="' + actor + '">'
|
||||
'name="actor" value="' + actor + '">\n'
|
||||
sharedItemsForm += \
|
||||
' <input type="hidden" ' + \
|
||||
'name="searchtext" value="' + \
|
||||
searchStrLower + '"><br>'
|
||||
searchStrLower + '"><br>\n'
|
||||
sharedItemsForm += \
|
||||
' <center><a href="' + actor + \
|
||||
'" type="submit" name="submitSearch">'
|
||||
'" type="submit" name="submitSearch">\n'
|
||||
sharedItemsForm += \
|
||||
' <img loading="lazy" ' + \
|
||||
'class="pageicon" src="/' + iconsDir + \
|
||||
'/pageup.png" title="' + \
|
||||
translate['Page up'] + \
|
||||
'" alt="' + translate['Page up'] + \
|
||||
'"/></a>'
|
||||
sharedItemsForm += ' </center>'
|
||||
sharedItemsForm += '</form>'
|
||||
'"/></a>\n'
|
||||
sharedItemsForm += ' </center>\n'
|
||||
sharedItemsForm += '</form>\n'
|
||||
resultsExist = True
|
||||
ctr += 1
|
||||
if ctr >= resultsPerPage:
|
||||
|
|
@ -604,31 +605,31 @@ def htmlSearchSharedItems(translate: {},
|
|||
'<form method="POST" action="' + \
|
||||
postActor + \
|
||||
'/searchhandle?page=' + \
|
||||
str(pageNumber + 1) + '">'
|
||||
str(pageNumber + 1) + '">\n'
|
||||
sharedItemsForm += \
|
||||
' <input type="hidden" ' + \
|
||||
'name="actor" value="' + actor + '">'
|
||||
'name="actor" value="' + actor + '">\n'
|
||||
sharedItemsForm += \
|
||||
' <input type="hidden" ' + \
|
||||
'name="searchtext" value="' + \
|
||||
searchStrLower + '"><br>'
|
||||
searchStrLower + '"><br>\n'
|
||||
sharedItemsForm += \
|
||||
' <center><a href="' + actor + \
|
||||
'" type="submit" name="submitSearch">'
|
||||
'" type="submit" name="submitSearch">\n'
|
||||
sharedItemsForm += \
|
||||
' <img loading="lazy" ' + \
|
||||
'class="pageicon" src="/' + iconsDir + \
|
||||
'/pagedown.png" title="' + \
|
||||
translate['Page down'] + \
|
||||
'" alt="' + translate['Page down'] + \
|
||||
'"/></a>'
|
||||
sharedItemsForm += ' </center>'
|
||||
sharedItemsForm += '</form>'
|
||||
'"/></a>\n'
|
||||
sharedItemsForm += ' </center>\n'
|
||||
sharedItemsForm += '</form>\n'
|
||||
break
|
||||
ctr = 0
|
||||
if not resultsExist:
|
||||
sharedItemsForm += \
|
||||
'<center><h5>' + translate['No results'] + '</h5></center>'
|
||||
'<center><h5>' + translate['No results'] + '</h5></center>\n'
|
||||
sharedItemsForm += htmlFooter()
|
||||
return sharedItemsForm
|
||||
|
||||
|
|
@ -1167,6 +1168,22 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
with open(switchFilename, 'r') as switchfile:
|
||||
switchStr = switchfile.read()
|
||||
|
||||
autoTags = ''
|
||||
autoTagsFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/autotags.txt'
|
||||
if os.path.isfile(autoTagsFilename):
|
||||
with open(autoTagsFilename, 'r') as autoTagsFile:
|
||||
autoTags = autoTagsFile.read()
|
||||
|
||||
autoCW = ''
|
||||
autoCWFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/autocw.txt'
|
||||
if os.path.isfile(autoCWFilename):
|
||||
with open(autoCWFilename, 'r') as autoCWFile:
|
||||
autoCW = autoCWFile.read()
|
||||
|
||||
blockedStr = ''
|
||||
blockedFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
|
|
@ -1507,6 +1524,22 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
' <textarea id="message" name="switchWords" ' + \
|
||||
'style="height:200px">' + switchStr + '</textarea>\n'
|
||||
|
||||
editProfileForm += \
|
||||
' <br><b><label class="labels">' + \
|
||||
translate['Autogenerated Hashtags'] + '</label></b>\n'
|
||||
editProfileForm += ' <br><label class="labels">A -> #B</label>\n'
|
||||
editProfileForm += \
|
||||
' <textarea id="message" name="autoTags" ' + \
|
||||
'style="height:200px">' + autoTags + '</textarea>\n'
|
||||
|
||||
editProfileForm += \
|
||||
' <br><b><label class="labels">' + \
|
||||
translate['Autogenerated Content Warnings'] + '</label></b>\n'
|
||||
editProfileForm += ' <br><label class="labels">A -> B</label>\n'
|
||||
editProfileForm += \
|
||||
' <textarea id="message" name="autoCW" ' + \
|
||||
'style="height:200px">' + autoCW + '</textarea>\n'
|
||||
|
||||
editProfileForm += \
|
||||
' <br><b><label class="labels">' + \
|
||||
translate['Blocked accounts'] + '</label></b>\n'
|
||||
|
|
@ -1880,7 +1913,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
' <a href="' + inReplyTo + '">' + \
|
||||
translate['this post'] + '</a></p>\n'
|
||||
replyStr = '<input type="hidden" ' + \
|
||||
'name="replyTo" value="' + inReplyTo + '">'
|
||||
'name="replyTo" value="' + inReplyTo + '">\n'
|
||||
|
||||
# if replying to a non-public post then also make
|
||||
# this post non-public
|
||||
|
|
@ -4729,6 +4762,7 @@ def individualPostAsHtml(allowDownloads: bool,
|
|||
objectContent = removeTextFormatting(objectContent)
|
||||
objectContent = \
|
||||
switchWords(baseDir, nickname, domain, objectContent)
|
||||
objectContent = htmlReplaceEmailQuote(objectContent)
|
||||
objectContent = htmlReplaceQuoteMarks(objectContent)
|
||||
else:
|
||||
objectContent = \
|
||||
|
|
@ -6165,7 +6199,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
if optionsLink:
|
||||
optionsLinkStr = \
|
||||
' <input type="hidden" name="postUrl" value="' + \
|
||||
optionsLink + '">'
|
||||
optionsLink + '">\n'
|
||||
cssFilename = baseDir + '/epicyon-options.css'
|
||||
if os.path.isfile(baseDir + '/options.css'):
|
||||
cssFilename = baseDir + '/options.css'
|
||||
|
|
@ -6242,10 +6276,10 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
handle = optionsNickname + '@' + optionsDomainFull
|
||||
petname = getPetName(baseDir, nickname, domain, handle)
|
||||
optionsStr += \
|
||||
translate['Petname'] + ': ' + \
|
||||
'<input type="text" name="optionpetname" value="' + \
|
||||
' ' + translate['Petname'] + ': \n' + \
|
||||
' <input type="text" name="optionpetname" value="' + \
|
||||
petname + '">\n' \
|
||||
'<button type="submit" class="buttonsmall" ' + \
|
||||
' <button type="submit" class="buttonsmall" ' + \
|
||||
'name="submitPetname">' + \
|
||||
translate['Submit'] + '</button><br>\n'
|
||||
|
||||
|
|
@ -6253,24 +6287,24 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
if receivingCalendarEvents(baseDir, nickname, domain,
|
||||
optionsNickname, optionsDomainFull):
|
||||
optionsStr += \
|
||||
'<input type="checkbox" ' + \
|
||||
' <input type="checkbox" ' + \
|
||||
'class="profilecheckbox" name="onCalendar" checked> ' + \
|
||||
translate['Receive calendar events from this account'] + \
|
||||
'<button type="submit" class="buttonsmall" ' + \
|
||||
'\n <button type="submit" class="buttonsmall" ' + \
|
||||
'name="submitOnCalendar">' + \
|
||||
translate['Submit'] + '</button><br>\n'
|
||||
else:
|
||||
optionsStr += \
|
||||
'<input type="checkbox" ' + \
|
||||
' <input type="checkbox" ' + \
|
||||
'class="profilecheckbox" name="onCalendar"> ' + \
|
||||
translate['Receive calendar events from this account'] + \
|
||||
'<button type="submit" class="buttonsmall" ' + \
|
||||
'\n <button type="submit" class="buttonsmall" ' + \
|
||||
'name="submitOnCalendar">' + \
|
||||
translate['Submit'] + '</button><br>\n'
|
||||
|
||||
optionsStr += optionsLinkStr
|
||||
optionsStr += \
|
||||
' <a href="/"><button type="button" class="button" ' + \
|
||||
' <a href="/"><button type="button" class="buttonIcon" ' + \
|
||||
'name="submitBack">' + translate['Go Back'] + '</button></a>\n'
|
||||
optionsStr += \
|
||||
' <button type="submit" class="button" name="submitView">' + \
|
||||
|
|
@ -6302,7 +6336,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
|
||||
optionsStr += \
|
||||
' <br><br>' + translate['Notes'] + ': \n'
|
||||
optionsStr += '<button type="submit" class="buttonsmall" ' + \
|
||||
optionsStr += ' <button type="submit" class="buttonsmall" ' + \
|
||||
'name="submitPersonNotes">' + \
|
||||
translate['Submit'] + '</button><br>\n'
|
||||
optionsStr += \
|
||||
|
|
|
|||
|
|
@ -1159,7 +1159,7 @@
|
|||
<p>
|
||||
<p class="siteheader">An Internet of People, Not Corporate Agendas</p>
|
||||
<p class="intro">
|
||||
Epicyon is written in Python with a HTML+CSS web interface and uses <i>no javascript</i> which makes display in a web browser very lightweight.
|
||||
Epicyon is written in Python with a HTML+CSS web interface and uses <i>no javascript</i> which makes display in a web browser very lightweight. It can run as a <a href="https://en.wikipedia.org/wiki/Progressive_web_application">Progressive Web App</a> on mobile. <i>Just say "no"</i> to boring social media sites packed with generic adverts and zombified corporate influencers.
|
||||
</p>
|
||||
|
||||
<table style="margin:0% 0%;width:100%" border="0">
|
||||
|
|
@ -1171,6 +1171,10 @@
|
|||
<th><img width="90%" src="img/screenshots.jpg" alt="various screenshots" /></th>
|
||||
<th><img width="50%" src="img/mobile.jpg" alt="mobile screenshot" /></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><img width="90%" src="img/screenshot_login.jpg" alt="login screen" /></th>
|
||||
<th><img width="90%" src="img/screenshot_emojisearch.jpg" alt="emoji search" /></th>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="intro">
|
||||
|
|
@ -1444,7 +1448,7 @@
|
|||
Forward port 443 from your internet router to your server. If you have dynamic DNS make sure its configured. Add a TLS certificate:
|
||||
</p>
|
||||
<div class="shell">
|
||||
certbot certonly -n --server https://acme-v01.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
|
||||
certbot certonly -n --server https://acme-v02.api.letsencrypt.org/directory --standalone -d YOUR_DOMAIN --renew-by-default --agree-tos --email YOUR_EMAIL
|
||||
</div>
|
||||
<p class="intro">
|
||||
Restart your web server:
|
||||
|
|
|
|||