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:
|
||||
"""Is the given hashtag blocked?
|
||||
"""
|
||||
# avoid very long hashtags
|
||||
if len(hashtag) > 32:
|
||||
return True
|
||||
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
||||
if os.path.isfile(globalBlockingFilename):
|
||||
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
|
||||
if postJsonObject['object'].get('attributedTo'):
|
||||
authorNickname = None
|
||||
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||
actor = postJsonObject['object']['attributedTo']
|
||||
authorNickname = getNicknameFromActor(actor)
|
||||
if authorNickname:
|
||||
|
|
|
|||
|
|
@ -267,6 +267,9 @@ def addWebLinks(content: str) -> str:
|
|||
def validHashTag(hashtag: str) -> bool:
|
||||
"""Returns true if the give hashtag contains valid characters
|
||||
"""
|
||||
# long hashtags are not valid
|
||||
if len(hashtag) >= 32:
|
||||
return False
|
||||
validChars = set('0123456789' +
|
||||
'abcdefghijklmnopqrstuvwxyz' +
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
|
|
|
|||
250
daemon.py
250
daemon.py
|
|
@ -43,6 +43,8 @@ from matrix import getMatrixAddress
|
|||
from matrix import setMatrixAddress
|
||||
from donate import getDonationUrl
|
||||
from donate import setDonationUrl
|
||||
from person import setPersonNotes
|
||||
from person import getDefaultPersonContext
|
||||
from person import savePersonQrcode
|
||||
from person import randomizeActorImages
|
||||
from person import personUpgradeActor
|
||||
|
|
@ -191,6 +193,9 @@ from bookmarks import undoBookmark
|
|||
from petnames import setPetName
|
||||
from followingCalendar import addPersonToCalendar
|
||||
from followingCalendar import removePersonFromCalendar
|
||||
from devices import E2EEdevicesCollection
|
||||
from devices import E2EEvalidDevice
|
||||
from devices import E2EEaddDevice
|
||||
import os
|
||||
|
||||
|
||||
|
|
@ -1047,6 +1052,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return 1
|
||||
|
||||
def _isAuthorized(self) -> bool:
|
||||
self.authorizedNickname = None
|
||||
|
||||
if self.path.startswith('/icons/') or \
|
||||
self.path.startswith('/avatars/') or \
|
||||
self.path.startswith('/favicon.ico'):
|
||||
|
|
@ -1060,6 +1067,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
tokenStr = tokenStr.split(';')[0].strip()
|
||||
if self.server.tokensLookup.get(tokenStr):
|
||||
nickname = self.server.tokensLookup[tokenStr]
|
||||
self.authorizedNickname = nickname
|
||||
# default to the inbox of the person
|
||||
if self.path == '/':
|
||||
self.path = '/users/' + nickname + '/inbox'
|
||||
|
|
@ -1535,6 +1543,25 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self._404()
|
||||
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:
|
||||
# show the person options screen with view/follow/block/report
|
||||
if '?options=' in self.path:
|
||||
|
|
@ -1637,7 +1664,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
# remove a shared item
|
||||
if htmlGET and '?rmshare=' in self.path:
|
||||
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]
|
||||
actor = \
|
||||
self.server.httpPrefix + '://' + \
|
||||
|
|
@ -3336,7 +3363,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
shareDescription = \
|
||||
inReplyToUrl.replace('sharedesc:', '')
|
||||
shareDescription = \
|
||||
urllib.parse.unquote(shareDescription.strip())
|
||||
urllib.parse.unquote_plus(shareDescription.strip())
|
||||
self.path = self.path.split('?replydm=')[0]+'/newdm'
|
||||
if self.server.debug:
|
||||
print('DEBUG: replydm path ' + self.path)
|
||||
|
|
@ -5754,6 +5781,159 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
postBytes, boundary)
|
||||
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):
|
||||
POSTstartTime = time.time()
|
||||
POSTtimings = []
|
||||
|
|
@ -5827,6 +6007,11 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
print('POST Not authorized')
|
||||
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
|
||||
self.outboxAuthenticated = False
|
||||
self.postToNickname = None
|
||||
|
|
@ -6616,6 +6801,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
os.remove(gitProjectsFilename)
|
||||
# save actor json file within accounts
|
||||
if actorChanged:
|
||||
# update the context for the actor
|
||||
actorJson['@context'] = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
getDefaultPersonContext()
|
||||
]
|
||||
randomizeActorImages(actorJson)
|
||||
saveJson(actorJson, actorFilename)
|
||||
webfingerUpdate(self.server.baseDir,
|
||||
|
|
@ -6706,9 +6897,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if '=' in moderationStr:
|
||||
moderationText = \
|
||||
moderationStr.split('=')[1].strip()
|
||||
moderationText = moderationText.replace('+', ' ')
|
||||
modText = moderationText.replace('+', ' ')
|
||||
moderationText = \
|
||||
urllib.parse.unquote(moderationText.strip())
|
||||
urllib.parse.unquote_plus(modText.strip())
|
||||
elif moderationStr.startswith('submitInfo'):
|
||||
msg = htmlModerationInfo(self.server.translate,
|
||||
self.server.baseDir,
|
||||
|
|
@ -6882,7 +7073,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
questionParams = questionParams.replace('+', ' ')
|
||||
questionParams = questionParams.replace('%3F', '')
|
||||
questionParams = \
|
||||
urllib.parse.unquote(questionParams.strip())
|
||||
urllib.parse.unquote_plus(questionParams.strip())
|
||||
# post being voted on
|
||||
messageId = None
|
||||
if 'messageId=' in questionParams:
|
||||
|
|
@ -6964,9 +7155,8 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
searchStr = searchParams.split('searchtext=')[1]
|
||||
if '&' in searchStr:
|
||||
searchStr = searchStr.split('&')[0]
|
||||
searchStr = searchStr.replace('+', ' ')
|
||||
searchStr = \
|
||||
urllib.parse.unquote(searchStr.strip())
|
||||
urllib.parse.unquote_plus(searchStr.strip())
|
||||
searchStr2 = searchStr.lower().strip('\n').strip('\r')
|
||||
print('searchStr: ' + searchStr)
|
||||
if searchForEmoji:
|
||||
|
|
@ -7172,7 +7362,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
removeShareConfirmParams = \
|
||||
removeShareConfirmParams.replace('+', ' ').strip()
|
||||
removeShareConfirmParams = \
|
||||
urllib.parse.unquote(removeShareConfirmParams)
|
||||
urllib.parse.unquote_plus(removeShareConfirmParams)
|
||||
shareActor = removeShareConfirmParams.split('actor=')[1]
|
||||
if '&' in shareActor:
|
||||
shareActor = shareActor.split('&')[0]
|
||||
|
|
@ -7235,7 +7425,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitYes=' in removePostConfirmParams:
|
||||
removePostConfirmParams = \
|
||||
urllib.parse.unquote(removePostConfirmParams)
|
||||
urllib.parse.unquote_plus(removePostConfirmParams)
|
||||
removeMessageId = \
|
||||
removePostConfirmParams.split('messageId=')[1]
|
||||
if '&' in removeMessageId:
|
||||
|
|
@ -7327,7 +7517,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitView=' in followConfirmParams:
|
||||
followingActor = \
|
||||
urllib.parse.unquote(followConfirmParams)
|
||||
urllib.parse.unquote_plus(followConfirmParams)
|
||||
followingActor = followingActor.split('actor=')[1]
|
||||
if '&' in followingActor:
|
||||
followingActor = followingActor.split('&')[0]
|
||||
|
|
@ -7336,7 +7526,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitYes=' in followConfirmParams:
|
||||
followingActor = \
|
||||
urllib.parse.unquote(followConfirmParams)
|
||||
urllib.parse.unquote_plus(followConfirmParams)
|
||||
followingActor = followingActor.split('actor=')[1]
|
||||
if '&' in followingActor:
|
||||
followingActor = followingActor.split('&')[0]
|
||||
|
|
@ -7409,7 +7599,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitYes=' in followConfirmParams:
|
||||
followingActor = \
|
||||
urllib.parse.unquote(followConfirmParams)
|
||||
urllib.parse.unquote_plus(followConfirmParams)
|
||||
followingActor = followingActor.split('actor=')[1]
|
||||
if '&' in followingActor:
|
||||
followingActor = followingActor.split('&')[0]
|
||||
|
|
@ -7503,7 +7693,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitYes=' in blockConfirmParams:
|
||||
blockingActor = \
|
||||
urllib.parse.unquote(blockConfirmParams)
|
||||
urllib.parse.unquote_plus(blockConfirmParams)
|
||||
blockingActor = blockingActor.split('actor=')[1]
|
||||
if '&' in blockingActor:
|
||||
blockingActor = blockingActor.split('&')[0]
|
||||
|
|
@ -7600,7 +7790,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
if '&submitYes=' in blockConfirmParams:
|
||||
blockingActor = \
|
||||
urllib.parse.unquote(blockConfirmParams)
|
||||
urllib.parse.unquote_plus(blockConfirmParams)
|
||||
blockingActor = blockingActor.split('actor=')[1]
|
||||
if '&' in blockingActor:
|
||||
blockingActor = blockingActor.split('&')[0]
|
||||
|
|
@ -7698,7 +7888,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.POSTbusy = False
|
||||
return
|
||||
optionsConfirmParams = \
|
||||
urllib.parse.unquote(optionsConfirmParams)
|
||||
urllib.parse.unquote_plus(optionsConfirmParams)
|
||||
# page number to return to
|
||||
if 'pageNumber=' in optionsConfirmParams:
|
||||
pageNumberStr = optionsConfirmParams.split('pageNumber=')[1]
|
||||
|
|
@ -7731,6 +7921,16 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'?' in petname or '#' in petname:
|
||||
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)
|
||||
if not optionsNickname:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
|
|
@ -7773,7 +7973,23 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
chooserNickname,
|
||||
self.server.domain,
|
||||
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 +
|
||||
'?page='+str(pageNumber), cookie,
|
||||
callingDomain)
|
||||
|
|
@ -7797,7 +8013,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.domain,
|
||||
optionsNickname,
|
||||
optionsDomainFull)
|
||||
self._redirect_headers(originPathStr + '/' +
|
||||
self._redirect_headers(usersPath + '/' +
|
||||
self.server.defaultTimeline +
|
||||
'?page='+str(pageNumber), cookie,
|
||||
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%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: var(--font-size4);
|
||||
width: 90%;
|
||||
background-color: var(--text-entry-background);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 400px) {
|
||||
.followText {
|
||||
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
|
||||
if not postJsonObject['object'].get('attributedTo'):
|
||||
return False
|
||||
if not isinstance(postJsonObject['object']['attributedTo'], str):
|
||||
return False
|
||||
if not isGitPatch(baseDir, nickname, domain,
|
||||
postJsonObject['object']['type'],
|
||||
postJsonObject['object']['summary'],
|
||||
|
|
|
|||
6
inbox.py
6
inbox.py
|
|
@ -1422,12 +1422,15 @@ def receiveAnnounce(recentPostsCache: {},
|
|||
# so that their avatar can be shown
|
||||
lookupActor = None
|
||||
if postJsonObject.get('attributedTo'):
|
||||
if isinstance(postJsonObject['attributedTo'], str):
|
||||
lookupActor = postJsonObject['attributedTo']
|
||||
else:
|
||||
if postJsonObject.get('object'):
|
||||
if isinstance(postJsonObject['object'], dict):
|
||||
if postJsonObject['object'].get('attributedTo'):
|
||||
lookupActor = postJsonObject['object']['attributedTo']
|
||||
attrib = postJsonObject['object']['attributedTo']
|
||||
if isinstance(attrib, str):
|
||||
lookupActor = attrib
|
||||
if lookupActor:
|
||||
if '/users/' in lookupActor or \
|
||||
'/channel/' in lookupActor or \
|
||||
|
|
@ -2190,6 +2193,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
postJsonObject['object'].get('summary') and \
|
||||
postJsonObject['object'].get('attributedTo'):
|
||||
attributedTo = postJsonObject['object']['attributedTo']
|
||||
if isinstance(attributedTo, str):
|
||||
fromNickname = getNicknameFromActor(attributedTo)
|
||||
fromDomain, fromPort = getDomainFromActor(attributedTo)
|
||||
if fromPort:
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
if messageJson['type'] == 'Create' or \
|
||||
messageJson['type'] == 'Question' or \
|
||||
messageJson['type'] == 'Note' or \
|
||||
messageJson['type'] == 'EncryptedMessage' or \
|
||||
messageJson['type'] == 'Article' or \
|
||||
messageJson['type'] == 'Patch' or \
|
||||
messageJson['type'] == 'Announce':
|
||||
|
|
|
|||
72
person.py
72
person.py
|
|
@ -163,6 +163,38 @@ def randomizeActorImages(personJson: {}) -> None:
|
|||
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,
|
||||
httpPrefix: str, saveToFile: bool,
|
||||
manualFollowerApproval: bool,
|
||||
|
|
@ -212,34 +244,16 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
personId + '/avatar' + \
|
||||
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 = {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
contextDict
|
||||
getDefaultPersonContext()
|
||||
],
|
||||
'attachment': [],
|
||||
'alsoKnownAs': [],
|
||||
'discoverable': False,
|
||||
'devices': personId + '/collections/devices',
|
||||
'endpoints': {
|
||||
'id': personId+'/endpoints',
|
||||
'sharedInbox': httpPrefix+'://'+domain+'/inbox',
|
||||
|
|
@ -1061,3 +1075,21 @@ def personUnsnooze(baseDir: str, nickname: str, domain: str,
|
|||
if writeSnoozedFile:
|
||||
writeSnoozedFile.write(content)
|
||||
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
|
||||
if postJsonObject['object']['type'] != 'Note' and \
|
||||
postJsonObject['object']['type'] != 'Patch' and \
|
||||
postJsonObject['object']['type'] != 'EncryptedMessage' and \
|
||||
postJsonObject['object']['type'] != 'Article':
|
||||
return False
|
||||
if postJsonObject['object'].get('moderationStatus'):
|
||||
|
|
@ -2466,6 +2467,7 @@ def isReply(postJsonObject: {}, actor: str) -> bool:
|
|||
if postJsonObject['object'].get('moderationStatus'):
|
||||
return False
|
||||
if postJsonObject['object']['type'] != 'Note' and \
|
||||
postJsonObject['object']['type'] != 'EncryptedMessage' and \
|
||||
postJsonObject['object']['type'] != 'Article':
|
||||
return False
|
||||
if postJsonObject['object'].get('inReplyTo'):
|
||||
|
|
@ -2577,6 +2579,7 @@ def addPostStringToTimeline(postStr: str, boxname: str,
|
|||
"""
|
||||
# must be a recognized ActivityPub type
|
||||
if ('"Note"' in postStr or
|
||||
'"EncryptedMessage"' in postStr or
|
||||
'"Article"' in postStr or
|
||||
'"Patch"' in postStr or
|
||||
'"Announce"' in postStr or
|
||||
|
|
|
|||
7
tests.py
7
tests.py
|
|
@ -1684,6 +1684,13 @@ def testWebLinks():
|
|||
'<p>filepopout=' + \
|
||||
'TemplateAttachmentRichPopout'
|
||||
|
||||
exampleText = \
|
||||
'<p>Test1 test2 #YetAnotherExcessivelyLongwindedAndBoringHashtag</p>'
|
||||
resultText = removeLongWords(addWebLinks(exampleText), 40, [])
|
||||
assert(resultText ==
|
||||
'<p>Test1 test2 '
|
||||
'#YetAnotherExcessivelyLongwindedAndBorin\ngHashtag</p>')
|
||||
|
||||
|
||||
def testAddEmoji():
|
||||
print('testAddEmoji')
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "درجات الرمادي",
|
||||
"Liked by": "نال إعجاب",
|
||||
"Solidaric": "تضامن",
|
||||
"YouTube Replacement Domain": "استبدال نطاق يوتيوب"
|
||||
"YouTube Replacement Domain": "استبدال نطاق يوتيوب",
|
||||
"Notes": "ملاحظات"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Escala de grisos",
|
||||
"Liked by": "M'agrada",
|
||||
"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",
|
||||
"Liked by": "Hoffi",
|
||||
"Solidaric": "Undod",
|
||||
"YouTube Replacement Domain": "Parth Amnewid YouTube"
|
||||
"YouTube Replacement Domain": "Parth Amnewid YouTube",
|
||||
"Notes": "Nodiadau"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Graustufen",
|
||||
"Liked by": "Gefallen von",
|
||||
"Solidaric": "Solidarität",
|
||||
"YouTube Replacement Domain": "YouTube-Ersatzdomain"
|
||||
"YouTube Replacement Domain": "YouTube-Ersatzdomain",
|
||||
"Notes": "Anmerkungen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Grayscale",
|
||||
"Liked by": "Liked by",
|
||||
"Solidaric": "Solidaric",
|
||||
"YouTube Replacement Domain": "YouTube Replacement Domain"
|
||||
"YouTube Replacement Domain": "YouTube Replacement Domain",
|
||||
"Notes": "Notes"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Escala de grises",
|
||||
"Liked by": "Apreciado por",
|
||||
"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",
|
||||
"Liked by": "Aimé par",
|
||||
"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",
|
||||
"Liked by": "Thaitin",
|
||||
"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": "ग्रेस्केल",
|
||||
"Liked by": "द्वारा पसंद किया गया",
|
||||
"Solidaric": "एकजुटता",
|
||||
"YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन"
|
||||
"YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन",
|
||||
"Notes": "टिप्पणियाँ"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Scala di grigi",
|
||||
"Liked by": "Mi è piaciuto",
|
||||
"Solidaric": "Solidarietà",
|
||||
"YouTube Replacement Domain": "Dominio sostitutivo di YouTube"
|
||||
"YouTube Replacement Domain": "Dominio sostitutivo di YouTube",
|
||||
"Notes": "Appunti"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "グレースケール",
|
||||
"Liked by": "好き",
|
||||
"Solidaric": "連帯",
|
||||
"YouTube Replacement Domain": "YouTube交換ドメイン"
|
||||
"YouTube Replacement Domain": "YouTube交換ドメイン",
|
||||
"Notes": "ノート"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,5 +250,6 @@
|
|||
"Grayscale": "Grayscale",
|
||||
"Liked by": "Liked by",
|
||||
"Solidaric": "Solidaric",
|
||||
"YouTube Replacement Domain": "YouTube Replacement Domain"
|
||||
"YouTube Replacement Domain": "YouTube Replacement Domain",
|
||||
"Notes": "Notes"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,5 +254,6 @@
|
|||
"Grayscale": "Escala de cinza",
|
||||
"Liked by": "Curtida por",
|
||||
"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": "Оттенки серого",
|
||||
"Liked by": "Понравилось",
|
||||
"Solidaric": "солидарность",
|
||||
"YouTube Replacement Domain": "Запасной домен YouTube"
|
||||
"YouTube Replacement Domain": "Запасной домен YouTube",
|
||||
"Notes": "Ноты"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,5 +253,6 @@
|
|||
"Grayscale": "灰阶",
|
||||
"Liked by": "喜欢的人",
|
||||
"Solidaric": "团结互助",
|
||||
"YouTube Replacement Domain": "YouTube替换域"
|
||||
"YouTube Replacement Domain": "YouTube替换域",
|
||||
"Notes": "笔记"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ from git import isGitPatch
|
|||
from theme import getThemesList
|
||||
from petnames import getPetName
|
||||
from followingCalendar import receivingCalendarEvents
|
||||
from devices import E2EEdecryptMessageFromDevice
|
||||
|
||||
|
||||
def getAltPath(actor: str, domainFull: str, callingDomain: str) -> str:
|
||||
|
|
@ -3059,7 +3060,10 @@ def addEmbeddedVideoFromSites(translate: {}, content: str,
|
|||
return content
|
||||
|
||||
invidiousSites = ('https://invidio.us',
|
||||
'axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
|
||||
'https://invidious.snopyta.org',
|
||||
'http://c7hqkpkpemu6e7emz5b4vy' +
|
||||
'z7idjgdvgaaa3dyimmeojqbgpea3xqjoid.onion',
|
||||
'http://axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4' +
|
||||
'bzzsg2ii4fv2iid.onion')
|
||||
for videoSite in invidiousSites:
|
||||
if '"' + videoSite in content:
|
||||
|
|
@ -3812,6 +3816,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if showIcons:
|
||||
replyToLink = postJsonObject['object']['id']
|
||||
if postJsonObject['object'].get('attributedTo'):
|
||||
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||
replyToLink += \
|
||||
'?mention=' + postJsonObject['object']['attributedTo']
|
||||
if postJsonObject['object'].get('content'):
|
||||
|
|
@ -3984,6 +3989,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if showRepeatIcon:
|
||||
if isAnnounced:
|
||||
if postJsonObject['object'].get('attributedTo'):
|
||||
attributedTo = ''
|
||||
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||
attributedTo = postJsonObject['object']['attributedTo']
|
||||
if attributedTo.startswith(postActor):
|
||||
titleStr += \
|
||||
|
|
@ -3993,8 +4000,9 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
'" src="/' + iconsDir + \
|
||||
'/repeat_inactive.png" class="announceOrReply"/>\n'
|
||||
else:
|
||||
announceNickname = \
|
||||
getNicknameFromActor(attributedTo)
|
||||
announceNickname = None
|
||||
if attributedTo:
|
||||
announceNickname = getNicknameFromActor(attributedTo)
|
||||
if announceNickname:
|
||||
announceDomain, announcePort = \
|
||||
getDomainFromActor(attributedTo)
|
||||
|
|
@ -4248,8 +4256,13 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if not postJsonObject['object'].get('summary'):
|
||||
postJsonObject['object']['summary'] = ''
|
||||
|
||||
if postJsonObject['object'].get('cipherText'):
|
||||
postJsonObject['object']['content'] = \
|
||||
E2EEdecryptMessageFromDevice(postJsonObject['object'])
|
||||
|
||||
if not postJsonObject['object'].get('content'):
|
||||
return ''
|
||||
|
||||
isPatch = isGitPatch(baseDir, nickname, domain,
|
||||
postJsonObject['object']['type'],
|
||||
postJsonObject['object']['summary'],
|
||||
|
|
@ -5560,10 +5573,10 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
optionsStr += ' <a href="' + optionsActor + '">\n'
|
||||
optionsStr += ' <img loading="lazy" src="' + optionsProfileUrl + \
|
||||
'"/></a>\n'
|
||||
handle = getNicknameFromActor(optionsActor) + '@' + optionsDomain
|
||||
optionsStr += \
|
||||
' <p class="optionsText">' + translate['Options for'] + \
|
||||
' @' + getNicknameFromActor(optionsActor) + '@' + \
|
||||
optionsDomain + '</p>\n'
|
||||
' @' + handle + '</p>\n'
|
||||
if emailAddress:
|
||||
optionsStr += \
|
||||
'<p class="imText">' + translate['Email'] + \
|
||||
|
|
@ -5652,6 +5665,24 @@ def htmlPersonOptions(translate: {}, baseDir: str,
|
|||
' <button type="submit" class="button" name="submitReport">' + \
|
||||
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 += '</center>\n'
|
||||
optionsStr += '</div>\n'
|
||||
|
|
|
|||
Loading…
Reference in New Issue