Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
20
README.md
|
@ -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
|
||||
```
|
||||
|
||||
|
|
35
blocking.py
|
@ -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
|
||||
|
||||
|
||||
|
|
2
blog.py
|
@ -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/', '/')
|
||||
|
|
47
content.py
|
@ -63,12 +63,14 @@ def htmlReplaceEmailQuote(content: str) -> str:
|
|||
# replace quote paragraph
|
||||
if '<p>"' in content:
|
||||
if '"</p>' in content:
|
||||
content = content.replace('<p>"', '<p><blockquote>')
|
||||
content = content.replace('"</p>', '</blockquote></p>')
|
||||
if content.count('<p>"') == content.count('"</p>'):
|
||||
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><')
|
||||
if content.count('>\u201c') == content.count('\u201d<'):
|
||||
content = content.replace('>\u201c', '><blockquote>')
|
||||
content = content.replace('\u201d<', '</blockquote><')
|
||||
# replace email style quote
|
||||
if '>> ' not in content:
|
||||
return content
|
||||
|
@ -103,6 +105,12 @@ def htmlReplaceQuoteMarks(content: str) -> str:
|
|||
if '"' not in content:
|
||||
return content
|
||||
|
||||
# only if there are a few quote marks
|
||||
if content.count('"') > 4:
|
||||
return content
|
||||
if content.count('"') > 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 = []
|
||||
|
|
16
deploy/onion
|
@ -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
|
||||
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"turkey": "1F983",
|
||||
"turtle": "1F422",
|
||||
"twitter": "E040",
|
||||
"birdsite": "E040",
|
||||
"two": "0032",
|
||||
"umbrellawithraindrops": "2614",
|
||||
"unamusedface": "1F612",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
112
epicyon.py
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 307 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 91 KiB |
After Width: | Height: | Size: 636 B |
After Width: | Height: | Size: 668 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 743 B |
After Width: | Height: | Size: 699 B |
After Width: | Height: | Size: 652 B |
After Width: | Height: | Size: 680 B |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 11 KiB |