mirror of https://gitlab.com/bashrc2/epicyon
Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
commit
5d87949c6c
|
|
@ -118,6 +118,9 @@ def removeBlock(baseDir: str, nickname: str, domain: str,
|
||||||
def isBlockedHashtag(baseDir: str, hashtag: str) -> bool:
|
def isBlockedHashtag(baseDir: str, hashtag: str) -> bool:
|
||||||
"""Is the given hashtag blocked?
|
"""Is the given hashtag blocked?
|
||||||
"""
|
"""
|
||||||
|
# avoid very long hashtags
|
||||||
|
if len(hashtag) > 32:
|
||||||
|
return True
|
||||||
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
||||||
if os.path.isfile(globalBlockingFilename):
|
if os.path.isfile(globalBlockingFilename):
|
||||||
hashtag = hashtag.strip('\n').strip('\r')
|
hashtag = hashtag.strip('\n').strip('\r')
|
||||||
|
|
|
||||||
2
blog.py
2
blog.py
|
|
@ -170,6 +170,8 @@ def htmlBlogPostContent(authorized: bool,
|
||||||
|
|
||||||
# get the handle of the author
|
# get the handle of the author
|
||||||
if postJsonObject['object'].get('attributedTo'):
|
if postJsonObject['object'].get('attributedTo'):
|
||||||
|
authorNickname = None
|
||||||
|
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||||
actor = postJsonObject['object']['attributedTo']
|
actor = postJsonObject['object']['attributedTo']
|
||||||
authorNickname = getNicknameFromActor(actor)
|
authorNickname = getNicknameFromActor(actor)
|
||||||
if authorNickname:
|
if authorNickname:
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,9 @@ def addWebLinks(content: str) -> str:
|
||||||
def validHashTag(hashtag: str) -> bool:
|
def validHashTag(hashtag: str) -> bool:
|
||||||
"""Returns true if the give hashtag contains valid characters
|
"""Returns true if the give hashtag contains valid characters
|
||||||
"""
|
"""
|
||||||
|
# long hashtags are not valid
|
||||||
|
if len(hashtag) >= 32:
|
||||||
|
return False
|
||||||
validChars = set('0123456789' +
|
validChars = set('0123456789' +
|
||||||
'abcdefghijklmnopqrstuvwxyz' +
|
'abcdefghijklmnopqrstuvwxyz' +
|
||||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||||
|
|
|
||||||
250
daemon.py
250
daemon.py
|
|
@ -43,6 +43,8 @@ from matrix import getMatrixAddress
|
||||||
from matrix import setMatrixAddress
|
from matrix import setMatrixAddress
|
||||||
from donate import getDonationUrl
|
from donate import getDonationUrl
|
||||||
from donate import setDonationUrl
|
from donate import setDonationUrl
|
||||||
|
from person import setPersonNotes
|
||||||
|
from person import getDefaultPersonContext
|
||||||
from person import savePersonQrcode
|
from person import savePersonQrcode
|
||||||
from person import randomizeActorImages
|
from person import randomizeActorImages
|
||||||
from person import personUpgradeActor
|
from person import personUpgradeActor
|
||||||
|
|
@ -191,6 +193,9 @@ from bookmarks import undoBookmark
|
||||||
from petnames import setPetName
|
from petnames import setPetName
|
||||||
from followingCalendar import addPersonToCalendar
|
from followingCalendar import addPersonToCalendar
|
||||||
from followingCalendar import removePersonFromCalendar
|
from followingCalendar import removePersonFromCalendar
|
||||||
|
from devices import E2EEdevicesCollection
|
||||||
|
from devices import E2EEvalidDevice
|
||||||
|
from devices import E2EEaddDevice
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1047,6 +1052,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def _isAuthorized(self) -> bool:
|
def _isAuthorized(self) -> bool:
|
||||||
|
self.authorizedNickname = None
|
||||||
|
|
||||||
if self.path.startswith('/icons/') or \
|
if self.path.startswith('/icons/') or \
|
||||||
self.path.startswith('/avatars/') or \
|
self.path.startswith('/avatars/') or \
|
||||||
self.path.startswith('/favicon.ico'):
|
self.path.startswith('/favicon.ico'):
|
||||||
|
|
@ -1060,6 +1067,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
tokenStr = tokenStr.split(';')[0].strip()
|
tokenStr = tokenStr.split(';')[0].strip()
|
||||||
if self.server.tokensLookup.get(tokenStr):
|
if self.server.tokensLookup.get(tokenStr):
|
||||||
nickname = self.server.tokensLookup[tokenStr]
|
nickname = self.server.tokensLookup[tokenStr]
|
||||||
|
self.authorizedNickname = nickname
|
||||||
# default to the inbox of the person
|
# default to the inbox of the person
|
||||||
if self.path == '/':
|
if self.path == '/':
|
||||||
self.path = '/users/' + nickname + '/inbox'
|
self.path = '/users/' + nickname + '/inbox'
|
||||||
|
|
@ -1535,6 +1543,25 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self._404()
|
self._404()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# list of registered devices for e2ee
|
||||||
|
# see https://github.com/tootsuite/mastodon/pull/13820
|
||||||
|
if authorized and '/users/' in self.path:
|
||||||
|
if self.path.endswith('/collections/devices'):
|
||||||
|
nickname = self.path.split('/users/')
|
||||||
|
if '/' in nickname:
|
||||||
|
nickname = nickname.split('/')[0]
|
||||||
|
devJson = E2EEdevicesCollection(self.server.baseDir,
|
||||||
|
nickname,
|
||||||
|
self.server.domain,
|
||||||
|
self.server.domainFull,
|
||||||
|
self.server.httpPrefix)
|
||||||
|
msg = json.dumps(devJson,
|
||||||
|
ensure_ascii=False).encode('utf-8')
|
||||||
|
self._set_headers('application/json',
|
||||||
|
len(msg),
|
||||||
|
None, callingDomain)
|
||||||
|
self._write(msg)
|
||||||
|
|
||||||
if htmlGET and '/users/' in self.path:
|
if htmlGET and '/users/' in self.path:
|
||||||
# show the person options screen with view/follow/block/report
|
# show the person options screen with view/follow/block/report
|
||||||
if '?options=' in self.path:
|
if '?options=' in self.path:
|
||||||
|
|
@ -1637,7 +1664,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
# remove a shared item
|
# remove a shared item
|
||||||
if htmlGET and '?rmshare=' in self.path:
|
if htmlGET and '?rmshare=' in self.path:
|
||||||
shareName = self.path.split('?rmshare=')[1]
|
shareName = self.path.split('?rmshare=')[1]
|
||||||
shareName = urllib.parse.unquote(shareName.strip())
|
shareName = urllib.parse.unquote_plus(shareName.strip())
|
||||||
usersPath = self.path.split('?rmshare=')[0]
|
usersPath = self.path.split('?rmshare=')[0]
|
||||||
actor = \
|
actor = \
|
||||||
self.server.httpPrefix + '://' + \
|
self.server.httpPrefix + '://' + \
|
||||||
|
|
@ -3336,7 +3363,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
shareDescription = \
|
shareDescription = \
|
||||||
inReplyToUrl.replace('sharedesc:', '')
|
inReplyToUrl.replace('sharedesc:', '')
|
||||||
shareDescription = \
|
shareDescription = \
|
||||||
urllib.parse.unquote(shareDescription.strip())
|
urllib.parse.unquote_plus(shareDescription.strip())
|
||||||
self.path = self.path.split('?replydm=')[0]+'/newdm'
|
self.path = self.path.split('?replydm=')[0]+'/newdm'
|
||||||
if self.server.debug:
|
if self.server.debug:
|
||||||
print('DEBUG: replydm path ' + self.path)
|
print('DEBUG: replydm path ' + self.path)
|
||||||
|
|
@ -5754,6 +5781,159 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
postBytes, boundary)
|
postBytes, boundary)
|
||||||
return pageNumber
|
return pageNumber
|
||||||
|
|
||||||
|
def _cryptoAPIreadHandle(self):
|
||||||
|
"""Reads handle
|
||||||
|
"""
|
||||||
|
messageBytes = None
|
||||||
|
maxDeviceIdLength = 2048
|
||||||
|
length = int(self.headers['Content-length'])
|
||||||
|
if length >= maxDeviceIdLength:
|
||||||
|
print('WARN: handle post to crypto API is too long ' +
|
||||||
|
str(length) + ' bytes')
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
messageBytes = self.rfile.read(length)
|
||||||
|
except SocketError as e:
|
||||||
|
if e.errno == errno.ECONNRESET:
|
||||||
|
print('WARN: handle POST messageBytes ' +
|
||||||
|
'connection reset by peer')
|
||||||
|
else:
|
||||||
|
print('WARN: handle POST messageBytes socket error')
|
||||||
|
return {}
|
||||||
|
except ValueError as e:
|
||||||
|
print('ERROR: handle POST messageBytes rfile.read failed')
|
||||||
|
print(e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
lenMessage = len(messageBytes)
|
||||||
|
if lenMessage > 2048:
|
||||||
|
print('WARN: handle post to crypto API is too long ' +
|
||||||
|
str(lenMessage) + ' bytes')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
handle = messageBytes.decode("utf-8")
|
||||||
|
if not handle:
|
||||||
|
return None
|
||||||
|
if '@' not in handle:
|
||||||
|
return None
|
||||||
|
if '[' in handle:
|
||||||
|
return json.loads(messageBytes)
|
||||||
|
if handle.startswith('@'):
|
||||||
|
handle = handle[1:]
|
||||||
|
if '@' not in handle:
|
||||||
|
return None
|
||||||
|
return handle.strip()
|
||||||
|
|
||||||
|
def _cryptoAPIreadJson(self) -> {}:
|
||||||
|
"""Obtains json from POST to the crypto API
|
||||||
|
"""
|
||||||
|
messageBytes = None
|
||||||
|
maxCryptoMessageLength = 10240
|
||||||
|
length = int(self.headers['Content-length'])
|
||||||
|
if length >= maxCryptoMessageLength:
|
||||||
|
print('WARN: post to crypto API is too long ' +
|
||||||
|
str(length) + ' bytes')
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
messageBytes = self.rfile.read(length)
|
||||||
|
except SocketError as e:
|
||||||
|
if e.errno == errno.ECONNRESET:
|
||||||
|
print('WARN: POST messageBytes ' +
|
||||||
|
'connection reset by peer')
|
||||||
|
else:
|
||||||
|
print('WARN: POST messageBytes socket error')
|
||||||
|
return {}
|
||||||
|
except ValueError as e:
|
||||||
|
print('ERROR: POST messageBytes rfile.read failed')
|
||||||
|
print(e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
lenMessage = len(messageBytes)
|
||||||
|
if lenMessage > 10240:
|
||||||
|
print('WARN: post to crypto API is too long ' +
|
||||||
|
str(lenMessage) + ' bytes')
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return json.loads(messageBytes)
|
||||||
|
|
||||||
|
def _cryptoAPIQuery(self, callingDomain: str) -> bool:
|
||||||
|
handle = self._cryptoAPIreadHandle()
|
||||||
|
if not handle:
|
||||||
|
return False
|
||||||
|
if isinstance(handle, str):
|
||||||
|
personDir = self.server.baseDir + '/accounts/' + handle
|
||||||
|
if not os.path.isdir(personDir + '/devices'):
|
||||||
|
return False
|
||||||
|
devicesList = []
|
||||||
|
for subdir, dirs, files in os.walk(personDir + '/devices'):
|
||||||
|
for f in files:
|
||||||
|
deviceFilename = os.path.join(personDir + '/devices', f)
|
||||||
|
if not os.path.isfile(deviceFilename):
|
||||||
|
continue
|
||||||
|
contentJson = loadJson(deviceFilename)
|
||||||
|
if contentJson:
|
||||||
|
devicesList.append(contentJson)
|
||||||
|
# return the list of devices for this handle
|
||||||
|
msg = \
|
||||||
|
json.dumps(devicesList,
|
||||||
|
ensure_ascii=False).encode('utf-8')
|
||||||
|
self._set_headers('application/json',
|
||||||
|
len(msg),
|
||||||
|
None, callingDomain)
|
||||||
|
self._write(msg)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _cryptoAPI(self, path: str, authorized: bool) -> None:
|
||||||
|
"""POST or GET with the crypto API
|
||||||
|
"""
|
||||||
|
if authorized and path.startswith('/api/v1/crypto/keys/upload'):
|
||||||
|
# register a device to an authorized account
|
||||||
|
if not self.authorizedNickname:
|
||||||
|
self._400()
|
||||||
|
return
|
||||||
|
deviceKeys = self._cryptoAPIreadJson()
|
||||||
|
if not deviceKeys:
|
||||||
|
self._400()
|
||||||
|
return
|
||||||
|
if isinstance(deviceKeys, dict):
|
||||||
|
if not E2EEvalidDevice(deviceKeys):
|
||||||
|
self._400()
|
||||||
|
return
|
||||||
|
E2EEaddDevice(self.server.baseDir,
|
||||||
|
self.authorizedNickname,
|
||||||
|
self.server.domain,
|
||||||
|
deviceKeys['deviceId'],
|
||||||
|
deviceKeys['name'],
|
||||||
|
deviceKeys['claim'],
|
||||||
|
deviceKeys['fingerprintKey']['publicKeyBase64'],
|
||||||
|
deviceKeys['identityKey']['publicKeyBase64'],
|
||||||
|
deviceKeys['fingerprintKey']['type'],
|
||||||
|
deviceKeys['identityKey']['type'])
|
||||||
|
self._200()
|
||||||
|
return
|
||||||
|
self._400()
|
||||||
|
elif path.startswith('/api/v1/crypto/keys/query'):
|
||||||
|
# given a handle (nickname@domain) return the devices
|
||||||
|
# registered to that handle
|
||||||
|
if not self._cryptoAPIQuery():
|
||||||
|
self._400()
|
||||||
|
elif path.startswith('/api/v1/crypto/keys/claim'):
|
||||||
|
# TODO
|
||||||
|
self._200()
|
||||||
|
elif authorized and path.startswith('/api/v1/crypto/delivery'):
|
||||||
|
# TODO
|
||||||
|
self._200()
|
||||||
|
elif (authorized and
|
||||||
|
path.startswith('/api/v1/crypto/encrypted_messages/clear')):
|
||||||
|
# TODO
|
||||||
|
self._200()
|
||||||
|
elif path.startswith('/api/v1/crypto/encrypted_messages'):
|
||||||
|
# TODO
|
||||||
|
self._200()
|
||||||
|
else:
|
||||||
|
self._400()
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
POSTstartTime = time.time()
|
POSTstartTime = time.time()
|
||||||
POSTtimings = []
|
POSTtimings = []
|
||||||
|
|
@ -5827,6 +6007,11 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
print('POST Not authorized')
|
print('POST Not authorized')
|
||||||
print(str(self.headers))
|
print(str(self.headers))
|
||||||
|
|
||||||
|
if self.path.startswith('/api/v1/crypto/'):
|
||||||
|
self._cryptoAPI(self.path, authorized)
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return
|
||||||
|
|
||||||
# if this is a POST to the outbox then check authentication
|
# if this is a POST to the outbox then check authentication
|
||||||
self.outboxAuthenticated = False
|
self.outboxAuthenticated = False
|
||||||
self.postToNickname = None
|
self.postToNickname = None
|
||||||
|
|
@ -6616,6 +6801,12 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
os.remove(gitProjectsFilename)
|
os.remove(gitProjectsFilename)
|
||||||
# save actor json file within accounts
|
# save actor json file within accounts
|
||||||
if actorChanged:
|
if actorChanged:
|
||||||
|
# update the context for the actor
|
||||||
|
actorJson['@context'] = [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
getDefaultPersonContext()
|
||||||
|
]
|
||||||
randomizeActorImages(actorJson)
|
randomizeActorImages(actorJson)
|
||||||
saveJson(actorJson, actorFilename)
|
saveJson(actorJson, actorFilename)
|
||||||
webfingerUpdate(self.server.baseDir,
|
webfingerUpdate(self.server.baseDir,
|
||||||
|
|
@ -6706,9 +6897,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if '=' in moderationStr:
|
if '=' in moderationStr:
|
||||||
moderationText = \
|
moderationText = \
|
||||||
moderationStr.split('=')[1].strip()
|
moderationStr.split('=')[1].strip()
|
||||||
moderationText = moderationText.replace('+', ' ')
|
modText = moderationText.replace('+', ' ')
|
||||||
moderationText = \
|
moderationText = \
|
||||||
urllib.parse.unquote(moderationText.strip())
|
urllib.parse.unquote_plus(modText.strip())
|
||||||
elif moderationStr.startswith('submitInfo'):
|
elif moderationStr.startswith('submitInfo'):
|
||||||
msg = htmlModerationInfo(self.server.translate,
|
msg = htmlModerationInfo(self.server.translate,
|
||||||
self.server.baseDir,
|
self.server.baseDir,
|
||||||
|
|
@ -6882,7 +7073,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
questionParams = questionParams.replace('+', ' ')
|
questionParams = questionParams.replace('+', ' ')
|
||||||
questionParams = questionParams.replace('%3F', '')
|
questionParams = questionParams.replace('%3F', '')
|
||||||
questionParams = \
|
questionParams = \
|
||||||
urllib.parse.unquote(questionParams.strip())
|
urllib.parse.unquote_plus(questionParams.strip())
|
||||||
# post being voted on
|
# post being voted on
|
||||||
messageId = None
|
messageId = None
|
||||||
if 'messageId=' in questionParams:
|
if 'messageId=' in questionParams:
|
||||||
|
|
@ -6964,9 +7155,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
searchStr = searchParams.split('searchtext=')[1]
|
searchStr = searchParams.split('searchtext=')[1]
|
||||||
if '&' in searchStr:
|
if '&' in searchStr:
|
||||||
searchStr = searchStr.split('&')[0]
|
searchStr = searchStr.split('&')[0]
|
||||||
searchStr = searchStr.replace('+', ' ')
|
|
||||||
searchStr = \
|
searchStr = \
|
||||||
urllib.parse.unquote(searchStr.strip())
|
urllib.parse.unquote_plus(searchStr.strip())
|
||||||
searchStr2 = searchStr.lower().strip('\n').strip('\r')
|
searchStr2 = searchStr.lower().strip('\n').strip('\r')
|
||||||
print('searchStr: ' + searchStr)
|
print('searchStr: ' + searchStr)
|
||||||
if searchForEmoji:
|
if searchForEmoji:
|
||||||
|
|
@ -7172,7 +7362,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
removeShareConfirmParams = \
|
removeShareConfirmParams = \
|
||||||
removeShareConfirmParams.replace('+', ' ').strip()
|
removeShareConfirmParams.replace('+', ' ').strip()
|
||||||
removeShareConfirmParams = \
|
removeShareConfirmParams = \
|
||||||
urllib.parse.unquote(removeShareConfirmParams)
|
urllib.parse.unquote_plus(removeShareConfirmParams)
|
||||||
shareActor = removeShareConfirmParams.split('actor=')[1]
|
shareActor = removeShareConfirmParams.split('actor=')[1]
|
||||||
if '&' in shareActor:
|
if '&' in shareActor:
|
||||||
shareActor = shareActor.split('&')[0]
|
shareActor = shareActor.split('&')[0]
|
||||||
|
|
@ -7235,7 +7425,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitYes=' in removePostConfirmParams:
|
if '&submitYes=' in removePostConfirmParams:
|
||||||
removePostConfirmParams = \
|
removePostConfirmParams = \
|
||||||
urllib.parse.unquote(removePostConfirmParams)
|
urllib.parse.unquote_plus(removePostConfirmParams)
|
||||||
removeMessageId = \
|
removeMessageId = \
|
||||||
removePostConfirmParams.split('messageId=')[1]
|
removePostConfirmParams.split('messageId=')[1]
|
||||||
if '&' in removeMessageId:
|
if '&' in removeMessageId:
|
||||||
|
|
@ -7327,7 +7517,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitView=' in followConfirmParams:
|
if '&submitView=' in followConfirmParams:
|
||||||
followingActor = \
|
followingActor = \
|
||||||
urllib.parse.unquote(followConfirmParams)
|
urllib.parse.unquote_plus(followConfirmParams)
|
||||||
followingActor = followingActor.split('actor=')[1]
|
followingActor = followingActor.split('actor=')[1]
|
||||||
if '&' in followingActor:
|
if '&' in followingActor:
|
||||||
followingActor = followingActor.split('&')[0]
|
followingActor = followingActor.split('&')[0]
|
||||||
|
|
@ -7336,7 +7526,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitYes=' in followConfirmParams:
|
if '&submitYes=' in followConfirmParams:
|
||||||
followingActor = \
|
followingActor = \
|
||||||
urllib.parse.unquote(followConfirmParams)
|
urllib.parse.unquote_plus(followConfirmParams)
|
||||||
followingActor = followingActor.split('actor=')[1]
|
followingActor = followingActor.split('actor=')[1]
|
||||||
if '&' in followingActor:
|
if '&' in followingActor:
|
||||||
followingActor = followingActor.split('&')[0]
|
followingActor = followingActor.split('&')[0]
|
||||||
|
|
@ -7409,7 +7599,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitYes=' in followConfirmParams:
|
if '&submitYes=' in followConfirmParams:
|
||||||
followingActor = \
|
followingActor = \
|
||||||
urllib.parse.unquote(followConfirmParams)
|
urllib.parse.unquote_plus(followConfirmParams)
|
||||||
followingActor = followingActor.split('actor=')[1]
|
followingActor = followingActor.split('actor=')[1]
|
||||||
if '&' in followingActor:
|
if '&' in followingActor:
|
||||||
followingActor = followingActor.split('&')[0]
|
followingActor = followingActor.split('&')[0]
|
||||||
|
|
@ -7503,7 +7693,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitYes=' in blockConfirmParams:
|
if '&submitYes=' in blockConfirmParams:
|
||||||
blockingActor = \
|
blockingActor = \
|
||||||
urllib.parse.unquote(blockConfirmParams)
|
urllib.parse.unquote_plus(blockConfirmParams)
|
||||||
blockingActor = blockingActor.split('actor=')[1]
|
blockingActor = blockingActor.split('actor=')[1]
|
||||||
if '&' in blockingActor:
|
if '&' in blockingActor:
|
||||||
blockingActor = blockingActor.split('&')[0]
|
blockingActor = blockingActor.split('&')[0]
|
||||||
|
|
@ -7600,7 +7790,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return
|
return
|
||||||
if '&submitYes=' in blockConfirmParams:
|
if '&submitYes=' in blockConfirmParams:
|
||||||
blockingActor = \
|
blockingActor = \
|
||||||
urllib.parse.unquote(blockConfirmParams)
|
urllib.parse.unquote_plus(blockConfirmParams)
|
||||||
blockingActor = blockingActor.split('actor=')[1]
|
blockingActor = blockingActor.split('actor=')[1]
|
||||||
if '&' in blockingActor:
|
if '&' in blockingActor:
|
||||||
blockingActor = blockingActor.split('&')[0]
|
blockingActor = blockingActor.split('&')[0]
|
||||||
|
|
@ -7698,7 +7888,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.POSTbusy = False
|
self.server.POSTbusy = False
|
||||||
return
|
return
|
||||||
optionsConfirmParams = \
|
optionsConfirmParams = \
|
||||||
urllib.parse.unquote(optionsConfirmParams)
|
urllib.parse.unquote_plus(optionsConfirmParams)
|
||||||
# page number to return to
|
# page number to return to
|
||||||
if 'pageNumber=' in optionsConfirmParams:
|
if 'pageNumber=' in optionsConfirmParams:
|
||||||
pageNumberStr = optionsConfirmParams.split('pageNumber=')[1]
|
pageNumberStr = optionsConfirmParams.split('pageNumber=')[1]
|
||||||
|
|
@ -7731,6 +7921,16 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
'?' in petname or '#' in petname:
|
'?' in petname or '#' in petname:
|
||||||
petname = None
|
petname = None
|
||||||
|
|
||||||
|
personNotes = None
|
||||||
|
if 'optionnotes' in optionsConfirmParams:
|
||||||
|
personNotes = optionsConfirmParams.split('optionnotes=')[1]
|
||||||
|
if '&' in personNotes:
|
||||||
|
personNotes = personNotes.split('&')[0]
|
||||||
|
personNotes = urllib.parse.unquote_plus(personNotes.strip())
|
||||||
|
# Limit the length of the notes
|
||||||
|
if len(personNotes) > 64000:
|
||||||
|
personNotes = None
|
||||||
|
|
||||||
optionsNickname = getNicknameFromActor(optionsActor)
|
optionsNickname = getNicknameFromActor(optionsActor)
|
||||||
if not optionsNickname:
|
if not optionsNickname:
|
||||||
if callingDomain.endswith('.onion') and \
|
if callingDomain.endswith('.onion') and \
|
||||||
|
|
@ -7773,7 +7973,23 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
chooserNickname,
|
chooserNickname,
|
||||||
self.server.domain,
|
self.server.domain,
|
||||||
handle, petname)
|
handle, petname)
|
||||||
self._redirect_headers(originPathStr + '/' +
|
self._redirect_headers(usersPath + '/' +
|
||||||
|
self.server.defaultTimeline +
|
||||||
|
'?page='+str(pageNumber), cookie,
|
||||||
|
callingDomain)
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return
|
||||||
|
if '&submitPersonNotes=' in optionsConfirmParams:
|
||||||
|
if self.server.debug:
|
||||||
|
print('Change person notes')
|
||||||
|
handle = optionsNickname + '@' + optionsDomainFull
|
||||||
|
if not personNotes:
|
||||||
|
personNotes = ''
|
||||||
|
setPersonNotes(self.server.baseDir,
|
||||||
|
chooserNickname,
|
||||||
|
self.server.domain,
|
||||||
|
handle, personNotes)
|
||||||
|
self._redirect_headers(usersPath + '/' +
|
||||||
self.server.defaultTimeline +
|
self.server.defaultTimeline +
|
||||||
'?page='+str(pageNumber), cookie,
|
'?page='+str(pageNumber), cookie,
|
||||||
callingDomain)
|
callingDomain)
|
||||||
|
|
@ -7797,7 +8013,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.domain,
|
self.server.domain,
|
||||||
optionsNickname,
|
optionsNickname,
|
||||||
optionsDomainFull)
|
optionsDomainFull)
|
||||||
self._redirect_headers(originPathStr + '/' +
|
self._redirect_headers(usersPath + '/' +
|
||||||
self.server.defaultTimeline +
|
self.server.defaultTimeline +
|
||||||
'?page='+str(pageNumber), cookie,
|
'?page='+str(pageNumber), cookie,
|
||||||
callingDomain)
|
callingDomain)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
__filename__ = "devices.py"
|
||||||
|
__author__ = "Bob Mottram"
|
||||||
|
__license__ = "AGPL3+"
|
||||||
|
__version__ = "1.1.0"
|
||||||
|
__maintainer__ = "Bob Mottram"
|
||||||
|
__email__ = "bob@freedombone.net"
|
||||||
|
__status__ = "Production"
|
||||||
|
|
||||||
|
# REST API overview
|
||||||
|
#
|
||||||
|
# To support Olm, the following APIs are required:
|
||||||
|
#
|
||||||
|
# * Uploading keys for a device (current app)
|
||||||
|
# POST /api/v1/crypto/keys/upload
|
||||||
|
#
|
||||||
|
# * Querying available devices of people you want to establish a session with
|
||||||
|
# POST /api/v1/crypto/keys/query
|
||||||
|
#
|
||||||
|
# * Claiming a pre-key (one-time-key) for each device you want to establish
|
||||||
|
# a session with
|
||||||
|
# POST /api/v1/crypto/keys/claim
|
||||||
|
#
|
||||||
|
# * Sending encrypted messages directly to specific devices of other people
|
||||||
|
# POST /api/v1/crypto/delivery
|
||||||
|
#
|
||||||
|
# * Collect encrypted messages addressed to the current device
|
||||||
|
# GET /api/v1/crypto/encrypted_messages
|
||||||
|
#
|
||||||
|
# * Clear all encrypted messages addressed to the current device
|
||||||
|
# POST /api/v1/crypto/encrypted_messages/clear
|
||||||
|
|
||||||
|
import os
|
||||||
|
from utils import loadJson
|
||||||
|
from utils import saveJson
|
||||||
|
|
||||||
|
|
||||||
|
def E2EEremoveDevice(baseDir: str, nickname: str, domain: str,
|
||||||
|
deviceId: str) -> bool:
|
||||||
|
"""Unregisters a device for e2ee
|
||||||
|
"""
|
||||||
|
personDir = baseDir + '/accounts/' + nickname + '@' + domain
|
||||||
|
deviceFilename = personDir + '/devices/' + deviceId + '.json'
|
||||||
|
if os.path.isfile(deviceFilename):
|
||||||
|
os.remove(deviceFilename)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def E2EEvalidDevice(deviceJson: {}) -> bool:
|
||||||
|
"""Returns true if the given json contains valid device keys
|
||||||
|
"""
|
||||||
|
if not isinstance(deviceJson, dict):
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('deviceId'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['deviceId'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('type'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['type'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('name'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['name'], str):
|
||||||
|
return False
|
||||||
|
if deviceJson['type'] != 'Device':
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('claim'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['claim'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('fingerprintKey'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['fingerprintKey'], dict):
|
||||||
|
return False
|
||||||
|
if not deviceJson['fingerprintKey'].get('type'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['fingerprintKey']['type'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson['fingerprintKey'].get('publicKeyBase64'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['fingerprintKey']['publicKeyBase64'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson.get('identityKey'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['identityKey'], dict):
|
||||||
|
return False
|
||||||
|
if not deviceJson['identityKey'].get('type'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['identityKey']['type'], str):
|
||||||
|
return False
|
||||||
|
if not deviceJson['identityKey'].get('publicKeyBase64'):
|
||||||
|
return False
|
||||||
|
if not isinstance(deviceJson['identityKey']['publicKeyBase64'], str):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def E2EEaddDevice(baseDir: str, nickname: str, domain: str,
|
||||||
|
deviceId: str, name: str, claimUrl: str,
|
||||||
|
fingerprintPublicKey: str,
|
||||||
|
identityPublicKey: str,
|
||||||
|
fingerprintKeyType="Ed25519Key",
|
||||||
|
identityKeyType="Curve25519Key") -> bool:
|
||||||
|
"""Registers a device for e2ee
|
||||||
|
claimUrl could be something like:
|
||||||
|
http://localhost:3000/users/admin/claim?id=11119
|
||||||
|
"""
|
||||||
|
if ' ' in deviceId or '/' in deviceId or \
|
||||||
|
'?' in deviceId or '#' in deviceId or \
|
||||||
|
'.' in deviceId:
|
||||||
|
return False
|
||||||
|
personDir = baseDir + '/accounts/' + nickname + '@' + domain
|
||||||
|
if not os.path.isdir(personDir):
|
||||||
|
return False
|
||||||
|
if not os.path.isdir(personDir + '/devices'):
|
||||||
|
os.mkdir(personDir + '/devices')
|
||||||
|
deviceDict = {
|
||||||
|
"deviceId": deviceId,
|
||||||
|
"type": "Device",
|
||||||
|
"name": name,
|
||||||
|
"claim": claimUrl,
|
||||||
|
"fingerprintKey": {
|
||||||
|
"type": fingerprintKeyType,
|
||||||
|
"publicKeyBase64": fingerprintPublicKey
|
||||||
|
},
|
||||||
|
"identityKey": {
|
||||||
|
"type": identityKeyType,
|
||||||
|
"publicKeyBase64": identityPublicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deviceFilename = personDir + '/devices/' + deviceId + '.json'
|
||||||
|
return saveJson(deviceDict, deviceFilename)
|
||||||
|
|
||||||
|
|
||||||
|
def E2EEdevicesCollection(baseDir: str, nickname: str, domain: str,
|
||||||
|
domainFull: str, httpPrefix: str) -> {}:
|
||||||
|
"""Returns a list of registered devices
|
||||||
|
"""
|
||||||
|
personDir = baseDir + '/accounts/' + nickname + '@' + domain
|
||||||
|
if not os.path.isdir(personDir):
|
||||||
|
return {}
|
||||||
|
personId = httpPrefix + '://' + domainFull + '/users/' + nickname
|
||||||
|
if not os.path.isdir(personDir + '/devices'):
|
||||||
|
os.mkdir(personDir + '/devices')
|
||||||
|
deviceList = []
|
||||||
|
for subdir, dirs, files in os.walk(personDir + '/devices/'):
|
||||||
|
for dev in files:
|
||||||
|
if not dev.endswith('.json'):
|
||||||
|
continue
|
||||||
|
deviceFilename = os.path.join(personDir + '/devices', dev)
|
||||||
|
devJson = loadJson(deviceFilename)
|
||||||
|
if devJson:
|
||||||
|
deviceList.append(devJson)
|
||||||
|
|
||||||
|
devicesDict = {
|
||||||
|
'id': personId + '/collections/devices',
|
||||||
|
'type': 'Collection',
|
||||||
|
'totalItems': len(deviceList),
|
||||||
|
'items': deviceList
|
||||||
|
}
|
||||||
|
return devicesDict
|
||||||
|
|
||||||
|
|
||||||
|
def E2EEdecryptMessageFromDevice(messageJson: {}) -> str:
|
||||||
|
"""Locally decrypts a message on the device.
|
||||||
|
This should probably be a link to a local script
|
||||||
|
or native app, such that what the user sees isn't
|
||||||
|
something which the server could get access to.
|
||||||
|
"""
|
||||||
|
# TODO
|
||||||
|
# {
|
||||||
|
# "type": "EncryptedMessage",
|
||||||
|
# "messageType": 0,
|
||||||
|
# "cipherText": "...",
|
||||||
|
# "digest": {
|
||||||
|
# "type": "Digest",
|
||||||
|
# "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#hmac-sha256",
|
||||||
|
# "digestValue": "5f6ad31acd64995483d75c7..."
|
||||||
|
# },
|
||||||
|
# "messageFranking": "...",
|
||||||
|
# "attributedTo": {
|
||||||
|
# "type": "Device",
|
||||||
|
# "deviceId": "11119"
|
||||||
|
# },
|
||||||
|
# "to": {
|
||||||
|
# "type": "Device",
|
||||||
|
# "deviceId": "11876"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
return ''
|
||||||
|
|
@ -112,6 +112,12 @@ a:link {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-size: var(--font-size4);
|
||||||
|
width: 90%;
|
||||||
|
background-color: var(--text-entry-background);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 400px) {
|
@media screen and (min-width: 400px) {
|
||||||
.followText {
|
.followText {
|
||||||
font-size: var(--follow-text-size1);
|
font-size: var(--follow-text-size1);
|
||||||
|
|
|
||||||
2
git.py
2
git.py
|
|
@ -126,6 +126,8 @@ def convertPostToPatch(baseDir: str, nickname: str, domain: str,
|
||||||
return False
|
return False
|
||||||
if not postJsonObject['object'].get('attributedTo'):
|
if not postJsonObject['object'].get('attributedTo'):
|
||||||
return False
|
return False
|
||||||
|
if not isinstance(postJsonObject['object']['attributedTo'], str):
|
||||||
|
return False
|
||||||
if not isGitPatch(baseDir, nickname, domain,
|
if not isGitPatch(baseDir, nickname, domain,
|
||||||
postJsonObject['object']['type'],
|
postJsonObject['object']['type'],
|
||||||
postJsonObject['object']['summary'],
|
postJsonObject['object']['summary'],
|
||||||
|
|
|
||||||
6
inbox.py
6
inbox.py
|
|
@ -1422,12 +1422,15 @@ def receiveAnnounce(recentPostsCache: {},
|
||||||
# so that their avatar can be shown
|
# so that their avatar can be shown
|
||||||
lookupActor = None
|
lookupActor = None
|
||||||
if postJsonObject.get('attributedTo'):
|
if postJsonObject.get('attributedTo'):
|
||||||
|
if isinstance(postJsonObject['attributedTo'], str):
|
||||||
lookupActor = postJsonObject['attributedTo']
|
lookupActor = postJsonObject['attributedTo']
|
||||||
else:
|
else:
|
||||||
if postJsonObject.get('object'):
|
if postJsonObject.get('object'):
|
||||||
if isinstance(postJsonObject['object'], dict):
|
if isinstance(postJsonObject['object'], dict):
|
||||||
if postJsonObject['object'].get('attributedTo'):
|
if postJsonObject['object'].get('attributedTo'):
|
||||||
lookupActor = postJsonObject['object']['attributedTo']
|
attrib = postJsonObject['object']['attributedTo']
|
||||||
|
if isinstance(attrib, str):
|
||||||
|
lookupActor = attrib
|
||||||
if lookupActor:
|
if lookupActor:
|
||||||
if '/users/' in lookupActor or \
|
if '/users/' in lookupActor or \
|
||||||
'/channel/' in lookupActor or \
|
'/channel/' in lookupActor or \
|
||||||
|
|
@ -2190,6 +2193,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
postJsonObject['object'].get('summary') and \
|
postJsonObject['object'].get('summary') and \
|
||||||
postJsonObject['object'].get('attributedTo'):
|
postJsonObject['object'].get('attributedTo'):
|
||||||
attributedTo = postJsonObject['object']['attributedTo']
|
attributedTo = postJsonObject['object']['attributedTo']
|
||||||
|
if isinstance(attributedTo, str):
|
||||||
fromNickname = getNicknameFromActor(attributedTo)
|
fromNickname = getNicknameFromActor(attributedTo)
|
||||||
fromDomain, fromPort = getDomainFromActor(attributedTo)
|
fromDomain, fromPort = getDomainFromActor(attributedTo)
|
||||||
if fromPort:
|
if fromPort:
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
||||||
if messageJson['type'] == 'Create' or \
|
if messageJson['type'] == 'Create' or \
|
||||||
messageJson['type'] == 'Question' or \
|
messageJson['type'] == 'Question' or \
|
||||||
messageJson['type'] == 'Note' or \
|
messageJson['type'] == 'Note' or \
|
||||||
|
messageJson['type'] == 'EncryptedMessage' or \
|
||||||
messageJson['type'] == 'Article' or \
|
messageJson['type'] == 'Article' or \
|
||||||
messageJson['type'] == 'Patch' or \
|
messageJson['type'] == 'Patch' or \
|
||||||
messageJson['type'] == 'Announce':
|
messageJson['type'] == 'Announce':
|
||||||
|
|
|
||||||
72
person.py
72
person.py
|
|
@ -163,6 +163,38 @@ def randomizeActorImages(personJson: {}) -> None:
|
||||||
personId + '/image' + randStr + '.' + existingExtension
|
personId + '/image' + randStr + '.' + existingExtension
|
||||||
|
|
||||||
|
|
||||||
|
def getDefaultPersonContext() -> str:
|
||||||
|
"""Gets the default actor context
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'Emoji': 'toot:Emoji',
|
||||||
|
'Hashtag': 'as:Hashtag',
|
||||||
|
'IdentityProof': 'toot:IdentityProof',
|
||||||
|
'PropertyValue': 'schema:PropertyValue',
|
||||||
|
'alsoKnownAs': {
|
||||||
|
'@id': 'as:alsoKnownAs', '@type': '@id'
|
||||||
|
},
|
||||||
|
'focalPoint': {
|
||||||
|
'@container': '@list', '@id': 'toot:focalPoint'
|
||||||
|
},
|
||||||
|
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
|
||||||
|
'movedTo': {
|
||||||
|
'@id': 'as:movedTo', '@type': '@id'
|
||||||
|
},
|
||||||
|
'schema': 'http://schema.org#',
|
||||||
|
'value': 'schema:value',
|
||||||
|
'Curve25519Key': 'toot:Curve25519Key',
|
||||||
|
'Device': 'toot:Device',
|
||||||
|
'Ed25519Key': 'toot:Ed25519Key',
|
||||||
|
'Ed25519Signature': 'toot:Ed25519Signature',
|
||||||
|
'EncryptedMessage': 'toot:EncryptedMessage',
|
||||||
|
'identityKey': {'@id': 'toot:identityKey', '@type': '@id'},
|
||||||
|
'fingerprintKey': {'@id': 'toot:fingerprintKey', '@type': '@id'},
|
||||||
|
'messageFranking': 'toot:messageFranking',
|
||||||
|
'publicKeyBase64': 'toot:publicKeyBase64'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
httpPrefix: str, saveToFile: bool,
|
httpPrefix: str, saveToFile: bool,
|
||||||
manualFollowerApproval: bool,
|
manualFollowerApproval: bool,
|
||||||
|
|
@ -212,34 +244,16 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
personId + '/avatar' + \
|
personId + '/avatar' + \
|
||||||
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
str(randint(10000000000000, 99999999999999)) + '.png' # nosec
|
||||||
|
|
||||||
contextDict = {
|
|
||||||
'Emoji': 'toot:Emoji',
|
|
||||||
'Hashtag': 'as:Hashtag',
|
|
||||||
'IdentityProof': 'toot:IdentityProof',
|
|
||||||
'PropertyValue': 'schema:PropertyValue',
|
|
||||||
'alsoKnownAs': {
|
|
||||||
'@id': 'as:alsoKnownAs', '@type': '@id'
|
|
||||||
},
|
|
||||||
'focalPoint': {
|
|
||||||
'@container': '@list', '@id': 'toot:focalPoint'
|
|
||||||
},
|
|
||||||
'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers',
|
|
||||||
'movedTo': {
|
|
||||||
'@id': 'as:movedTo', '@type': '@id'
|
|
||||||
},
|
|
||||||
'schema': 'http://schema.org#',
|
|
||||||
'value': 'schema:value'
|
|
||||||
}
|
|
||||||
|
|
||||||
newPerson = {
|
newPerson = {
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
'https://w3id.org/security/v1',
|
'https://w3id.org/security/v1',
|
||||||
contextDict
|
getDefaultPersonContext()
|
||||||
],
|
],
|
||||||
'attachment': [],
|
'attachment': [],
|
||||||
'alsoKnownAs': [],
|
'alsoKnownAs': [],
|
||||||
'discoverable': False,
|
'discoverable': False,
|
||||||
|
'devices': personId + '/collections/devices',
|
||||||
'endpoints': {
|
'endpoints': {
|
||||||
'id': personId+'/endpoints',
|
'id': personId+'/endpoints',
|
||||||
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
||||||
|
|
@ -1061,3 +1075,21 @@ def personUnsnooze(baseDir: str, nickname: str, domain: str,
|
||||||
if writeSnoozedFile:
|
if writeSnoozedFile:
|
||||||
writeSnoozedFile.write(content)
|
writeSnoozedFile.write(content)
|
||||||
writeSnoozedFile.close()
|
writeSnoozedFile.close()
|
||||||
|
|
||||||
|
|
||||||
|
def setPersonNotes(baseDir: str, nickname: str, domain: str,
|
||||||
|
handle: str, notes: str) -> bool:
|
||||||
|
"""Adds notes about a person
|
||||||
|
"""
|
||||||
|
if '@' not in handle:
|
||||||
|
return False
|
||||||
|
if handle.startswith('@'):
|
||||||
|
handle = handle[1:]
|
||||||
|
notesDir = baseDir + '/accounts/' + \
|
||||||
|
nickname + '@' + domain + '/notes'
|
||||||
|
if not os.path.isdir(notesDir):
|
||||||
|
os.mkdir(notesDir)
|
||||||
|
notesFilename = notesDir + '/' + handle + '.txt'
|
||||||
|
with open(notesFilename, 'w+') as notesFile:
|
||||||
|
notesFile.write(notes)
|
||||||
|
return True
|
||||||
|
|
|
||||||
3
posts.py
3
posts.py
|
|
@ -2400,6 +2400,7 @@ def isDM(postJsonObject: {}) -> bool:
|
||||||
return False
|
return False
|
||||||
if postJsonObject['object']['type'] != 'Note' and \
|
if postJsonObject['object']['type'] != 'Note' and \
|
||||||
postJsonObject['object']['type'] != 'Patch' and \
|
postJsonObject['object']['type'] != 'Patch' and \
|
||||||
|
postJsonObject['object']['type'] != 'EncryptedMessage' and \
|
||||||
postJsonObject['object']['type'] != 'Article':
|
postJsonObject['object']['type'] != 'Article':
|
||||||
return False
|
return False
|
||||||
if postJsonObject['object'].get('moderationStatus'):
|
if postJsonObject['object'].get('moderationStatus'):
|
||||||
|
|
@ -2466,6 +2467,7 @@ def isReply(postJsonObject: {}, actor: str) -> bool:
|
||||||
if postJsonObject['object'].get('moderationStatus'):
|
if postJsonObject['object'].get('moderationStatus'):
|
||||||
return False
|
return False
|
||||||
if postJsonObject['object']['type'] != 'Note' and \
|
if postJsonObject['object']['type'] != 'Note' and \
|
||||||
|
postJsonObject['object']['type'] != 'EncryptedMessage' and \
|
||||||
postJsonObject['object']['type'] != 'Article':
|
postJsonObject['object']['type'] != 'Article':
|
||||||
return False
|
return False
|
||||||
if postJsonObject['object'].get('inReplyTo'):
|
if postJsonObject['object'].get('inReplyTo'):
|
||||||
|
|
@ -2577,6 +2579,7 @@ def addPostStringToTimeline(postStr: str, boxname: str,
|
||||||
"""
|
"""
|
||||||
# must be a recognized ActivityPub type
|
# must be a recognized ActivityPub type
|
||||||
if ('"Note"' in postStr or
|
if ('"Note"' in postStr or
|
||||||
|
'"EncryptedMessage"' in postStr or
|
||||||
'"Article"' in postStr or
|
'"Article"' in postStr or
|
||||||
'"Patch"' in postStr or
|
'"Patch"' in postStr or
|
||||||
'"Announce"' in postStr or
|
'"Announce"' in postStr or
|
||||||
|
|
|
||||||
7
tests.py
7
tests.py
|
|
@ -1684,6 +1684,13 @@ def testWebLinks():
|
||||||
'<p>filepopout=' + \
|
'<p>filepopout=' + \
|
||||||
'TemplateAttachmentRichPopout'
|
'TemplateAttachmentRichPopout'
|
||||||
|
|
||||||
|
exampleText = \
|
||||||
|
'<p>Test1 test2 #YetAnotherExcessivelyLongwindedAndBoringHashtag</p>'
|
||||||
|
resultText = removeLongWords(addWebLinks(exampleText), 40, [])
|
||||||
|
assert(resultText ==
|
||||||
|
'<p>Test1 test2 '
|
||||||
|
'#YetAnotherExcessivelyLongwindedAndBorin\ngHashtag</p>')
|
||||||
|
|
||||||
|
|
||||||
def testAddEmoji():
|
def testAddEmoji():
|
||||||
print('testAddEmoji')
|
print('testAddEmoji')
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "درجات الرمادي",
|
"Grayscale": "درجات الرمادي",
|
||||||
"Liked by": "نال إعجاب",
|
"Liked by": "نال إعجاب",
|
||||||
"Solidaric": "تضامن",
|
"Solidaric": "تضامن",
|
||||||
"YouTube Replacement Domain": "استبدال نطاق يوتيوب"
|
"YouTube Replacement Domain": "استبدال نطاق يوتيوب",
|
||||||
|
"Notes": "ملاحظات"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Escala de grisos",
|
"Grayscale": "Escala de grisos",
|
||||||
"Liked by": "M'agrada",
|
"Liked by": "M'agrada",
|
||||||
"Solidaric": "Solidaritat",
|
"Solidaric": "Solidaritat",
|
||||||
"YouTube Replacement Domain": "Domini de substitució de YouTube"
|
"YouTube Replacement Domain": "Domini de substitució de YouTube",
|
||||||
|
"Notes": "Notes"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Graddlwyd",
|
"Grayscale": "Graddlwyd",
|
||||||
"Liked by": "Hoffi",
|
"Liked by": "Hoffi",
|
||||||
"Solidaric": "Undod",
|
"Solidaric": "Undod",
|
||||||
"YouTube Replacement Domain": "Parth Amnewid YouTube"
|
"YouTube Replacement Domain": "Parth Amnewid YouTube",
|
||||||
|
"Notes": "Nodiadau"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Graustufen",
|
"Grayscale": "Graustufen",
|
||||||
"Liked by": "Gefallen von",
|
"Liked by": "Gefallen von",
|
||||||
"Solidaric": "Solidarität",
|
"Solidaric": "Solidarität",
|
||||||
"YouTube Replacement Domain": "YouTube-Ersatzdomain"
|
"YouTube Replacement Domain": "YouTube-Ersatzdomain",
|
||||||
|
"Notes": "Anmerkungen"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Grayscale",
|
"Grayscale": "Grayscale",
|
||||||
"Liked by": "Liked by",
|
"Liked by": "Liked by",
|
||||||
"Solidaric": "Solidaric",
|
"Solidaric": "Solidaric",
|
||||||
"YouTube Replacement Domain": "YouTube Replacement Domain"
|
"YouTube Replacement Domain": "YouTube Replacement Domain",
|
||||||
|
"Notes": "Notes"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Escala de grises",
|
"Grayscale": "Escala de grises",
|
||||||
"Liked by": "Apreciado por",
|
"Liked by": "Apreciado por",
|
||||||
"Solidaric": "Solidaridad",
|
"Solidaric": "Solidaridad",
|
||||||
"YouTube Replacement Domain": "Dominio de reemplazo de YouTube"
|
"YouTube Replacement Domain": "Dominio de reemplazo de YouTube",
|
||||||
|
"Notes": "Notas"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Niveaux de gris",
|
"Grayscale": "Niveaux de gris",
|
||||||
"Liked by": "Aimé par",
|
"Liked by": "Aimé par",
|
||||||
"Solidaric": "Solidarité",
|
"Solidaric": "Solidarité",
|
||||||
"YouTube Replacement Domain": "Domaine de remplacement YouTube"
|
"YouTube Replacement Domain": "Domaine de remplacement YouTube",
|
||||||
|
"Notes": "Remarques"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Liathscála",
|
"Grayscale": "Liathscála",
|
||||||
"Liked by": "Thaitin",
|
"Liked by": "Thaitin",
|
||||||
"Solidaric": "Dlúthpháirtíocht",
|
"Solidaric": "Dlúthpháirtíocht",
|
||||||
"YouTube Replacement Domain": "Fearann Athsholáthair YouTube"
|
"YouTube Replacement Domain": "Fearann Athsholáthair YouTube",
|
||||||
|
"Notes": "Nótaí"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "ग्रेस्केल",
|
"Grayscale": "ग्रेस्केल",
|
||||||
"Liked by": "द्वारा पसंद किया गया",
|
"Liked by": "द्वारा पसंद किया गया",
|
||||||
"Solidaric": "एकजुटता",
|
"Solidaric": "एकजुटता",
|
||||||
"YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन"
|
"YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन",
|
||||||
|
"Notes": "टिप्पणियाँ"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Scala di grigi",
|
"Grayscale": "Scala di grigi",
|
||||||
"Liked by": "Mi è piaciuto",
|
"Liked by": "Mi è piaciuto",
|
||||||
"Solidaric": "Solidarietà",
|
"Solidaric": "Solidarietà",
|
||||||
"YouTube Replacement Domain": "Dominio sostitutivo di YouTube"
|
"YouTube Replacement Domain": "Dominio sostitutivo di YouTube",
|
||||||
|
"Notes": "Appunti"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "グレースケール",
|
"Grayscale": "グレースケール",
|
||||||
"Liked by": "好き",
|
"Liked by": "好き",
|
||||||
"Solidaric": "連帯",
|
"Solidaric": "連帯",
|
||||||
"YouTube Replacement Domain": "YouTube交換ドメイン"
|
"YouTube Replacement Domain": "YouTube交換ドメイン",
|
||||||
|
"Notes": "ノート"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,5 +250,6 @@
|
||||||
"Grayscale": "Grayscale",
|
"Grayscale": "Grayscale",
|
||||||
"Liked by": "Liked by",
|
"Liked by": "Liked by",
|
||||||
"Solidaric": "Solidaric",
|
"Solidaric": "Solidaric",
|
||||||
"YouTube Replacement Domain": "YouTube Replacement Domain"
|
"YouTube Replacement Domain": "YouTube Replacement Domain",
|
||||||
|
"Notes": "Notes"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Escala de cinza",
|
"Grayscale": "Escala de cinza",
|
||||||
"Liked by": "Curtida por",
|
"Liked by": "Curtida por",
|
||||||
"Solidaric": "Solidariedade",
|
"Solidaric": "Solidariedade",
|
||||||
"YouTube Replacement Domain": "Domínio de substituição do YouTube"
|
"YouTube Replacement Domain": "Domínio de substituição do YouTube",
|
||||||
|
"Notes": "Notas"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -254,5 +254,6 @@
|
||||||
"Grayscale": "Оттенки серого",
|
"Grayscale": "Оттенки серого",
|
||||||
"Liked by": "Понравилось",
|
"Liked by": "Понравилось",
|
||||||
"Solidaric": "солидарность",
|
"Solidaric": "солидарность",
|
||||||
"YouTube Replacement Domain": "Запасной домен YouTube"
|
"YouTube Replacement Domain": "Запасной домен YouTube",
|
||||||
|
"Notes": "Ноты"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -253,5 +253,6 @@
|
||||||
"Grayscale": "灰阶",
|
"Grayscale": "灰阶",
|
||||||
"Liked by": "喜欢的人",
|
"Liked by": "喜欢的人",
|
||||||
"Solidaric": "团结互助",
|
"Solidaric": "团结互助",
|
||||||
"YouTube Replacement Domain": "YouTube替换域"
|
"YouTube Replacement Domain": "YouTube替换域",
|
||||||
|
"Notes": "笔记"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ from git import isGitPatch
|
||||||
from theme import getThemesList
|
from theme import getThemesList
|
||||||
from petnames import getPetName
|
from petnames import getPetName
|
||||||
from followingCalendar import receivingCalendarEvents
|
from followingCalendar import receivingCalendarEvents
|
||||||
|
from devices import E2EEdecryptMessageFromDevice
|
||||||
|
|
||||||
|
|
||||||
def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str:
|
def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str:
|
||||||
|
|
@ -3059,7 +3060,10 @@ def addEmbeddedVideoFromSites(translate: {}, content: str,
|
||||||
return content
|
return content
|
||||||
|
|
||||||
invidiousSites = ('https://invidio.us',
|
invidiousSites = ('https://invidio.us',
|
||||||
'axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
|
'https://invidious.snopyta.org',
|
||||||
|
'http://c7hqkpkpemu6e7emz5b4vy' +
|
||||||
|
'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion',
|
||||||
|
'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
|
||||||
'bzzsg2ii4fv2iid.onion')
|
'bzzsg2ii4fv2iid.onion')
|
||||||
for videoSite in invidiousSites:
|
for videoSite in invidiousSites:
|
||||||
if '"' + videoSite in content:
|
if '"' + videoSite in content:
|
||||||
|
|
@ -3812,6 +3816,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
if showIcons:
|
if showIcons:
|
||||||
replyToLink = postJsonObject['object']['id']
|
replyToLink = postJsonObject['object']['id']
|
||||||
if postJsonObject['object'].get('attributedTo'):
|
if postJsonObject['object'].get('attributedTo'):
|
||||||
|
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||||
replyToLink += \
|
replyToLink += \
|
||||||
'?mention=' + postJsonObject['object']['attributedTo']
|
'?mention=' + postJsonObject['object']['attributedTo']
|
||||||
if postJsonObject['object'].get('content'):
|
if postJsonObject['object'].get('content'):
|
||||||
|
|
@ -3984,6 +3989,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
if showRepeatIcon:
|
if showRepeatIcon:
|
||||||
if isAnnounced:
|
if isAnnounced:
|
||||||
if postJsonObject['object'].get('attributedTo'):
|
if postJsonObject['object'].get('attributedTo'):
|
||||||
|
attributedTo = ''
|
||||||
|
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||||
attributedTo = postJsonObject['object']['attributedTo']
|
attributedTo = postJsonObject['object']['attributedTo']
|
||||||
if attributedTo.startswith(postActor):
|
if attributedTo.startswith(postActor):
|
||||||
titleStr += \
|
titleStr += \
|
||||||
|
|
@ -3993,8 +4000,9 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
'" src="/' + iconsDir + \
|
'" src="/' + iconsDir + \
|
||||||
'/repeat_inactive.png" class="announceOrReply"/>\n'
|
'/repeat_inactive.png" class="announceOrReply"/>\n'
|
||||||
else:
|
else:
|
||||||
announceNickname = \
|
announceNickname = None
|
||||||
getNicknameFromActor(attributedTo)
|
if attributedTo:
|
||||||
|
announceNickname = getNicknameFromActor(attributedTo)
|
||||||
if announceNickname:
|
if announceNickname:
|
||||||
announceDomain, announcePort = \
|
announceDomain, announcePort = \
|
||||||
getDomainFromActor(attributedTo)
|
getDomainFromActor(attributedTo)
|
||||||
|
|
@ -4248,8 +4256,13 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
if not postJsonObject['object'].get('summary'):
|
if not postJsonObject['object'].get('summary'):
|
||||||
postJsonObject['object']['summary'] = ''
|
postJsonObject['object']['summary'] = ''
|
||||||
|
|
||||||
|
if postJsonObject['object'].get('cipherText'):
|
||||||
|
postJsonObject['object']['content'] = \
|
||||||
|
E2EEdecryptMessageFromDevice(postJsonObject['object'])
|
||||||
|
|
||||||
if not postJsonObject['object'].get('content'):
|
if not postJsonObject['object'].get('content'):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
isPatch = isGitPatch(baseDir, nickname, domain,
|
isPatch = isGitPatch(baseDir, nickname, domain,
|
||||||
postJsonObject['object']['type'],
|
postJsonObject['object']['type'],
|
||||||
postJsonObject['object']['summary'],
|
postJsonObject['object']['summary'],
|
||||||
|
|
@ -5560,10 +5573,10 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
||||||
optionsStr += ' <a href="' + optionsActor + '">\n'
|
optionsStr += ' <a href="' + optionsActor + '">\n'
|
||||||
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + \
|
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + \
|
||||||
'"/></a>\n'
|
'"/></a>\n'
|
||||||
|
handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
|
||||||
optionsStr += \
|
optionsStr += \
|
||||||
' <p class="optionsText">' + translate['Options for'] + \
|
' <p class="optionsText">' + translate['Options for'] + \
|
||||||
' @' + getNicknameFromActor(optionsActor) + '@' + \
|
' @' + handle + '</p>\n'
|
||||||
optionsDomain + '</p>\n'
|
|
||||||
if emailAddress:
|
if emailAddress:
|
||||||
optionsStr += \
|
optionsStr += \
|
||||||
'<p class="imText">' + translate['Email'] + \
|
'<p class="imText">' + translate['Email'] + \
|
||||||
|
|
@ -5652,6 +5665,24 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
||||||
' <button type="submit" class="button" name="submitReport">' + \
|
' <button type="submit" class="button" name="submitReport">' + \
|
||||||
translate['Report'] + '</button>\n'
|
translate['Report'] + '</button>\n'
|
||||||
|
|
||||||
|
personNotes = ''
|
||||||
|
personNotesFilename = \
|
||||||
|
baseDir + '/accounts/' + nickname + '@' + domain + \
|
||||||
|
'/notes/' + handle + '.txt'
|
||||||
|
if os.path.isfile(personNotesFilename):
|
||||||
|
with open(personNotesFilename, 'r') as fp:
|
||||||
|
personNotes = fp.read()
|
||||||
|
|
||||||
|
optionsStr += \
|
||||||
|
' <br><br>' + translate['Notes'] + ': \n'
|
||||||
|
optionsStr += '<button type="submit" class="button" ' + \
|
||||||
|
'name="submitPersonNotes">' + \
|
||||||
|
translate['Submit'] + '</button><br>\n'
|
||||||
|
optionsStr += \
|
||||||
|
' <textarea id="message" ' + \
|
||||||
|
'name="optionnotes" style="height:400px">' + \
|
||||||
|
personNotes + '</textarea>\n'
|
||||||
|
|
||||||
optionsStr += ' </form>\n'
|
optionsStr += ' </form>\n'
|
||||||
optionsStr += '</center>\n'
|
optionsStr += '</center>\n'
|
||||||
optionsStr += '</div>\n'
|
optionsStr += '</div>\n'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue