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
|
||||
cd .. && mv ${APP} ${APP}-${VERSION} && tar -zcvf ${APP}_${VERSION}.orig.tar.gz ${APP}-${VERSION}/ && mv ${APP}-${VERSION} ${APP}
|
||||
clean:
|
||||
rm -f *.*~ *~
|
||||
rm -f *.*~ *~ *.dot
|
||||
rm -f orgs/*~
|
||||
rm -f website/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 \
|
||||
imagemagick python-pillow python-requests \
|
||||
perl-image-exiftool python-numpy python-dateutil \
|
||||
certbot flake8
|
||||
certbot flake8 bandit
|
||||
sudo pip3 install pyLD pyqrcode pypng
|
||||
```
|
||||
|
||||
|
|
@ -41,7 +41,8 @@ sudo apt install -y \
|
|||
python3-idna python3-requests \
|
||||
python3-pyld python3-django-timezone-field \
|
||||
libimage-exiftool-perl python3-flake8 \
|
||||
python3-pyqrcode python3-png certbot nginx
|
||||
python3-pyqrcode python3-png python3-bandit \
|
||||
certbot nginx
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
|
@ -200,6 +201,16 @@ Static analysis can be run with:
|
|||
./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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
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 binascii
|
||||
import os
|
||||
import random
|
||||
import secrets
|
||||
|
||||
|
||||
def hashPassword(password: str) -> str:
|
||||
|
|
@ -162,4 +162,4 @@ def authorize(baseDir: str, path: str, authHeader: str, debug: bool) -> bool:
|
|||
def createPassword(length=10):
|
||||
validChars = 'abcdefghijklmnopqrstuvwxyz' + \
|
||||
'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
|
||||
|
||||
|
||||
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:
|
||||
"""Performs word replacements. eg. Trump -> The Orange Menace
|
||||
"""
|
||||
|
|
@ -400,6 +426,24 @@ def removeTextFormatting(content: str) -> str:
|
|||
return content
|
||||
|
||||
|
||||
def removeHtml(content: str) -> str:
|
||||
"""Removes html links from the given content.
|
||||
Used to ensure that profile descriptions don't contain dubious content
|
||||
"""
|
||||
if '<' not in content:
|
||||
return content
|
||||
removing = False
|
||||
result = ''
|
||||
for ch in content:
|
||||
if ch == '<':
|
||||
removing = True
|
||||
elif ch == '>':
|
||||
removing = False
|
||||
elif not removing:
|
||||
result += ch
|
||||
return result
|
||||
|
||||
|
||||
def removeLongWords(content: str, maxWordLength: int,
|
||||
longWordsList: []) -> str:
|
||||
"""Breaks up long words so that on mobile screens this doesn't
|
||||
|
|
|
|||
32
daemon.py
32
daemon.py
|
|
@ -30,7 +30,9 @@ from metadata import metaDataNodeInfo
|
|||
from pgp import getEmailAddress
|
||||
from pgp import setEmailAddress
|
||||
from pgp import getPGPpubKey
|
||||
from pgp import getPGPfingerprint
|
||||
from pgp import setPGPpubKey
|
||||
from pgp import setPGPfingerprint
|
||||
from xmpp import getXmppAddress
|
||||
from xmpp import setXmppAddress
|
||||
from ssb import getSSBAddress
|
||||
|
|
@ -177,6 +179,8 @@ from cache import getPersonFromCache
|
|||
from httpsig import verifyPostHeaders
|
||||
from theme import setTheme
|
||||
from theme import getTheme
|
||||
from theme import enableGrayscale
|
||||
from theme import disableGrayscale
|
||||
from schedule import runPostSchedule
|
||||
from schedule import runPostScheduleWatchdog
|
||||
from schedule import removeScheduledPosts
|
||||
|
|
@ -533,7 +537,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
except BaseException:
|
||||
pass
|
||||
if not etag:
|
||||
etag = sha1(data).hexdigest()
|
||||
etag = sha1(data).hexdigest() # nosec
|
||||
try:
|
||||
with open(mediaFilename + '.etag', 'w') as etagFile:
|
||||
etagFile.write(etag)
|
||||
|
|
@ -1549,6 +1553,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
optionsLink = optionsList[3]
|
||||
donateUrl = None
|
||||
PGPpubKey = None
|
||||
PGPfingerprint = None
|
||||
xmppAddress = None
|
||||
matrixAddress = None
|
||||
blogAddress = None
|
||||
|
|
@ -1567,6 +1572,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
toxAddress = getToxAddress(actorJson)
|
||||
emailAddress = getEmailAddress(actorJson)
|
||||
PGPpubKey = getPGPpubKey(actorJson)
|
||||
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||
msg = htmlPersonOptions(self.server.translate,
|
||||
self.server.baseDir,
|
||||
self.server.domain,
|
||||
|
|
@ -1577,7 +1583,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
pageNumber, donateUrl,
|
||||
xmppAddress, matrixAddress,
|
||||
ssbAddress, blogAddress,
|
||||
toxAddress, PGPpubKey,
|
||||
toxAddress,
|
||||
PGPpubKey, PGPfingerprint,
|
||||
emailAddress).encode('utf-8')
|
||||
self._set_headers('text/html', len(msg),
|
||||
cookie, callingDomain)
|
||||
|
|
@ -5093,7 +5100,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
else:
|
||||
with open(mediaFilename, 'rb') as avFile:
|
||||
mediaBinary = avFile.read()
|
||||
etag = sha1(mediaBinary).hexdigest()
|
||||
etag = sha1(mediaBinary).hexdigest() # nosec
|
||||
try:
|
||||
with open(mediaTagFilename, 'w') as etagFile:
|
||||
etagFile.write(etag)
|
||||
|
|
@ -6242,6 +6249,17 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
setPGPpubKey(actorJson, '')
|
||||
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)
|
||||
if fields.get('donateUrl'):
|
||||
if fields['donateUrl'] != currentDonateUrl:
|
||||
|
|
@ -6478,6 +6496,14 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if actorJson['type'] != 'Person':
|
||||
actorJson['type'] = 'Person'
|
||||
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
|
||||
filterFilename = \
|
||||
self.server.baseDir + '/accounts/' + \
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ if [ -f /usr/bin/pacman ]; then
|
|||
imagemagick python-pillow python-requests \
|
||||
perl-image-exiftool python-numpy python-dateutil \
|
||||
certbot flake8 git i2pd wget qrencode \
|
||||
proxychains midori
|
||||
proxychains midori bandit
|
||||
pip3 install pyLD pyqrcode pypng
|
||||
else
|
||||
apt-get update
|
||||
|
|
@ -75,7 +75,7 @@ else
|
|||
libimage-exiftool-perl python3-flake8 python3-pyld \
|
||||
python3-django-timezone-field nginx git i2pd wget \
|
||||
python3-pyqrcode qrencode python3-png \
|
||||
proxychains midori
|
||||
proxychains midori python3-bandit
|
||||
fi
|
||||
|
||||
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 \
|
||||
imagemagick python-pillow python-requests \
|
||||
perl-image-exiftool python-numpy python-dateutil \
|
||||
certbot flake8 git qrencode
|
||||
certbot flake8 git qrencode bandit
|
||||
pip3 install pyLD pyqrcode pypng
|
||||
else
|
||||
apt-get update
|
||||
|
|
@ -48,7 +48,7 @@ else
|
|||
python3-setuptools python3-socks python3-idna \
|
||||
libimage-exiftool-perl python3-flake8 python3-pyld \
|
||||
python3-django-timezone-field tor nginx git qrencode \
|
||||
python3-pyqrcode python3-png
|
||||
python3-pyqrcode python3-png python3-bandit
|
||||
fi
|
||||
|
||||
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:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -65,6 +73,8 @@ def setDonationUrl(actorJson: {}, donateUrl: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyFound)
|
||||
if notUrl:
|
||||
return
|
||||
|
||||
donateValue = \
|
||||
'<a href="' + donateUrl + \
|
||||
|
|
|
|||
|
|
@ -177,6 +177,21 @@ function notifications {
|
|||
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
|
||||
epicyonReplyFile="$epicyonDir/.newReply"
|
||||
if [ -f "$epicyonReplyFile" ]; then
|
||||
|
|
|
|||
79
epicyon.py
79
epicyon.py
|
|
@ -15,6 +15,7 @@ from person import deactivateAccount
|
|||
from skills import setSkillLevel
|
||||
from roles import setRole
|
||||
from webfinger import webfingerHandle
|
||||
from posts import getPublicPostDomains
|
||||
from posts import sendBlockViaServer
|
||||
from posts import sendUndoBlockViaServer
|
||||
from posts import createPublicPost
|
||||
|
|
@ -66,6 +67,7 @@ from shares import sendUndoShareViaServer
|
|||
from shares import addShare
|
||||
from theme import setTheme
|
||||
from announce import sendAnnounceViaServer
|
||||
from socnet import instancesGraph
|
||||
import argparse
|
||||
|
||||
|
||||
|
|
@ -146,6 +148,14 @@ parser.add_argument('--actor', dest='actor', type=str,
|
|||
parser.add_argument('--posts', dest='posts', type=str,
|
||||
default=None,
|
||||
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,
|
||||
default=None,
|
||||
help='Show raw json of posts for the given handle')
|
||||
|
|
@ -386,6 +396,14 @@ if baseDir.endswith('/'):
|
|||
|
||||
if 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')
|
||||
sys.exit()
|
||||
if not args.http:
|
||||
|
|
@ -395,8 +413,12 @@ if args.posts:
|
|||
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'
|
||||
getPublicPostsOfPerson(baseDir, nickname, domain, False, True,
|
||||
|
|
@ -404,6 +426,63 @@ if args.posts:
|
|||
__version__)
|
||||
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 '@' not in args.postsraw:
|
||||
print('Syntax: --postsraw nickname@domain')
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ You will need python version 3.7 or later.
|
|||
|
||||
On a Debian based system:
|
||||
|
||||
sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-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.
|
||||
|
||||
|
|
|
|||
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 receiveGitPatch
|
||||
from followingCalendar import receivingCalendarEvents
|
||||
from content import dangerousMarkup
|
||||
|
||||
|
||||
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
||||
|
|
@ -981,6 +982,7 @@ def receiveUpdate(recentPostsCache: {}, session, baseDir: str,
|
|||
def receiveLike(recentPostsCache: {},
|
||||
session, handle: str, isGroup: bool, baseDir: str,
|
||||
httpPrefix: str, domain: str, port: int,
|
||||
onionDomain: str,
|
||||
sendThreads: [], postLog: [], cachedWebfingers: {},
|
||||
personCache: {}, messageJson: {}, federationList: [],
|
||||
debug: bool) -> bool:
|
||||
|
|
@ -1033,6 +1035,8 @@ def receiveLike(recentPostsCache: {},
|
|||
updateLikesCollection(recentPostsCache, baseDir, postFilename,
|
||||
messageJson['object'],
|
||||
messageJson['actor'], domain, debug)
|
||||
likeNotify(baseDir, domain, onionDomain, handle,
|
||||
messageJson['actor'], messageJson['object'])
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -1596,22 +1600,20 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
|
|||
return False
|
||||
if 'Z' not in messageJson['object']['published']:
|
||||
return False
|
||||
|
||||
if isGitPatch(baseDir, nickname, domain,
|
||||
messageJson['object']['type'],
|
||||
messageJson['object']['summary'],
|
||||
messageJson['object']['content']):
|
||||
return True
|
||||
# check for bad html
|
||||
invalidStrings = ('<script>', '</script>', '</canvas>',
|
||||
'</style>', '</abbr>',
|
||||
'</html>', '</body>', '<br>', '<hr>')
|
||||
for badStr in invalidStrings:
|
||||
if badStr in messageJson['object']['content']:
|
||||
|
||||
if dangerousMarkup(messageJson['object']['content']):
|
||||
if messageJson['object'].get('id'):
|
||||
print('REJECT ARBITRARY HTML: ' + messageJson['object']['id'])
|
||||
print('REJECT ARBITRARY HTML: bad string in post - ' +
|
||||
messageJson['object']['content'])
|
||||
return False
|
||||
|
||||
# check (rough) number of mentions
|
||||
mentionsEst = estimateNumberOfMentions(messageJson['object']['content'])
|
||||
if mentionsEst > maxMentions:
|
||||
|
|
@ -1704,6 +1706,54 @@ def dmNotify(baseDir: str, handle: str, url: str) -> None:
|
|||
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:
|
||||
"""Creates a notification that a new reply has arrived
|
||||
"""
|
||||
|
|
@ -1970,6 +2020,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
session, handle, isGroup,
|
||||
baseDir, httpPrefix,
|
||||
domain, port,
|
||||
onionDomain,
|
||||
sendThreads, postLog,
|
||||
cachedWebfingers,
|
||||
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
|
||||
"""
|
||||
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'):
|
||||
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'):
|
||||
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:
|
||||
|
|
@ -116,7 +119,7 @@ def updateEtag(mediaFilename: str) -> None:
|
|||
if not data:
|
||||
return
|
||||
# calculate hash
|
||||
etag = sha1(data).hexdigest()
|
||||
etag = sha1(data).hexdigest() # nosec
|
||||
# save the hash
|
||||
try:
|
||||
with open(mediaFilename + '.etag', 'w') as etagFile:
|
||||
|
|
|
|||
17
person.py
17
person.py
|
|
@ -151,14 +151,16 @@ def randomizeActorImages(personJson: {}) -> None:
|
|||
personId = personJson['id']
|
||||
lastPartOfFilename = personJson['icon']['url'].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'] = \
|
||||
personId + '/avatar' + str(randint(10000000000000, 99999999999999)) + \
|
||||
'.' + existingExtension
|
||||
personId + '/avatar' + randStr + '.' + existingExtension
|
||||
lastPartOfFilename = personJson['image']['url'].split('/')[-1]
|
||||
existingExtension = lastPartOfFilename.split('.')[1]
|
||||
randStr = str(randint(10000000000000, 99999999999999)) # nosec
|
||||
personJson['image']['url'] = \
|
||||
personId + '/image' + str(randint(10000000000000, 99999999999999)) + \
|
||||
'.' + existingExtension
|
||||
personId + '/image' + randStr + '.' + existingExtension
|
||||
|
||||
|
||||
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
|
||||
personType = 'Application'
|
||||
|
||||
# NOTE: these image files don't need to have
|
||||
# cryptographically secure names
|
||||
|
||||
imageUrl = \
|
||||
personId + '/image' + \
|
||||
str(randint(10000000000000, 99999999999999)) + '.png'
|
||||
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
||||
|
||||
iconUrl = \
|
||||
personId + '/avatar' + \
|
||||
str(randint(10000000000000, 99999999999999)) + '.png'
|
||||
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
||||
|
||||
contextDict = {
|
||||
'Emoji': 'toot:Emoji',
|
||||
|
|
|
|||
96
pgp.py
96
pgp.py
|
|
@ -53,9 +53,39 @@ def getPGPpubKey(actorJson: {}) -> str:
|
|||
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:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -72,12 +102,7 @@ def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyFound)
|
||||
|
||||
if '@' not in emailAddress:
|
||||
return
|
||||
if '.' not in emailAddress:
|
||||
return
|
||||
if emailAddress.startswith('@'):
|
||||
if notEmailAddress:
|
||||
return
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
|
|
@ -103,6 +128,13 @@ def setEmailAddress(actorJson: {}, emailAddress: str) -> None:
|
|||
def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -119,8 +151,7 @@ def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyValue)
|
||||
|
||||
if '--BEGIN PGP PUBLIC KEY' not in PGPpubKey:
|
||||
if removeKey:
|
||||
return
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
|
|
@ -141,3 +172,52 @@ def setPGPpubKey(actorJson: {}, PGPpubKey: str) -> None:
|
|||
"value": PGPpubKey
|
||||
}
|
||||
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: {},
|
||||
projectVersion: str, httpPrefix: str,
|
||||
domain: str) -> None:
|
||||
domain: str, depth=0) -> {}:
|
||||
if depth > 10:
|
||||
return None
|
||||
|
||||
feedJson = getJson(session, feedUrl, asHeader, None,
|
||||
projectVersion, httpPrefix, domain)
|
||||
if not feedJson:
|
||||
return
|
||||
return None
|
||||
|
||||
if 'orderedItems' in feedJson:
|
||||
for item in feedJson['orderedItems']:
|
||||
|
|
@ -168,7 +171,8 @@ def parseUserFeed(session, feedUrl: str, asHeader: {},
|
|||
userFeed = \
|
||||
parseUserFeed(session, nextUrl, asHeader,
|
||||
projectVersion, httpPrefix,
|
||||
domain)
|
||||
domain, depth+1)
|
||||
if userFeed:
|
||||
for item in userFeed:
|
||||
yield item
|
||||
elif isinstance(nextUrl, dict):
|
||||
|
|
@ -440,6 +444,58 @@ def getPosts(session, outboxUrl: str, maxPosts: int,
|
|||
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,
|
||||
nickname: str, domain: str, boxname: str) -> None:
|
||||
"""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)
|
||||
|
||||
|
||||
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,
|
||||
nickname: str, domain: str, port: int,
|
||||
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:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -57,16 +69,7 @@ def setSSBAddress(actorJson: {}, ssbAddress: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyFound)
|
||||
|
||||
if not ssbAddress.startswith('@'):
|
||||
return
|
||||
if '=.' not in ssbAddress:
|
||||
return
|
||||
if '"' in ssbAddress:
|
||||
return
|
||||
if ' ' in ssbAddress:
|
||||
return
|
||||
if ',' in ssbAddress:
|
||||
if notSSBAddress:
|
||||
return
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
|
|
|
|||
43
tests.py
43
tests.py
|
|
@ -64,6 +64,8 @@ from media import getAttachmentMediaType
|
|||
from delete import sendDeleteViaServer
|
||||
from inbox import validInbox
|
||||
from inbox import validInboxFilenames
|
||||
from content import dangerousMarkup
|
||||
from content import removeHtml
|
||||
from content import addWebLinks
|
||||
from content import replaceEmojiFromTags
|
||||
from content import addHtmlTags
|
||||
|
|
@ -1873,8 +1875,49 @@ def testSiteIsActive():
|
|||
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():
|
||||
print('Running tests...')
|
||||
testDangerousMarkup()
|
||||
testRemoveHtml()
|
||||
testSiteIsActive()
|
||||
testJsonld()
|
||||
testRemoveTextFormatting()
|
||||
|
|
|
|||
64
theme.py
64
theme.py
|
|
@ -12,6 +12,11 @@ from utils import saveJson
|
|||
from shutil import copyfile
|
||||
|
||||
|
||||
def getThemeFiles() -> []:
|
||||
return ('epicyon.css', 'login.css', 'follow.css',
|
||||
'suspended.css', 'calendar.css', 'blog.css')
|
||||
|
||||
|
||||
def getThemesList() -> []:
|
||||
"""Returns the list of available themes
|
||||
Note that these should be capitalized, since they're
|
||||
|
|
@ -44,8 +49,7 @@ def getTheme(baseDir: str) -> str:
|
|||
|
||||
|
||||
def removeTheme(baseDir: str):
|
||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
||||
'suspended.css', 'calendar.css', 'blog.css')
|
||||
themeFiles = getThemeFiles()
|
||||
for filename in themeFiles:
|
||||
if os.path.isfile(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:
|
||||
"""Uses a dictionary to set a theme
|
||||
"""
|
||||
if name:
|
||||
setThemeInConfig(baseDir, name)
|
||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
||||
'suspended.css', 'calendar.css', 'blog.css')
|
||||
themeFiles = getThemeFiles()
|
||||
for filename in themeFiles:
|
||||
templateFilename = baseDir + '/epicyon-' + filename
|
||||
if filename == 'epicyon.css':
|
||||
|
|
@ -105,6 +109,50 @@ def setThemeFromDict(baseDir: str, name: str, themeParams: {}) -> None:
|
|||
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):
|
||||
"""Uses a dictionary to set a theme
|
||||
"""
|
||||
|
|
@ -124,8 +172,7 @@ def setCustomFont(baseDir: str):
|
|||
if not customFontExt:
|
||||
return
|
||||
|
||||
themeFiles = ('epicyon.css', 'login.css', 'follow.css',
|
||||
'suspended.css', 'calendar.css', 'blog.css')
|
||||
themeFiles = getThemeFiles()
|
||||
for filename in themeFiles:
|
||||
templateFilename = baseDir + '/' + filename
|
||||
if not os.path.isfile(templateFilename):
|
||||
|
|
@ -613,4 +660,9 @@ def setTheme(baseDir: str, name: str) -> bool:
|
|||
result = True
|
||||
|
||||
setCustomFont(baseDir)
|
||||
grayscaleFilename = baseDir + '/accounts/.grayscale'
|
||||
if os.path.isfile(grayscaleFilename):
|
||||
enableGrayscale(baseDir)
|
||||
else:
|
||||
disableGrayscale(baseDir)
|
||||
return result
|
||||
|
|
|
|||
28
tox.py
28
tox.py
|
|
@ -43,6 +43,21 @@ def getToxAddress(actorJson: {}) -> str:
|
|||
def setToxAddress(actorJson: {}, toxAddress: str) -> None:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -59,18 +74,7 @@ def setToxAddress(actorJson: {}, toxAddress: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyFound)
|
||||
|
||||
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:
|
||||
if notToxAddress:
|
||||
return
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@
|
|||
"Matrix": "Matrix",
|
||||
"Email": "البريد الإلكتروني",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "بصمة PGP",
|
||||
"This is a scheduled post.": "هذا هو المقرر المقرر.",
|
||||
"Remove scheduled posts": "إزالة المشاركات المجدولة",
|
||||
"Remove Twitter posts": "إزالة مشاركات Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "حظ أوفر في المرة القادمة",
|
||||
"Unavailable": "غير متوفره",
|
||||
"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",
|
||||
"Email": "Correu electrònic",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Empremta digital PGP",
|
||||
"This is a scheduled post.": "Aquesta és una publicació programada.",
|
||||
"Remove scheduled posts": "Elimineu les publicacions programades",
|
||||
"Remove Twitter posts": "Elimina les publicacions de Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Que tingueu més sort la propera vegada",
|
||||
"Unavailable": "No disponible",
|
||||
"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",
|
||||
"Email": "E-bost",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Olion Bysedd PGP",
|
||||
"This is a scheduled post.": "Mae hon yn swydd wedi'i hamserlennu.",
|
||||
"Remove scheduled posts": "Tynnwch y swyddi a drefnwyd",
|
||||
"Remove Twitter posts": "Dileu postiadau Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Gwell lwc y tro nesaf",
|
||||
"Unavailable": "Ddim ar gael",
|
||||
"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",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "PGP Fingerabdruck",
|
||||
"This is a scheduled post.": "Dies ist ein geplanter Beitrag.",
|
||||
"Remove scheduled posts": "Geplante Posts entfernen",
|
||||
"Remove Twitter posts": "Entfernen Sie Twitter-Posts",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Viel Glück beim nächsten Mal",
|
||||
"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",
|
||||
"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",
|
||||
"Matrix": "Matrix",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP": "PGP Key",
|
||||
"PGP Fingerprint": "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"
|
||||
"Receive calendar events from this account": "Receive calendar events from this account",
|
||||
"Grayscale": "Grayscale"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@
|
|||
"Matrix": "Matrix",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Huella digital PGP",
|
||||
"This is a scheduled post.": "Esta es una publicación programada.",
|
||||
"Remove scheduled posts": "Eliminar publicaciones programadas",
|
||||
"Remove Twitter posts": "Eliminar publicaciones de Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Mejor suerte la próxima vez",
|
||||
"Unavailable": "Indisponible",
|
||||
"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",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Empreinte digitale PGP",
|
||||
"This is a scheduled post.": "Il s'agit d'un article programmé.",
|
||||
"Remove scheduled posts": "Supprimer les messages planifiés",
|
||||
"Remove Twitter posts": "Supprimer les messages Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Plus de chance la prochaine fois",
|
||||
"Unavailable": "Indisponible",
|
||||
"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",
|
||||
"Email": "Ríomhphost",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Méarlorg PGP",
|
||||
"This is a scheduled post.": "Is post sceidealta é seo.",
|
||||
"Remove scheduled posts": "Bain na poist sceidealta",
|
||||
"Remove Twitter posts": "Bain poist Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Ádh níos fearr an chéad uair eile",
|
||||
"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í",
|
||||
"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",
|
||||
"Matrix": "Matrix",
|
||||
"Email": "ईमेल",
|
||||
"PGP": "PGP",
|
||||
"PGP": "पीजीपी",
|
||||
"PGP Fingerprint": "पीजीपी फिंगरप्रिंट",
|
||||
"This is a scheduled post.": "यह एक अनुसूचित पद है।",
|
||||
"Remove scheduled posts": "अनुसूचित पदों को हटा दें",
|
||||
"Remove Twitter posts": "ट्विटर पोस्ट हटाएं",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "अगली बार किस्मत तुम्हारा साथ देगी",
|
||||
"Unavailable": "अनुपलब्ध",
|
||||
"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",
|
||||
"Email": "E-mail",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Impronta digitale PGP",
|
||||
"This is a scheduled post.": "Questo è un post programmato",
|
||||
"Remove scheduled posts": "Rimuovi i post programmati",
|
||||
"Remove Twitter posts": "Rimuovi i post di Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "La prossima volta sarai più fortunato",
|
||||
"Unavailable": "non disponibile",
|
||||
"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",
|
||||
"Email": "Eメール",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "PGPフィンガープリント",
|
||||
"This is a scheduled post.": "これはスケジュールされた投稿です。",
|
||||
"Remove scheduled posts": "スケジュールされた投稿を削除する",
|
||||
"Remove Twitter posts": "Twitterの投稿を削除する",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "次回は幸運を",
|
||||
"Unavailable": "利用できません",
|
||||
"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",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "PGP Fingerprint",
|
||||
"This is a scheduled post.": "This is a scheduled post.",
|
||||
"Remove scheduled posts": "Remove scheduled posts",
|
||||
"Remove Twitter posts": "Remove Twitter posts",
|
||||
|
|
@ -245,5 +246,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"
|
||||
"Receive calendar events from this account": "Receive calendar events from this account",
|
||||
"Grayscale": "Grayscale"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@
|
|||
"Matrix": "Matrix",
|
||||
"Email": "Email",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "Impressão digital PGP",
|
||||
"This is a scheduled post.": "Esta é uma postagem agendada.",
|
||||
"Remove scheduled posts": "Remover postagens agendadas",
|
||||
"Remove Twitter posts": "Remover postagens do Twitter",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Mais sorte da próxima vez",
|
||||
"Unavailable": "Indisponível",
|
||||
"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",
|
||||
"Email": "Эл. адрес",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "PGP Отпечаток пальца",
|
||||
"This is a scheduled post.": "Это запланированный пост.",
|
||||
"Remove scheduled posts": "Удалить запланированные сообщения",
|
||||
"Remove Twitter posts": "Удалить сообщения из Твиттера",
|
||||
|
|
@ -249,5 +250,6 @@
|
|||
"Better luck next time": "Повезет в следующий раз",
|
||||
"Unavailable": "Недоступен",
|
||||
"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",
|
||||
"Email": "电子邮件",
|
||||
"PGP": "PGP",
|
||||
"PGP Fingerprint": "PGP指纹",
|
||||
"This is a scheduled post.": "这是预定的帖子。",
|
||||
"Remove scheduled posts": "删除预定的帖子",
|
||||
"Remove Twitter posts": "删除Twitter帖子",
|
||||
|
|
@ -248,5 +249,6 @@
|
|||
"Better luck next time": "下次好运",
|
||||
"Unavailable": "不可用",
|
||||
"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
|
||||
from socket import error as SocketError
|
||||
import errno
|
||||
from urllib.request import urlopen
|
||||
import urllib.request
|
||||
from pprint import pprint
|
||||
from calendar import monthrange
|
||||
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
|
||||
trying to send posts to it.
|
||||
"""
|
||||
if not url.startswith('http'):
|
||||
return False
|
||||
try:
|
||||
urlopen(url, timeout=10)
|
||||
req = urllib.request.Request(url)
|
||||
urllib.request.urlopen(req, timeout=10) # nosec
|
||||
return True
|
||||
except SocketError as e:
|
||||
if e.errno == errno.ECONNRESET:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from person import personBoxJson
|
|||
from person import isPersonSnoozed
|
||||
from pgp import getEmailAddress
|
||||
from pgp import getPGPpubKey
|
||||
from pgp import getPGPfingerprint
|
||||
from xmpp import getXmppAddress
|
||||
from ssb import getSSBAddress
|
||||
from tox import getToxAddress
|
||||
|
|
@ -62,6 +63,7 @@ from content import getMentionsFromHtml
|
|||
from content import addHtmlTags
|
||||
from content import replaceEmojiFromTags
|
||||
from content import removeLongWords
|
||||
from content import removeHtml
|
||||
from config import getConfigParam
|
||||
from skills import getSkills
|
||||
from cache import getPersonFromCache
|
||||
|
|
@ -1050,6 +1052,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
donateUrl = ''
|
||||
emailAddress = ''
|
||||
PGPpubKey = ''
|
||||
PGPfingerprint = ''
|
||||
xmppAddress = ''
|
||||
matrixAddress = ''
|
||||
ssbAddress = ''
|
||||
|
|
@ -1066,6 +1069,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
toxAddress = getToxAddress(actorJson)
|
||||
emailAddress = getEmailAddress(actorJson)
|
||||
PGPpubKey = getPGPpubKey(actorJson)
|
||||
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||
if actorJson.get('name'):
|
||||
displayNickname = actorJson['name']
|
||||
if actorJson.get('summary'):
|
||||
|
|
@ -1239,6 +1243,15 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
themes = getThemesList()
|
||||
themesDropdown = '<div class="container">'
|
||||
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" ' + \
|
||||
'name="themeDropdown" class="theme">'
|
||||
for themeName in themes:
|
||||
|
|
@ -1331,6 +1344,12 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
translate['Email'] + '</label><br>'
|
||||
editProfileForm += \
|
||||
' <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 += \
|
||||
'<label class="labels">' + translate['PGP'] + '</label><br>'
|
||||
editProfileForm += \
|
||||
|
|
@ -2550,13 +2569,15 @@ def htmlProfile(defaultTimeline: str,
|
|||
donateSection = ''
|
||||
donateUrl = getDonationUrl(profileJson)
|
||||
PGPpubKey = getPGPpubKey(profileJson)
|
||||
PGPfingerprint = getPGPfingerprint(profileJson)
|
||||
emailAddress = getEmailAddress(profileJson)
|
||||
xmppAddress = getXmppAddress(profileJson)
|
||||
matrixAddress = getMatrixAddress(profileJson)
|
||||
ssbAddress = getSSBAddress(profileJson)
|
||||
toxAddress = getToxAddress(profileJson)
|
||||
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 += ' <center>\n'
|
||||
if donateUrl:
|
||||
|
|
@ -2583,6 +2604,10 @@ def htmlProfile(defaultTimeline: str,
|
|||
donateSection += \
|
||||
'<p>Tox: <label class="ssbaddr">' + \
|
||||
toxAddress + '</label></p>\n'
|
||||
if PGPfingerprint:
|
||||
donateSection += \
|
||||
'<p class="pgp">PGP: ' + \
|
||||
PGPfingerprint.replace('\n', '<br>') + '</p>\n'
|
||||
if PGPpubKey:
|
||||
donateSection += \
|
||||
'<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
|
||||
|
|
@ -3800,7 +3825,14 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
likeIcon = 'like_inactive.png'
|
||||
likeLink = 'like'
|
||||
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'
|
||||
if likedByPerson(postJsonObject, nickname, fullDomain):
|
||||
likeLink = 'unlike'
|
||||
|
|
@ -3811,9 +3843,10 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
pageNumberParam + \
|
||||
'?actor=' + postJsonObject['actor'] + \
|
||||
'?bm=' + timelinePostBookmark + \
|
||||
'?tl=' + boxName + '" title="' + likeTitle + '">'
|
||||
'?tl=' + boxName + '" title="' + \
|
||||
likeTitle + likeCountStr + '">'
|
||||
likeStr += \
|
||||
'<img loading="lazy" title="' + likeTitle + \
|
||||
'<img loading="lazy" title="' + likeTitle + likeCountStr + \
|
||||
'" alt="' + likeTitle + \
|
||||
' |" src="/' + iconsDir + '/' + likeIcon + '"/></a>'
|
||||
|
||||
|
|
@ -5314,6 +5347,7 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
blogAddress: str,
|
||||
toxAddress: str,
|
||||
PGPpubKey: str,
|
||||
PGPfingerprint: str,
|
||||
emailAddress) -> str:
|
||||
"""Show options for a person: view/follow/block/report
|
||||
"""
|
||||
|
|
@ -5411,6 +5445,9 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
if toxAddress:
|
||||
optionsStr += \
|
||||
'<p class="imText">Tox: ' + toxAddress + '</p>'
|
||||
if PGPfingerprint:
|
||||
optionsStr += '<p class="pgp">PGP: ' + \
|
||||
PGPfingerprint.replace('\n', '<br>') + '</p>'
|
||||
if PGPpubKey:
|
||||
optionsStr += '<p class="pgp">' + \
|
||||
PGPpubKey.replace('\n', '<br>') + '</p>'
|
||||
|
|
@ -6196,6 +6233,8 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
|
|||
profileJson['summary'].replace('<br>', '\n')
|
||||
avatarDescription = avatarDescription.replace('<p>', '')
|
||||
avatarDescription = avatarDescription.replace('</p>', '')
|
||||
if '<' in avatarDescription:
|
||||
avatarDescription = removeHtml(avatarDescription)
|
||||
profileStr = ' <div class="hero-image">'
|
||||
profileStr += ' <div class="hero-text">'
|
||||
if avatarUrl:
|
||||
|
|
|
|||
|
|
@ -1264,7 +1264,7 @@
|
|||
<p class="intro">You will need python version 3.7 or later.</p>
|
||||
<p class="intro">On a Debian based system:</p>
|
||||
<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>
|
||||
|
||||
<p class="intro">
|
||||
|
|
|
|||
15
xmpp.py
15
xmpp.py
|
|
@ -36,6 +36,14 @@ def getXmppAddress(actorJson: {}) -> str:
|
|||
def setXmppAddress(actorJson: {}, xmppAddress: str) -> None:
|
||||
"""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'):
|
||||
actorJson['attachment'] = []
|
||||
|
||||
|
|
@ -53,12 +61,7 @@ def setXmppAddress(actorJson: {}, xmppAddress: str) -> None:
|
|||
break
|
||||
if propertyFound:
|
||||
actorJson['attachment'].remove(propertyFound)
|
||||
|
||||
if '@' not in xmppAddress:
|
||||
return
|
||||
if '.' not in xmppAddress:
|
||||
return
|
||||
if '"' in xmppAddress:
|
||||
if notXmppAddress:
|
||||
return
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
|
|
|
|||
Loading…
Reference in New Issue