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

main
Bob Mottram 2020-07-11 11:54:26 +01:00
commit 415c215eb1
43 changed files with 815 additions and 108 deletions

View File

@ -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/*~

View File

@ -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.

View File

@ -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:

View File

@ -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)))

View File

@ -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

View File

@ -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/' + \

View File

@ -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

View File

@ -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'

View File

@ -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 + \

View File

@ -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

View File

@ -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')

View File

@ -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.

BIN
img/eyes.jpg 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -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,

View File

@ -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:

View File

@ -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
View File

@ -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
View File

@ -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: [],

2
security_audit 100755
View File

@ -0,0 +1,2 @@
#!/bin/bash
bandit *.py -x tests.py

83
socnet.py 100644
View File

@ -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
View File

@ -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']:

View File

@ -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()

View File

@ -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
View File

@ -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']:

View File

@ -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": "درجات الرمادي"
} }

View File

@ -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 daquest compte" "Receive calendar events from this account": "Rep esdeveniments del calendari des daquest compte",
"Grayscale": "Escala de grisos"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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": "ग्रेस्केल"
} }

View File

@ -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"
} }

View File

@ -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": "グレースケール"
} }

View File

@ -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"
} }

View File

@ -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"
} }

View File

@ -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": "Оттенки серого"
} }

View File

@ -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": "灰阶"
} }

View File

@ -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:

View File

@ -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:

View File

@ -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
View File

@ -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']: