mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
commit
415c215eb1
2
Makefile
2
Makefile
|
|
@ -15,7 +15,7 @@ source:
|
||||||
rm -f ../${APP}*.deb ../${APP}*.changes ../${APP}*.asc ../${APP}*.dsc
|
rm -f ../${APP}*.deb ../${APP}*.changes ../${APP}*.asc ../${APP}*.dsc
|
||||||
cd .. && mv ${APP} ${APP}-${VERSION} && tar -zcvf ${APP}_${VERSION}.orig.tar.gz ${APP}-${VERSION}/ && mv ${APP}-${VERSION} ${APP}
|
cd .. && mv ${APP} ${APP}-${VERSION} && tar -zcvf ${APP}_${VERSION}.orig.tar.gz ${APP}-${VERSION}/ && mv ${APP}-${VERSION} ${APP}
|
||||||
clean:
|
clean:
|
||||||
rm -f *.*~ *~
|
rm -f *.*~ *~ *.dot
|
||||||
rm -f orgs/*~
|
rm -f orgs/*~
|
||||||
rm -f website/EN/*~
|
rm -f website/EN/*~
|
||||||
rm -f gemini/EN/*~
|
rm -f gemini/EN/*~
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -26,7 +26,7 @@ On Arch/Parabola:
|
||||||
sudo pacman -S tor python-pip python-pysocks python-pycryptodome \
|
sudo pacman -S tor python-pip python-pysocks python-pycryptodome \
|
||||||
imagemagick python-pillow python-requests \
|
imagemagick python-pillow python-requests \
|
||||||
perl-image-exiftool python-numpy python-dateutil \
|
perl-image-exiftool python-numpy python-dateutil \
|
||||||
certbot flake8
|
certbot flake8 bandit
|
||||||
sudo pip3 install pyLD pyqrcode pypng
|
sudo pip3 install pyLD pyqrcode pypng
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -41,7 +41,8 @@ sudo apt install -y \
|
||||||
python3-idna python3-requests \
|
python3-idna python3-requests \
|
||||||
python3-pyld python3-django-timezone-field \
|
python3-pyld python3-django-timezone-field \
|
||||||
libimage-exiftool-perl python3-flake8 \
|
libimage-exiftool-perl python3-flake8 \
|
||||||
python3-pyqrcode python3-png certbot nginx
|
python3-pyqrcode python3-png python3-bandit \
|
||||||
|
certbot nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
@ -200,6 +201,16 @@ Static analysis can be run with:
|
||||||
./static_analysis
|
./static_analysis
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Running a security audit
|
||||||
|
|
||||||
|
To run a security audit:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
./security_audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that not all of the issues identified will necessarily be relevant to this project.
|
||||||
|
|
||||||
## Installing on Onion or i2p domains
|
## Installing on Onion or i2p domains
|
||||||
|
|
||||||
If you don't have access to the clearnet, or prefer not to use it, then it's possible to run an Epicyon instance easily from your laptop. There are scripts within the ```deploy``` directory which can be used to install an instance on a Debian or Arch/Parabola operating system. With some modification of package names they could be also used with other distros.
|
If you don't have access to the clearnet, or prefer not to use it, then it's possible to run an Epicyon instance easily from your laptop. There are scripts within the ```deploy``` directory which can be used to install an instance on a Debian or Arch/Parabola operating system. With some modification of package names they could be also used with other distros.
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,23 @@ If you want to view the raw json:
|
||||||
python3 epicyon.py --postsraw nickname@domain
|
python3 epicyon.py --postsraw nickname@domain
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Listing referenced domains
|
||||||
|
|
||||||
|
To list the domains referenced in public posts:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 epicyon.py --postDomains nickname@domain
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plotting federated instances
|
||||||
|
|
||||||
|
To plot a set of federated instances, based upon a sample of handles on those instances:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
python3 epicyon.py --socnet nickname1@domain1,nickname2@domain2,nickname3@domain3
|
||||||
|
xdot socnet.dot
|
||||||
|
```
|
||||||
|
|
||||||
## Delete posts
|
## Delete posts
|
||||||
|
|
||||||
To delete a post which you wrote you must first know its url. It is usually something like:
|
To delete a post which you wrote you must first know its url. It is usually something like:
|
||||||
|
|
|
||||||
4
auth.py
4
auth.py
|
|
@ -10,7 +10,7 @@ import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import binascii
|
import binascii
|
||||||
import os
|
import os
|
||||||
import random
|
import secrets
|
||||||
|
|
||||||
|
|
||||||
def hashPassword(password: str) -> str:
|
def hashPassword(password: str) -> str:
|
||||||
|
|
@ -162,4 +162,4 @@ def authorize(baseDir: str, path: str, authHeader: str, debug: bool) -> bool:
|
||||||
def createPassword(length=10):
|
def createPassword(length=10):
|
||||||
validChars = 'abcdefghijklmnopqrstuvwxyz' + \
|
validChars = 'abcdefghijklmnopqrstuvwxyz' + \
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||||
return ''.join((random.choice(validChars) for i in range(length)))
|
return ''.join((secrets.choice(validChars) for i in range(length)))
|
||||||
|
|
|
||||||
44
content.py
44
content.py
|
|
@ -14,6 +14,32 @@ from utils import fileLastModified
|
||||||
from utils import getLinkPrefixes
|
from utils import getLinkPrefixes
|
||||||
|
|
||||||
|
|
||||||
|
def dangerousMarkup(content: str) -> bool:
|
||||||
|
"""Returns true if the given content contains dangerous html markup
|
||||||
|
"""
|
||||||
|
if '<' not in content:
|
||||||
|
return False
|
||||||
|
if '>' not in content:
|
||||||
|
return False
|
||||||
|
contentSections = content.split('<')
|
||||||
|
invalidStrings = ('script', 'canvas', 'style', 'abbr',
|
||||||
|
'frame', 'iframe', 'html', 'body',
|
||||||
|
'hr')
|
||||||
|
for markup in contentSections:
|
||||||
|
if '>' not in markup:
|
||||||
|
continue
|
||||||
|
markup = markup.split('>')[0].strip()
|
||||||
|
if ' ' not in markup:
|
||||||
|
for badStr in invalidStrings:
|
||||||
|
if badStr in markup:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
for badStr in invalidStrings:
|
||||||
|
if badStr + ' ' in markup:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def switchWords(baseDir: str, nickname: str, domain: str, content: str) -> str:
|
def switchWords(baseDir: str, nickname: str, domain: str, content: str) -> str:
|
||||||
"""Performs word replacements. eg. Trump -> The Orange Menace
|
"""Performs word replacements. eg. Trump -> The Orange Menace
|
||||||
"""
|
"""
|
||||||
|
|
@ -400,6 +426,24 @@ def removeTextFormatting(content: str) -> str:
|
||||||
return content
|
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
|
||||||
|
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,
|
def removeLongWords(content: str, maxWordLength: int,
|
||||||
longWordsList: []) -> str:
|
longWordsList: []) -> str:
|
||||||
"""Breaks up long words so that on mobile screens this doesn't
|
"""Breaks up long words so that on mobile screens this doesn't
|
||||||
|
|
|
||||||
32
daemon.py
32
daemon.py
|
|
@ -30,7 +30,9 @@ from metadata import metaDataNodeInfo
|
||||||
from pgp import getEmailAddress
|
from pgp import getEmailAddress
|
||||||
from pgp import setEmailAddress
|
from pgp import setEmailAddress
|
||||||
from pgp import getPGPpubKey
|
from pgp import getPGPpubKey
|
||||||
|
from pgp import getPGPfingerprint
|
||||||
from pgp import setPGPpubKey
|
from pgp import setPGPpubKey
|
||||||
|
from pgp import setPGPfingerprint
|
||||||
from xmpp import getXmppAddress
|
from xmpp import getXmppAddress
|
||||||
from xmpp import setXmppAddress
|
from xmpp import setXmppAddress
|
||||||
from ssb import getSSBAddress
|
from ssb import getSSBAddress
|
||||||
|
|
@ -177,6 +179,8 @@ from cache import getPersonFromCache
|
||||||
from httpsig import verifyPostHeaders
|
from httpsig import verifyPostHeaders
|
||||||
from theme import setTheme
|
from theme import setTheme
|
||||||
from theme import getTheme
|
from theme import getTheme
|
||||||
|
from theme import enableGrayscale
|
||||||
|
from theme import disableGrayscale
|
||||||
from schedule import runPostSchedule
|
from schedule import runPostSchedule
|
||||||
from schedule import runPostScheduleWatchdog
|
from schedule import runPostScheduleWatchdog
|
||||||
from schedule import removeScheduledPosts
|
from schedule import removeScheduledPosts
|
||||||
|
|
@ -533,7 +537,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
except BaseException:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
if not etag:
|
if not etag:
|
||||||
etag = sha1(data).hexdigest()
|
etag = sha1(data).hexdigest() # nosec
|
||||||
try:
|
try:
|
||||||
with open(mediaFilename + '.etag', 'w') as etagFile:
|
with open(mediaFilename + '.etag', 'w') as etagFile:
|
||||||
etagFile.write(etag)
|
etagFile.write(etag)
|
||||||
|
|
@ -1549,6 +1553,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
optionsLink = optionsList[3]
|
optionsLink = optionsList[3]
|
||||||
donateUrl = None
|
donateUrl = None
|
||||||
PGPpubKey = None
|
PGPpubKey = None
|
||||||
|
PGPfingerprint = None
|
||||||
xmppAddress = None
|
xmppAddress = None
|
||||||
matrixAddress = None
|
matrixAddress = None
|
||||||
blogAddress = None
|
blogAddress = None
|
||||||
|
|
@ -1567,6 +1572,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
toxAddress = getToxAddress(actorJson)
|
toxAddress = getToxAddress(actorJson)
|
||||||
emailAddress = getEmailAddress(actorJson)
|
emailAddress = getEmailAddress(actorJson)
|
||||||
PGPpubKey = getPGPpubKey(actorJson)
|
PGPpubKey = getPGPpubKey(actorJson)
|
||||||
|
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||||
msg = htmlPersonOptions(self.server.translate,
|
msg = htmlPersonOptions(self.server.translate,
|
||||||
self.server.baseDir,
|
self.server.baseDir,
|
||||||
self.server.domain,
|
self.server.domain,
|
||||||
|
|
@ -1577,7 +1583,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
pageNumber, donateUrl,
|
pageNumber, donateUrl,
|
||||||
xmppAddress, matrixAddress,
|
xmppAddress, matrixAddress,
|
||||||
ssbAddress, blogAddress,
|
ssbAddress, blogAddress,
|
||||||
toxAddress, PGPpubKey,
|
toxAddress,
|
||||||
|
PGPpubKey, PGPfingerprint,
|
||||||
emailAddress).encode('utf-8')
|
emailAddress).encode('utf-8')
|
||||||
self._set_headers('text/html', len(msg),
|
self._set_headers('text/html', len(msg),
|
||||||
cookie, callingDomain)
|
cookie, callingDomain)
|
||||||
|
|
@ -5093,7 +5100,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
else:
|
else:
|
||||||
with open(mediaFilename, 'rb') as avFile:
|
with open(mediaFilename, 'rb') as avFile:
|
||||||
mediaBinary = avFile.read()
|
mediaBinary = avFile.read()
|
||||||
etag = sha1(mediaBinary).hexdigest()
|
etag = sha1(mediaBinary).hexdigest() # nosec
|
||||||
try:
|
try:
|
||||||
with open(mediaTagFilename, 'w') as etagFile:
|
with open(mediaTagFilename, 'w') as etagFile:
|
||||||
etagFile.write(etag)
|
etagFile.write(etag)
|
||||||
|
|
@ -6242,6 +6249,17 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
setPGPpubKey(actorJson, '')
|
setPGPpubKey(actorJson, '')
|
||||||
actorChanged = True
|
actorChanged = True
|
||||||
|
|
||||||
|
currentPGPfingerprint = getPGPfingerprint(actorJson)
|
||||||
|
if fields.get('openpgp'):
|
||||||
|
if fields['openpgp'] != currentPGPfingerprint:
|
||||||
|
setPGPfingerprint(actorJson,
|
||||||
|
fields['openpgp'])
|
||||||
|
actorChanged = True
|
||||||
|
else:
|
||||||
|
if currentPGPfingerprint:
|
||||||
|
setPGPfingerprint(actorJson, '')
|
||||||
|
actorChanged = True
|
||||||
|
|
||||||
currentDonateUrl = getDonationUrl(actorJson)
|
currentDonateUrl = getDonationUrl(actorJson)
|
||||||
if fields.get('donateUrl'):
|
if fields.get('donateUrl'):
|
||||||
if fields['donateUrl'] != currentDonateUrl:
|
if fields['donateUrl'] != currentDonateUrl:
|
||||||
|
|
@ -6478,6 +6496,14 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if actorJson['type'] != 'Person':
|
if actorJson['type'] != 'Person':
|
||||||
actorJson['type'] = 'Person'
|
actorJson['type'] = 'Person'
|
||||||
actorChanged = True
|
actorChanged = True
|
||||||
|
grayscale = False
|
||||||
|
if fields.get('grayscale'):
|
||||||
|
if fields['grayscale'] == 'on':
|
||||||
|
grayscale = True
|
||||||
|
if grayscale:
|
||||||
|
enableGrayscale(self.server.baseDir)
|
||||||
|
else:
|
||||||
|
disableGrayscale(self.server.baseDir)
|
||||||
# save filtered words list
|
# save filtered words list
|
||||||
filterFilename = \
|
filterFilename = \
|
||||||
self.server.baseDir + '/accounts/' + \
|
self.server.baseDir + '/accounts/' + \
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ if [ -f /usr/bin/pacman ]; then
|
||||||
imagemagick python-pillow python-requests \
|
imagemagick python-pillow python-requests \
|
||||||
perl-image-exiftool python-numpy python-dateutil \
|
perl-image-exiftool python-numpy python-dateutil \
|
||||||
certbot flake8 git i2pd wget qrencode \
|
certbot flake8 git i2pd wget qrencode \
|
||||||
proxychains midori
|
proxychains midori bandit
|
||||||
pip3 install pyLD pyqrcode pypng
|
pip3 install pyLD pyqrcode pypng
|
||||||
else
|
else
|
||||||
apt-get update
|
apt-get update
|
||||||
|
|
@ -75,7 +75,7 @@ else
|
||||||
libimage-exiftool-perl python3-flake8 python3-pyld \
|
libimage-exiftool-perl python3-flake8 python3-pyld \
|
||||||
python3-django-timezone-field nginx git i2pd wget \
|
python3-django-timezone-field nginx git i2pd wget \
|
||||||
python3-pyqrcode qrencode python3-png \
|
python3-pyqrcode qrencode python3-png \
|
||||||
proxychains midori
|
proxychains midori python3-bandit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d /etc/i2pd ]; then
|
if [ ! -d /etc/i2pd ]; then
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ if [ -f /usr/bin/pacman ]; then
|
||||||
pacman -S --noconfirm tor python-pip python-pysocks python-pycryptodome \
|
pacman -S --noconfirm tor python-pip python-pysocks python-pycryptodome \
|
||||||
imagemagick python-pillow python-requests \
|
imagemagick python-pillow python-requests \
|
||||||
perl-image-exiftool python-numpy python-dateutil \
|
perl-image-exiftool python-numpy python-dateutil \
|
||||||
certbot flake8 git qrencode
|
certbot flake8 git qrencode bandit
|
||||||
pip3 install pyLD pyqrcode pypng
|
pip3 install pyLD pyqrcode pypng
|
||||||
else
|
else
|
||||||
apt-get update
|
apt-get update
|
||||||
|
|
@ -48,7 +48,7 @@ else
|
||||||
python3-setuptools python3-socks python3-idna \
|
python3-setuptools python3-socks python3-idna \
|
||||||
libimage-exiftool-perl python3-flake8 python3-pyld \
|
libimage-exiftool-perl python3-flake8 python3-pyld \
|
||||||
python3-django-timezone-field tor nginx git qrencode \
|
python3-django-timezone-field tor nginx git qrencode \
|
||||||
python3-pyqrcode python3-png
|
python3-pyqrcode python3-png python3-bandit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo 'Cloning the epicyon repo'
|
echo 'Cloning the epicyon repo'
|
||||||
|
|
|
||||||
10
donate.py
10
donate.py
|
|
@ -41,6 +41,14 @@ def getDonationUrl(actorJson: {}) -> str:
|
||||||
def setDonationUrl(actorJson: {}, donateUrl: str) -> None:
|
def setDonationUrl(actorJson: {}, donateUrl: str) -> None:
|
||||||
"""Sets a link used for donations
|
"""Sets a link used for donations
|
||||||
"""
|
"""
|
||||||
|
notUrl = False
|
||||||
|
if '.' not in donateUrl:
|
||||||
|
notUrl = True
|
||||||
|
if '://' not in donateUrl:
|
||||||
|
notUrl = True
|
||||||
|
if ' ' in donateUrl:
|
||||||
|
notUrl = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -65,6 +73,8 @@ def setDonationUrl(actorJson: {}, donateUrl: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyFound)
|
actorJson['attachment'].remove(propertyFound)
|
||||||
|
if notUrl:
|
||||||
|
return
|
||||||
|
|
||||||
donateValue = \
|
donateValue = \
|
||||||
'<a href="' + donateUrl + \
|
'<a href="' + donateUrl + \
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,21 @@ function notifications {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# send notifications for likes to XMPP/email users
|
||||||
|
epicyonLikeFile="$epicyonDir/.newLike"
|
||||||
|
if [ -f "$epicyonLikeFile" ]; then
|
||||||
|
if ! grep -q "##sent##" "$epicyonLikeFile"; then
|
||||||
|
epicyonLikeMessage=$(notification_translate_text 'liked your post')
|
||||||
|
epicyonLikeFileContent=$(cat "$epicyonLikeFile" | awk -F ' ' '{print $1}')" "$(echo "$epicyonLikeMessage")" "$(cat "$epicyonLikeFile" | awk -F ' ' '{print $2}')
|
||||||
|
if [[ "$epicyonLikeFileContent" == *':'* ]]; then
|
||||||
|
epicyonLikeMessage="Epicyon: $epicyonLikeFileContent"
|
||||||
|
fi
|
||||||
|
"${PROJECT_NAME}-notification" -u "$USERNAME" -s "Epicyon" -m "$epicyonLikeMessage" --sensitive yes
|
||||||
|
echo "##sent##" > "$epicyonLikeFile"
|
||||||
|
chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonLkeFile"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# send notifications for replies to XMPP/email users
|
# send notifications for replies to XMPP/email users
|
||||||
epicyonReplyFile="$epicyonDir/.newReply"
|
epicyonReplyFile="$epicyonDir/.newReply"
|
||||||
if [ -f "$epicyonReplyFile" ]; then
|
if [ -f "$epicyonReplyFile" ]; then
|
||||||
|
|
|
||||||
79
epicyon.py
79
epicyon.py
|
|
@ -15,6 +15,7 @@ from person import deactivateAccount
|
||||||
from skills import setSkillLevel
|
from skills import setSkillLevel
|
||||||
from roles import setRole
|
from roles import setRole
|
||||||
from webfinger import webfingerHandle
|
from webfinger import webfingerHandle
|
||||||
|
from posts import getPublicPostDomains
|
||||||
from posts import sendBlockViaServer
|
from posts import sendBlockViaServer
|
||||||
from posts import sendUndoBlockViaServer
|
from posts import sendUndoBlockViaServer
|
||||||
from posts import createPublicPost
|
from posts import createPublicPost
|
||||||
|
|
@ -66,6 +67,7 @@ from shares import sendUndoShareViaServer
|
||||||
from shares import addShare
|
from shares import addShare
|
||||||
from theme import setTheme
|
from theme import setTheme
|
||||||
from announce import sendAnnounceViaServer
|
from announce import sendAnnounceViaServer
|
||||||
|
from socnet import instancesGraph
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -146,6 +148,14 @@ parser.add_argument('--actor', dest='actor', type=str,
|
||||||
parser.add_argument('--posts', dest='posts', type=str,
|
parser.add_argument('--posts', dest='posts', type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Show posts for the given handle')
|
help='Show posts for the given handle')
|
||||||
|
parser.add_argument('--postDomains', dest='postDomains', type=str,
|
||||||
|
default=None,
|
||||||
|
help='Show domains referenced in public '
|
||||||
|
'posts for the given handle')
|
||||||
|
parser.add_argument('--socnet', dest='socnet', type=str,
|
||||||
|
default=None,
|
||||||
|
help='Show dot diagram for social network '
|
||||||
|
'of federated instances')
|
||||||
parser.add_argument('--postsraw', dest='postsraw', type=str,
|
parser.add_argument('--postsraw', dest='postsraw', type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Show raw json of posts for the given handle')
|
help='Show raw json of posts for the given handle')
|
||||||
|
|
@ -386,6 +396,14 @@ if baseDir.endswith('/'):
|
||||||
|
|
||||||
if args.posts:
|
if args.posts:
|
||||||
if '@' not in args.posts:
|
if '@' not in args.posts:
|
||||||
|
if '/users/' in args.posts:
|
||||||
|
postsNickname = getNicknameFromActor(args.posts)
|
||||||
|
postsDomain, postsPort = getDomainFromActor(args.posts)
|
||||||
|
args.posts = postsNickname + '@' + postsDomain
|
||||||
|
if postsPort:
|
||||||
|
if postsPort != 80 and postsPort != 443:
|
||||||
|
args.posts += ':' + str(postsPort)
|
||||||
|
else:
|
||||||
print('Syntax: --posts nickname@domain')
|
print('Syntax: --posts nickname@domain')
|
||||||
sys.exit()
|
sys.exit()
|
||||||
if not args.http:
|
if not args.http:
|
||||||
|
|
@ -395,8 +413,12 @@ if args.posts:
|
||||||
proxyType = None
|
proxyType = None
|
||||||
if args.tor or domain.endswith('.onion'):
|
if args.tor or domain.endswith('.onion'):
|
||||||
proxyType = 'tor'
|
proxyType = 'tor'
|
||||||
|
if domain.endswith('.onion'):
|
||||||
|
args.port = 80
|
||||||
elif args.i2p or domain.endswith('.i2p'):
|
elif args.i2p or domain.endswith('.i2p'):
|
||||||
proxyType = 'i2p'
|
proxyType = 'i2p'
|
||||||
|
if domain.endswith('.i2p'):
|
||||||
|
args.port = 80
|
||||||
elif args.gnunet:
|
elif args.gnunet:
|
||||||
proxyType = 'gnunet'
|
proxyType = 'gnunet'
|
||||||
getPublicPostsOfPerson(baseDir, nickname, domain, False, True,
|
getPublicPostsOfPerson(baseDir, nickname, domain, False, True,
|
||||||
|
|
@ -404,6 +426,63 @@ if args.posts:
|
||||||
__version__)
|
__version__)
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
|
if args.postDomains:
|
||||||
|
if '@' not in args.postDomains:
|
||||||
|
if '/users/' in args.postDomains:
|
||||||
|
postsNickname = getNicknameFromActor(args.posts)
|
||||||
|
postsDomain, postsPort = getDomainFromActor(args.posts)
|
||||||
|
args.postDomains = postsNickname + '@' + postsDomain
|
||||||
|
if postsPort:
|
||||||
|
if postsPort != 80 and postsPort != 443:
|
||||||
|
args.postDomains += ':' + str(postsPort)
|
||||||
|
else:
|
||||||
|
print('Syntax: --postDomains nickname@domain')
|
||||||
|
sys.exit()
|
||||||
|
if not args.http:
|
||||||
|
args.port = 443
|
||||||
|
nickname = args.postDomains.split('@')[0]
|
||||||
|
domain = args.postDomains.split('@')[1]
|
||||||
|
proxyType = None
|
||||||
|
if args.tor or domain.endswith('.onion'):
|
||||||
|
proxyType = 'tor'
|
||||||
|
if domain.endswith('.onion'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.i2p or domain.endswith('.i2p'):
|
||||||
|
proxyType = 'i2p'
|
||||||
|
if domain.endswith('.i2p'):
|
||||||
|
args.port = 80
|
||||||
|
elif args.gnunet:
|
||||||
|
proxyType = 'gnunet'
|
||||||
|
domainList = []
|
||||||
|
domainList = getPublicPostDomains(baseDir, nickname, domain,
|
||||||
|
proxyType, args.port,
|
||||||
|
httpPrefix, debug,
|
||||||
|
__version__, domainList)
|
||||||
|
for postDomain in domainList:
|
||||||
|
print(postDomain)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if args.socnet:
|
||||||
|
if ',' not in args.socnet:
|
||||||
|
print('Syntax: '
|
||||||
|
'--socnet nick1@domain1,nick2@domain2,nick3@domain3')
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
if not args.http:
|
||||||
|
args.port = 443
|
||||||
|
proxyType = 'tor'
|
||||||
|
dotGraph = instancesGraph(baseDir, args.socnet,
|
||||||
|
proxyType, args.port,
|
||||||
|
httpPrefix, debug,
|
||||||
|
__version__)
|
||||||
|
try:
|
||||||
|
with open('socnet.dot', 'w') as fp:
|
||||||
|
fp.write(dotGraph)
|
||||||
|
print('Saved to socnet.dot')
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
if args.postsraw:
|
if args.postsraw:
|
||||||
if '@' not in args.postsraw:
|
if '@' not in args.postsraw:
|
||||||
print('Syntax: --postsraw nickname@domain')
|
print('Syntax: --postsraw nickname@domain')
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ You will need python version 3.7 or later.
|
||||||
|
|
||||||
On a Debian based system:
|
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-pyld python3-django-timezone-field python3-pyqrcode python3-png 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-pyld python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
63
inbox.py
63
inbox.py
|
|
@ -63,6 +63,7 @@ from media import replaceYouTube
|
||||||
from git import isGitPatch
|
from git import isGitPatch
|
||||||
from git import receiveGitPatch
|
from git import receiveGitPatch
|
||||||
from followingCalendar import receivingCalendarEvents
|
from followingCalendar import receivingCalendarEvents
|
||||||
|
from content import dangerousMarkup
|
||||||
|
|
||||||
|
|
||||||
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
||||||
|
|
@ -981,6 +982,7 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str,
|
||||||
def receiveLike(recentPostsCache: {},
|
def receiveLike(recentPostsCache: {},
|
||||||
session, handle: str, isGroup: bool, baseDir: str,
|
session, handle: str, isGroup: bool, baseDir: str,
|
||||||
httpPrefix: str, domain: str, port: int,
|
httpPrefix: str, domain: str, port: int,
|
||||||
|
onionDomain: str,
|
||||||
sendThreads: [], postLog: [], cachedWebfingers: {},
|
sendThreads: [], postLog: [], cachedWebfingers: {},
|
||||||
personCache: {}, messageJson: {}, federationList: [],
|
personCache: {}, messageJson: {}, federationList: [],
|
||||||
debug: bool) -> bool:
|
debug: bool) -> bool:
|
||||||
|
|
@ -1033,6 +1035,8 @@ def receiveLike(recentPostsCache: {},
|
||||||
updateLikesCollection(recentPostsCache, baseDir, postFilename,
|
updateLikesCollection(recentPostsCache, baseDir, postFilename,
|
||||||
messageJson['object'],
|
messageJson['object'],
|
||||||
messageJson['actor'], domain, debug)
|
messageJson['actor'], domain, debug)
|
||||||
|
likeNotify(baseDir, domain, onionDomain, handle,
|
||||||
|
messageJson['actor'], messageJson['object'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1596,22 +1600,20 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
|
||||||
return False
|
return False
|
||||||
if 'Z' not in messageJson['object']['published']:
|
if 'Z' not in messageJson['object']['published']:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if isGitPatch(baseDir, nickname, domain,
|
if isGitPatch(baseDir, nickname, domain,
|
||||||
messageJson['object']['type'],
|
messageJson['object']['type'],
|
||||||
messageJson['object']['summary'],
|
messageJson['object']['summary'],
|
||||||
messageJson['object']['content']):
|
messageJson['object']['content']):
|
||||||
return True
|
return True
|
||||||
# check for bad html
|
|
||||||
invalidStrings = ('<script>', '</script>', '</canvas>',
|
if dangerousMarkup(messageJson['object']['content']):
|
||||||
'</style>', '</abbr>',
|
|
||||||
'</html>', '</body>', '<br>', '<hr>')
|
|
||||||
for badStr in invalidStrings:
|
|
||||||
if badStr in messageJson['object']['content']:
|
|
||||||
if messageJson['object'].get('id'):
|
if messageJson['object'].get('id'):
|
||||||
print('REJECT ARBITRARY HTML: ' + messageJson['object']['id'])
|
print('REJECT ARBITRARY HTML: ' + messageJson['object']['id'])
|
||||||
print('REJECT ARBITRARY HTML: bad string in post - ' +
|
print('REJECT ARBITRARY HTML: bad string in post - ' +
|
||||||
messageJson['object']['content'])
|
messageJson['object']['content'])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check (rough) number of mentions
|
# check (rough) number of mentions
|
||||||
mentionsEst = estimateNumberOfMentions(messageJson['object']['content'])
|
mentionsEst = estimateNumberOfMentions(messageJson['object']['content'])
|
||||||
if mentionsEst > maxMentions:
|
if mentionsEst > maxMentions:
|
||||||
|
|
@ -1704,6 +1706,54 @@ def dmNotify(baseDir: str, handle: str, url: str) -> None:
|
||||||
fp.write(url)
|
fp.write(url)
|
||||||
|
|
||||||
|
|
||||||
|
def likeNotify(baseDir: str, domain: str, onionDomain: str,
|
||||||
|
handle: str, actor: str, url: str) -> None:
|
||||||
|
"""Creates a notification that a like has arrived
|
||||||
|
"""
|
||||||
|
# This is not you liking your own post
|
||||||
|
if actor in url:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check that the liked post was by this handle
|
||||||
|
nickname = handle.split('@')[0]
|
||||||
|
if '/' + domain + '/users/' + nickname not in url:
|
||||||
|
if not onionDomain:
|
||||||
|
return
|
||||||
|
if '/' + onionDomain + '/users/' + nickname not in url:
|
||||||
|
return
|
||||||
|
|
||||||
|
accountDir = baseDir + '/accounts/' + handle
|
||||||
|
if not os.path.isdir(accountDir):
|
||||||
|
return
|
||||||
|
likeFile = accountDir + '/.newLike'
|
||||||
|
if os.path.isfile(likeFile):
|
||||||
|
if '##sent##' not in open(likeFile).read():
|
||||||
|
return
|
||||||
|
|
||||||
|
likerNickname = getNicknameFromActor(actor)
|
||||||
|
likerDomain, likerPort = getDomainFromActor(actor)
|
||||||
|
if likerNickname and likerDomain:
|
||||||
|
likerHandle = likerNickname + '@' + likerDomain
|
||||||
|
else:
|
||||||
|
print('likeNotify likerHandle: ' +
|
||||||
|
str(likerNickname) + '@' + str(likerDomain))
|
||||||
|
likerHandle = actor
|
||||||
|
if likerHandle != handle:
|
||||||
|
likeStr = likerHandle + ' ' + url
|
||||||
|
prevLikeFile = accountDir + '/.prevLike'
|
||||||
|
# was there a previous like notification?
|
||||||
|
if os.path.isfile(prevLikeFile):
|
||||||
|
# is it the same as the current notification ?
|
||||||
|
with open(prevLikeFile, 'r') as likeFile:
|
||||||
|
prevLikeStr = likeFile.read()
|
||||||
|
if prevLikeStr == likeStr:
|
||||||
|
return
|
||||||
|
with open(prevLikeFile, 'w') as fp:
|
||||||
|
fp.write(likeStr)
|
||||||
|
with open(likeFile, 'w') as fp:
|
||||||
|
fp.write(likeStr)
|
||||||
|
|
||||||
|
|
||||||
def replyNotify(baseDir: str, handle: str, url: str) -> None:
|
def replyNotify(baseDir: str, handle: str, url: str) -> None:
|
||||||
"""Creates a notification that a new reply has arrived
|
"""Creates a notification that a new reply has arrived
|
||||||
"""
|
"""
|
||||||
|
|
@ -1970,6 +2020,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
session, handle, isGroup,
|
session, handle, isGroup,
|
||||||
baseDir, httpPrefix,
|
baseDir, httpPrefix,
|
||||||
domain, port,
|
domain, port,
|
||||||
|
onionDomain,
|
||||||
sendThreads, postLog,
|
sendThreads, postLog,
|
||||||
cachedWebfingers,
|
cachedWebfingers,
|
||||||
personCache,
|
personCache,
|
||||||
|
|
|
||||||
9
media.py
9
media.py
|
|
@ -38,12 +38,15 @@ def removeMetaData(imageFilename: str, outputFilename: str) -> None:
|
||||||
so better to use a dedicated tool if one is installed
|
so better to use a dedicated tool if one is installed
|
||||||
"""
|
"""
|
||||||
copyfile(imageFilename, outputFilename)
|
copyfile(imageFilename, outputFilename)
|
||||||
|
if not os.path.isfile(outputFilename):
|
||||||
|
print('ERROR: unable to remove metadata from ' + imageFilename)
|
||||||
|
return
|
||||||
if os.path.isfile('/usr/bin/exiftool'):
|
if os.path.isfile('/usr/bin/exiftool'):
|
||||||
print('Removing metadata from ' + outputFilename + ' using exiftool')
|
print('Removing metadata from ' + outputFilename + ' using exiftool')
|
||||||
os.system('exiftool -all= ' + outputFilename)
|
os.system('exiftool -all= ' + outputFilename) # nosec
|
||||||
elif os.path.isfile('/usr/bin/mogrify'):
|
elif os.path.isfile('/usr/bin/mogrify'):
|
||||||
print('Removing metadata from ' + outputFilename + ' using mogrify')
|
print('Removing metadata from ' + outputFilename + ' using mogrify')
|
||||||
os.system('/usr/bin/mogrify -strip '+outputFilename)
|
os.system('/usr/bin/mogrify -strip ' + outputFilename) # nosec
|
||||||
|
|
||||||
|
|
||||||
def getImageHash(imageFilename: str) -> str:
|
def getImageHash(imageFilename: str) -> str:
|
||||||
|
|
@ -116,7 +119,7 @@ def updateEtag(mediaFilename: str) -> None:
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
# calculate hash
|
# calculate hash
|
||||||
etag = sha1(data).hexdigest()
|
etag = sha1(data).hexdigest() # nosec
|
||||||
# save the hash
|
# save the hash
|
||||||
try:
|
try:
|
||||||
with open(mediaFilename + '.etag', 'w') as etagFile:
|
with open(mediaFilename + '.etag', 'w') as etagFile:
|
||||||
|
|
|
||||||
17
person.py
17
person.py
|
|
@ -151,14 +151,16 @@ def randomizeActorImages(personJson: {}) -> None:
|
||||||
personId = personJson['id']
|
personId = personJson['id']
|
||||||
lastPartOfFilename = personJson['icon']['url'].split('/')[-1]
|
lastPartOfFilename = personJson['icon']['url'].split('/')[-1]
|
||||||
existingExtension = lastPartOfFilename.split('.')[1]
|
existingExtension = lastPartOfFilename.split('.')[1]
|
||||||
|
# NOTE: these files don't need to have cryptographically
|
||||||
|
# secure names
|
||||||
|
randStr = str(randint(10000000000000, 99999999999999)) # nosec
|
||||||
personJson['icon']['url'] = \
|
personJson['icon']['url'] = \
|
||||||
personId + '/avatar' + str(randint(10000000000000, 99999999999999)) + \
|
personId + '/avatar' + randStr + '.' + existingExtension
|
||||||
'.' + existingExtension
|
|
||||||
lastPartOfFilename = personJson['image']['url'].split('/')[-1]
|
lastPartOfFilename = personJson['image']['url'].split('/')[-1]
|
||||||
existingExtension = lastPartOfFilename.split('.')[1]
|
existingExtension = lastPartOfFilename.split('.')[1]
|
||||||
|
randStr = str(randint(10000000000000, 99999999999999)) # nosec
|
||||||
personJson['image']['url'] = \
|
personJson['image']['url'] = \
|
||||||
personId + '/image' + str(randint(10000000000000, 99999999999999)) + \
|
personId + '/image' + randStr + '.' + existingExtension
|
||||||
'.' + existingExtension
|
|
||||||
|
|
||||||
|
|
||||||
def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
|
|
@ -197,13 +199,16 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
approveFollowers = True
|
approveFollowers = True
|
||||||
personType = 'Application'
|
personType = 'Application'
|
||||||
|
|
||||||
|
# NOTE: these image files don't need to have
|
||||||
|
# cryptographically secure names
|
||||||
|
|
||||||
imageUrl = \
|
imageUrl = \
|
||||||
personId + '/image' + \
|
personId + '/image' + \
|
||||||
str(randint(10000000000000, 99999999999999)) + '.png'
|
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
||||||
|
|
||||||
iconUrl = \
|
iconUrl = \
|
||||||
personId + '/avatar' + \
|
personId + '/avatar' + \
|
||||||
str(randint(10000000000000, 99999999999999)) + '.png'
|
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
||||||
|
|
||||||
contextDict = {
|
contextDict = {
|
||||||
'Emoji': 'toot:Emoji',
|
'Emoji': 'toot:Emoji',
|
||||||
|
|
|
||||||
96
pgp.py
96
pgp.py
|
|
@ -53,9 +53,39 @@ def getPGPpubKey(actorJson: {}) -> str:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def getPGPfingerprint(actorJson: {}) -> str:
|
||||||
|
"""Returns PGP fingerprint for the given actor
|
||||||
|
"""
|
||||||
|
if not actorJson.get('attachment'):
|
||||||
|
return ''
|
||||||
|
for propertyValue in actorJson['attachment']:
|
||||||
|
if not propertyValue.get('name'):
|
||||||
|
continue
|
||||||
|
if not propertyValue['name'].lower().startswith('openpgp'):
|
||||||
|
continue
|
||||||
|
if not propertyValue.get('type'):
|
||||||
|
continue
|
||||||
|
if not propertyValue.get('value'):
|
||||||
|
continue
|
||||||
|
if propertyValue['type'] != 'PropertyValue':
|
||||||
|
continue
|
||||||
|
if len(propertyValue['value']) < 10:
|
||||||
|
continue
|
||||||
|
return propertyValue['value']
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
||||||
"""Sets the email address for the given actor
|
"""Sets the email address for the given actor
|
||||||
"""
|
"""
|
||||||
|
notEmailAddress = False
|
||||||
|
if '@' not in emailAddress:
|
||||||
|
notEmailAddress = True
|
||||||
|
if '.' not in emailAddress:
|
||||||
|
notEmailAddress = True
|
||||||
|
if emailAddress.startswith('@'):
|
||||||
|
notEmailAddress = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -72,12 +102,7 @@ def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyFound)
|
actorJson['attachment'].remove(propertyFound)
|
||||||
|
if notEmailAddress:
|
||||||
if '@' not in emailAddress:
|
|
||||||
return
|
|
||||||
if '.' not in emailAddress:
|
|
||||||
return
|
|
||||||
if emailAddress.startswith('@'):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for propertyValue in actorJson['attachment']:
|
for propertyValue in actorJson['attachment']:
|
||||||
|
|
@ -103,6 +128,13 @@ def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
||||||
def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
||||||
"""Sets a PGP public key for the given actor
|
"""Sets a PGP public key for the given actor
|
||||||
"""
|
"""
|
||||||
|
removeKey = False
|
||||||
|
if not PGPpubKey:
|
||||||
|
removeKey = True
|
||||||
|
else:
|
||||||
|
if '--BEGIN PGP PUBLIC KEY' not in PGPpubKey:
|
||||||
|
removeKey = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -119,8 +151,7 @@ def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyValue)
|
actorJson['attachment'].remove(propertyValue)
|
||||||
|
if removeKey:
|
||||||
if '--BEGIN PGP PUBLIC KEY' not in PGPpubKey:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for propertyValue in actorJson['attachment']:
|
for propertyValue in actorJson['attachment']:
|
||||||
|
|
@ -141,3 +172,52 @@ def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
||||||
"value": PGPpubKey
|
"value": PGPpubKey
|
||||||
}
|
}
|
||||||
actorJson['attachment'].append(newPGPpubKey)
|
actorJson['attachment'].append(newPGPpubKey)
|
||||||
|
|
||||||
|
|
||||||
|
def setPGPfingerprint(actorJson: {}, fingerprint: str) -> None:
|
||||||
|
"""Sets a PGP fingerprint for the given actor
|
||||||
|
"""
|
||||||
|
removeFingerprint = False
|
||||||
|
if not fingerprint:
|
||||||
|
removeFingerprint = True
|
||||||
|
else:
|
||||||
|
if len(fingerprint) < 10:
|
||||||
|
removeFingerprint = True
|
||||||
|
|
||||||
|
if not actorJson.get('attachment'):
|
||||||
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
# remove any existing value
|
||||||
|
propertyFound = None
|
||||||
|
for propertyValue in actorJson['attachment']:
|
||||||
|
if not propertyValue.get('name'):
|
||||||
|
continue
|
||||||
|
if not propertyValue.get('type'):
|
||||||
|
continue
|
||||||
|
if not propertyValue['name'].lower().startswith('openpgp'):
|
||||||
|
continue
|
||||||
|
propertyFound = propertyValue
|
||||||
|
break
|
||||||
|
if propertyFound:
|
||||||
|
actorJson['attachment'].remove(propertyValue)
|
||||||
|
if removeFingerprint:
|
||||||
|
return
|
||||||
|
|
||||||
|
for propertyValue in actorJson['attachment']:
|
||||||
|
if not propertyValue.get('name'):
|
||||||
|
continue
|
||||||
|
if not propertyValue.get('type'):
|
||||||
|
continue
|
||||||
|
if not propertyValue['name'].lower().startswith('openpgp'):
|
||||||
|
continue
|
||||||
|
if propertyValue['type'] != 'PropertyValue':
|
||||||
|
continue
|
||||||
|
propertyValue['value'] = fingerprint.strip()
|
||||||
|
return
|
||||||
|
|
||||||
|
newPGPfingerprint = {
|
||||||
|
"name": "OpenPGP",
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"value": fingerprint
|
||||||
|
}
|
||||||
|
actorJson['attachment'].append(newPGPfingerprint)
|
||||||
|
|
|
||||||
110
posts.py
110
posts.py
|
|
@ -146,11 +146,14 @@ def getUserUrl(wfRequest: {}) -> str:
|
||||||
|
|
||||||
def parseUserFeed(session, feedUrl: str, asHeader: {},
|
def parseUserFeed(session, feedUrl: str, asHeader: {},
|
||||||
projectVersion: str, httpPrefix: str,
|
projectVersion: str, httpPrefix: str,
|
||||||
domain: str) -> None:
|
domain: str, depth=0) -> {}:
|
||||||
|
if depth > 10:
|
||||||
|
return None
|
||||||
|
|
||||||
feedJson = getJson(session, feedUrl, asHeader, None,
|
feedJson = getJson(session, feedUrl, asHeader, None,
|
||||||
projectVersion, httpPrefix, domain)
|
projectVersion, httpPrefix, domain)
|
||||||
if not feedJson:
|
if not feedJson:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if 'orderedItems' in feedJson:
|
if 'orderedItems' in feedJson:
|
||||||
for item in feedJson['orderedItems']:
|
for item in feedJson['orderedItems']:
|
||||||
|
|
@ -168,7 +171,8 @@ def parseUserFeed(session, feedUrl: str, asHeader: {},
|
||||||
userFeed = \
|
userFeed = \
|
||||||
parseUserFeed(session, nextUrl, asHeader,
|
parseUserFeed(session, nextUrl, asHeader,
|
||||||
projectVersion, httpPrefix,
|
projectVersion, httpPrefix,
|
||||||
domain)
|
domain, depth+1)
|
||||||
|
if userFeed:
|
||||||
for item in userFeed:
|
for item in userFeed:
|
||||||
yield item
|
yield item
|
||||||
elif isinstance(nextUrl, dict):
|
elif isinstance(nextUrl, dict):
|
||||||
|
|
@ -440,6 +444,58 @@ def getPosts(session, outboxUrl: str, maxPosts: int,
|
||||||
return personPosts
|
return personPosts
|
||||||
|
|
||||||
|
|
||||||
|
def getPostDomains(session, outboxUrl: str, maxPosts: int,
|
||||||
|
maxMentions: int,
|
||||||
|
maxEmoji: int, maxAttachments: int,
|
||||||
|
federationList: [],
|
||||||
|
personCache: {},
|
||||||
|
debug: bool,
|
||||||
|
projectVersion: str, httpPrefix: str,
|
||||||
|
domain: str, domainList=[]) -> []:
|
||||||
|
"""Returns a list of domains referenced within public posts
|
||||||
|
"""
|
||||||
|
if not outboxUrl:
|
||||||
|
return []
|
||||||
|
profileStr = 'https://www.w3.org/ns/activitystreams'
|
||||||
|
asHeader = {
|
||||||
|
'Accept': 'application/activity+json; profile="' + profileStr + '"'
|
||||||
|
}
|
||||||
|
if '/outbox/' in outboxUrl:
|
||||||
|
asHeader = {
|
||||||
|
'Accept': 'application/ld+json; profile="' + profileStr + '"'
|
||||||
|
}
|
||||||
|
|
||||||
|
postDomains = domainList
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
userFeed = parseUserFeed(session, outboxUrl, asHeader,
|
||||||
|
projectVersion, httpPrefix, domain)
|
||||||
|
for item in userFeed:
|
||||||
|
i += 1
|
||||||
|
if i > maxPosts:
|
||||||
|
break
|
||||||
|
if not item.get('object'):
|
||||||
|
continue
|
||||||
|
if not isinstance(item['object'], dict):
|
||||||
|
continue
|
||||||
|
if item['object'].get('inReplyTo'):
|
||||||
|
postDomain, postPort = \
|
||||||
|
getDomainFromActor(item['object']['inReplyTo'])
|
||||||
|
if postDomain not in postDomains:
|
||||||
|
postDomains.append(postDomain)
|
||||||
|
|
||||||
|
if item['object'].get('tag'):
|
||||||
|
for tagItem in item['object']['tag']:
|
||||||
|
tagType = tagItem['type'].lower()
|
||||||
|
if tagType == 'mention':
|
||||||
|
if tagItem.get('href'):
|
||||||
|
postDomain, postPort = \
|
||||||
|
getDomainFromActor(tagItem['href'])
|
||||||
|
if postDomain not in postDomains:
|
||||||
|
postDomains.append(postDomain)
|
||||||
|
return postDomains
|
||||||
|
|
||||||
|
|
||||||
def deleteAllPosts(baseDir: str,
|
def deleteAllPosts(baseDir: str,
|
||||||
nickname: str, domain: str, boxname: str) -> None:
|
nickname: str, domain: str, boxname: str) -> None:
|
||||||
"""Deletes all posts for a person from inbox or outbox
|
"""Deletes all posts for a person from inbox or outbox
|
||||||
|
|
@ -2933,6 +2989,54 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
|
||||||
projectVersion, httpPrefix, domain)
|
projectVersion, httpPrefix, domain)
|
||||||
|
|
||||||
|
|
||||||
|
def getPublicPostDomains(baseDir: str, nickname: str, domain: str,
|
||||||
|
proxyType: str, port: int, httpPrefix: str,
|
||||||
|
debug: bool, projectVersion: str,
|
||||||
|
domainList=[]) -> []:
|
||||||
|
""" Returns a list of domains referenced within public posts
|
||||||
|
"""
|
||||||
|
session = createSession(proxyType)
|
||||||
|
if not session:
|
||||||
|
return domainList
|
||||||
|
personCache = {}
|
||||||
|
cachedWebfingers = {}
|
||||||
|
federationList = []
|
||||||
|
|
||||||
|
domainFull = domain
|
||||||
|
if port:
|
||||||
|
if port != 80 and port != 443:
|
||||||
|
if ':' not in domain:
|
||||||
|
domainFull = domain + ':' + str(port)
|
||||||
|
handle = httpPrefix + "://" + domainFull + "/@" + nickname
|
||||||
|
wfRequest = \
|
||||||
|
webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
|
||||||
|
domain, projectVersion)
|
||||||
|
if not wfRequest:
|
||||||
|
return domainList
|
||||||
|
if not isinstance(wfRequest, dict):
|
||||||
|
print('Webfinger for ' + handle + ' did not return a dict. ' +
|
||||||
|
str(wfRequest))
|
||||||
|
return domainList
|
||||||
|
|
||||||
|
(personUrl, pubKeyId, pubKey,
|
||||||
|
personId, shaedInbox,
|
||||||
|
capabilityAcquisition,
|
||||||
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
|
personCache,
|
||||||
|
projectVersion, httpPrefix,
|
||||||
|
nickname, domain, 'outbox')
|
||||||
|
maxMentions = 99
|
||||||
|
maxEmoji = 99
|
||||||
|
maxAttachments = 5
|
||||||
|
postDomains = \
|
||||||
|
getPostDomains(session, personUrl, 64, maxMentions, maxEmoji,
|
||||||
|
maxAttachments, federationList,
|
||||||
|
personCache, debug,
|
||||||
|
projectVersion, httpPrefix, domain, domainList)
|
||||||
|
postDomains.sort()
|
||||||
|
return postDomains
|
||||||
|
|
||||||
|
|
||||||
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str,
|
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str,
|
||||||
nickname: str, domain: str, port: int,
|
nickname: str, domain: str, port: int,
|
||||||
followerUrl, updateCaps: [],
|
followerUrl, updateCaps: [],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
bandit *.py -x tests.py
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
__filename__ = "socnet.py"
|
||||||
|
__author__ = "Bob Mottram"
|
||||||
|
__license__ = "AGPL3+"
|
||||||
|
__version__ = "1.1.0"
|
||||||
|
__maintainer__ = "Bob Mottram"
|
||||||
|
__email__ = "bob@freedombone.net"
|
||||||
|
__status__ = "Production"
|
||||||
|
|
||||||
|
from session import createSession
|
||||||
|
from webfinger import webfingerHandle
|
||||||
|
from posts import getPersonBox
|
||||||
|
from posts import getPostDomains
|
||||||
|
|
||||||
|
|
||||||
|
def instancesGraph(baseDir: str, handles: str,
|
||||||
|
proxyType: str,
|
||||||
|
port: int, httpPrefix: str,
|
||||||
|
debug: bool, projectVersion: str) -> str:
|
||||||
|
""" Returns a dot graph of federating instances
|
||||||
|
based upon a few sample handles.
|
||||||
|
The handles argument should contain a comma separated list
|
||||||
|
of handles on different instances
|
||||||
|
"""
|
||||||
|
dotGraphStr = 'digraph instances {\n'
|
||||||
|
if ',' not in handles:
|
||||||
|
return dotGraphStr + '}\n'
|
||||||
|
session = createSession(proxyType)
|
||||||
|
if not session:
|
||||||
|
return dotGraphStr + '}\n'
|
||||||
|
|
||||||
|
personCache = {}
|
||||||
|
cachedWebfingers = {}
|
||||||
|
federationList = []
|
||||||
|
maxMentions = 99
|
||||||
|
maxEmoji = 99
|
||||||
|
maxAttachments = 5
|
||||||
|
|
||||||
|
personHandles = handles.split(',')
|
||||||
|
for handle in personHandles:
|
||||||
|
handle = handle.strip()
|
||||||
|
if handle.startswith('@'):
|
||||||
|
handle = handle[1:]
|
||||||
|
if '@' not in handle:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nickname = handle.split('@')[0]
|
||||||
|
domain = handle.split('@')[1]
|
||||||
|
|
||||||
|
domainFull = domain
|
||||||
|
if port:
|
||||||
|
if port != 80 and port != 443:
|
||||||
|
if ':' not in domain:
|
||||||
|
domainFull = domain + ':' + str(port)
|
||||||
|
handle = httpPrefix + "://" + domainFull + "/@" + nickname
|
||||||
|
wfRequest = \
|
||||||
|
webfingerHandle(session, handle, httpPrefix,
|
||||||
|
cachedWebfingers,
|
||||||
|
domain, projectVersion)
|
||||||
|
if not wfRequest:
|
||||||
|
return dotGraphStr + '}\n'
|
||||||
|
if not isinstance(wfRequest, dict):
|
||||||
|
print('Webfinger for ' + handle + ' did not return a dict. ' +
|
||||||
|
str(wfRequest))
|
||||||
|
return dotGraphStr + '}\n'
|
||||||
|
|
||||||
|
(personUrl, pubKeyId, pubKey,
|
||||||
|
personId, shaedInbox,
|
||||||
|
capabilityAcquisition,
|
||||||
|
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
|
||||||
|
personCache,
|
||||||
|
projectVersion, httpPrefix,
|
||||||
|
nickname, domain, 'outbox')
|
||||||
|
postDomains = \
|
||||||
|
getPostDomains(session, personUrl, 64, maxMentions, maxEmoji,
|
||||||
|
maxAttachments, federationList,
|
||||||
|
personCache, debug,
|
||||||
|
projectVersion, httpPrefix, domain, [])
|
||||||
|
postDomains.sort()
|
||||||
|
for fedDomain in postDomains:
|
||||||
|
dotLineStr = ' "' + domain + '" -> "' + fedDomain + '";\n'
|
||||||
|
if dotLineStr not in dotGraphStr:
|
||||||
|
dotGraphStr += dotLineStr
|
||||||
|
return dotGraphStr + '}\n'
|
||||||
23
ssb.py
23
ssb.py
|
|
@ -41,6 +41,18 @@ def getSSBAddress(actorJson: {}) -> str:
|
||||||
def setSSBAddress(actorJson: {}, ssbAddress: str) -> None:
|
def setSSBAddress(actorJson: {}, ssbAddress: str) -> None:
|
||||||
"""Sets an ssb address for the given actor
|
"""Sets an ssb address for the given actor
|
||||||
"""
|
"""
|
||||||
|
notSSBAddress = False
|
||||||
|
if not ssbAddress.startswith('@'):
|
||||||
|
notSSBAddress = True
|
||||||
|
if '=.' not in ssbAddress:
|
||||||
|
notSSBAddress = True
|
||||||
|
if '"' in ssbAddress:
|
||||||
|
notSSBAddress = True
|
||||||
|
if ' ' in ssbAddress:
|
||||||
|
notSSBAddress = True
|
||||||
|
if ',' in ssbAddress:
|
||||||
|
notSSBAddress = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -57,16 +69,7 @@ def setSSBAddress(actorJson: {}, ssbAddress: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyFound)
|
actorJson['attachment'].remove(propertyFound)
|
||||||
|
if notSSBAddress:
|
||||||
if not ssbAddress.startswith('@'):
|
|
||||||
return
|
|
||||||
if '=.' not in ssbAddress:
|
|
||||||
return
|
|
||||||
if '"' in ssbAddress:
|
|
||||||
return
|
|
||||||
if ' ' in ssbAddress:
|
|
||||||
return
|
|
||||||
if ',' in ssbAddress:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for propertyValue in actorJson['attachment']:
|
for propertyValue in actorJson['attachment']:
|
||||||
|
|
|
||||||
43
tests.py
43
tests.py
|
|
@ -64,6 +64,8 @@ from media import getAttachmentMediaType
|
||||||
from delete import sendDeleteViaServer
|
from delete import sendDeleteViaServer
|
||||||
from inbox import validInbox
|
from inbox import validInbox
|
||||||
from inbox import validInboxFilenames
|
from inbox import validInboxFilenames
|
||||||
|
from content import dangerousMarkup
|
||||||
|
from content import removeHtml
|
||||||
from content import addWebLinks
|
from content import addWebLinks
|
||||||
from content import replaceEmojiFromTags
|
from content import replaceEmojiFromTags
|
||||||
from content import addHtmlTags
|
from content import addHtmlTags
|
||||||
|
|
@ -1873,8 +1875,49 @@ def testSiteIsActive():
|
||||||
assert(not siteIsActive('https://notarealwebsite.a.b.c'))
|
assert(not siteIsActive('https://notarealwebsite.a.b.c'))
|
||||||
|
|
||||||
|
|
||||||
|
def testRemoveHtml():
|
||||||
|
print('testRemoveHtml')
|
||||||
|
testStr = 'This string has no html.'
|
||||||
|
assert(removeHtml(testStr) == testStr)
|
||||||
|
testStr = 'This string <a href="1234.567">has html</a>.'
|
||||||
|
assert(removeHtml(testStr) == 'This string has html.')
|
||||||
|
|
||||||
|
|
||||||
|
def testDangerousMarkup():
|
||||||
|
print('testDangerousMarkup')
|
||||||
|
content = '<p>This is a valid message</p>'
|
||||||
|
assert(not dangerousMarkup(content))
|
||||||
|
content = 'This is a valid message without markup'
|
||||||
|
assert(not dangerousMarkup(content))
|
||||||
|
content = '<p>This is a valid-looking message. But wait... ' + \
|
||||||
|
'<script>document.getElementById("concentrated")' + \
|
||||||
|
'.innerHTML = "evil";</script></p>'
|
||||||
|
assert(dangerousMarkup(content))
|
||||||
|
content = '<p>This is a valid-looking message. But wait... ' + \
|
||||||
|
'<script src="https://evilsite/payload.js" /></p>'
|
||||||
|
assert(dangerousMarkup(content))
|
||||||
|
content = '<p>This message embeds an evil frame.' + \
|
||||||
|
'<iframe src="somesite"></iframe></p>'
|
||||||
|
assert(dangerousMarkup(content))
|
||||||
|
content = '<p>This message tries to obfuscate an evil frame.' + \
|
||||||
|
'< iframe src = "somesite"></ iframe ></p>'
|
||||||
|
assert(dangerousMarkup(content))
|
||||||
|
content = '<p>This message is not necessarily evil, but annoying.' + \
|
||||||
|
'<hr><br><br><br><br><br><br><br><hr><hr></p>'
|
||||||
|
assert(dangerousMarkup(content))
|
||||||
|
content = '<p>This message contans a ' + \
|
||||||
|
'<a href="https://validsite/index.html">valid link.</a></p>'
|
||||||
|
assert(not dangerousMarkup(content))
|
||||||
|
content = '<p>This message contans a ' + \
|
||||||
|
'<a href="https://validsite/iframe.html">' + \
|
||||||
|
'valid link having invalid but harmless name.</a></p>'
|
||||||
|
assert(not dangerousMarkup(content))
|
||||||
|
|
||||||
|
|
||||||
def runAllTests():
|
def runAllTests():
|
||||||
print('Running tests...')
|
print('Running tests...')
|
||||||
|
testDangerousMarkup()
|
||||||
|
testRemoveHtml()
|
||||||
testSiteIsActive()
|
testSiteIsActive()
|
||||||
testJsonld()
|
testJsonld()
|
||||||
testRemoveTextFormatting()
|
testRemoveTextFormatting()
|
||||||
|
|
|
||||||
64
theme.py
64
theme.py
|
|
@ -12,6 +12,11 @@ from utils import saveJson
|
||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
|
|
||||||
|
|
||||||
|
def getThemeFiles() -> []:
|
||||||
|
return ('epicyon.css', 'login.css', 'follow.css',
|
||||||
|
'suspended.css', 'calendar.css', 'blog.css')
|
||||||
|
|
||||||
|
|
||||||
def getThemesList() -> []:
|
def getThemesList() -> []:
|
||||||
"""Returns the list of available themes
|
"""Returns the list of available themes
|
||||||
Note that these should be capitalized, since they're
|
Note that these should be capitalized, since they're
|
||||||
|
|
@ -44,8 +49,7 @@ def getTheme(baseDir: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def removeTheme(baseDir: str):
|
def removeTheme(baseDir: str):
|
||||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
themeFiles = getThemeFiles()
|
||||||
'suspended.css', 'calendar.css', 'blog.css')
|
|
||||||
for filename in themeFiles:
|
for filename in themeFiles:
|
||||||
if os.path.isfile(baseDir + '/' + filename):
|
if os.path.isfile(baseDir + '/' + filename):
|
||||||
os.remove(baseDir + '/' + filename)
|
os.remove(baseDir + '/' + filename)
|
||||||
|
|
@ -87,9 +91,9 @@ def setCSSparam(css: str, param: str, value: str) -> str:
|
||||||
def setThemeFromDict(baseDir: str, name: str, themeParams: {}) -> None:
|
def setThemeFromDict(baseDir: str, name: str, themeParams: {}) -> None:
|
||||||
"""Uses a dictionary to set a theme
|
"""Uses a dictionary to set a theme
|
||||||
"""
|
"""
|
||||||
|
if name:
|
||||||
setThemeInConfig(baseDir, name)
|
setThemeInConfig(baseDir, name)
|
||||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
themeFiles = getThemeFiles()
|
||||||
'suspended.css', 'calendar.css', 'blog.css')
|
|
||||||
for filename in themeFiles:
|
for filename in themeFiles:
|
||||||
templateFilename = baseDir + '/epicyon-' + filename
|
templateFilename = baseDir + '/epicyon-' + filename
|
||||||
if filename == 'epicyon.css':
|
if filename == 'epicyon.css':
|
||||||
|
|
@ -105,6 +109,50 @@ def setThemeFromDict(baseDir: str, name: str, themeParams: {}) -> None:
|
||||||
cssfile.write(css)
|
cssfile.write(css)
|
||||||
|
|
||||||
|
|
||||||
|
def enableGrayscale(baseDir: str) -> None:
|
||||||
|
"""Enables grayscale for the current theme
|
||||||
|
"""
|
||||||
|
themeFiles = getThemeFiles()
|
||||||
|
for filename in themeFiles:
|
||||||
|
templateFilename = baseDir + '/' + filename
|
||||||
|
if not os.path.isfile(templateFilename):
|
||||||
|
continue
|
||||||
|
with open(templateFilename, 'r') as cssfile:
|
||||||
|
css = cssfile.read()
|
||||||
|
if 'grayscale' not in css:
|
||||||
|
css = \
|
||||||
|
css.replace('body, html {',
|
||||||
|
'body, html {\n filter: grayscale(100%);')
|
||||||
|
filename = baseDir + '/' + filename
|
||||||
|
with open(filename, 'w') as cssfile:
|
||||||
|
cssfile.write(css)
|
||||||
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
||||||
|
if not os.path.isfile(grayscaleFilename):
|
||||||
|
with open(grayscaleFilename, 'w') as grayfile:
|
||||||
|
grayfile.write(' ')
|
||||||
|
|
||||||
|
|
||||||
|
def disableGrayscale(baseDir: str) -> None:
|
||||||
|
"""Disables grayscale for the current theme
|
||||||
|
"""
|
||||||
|
themeFiles = getThemeFiles()
|
||||||
|
for filename in themeFiles:
|
||||||
|
templateFilename = baseDir + '/' + filename
|
||||||
|
if not os.path.isfile(templateFilename):
|
||||||
|
continue
|
||||||
|
with open(templateFilename, 'r') as cssfile:
|
||||||
|
css = cssfile.read()
|
||||||
|
if 'grayscale' in css:
|
||||||
|
css = \
|
||||||
|
css.replace('\n filter: grayscale(100%);', '')
|
||||||
|
filename = baseDir + '/' + filename
|
||||||
|
with open(filename, 'w') as cssfile:
|
||||||
|
cssfile.write(css)
|
||||||
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
||||||
|
if os.path.isfile(grayscaleFilename):
|
||||||
|
os.remove(grayscaleFilename)
|
||||||
|
|
||||||
|
|
||||||
def setCustomFont(baseDir: str):
|
def setCustomFont(baseDir: str):
|
||||||
"""Uses a dictionary to set a theme
|
"""Uses a dictionary to set a theme
|
||||||
"""
|
"""
|
||||||
|
|
@ -124,8 +172,7 @@ def setCustomFont(baseDir: str):
|
||||||
if not customFontExt:
|
if not customFontExt:
|
||||||
return
|
return
|
||||||
|
|
||||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
themeFiles = getThemeFiles()
|
||||||
'suspended.css', 'calendar.css', 'blog.css')
|
|
||||||
for filename in themeFiles:
|
for filename in themeFiles:
|
||||||
templateFilename = baseDir + '/' + filename
|
templateFilename = baseDir + '/' + filename
|
||||||
if not os.path.isfile(templateFilename):
|
if not os.path.isfile(templateFilename):
|
||||||
|
|
@ -613,4 +660,9 @@ def setTheme(baseDir: str, name: str) -> bool:
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
setCustomFont(baseDir)
|
setCustomFont(baseDir)
|
||||||
|
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
||||||
|
if os.path.isfile(grayscaleFilename):
|
||||||
|
enableGrayscale(baseDir)
|
||||||
|
else:
|
||||||
|
disableGrayscale(baseDir)
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
28
tox.py
28
tox.py
|
|
@ -43,6 +43,21 @@ def getToxAddress(actorJson: {}) -> str:
|
||||||
def setToxAddress(actorJson: {}, toxAddress: str) -> None:
|
def setToxAddress(actorJson: {}, toxAddress: str) -> None:
|
||||||
"""Sets an tox address for the given actor
|
"""Sets an tox address for the given actor
|
||||||
"""
|
"""
|
||||||
|
notToxAddress = False
|
||||||
|
|
||||||
|
if len(toxAddress) != 76:
|
||||||
|
notToxAddress = True
|
||||||
|
if toxAddress.upper() != toxAddress:
|
||||||
|
notToxAddress = True
|
||||||
|
if '"' in toxAddress:
|
||||||
|
notToxAddress = True
|
||||||
|
if ' ' in toxAddress:
|
||||||
|
notToxAddress = True
|
||||||
|
if '.' in toxAddress:
|
||||||
|
notToxAddress = True
|
||||||
|
if ',' in toxAddress:
|
||||||
|
notToxAddress = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -59,18 +74,7 @@ def setToxAddress(actorJson: {}, toxAddress: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyFound)
|
actorJson['attachment'].remove(propertyFound)
|
||||||
|
if notToxAddress:
|
||||||
if len(toxAddress) != 76:
|
|
||||||
return
|
|
||||||
if toxAddress.upper() != toxAddress:
|
|
||||||
return
|
|
||||||
if '"' in toxAddress:
|
|
||||||
return
|
|
||||||
if ' ' in toxAddress:
|
|
||||||
return
|
|
||||||
if '.' in toxAddress:
|
|
||||||
return
|
|
||||||
if ',' in toxAddress:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for propertyValue in actorJson['attachment']:
|
for propertyValue in actorJson['attachment']:
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "البريد الإلكتروني",
|
"Email": "البريد الإلكتروني",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "بصمة PGP",
|
||||||
"This is a scheduled post.": "هذا هو المقرر المقرر.",
|
"This is a scheduled post.": "هذا هو المقرر المقرر.",
|
||||||
"Remove scheduled posts": "إزالة المشاركات المجدولة",
|
"Remove scheduled posts": "إزالة المشاركات المجدولة",
|
||||||
"Remove Twitter posts": "إزالة مشاركات Twitter",
|
"Remove Twitter posts": "إزالة مشاركات Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "حظ أوفر في المرة القادمة",
|
"Better luck next time": "حظ أوفر في المرة القادمة",
|
||||||
"Unavailable": "غير متوفره",
|
"Unavailable": "غير متوفره",
|
||||||
"The server is busy. Please try again later": "الخادم مشغول. الرجاء معاودة المحاولة في وقت لاحق",
|
"The server is busy. Please try again later": "الخادم مشغول. الرجاء معاودة المحاولة في وقت لاحق",
|
||||||
"Receive calendar events from this account": "تلقي أحداث التقويم من هذا الحساب"
|
"Receive calendar events from this account": "تلقي أحداث التقويم من هذا الحساب",
|
||||||
|
"Grayscale": "درجات الرمادي"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Correu electrònic",
|
"Email": "Correu electrònic",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Empremta digital PGP",
|
||||||
"This is a scheduled post.": "Aquesta és una publicació programada.",
|
"This is a scheduled post.": "Aquesta és una publicació programada.",
|
||||||
"Remove scheduled posts": "Elimineu les publicacions programades",
|
"Remove scheduled posts": "Elimineu les publicacions programades",
|
||||||
"Remove Twitter posts": "Elimina les publicacions de Twitter",
|
"Remove Twitter posts": "Elimina les publicacions de Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Que tingueu més sort la propera vegada",
|
"Better luck next time": "Que tingueu més sort la propera vegada",
|
||||||
"Unavailable": "No disponible",
|
"Unavailable": "No disponible",
|
||||||
"The server is busy. Please try again later": "El servidor està ocupat. Siusplau, intenta-ho més tard",
|
"The server is busy. Please try again later": "El servidor està ocupat. Siusplau, intenta-ho més tard",
|
||||||
"Receive calendar events from this account": "Rep esdeveniments del calendari des d’aquest compte"
|
"Receive calendar events from this account": "Rep esdeveniments del calendari des d’aquest compte",
|
||||||
|
"Grayscale": "Escala de grisos"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "E-bost",
|
"Email": "E-bost",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Olion Bysedd PGP",
|
||||||
"This is a scheduled post.": "Mae hon yn swydd wedi'i hamserlennu.",
|
"This is a scheduled post.": "Mae hon yn swydd wedi'i hamserlennu.",
|
||||||
"Remove scheduled posts": "Tynnwch y swyddi a drefnwyd",
|
"Remove scheduled posts": "Tynnwch y swyddi a drefnwyd",
|
||||||
"Remove Twitter posts": "Dileu postiadau Twitter",
|
"Remove Twitter posts": "Dileu postiadau Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Gwell lwc y tro nesaf",
|
"Better luck next time": "Gwell lwc y tro nesaf",
|
||||||
"Unavailable": "Ddim ar gael",
|
"Unavailable": "Ddim ar gael",
|
||||||
"The server is busy. Please try again later": "Mae'r gweinydd yn brysur. Rho gynnig Arni eto'n hwyrach",
|
"The server is busy. Please try again later": "Mae'r gweinydd yn brysur. Rho gynnig Arni eto'n hwyrach",
|
||||||
"Receive calendar events from this account": "Derbyn digwyddiadau calendr o'r cyfrif hwn"
|
"Receive calendar events from this account": "Derbyn digwyddiadau calendr o'r cyfrif hwn",
|
||||||
|
"Grayscale": "Graddlwyd"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "PGP Fingerabdruck",
|
||||||
"This is a scheduled post.": "Dies ist ein geplanter Beitrag.",
|
"This is a scheduled post.": "Dies ist ein geplanter Beitrag.",
|
||||||
"Remove scheduled posts": "Geplante Posts entfernen",
|
"Remove scheduled posts": "Geplante Posts entfernen",
|
||||||
"Remove Twitter posts": "Entfernen Sie Twitter-Posts",
|
"Remove Twitter posts": "Entfernen Sie Twitter-Posts",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Viel Glück beim nächsten Mal",
|
"Better luck next time": "Viel Glück beim nächsten Mal",
|
||||||
"Unavailable": "Nicht verfügbar",
|
"Unavailable": "Nicht verfügbar",
|
||||||
"The server is busy. Please try again later": "Der Server ist beschäftigt. Bitte versuchen Sie es später noch einmal",
|
"The server is busy. Please try again later": "Der Server ist beschäftigt. Bitte versuchen Sie es später noch einmal",
|
||||||
"Receive calendar events from this account": "Erhalten Sie Kalenderereignisse von diesem Konto"
|
"Receive calendar events from this account": "Erhalten Sie Kalenderereignisse von diesem Konto",
|
||||||
|
"Grayscale": "Graustufen"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,8 @@
|
||||||
"XMPP": "XMPP",
|
"XMPP": "XMPP",
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP Key",
|
||||||
|
"PGP Fingerprint": "PGP Fingerprint",
|
||||||
"This is a scheduled post.": "This is a scheduled post.",
|
"This is a scheduled post.": "This is a scheduled post.",
|
||||||
"Remove scheduled posts": "Remove scheduled posts",
|
"Remove scheduled posts": "Remove scheduled posts",
|
||||||
"Remove Twitter posts": "Remove Twitter posts",
|
"Remove Twitter posts": "Remove Twitter posts",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Better luck next time",
|
"Better luck next time": "Better luck next time",
|
||||||
"Unavailable": "Unavailable",
|
"Unavailable": "Unavailable",
|
||||||
"The server is busy. Please try again later": "The server is busy. Please try again later",
|
"The server is busy. Please try again later": "The server is busy. Please try again later",
|
||||||
"Receive calendar events from this account": "Receive calendar events from this account"
|
"Receive calendar events from this account": "Receive calendar events from this account",
|
||||||
|
"Grayscale": "Grayscale"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Huella digital PGP",
|
||||||
"This is a scheduled post.": "Esta es una publicación programada.",
|
"This is a scheduled post.": "Esta es una publicación programada.",
|
||||||
"Remove scheduled posts": "Eliminar publicaciones programadas",
|
"Remove scheduled posts": "Eliminar publicaciones programadas",
|
||||||
"Remove Twitter posts": "Eliminar publicaciones de Twitter",
|
"Remove Twitter posts": "Eliminar publicaciones de Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Mejor suerte la próxima vez",
|
"Better luck next time": "Mejor suerte la próxima vez",
|
||||||
"Unavailable": "Indisponible",
|
"Unavailable": "Indisponible",
|
||||||
"The server is busy. Please try again later": "El servidor esta ocupado. Por favor, inténtelo de nuevo más tarde",
|
"The server is busy. Please try again later": "El servidor esta ocupado. Por favor, inténtelo de nuevo más tarde",
|
||||||
"Receive calendar events from this account": "Recibe eventos de calendario de esta cuenta"
|
"Receive calendar events from this account": "Recibe eventos de calendario de esta cuenta",
|
||||||
|
"Grayscale": "Escala de grises"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Empreinte digitale PGP",
|
||||||
"This is a scheduled post.": "Il s'agit d'un article programmé.",
|
"This is a scheduled post.": "Il s'agit d'un article programmé.",
|
||||||
"Remove scheduled posts": "Supprimer les messages planifiés",
|
"Remove scheduled posts": "Supprimer les messages planifiés",
|
||||||
"Remove Twitter posts": "Supprimer les messages Twitter",
|
"Remove Twitter posts": "Supprimer les messages Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Plus de chance la prochaine fois",
|
"Better luck next time": "Plus de chance la prochaine fois",
|
||||||
"Unavailable": "Indisponible",
|
"Unavailable": "Indisponible",
|
||||||
"The server is busy. Please try again later": "Le serveur est occupé. Veuillez réessayer plus tard",
|
"The server is busy. Please try again later": "Le serveur est occupé. Veuillez réessayer plus tard",
|
||||||
"Receive calendar events from this account": "Recevoir des événements d'agenda de ce compte"
|
"Receive calendar events from this account": "Recevoir des événements d'agenda de ce compte",
|
||||||
|
"Grayscale": "Niveaux de gris"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Ríomhphost",
|
"Email": "Ríomhphost",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Méarlorg PGP",
|
||||||
"This is a scheduled post.": "Is post sceidealta é seo.",
|
"This is a scheduled post.": "Is post sceidealta é seo.",
|
||||||
"Remove scheduled posts": "Bain na poist sceidealta",
|
"Remove scheduled posts": "Bain na poist sceidealta",
|
||||||
"Remove Twitter posts": "Bain poist Twitter",
|
"Remove Twitter posts": "Bain poist Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Ádh níos fearr an chéad uair eile",
|
"Better luck next time": "Ádh níos fearr an chéad uair eile",
|
||||||
"Unavailable": "Níl sé ar fáil",
|
"Unavailable": "Níl sé ar fáil",
|
||||||
"The server is busy. Please try again later": "Tá an freastalaí gnóthach. Bain triail eile as níos déanaí",
|
"The server is busy. Please try again later": "Tá an freastalaí gnóthach. Bain triail eile as níos déanaí",
|
||||||
"Receive calendar events from this account": "Faigh imeachtaí féilire ón gcuntas seo"
|
"Receive calendar events from this account": "Faigh imeachtaí féilire ón gcuntas seo",
|
||||||
|
"Grayscale": "Liathscála"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,8 @@
|
||||||
"XMPP": "XMPP",
|
"XMPP": "XMPP",
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "ईमेल",
|
"Email": "ईमेल",
|
||||||
"PGP": "PGP",
|
"PGP": "पीजीपी",
|
||||||
|
"PGP Fingerprint": "पीजीपी फिंगरप्रिंट",
|
||||||
"This is a scheduled post.": "यह एक अनुसूचित पद है।",
|
"This is a scheduled post.": "यह एक अनुसूचित पद है।",
|
||||||
"Remove scheduled posts": "अनुसूचित पदों को हटा दें",
|
"Remove scheduled posts": "अनुसूचित पदों को हटा दें",
|
||||||
"Remove Twitter posts": "ट्विटर पोस्ट हटाएं",
|
"Remove Twitter posts": "ट्विटर पोस्ट हटाएं",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "अगली बार किस्मत तुम्हारा साथ देगी",
|
"Better luck next time": "अगली बार किस्मत तुम्हारा साथ देगी",
|
||||||
"Unavailable": "अनुपलब्ध",
|
"Unavailable": "अनुपलब्ध",
|
||||||
"The server is busy. Please try again later": "सर्वर व्यस्त है। बाद में पुन: प्रयास करें",
|
"The server is busy. Please try again later": "सर्वर व्यस्त है। बाद में पुन: प्रयास करें",
|
||||||
"Receive calendar events from this account": "इस खाते से कैलेंडर ईवेंट प्राप्त करें"
|
"Receive calendar events from this account": "इस खाते से कैलेंडर ईवेंट प्राप्त करें",
|
||||||
|
"Grayscale": "ग्रेस्केल"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "E-mail",
|
"Email": "E-mail",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Impronta digitale PGP",
|
||||||
"This is a scheduled post.": "Questo è un post programmato",
|
"This is a scheduled post.": "Questo è un post programmato",
|
||||||
"Remove scheduled posts": "Rimuovi i post programmati",
|
"Remove scheduled posts": "Rimuovi i post programmati",
|
||||||
"Remove Twitter posts": "Rimuovi i post di Twitter",
|
"Remove Twitter posts": "Rimuovi i post di Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "La prossima volta sarai più fortunato",
|
"Better luck next time": "La prossima volta sarai più fortunato",
|
||||||
"Unavailable": "non disponibile",
|
"Unavailable": "non disponibile",
|
||||||
"The server is busy. Please try again later": "Il server è occupato. Per favore riprova più tardi",
|
"The server is busy. Please try again later": "Il server è occupato. Per favore riprova più tardi",
|
||||||
"Receive calendar events from this account": "Ricevi eventi di calendario da questo account"
|
"Receive calendar events from this account": "Ricevi eventi di calendario da questo account",
|
||||||
|
"Grayscale": "Scala di grigi"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Eメール",
|
"Email": "Eメール",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "PGPフィンガープリント",
|
||||||
"This is a scheduled post.": "これはスケジュールされた投稿です。",
|
"This is a scheduled post.": "これはスケジュールされた投稿です。",
|
||||||
"Remove scheduled posts": "スケジュールされた投稿を削除する",
|
"Remove scheduled posts": "スケジュールされた投稿を削除する",
|
||||||
"Remove Twitter posts": "Twitterの投稿を削除する",
|
"Remove Twitter posts": "Twitterの投稿を削除する",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "次回は幸運を",
|
"Better luck next time": "次回は幸運を",
|
||||||
"Unavailable": "利用できません",
|
"Unavailable": "利用できません",
|
||||||
"The server is busy. Please try again later": "サーバーはビジーです。 後でもう一度やり直してください",
|
"The server is busy. Please try again later": "サーバーはビジーです。 後でもう一度やり直してください",
|
||||||
"Receive calendar events from this account": "このアカウントからカレンダーイベントを受信します"
|
"Receive calendar events from this account": "このアカウントからカレンダーイベントを受信します",
|
||||||
|
"Grayscale": "グレースケール"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "PGP Fingerprint",
|
||||||
"This is a scheduled post.": "This is a scheduled post.",
|
"This is a scheduled post.": "This is a scheduled post.",
|
||||||
"Remove scheduled posts": "Remove scheduled posts",
|
"Remove scheduled posts": "Remove scheduled posts",
|
||||||
"Remove Twitter posts": "Remove Twitter posts",
|
"Remove Twitter posts": "Remove Twitter posts",
|
||||||
|
|
@ -245,5 +246,6 @@
|
||||||
"Better luck next time": "Better luck next time",
|
"Better luck next time": "Better luck next time",
|
||||||
"Unavailable": "Unavailable",
|
"Unavailable": "Unavailable",
|
||||||
"The server is busy. Please try again later": "The server is busy. Please try again later",
|
"The server is busy. Please try again later": "The server is busy. Please try again later",
|
||||||
"Receive calendar events from this account": "Receive calendar events from this account"
|
"Receive calendar events from this account": "Receive calendar events from this account",
|
||||||
|
"Grayscale": "Grayscale"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "Impressão digital PGP",
|
||||||
"This is a scheduled post.": "Esta é uma postagem agendada.",
|
"This is a scheduled post.": "Esta é uma postagem agendada.",
|
||||||
"Remove scheduled posts": "Remover postagens agendadas",
|
"Remove scheduled posts": "Remover postagens agendadas",
|
||||||
"Remove Twitter posts": "Remover postagens do Twitter",
|
"Remove Twitter posts": "Remover postagens do Twitter",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Mais sorte da próxima vez",
|
"Better luck next time": "Mais sorte da próxima vez",
|
||||||
"Unavailable": "Indisponível",
|
"Unavailable": "Indisponível",
|
||||||
"The server is busy. Please try again later": "O servidor está ocupado. Por favor, tente novamente mais tarde",
|
"The server is busy. Please try again later": "O servidor está ocupado. Por favor, tente novamente mais tarde",
|
||||||
"Receive calendar events from this account": "Receba eventos da agenda desta conta"
|
"Receive calendar events from this account": "Receba eventos da agenda desta conta",
|
||||||
|
"Grayscale": "Escala de cinza"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "Эл. адрес",
|
"Email": "Эл. адрес",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "PGP Отпечаток пальца",
|
||||||
"This is a scheduled post.": "Это запланированный пост.",
|
"This is a scheduled post.": "Это запланированный пост.",
|
||||||
"Remove scheduled posts": "Удалить запланированные сообщения",
|
"Remove scheduled posts": "Удалить запланированные сообщения",
|
||||||
"Remove Twitter posts": "Удалить сообщения из Твиттера",
|
"Remove Twitter posts": "Удалить сообщения из Твиттера",
|
||||||
|
|
@ -249,5 +250,6 @@
|
||||||
"Better luck next time": "Повезет в следующий раз",
|
"Better luck next time": "Повезет в следующий раз",
|
||||||
"Unavailable": "Недоступен",
|
"Unavailable": "Недоступен",
|
||||||
"The server is busy. Please try again later": "Сервер занят. Пожалуйста, попробуйте позже",
|
"The server is busy. Please try again later": "Сервер занят. Пожалуйста, попробуйте позже",
|
||||||
"Receive calendar events from this account": "Получать события календаря от этого аккаунта"
|
"Receive calendar events from this account": "Получать события календаря от этого аккаунта",
|
||||||
|
"Grayscale": "Оттенки серого"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@
|
||||||
"Matrix": "Matrix",
|
"Matrix": "Matrix",
|
||||||
"Email": "电子邮件",
|
"Email": "电子邮件",
|
||||||
"PGP": "PGP",
|
"PGP": "PGP",
|
||||||
|
"PGP Fingerprint": "PGP指纹",
|
||||||
"This is a scheduled post.": "这是预定的帖子。",
|
"This is a scheduled post.": "这是预定的帖子。",
|
||||||
"Remove scheduled posts": "删除预定的帖子",
|
"Remove scheduled posts": "删除预定的帖子",
|
||||||
"Remove Twitter posts": "删除Twitter帖子",
|
"Remove Twitter posts": "删除Twitter帖子",
|
||||||
|
|
@ -248,5 +249,6 @@
|
||||||
"Better luck next time": "下次好运",
|
"Better luck next time": "下次好运",
|
||||||
"Unavailable": "不可用",
|
"Unavailable": "不可用",
|
||||||
"The server is busy. Please try again later": "服务器忙。 请稍后再试",
|
"The server is busy. Please try again later": "服务器忙。 请稍后再试",
|
||||||
"Receive calendar events from this account": "从该帐户接收日历事件"
|
"Receive calendar events from this account": "从该帐户接收日历事件",
|
||||||
|
"Grayscale": "灰阶"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
utils.py
7
utils.py
|
|
@ -13,7 +13,7 @@ import datetime
|
||||||
import json
|
import json
|
||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
import errno
|
import errno
|
||||||
from urllib.request import urlopen
|
import urllib.request
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
from followingCalendar import addPersonToCalendar
|
from followingCalendar import addPersonToCalendar
|
||||||
|
|
@ -1095,8 +1095,11 @@ def siteIsActive(url: str) -> bool:
|
||||||
This can be used to check that an instance is online before
|
This can be used to check that an instance is online before
|
||||||
trying to send posts to it.
|
trying to send posts to it.
|
||||||
"""
|
"""
|
||||||
|
if not url.startswith('http'):
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
urlopen(url, timeout=10)
|
req = urllib.request.Request(url)
|
||||||
|
urllib.request.urlopen(req, timeout=10) # nosec
|
||||||
return True
|
return True
|
||||||
except SocketError as e:
|
except SocketError as e:
|
||||||
if e.errno == errno.ECONNRESET:
|
if e.errno == errno.ECONNRESET:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from person import personBoxJson
|
||||||
from person import isPersonSnoozed
|
from person import isPersonSnoozed
|
||||||
from pgp import getEmailAddress
|
from pgp import getEmailAddress
|
||||||
from pgp import getPGPpubKey
|
from pgp import getPGPpubKey
|
||||||
|
from pgp import getPGPfingerprint
|
||||||
from xmpp import getXmppAddress
|
from xmpp import getXmppAddress
|
||||||
from ssb import getSSBAddress
|
from ssb import getSSBAddress
|
||||||
from tox import getToxAddress
|
from tox import getToxAddress
|
||||||
|
|
@ -62,6 +63,7 @@ from content import getMentionsFromHtml
|
||||||
from content import addHtmlTags
|
from content import addHtmlTags
|
||||||
from content import replaceEmojiFromTags
|
from content import replaceEmojiFromTags
|
||||||
from content import removeLongWords
|
from content import removeLongWords
|
||||||
|
from content import removeHtml
|
||||||
from config import getConfigParam
|
from config import getConfigParam
|
||||||
from skills import getSkills
|
from skills import getSkills
|
||||||
from cache import getPersonFromCache
|
from cache import getPersonFromCache
|
||||||
|
|
@ -1050,6 +1052,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
||||||
donateUrl = ''
|
donateUrl = ''
|
||||||
emailAddress = ''
|
emailAddress = ''
|
||||||
PGPpubKey = ''
|
PGPpubKey = ''
|
||||||
|
PGPfingerprint = ''
|
||||||
xmppAddress = ''
|
xmppAddress = ''
|
||||||
matrixAddress = ''
|
matrixAddress = ''
|
||||||
ssbAddress = ''
|
ssbAddress = ''
|
||||||
|
|
@ -1066,6 +1069,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
||||||
toxAddress = getToxAddress(actorJson)
|
toxAddress = getToxAddress(actorJson)
|
||||||
emailAddress = getEmailAddress(actorJson)
|
emailAddress = getEmailAddress(actorJson)
|
||||||
PGPpubKey = getPGPpubKey(actorJson)
|
PGPpubKey = getPGPpubKey(actorJson)
|
||||||
|
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||||
if actorJson.get('name'):
|
if actorJson.get('name'):
|
||||||
displayNickname = actorJson['name']
|
displayNickname = actorJson['name']
|
||||||
if actorJson.get('summary'):
|
if actorJson.get('summary'):
|
||||||
|
|
@ -1239,6 +1243,15 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
||||||
themes = getThemesList()
|
themes = getThemesList()
|
||||||
themesDropdown = '<div class="container">'
|
themesDropdown = '<div class="container">'
|
||||||
themesDropdown += ' <b>' + translate['Theme'] + '</b><br>'
|
themesDropdown += ' <b>' + translate['Theme'] + '</b><br>'
|
||||||
|
grayscaleFilename = \
|
||||||
|
baseDir + '/accounts/.grayscale'
|
||||||
|
grayscale = ''
|
||||||
|
if os.path.isfile(grayscaleFilename):
|
||||||
|
grayscale = 'checked'
|
||||||
|
themesDropdown += \
|
||||||
|
' <input type="checkbox" class="profilecheckbox" ' + \
|
||||||
|
'name="grayscale" ' + grayscale + \
|
||||||
|
'> ' + translate['Grayscale'] + '<br>'
|
||||||
themesDropdown += ' <select id="themeDropdown" ' + \
|
themesDropdown += ' <select id="themeDropdown" ' + \
|
||||||
'name="themeDropdown" class="theme">'
|
'name="themeDropdown" class="theme">'
|
||||||
for themeName in themes:
|
for themeName in themes:
|
||||||
|
|
@ -1331,6 +1344,12 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
||||||
translate['Email'] + '</label><br>'
|
translate['Email'] + '</label><br>'
|
||||||
editProfileForm += \
|
editProfileForm += \
|
||||||
' <input type="text" name="email" value="' + emailAddress + '">'
|
' <input type="text" name="email" value="' + emailAddress + '">'
|
||||||
|
editProfileForm += \
|
||||||
|
'<label class="labels">' + \
|
||||||
|
translate['PGP Fingerprint'] + '</label><br>'
|
||||||
|
editProfileForm += \
|
||||||
|
' <input type="text" name="openpgp" value="' + \
|
||||||
|
PGPfingerprint + '">'
|
||||||
editProfileForm += \
|
editProfileForm += \
|
||||||
'<label class="labels">' + translate['PGP'] + '</label><br>'
|
'<label class="labels">' + translate['PGP'] + '</label><br>'
|
||||||
editProfileForm += \
|
editProfileForm += \
|
||||||
|
|
@ -2550,13 +2569,15 @@ def htmlProfile(defaultTimeline: str,
|
||||||
donateSection = ''
|
donateSection = ''
|
||||||
donateUrl = getDonationUrl(profileJson)
|
donateUrl = getDonationUrl(profileJson)
|
||||||
PGPpubKey = getPGPpubKey(profileJson)
|
PGPpubKey = getPGPpubKey(profileJson)
|
||||||
|
PGPfingerprint = getPGPfingerprint(profileJson)
|
||||||
emailAddress = getEmailAddress(profileJson)
|
emailAddress = getEmailAddress(profileJson)
|
||||||
xmppAddress = getXmppAddress(profileJson)
|
xmppAddress = getXmppAddress(profileJson)
|
||||||
matrixAddress = getMatrixAddress(profileJson)
|
matrixAddress = getMatrixAddress(profileJson)
|
||||||
ssbAddress = getSSBAddress(profileJson)
|
ssbAddress = getSSBAddress(profileJson)
|
||||||
toxAddress = getToxAddress(profileJson)
|
toxAddress = getToxAddress(profileJson)
|
||||||
if donateUrl or xmppAddress or matrixAddress or \
|
if donateUrl or xmppAddress or matrixAddress or \
|
||||||
ssbAddress or toxAddress or PGPpubKey or emailAddress:
|
ssbAddress or toxAddress or PGPpubKey or \
|
||||||
|
PGPfingerprint or emailAddress:
|
||||||
donateSection = '<div class="container">\n'
|
donateSection = '<div class="container">\n'
|
||||||
donateSection += ' <center>\n'
|
donateSection += ' <center>\n'
|
||||||
if donateUrl:
|
if donateUrl:
|
||||||
|
|
@ -2583,6 +2604,10 @@ def htmlProfile(defaultTimeline: str,
|
||||||
donateSection += \
|
donateSection += \
|
||||||
'<p>Tox: <label class="ssbaddr">' + \
|
'<p>Tox: <label class="ssbaddr">' + \
|
||||||
toxAddress + '</label></p>\n'
|
toxAddress + '</label></p>\n'
|
||||||
|
if PGPfingerprint:
|
||||||
|
donateSection += \
|
||||||
|
'<p class="pgp">PGP: ' + \
|
||||||
|
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
|
||||||
if PGPpubKey:
|
if PGPpubKey:
|
||||||
donateSection += \
|
donateSection += \
|
||||||
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
|
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
|
||||||
|
|
@ -3800,7 +3825,14 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
likeIcon = 'like_inactive.png'
|
likeIcon = 'like_inactive.png'
|
||||||
likeLink = 'like'
|
likeLink = 'like'
|
||||||
likeTitle = translate['Like this post']
|
likeTitle = translate['Like this post']
|
||||||
if noOfLikes(postJsonObject) > 0:
|
likeCount = noOfLikes(postJsonObject)
|
||||||
|
likeCountStr = ''
|
||||||
|
if likeCount > 0:
|
||||||
|
if likeCount > 1:
|
||||||
|
if likeCount <= 10:
|
||||||
|
likeCountStr = ' (' + str(likeCount) + ')'
|
||||||
|
else:
|
||||||
|
likeCountStr = ' (10+)'
|
||||||
likeIcon = 'like.png'
|
likeIcon = 'like.png'
|
||||||
if likedByPerson(postJsonObject, nickname, fullDomain):
|
if likedByPerson(postJsonObject, nickname, fullDomain):
|
||||||
likeLink = 'unlike'
|
likeLink = 'unlike'
|
||||||
|
|
@ -3811,9 +3843,10 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
pageNumberParam + \
|
pageNumberParam + \
|
||||||
'?actor=' + postJsonObject['actor'] + \
|
'?actor=' + postJsonObject['actor'] + \
|
||||||
'?bm=' + timelinePostBookmark + \
|
'?bm=' + timelinePostBookmark + \
|
||||||
'?tl=' + boxName + '" title="' + likeTitle + '">'
|
'?tl=' + boxName + '" title="' + \
|
||||||
|
likeTitle + likeCountStr + '">'
|
||||||
likeStr += \
|
likeStr += \
|
||||||
'<img loading="lazy" title="' + likeTitle + \
|
'<img loading="lazy" title="' + likeTitle + likeCountStr + \
|
||||||
'" alt="' + likeTitle + \
|
'" alt="' + likeTitle + \
|
||||||
' |" src="/' + iconsDir + '/' + likeIcon + '"/></a>'
|
' |" src="/' + iconsDir + '/' + likeIcon + '"/></a>'
|
||||||
|
|
||||||
|
|
@ -5314,6 +5347,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
||||||
blogAddress: str,
|
blogAddress: str,
|
||||||
toxAddress: str,
|
toxAddress: str,
|
||||||
PGPpubKey: str,
|
PGPpubKey: str,
|
||||||
|
PGPfingerprint: str,
|
||||||
emailAddress) -> str:
|
emailAddress) -> str:
|
||||||
"""Show options for a person: view/follow/block/report
|
"""Show options for a person: view/follow/block/report
|
||||||
"""
|
"""
|
||||||
|
|
@ -5411,6 +5445,9 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
||||||
if toxAddress:
|
if toxAddress:
|
||||||
optionsStr += \
|
optionsStr += \
|
||||||
'<p class="imText">Tox: ' + toxAddress + '</p>'
|
'<p class="imText">Tox: ' + toxAddress + '</p>'
|
||||||
|
if PGPfingerprint:
|
||||||
|
optionsStr += '<p class="pgp">PGP: ' + \
|
||||||
|
PGPfingerprint.replace('\n', '<br>') + '</p>'
|
||||||
if PGPpubKey:
|
if PGPpubKey:
|
||||||
optionsStr += '<p class="pgp">' + \
|
optionsStr += '<p class="pgp">' + \
|
||||||
PGPpubKey.replace('\n', '<br>') + '</p>'
|
PGPpubKey.replace('\n', '<br>') + '</p>'
|
||||||
|
|
@ -6196,6 +6233,8 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
profileJson['summary'].replace('<br>', '\n')
|
profileJson['summary'].replace('<br>', '\n')
|
||||||
avatarDescription = avatarDescription.replace('<p>', '')
|
avatarDescription = avatarDescription.replace('<p>', '')
|
||||||
avatarDescription = avatarDescription.replace('</p>', '')
|
avatarDescription = avatarDescription.replace('</p>', '')
|
||||||
|
if '<' in avatarDescription:
|
||||||
|
avatarDescription = removeHtml(avatarDescription)
|
||||||
profileStr = ' <div class="hero-image">'
|
profileStr = ' <div class="hero-image">'
|
||||||
profileStr += ' <div class="hero-text">'
|
profileStr += ' <div class="hero-text">'
|
||||||
if avatarUrl:
|
if avatarUrl:
|
||||||
|
|
|
||||||
|
|
@ -1264,7 +1264,7 @@
|
||||||
<p class="intro">You will need python version 3.7 or later.</p>
|
<p class="intro">You will need python version 3.7 or later.</p>
|
||||||
<p class="intro">On a Debian based system:</p>
|
<p class="intro">On a Debian based system:</p>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<p>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-pyld python3-django-timezone-field python3-pyqrcode python3-png libimage-exiftool-perl certbot nginx</p>
|
<p>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-pyld python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="intro">
|
<p class="intro">
|
||||||
|
|
|
||||||
15
xmpp.py
15
xmpp.py
|
|
@ -36,6 +36,14 @@ def getXmppAddress(actorJson: {}) -> str:
|
||||||
def setXmppAddress(actorJson: {}, xmppAddress: str) -> None:
|
def setXmppAddress(actorJson: {}, xmppAddress: str) -> None:
|
||||||
"""Sets an xmpp address for the given actor
|
"""Sets an xmpp address for the given actor
|
||||||
"""
|
"""
|
||||||
|
notXmppAddress = False
|
||||||
|
if '@' not in xmppAddress:
|
||||||
|
notXmppAddress = True
|
||||||
|
if '.' not in xmppAddress:
|
||||||
|
notXmppAddress = True
|
||||||
|
if '"' in xmppAddress:
|
||||||
|
notXmppAddress = True
|
||||||
|
|
||||||
if not actorJson.get('attachment'):
|
if not actorJson.get('attachment'):
|
||||||
actorJson['attachment'] = []
|
actorJson['attachment'] = []
|
||||||
|
|
||||||
|
|
@ -53,12 +61,7 @@ def setXmppAddress(actorJson: {}, xmppAddress: str) -> None:
|
||||||
break
|
break
|
||||||
if propertyFound:
|
if propertyFound:
|
||||||
actorJson['attachment'].remove(propertyFound)
|
actorJson['attachment'].remove(propertyFound)
|
||||||
|
if notXmppAddress:
|
||||||
if '@' not in xmppAddress:
|
|
||||||
return
|
|
||||||
if '.' not in xmppAddress:
|
|
||||||
return
|
|
||||||
if '"' in xmppAddress:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for propertyValue in actorJson['attachment']:
|
for propertyValue in actorJson['attachment']:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue