Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

merge-requests/30/head
Bob Mottram 2020-11-01 19:24:24 +00:00
commit f98789758c
185 changed files with 3761 additions and 979 deletions

View File

@ -41,12 +41,12 @@ sudo apt install -y \
tor python3-socks imagemagick \
python3-numpy python3-setuptools \
python3-crypto python3-pycryptodome \
python3-dateutil python3-pil.imagetk
python3-dateutil python3-pil.imagetk \
python3-idna python3-requests \
python3-django-timezone-field \
libimage-exiftool-perl python3-flake8 \
python3-pyqrcode python3-png python3-bandit \
certbot nginx
certbot nginx wget
```
## Installation
@ -61,6 +61,14 @@ Add a dedicated user so that we don't have to run as root.
adduser --system --home=/opt/epicyon --group epicyon
```
Link news mirrors:
``` bash
mkdir /var/www/YOUR_DOMAIN
mkdir -p /opt/epicyon/accounts/newsmirror
ln -s /opt/epicyon/accounts/newsmirror /var/www/YOUR_DOMAIN/newsmirror
```
Edit */etc/systemd/system/epicyon.service* and add the following:
``` systemd
@ -151,7 +159,12 @@ server {
error_log /dev/null;
index index.html;
location /newsmirror {
root /var/www/YOUR_DOMAIN;
try_files $uri =404;
}
location / {
proxy_http_version 1.1;
client_max_body_size 31M;
@ -259,4 +272,3 @@ To run the network tests. These simulate instances exchanging messages.
``` bash
python3 epicyon.py --testsnetwork
```

View File

@ -28,8 +28,9 @@ def addGlobalBlock(baseDir: str,
return False
# block an account handle or domain
blockFile = open(blockingFilename, "a+")
blockFile.write(blockHandle + '\n')
blockFile.close()
if blockFile:
blockFile.write(blockHandle + '\n')
blockFile.close()
else:
blockHashtag = blockNickname
# is the hashtag already blocked?
@ -38,8 +39,9 @@ def addGlobalBlock(baseDir: str,
return False
# block a hashtag
blockFile = open(blockingFilename, "a+")
blockFile.write(blockHashtag + '\n')
blockFile.close()
if blockFile:
blockFile.write(blockHashtag + '\n')
blockFile.close()
return True
@ -147,20 +149,37 @@ def getDomainBlocklist(baseDir: str) -> str:
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if not os.path.isfile(globalBlockingFilename):
return blockedStr
with open(globalBlockingFilename, 'r') as file:
blockedStr += file.read()
with open(globalBlockingFilename, 'r') as fpBlocked:
blockedStr += fpBlocked.read()
return blockedStr
def isBlockedDomain(baseDir: str, domain: str) -> bool:
"""Is the given domain blocked?
"""
if '.' not in domain:
return False
if isEvil(domain):
return True
# by checking a shorter version we can thwart adversaries
# who constantly change their subdomain
sections = domain.split('.')
noOfSections = len(sections)
shortDomain = None
if noOfSections > 2:
shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1]
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(globalBlockingFilename):
if '*@' + domain in open(globalBlockingFilename).read():
return True
with open(globalBlockingFilename, 'r') as fpBlocked:
blockedStr = fpBlocked.read()
if '*@' + domain in blockedStr:
return True
if shortDomain:
if '*@' + shortDomain in blockedStr:
return True
return False

View File

@ -279,6 +279,7 @@ def htmlBlogPostRSS2(authorized: bool,
handle: str, restrictToDomain: bool) -> str:
"""Returns the RSS version 2 feed for a single blog post
"""
rssStr = ''
messageLink = ''
if postJsonObject['object'].get('id'):
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
@ -305,6 +306,7 @@ def htmlBlogPostRSS3(authorized: bool,
handle: str, restrictToDomain: bool) -> str:
"""Returns the RSS version 3 feed for a single blog post
"""
rssStr = ''
messageLink = ''
if postJsonObject['object'].get('id'):
messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')

View File

@ -63,12 +63,14 @@ def htmlReplaceEmailQuote(content: str) -> str:
# replace quote paragraph
if '<p>&quot;' in content:
if '&quot;</p>' in content:
content = content.replace('<p>&quot;', '<p><blockquote>')
content = content.replace('&quot;</p>', '</blockquote></p>')
if content.count('<p>&quot;') == content.count('&quot;</p>'):
content = content.replace('<p>&quot;', '<p><blockquote>')
content = content.replace('&quot;</p>', '</blockquote></p>')
if '>\u201c' in content:
if '\u201d<' in content:
content = content.replace('>\u201c', '><blockquote>')
content = content.replace('\u201d<', '</blockquote><')
if content.count('>\u201c') == content.count('\u201d<'):
content = content.replace('>\u201c', '><blockquote>')
content = content.replace('\u201d<', '</blockquote><')
# replace email style quote
if '>&gt; ' not in content:
return content
@ -103,6 +105,12 @@ def htmlReplaceQuoteMarks(content: str) -> str:
if '&quot;' not in content:
return content
# only if there are a few quote marks
if content.count('"') > 4:
return content
if content.count('&quot;') > 4:
return content
newContent = content
if '"' in content:
sections = content.split('"')
@ -353,6 +361,7 @@ def validHashTag(hashtag: str) -> bool:
# long hashtags are not valid
if len(hashtag) >= 32:
return False
# TODO: this may need to be an international character set
validChars = set('0123456789' +
'abcdefghijklmnopqrstuvwxyz' +
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
@ -374,7 +383,7 @@ def addHashTags(wordStr: str, httpPrefix: str, domain: str,
hashtagUrl = httpPrefix + "://" + domain + "/tags/" + hashtag
postHashtags[hashtag] = {
'href': hashtagUrl,
'name': '#'+hashtag,
'name': '#' + hashtag,
'type': 'Hashtag'
}
replaceHashTags[wordStr] = "<a href=\"" + hashtagUrl + \
@ -560,25 +569,6 @@ def removeTextFormatting(content: str) -> str:
return content
def removeHtml(content: str) -> str:
"""Removes html links from the given content.
Used to ensure that profile descriptions don't contain dubious content
"""
if '<' not in content:
return content
removing = False
content = content.replace('<q>', '"').replace('</q>', '"')
result = ''
for ch in content:
if ch == '<':
removing = True
elif ch == '>':
removing = False
elif not removing:
result += ch
return result
def removeLongWords(content: str, maxWordLength: int,
longWordsList: []) -> str:
"""Breaks up long words so that on mobile screens this doesn't
@ -649,7 +639,7 @@ def removeLongWords(content: str, maxWordLength: int,
wordStr[:maxWordLength])
if content.startswith('<p>'):
if not content.endswith('</p>'):
content = content.strip()+'</p>'
content = content.strip() + '</p>'
return content
@ -701,7 +691,12 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
content = content.replace('\r', '')
content = content.replace('\n', ' --linebreak-- ')
content = addMusicTag(content, 'nowplaying')
words = content.replace(',', ' ').replace(';', ' ').split(' ')
contentSimplified = \
content.replace(',', ' ').replace(';', ' ').replace('- ', ' ')
contentSimplified = contentSimplified.replace('. ', ' ').strip()
if contentSimplified.endswith('.'):
contentSimplified = contentSimplified[:len(contentSimplified)-1]
words = contentSimplified.split(' ')
# remove . for words which are not mentions
newWords = []

643
daemon.py

File diff suppressed because it is too large Load Diff

View File

@ -229,6 +229,9 @@ fi
if [ ! -d ${web_dir}/cache ]; then
mkdir ${web_dir}/cache
fi
if [ ! -d /var/www/${ONION_DOMAIN}/htdocs ]; then
mkdir -p /var/www/${ONION_DOMAIN}/htdocs
fi
echo "Creating nginx virtual host for ${ONION_DOMAIN}"
{ echo "proxy_cache_path ${web_dir}/cache levels=1:2 keys_zone=my_cache:10m max_size=10g";
@ -252,6 +255,12 @@ echo "Creating nginx virtual host for ${ONION_DOMAIN}"
echo ' error_log /dev/null;';
echo '';
echo ' index index.html;';
echo '';
echo ' location /newsmirror {';
echo ' root /var/www/${ONION_DOMAIN}/htdocs;';
echo ' try_files $uri =404;';
echo ' }';
echo '';
echo ' location / {';
echo ' proxy_http_version 1.1;';
echo ' client_max_body_size 31M;';
@ -299,6 +308,13 @@ echo "Creating nginx virtual host for ${ONION_DOMAIN}"
echo ' }';
echo '}'; } > "/etc/nginx/sites-available/${username}"
chown -R www-data:www-data /var/www/${ONION_DOMAIN}/htdocs
if [ ! -d ${install_destination}/accounts/newsmirror ]; then
mkdir -p ${install_destination}/accounts/newsmirror
chown -R ${username}:${username} ${install_destination}
fi
ln -s ${install_destination}/newsmirror /var/www/${ONION_DOMAIN}/htdocs/newsmirror
ln -s "/etc/nginx/sites-available/${username}" /etc/nginx/sites-enabled/
systemctl restart nginx

View File

@ -450,6 +450,7 @@
"turkey": "1F983",
"turtle": "1F422",
"twitter": "E040",
"birdsite": "E040",
"two": "0032",
"umbrellawithraindrops": "2614",
"unamusedface": "1F612",

View File

@ -10,8 +10,8 @@
--border-width: 2px;
--font-size-header: 18px;
--font-color-header: #ccc;
--font-size: 22px;
--font-size-mobile: 40px;
--login-font-size: 22px;
--login-font-size-mobile: 40px;
--text-entry-foreground: #ccc;
--text-entry-background: #111;
--time-color: #aaa;
@ -21,6 +21,7 @@
--form-border-radius: 30px;
--focus-color: white;
--line-spacing: 130%;
--login-logo-width: 20%;
}
@font-face {
@ -53,7 +54,7 @@ body, html {
max-width: 60%;
min-width: 600px;
margin: 0 auto;
font-size: var(--font-size);
font-size: var(--login-font-size);
line-height: var(--line-spacing);
}
@ -89,7 +90,7 @@ input[type=text], input[type=password] {
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
@ -101,12 +102,12 @@ button {
border: none;
cursor: pointer;
width: 100%;
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
.login-text {
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
@ -119,6 +120,10 @@ button:hover {
margin: 24px 0 12px 0;
}
.imgcontainer img {
width: var(--login-logo-width);
}
img.avatar {
width: 40%;
border-radius: 50%;
@ -148,12 +153,12 @@ span.psw {
max-width: 60%;
min-width: 600px;
margin: 0 auto;
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
position: relative;
}
.login-text {
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
input[type=text], input[type=password] {
@ -163,7 +168,7 @@ span.psw {
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
button {
@ -174,7 +179,7 @@ span.psw {
border: none;
cursor: pointer;
width: 100%;
font-size: var(--font-size);
font-size: var(--login-font-size);
font-family: Arial, Helvetica, sans-serif;
}
}
@ -188,12 +193,12 @@ span.psw {
max-width: 95%;
min-width: 600px;
margin: 0 auto;
font-size: var(--font-size-mobile);
font-size: var(--login-font-size-mobile);
font-family: Arial, Helvetica, sans-serif;
position: relative;
}
.login-text {
font-size: var(--font-size-mobile);
font-size: var(--login-font-size-mobile);
font-family: Arial, Helvetica, sans-serif;
}
input[type=text], input[type=password] {
@ -203,7 +208,7 @@ span.psw {
display: inline-block;
border: 1px solid #ccc;
box-sizing: border-box;
font-size: var(--font-size-mobile);
font-size: var(--login-font-size-mobile);
font-family: Arial, Helvetica, sans-serif;
}
button {
@ -214,7 +219,7 @@ span.psw {
border: none;
cursor: pointer;
width: 100%;
font-size: var(--font-size-mobile);
font-size: var(--login-font-size-mobile);
font-family: Arial, Helvetica, sans-serif;
}
}

View File

@ -21,13 +21,15 @@
--main-visited-color: #888;
--border-color: #505050;
--border-width: 2px;
--border-width-header: 2px;
--font-size-header: 18px;
--font-size-header-mobile: 32px;
--font-color-header: #ccc;
--font-size-button-mobile: 34px;
--font-size-links: 18px;
--font-size-publish-button: 18px;
--font-size-newswire: 18px;
--font-size-newswire-mobile: 48px;
--font-size-newswire-mobile: 40px;
--font-size: 30px;
--font-size2: 24px;
--font-size3: 38px;
@ -41,11 +43,14 @@
--font-size-tox2: 8px;
--time-color: #aaa;
--time-vertical-align: 4px;
--time-vertical-align-mobile: 25px;
--publish-button-text: #FFFFFF;
--button-text: #FFFFFF;
--button-selected-text: #FFFFFF;
--publish-button-background: #999;
--button-background: #999;
--button-background-hover: #777;
--button-text-hover: white;
--button-selected: #666;
--button-highlighted: green;
--button-fg-highlighted: #FFFFFF;
@ -91,6 +96,28 @@
--newswire-voted-background-color: black;
--login-button-color: #2965;
--login-button-fg-color: black;
--button-event-corner-radius: 60px;
--button-event-background-color: green;
--button-event-fg-color: white;
--hashtag-background-color: black;
--hashtag-fg-color: white;
--tab-border-width: 0px;
--tab-border-color: grey;
--icon-brightness-change: 150%;
--container-button-padding: 20px;
--container-padding: 2%;
--container-padding-bottom: 1%;
--container-padding-bottom-mobile: 0%;
--vertical-between-posts: 10px;
--vertical-between-posts-header: 10px;
--containericons-horizontal-spacing: 1%;
--containericons-horizontal-spacing-mobile: 3%;
--containericons-horizontal-offset: -1%;
--likes-count-offset: 5px;
--likes-count-offset-mobile: 10px;
--publish-button-vertical-offset: 10px;
--banner-height: 15vh;
--banner-height-mobile: 10vh;
}
@font-face {
@ -184,7 +211,7 @@ a:visited:hover {
}
.buttonevent:hover {
filter: brightness(150%);
filter: brightness(var(----icon-brightness-change));
}
a:focus {
@ -292,11 +319,16 @@ a:focus {
}
.container img.timelineicon:hover {
filter: brightness(150%);
filter: brightness(var(--icon-brightness-change));
}
.containerHeader img.timelineicon:hover {
filter: brightness(var(--icon-brightness-change));
}
.buttonunfollow:hover {
background-color: var(--button-background-hover);
color: var(--button-text-hover);
}
.followRequestHandle {
@ -323,10 +355,12 @@ a:focus {
.button:hover {
background-color: var(--button-background-hover);
color: var(--button-text-hover);
}
.donateButton:hover {
background-color: var(--button-background-hover);
color: var(--button-text-hover);
}
.buttonselected span {
@ -349,14 +383,23 @@ a:focus {
.buttonselected:hover {
background-color: var(--button-background-hover);
color: var(--button-text-hover);
}
.container {
.containerNewPost {
border: var(--border-width) solid var(--border-color);
background-color: var(--main-bg-color);
border-radius: var(--timeline-border-radius);
padding: 20px;
margin: 10px;
padding: var(--container-padding);
margin: var(--vertical-between-posts);
}
.containerHeader {
border: var(--border-width-header) solid var(--border-color);
background-color: var(--main-bg-color);
border-radius: var(--timeline-border-radius);
padding: var(--container-button-padding);
margin: var(--vertical-between-posts-header);
}
.media {
@ -367,8 +410,19 @@ a:focus {
}
.message {
margin-left: 7%;
width: 90%;
margin-left: 0%;
margin-right: 0%;
width: 100%;
}
.addedHashtag:link {
background-color: var(--hashtag-background-color);
color: var(--hashtag-fg-color);
}
.addedHashtag:visited {
background-color: var(--hashtag-background-color);
color: var(--hashtag-fg-color);
}
.message:focus{
@ -384,12 +438,6 @@ a:focus {
font-family: 'monospace';
}
.container::after {
content: "";
clear: both;
display: table;
}
.searchEmoji {
vertical-align: middle;
float: none;
@ -421,7 +469,7 @@ a:focus {
.containericons {
padding: 0px 0px;
margin: 0px 0px;
margin: 0px var(--containericons-horizontal-offset);
}
.replyingto {
@ -467,11 +515,13 @@ a:focus {
}
.container img.attachment {
max-width: 120%;
margin-left: 5%;
width: 120%;
max-width: 140%;
margin-left: -2%;
margin-right: 2%;
width: 125%;
padding-bottom: 3%;
}
.container img.right {
float: var(--icons-side);
margin-left: 0px;
@ -479,6 +529,13 @@ a:focus {
padding: 0 0;
margin: 0 0;
}
.containerHeader img.right {
float: var(--icons-side);
margin-left: 0px;
margin-right:0;
padding: 0 0;
margin: 0 0;
}
.containericons img.right {
float: var(--icons-side);
margin-left: 20px;
@ -486,7 +543,7 @@ a:focus {
}
.containericons img:hover {
filter: brightness(150%);
filter: brightness(var(----icon-brightness-change));
}
.post-title {
@ -542,10 +599,11 @@ input[type=number] {
}
.transparent {
color: rgba(0, 0, 0, 0.0);
color: transparent;
background: transparent;
font-size: 0px;
font-family: Arial, Helvetica, sans-serif;
line-height: 0;
line-height: 0px;
height: 0px;
}
.labelsright {
@ -946,6 +1004,14 @@ aside .toggle-inside li {
display: none;
}
div.containerHeader {
overflow: auto;
}
div.container {
overflow: hidden;
}
@media screen and (min-width: 400px) {
body, html {
background-color: var(--main-bg-color);
@ -957,13 +1023,23 @@ aside .toggle-inside li {
font-size: var(--font-size);
line-height: var(--line-spacing);
}
.container {
border: var(--border-width) solid var(--border-color);
background-color: var(--main-bg-color);
border-radius: var(--timeline-border-radius);
padding-left: var(--container-padding);
padding-right: var(--container-padding);
padding-top: var(--container-padding);
padding-bottom: var(--container-padding-bottom);
margin: var(--vertical-between-posts);
}
h3.linksHeader {
background-color: var(--column-left-header-background);
color: var(--column-left-header-color);
font-size: var(--column-left-header-size);
text-transform: uppercase;
padding: 4px;
border: none;
background-color: var(--column-left-header-background);
color: var(--column-left-header-color);
font-size: var(--column-left-header-size);
text-transform: uppercase;
padding: 4px;
border: none;
}
.newswireItem {
font-size: var(--font-size-newswire);
@ -1001,20 +1077,16 @@ aside .toggle-inside li {
color: var(--column-right-fg-color-voted-on);
float: right;
}
.imageAnchorMobile img{
.imageAnchorMobile img {
display: none;
}
.timeline-banner {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
height: 15%;
background-position: center;
background-repeat: no-repeat;
background-size: 100vw;
position: relative;
width: 98vw;
height: var(--banner-height);
}
.timeline {
border: 0;
width: 100vw;
width: 98vw;
}
.col-left a:link {
background: var(--column-left-color);
@ -1028,7 +1100,6 @@ aside .toggle-inside li {
}
.col-left {
color: var(--column-left-fg-color);
padding: 10px 10px;
font-size: var(--font-size-links);
float: left;
width: var(--column-left-width);
@ -1065,14 +1136,14 @@ aside .toggle-inside li {
.column-right {
background-color: var(--column-left-color);
width: var(--column-right-width);
overflow: hidden;
}
.col-right {
background-color: var(--column-left-color);
color: var(--column-left-fg-color);
padding-left: 10px;
padding-right: 30px;
font-size: var(--font-size-links);
width: var(--column-right-width);
overflow: hidden;
}
.col-right img.rightColEdit {
background: var(--column-left-color);
@ -1095,7 +1166,7 @@ aside .toggle-inside li {
font-family: Arial, Helvetica, sans-serif;
float: right;
padding: 10px 0;
transform: translateX(-10px);
transform: translateX(var(--likes-count-offset));
font-weight: bold;
}
.container p.administeredby {
@ -1132,10 +1203,10 @@ aside .toggle-inside li {
font-family: Arial, Helvetica, sans-serif;
}
div.gallery {
margin: 5px;
margin: 5px 1.5%;
border: 1px solid var(--gallery-border);
float: left;
width: 100%;
width: 95%;
object-fit: scale-down;
}
div.imagedesc {
@ -1150,6 +1221,14 @@ aside .toggle-inside li {
margin-right: 20px;
border-radius: var(--image-corners);
}
.containerHeader img {
float: left;
max-width: 400px;
width: 5%;
padding: 0px 7px;
margin-right: 20px;
border-radius: var(--image-corners);
}
.container img.emojisearch {
width: 15%;
float: right;
@ -1162,6 +1241,14 @@ aside .toggle-inside li {
transform: translateY(-25%);
}
.container img.timelineicon {
float: var(--icons-side);
margin-left: 0px;
margin-right: 0px;
padding: 0px 0px;
margin: 0px 0px;
width: 50px;
}
.containerHeader img.timelineicon {
float: var(--icons-side);
margin-left: 0px;
margin-right:0;
@ -1182,7 +1269,8 @@ aside .toggle-inside li {
float: var(--icons-side);
max-width: 200px;
width: 3%;
margin: 0px 1%;
margin: 0px var(--containericons-horizontal-spacing);
margin-right: 0px;
border-radius: 0%;
}
div.mediaicons img {
@ -1208,10 +1296,10 @@ aside .toggle-inside li {
transform: translateY(-10%);
}
.buttonevent {
border-radius: var(--button-corner-radius);
background-color: var(--button-highlighted);
border-radius: var(--button-event-corner-radius);
background-color: var(--button-event-background-color);
border: none;
color: var(--button-fg-highlighted);
color: var(--button-event-fg-color);
text-align: center;
font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif;
@ -1220,21 +1308,31 @@ aside .toggle-inside li {
cursor: pointer;
margin: 5px;
}
.frontPageMobileButtons{
display: none;
}
.buttonMobile {
background: transparent;
border: none !important;
font-size: 0;
}
.button {
border-radius: var(--button-corner-radius);
background-color: var(--button-background);
border: none;
color: var(--button-text);
text-align: center;
font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding);
width: 10%;
max-width: 200px;
margin: 5px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
border-top: var(--tab-border-width) solid var(--tab-border-color);
border-bottom: none;
border-left: var(--tab-border-width) solid var(--tab-border-color);
border-right: var(--tab-border-width) solid var(--tab-border-color);
}
.publishbtn {
border-radius: var(--button-corner-radius);
@ -1242,7 +1340,7 @@ aside .toggle-inside li {
border: none;
color: var(--publish-button-text);
text-align: center;
font-size: var(--font-size-header);
font-size: var(--font-size-publish-button);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding);
width: 10%;
@ -1250,7 +1348,8 @@ aside .toggle-inside li {
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: -20px 0px;
margin: 0 0px;
margin-top: var(--publish-button-vertical-offset);
}
.buttonhighlighted {
border-radius: var(--button-corner-radius);
@ -1271,18 +1370,20 @@ aside .toggle-inside li {
.buttonselected {
border-radius: var(--button-corner-radius);
background-color: var(--button-selected);
border: none;
color: var(--button-text);
color: var(--button-selected-text);
text-align: center;
font-size: var(--font-size-header);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding);
width: 10%;
max-width: 100px;
margin: 5px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
border-top: var(--tab-border-width) solid var(--tab-border-color);
border-bottom: none;
border-left: var(--tab-border-width) solid var(--tab-border-color);
border-right: var(--tab-border-width) solid var(--tab-border-color);
}
.buttonselectedhighlighted {
border-radius: var(--button-corner-radius);
@ -1543,6 +1644,9 @@ aside .toggle-inside li {
padding: 10px;
margin: 20px 60px;
}
.columnIcons img {
float: right;
}
}
@media screen and (min-width: 2200px) {
@ -1566,13 +1670,23 @@ aside .toggle-inside li {
font-size: var(--font-size);
line-height: var(--line-spacing);
}
.container {
border: var(--border-width) solid var(--border-color);
background-color: var(--main-bg-color);
border-radius: var(--timeline-border-radius);
padding-left: var(--container-padding);
padding-right: var(--container-padding);
padding-top: var(--container-padding);
padding-bottom: var(--container-padding-bottom-mobile);
margin: var(--vertical-between-posts);
}
h3.linksHeader {
background-color: var(--column-left-header-background);
color: var(--column-left-header-color);
font-size: var(--column-left-header-size-mobile);
text-transform: uppercase;
padding: 4px;
border: none;
background-color: var(--column-left-header-background);
color: var(--column-left-header-color);
font-size: var(--column-left-header-size-mobile);
text-transform: uppercase;
padding: 4px;
border: none;
}
.leftColEditImage {
background: var(--main-bg-color);
@ -1636,20 +1750,18 @@ aside .toggle-inside li {
color: var(--column-right-fg-color-voted-on);
float: right;
}
.imageAnchorMobile img{
.imageAnchorMobile img {
display: inline;
}
.timeline-banner {
background-image: linear-gradient(rgba(0, 0, 0, 0.0), rgba(0, 0, 0, 0.5)), url("banner.png");
height: 6%;
background-position: center;
background-repeat: no-repeat;
background-size: 145vw;
position: relative;
width: 98vw;
height: var(--banner-height-mobile);
}
.timeline {
border: 0;
width: 100vw;
table-layout: fixed;
overflow: hidden;
}
.column-left {
display: none;
@ -1680,7 +1792,7 @@ aside .toggle-inside li {
font-family: Arial, Helvetica, sans-serif;
float: right;
padding: 32px 0;
transform: translateX(-20px);
transform: translateX(var(--likes-count-offset-mobile));
font-weight: bold;
}
.container p.administeredby {
@ -1716,10 +1828,10 @@ aside .toggle-inside li {
background-color: var(--main-bg-color);
}
div.gallery {
margin: 5px;
margin: 5px 1.5%;
border: 1px solid var(--gallery-border);
float: left;
width: 100%;
width: 98%;
}
div.imagedesc {
padding: 35px;
@ -1733,6 +1845,14 @@ aside .toggle-inside li {
margin-right: 20px;
border-radius: var(--image-corners);
}
.containerHeader img {
float: left;
max-width: 400px;
width: 15%;
padding: 0px 7px;
margin-right: 20px;
border-radius: var(--image-corners);
}
.container img.emojisearch {
width: 25%;
float: right;
@ -1745,6 +1865,14 @@ aside .toggle-inside li {
transform: translateY(-25%);
}
.container img.timelineicon {
float: var(--icons-side);
margin-left: 0px;
margin-right: 0px;
padding: 0px 0px;
margin: 0px 0px;
width: 100px;
}
.containerHeader img.timelineicon {
float: var(--icons-side);
margin-left: 0px;
margin-right:0;
@ -1779,7 +1907,8 @@ aside .toggle-inside li {
float: var(--icons-side);
max-width: 200px;
width: 7%;
margin: 1% 3%;
margin: 1% var(--containericons-horizontal-spacing-mobile);
margin-right: 0px;
border-radius: 0%;
}
.timeline-avatar img {
@ -1791,10 +1920,10 @@ aside .toggle-inside li {
transform: translateY(-10%);
}
.buttonevent {
border-radius: var(--button-corner-radius);
background-color: var(--button-highlighted);
border-radius: var(--button-event-corner-radius);
background-color: var(--button-event-background-color);
border: none;
color: var(--button-fg-highlighted);
color: var(--button-event-fg-color);
text-align: center;
font-size: var(--font-size-button-mobile);
font-family: Arial, Helvetica, sans-serif;
@ -1806,18 +1935,53 @@ aside .toggle-inside li {
.button {
border-radius: var(--button-corner-radius);
background-color: var(--button-background);
border: none;
color: var(--button-text);
text-align: center;
font-size: var(--font-size-button-mobile);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
margin: 5px;
border-top: var(--tab-border-width) solid var(--tab-border-color);
border-bottom: none;
border-left: var(--tab-border-width) solid var(--tab-border-color);
border-right: var(--tab-border-width) solid var(--tab-border-color);
}
.frontPageMobileButtons{
display: block;
border: var(--border-width-header) solid var(--border-color);
background-color: var(--main-bg-color);
border-radius: var(--timeline-border-radius);
padding: var(--container-button-padding);
margin: var(--vertical-between-posts-header);
}
.frontPageMobileButtons img {
float: right;
max-width: 400px;
width: 10%;
padding: 0px 7px;
margin-right: 20px;
}
.buttonMobile {
border-radius: var(--button-corner-radius);
background-color: var(--button-background);
color: var(--button-text);
text-align: center;
font-size: var(--font-size-button-mobile);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding-mobile);
width: 20%;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 5px;
border-top: var(--tab-border-width) solid var(--tab-border-color);
border-bottom: none;
border-left: var(--tab-border-width) solid var(--tab-border-color);
border-right: var(--tab-border-width) solid var(--tab-border-color);
}
.publishbtn {
border-radius: var(--button-corner-radius);
@ -1854,18 +2018,20 @@ aside .toggle-inside li {
.buttonselected {
border-radius: var(--button-corner-radius);
background-color: var(--button-selected);
border: none;
color: var(--button-text);
color: var(--button-selected-text);
text-align: center;
font-size: var(--font-size-button-mobile);
font-family: Arial, Helvetica, sans-serif;
padding: var(--button-height-padding-mobile);
width: 20%;
max-width: 400px;
min-width: var(--button-width-chars);
transition: all 0.5s;
cursor: pointer;
margin: 15px;
margin: 5px;
border-top: var(--tab-border-width) solid var(--tab-border-color);
border-bottom: none;
border-left: var(--tab-border-width) solid var(--tab-border-color);
border-right: var(--tab-border-width) solid var(--tab-border-color);
}
.buttonselectedhighlighted {
border-radius: var(--button-corner-radius);
@ -1921,7 +2087,7 @@ aside .toggle-inside li {
.time-right {
float: var(--icons-side);
color: var(--time-color);
margin: 25px 20px;
margin: var(--time-vertical-align-mobile) 20px;
}
input[type=text], select, textarea {
width: 100%;
@ -2127,4 +2293,9 @@ aside .toggle-inside li {
padding: 10px;
margin: 40px 80px;
}
.columnIcons img {
width: 10%;
float: right;
margin-right: 1vw;
}
}

View File

@ -120,6 +120,21 @@ parser.add_argument('--maxFeedSize',
dest='maxNewswireFeedSizeKb', type=int,
default=2048,
help='Maximum newswire rss/atom feed size in K')
parser.add_argument('--maxMirroredArticles',
dest='maxMirroredArticles', type=int,
default=100,
help='Maximum number of news articles to mirror.' +
' Set to zero for indefinite mirroring.')
parser.add_argument('--maxNewsPosts',
dest='maxNewsPosts', type=int,
default=0,
help='Maximum number of news timeline posts to keep. ' +
'Zero for no expiry.')
parser.add_argument('--maxFollowers',
dest='maxFollowers', type=int,
default=2000,
help='Maximum number of followers per account. ' +
'Zero for no limit.')
parser.add_argument('--postcache', dest='maxRecentPosts', type=int,
default=512,
help='The maximum number of recent posts to store in RAM')
@ -194,6 +209,41 @@ parser.add_argument("--repliesEnabled", "--commentsEnabled",
type=str2bool, nargs='?',
const=True, default=True,
help="Enable replies to a post")
parser.add_argument("--showPublishAsIcon",
dest='showPublishAsIcon',
type=str2bool, nargs='?',
const=True, default=True,
help="Whether to show newswire publish " +
"as an icon or a button")
parser.add_argument("--fullWidthTimelineButtonHeader",
dest='fullWidthTimelineButtonHeader',
type=str2bool, nargs='?',
const=True, default=False,
help="Whether to show the timeline " +
"button header containing inbox and outbox " +
"as the full width of the screen")
parser.add_argument("--allowNewsFollowers",
dest='allowNewsFollowers',
type=str2bool, nargs='?',
const=True, default=False,
help="Whether to allow the news account to be followed")
parser.add_argument("--iconsAsButtons",
dest='iconsAsButtons',
type=str2bool, nargs='?',
const=True, default=False,
help="Show header icons as buttons")
parser.add_argument("--rssIconAtTop",
dest='rssIconAtTop',
type=str2bool, nargs='?',
const=True, default=True,
help="Whether to show the rss icon at teh top or bottom" +
"of the timeline")
parser.add_argument("--publishButtonAtTop",
dest='publishButtonAtTop',
type=str2bool, nargs='?',
const=True, default=False,
help="Whether to show the publish button at the top of " +
"the newswire column")
parser.add_argument("--noapproval", type=str2bool, nargs='?',
const=True, default=False,
help="Allow followers without approval")
@ -1937,15 +1987,58 @@ if dateonly:
maxNewswirePostsPerSource = \
getConfigParam(baseDir, 'maxNewswirePostsPerSource')
if maxNewswirePostsPerSource:
if maxNewswirePostsPerSource.isdigit():
args.maxNewswirePostsPerSource = maxNewswirePostsPerSource
args.maxNewswirePostsPerSource = int(maxNewswirePostsPerSource)
# set the maximum size of a newswire rss/atom feed in Kilobytes
maxNewswireFeedSizeKb = \
getConfigParam(baseDir, 'maxNewswireFeedSizeKb')
if maxNewswireFeedSizeKb:
if maxNewswireFeedSizeKb.isdigit():
args.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb
args.maxNewswireFeedSizeKb = int(maxNewswireFeedSizeKb)
maxMirroredArticles = \
getConfigParam(baseDir, 'maxMirroredArticles')
if maxMirroredArticles is not None:
args.maxMirroredArticles = int(maxMirroredArticles)
maxNewsPosts = \
getConfigParam(baseDir, 'maxNewsPosts')
if maxNewsPosts is not None:
args.maxNewsPosts = int(maxNewsPosts)
maxFollowers = \
getConfigParam(baseDir, 'maxFollowers')
if maxFollowers is not None:
args.maxFollowers = int(maxFollowers)
allowNewsFollowers = \
getConfigParam(baseDir, 'allowNewsFollowers')
if allowNewsFollowers is not None:
args.allowNewsFollowers = bool(allowNewsFollowers)
showPublishAsIcon = \
getConfigParam(baseDir, 'showPublishAsIcon')
if showPublishAsIcon is not None:
args.showPublishAsIcon = bool(showPublishAsIcon)
iconsAsButtons = \
getConfigParam(baseDir, 'iconsAsButtons')
if iconsAsButtons is not None:
args.iconsAsButtons = bool(iconsAsButtons)
rssIconAtTop = \
getConfigParam(baseDir, 'rssIconAtTop')
if rssIconAtTop is not None:
args.rssIconAtTop = bool(rssIconAtTop)
publishButtonAtTop = \
getConfigParam(baseDir, 'publishButtonAtTop')
if publishButtonAtTop is not None:
args.publishButtonAtTop = bool(publishButtonAtTop)
fullWidthTimelineButtonHeader = \
getConfigParam(baseDir, 'fullWidthTimelineButtonHeader')
if fullWidthTimelineButtonHeader is not None:
args.fullWidthTimelineButtonHeader = bool(fullWidthTimelineButtonHeader)
YTDomain = getConfigParam(baseDir, 'youtubedomain')
if YTDomain:
@ -1960,7 +2053,16 @@ if setTheme(baseDir, themeName, domain):
print('Theme set to ' + themeName)
if __name__ == "__main__":
runDaemon(args.maxNewswireFeedSizeKb,
runDaemon(args.publishButtonAtTop,
args.rssIconAtTop,
args.iconsAsButtons,
args.fullWidthTimelineButtonHeader,
args.showPublishAsIcon,
args.maxFollowers,
args.allowNewsFollowers,
args.maxNewsPosts,
args.maxMirroredArticles,
args.maxNewswireFeedSizeKb,
args.maxNewswirePostsPerSource,
args.dateonly,
args.votingtime,

140
follow.py
View File

@ -8,6 +8,7 @@ __status__ = "Production"
from pprint import pprint
import os
from utils import isSystemAccount
from utils import getFollowersList
from utils import validNickname
from utils import domainPermitted
@ -28,9 +29,14 @@ from session import postJson
def preApprovedFollower(baseDir: str,
nickname: str, domain: str,
approveHandle: str) -> bool:
approveHandle: str,
allowNewsFollowers: bool) -> bool:
"""Is the given handle an already manually approved follower?
"""
# optionally allow the news account to be followed
if nickname == 'news' and allowNewsFollowers:
return True
handle = nickname + '@' + domain
accountDir = baseDir + '/accounts/' + handle
approvedFilename = accountDir + '/approved.txt'
@ -149,7 +155,26 @@ def isFollowerOfPerson(baseDir: str, nickname: str, domain: str,
if not os.path.isfile(followersFile):
return False
handle = followerNickname + '@' + followerDomain
return handle in open(followersFile).read()
alreadyFollowing = False
followersStr = ''
with open(followersFile, 'r') as fpFollowers:
followersStr = fpFollowers.read()
if handle in followersStr:
alreadyFollowing = True
elif '://' + followerDomain + \
'/profile/' + followerNickname in followersStr:
alreadyFollowing = True
elif '://' + followerDomain + \
'/channel/' + followerNickname in followersStr:
alreadyFollowing = True
elif '://' + followerDomain + \
'/accounts/' + followerNickname in followersStr:
alreadyFollowing = True
return alreadyFollowing
def unfollowPerson(baseDir: str, nickname: str, domain: str,
@ -247,13 +272,19 @@ def getNoOfFollows(baseDir: str, nickname: str, domain: str,
with open(filename, "r") as f:
lines = f.readlines()
for line in lines:
if '#' not in line:
if '@' in line and \
'.' in line and \
not line.startswith('http'):
ctr += 1
elif line.startswith('http') and '/users/' in line:
ctr += 1
if '#' in line:
continue
if '@' in line and \
'.' in line and \
not line.startswith('http'):
ctr += 1
elif ((line.startswith('http') or
line.startswith('dat')) and
('/users/' in line or
'/profile/' in line or
'/accounts/' in line or
'/channel/' in line)):
ctr += 1
return ctr
@ -269,7 +300,8 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
httpPrefix: str, authenticated: bool,
followsPerPage=12,
followFile='following') -> {}:
"""Returns the following and followers feeds from GET requests
"""Returns the following and followers feeds from GET requests.
This accesses the following.txt or followers.txt and builds a collection.
"""
# Show a small number of follows to non-authenticated viewers
if not authenticated:
@ -360,6 +392,7 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
for line in lines:
if '#' not in line:
if '@' in line and not line.startswith('http'):
# nickname@domain
pageCtr += 1
totalCtr += 1
if currPage == pageNumber:
@ -371,7 +404,12 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
line2.split('@')[0]
following['orderedItems'].append(url)
elif ((line.startswith('http') or
line.startswith('dat')) and '/users/' in line):
line.startswith('dat')) and
('/users/' in line or
'/profile/' in line or
'/accounts/' in line or
'/channel/' in line)):
# https://domain/users/nickname
pageCtr += 1
totalCtr += 1
if currPage == pageNumber:
@ -394,12 +432,13 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str,
def followApprovalRequired(baseDir: str, nicknameToFollow: str,
domainToFollow: str, debug: bool,
followRequestHandle: str) -> bool:
followRequestHandle: str,
allowNewsFollowers: bool) -> bool:
""" Returns the policy for follower approvals
"""
# has this handle already been manually approved?
if preApprovedFollower(baseDir, nicknameToFollow, domainToFollow,
followRequestHandle):
followRequestHandle, allowNewsFollowers):
return False
manuallyApproveFollows = False
@ -453,7 +492,7 @@ def storeFollowRequest(baseDir: str,
nicknameToFollow: str, domainToFollow: str, port: int,
nickname: str, domain: str, fromPort: int,
followJson: {},
debug: bool) -> bool:
debug: bool, personUrl: str) -> bool:
"""Stores the follow request for later use
"""
accountsDir = baseDir + '/accounts/' + \
@ -462,14 +501,31 @@ def storeFollowRequest(baseDir: str,
return False
approveHandle = nickname + '@' + domain
domainFull = domain
if fromPort:
if fromPort != 80 and fromPort != 443:
if ':' not in domain:
approveHandle = nickname + '@' + domain + ':' + str(fromPort)
domainFull = domain + ':' + str(fromPort)
followersFilename = accountsDir + '/followers.txt'
if os.path.isfile(followersFilename):
if approveHandle in open(followersFilename).read():
alreadyFollowing = False
followersStr = ''
with open(followersFilename, 'r') as fpFollowers:
followersStr = fpFollowers.read()
if approveHandle in followersStr:
alreadyFollowing = True
elif '://' + domainFull + '/profile/' + nickname in followersStr:
alreadyFollowing = True
elif '://' + domainFull + '/channel/' + nickname in followersStr:
alreadyFollowing = True
elif '://' + domainFull + '/accounts/' + nickname in followersStr:
alreadyFollowing = True
if alreadyFollowing:
if debug:
print('DEBUG: ' +
nicknameToFollow + '@' + domainToFollow +
@ -488,17 +544,23 @@ def storeFollowRequest(baseDir: str,
# add to a file which contains a list of requests
approveFollowsFilename = accountsDir + '/followrequests.txt'
# store either nick@domain or the full person/actor url
approveHandleStored = approveHandle
if '/users/' not in personUrl:
approveHandleStored = personUrl
if os.path.isfile(approveFollowsFilename):
if approveHandle not in open(approveFollowsFilename).read():
with open(approveFollowsFilename, 'a+') as fp:
fp.write(approveHandle + '\n')
fp.write(approveHandleStored + '\n')
else:
if debug:
print('DEBUG: ' + approveHandle +
print('DEBUG: ' + approveHandleStored +
' is already awaiting approval')
else:
with open(approveFollowsFilename, "w+") as fp:
fp.write(approveHandle + '\n')
fp.write(approveHandleStored + '\n')
# store the follow request in its own directory
# We don't rely upon the inbox because items in there could expire
@ -513,7 +575,9 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
port: int, sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
messageJson: {}, federationList: [],
debug: bool, projectVersion: str) -> bool:
debug: bool, projectVersion: str,
allowNewsFollowers: bool,
maxFollowers: int) -> bool:
"""Receives a follow request within the POST section of HTTPServer
"""
if not messageJson['type'].startswith('Follow'):
@ -528,7 +592,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
'/channel/' not in messageJson['actor'] and \
'/profile/' not in messageJson['actor']:
if debug:
print('DEBUG: "users" or "profile" missing from actor')
print('DEBUG: users/profile/accounts/channel missing from actor')
return False
domain, tempPort = getDomainFromActor(messageJson['actor'])
fromPort = port
@ -556,7 +620,8 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
'/channel/' not in messageJson['object'] and \
'/profile/' not in messageJson['object']:
if debug:
print('DEBUG: "users" or "profile" not found within object')
print('DEBUG: users/profile/channel/accounts ' +
'not found within object')
return False
domainToFollow, tempPort = getDomainFromActor(messageJson['object'])
if not domainPermitted(domainToFollow, federationList):
@ -574,10 +639,19 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
print('DEBUG: follow request does not contain a ' +
'nickname for the account followed')
return True
if nicknameToFollow == 'news' or nicknameToFollow == 'inbox':
if debug:
print('DEBUG: Cannot follow the news or inbox accounts')
return True
if isSystemAccount(nicknameToFollow):
if not (nicknameToFollow == 'news' and allowNewsFollowers):
if debug:
print('DEBUG: Cannot follow system account - ' +
nicknameToFollow)
return True
if maxFollowers > 0:
if getNoOfFollowers(baseDir,
nicknameToFollow, domainToFollow,
True) > maxFollowers:
print('WARN: ' + nicknameToFollow +
' has reached their maximum number of followers')
return True
handleToFollow = nicknameToFollow + '@' + domainToFollow
if domainToFollow == domain:
if not os.path.isdir(baseDir + '/accounts/' + handleToFollow):
@ -598,7 +672,8 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
# what is the followers policy?
approveHandle = nickname + '@' + domainFull
if followApprovalRequired(baseDir, nicknameToFollow,
domainToFollow, debug, approveHandle):
domainToFollow, debug, approveHandle,
allowNewsFollowers):
print('Follow approval is required')
if domain.endswith('.onion'):
if noOfFollowRequests(baseDir,
@ -626,7 +701,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
return storeFollowRequest(baseDir,
nicknameToFollow, domainToFollow, port,
nickname, domain, fromPort,
messageJson, debug)
messageJson, debug, messageJson['actor'])
else:
print('Follow request does not require approval')
# update the followers
@ -635,6 +710,12 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str,
followersFilename = \
baseDir + '/accounts/' + \
nicknameToFollow + '@' + domainToFollow + '/followers.txt'
# for actors which don't follow the mastodon
# /users/ path convention store the full actor
if '/users/' not in messageJson['actor']:
approveHandle = messageJson['actor']
print('Updating followers file: ' +
followersFilename + ' adding ' + approveHandle)
if os.path.isfile(followersFilename):
@ -788,7 +869,7 @@ def sendFollowRequest(session, baseDir: str,
clientToServer: bool, federationList: [],
sendThreads: [], postLog: [], cachedWebfingers: {},
personCache: {}, debug: bool,
projectVersion: str) -> {}:
projectVersion: str, allowNewsFollowers: bool) -> {}:
"""Gets the json object for sending a follow request
"""
if not domainPermitted(followDomain, federationList):
@ -830,7 +911,8 @@ def sendFollowRequest(session, baseDir: str,
'object': followedId
}
if followApprovalRequired(baseDir, nickname, domain, debug, followHandle):
if followApprovalRequired(baseDir, nickname, domain, debug,
followHandle, allowNewsFollowers):
# Remove any follow requests rejected for the account being followed.
# It's assumed that if you are following someone then you are
# ok with them following back. If this isn't the case then a rejected

View File

@ -13,6 +13,7 @@ Judges is under GPL. See https://webfonts.ffonts.net/Judges.font
LinBiolinum is under GPLv2. See https://www.1001fonts.com/linux-biolinum-font.html
LcdSolid is public domain. See https://www.fontspace.com/lcd-solid-font-f11346
MarginaliaRegular is public domain. See https://www.fontspace.com/marginalia-font-f32466
Nimbus Sans L is GPL. See https://www.fontsquirrel.com/fonts/nimbus-sans-l
Octavius is created by Jack Oatley and described as "100% free to use, though credit is appreciated" https://www.dafont.com/octavius.font
RailModel is GPL. See https://www.fontspace.com/rail-model-font-f10741
Solidaric by Bob Mottram is under AGPL

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,7 @@ You will need python version 3.7 or later.
On a Debian based system:
sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx
sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget
The following instructions install Epicyon to the /opt directory. It's not essential that it be installed there, and it could be in any other preferred directory.
@ -19,6 +19,12 @@ Create a user for the server to run as:
adduser --system --home=/opt/epicyon --group epicyon
chown -R epicyon:epicyon /opt/epicyon
Link news mirrors:
mkdir /var/www/YOUR_DOMAIN
mkdir -p /opt/epicyon/accounts/newsmirror
ln -s /opt/epicyon/accounts/newsmirror /var/www/YOUR_DOMAIN/newsmirror
Create a daemon:
nano /etc/systemd/system/epicyon.service
@ -104,6 +110,11 @@ And paste the following:
index index.html;
location /newsmirror {
root /var/www/YOUR_DOMAIN;
try_files $uri =404;
}
location / {
proxy_http_version 1.1;
client_max_body_size 31M;

45
hashtagrules.txt 100644
View File

@ -0,0 +1,45 @@
Epicyon news rules processing
=============================
As news arrives via RSS or Atom feeds it can be processed to add or remove hashtags, in accordance to some rules which you can define.
On the newswire edit screen, available to moderators, you can define the news processing rules. There is one rule per line.
Syntax
------
if [conditions] then [action]
Logical Operators
-----------------
The following operators are available:
not, and, or, xor, from, contains
Examples
--------
A simple example is:
if moderated and not #oxfordimc then block
For moderated feeds this will only allow items through if they have the #oxfordimc hashtag.
If you want to add hashtags an example is:
if contains "garden" or contains "lawn" then add #gardening
So if incoming news contains the word "garden" either in its title or description then it will automatically be assigned the hashtag #gardening. You can also add hashtags based upon other hashtags.
if #garden or #lawn then add #gardening
You can also remove hashtags.
if #garden or #lawn then remove #gardening
Which will remove #gardening if it exists as a hashtag within the news post.
You can add tags based upon the RSS link, such as:
if from "mycatsite.com" then add #cats

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More