Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon
2
Makefile
|
|
@ -23,4 +23,4 @@ clean:
|
||||||
rm -f deploy/*~
|
rm -f deploy/*~
|
||||||
rm -f translations/*~
|
rm -f translations/*~
|
||||||
rm -rf __pycache__
|
rm -rf __pycache__
|
||||||
rm calendar.css blog.css epicyon.css follow.css login.css options.css search.css suspended.css
|
rm -f calendar.css blog.css epicyon.css follow.css login.css options.css search.css suspended.css
|
||||||
|
|
|
||||||
103
blocking.py
|
|
@ -7,6 +7,9 @@ __email__ = "bob@freedombone.net"
|
||||||
__status__ = "Production"
|
__status__ = "Production"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from utils import fileLastModified
|
||||||
|
from utils import setConfigParam
|
||||||
from utils import hasUsersPath
|
from utils import hasUsersPath
|
||||||
from utils import getFullDomain
|
from utils import getFullDomain
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
|
|
@ -175,6 +178,9 @@ def isBlockedDomain(baseDir: str, domain: str) -> bool:
|
||||||
if noOfSections > 2:
|
if noOfSections > 2:
|
||||||
shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1]
|
shortDomain = domain[noOfSections-2] + '.' + domain[noOfSections-1]
|
||||||
|
|
||||||
|
allowFilename = baseDir + '/accounts/allowedinstances.txt'
|
||||||
|
if not os.path.isfile(allowFilename):
|
||||||
|
# instance block list
|
||||||
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
||||||
if os.path.isfile(globalBlockingFilename):
|
if os.path.isfile(globalBlockingFilename):
|
||||||
with open(globalBlockingFilename, 'r') as fpBlocked:
|
with open(globalBlockingFilename, 'r') as fpBlocked:
|
||||||
|
|
@ -184,6 +190,15 @@ def isBlockedDomain(baseDir: str, domain: str) -> bool:
|
||||||
if shortDomain:
|
if shortDomain:
|
||||||
if '*@' + shortDomain in blockedStr:
|
if '*@' + shortDomain in blockedStr:
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
# instance allow list
|
||||||
|
if not shortDomain:
|
||||||
|
if domain not in open(allowFilename).read():
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
if shortDomain not in open(allowFilename).read():
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -344,3 +359,91 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str,
|
||||||
nicknameBlocked, domainBlockedFull)
|
nicknameBlocked, domainBlockedFull)
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: post undo blocked via c2s - ' + postFilename)
|
print('DEBUG: post undo blocked via c2s - ' + postFilename)
|
||||||
|
|
||||||
|
|
||||||
|
def setBrochMode(baseDir: str, domainFull: str, enabled: bool) -> None:
|
||||||
|
"""Broch mode can be used to lock down the instance during
|
||||||
|
a period of time when it is temporarily under attack.
|
||||||
|
For example, where an adversary is constantly spinning up new
|
||||||
|
instances.
|
||||||
|
It surveys the following lists of all accounts and uses that
|
||||||
|
to construct an instance level allow list. Anything arriving
|
||||||
|
which is then not from one of the allowed domains will be dropped
|
||||||
|
"""
|
||||||
|
allowFilename = baseDir + '/accounts/allowedinstances.txt'
|
||||||
|
|
||||||
|
if not enabled:
|
||||||
|
# remove instance allow list
|
||||||
|
if os.path.isfile(allowFilename):
|
||||||
|
os.remove(allowFilename)
|
||||||
|
print('Broch mode turned off')
|
||||||
|
else:
|
||||||
|
if os.path.isfile(allowFilename):
|
||||||
|
lastModified = fileLastModified(allowFilename)
|
||||||
|
print('Broch mode already activated ' + lastModified)
|
||||||
|
return
|
||||||
|
# generate instance allow list
|
||||||
|
allowedDomains = [domainFull]
|
||||||
|
followFiles = ('following.txt', 'followers.txt')
|
||||||
|
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
|
||||||
|
for acct in dirs:
|
||||||
|
if '@' not in acct:
|
||||||
|
continue
|
||||||
|
if 'inbox@' in acct or 'news@' in acct:
|
||||||
|
continue
|
||||||
|
accountDir = os.path.join(baseDir + '/accounts', acct)
|
||||||
|
for followFileType in followFiles:
|
||||||
|
followingFilename = accountDir + '/' + followFileType
|
||||||
|
if not os.path.isfile(followingFilename):
|
||||||
|
continue
|
||||||
|
with open(followingFilename, "r") as f:
|
||||||
|
followList = f.readlines()
|
||||||
|
for handle in followList:
|
||||||
|
if '@' not in handle:
|
||||||
|
continue
|
||||||
|
handle = handle.replace('\n', '')
|
||||||
|
handleDomain = handle.split('@')[1]
|
||||||
|
if handleDomain not in allowedDomains:
|
||||||
|
allowedDomains.append(handleDomain)
|
||||||
|
break
|
||||||
|
|
||||||
|
# write the allow file
|
||||||
|
allowFile = open(allowFilename, "w+")
|
||||||
|
if allowFile:
|
||||||
|
allowFile.write(domainFull + '\n')
|
||||||
|
for d in allowedDomains:
|
||||||
|
allowFile.write(d + '\n')
|
||||||
|
allowFile.close()
|
||||||
|
print('Broch mode enabled')
|
||||||
|
|
||||||
|
setConfigParam(baseDir, "brochMode", enabled)
|
||||||
|
|
||||||
|
|
||||||
|
def brochModeLapses(baseDir: str, lapseDays=7) -> bool:
|
||||||
|
"""After broch mode is enabled it automatically
|
||||||
|
elapses after a period of time
|
||||||
|
"""
|
||||||
|
allowFilename = baseDir + '/accounts/allowedinstances.txt'
|
||||||
|
if not os.path.isfile(allowFilename):
|
||||||
|
return False
|
||||||
|
lastModified = fileLastModified(allowFilename)
|
||||||
|
modifiedDate = None
|
||||||
|
brochMode = True
|
||||||
|
try:
|
||||||
|
modifiedDate = \
|
||||||
|
datetime.strptime(lastModified, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
except BaseException:
|
||||||
|
return brochMode
|
||||||
|
if not modifiedDate:
|
||||||
|
return brochMode
|
||||||
|
currTime = datetime.datetime.utcnow()
|
||||||
|
daysSinceBroch = (currTime - modifiedDate).days
|
||||||
|
if daysSinceBroch >= lapseDays:
|
||||||
|
try:
|
||||||
|
os.remove(allowFilename)
|
||||||
|
brochMode = False
|
||||||
|
setConfigParam(baseDir, "brochMode", brochMode)
|
||||||
|
print('Broch mode has elapsed')
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
return brochMode
|
||||||
|
|
|
||||||
34
cache.py
|
|
@ -8,11 +8,45 @@ __status__ = "Production"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
|
from session import urlExists
|
||||||
from utils import loadJson
|
from utils import loadJson
|
||||||
from utils import saveJson
|
from utils import saveJson
|
||||||
from utils import getFileCaseInsensitive
|
from utils import getFileCaseInsensitive
|
||||||
|
|
||||||
|
|
||||||
|
def _removePersonFromCache(baseDir: str, personUrl: str,
|
||||||
|
personCache: {}) -> bool:
|
||||||
|
"""Removes an actor from the cache
|
||||||
|
"""
|
||||||
|
cacheFilename = baseDir + '/cache/actors/' + \
|
||||||
|
personUrl.replace('/', '#')+'.json'
|
||||||
|
if os.path.isfile(cacheFilename):
|
||||||
|
try:
|
||||||
|
os.remove(cacheFilename)
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
if personCache.get(personUrl):
|
||||||
|
del personCache[personUrl]
|
||||||
|
|
||||||
|
|
||||||
|
def checkForChangedActor(session, baseDir: str,
|
||||||
|
httpPrefix: str, domainFull: str,
|
||||||
|
personUrl: str, avatarUrl: str, personCache: {},
|
||||||
|
timeoutSec: int):
|
||||||
|
"""Checks if the avatar url exists and if not then
|
||||||
|
the actor has probably changed without receiving an actor/Person Update.
|
||||||
|
So clear the actor from the cache and it will be refreshed when the next
|
||||||
|
post from them is sent
|
||||||
|
"""
|
||||||
|
if not session or not avatarUrl:
|
||||||
|
return
|
||||||
|
if domainFull in avatarUrl:
|
||||||
|
return
|
||||||
|
if urlExists(session, avatarUrl, timeoutSec, httpPrefix, domainFull):
|
||||||
|
return
|
||||||
|
_removePersonFromCache(baseDir, personUrl, personCache)
|
||||||
|
|
||||||
|
|
||||||
def storePersonInCache(baseDir: str, personUrl: str,
|
def storePersonInCache(baseDir: str, personUrl: str,
|
||||||
personJson: {}, personCache: {},
|
personJson: {}, personCache: {},
|
||||||
allowWriteToFile: bool) -> None:
|
allowWriteToFile: bool) -> None:
|
||||||
|
|
|
||||||
|
|
@ -788,6 +788,13 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
|
||||||
prevWordStr = ''
|
prevWordStr = ''
|
||||||
continue
|
continue
|
||||||
elif firstChar == '#':
|
elif firstChar == '#':
|
||||||
|
# remove any endings from the hashtag
|
||||||
|
hashTagEndings = ('.', ':', ';', '-', '\n')
|
||||||
|
for ending in hashTagEndings:
|
||||||
|
if wordStr.endswith(ending):
|
||||||
|
wordStr = wordStr[:len(wordStr) - 1]
|
||||||
|
break
|
||||||
|
|
||||||
if _addHashTags(wordStr, httpPrefix, originalDomain,
|
if _addHashTags(wordStr, httpPrefix, originalDomain,
|
||||||
replaceHashTags, hashtags):
|
replaceHashTags, hashtags):
|
||||||
prevWordStr = ''
|
prevWordStr = ''
|
||||||
|
|
|
||||||
143
daemon.py
|
|
@ -109,6 +109,7 @@ from threads import threadWithTrace
|
||||||
from threads import removeDormantThreads
|
from threads import removeDormantThreads
|
||||||
from media import replaceYouTube
|
from media import replaceYouTube
|
||||||
from media import attachMedia
|
from media import attachMedia
|
||||||
|
from blocking import setBrochMode
|
||||||
from blocking import addBlock
|
from blocking import addBlock
|
||||||
from blocking import removeBlock
|
from blocking import removeBlock
|
||||||
from blocking import addGlobalBlock
|
from blocking import addGlobalBlock
|
||||||
|
|
@ -185,6 +186,7 @@ from shares import addShare
|
||||||
from shares import removeShare
|
from shares import removeShare
|
||||||
from shares import expireShares
|
from shares import expireShares
|
||||||
from categories import setHashtagCategory
|
from categories import setHashtagCategory
|
||||||
|
from utils import getLocalNetworkAddresses
|
||||||
from utils import decodedHost
|
from utils import decodedHost
|
||||||
from utils import isPublicPost
|
from utils import isPublicPost
|
||||||
from utils import getLockedAccount
|
from utils import getLockedAccount
|
||||||
|
|
@ -218,6 +220,7 @@ from utils import loadJson
|
||||||
from utils import saveJson
|
from utils import saveJson
|
||||||
from utils import isSuspended
|
from utils import isSuspended
|
||||||
from utils import dangerousMarkup
|
from utils import dangerousMarkup
|
||||||
|
from utils import refreshNewswire
|
||||||
from manualapprove import manualDenyFollowRequest
|
from manualapprove import manualDenyFollowRequest
|
||||||
from manualapprove import manualApproveFollowRequest
|
from manualapprove import manualApproveFollowRequest
|
||||||
from announce import createAnnounce
|
from announce import createAnnounce
|
||||||
|
|
@ -227,6 +230,7 @@ from content import extractMediaInFormPOST
|
||||||
from content import saveMediaInFormPOST
|
from content import saveMediaInFormPOST
|
||||||
from content import extractTextFieldsInPOST
|
from content import extractTextFieldsInPOST
|
||||||
from media import removeMetaData
|
from media import removeMetaData
|
||||||
|
from cache import checkForChangedActor
|
||||||
from cache import storePersonInCache
|
from cache import storePersonInCache
|
||||||
from cache import getPersonFromCache
|
from cache import getPersonFromCache
|
||||||
from httpsig import verifyPostHeaders
|
from httpsig import verifyPostHeaders
|
||||||
|
|
@ -392,7 +396,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
schedulePost,
|
schedulePost,
|
||||||
eventDate,
|
eventDate,
|
||||||
eventTime,
|
eventTime,
|
||||||
location)
|
location, False)
|
||||||
if messageJson:
|
if messageJson:
|
||||||
# name field contains the answer
|
# name field contains the answer
|
||||||
messageJson['object']['name'] = answer
|
messageJson['object']['name'] = answer
|
||||||
|
|
@ -476,6 +480,10 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if 'text/html' not in self.headers['Accept']:
|
if 'text/html' not in self.headers['Accept']:
|
||||||
return False
|
return False
|
||||||
if self.headers['Accept'].startswith('*'):
|
if self.headers['Accept'].startswith('*'):
|
||||||
|
if self.headers.get('User-Agent'):
|
||||||
|
if 'ELinks' in self.headers['User-Agent'] or \
|
||||||
|
'Lynx' in self.headers['User-Agent']:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
if 'json' in self.headers['Accept']:
|
if 'json' in self.headers['Accept']:
|
||||||
return False
|
return False
|
||||||
|
|
@ -1151,7 +1159,38 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
# check for blocked domains so that they can be rejected early
|
# check for blocked domains so that they can be rejected early
|
||||||
messageDomain = None
|
messageDomain = None
|
||||||
if messageJson.get('actor'):
|
if not messageJson.get('actor'):
|
||||||
|
print('Message arriving at inbox queue has no actor')
|
||||||
|
self._400()
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return 3
|
||||||
|
|
||||||
|
# actor should be a string
|
||||||
|
if not isinstance(messageJson['actor'], str):
|
||||||
|
self._400()
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return 3
|
||||||
|
|
||||||
|
# actor should look like a url
|
||||||
|
if '://' not in messageJson['actor'] or \
|
||||||
|
'.' not in messageJson['actor']:
|
||||||
|
print('POST actor does not look like a url ' +
|
||||||
|
messageJson['actor'])
|
||||||
|
self._400()
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return 3
|
||||||
|
|
||||||
|
# sent by an actor on a local network address?
|
||||||
|
if not self.server.allowLocalNetworkAccess:
|
||||||
|
localNetworkPatternList = getLocalNetworkAddresses()
|
||||||
|
for localNetworkPattern in localNetworkPatternList:
|
||||||
|
if localNetworkPattern in messageJson['actor']:
|
||||||
|
print('POST actor contains local network address ' +
|
||||||
|
messageJson['actor'])
|
||||||
|
self._400()
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return 3
|
||||||
|
|
||||||
messageDomain, messagePort = \
|
messageDomain, messagePort = \
|
||||||
getDomainFromActor(messageJson['actor'])
|
getDomainFromActor(messageJson['actor'])
|
||||||
if isBlockedDomain(self.server.baseDir, messageDomain):
|
if isBlockedDomain(self.server.baseDir, messageDomain):
|
||||||
|
|
@ -1159,11 +1198,6 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self._400()
|
self._400()
|
||||||
self.server.POSTbusy = False
|
self.server.POSTbusy = False
|
||||||
return 3
|
return 3
|
||||||
else:
|
|
||||||
print('Message arriving at inbox queue has no actor')
|
|
||||||
self._400()
|
|
||||||
self.server.POSTbusy = False
|
|
||||||
return 3
|
|
||||||
|
|
||||||
# if the inbox queue is full then return a busy code
|
# if the inbox queue is full then return a busy code
|
||||||
if len(self.server.inboxQueue) >= self.server.maxQueueLength:
|
if len(self.server.inboxQueue) >= self.server.maxQueueLength:
|
||||||
|
|
@ -1947,12 +1981,50 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if postsToNews == 'on':
|
if postsToNews == 'on':
|
||||||
if os.path.isfile(newswireBlockedFilename):
|
if os.path.isfile(newswireBlockedFilename):
|
||||||
os.remove(newswireBlockedFilename)
|
os.remove(newswireBlockedFilename)
|
||||||
|
refreshNewswire(self.server.baseDir)
|
||||||
else:
|
else:
|
||||||
if os.path.isdir(accountDir):
|
if os.path.isdir(accountDir):
|
||||||
noNewswireFile = open(newswireBlockedFilename, "w+")
|
noNewswireFile = open(newswireBlockedFilename, "w+")
|
||||||
if noNewswireFile:
|
if noNewswireFile:
|
||||||
noNewswireFile.write('\n')
|
noNewswireFile.write('\n')
|
||||||
noNewswireFile.close()
|
noNewswireFile.close()
|
||||||
|
refreshNewswire(self.server.baseDir)
|
||||||
|
usersPathStr = \
|
||||||
|
usersPath + '/' + self.server.defaultTimeline + \
|
||||||
|
'?page=' + str(pageNumber)
|
||||||
|
self._redirect_headers(usersPathStr, cookie,
|
||||||
|
callingDomain)
|
||||||
|
self.server.POSTbusy = False
|
||||||
|
return
|
||||||
|
|
||||||
|
# person options screen, permission to post to featured articles
|
||||||
|
# See htmlPersonOptions
|
||||||
|
if '&submitPostToFeatures=' in optionsConfirmParams:
|
||||||
|
adminNickname = getConfigParam(self.server.baseDir, 'admin')
|
||||||
|
if (chooserNickname != optionsNickname and
|
||||||
|
(chooserNickname == adminNickname or
|
||||||
|
(isModerator(self.server.baseDir, chooserNickname) and
|
||||||
|
not isModerator(self.server.baseDir, optionsNickname)))):
|
||||||
|
postsToFeatures = None
|
||||||
|
if 'postsToFeatures=' in optionsConfirmParams:
|
||||||
|
postsToFeatures = \
|
||||||
|
optionsConfirmParams.split('postsToFeatures=')[1]
|
||||||
|
if '&' in postsToFeatures:
|
||||||
|
postsToFeatures = postsToFeatures.split('&')[0]
|
||||||
|
accountDir = self.server.baseDir + '/accounts/' + \
|
||||||
|
optionsNickname + '@' + optionsDomain
|
||||||
|
featuresBlockedFilename = accountDir + '/.nofeatures'
|
||||||
|
if postsToFeatures == 'on':
|
||||||
|
if os.path.isfile(featuresBlockedFilename):
|
||||||
|
os.remove(featuresBlockedFilename)
|
||||||
|
refreshNewswire(self.server.baseDir)
|
||||||
|
else:
|
||||||
|
if os.path.isdir(accountDir):
|
||||||
|
noFeaturesFile = open(featuresBlockedFilename, "w+")
|
||||||
|
if noFeaturesFile:
|
||||||
|
noFeaturesFile.write('\n')
|
||||||
|
noFeaturesFile.close()
|
||||||
|
refreshNewswire(self.server.baseDir)
|
||||||
usersPathStr = \
|
usersPathStr = \
|
||||||
usersPath + '/' + self.server.defaultTimeline + \
|
usersPath + '/' + self.server.defaultTimeline + \
|
||||||
'?page=' + str(pageNumber)
|
'?page=' + str(pageNumber)
|
||||||
|
|
@ -4472,6 +4544,18 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
setConfigParam(baseDir, "verifyAllSignatures",
|
setConfigParam(baseDir, "verifyAllSignatures",
|
||||||
verifyAllSignatures)
|
verifyAllSignatures)
|
||||||
|
|
||||||
|
brochMode = False
|
||||||
|
if fields.get('brochMode'):
|
||||||
|
if fields['brochMode'] == 'on':
|
||||||
|
brochMode = True
|
||||||
|
currBrochMode = \
|
||||||
|
getConfigParam(baseDir, "brochMode")
|
||||||
|
if brochMode != currBrochMode:
|
||||||
|
setBrochMode(self.server.baseDir,
|
||||||
|
self.server.domainFull,
|
||||||
|
brochMode)
|
||||||
|
setConfigParam(baseDir, "brochMode", brochMode)
|
||||||
|
|
||||||
# change moderators list
|
# change moderators list
|
||||||
if fields.get('moderators'):
|
if fields.get('moderators'):
|
||||||
if path.startswith('/users/' +
|
if path.startswith('/users/' +
|
||||||
|
|
@ -5449,6 +5533,15 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
PGPfingerprint = getPGPfingerprint(actorJson)
|
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||||
if actorJson.get('alsoKnownAs'):
|
if actorJson.get('alsoKnownAs'):
|
||||||
alsoKnownAs = actorJson['alsoKnownAs']
|
alsoKnownAs = actorJson['alsoKnownAs']
|
||||||
|
|
||||||
|
if self.server.session:
|
||||||
|
checkForChangedActor(self.server.session,
|
||||||
|
self.server.baseDir,
|
||||||
|
self.server.httpPrefix,
|
||||||
|
self.server.domainFull,
|
||||||
|
optionsActor, optionsProfileUrl,
|
||||||
|
self.server.personCache, 5)
|
||||||
|
|
||||||
msg = htmlPersonOptions(self.server.defaultTimeline,
|
msg = htmlPersonOptions(self.server.defaultTimeline,
|
||||||
self.server.cssCache,
|
self.server.cssCache,
|
||||||
self.server.translate,
|
self.server.translate,
|
||||||
|
|
@ -5468,7 +5561,9 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.dormantMonths,
|
self.server.dormantMonths,
|
||||||
backToPath,
|
backToPath,
|
||||||
lockedAccount,
|
lockedAccount,
|
||||||
movedTo, alsoKnownAs).encode('utf-8')
|
movedTo, alsoKnownAs,
|
||||||
|
self.server.textModeBanner,
|
||||||
|
self.server.newsInstance).encode('utf-8')
|
||||||
msglen = len(msg)
|
msglen = len(msg)
|
||||||
self._set_headers('text/html', msglen,
|
self._set_headers('text/html', msglen,
|
||||||
cookie, callingDomain)
|
cookie, callingDomain)
|
||||||
|
|
@ -10010,6 +10105,16 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
# replace https://domain/@nick with https://domain/users/nick
|
# replace https://domain/@nick with https://domain/users/nick
|
||||||
if self.path.startswith('/@'):
|
if self.path.startswith('/@'):
|
||||||
self.path = self.path.replace('/@', '/users/')
|
self.path = self.path.replace('/@', '/users/')
|
||||||
|
# replace https://domain/@nick/statusnumber
|
||||||
|
# with https://domain/users/nick/statuses/statusnumber
|
||||||
|
nickname = self.path.split('/users/')[1]
|
||||||
|
if '/' in nickname:
|
||||||
|
statusNumberStr = nickname.split('/')[1]
|
||||||
|
if statusNumberStr.isdigit():
|
||||||
|
nickname = nickname.split('/')[0]
|
||||||
|
self.path = \
|
||||||
|
self.path.replace('/users/' + nickname + '/',
|
||||||
|
'/users/' + nickname + '/statuses/')
|
||||||
|
|
||||||
# turn off dropdowns on new post screen
|
# turn off dropdowns on new post screen
|
||||||
noDropDown = False
|
noDropDown = False
|
||||||
|
|
@ -10038,9 +10143,13 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
|
|
||||||
# manifest for progressive web apps
|
# manifest for progressive web apps
|
||||||
if '/manifest.json' in self.path:
|
if '/manifest.json' in self.path:
|
||||||
|
if self._hasAccept(callingDomain):
|
||||||
|
if not self._requestHTTP():
|
||||||
self._progressiveWebAppManifest(callingDomain,
|
self._progressiveWebAppManifest(callingDomain,
|
||||||
GETstartTime, GETtimings)
|
GETstartTime, GETtimings)
|
||||||
return
|
return
|
||||||
|
else:
|
||||||
|
self.path = '/'
|
||||||
|
|
||||||
# default newswire favicon, for links to sites which
|
# default newswire favicon, for links to sites which
|
||||||
# have no favicon
|
# have no favicon
|
||||||
|
|
@ -11084,7 +11193,8 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
self.server.translate,
|
self.server.translate,
|
||||||
self.server.baseDir, self.path,
|
self.server.baseDir, self.path,
|
||||||
self.server.httpPrefix,
|
self.server.httpPrefix,
|
||||||
self.server.domainFull).encode('utf-8')
|
self.server.domainFull,
|
||||||
|
self.server.textModeBanner).encode('utf-8')
|
||||||
msglen = len(msg)
|
msglen = len(msg)
|
||||||
self._set_headers('text/html', msglen, cookie, callingDomain)
|
self._set_headers('text/html', msglen, cookie, callingDomain)
|
||||||
self._write(msg)
|
self._write(msg)
|
||||||
|
|
@ -12373,7 +12483,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
fields['replyTo'], fields['replyTo'],
|
fields['replyTo'], fields['replyTo'],
|
||||||
fields['subject'], fields['schedulePost'],
|
fields['subject'], fields['schedulePost'],
|
||||||
fields['eventDate'], fields['eventTime'],
|
fields['eventDate'], fields['eventTime'],
|
||||||
fields['location'])
|
fields['location'], False)
|
||||||
if messageJson:
|
if messageJson:
|
||||||
if fields['schedulePost']:
|
if fields['schedulePost']:
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -12419,6 +12529,12 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
return -1
|
return -1
|
||||||
|
if not fields['subject']:
|
||||||
|
print('WARN: blog posts must have a title')
|
||||||
|
return -1
|
||||||
|
if not fields['message']:
|
||||||
|
print('WARN: blog posts must have content')
|
||||||
|
return -1
|
||||||
# submit button on newblog screen
|
# submit button on newblog screen
|
||||||
messageJson = \
|
messageJson = \
|
||||||
createBlogPost(self.server.baseDir, nickname,
|
createBlogPost(self.server.baseDir, nickname,
|
||||||
|
|
@ -12438,6 +12554,7 @@ class PubServer(BaseHTTPRequestHandler):
|
||||||
if fields['schedulePost']:
|
if fields['schedulePost']:
|
||||||
return 1
|
return 1
|
||||||
if self._postToOutbox(messageJson, __version__, nickname):
|
if self._postToOutbox(messageJson, __version__, nickname):
|
||||||
|
refreshNewswire(self.server.baseDir)
|
||||||
populateReplies(self.server.baseDir,
|
populateReplies(self.server.baseDir,
|
||||||
self.server.httpPrefix,
|
self.server.httpPrefix,
|
||||||
self.server.domainFull,
|
self.server.domainFull,
|
||||||
|
|
@ -13820,7 +13937,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
def runDaemon(verifyAllSignatures: bool,
|
def runDaemon(brochMode: bool,
|
||||||
|
verifyAllSignatures: bool,
|
||||||
sendThreadsTimeoutMins: int,
|
sendThreadsTimeoutMins: int,
|
||||||
dormantMonths: int,
|
dormantMonths: int,
|
||||||
maxNewswirePosts: int,
|
maxNewswirePosts: int,
|
||||||
|
|
@ -14073,6 +14191,9 @@ def runDaemon(verifyAllSignatures: bool,
|
||||||
# cache to store css files
|
# cache to store css files
|
||||||
httpd.cssCache = {}
|
httpd.cssCache = {}
|
||||||
|
|
||||||
|
# whether to enable broch mode, which locks down the instance
|
||||||
|
setBrochMode(baseDir, httpd.domainFull, brochMode)
|
||||||
|
|
||||||
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
|
if not os.path.isdir(baseDir + '/accounts/inbox@' + domain):
|
||||||
print('Creating shared inbox: inbox@' + domain)
|
print('Creating shared inbox: inbox@' + domain)
|
||||||
createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix)
|
createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix)
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"android": "android",
|
||||||
"popcorn": "1F37F",
|
"popcorn": "1F37F",
|
||||||
"1stplacemedal": "1F947",
|
"1stplacemedal": "1F947",
|
||||||
"abbutton": "1F18E",
|
"abbutton": "1F18E",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,14 @@ a:focus {
|
||||||
border: 2px solid var(--focus-color);
|
border: 2px solid var(--focus-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transparent {
|
||||||
|
color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0px;
|
||||||
|
line-height: 0px;
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar__day__header,
|
.calendar__day__header,
|
||||||
.calendar__day__cell {
|
.calendar__day__cell {
|
||||||
border: 2px solid var(--lines-color);
|
border: 2px solid var(--lines-color);
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,14 @@ a:focus {
|
||||||
border: 2px solid var(--focus-color);
|
border: 2px solid var(--focus-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transparent {
|
||||||
|
color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 0px;
|
||||||
|
line-height: 0px;
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.follow {
|
.follow {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
--quote-right-margin: 0.1em;
|
--quote-right-margin: 0.1em;
|
||||||
--quote-font-weight: normal;
|
--quote-font-weight: normal;
|
||||||
--quote-font-size: 120%;
|
--quote-font-size: 120%;
|
||||||
--line-spacing: 130%;
|
--line-spacing: 180%;
|
||||||
--line-spacing-newswire: 120%;
|
--line-spacing-newswire: 120%;
|
||||||
--newswire-item-moderated-color: white;
|
--newswire-item-moderated-color: white;
|
||||||
--newswire-date-moderated-color: white;
|
--newswire-date-moderated-color: white;
|
||||||
|
|
@ -106,11 +106,11 @@
|
||||||
--column-left-icons-margin: 0;
|
--column-left-icons-margin: 0;
|
||||||
--column-right-border-width: 0;
|
--column-right-border-width: 0;
|
||||||
--column-left-border-color: black;
|
--column-left-border-color: black;
|
||||||
--column-left-icon-size: 20%;
|
--column-left-icon-size: 2.1vw;
|
||||||
--column-left-icon-size-mobile: 10%;
|
--column-left-icon-size-mobile: 10%;
|
||||||
--column-left-image-width-mobile: 40vw;
|
--column-left-image-width-mobile: 40vw;
|
||||||
--column-right-image-width-mobile: 100vw;
|
--column-right-image-width-mobile: 100vw;
|
||||||
--column-right-icon-size: 20%;
|
--column-right-icon-size: 2.1vw;
|
||||||
--column-right-icon-size-mobile: 10%;
|
--column-right-icon-size-mobile: 10%;
|
||||||
--newswire-date-color: white;
|
--newswire-date-color: white;
|
||||||
--newswire-voted-background-color: black;
|
--newswire-voted-background-color: black;
|
||||||
|
|
|
||||||
13
epicyon.py
|
|
@ -279,6 +279,11 @@ parser.add_argument("--verifyAllSignatures",
|
||||||
const=True, default=False,
|
const=True, default=False,
|
||||||
help="Whether to require that all incoming " +
|
help="Whether to require that all incoming " +
|
||||||
"posts have valid jsonld signatures")
|
"posts have valid jsonld signatures")
|
||||||
|
parser.add_argument("--brochMode",
|
||||||
|
dest='brochMode',
|
||||||
|
type=str2bool, nargs='?',
|
||||||
|
const=True, default=False,
|
||||||
|
help="Enable broch mode")
|
||||||
parser.add_argument("--noapproval", type=str2bool, nargs='?',
|
parser.add_argument("--noapproval", type=str2bool, nargs='?',
|
||||||
const=True, default=False,
|
const=True, default=False,
|
||||||
help="Allow followers without approval")
|
help="Allow followers without approval")
|
||||||
|
|
@ -2292,6 +2297,11 @@ verifyAllSignatures = \
|
||||||
if verifyAllSignatures is not None:
|
if verifyAllSignatures is not None:
|
||||||
args.verifyAllSignatures = bool(verifyAllSignatures)
|
args.verifyAllSignatures = bool(verifyAllSignatures)
|
||||||
|
|
||||||
|
brochMode = \
|
||||||
|
getConfigParam(baseDir, 'brochMode')
|
||||||
|
if brochMode is not None:
|
||||||
|
args.brochMode = bool(brochMode)
|
||||||
|
|
||||||
YTDomain = getConfigParam(baseDir, 'youtubedomain')
|
YTDomain = getConfigParam(baseDir, 'youtubedomain')
|
||||||
if YTDomain:
|
if YTDomain:
|
||||||
if '://' in YTDomain:
|
if '://' in YTDomain:
|
||||||
|
|
@ -2305,7 +2315,8 @@ if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess):
|
||||||
print('Theme set to ' + themeName)
|
print('Theme set to ' + themeName)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
runDaemon(args.verifyAllSignatures,
|
runDaemon(args.brochMode,
|
||||||
|
args.verifyAllSignatures,
|
||||||
args.sendThreadsTimeoutMins,
|
args.sendThreadsTimeoutMins,
|
||||||
args.dormantMonths,
|
args.dormantMonths,
|
||||||
args.maxNewswirePosts,
|
args.maxNewswirePosts,
|
||||||
|
|
|
||||||
52
inbox.py
|
|
@ -51,6 +51,7 @@ from bookmarks import updateBookmarksCollection
|
||||||
from bookmarks import undoBookmarksCollectionEntry
|
from bookmarks import undoBookmarksCollectionEntry
|
||||||
from blocking import isBlocked
|
from blocking import isBlocked
|
||||||
from blocking import isBlockedDomain
|
from blocking import isBlockedDomain
|
||||||
|
from blocking import brochModeLapses
|
||||||
from filters import isFiltered
|
from filters import isFiltered
|
||||||
from utils import updateAnnounceCollection
|
from utils import updateAnnounceCollection
|
||||||
from utils import undoAnnounceCollectionEntry
|
from utils import undoAnnounceCollectionEntry
|
||||||
|
|
@ -2518,6 +2519,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
# heartbeat to monitor whether the inbox queue is running
|
# heartbeat to monitor whether the inbox queue is running
|
||||||
heartBeatCtr += 5
|
heartBeatCtr += 5
|
||||||
if heartBeatCtr >= 10:
|
if heartBeatCtr >= 10:
|
||||||
|
# turn off broch mode after it has timed out
|
||||||
|
brochModeLapses(baseDir)
|
||||||
print('>>> Heartbeat Q:' + str(len(queue)) + ' ' +
|
print('>>> Heartbeat Q:' + str(len(queue)) + ' ' +
|
||||||
'{:%F %T}'.format(datetime.datetime.now()))
|
'{:%F %T}'.format(datetime.datetime.now()))
|
||||||
heartBeatCtr = 0
|
heartBeatCtr = 0
|
||||||
|
|
@ -2726,6 +2729,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
print('DEBUG: checking http header signature')
|
print('DEBUG: checking http header signature')
|
||||||
pprint(queueJson['httpHeaders'])
|
pprint(queueJson['httpHeaders'])
|
||||||
postStr = json.dumps(queueJson['post'])
|
postStr = json.dumps(queueJson['post'])
|
||||||
|
httpSignatureFailed = False
|
||||||
if not verifyPostHeaders(httpPrefix,
|
if not verifyPostHeaders(httpPrefix,
|
||||||
pubKey,
|
pubKey,
|
||||||
queueJson['httpHeaders'],
|
queueJson['httpHeaders'],
|
||||||
|
|
@ -2733,19 +2737,17 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
queueJson['digest'],
|
queueJson['digest'],
|
||||||
postStr,
|
postStr,
|
||||||
debug):
|
debug):
|
||||||
|
httpSignatureFailed = True
|
||||||
print('Queue: Header signature check failed')
|
print('Queue: Header signature check failed')
|
||||||
|
if debug:
|
||||||
pprint(queueJson['httpHeaders'])
|
pprint(queueJson['httpHeaders'])
|
||||||
if os.path.isfile(queueFilename):
|
else:
|
||||||
os.remove(queueFilename)
|
|
||||||
if len(queue) > 0:
|
|
||||||
queue.pop(0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
print('DEBUG: http header signature check success')
|
print('DEBUG: http header signature check success')
|
||||||
|
|
||||||
# check if a json signature exists on this post
|
# check if a json signature exists on this post
|
||||||
checkJsonSignature = False
|
hasJsonSignature = False
|
||||||
|
jwebsigType = None
|
||||||
originalJson = queueJson['original']
|
originalJson = queueJson['original']
|
||||||
if originalJson.get('@context') and \
|
if originalJson.get('@context') and \
|
||||||
originalJson.get('signature'):
|
originalJson.get('signature'):
|
||||||
|
|
@ -2754,27 +2756,41 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
jwebsig = originalJson['signature']
|
jwebsig = originalJson['signature']
|
||||||
# signature exists and is of the expected type
|
# signature exists and is of the expected type
|
||||||
if jwebsig.get('type') and jwebsig.get('signatureValue'):
|
if jwebsig.get('type') and jwebsig.get('signatureValue'):
|
||||||
if jwebsig['type'] == 'RsaSignature2017':
|
jwebsigType = jwebsig['type']
|
||||||
|
if jwebsigType == 'RsaSignature2017':
|
||||||
if hasValidContext(originalJson):
|
if hasValidContext(originalJson):
|
||||||
checkJsonSignature = True
|
hasJsonSignature = True
|
||||||
else:
|
else:
|
||||||
print('unrecognised @context: ' +
|
print('unrecognised @context: ' +
|
||||||
str(originalJson['@context']))
|
str(originalJson['@context']))
|
||||||
|
|
||||||
# strict enforcement of json signatures
|
# strict enforcement of json signatures
|
||||||
if verifyAllSignatures and \
|
if not hasJsonSignature:
|
||||||
not checkJsonSignature:
|
if httpSignatureFailed:
|
||||||
print('inbox post does not have a jsonld signature ' +
|
if jwebsigType:
|
||||||
|
print('Queue: Header signature check failed and does ' +
|
||||||
|
'not have a recognised jsonld signature type ' +
|
||||||
|
jwebsigType)
|
||||||
|
else:
|
||||||
|
print('Queue: Header signature check failed and ' +
|
||||||
|
'does not have jsonld signature')
|
||||||
|
if debug:
|
||||||
|
pprint(queueJson['httpHeaders'])
|
||||||
|
|
||||||
|
if verifyAllSignatures:
|
||||||
|
print('Queue: inbox post does not have a jsonld signature ' +
|
||||||
keyId + ' ' + str(originalJson))
|
keyId + ' ' + str(originalJson))
|
||||||
|
|
||||||
|
if httpSignatureFailed or verifyAllSignatures:
|
||||||
if os.path.isfile(queueFilename):
|
if os.path.isfile(queueFilename):
|
||||||
os.remove(queueFilename)
|
os.remove(queueFilename)
|
||||||
if len(queue) > 0:
|
if len(queue) > 0:
|
||||||
queue.pop(0)
|
queue.pop(0)
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
if checkJsonSignature and verifyAllSignatures:
|
if httpSignatureFailed or verifyAllSignatures:
|
||||||
# use the original json message received, not one which may have
|
# use the original json message received, not one which
|
||||||
# been modified along the way
|
# may have been modified along the way
|
||||||
if not verifyJsonSignature(originalJson, pubKey):
|
if not verifyJsonSignature(originalJson, pubKey):
|
||||||
if debug:
|
if debug:
|
||||||
print('WARN: jsonld inbox signature check failed ' +
|
print('WARN: jsonld inbox signature check failed ' +
|
||||||
|
|
@ -2787,6 +2803,10 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
||||||
if len(queue) > 0:
|
if len(queue) > 0:
|
||||||
queue.pop(0)
|
queue.pop(0)
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
|
if httpSignatureFailed:
|
||||||
|
print('jsonld inbox signature check success ' +
|
||||||
|
'via relay ' + keyId)
|
||||||
else:
|
else:
|
||||||
print('jsonld inbox signature check success ' + keyId)
|
print('jsonld inbox signature check success ' + keyId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -660,6 +660,7 @@ def runNewswireDaemon(baseDir: str, httpd,
|
||||||
"""Periodically updates RSS feeds
|
"""Periodically updates RSS feeds
|
||||||
"""
|
"""
|
||||||
newswireStateFilename = baseDir + '/accounts/.newswirestate.json'
|
newswireStateFilename = baseDir + '/accounts/.newswirestate.json'
|
||||||
|
refreshFilename = baseDir + '/accounts/.refresh_newswire'
|
||||||
|
|
||||||
# initial sleep to allow the system to start up
|
# initial sleep to allow the system to start up
|
||||||
time.sleep(50)
|
time.sleep(50)
|
||||||
|
|
@ -722,7 +723,16 @@ def runNewswireDaemon(baseDir: str, httpd,
|
||||||
httpd.maxNewsPosts)
|
httpd.maxNewsPosts)
|
||||||
|
|
||||||
# wait a while before the next feeds update
|
# wait a while before the next feeds update
|
||||||
time.sleep(1200)
|
for tick in range(120):
|
||||||
|
time.sleep(10)
|
||||||
|
# if a new blog post has been created then stop
|
||||||
|
# waiting and recalculate the newswire
|
||||||
|
if os.path.isfile(refreshFilename):
|
||||||
|
try:
|
||||||
|
os.remove(refreshFilename)
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def runNewswireWatchdog(projectVersion: str, httpd) -> None:
|
def runNewswireWatchdog(projectVersion: str, httpd) -> None:
|
||||||
|
|
|
||||||
140
newswire.py
|
|
@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
|
||||||
__status__ = "Production"
|
__status__ = "Production"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
import requests
|
import requests
|
||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
import errno
|
import errno
|
||||||
|
|
@ -301,6 +302,7 @@ def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
continue
|
continue
|
||||||
title = rssItem.split('<title>')[1]
|
title = rssItem.split('<title>')[1]
|
||||||
title = _removeCDATA(title.split('</title>')[0])
|
title = _removeCDATA(title.split('</title>')[0])
|
||||||
|
title = removeHtml(title)
|
||||||
description = ''
|
description = ''
|
||||||
if '<description>' in rssItem and '</description>' in rssItem:
|
if '<description>' in rssItem and '</description>' in rssItem:
|
||||||
description = rssItem.split('<description>')[1]
|
description = rssItem.split('<description>')[1]
|
||||||
|
|
@ -332,12 +334,14 @@ def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
result, pubDateStr,
|
result, pubDateStr,
|
||||||
title, link,
|
title, link,
|
||||||
votesStatus, postFilename,
|
votesStatus, postFilename,
|
||||||
description, moderated, mirrored)
|
description, moderated,
|
||||||
|
mirrored)
|
||||||
postCtr += 1
|
postCtr += 1
|
||||||
if postCtr >= maxPostsPerSource:
|
if postCtr >= maxPostsPerSource:
|
||||||
break
|
break
|
||||||
if postCtr > 0:
|
if postCtr > 0:
|
||||||
print('Added ' + str(postCtr) + ' rss 2.0 feed items to newswire')
|
print('Added ' + str(postCtr) +
|
||||||
|
' rss 2.0 feed items to newswire')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -385,6 +389,7 @@ def _xml1StrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
continue
|
continue
|
||||||
title = rssItem.split('<title>')[1]
|
title = rssItem.split('<title>')[1]
|
||||||
title = _removeCDATA(title.split('</title>')[0])
|
title = _removeCDATA(title.split('</title>')[0])
|
||||||
|
title = removeHtml(title)
|
||||||
description = ''
|
description = ''
|
||||||
if '<description>' in rssItem and '</description>' in rssItem:
|
if '<description>' in rssItem and '</description>' in rssItem:
|
||||||
description = rssItem.split('<description>')[1]
|
description = rssItem.split('<description>')[1]
|
||||||
|
|
@ -416,12 +421,14 @@ def _xml1StrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
result, pubDateStr,
|
result, pubDateStr,
|
||||||
title, link,
|
title, link,
|
||||||
votesStatus, postFilename,
|
votesStatus, postFilename,
|
||||||
description, moderated, mirrored)
|
description, moderated,
|
||||||
|
mirrored)
|
||||||
postCtr += 1
|
postCtr += 1
|
||||||
if postCtr >= maxPostsPerSource:
|
if postCtr >= maxPostsPerSource:
|
||||||
break
|
break
|
||||||
if postCtr > 0:
|
if postCtr > 0:
|
||||||
print('Added ' + str(postCtr) + ' rss 1.0 feed items to newswire')
|
print('Added ' + str(postCtr) +
|
||||||
|
' rss 1.0 feed items to newswire')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -457,6 +464,7 @@ def _atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
continue
|
continue
|
||||||
title = atomItem.split('<title>')[1]
|
title = atomItem.split('<title>')[1]
|
||||||
title = _removeCDATA(title.split('</title>')[0])
|
title = _removeCDATA(title.split('</title>')[0])
|
||||||
|
title = removeHtml(title)
|
||||||
description = ''
|
description = ''
|
||||||
if '<summary>' in atomItem and '</summary>' in atomItem:
|
if '<summary>' in atomItem and '</summary>' in atomItem:
|
||||||
description = atomItem.split('<summary>')[1]
|
description = atomItem.split('<summary>')[1]
|
||||||
|
|
@ -488,12 +496,124 @@ def _atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
result, pubDateStr,
|
result, pubDateStr,
|
||||||
title, link,
|
title, link,
|
||||||
votesStatus, postFilename,
|
votesStatus, postFilename,
|
||||||
description, moderated, mirrored)
|
description, moderated,
|
||||||
|
mirrored)
|
||||||
postCtr += 1
|
postCtr += 1
|
||||||
if postCtr >= maxPostsPerSource:
|
if postCtr >= maxPostsPerSource:
|
||||||
break
|
break
|
||||||
if postCtr > 0:
|
if postCtr > 0:
|
||||||
print('Added ' + str(postCtr) + ' atom feed items to newswire')
|
print('Added ' + str(postCtr) +
|
||||||
|
' atom feed items to newswire')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _jsonFeedV1ToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
|
moderated: bool, mirrored: bool,
|
||||||
|
maxPostsPerSource: int,
|
||||||
|
maxFeedItemSizeKb: int) -> {}:
|
||||||
|
"""Converts a json feed string to a dictionary
|
||||||
|
See https://jsonfeed.org/version/1.1
|
||||||
|
"""
|
||||||
|
if '"items"' not in xmlStr:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
feedJson = json.loads(xmlStr)
|
||||||
|
except BaseException:
|
||||||
|
return {}
|
||||||
|
maxBytes = maxFeedItemSizeKb * 1024
|
||||||
|
if not feedJson.get('version'):
|
||||||
|
return {}
|
||||||
|
if not feedJson['version'].startswith('https://jsonfeed.org/version/1'):
|
||||||
|
return {}
|
||||||
|
if not feedJson.get('items'):
|
||||||
|
return {}
|
||||||
|
if not isinstance(feedJson['items'], list):
|
||||||
|
return {}
|
||||||
|
postCtr = 0
|
||||||
|
result = {}
|
||||||
|
for jsonFeedItem in feedJson['items']:
|
||||||
|
if not jsonFeedItem:
|
||||||
|
continue
|
||||||
|
if not isinstance(jsonFeedItem, dict):
|
||||||
|
continue
|
||||||
|
if not jsonFeedItem.get('url'):
|
||||||
|
continue
|
||||||
|
if not isinstance(jsonFeedItem['url'], str):
|
||||||
|
continue
|
||||||
|
if not jsonFeedItem.get('date_published'):
|
||||||
|
if not jsonFeedItem.get('date_modified'):
|
||||||
|
continue
|
||||||
|
if not jsonFeedItem.get('content_text'):
|
||||||
|
if not jsonFeedItem.get('content_html'):
|
||||||
|
continue
|
||||||
|
if jsonFeedItem.get('content_html'):
|
||||||
|
if not isinstance(jsonFeedItem['content_html'], str):
|
||||||
|
continue
|
||||||
|
title = removeHtml(jsonFeedItem['content_html'])
|
||||||
|
else:
|
||||||
|
if not isinstance(jsonFeedItem['content_text'], str):
|
||||||
|
continue
|
||||||
|
title = removeHtml(jsonFeedItem['content_text'])
|
||||||
|
if len(title) > maxBytes:
|
||||||
|
print('WARN: json feed title is too long')
|
||||||
|
continue
|
||||||
|
description = ''
|
||||||
|
if jsonFeedItem.get('description'):
|
||||||
|
if not isinstance(jsonFeedItem['description'], str):
|
||||||
|
continue
|
||||||
|
description = removeHtml(jsonFeedItem['description'])
|
||||||
|
if len(description) > maxBytes:
|
||||||
|
print('WARN: json feed description is too long')
|
||||||
|
continue
|
||||||
|
if jsonFeedItem.get('tags'):
|
||||||
|
if isinstance(jsonFeedItem['tags'], list):
|
||||||
|
for tagName in jsonFeedItem['tags']:
|
||||||
|
if not isinstance(tagName, str):
|
||||||
|
continue
|
||||||
|
if ' ' in tagName:
|
||||||
|
continue
|
||||||
|
if not tagName.startswith('#'):
|
||||||
|
tagName = '#' + tagName
|
||||||
|
if tagName not in description:
|
||||||
|
description += ' ' + tagName
|
||||||
|
|
||||||
|
link = jsonFeedItem['url']
|
||||||
|
if '://' not in link:
|
||||||
|
continue
|
||||||
|
if len(link) > maxBytes:
|
||||||
|
print('WARN: json feed link is too long')
|
||||||
|
continue
|
||||||
|
itemDomain = link.split('://')[1]
|
||||||
|
if '/' in itemDomain:
|
||||||
|
itemDomain = itemDomain.split('/')[0]
|
||||||
|
if isBlockedDomain(baseDir, itemDomain):
|
||||||
|
continue
|
||||||
|
if jsonFeedItem.get('date_published'):
|
||||||
|
if not isinstance(jsonFeedItem['date_published'], str):
|
||||||
|
continue
|
||||||
|
pubDate = jsonFeedItem['date_published']
|
||||||
|
else:
|
||||||
|
if not isinstance(jsonFeedItem['date_modified'], str):
|
||||||
|
continue
|
||||||
|
pubDate = jsonFeedItem['date_modified']
|
||||||
|
|
||||||
|
pubDateStr = parseFeedDate(pubDate)
|
||||||
|
if pubDateStr:
|
||||||
|
if _validFeedDate(pubDateStr):
|
||||||
|
postFilename = ''
|
||||||
|
votesStatus = []
|
||||||
|
_addNewswireDictEntry(baseDir, domain,
|
||||||
|
result, pubDateStr,
|
||||||
|
title, link,
|
||||||
|
votesStatus, postFilename,
|
||||||
|
description, moderated,
|
||||||
|
mirrored)
|
||||||
|
postCtr += 1
|
||||||
|
if postCtr >= maxPostsPerSource:
|
||||||
|
break
|
||||||
|
if postCtr > 0:
|
||||||
|
print('Added ' + str(postCtr) +
|
||||||
|
' json feed items to newswire')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -593,6 +713,10 @@ def _xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||||
return _atomFeedToDict(baseDir, domain,
|
return _atomFeedToDict(baseDir, domain,
|
||||||
xmlStr, moderated, mirrored,
|
xmlStr, moderated, mirrored,
|
||||||
maxPostsPerSource, maxFeedItemSizeKb)
|
maxPostsPerSource, maxFeedItemSizeKb)
|
||||||
|
elif 'https://jsonfeed.org/version/1' in xmlStr:
|
||||||
|
return _jsonFeedV1ToDict(baseDir, domain,
|
||||||
|
xmlStr, moderated, mirrored,
|
||||||
|
maxPostsPerSource, maxFeedItemSizeKb)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -794,7 +918,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str,
|
||||||
locatePost(baseDir, nickname,
|
locatePost(baseDir, nickname,
|
||||||
domain, postUrl, False)
|
domain, postUrl, False)
|
||||||
if not fullPostFilename:
|
if not fullPostFilename:
|
||||||
print('Unable to locate post ' + postUrl)
|
print('Unable to locate post for newswire ' + postUrl)
|
||||||
ctr += 1
|
ctr += 1
|
||||||
if ctr >= maxBlogsPerAccount:
|
if ctr >= maxBlogsPerAccount:
|
||||||
break
|
break
|
||||||
|
|
@ -840,7 +964,7 @@ def _addBlogsToNewswire(baseDir: str, domain: str, newswire: {},
|
||||||
for handle in dirs:
|
for handle in dirs:
|
||||||
if '@' not in handle:
|
if '@' not in handle:
|
||||||
continue
|
continue
|
||||||
if 'inbox@' in handle:
|
if 'inbox@' in handle or 'news@' in handle:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
nickname = handle.split('@')[0]
|
nickname = handle.split('@')[0]
|
||||||
|
|
|
||||||
23
outbox.py
|
|
@ -14,10 +14,12 @@ from posts import outboxMessageCreateWrap
|
||||||
from posts import savePostToBox
|
from posts import savePostToBox
|
||||||
from posts import sendToFollowersThread
|
from posts import sendToFollowersThread
|
||||||
from posts import sendToNamedAddresses
|
from posts import sendToNamedAddresses
|
||||||
|
from utils import getLocalNetworkAddresses
|
||||||
from utils import getFullDomain
|
from utils import getFullDomain
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
from utils import getDomainFromActor
|
from utils import getDomainFromActor
|
||||||
from utils import dangerousMarkup
|
from utils import dangerousMarkup
|
||||||
|
from utils import isFeaturedWriter
|
||||||
from blocking import isBlockedDomain
|
from blocking import isBlockedDomain
|
||||||
from blocking import outboxBlock
|
from blocking import outboxBlock
|
||||||
from blocking import outboxUndoBlock
|
from blocking import outboxUndoBlock
|
||||||
|
|
@ -113,6 +115,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
||||||
'Create does not have the "to" parameter ' +
|
'Create does not have the "to" parameter ' +
|
||||||
str(messageJson))
|
str(messageJson))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# actor should be a string
|
||||||
|
if not isinstance(messageJson['actor'], str):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# actor should look like a url
|
||||||
|
if '://' not in messageJson['actor'] or \
|
||||||
|
'.' not in messageJson['actor']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# sent by an actor on a local network address?
|
||||||
|
if not allowLocalNetworkAccess:
|
||||||
|
localNetworkPatternList = getLocalNetworkAddresses()
|
||||||
|
for localNetworkPattern in localNetworkPatternList:
|
||||||
|
if localNetworkPattern in messageJson['actor']:
|
||||||
|
return False
|
||||||
|
|
||||||
testDomain, testPort = getDomainFromActor(messageJson['actor'])
|
testDomain, testPort = getDomainFromActor(messageJson['actor'])
|
||||||
testDomain = getFullDomain(testDomain, testPort)
|
testDomain = getFullDomain(testDomain, testPort)
|
||||||
if isBlockedDomain(baseDir, testDomain):
|
if isBlockedDomain(baseDir, testDomain):
|
||||||
|
|
@ -211,8 +230,10 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
||||||
# save all instance blogs to the news actor
|
# save all instance blogs to the news actor
|
||||||
if postToNickname != 'news' and outboxName == 'tlblogs':
|
if postToNickname != 'news' and outboxName == 'tlblogs':
|
||||||
if '/' in savedFilename:
|
if '/' in savedFilename:
|
||||||
|
if isFeaturedWriter(baseDir, postToNickname, domain):
|
||||||
savedPostId = savedFilename.split('/')[-1]
|
savedPostId = savedFilename.split('/')[-1]
|
||||||
blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs'
|
blogsDir = \
|
||||||
|
baseDir + '/accounts/news@' + domain + '/tlblogs'
|
||||||
if not os.path.isdir(blogsDir):
|
if not os.path.isdir(blogsDir):
|
||||||
os.mkdir(blogsDir)
|
os.mkdir(blogsDir)
|
||||||
copyfile(savedFilename, blogsDir + '/' + savedPostId)
|
copyfile(savedFilename, blogsDir + '/' + savedPostId)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ from utils import loadJson
|
||||||
from utils import saveJson
|
from utils import saveJson
|
||||||
from utils import setConfigParam
|
from utils import setConfigParam
|
||||||
from utils import getConfigParam
|
from utils import getConfigParam
|
||||||
|
from utils import refreshNewswire
|
||||||
|
|
||||||
|
|
||||||
def generateRSAKey() -> (str, str):
|
def generateRSAKey() -> (str, str):
|
||||||
|
|
@ -915,6 +916,9 @@ def removeAccount(baseDir: str, nickname: str,
|
||||||
os.remove(baseDir + '/wfdeactivated/' + handle + '.json')
|
os.remove(baseDir + '/wfdeactivated/' + handle + '.json')
|
||||||
if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname):
|
if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname):
|
||||||
shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname)
|
shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname)
|
||||||
|
|
||||||
|
refreshNewswire(baseDir)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -944,6 +948,9 @@ def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool:
|
||||||
os.mkdir(deactivatedSharefilesDir)
|
os.mkdir(deactivatedSharefilesDir)
|
||||||
shutil.move(baseDir + '/sharefiles/' + nickname,
|
shutil.move(baseDir + '/sharefiles/' + nickname,
|
||||||
deactivatedSharefilesDir + '/' + nickname)
|
deactivatedSharefilesDir + '/' + nickname)
|
||||||
|
|
||||||
|
refreshNewswire(baseDir)
|
||||||
|
|
||||||
return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
|
return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -970,6 +977,8 @@ def activateAccount(baseDir: str, nickname: str, domain: str) -> None:
|
||||||
shutil.move(deactivatedSharefilesDir + '/' + nickname,
|
shutil.move(deactivatedSharefilesDir + '/' + nickname,
|
||||||
baseDir + '/sharefiles/' + nickname)
|
baseDir + '/sharefiles/' + nickname)
|
||||||
|
|
||||||
|
refreshNewswire(baseDir)
|
||||||
|
|
||||||
|
|
||||||
def isPersonSnoozed(baseDir: str, nickname: str, domain: str,
|
def isPersonSnoozed(baseDir: str, nickname: str, domain: str,
|
||||||
snoozeActor: str) -> bool:
|
snoozeActor: str) -> bool:
|
||||||
|
|
|
||||||
95
posts.py
|
|
@ -30,6 +30,8 @@ from session import postJsonString
|
||||||
from session import postImage
|
from session import postImage
|
||||||
from webfinger import webfingerHandle
|
from webfinger import webfingerHandle
|
||||||
from httpsig import createSignedHeader
|
from httpsig import createSignedHeader
|
||||||
|
from siteactive import siteIsActive
|
||||||
|
from utils import removeInvalidChars
|
||||||
from utils import fileLastModified
|
from utils import fileLastModified
|
||||||
from utils import isPublicPost
|
from utils import isPublicPost
|
||||||
from utils import hasUsersPath
|
from utils import hasUsersPath
|
||||||
|
|
@ -38,7 +40,6 @@ from utils import getFullDomain
|
||||||
from utils import getFollowersList
|
from utils import getFollowersList
|
||||||
from utils import isEvil
|
from utils import isEvil
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
from utils import siteIsActive
|
|
||||||
from utils import getCachedPostFilename
|
from utils import getCachedPostFilename
|
||||||
from utils import getStatusNumber
|
from utils import getStatusNumber
|
||||||
from utils import createPersonDir
|
from utils import createPersonDir
|
||||||
|
|
@ -823,7 +824,7 @@ def validContentWarning(cw: str) -> str:
|
||||||
# so remove them
|
# so remove them
|
||||||
if '#' in cw:
|
if '#' in cw:
|
||||||
cw = cw.replace('#', '').replace(' ', ' ')
|
cw = cw.replace('#', '').replace(' ', ' ')
|
||||||
return cw
|
return removeInvalidChars(cw)
|
||||||
|
|
||||||
|
|
||||||
def _loadAutoCW(baseDir: str, nickname: str, domain: str) -> []:
|
def _loadAutoCW(baseDir: str, nickname: str, domain: str) -> []:
|
||||||
|
|
@ -880,6 +881,8 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
eventStatus=None, ticketUrl=None) -> {}:
|
eventStatus=None, ticketUrl=None) -> {}:
|
||||||
"""Creates a message
|
"""Creates a message
|
||||||
"""
|
"""
|
||||||
|
content = removeInvalidChars(content)
|
||||||
|
|
||||||
subject = _addAutoCW(baseDir, nickname, domain, subject, content)
|
subject = _addAutoCW(baseDir, nickname, domain, subject, content)
|
||||||
|
|
||||||
if nickname != 'news':
|
if nickname != 'news':
|
||||||
|
|
@ -924,7 +927,7 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
sensitive = False
|
sensitive = False
|
||||||
summary = None
|
summary = None
|
||||||
if subject:
|
if subject:
|
||||||
summary = validContentWarning(subject)
|
summary = removeInvalidChars(validContentWarning(subject))
|
||||||
sensitive = True
|
sensitive = True
|
||||||
|
|
||||||
toRecipients = []
|
toRecipients = []
|
||||||
|
|
@ -1047,6 +1050,8 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
||||||
postObjectType = 'Note'
|
postObjectType = 'Note'
|
||||||
if eventUUID:
|
if eventUUID:
|
||||||
postObjectType = 'Event'
|
postObjectType = 'Event'
|
||||||
|
if isArticle:
|
||||||
|
postObjectType = 'Article'
|
||||||
|
|
||||||
if not clientToServer:
|
if not clientToServer:
|
||||||
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
||||||
|
|
@ -1389,10 +1394,22 @@ def createPublicPost(baseDir: str,
|
||||||
imageDescription: str,
|
imageDescription: str,
|
||||||
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
||||||
schedulePost=False,
|
schedulePost=False,
|
||||||
eventDate=None, eventTime=None, location=None) -> {}:
|
eventDate=None, eventTime=None, location=None,
|
||||||
|
isArticle=False) -> {}:
|
||||||
"""Public post
|
"""Public post
|
||||||
"""
|
"""
|
||||||
domainFull = getFullDomain(domain, port)
|
domainFull = getFullDomain(domain, port)
|
||||||
|
isModerationReport = False
|
||||||
|
eventUUID = None
|
||||||
|
category = None
|
||||||
|
joinMode = None
|
||||||
|
endDate = None
|
||||||
|
endTime = None
|
||||||
|
maximumAttendeeCapacity = None
|
||||||
|
repliesModerationOption = None
|
||||||
|
anonymousParticipationEnabled = None
|
||||||
|
eventStatus = None
|
||||||
|
ticketUrl = None
|
||||||
return _createPostBase(baseDir, nickname, domain, port,
|
return _createPostBase(baseDir, nickname, domain, port,
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
httpPrefix + '://' + domainFull + '/users/' +
|
httpPrefix + '://' + domainFull + '/users/' +
|
||||||
|
|
@ -1401,38 +1418,27 @@ def createPublicPost(baseDir: str,
|
||||||
clientToServer, commentsEnabled,
|
clientToServer, commentsEnabled,
|
||||||
attachImageFilename, mediaType,
|
attachImageFilename, mediaType,
|
||||||
imageDescription,
|
imageDescription,
|
||||||
False, False, inReplyTo, inReplyToAtomUri, subject,
|
isModerationReport, isArticle,
|
||||||
schedulePost, eventDate, eventTime, location,
|
|
||||||
None, None, None, None, None,
|
|
||||||
None, None, None, None, None)
|
|
||||||
|
|
||||||
|
|
||||||
def createBlogPost(baseDir: str,
|
|
||||||
nickname: str, domain: str, port: int, httpPrefix: str,
|
|
||||||
content: str, followersOnly: bool, saveToFile: bool,
|
|
||||||
clientToServer: bool, commentsEnabled: bool,
|
|
||||||
attachImageFilename: str, mediaType: str,
|
|
||||||
imageDescription: str,
|
|
||||||
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
|
||||||
schedulePost=False,
|
|
||||||
eventDate=None, eventTime=None, location=None) -> {}:
|
|
||||||
blog = \
|
|
||||||
createPublicPost(baseDir,
|
|
||||||
nickname, domain, port, httpPrefix,
|
|
||||||
content, followersOnly, saveToFile,
|
|
||||||
clientToServer, commentsEnabled,
|
|
||||||
attachImageFilename, mediaType,
|
|
||||||
imageDescription,
|
|
||||||
inReplyTo, inReplyToAtomUri, subject,
|
inReplyTo, inReplyToAtomUri, subject,
|
||||||
schedulePost,
|
schedulePost, eventDate, eventTime, location,
|
||||||
eventDate, eventTime, location)
|
eventUUID, category, joinMode, endDate, endTime,
|
||||||
blog['object']['type'] = 'Article'
|
maximumAttendeeCapacity,
|
||||||
|
repliesModerationOption,
|
||||||
|
anonymousParticipationEnabled,
|
||||||
|
eventStatus, ticketUrl)
|
||||||
|
|
||||||
|
|
||||||
|
def _appendCitationsToBlogPost(baseDir: str,
|
||||||
|
nickname: str, domain: str,
|
||||||
|
blogJson: {}) -> None:
|
||||||
|
"""Appends any citations to a new blog post
|
||||||
|
"""
|
||||||
# append citations tags, stored in a file
|
# append citations tags, stored in a file
|
||||||
citationsFilename = \
|
citationsFilename = \
|
||||||
baseDir + '/accounts/' + \
|
baseDir + '/accounts/' + \
|
||||||
nickname + '@' + domain + '/.citations.txt'
|
nickname + '@' + domain + '/.citations.txt'
|
||||||
if os.path.isfile(citationsFilename):
|
if not os.path.isfile(citationsFilename):
|
||||||
|
return
|
||||||
citationsSeparator = '#####'
|
citationsSeparator = '#####'
|
||||||
with open(citationsFilename, "r") as f:
|
with open(citationsFilename, "r") as f:
|
||||||
citations = f.readlines()
|
citations = f.readlines()
|
||||||
|
|
@ -1450,9 +1456,32 @@ def createBlogPost(baseDir: str,
|
||||||
"name": title,
|
"name": title,
|
||||||
"url": link
|
"url": link
|
||||||
}
|
}
|
||||||
blog['object']['tag'].append(tagJson)
|
blogJson['object']['tag'].append(tagJson)
|
||||||
|
|
||||||
return blog
|
|
||||||
|
def createBlogPost(baseDir: str,
|
||||||
|
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||||
|
content: str, followersOnly: bool, saveToFile: bool,
|
||||||
|
clientToServer: bool, commentsEnabled: bool,
|
||||||
|
attachImageFilename: str, mediaType: str,
|
||||||
|
imageDescription: str,
|
||||||
|
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
||||||
|
schedulePost=False,
|
||||||
|
eventDate=None, eventTime=None, location=None) -> {}:
|
||||||
|
blogJson = \
|
||||||
|
createPublicPost(baseDir,
|
||||||
|
nickname, domain, port, httpPrefix,
|
||||||
|
content, followersOnly, saveToFile,
|
||||||
|
clientToServer, commentsEnabled,
|
||||||
|
attachImageFilename, mediaType,
|
||||||
|
imageDescription,
|
||||||
|
inReplyTo, inReplyToAtomUri, subject,
|
||||||
|
schedulePost,
|
||||||
|
eventDate, eventTime, location, True)
|
||||||
|
|
||||||
|
_appendCitationsToBlogPost(baseDir, nickname, domain, blogJson)
|
||||||
|
|
||||||
|
return blogJson
|
||||||
|
|
||||||
|
|
||||||
def createNewsPost(baseDir: str,
|
def createNewsPost(baseDir: str,
|
||||||
|
|
@ -1477,7 +1506,7 @@ def createNewsPost(baseDir: str,
|
||||||
imageDescription,
|
imageDescription,
|
||||||
inReplyTo, inReplyToAtomUri, subject,
|
inReplyTo, inReplyToAtomUri, subject,
|
||||||
schedulePost,
|
schedulePost,
|
||||||
eventDate, eventTime, location)
|
eventDate, eventTime, location, True)
|
||||||
blog['object']['type'] = 'Article'
|
blog['object']['type'] = 'Article'
|
||||||
return blog
|
return blog
|
||||||
|
|
||||||
|
|
|
||||||
32
session.py
|
|
@ -53,6 +53,37 @@ def createSession(proxyType: str):
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def urlExists(session, url: str, timeoutSec=3,
|
||||||
|
httpPrefix='https', domain='testdomain') -> bool:
|
||||||
|
if not isinstance(url, str):
|
||||||
|
print('url: ' + str(url))
|
||||||
|
print('ERROR: urlExists failed, url should be a string')
|
||||||
|
return False
|
||||||
|
sessionParams = {}
|
||||||
|
sessionHeaders = {}
|
||||||
|
sessionHeaders['User-Agent'] = 'Epicyon/' + __version__
|
||||||
|
if domain:
|
||||||
|
sessionHeaders['User-Agent'] += \
|
||||||
|
'; +' + httpPrefix + '://' + domain + '/'
|
||||||
|
if not session:
|
||||||
|
print('WARN: urlExists failed, no session specified')
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
result = session.get(url, headers=sessionHeaders,
|
||||||
|
params=sessionParams,
|
||||||
|
timeout=timeoutSec)
|
||||||
|
if result:
|
||||||
|
if result.status_code == 200 or \
|
||||||
|
result.status_code == 304:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print('urlExists for ' + url + ' returned ' +
|
||||||
|
str(result.status_code))
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def getJson(session, url: str, headers: {}, params: {},
|
def getJson(session, url: str, headers: {}, params: {},
|
||||||
version='1.2.0', httpPrefix='https',
|
version='1.2.0', httpPrefix='https',
|
||||||
domain='testdomain') -> {}:
|
domain='testdomain') -> {}:
|
||||||
|
|
@ -72,6 +103,7 @@ def getJson(session, url: str, headers: {}, params: {},
|
||||||
'; +' + httpPrefix + '://' + domain + '/'
|
'; +' + httpPrefix + '://' + domain + '/'
|
||||||
if not session:
|
if not session:
|
||||||
print('WARN: getJson failed, no session specified for getJson')
|
print('WARN: getJson failed, no session specified for getJson')
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
result = session.get(url, headers=sessionHeaders, params=sessionParams)
|
result = session.get(url, headers=sessionHeaders, params=sessionParams)
|
||||||
return result.json()
|
return result.json()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
__filename__ = "siteactive.py"
|
||||||
|
__author__ = "Bob Mottram"
|
||||||
|
__credits__ = ["webchk"]
|
||||||
|
__license__ = "AGPL3+"
|
||||||
|
__version__ = "1.2.0"
|
||||||
|
__maintainer__ = "Bob Mottram"
|
||||||
|
__email__ = "bob@freedombone.net"
|
||||||
|
__status__ = "Production"
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
|
||||||
|
class Result:
|
||||||
|
"""Holds result of an URL check.
|
||||||
|
|
||||||
|
The redirect attribute is a Result object that the URL was redirected to.
|
||||||
|
|
||||||
|
The sitemap_urls attribute will contain a list of Result object if url
|
||||||
|
is a sitemap file and http_response() was run with parse set to True.
|
||||||
|
"""
|
||||||
|
def __init__(self, url):
|
||||||
|
self.url = url
|
||||||
|
self.status = 0
|
||||||
|
self.desc = ''
|
||||||
|
self.headers = None
|
||||||
|
self.latency = 0
|
||||||
|
self.content = ''
|
||||||
|
self.redirect = None
|
||||||
|
self.sitemap_urls = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.status == 0:
|
||||||
|
return '{} ... {}'.format(self.url, self.desc)
|
||||||
|
return '{} ... {} {} ({})'.format(
|
||||||
|
self.url, self.status, self.desc, self.latency
|
||||||
|
)
|
||||||
|
|
||||||
|
def fill_headers(self, headers):
|
||||||
|
"""Takes a list of tuples and converts it a dictionary."""
|
||||||
|
self.headers = {h[0]: h[1] for h in headers}
|
||||||
|
|
||||||
|
|
||||||
|
def _siteActiveParseUrl(url):
|
||||||
|
"""Returns an object with properties representing
|
||||||
|
|
||||||
|
scheme: URL scheme specifier
|
||||||
|
netloc: Network location part
|
||||||
|
path: Hierarchical path
|
||||||
|
params: Parameters for last path element
|
||||||
|
query: Query component
|
||||||
|
fragment: Fragment identifier
|
||||||
|
username: User name
|
||||||
|
password: Password
|
||||||
|
hostname: Host name (lower case)
|
||||||
|
port: Port number as integer, if present
|
||||||
|
"""
|
||||||
|
loc = urlparse(url)
|
||||||
|
|
||||||
|
# if the scheme (http, https ...) is not available urlparse wont work
|
||||||
|
if loc.scheme == "":
|
||||||
|
url = "http://" + url
|
||||||
|
loc = urlparse(url)
|
||||||
|
return loc
|
||||||
|
|
||||||
|
|
||||||
|
def _siteACtiveHttpConnect(loc, timeout: int):
|
||||||
|
"""Connects to the host and returns an HTTP or HTTPS connections."""
|
||||||
|
if loc.scheme == "https":
|
||||||
|
ssl_context = ssl.SSLContext()
|
||||||
|
return http.client.HTTPSConnection(
|
||||||
|
loc.netloc, context=ssl_context, timeout=timeout)
|
||||||
|
return http.client.HTTPConnection(loc.netloc, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _siteActiveHttpRequest(loc, timeout: int):
|
||||||
|
"""Performs a HTTP request and return response in a Result object.
|
||||||
|
"""
|
||||||
|
conn = _siteACtiveHttpConnect(loc, timeout)
|
||||||
|
method = 'HEAD'
|
||||||
|
|
||||||
|
conn.request(method, loc.path)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
|
||||||
|
result = Result(loc.geturl())
|
||||||
|
result.status = resp.status
|
||||||
|
result.desc = resp.reason
|
||||||
|
result.fill_headers(resp.getheaders())
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def siteIsActive(url: str, timeout=10) -> bool:
|
||||||
|
"""Returns true if the current url is resolvable.
|
||||||
|
This can be used to check that an instance is online before
|
||||||
|
trying to send posts to it.
|
||||||
|
"""
|
||||||
|
if not url.startswith('http'):
|
||||||
|
return False
|
||||||
|
if '.onion/' in url or '.i2p/' in url or \
|
||||||
|
url.endswith('.onion') or \
|
||||||
|
url.endswith('.i2p'):
|
||||||
|
# skip this check for onion and i2p
|
||||||
|
return True
|
||||||
|
|
||||||
|
loc = _siteActiveParseUrl(url)
|
||||||
|
result = Result(url=url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = _siteActiveHttpRequest(loc, timeout)
|
||||||
|
|
||||||
|
if 400 <= result.status < 500:
|
||||||
|
return result
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except BaseException:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
18
tests.py
|
|
@ -38,7 +38,7 @@ from utils import getFullDomain
|
||||||
from utils import validNickname
|
from utils import validNickname
|
||||||
from utils import firstParagraphFromString
|
from utils import firstParagraphFromString
|
||||||
from utils import removeIdEnding
|
from utils import removeIdEnding
|
||||||
from utils import siteIsActive
|
from siteactive import siteIsActive
|
||||||
from utils import updateRecentPostsCache
|
from utils import updateRecentPostsCache
|
||||||
from utils import followPerson
|
from utils import followPerson
|
||||||
from utils import getNicknameFromActor
|
from utils import getNicknameFromActor
|
||||||
|
|
@ -325,8 +325,10 @@ def createServerAlice(path: str, domain: str, port: int,
|
||||||
sendThreadsTimeoutMins = 30
|
sendThreadsTimeoutMins = 30
|
||||||
maxFollowers = 10
|
maxFollowers = 10
|
||||||
verifyAllSignatures = True
|
verifyAllSignatures = True
|
||||||
|
brochMode = False
|
||||||
print('Server running: Alice')
|
print('Server running: Alice')
|
||||||
runDaemon(verifyAllSignatures,
|
runDaemon(brochMode,
|
||||||
|
verifyAllSignatures,
|
||||||
sendThreadsTimeoutMins,
|
sendThreadsTimeoutMins,
|
||||||
dormantMonths, maxNewswirePosts,
|
dormantMonths, maxNewswirePosts,
|
||||||
allowLocalNetworkAccess,
|
allowLocalNetworkAccess,
|
||||||
|
|
@ -420,8 +422,10 @@ def createServerBob(path: str, domain: str, port: int,
|
||||||
sendThreadsTimeoutMins = 30
|
sendThreadsTimeoutMins = 30
|
||||||
maxFollowers = 10
|
maxFollowers = 10
|
||||||
verifyAllSignatures = True
|
verifyAllSignatures = True
|
||||||
|
brochMode = False
|
||||||
print('Server running: Bob')
|
print('Server running: Bob')
|
||||||
runDaemon(verifyAllSignatures,
|
runDaemon(brochMode,
|
||||||
|
verifyAllSignatures,
|
||||||
sendThreadsTimeoutMins,
|
sendThreadsTimeoutMins,
|
||||||
dormantMonths, maxNewswirePosts,
|
dormantMonths, maxNewswirePosts,
|
||||||
allowLocalNetworkAccess,
|
allowLocalNetworkAccess,
|
||||||
|
|
@ -469,8 +473,10 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
|
||||||
sendThreadsTimeoutMins = 30
|
sendThreadsTimeoutMins = 30
|
||||||
maxFollowers = 10
|
maxFollowers = 10
|
||||||
verifyAllSignatures = True
|
verifyAllSignatures = True
|
||||||
|
brochMode = False
|
||||||
print('Server running: Eve')
|
print('Server running: Eve')
|
||||||
runDaemon(verifyAllSignatures,
|
runDaemon(brochMode,
|
||||||
|
verifyAllSignatures,
|
||||||
sendThreadsTimeoutMins,
|
sendThreadsTimeoutMins,
|
||||||
dormantMonths, maxNewswirePosts,
|
dormantMonths, maxNewswirePosts,
|
||||||
allowLocalNetworkAccess,
|
allowLocalNetworkAccess,
|
||||||
|
|
@ -2067,6 +2073,7 @@ def testJsonld():
|
||||||
|
|
||||||
def testSiteIsActive():
|
def testSiteIsActive():
|
||||||
print('testSiteIsActive')
|
print('testSiteIsActive')
|
||||||
|
assert(siteIsActive('https://archive.org'))
|
||||||
assert(siteIsActive('https://mastodon.social'))
|
assert(siteIsActive('https://mastodon.social'))
|
||||||
assert(not siteIsActive('https://notarealwebsite.a.b.c'))
|
assert(not siteIsActive('https://notarealwebsite.a.b.c'))
|
||||||
|
|
||||||
|
|
@ -2818,7 +2825,8 @@ def testFunctions():
|
||||||
'createServerBob',
|
'createServerBob',
|
||||||
'createServerEve',
|
'createServerEve',
|
||||||
'E2EEremoveDevice',
|
'E2EEremoveDevice',
|
||||||
'setOrganizationScheme'
|
'setOrganizationScheme',
|
||||||
|
'fill_headers'
|
||||||
]
|
]
|
||||||
excludeImports = [
|
excludeImports = [
|
||||||
'link',
|
'link',
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 131 KiB |
|
|
@ -1,4 +1,6 @@
|
||||||
{
|
{
|
||||||
|
"post-separator-margin-top": "10px",
|
||||||
|
"post-separator-margin-bottom": "10px",
|
||||||
"newswire-publish-icon": "True",
|
"newswire-publish-icon": "True",
|
||||||
"full-width-timeline-buttons": "False",
|
"full-width-timeline-buttons": "False",
|
||||||
"icons-as-buttons": "False",
|
"icons-as-buttons": "False",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"header-bg-color": "#efefef",
|
"header-bg-color": "#efefef",
|
||||||
"verticals-width": "10px",
|
"verticals-width": "10px",
|
||||||
"newswire-publish-icon": "False",
|
"newswire-publish-icon": "False",
|
||||||
"line-spacing-newswire": "150%",
|
"line-spacing-newswire": "180%",
|
||||||
"full-width-timeline-buttons": "False",
|
"full-width-timeline-buttons": "False",
|
||||||
"icons-as-buttons": "True",
|
"icons-as-buttons": "True",
|
||||||
"rss-icon-at-top": "False",
|
"rss-icon-at-top": "False",
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
"event-color": "#0481f5",
|
"event-color": "#0481f5",
|
||||||
"event-background": "#00014a",
|
"event-background": "#00014a",
|
||||||
"quote-right-margin": "0",
|
"quote-right-margin": "0",
|
||||||
"line-spacing": "150%",
|
"line-spacing": "180%",
|
||||||
"header-font": "'solidaric'",
|
"header-font": "'solidaric'",
|
||||||
"*font-family": "'solidaric'",
|
"*font-family": "'solidaric'",
|
||||||
"*src": "url('./fonts/solidaric.woff2') format('woff2')",
|
"*src": "url('./fonts/solidaric.woff2') format('woff2')",
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
"event-color": "#0481f5",
|
"event-color": "#0481f5",
|
||||||
"event-background": "#00014a",
|
"event-background": "#00014a",
|
||||||
"quote-right-margin": "0",
|
"quote-right-margin": "0",
|
||||||
"line-spacing": "150%",
|
"line-spacing": "180%",
|
||||||
"*font-family": "'Montserrat-Regular'",
|
"*font-family": "'Montserrat-Regular'",
|
||||||
"*src": "url('./fonts/Montserrat-Regular.ttf') format('truetype')",
|
"*src": "url('./fonts/Montserrat-Regular.ttf') format('truetype')",
|
||||||
"**font-family": "'Orbitron'",
|
"**font-family": "'Orbitron'",
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
"title-background": "#ccc",
|
"title-background": "#ccc",
|
||||||
"gallery-text-color": "#2d2c37",
|
"gallery-text-color": "#2d2c37",
|
||||||
"quote-right-margin": "0",
|
"quote-right-margin": "0",
|
||||||
"line-spacing": "150%",
|
"line-spacing": "180%",
|
||||||
"header-font": "'solidaric'",
|
"header-font": "'solidaric'",
|
||||||
"*font-family": "'solidaric'",
|
"*font-family": "'solidaric'",
|
||||||
"*src": "url('./fonts/solidaric.woff2') format('woff2')",
|
"*src": "url('./fonts/solidaric.woff2') format('woff2')",
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "تخطي إلى الجدول الزمني",
|
"Skip to timeline": "تخطي إلى الجدول الزمني",
|
||||||
"Skip to Newswire": "انتقل إلى Newswire",
|
"Skip to Newswire": "انتقل إلى Newswire",
|
||||||
"Skip to Links": "تخطي إلى روابط الويب",
|
"Skip to Links": "تخطي إلى روابط الويب",
|
||||||
"Publish a blog article": "نشر مقال بلوق"
|
"Publish a blog article": "نشر مقال بلوق",
|
||||||
|
"Featured writer": "كاتب متميز",
|
||||||
|
"Broch mode": "وضع الكتيب"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Ves a la cronologia",
|
"Skip to timeline": "Ves a la cronologia",
|
||||||
"Skip to Newswire": "Vés a Newswire",
|
"Skip to Newswire": "Vés a Newswire",
|
||||||
"Skip to Links": "Vés als enllaços web",
|
"Skip to Links": "Vés als enllaços web",
|
||||||
"Publish a blog article": "Publicar un article del bloc"
|
"Publish a blog article": "Publicar un article del bloc",
|
||||||
|
"Featured writer": "Escriptor destacat",
|
||||||
|
"Broch mode": "Mode Broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Neidio i'r llinell amser",
|
"Skip to timeline": "Neidio i'r llinell amser",
|
||||||
"Skip to Newswire": "Neidio i Newswire",
|
"Skip to Newswire": "Neidio i Newswire",
|
||||||
"Skip to Links": "Neidio i Dolenni Gwe",
|
"Skip to Links": "Neidio i Dolenni Gwe",
|
||||||
"Publish a blog article": "Cyhoeddi erthygl blog"
|
"Publish a blog article": "Cyhoeddi erthygl blog",
|
||||||
|
"Featured writer": "Awdur dan sylw",
|
||||||
|
"Broch mode": "Modd Broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Zur Zeitleiste springen",
|
"Skip to timeline": "Zur Zeitleiste springen",
|
||||||
"Skip to Newswire": "Springe zu Newswire",
|
"Skip to Newswire": "Springe zu Newswire",
|
||||||
"Skip to Links": "Springe zu Weblinks",
|
"Skip to Links": "Springe zu Weblinks",
|
||||||
"Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel"
|
"Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel",
|
||||||
|
"Featured writer": "Ausgewählter Schriftsteller",
|
||||||
|
"Broch mode": "Broch-Modus"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Skip to timeline",
|
"Skip to timeline": "Skip to timeline",
|
||||||
"Skip to Newswire": "Skip to Newswire",
|
"Skip to Newswire": "Skip to Newswire",
|
||||||
"Skip to Links": "Skip to Links",
|
"Skip to Links": "Skip to Links",
|
||||||
"Publish a blog article": "Publish a blog article"
|
"Publish a blog article": "Publish a blog article",
|
||||||
|
"Featured writer": "Featured writer",
|
||||||
|
"Broch mode": "Broch mode"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Saltar a la línea de tiempo",
|
"Skip to timeline": "Saltar a la línea de tiempo",
|
||||||
"Skip to Newswire": "Saltar a Newswire",
|
"Skip to Newswire": "Saltar a Newswire",
|
||||||
"Skip to Links": "Saltar a enlaces web",
|
"Skip to Links": "Saltar a enlaces web",
|
||||||
"Publish a blog article": "Publica un artículo de blog"
|
"Publish a blog article": "Publica un artículo de blog",
|
||||||
|
"Featured writer": "Escritora destacada",
|
||||||
|
"Broch mode": "Modo broche"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Passer à la chronologie",
|
"Skip to timeline": "Passer à la chronologie",
|
||||||
"Skip to Newswire": "Passer à Newswire",
|
"Skip to Newswire": "Passer à Newswire",
|
||||||
"Skip to Links": "Passer aux liens Web",
|
"Skip to Links": "Passer aux liens Web",
|
||||||
"Publish a blog article": "Publier un article de blog"
|
"Publish a blog article": "Publier un article de blog",
|
||||||
|
"Featured writer": "Écrivain en vedette",
|
||||||
|
"Broch mode": "Mode Broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Scipeáil chuig an amlíne",
|
"Skip to timeline": "Scipeáil chuig an amlíne",
|
||||||
"Skip to Newswire": "Scipeáil chuig Newswire",
|
"Skip to Newswire": "Scipeáil chuig Newswire",
|
||||||
"Skip to Links": "Scipeáil chuig Naisc Ghréasáin",
|
"Skip to Links": "Scipeáil chuig Naisc Ghréasáin",
|
||||||
"Publish a blog article": "Foilsigh alt blagála"
|
"Publish a blog article": "Foilsigh alt blagála",
|
||||||
|
"Featured writer": "Scríbhneoir mór le rá",
|
||||||
|
"Broch mode": "Modh broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "टाइमलाइन पर जाएं",
|
"Skip to timeline": "टाइमलाइन पर जाएं",
|
||||||
"Skip to Newswire": "Newswire पर जाएं",
|
"Skip to Newswire": "Newswire पर जाएं",
|
||||||
"Skip to Links": "वेब लिंक पर जाएं",
|
"Skip to Links": "वेब लिंक पर जाएं",
|
||||||
"Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें"
|
"Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें",
|
||||||
|
"Featured writer": "फीचर्ड लेखक",
|
||||||
|
"Broch mode": "ब्रोच मोड"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Passa alla sequenza temporale",
|
"Skip to timeline": "Passa alla sequenza temporale",
|
||||||
"Skip to Newswire": "Passa a Newswire",
|
"Skip to Newswire": "Passa a Newswire",
|
||||||
"Skip to Links": "Passa a collegamenti Web",
|
"Skip to Links": "Passa a collegamenti Web",
|
||||||
"Publish a blog article": "Pubblica un articolo sul blog"
|
"Publish a blog article": "Pubblica un articolo sul blog",
|
||||||
|
"Featured writer": "Scrittore in primo piano",
|
||||||
|
"Broch mode": "Modalità Broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "タイムラインにスキップ",
|
"Skip to timeline": "タイムラインにスキップ",
|
||||||
"Skip to Newswire": "Newswireにスキップ",
|
"Skip to Newswire": "Newswireにスキップ",
|
||||||
"Skip to Links": "Webリンクにスキップ",
|
"Skip to Links": "Webリンクにスキップ",
|
||||||
"Publish a blog article": "ブログ記事を公開する"
|
"Publish a blog article": "ブログ記事を公開する",
|
||||||
|
"Featured writer": "注目の作家",
|
||||||
|
"Broch mode": "ブロッホモード"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -363,5 +363,7 @@
|
||||||
"Skip to timeline": "Skip to timeline",
|
"Skip to timeline": "Skip to timeline",
|
||||||
"Skip to Newswire": "Skip to Newswire",
|
"Skip to Newswire": "Skip to Newswire",
|
||||||
"Skip to Links": "Skip to Links",
|
"Skip to Links": "Skip to Links",
|
||||||
"Publish a blog article": "Publish a blog article"
|
"Publish a blog article": "Publish a blog article",
|
||||||
|
"Featured writer": "Featured writer",
|
||||||
|
"Broch mode": "Broch mode"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Pular para a linha do tempo",
|
"Skip to timeline": "Pular para a linha do tempo",
|
||||||
"Skip to Newswire": "Pular para Newswire",
|
"Skip to Newswire": "Pular para Newswire",
|
||||||
"Skip to Links": "Pular para links da web",
|
"Skip to Links": "Pular para links da web",
|
||||||
"Publish a blog article": "Publique um artigo de blog"
|
"Publish a blog article": "Publique um artigo de blog",
|
||||||
|
"Featured writer": "Escritor em destaque",
|
||||||
|
"Broch mode": "Modo broch"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "Перейти к временной шкале",
|
"Skip to timeline": "Перейти к временной шкале",
|
||||||
"Skip to Newswire": "Перейти к ленте новостей",
|
"Skip to Newswire": "Перейти к ленте новостей",
|
||||||
"Skip to Links": "Перейти к веб-ссылкам",
|
"Skip to Links": "Перейти к веб-ссылкам",
|
||||||
"Publish a blog article": "Опубликовать статью в блоге"
|
"Publish a blog article": "Опубликовать статью в блоге",
|
||||||
|
"Featured writer": "Избранный писатель",
|
||||||
|
"Broch mode": "Брош режим"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,5 +367,7 @@
|
||||||
"Skip to timeline": "跳到时间线",
|
"Skip to timeline": "跳到时间线",
|
||||||
"Skip to Newswire": "跳到新闻专线",
|
"Skip to Newswire": "跳到新闻专线",
|
||||||
"Skip to Links": "跳到网页链接",
|
"Skip to Links": "跳到网页链接",
|
||||||
"Publish a blog article": "发布博客文章"
|
"Publish a blog article": "发布博客文章",
|
||||||
|
"Featured writer": "特色作家",
|
||||||
|
"Broch mode": "断点模式"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
utils.py
|
|
@ -11,9 +11,6 @@ import time
|
||||||
import shutil
|
import shutil
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
from socket import error as SocketError
|
|
||||||
import errno
|
|
||||||
import urllib.request
|
|
||||||
import idna
|
import idna
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
|
@ -21,6 +18,34 @@ from followingCalendar import addPersonToCalendar
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
|
||||||
|
# posts containing these strings will always get screened out,
|
||||||
|
# both incoming and outgoing.
|
||||||
|
# Could include dubious clacks or admin dogwhistles
|
||||||
|
invalidCharacters = (
|
||||||
|
'卐', '卍', '࿕', '࿖', '࿗', '࿘'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isFeaturedWriter(baseDir: str, nickname: str, domain: str) -> bool:
|
||||||
|
"""Is the given account a featured writer, appearing in the features
|
||||||
|
timeline on news instances?
|
||||||
|
"""
|
||||||
|
featuresBlockedFilename = \
|
||||||
|
baseDir + '/accounts/' + \
|
||||||
|
nickname + '@' + domain + '/.nofeatures'
|
||||||
|
return not os.path.isfile(featuresBlockedFilename)
|
||||||
|
|
||||||
|
|
||||||
|
def refreshNewswire(baseDir: str):
|
||||||
|
"""Causes the newswire to be updates after a change to user accounts
|
||||||
|
"""
|
||||||
|
refreshNewswireFilename = baseDir + '/accounts/.refresh_newswire'
|
||||||
|
if os.path.isfile(refreshNewswireFilename):
|
||||||
|
return
|
||||||
|
refreshFile = open(refreshNewswireFilename, 'w+')
|
||||||
|
refreshFile.write('\n')
|
||||||
|
refreshFile.close()
|
||||||
|
|
||||||
|
|
||||||
def getSHA256(msg: str):
|
def getSHA256(msg: str):
|
||||||
"""Returns a SHA256 hash of the given string
|
"""Returns a SHA256 hash of the given string
|
||||||
|
|
@ -517,17 +542,23 @@ def isEvil(domain: str) -> bool:
|
||||||
|
|
||||||
def containsInvalidChars(jsonStr: str) -> bool:
|
def containsInvalidChars(jsonStr: str) -> bool:
|
||||||
"""Does the given json string contain invalid characters?
|
"""Does the given json string contain invalid characters?
|
||||||
e.g. dubious clacks/admin dogwhistles
|
|
||||||
"""
|
"""
|
||||||
invalidStrings = {
|
for isInvalid in invalidCharacters:
|
||||||
'卐', '卍', '࿕', '࿖', '࿗', '࿘'
|
|
||||||
}
|
|
||||||
for isInvalid in invalidStrings:
|
|
||||||
if isInvalid in jsonStr:
|
if isInvalid in jsonStr:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def removeInvalidChars(text: str) -> str:
|
||||||
|
"""Removes any invalid characters from a string
|
||||||
|
"""
|
||||||
|
for isInvalid in invalidCharacters:
|
||||||
|
if isInvalid not in text:
|
||||||
|
continue
|
||||||
|
text = text.replace(isInvalid, '')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def createPersonDir(nickname: str, domain: str, baseDir: str,
|
def createPersonDir(nickname: str, domain: str, baseDir: str,
|
||||||
dirname: str) -> str:
|
dirname: str) -> str:
|
||||||
"""Create a directory for a person
|
"""Create a directory for a person
|
||||||
|
|
@ -574,6 +605,12 @@ def urlPermitted(url: str, federationList: []):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def getLocalNetworkAddresses() -> []:
|
||||||
|
"""Returns patterns for local network address detection
|
||||||
|
"""
|
||||||
|
return ('localhost', '127.0.', '192.168', '10.0.')
|
||||||
|
|
||||||
|
|
||||||
def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
|
def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
|
||||||
"""Returns true if the given content contains dangerous html markup
|
"""Returns true if the given content contains dangerous html markup
|
||||||
"""
|
"""
|
||||||
|
|
@ -584,7 +621,7 @@ def dangerousMarkup(content: str, allowLocalNetworkAccess: bool) -> bool:
|
||||||
contentSections = content.split('<')
|
contentSections = content.split('<')
|
||||||
invalidPartials = ()
|
invalidPartials = ()
|
||||||
if not allowLocalNetworkAccess:
|
if not allowLocalNetworkAccess:
|
||||||
invalidPartials = ('localhost', '127.0.', '192.168', '10.0.')
|
invalidPartials = getLocalNetworkAddresses()
|
||||||
invalidStrings = ('script', 'canvas', 'style', 'abbr',
|
invalidStrings = ('script', 'canvas', 'style', 'abbr',
|
||||||
'frame', 'iframe', 'html', 'body',
|
'frame', 'iframe', 'html', 'body',
|
||||||
'hr', 'allow-popups', 'allow-scripts')
|
'hr', 'allow-popups', 'allow-scripts')
|
||||||
|
|
@ -1841,28 +1878,6 @@ def updateAnnounceCollection(recentPostsCache: {},
|
||||||
saveJson(postJsonObject, postFilename)
|
saveJson(postJsonObject, postFilename)
|
||||||
|
|
||||||
|
|
||||||
def siteIsActive(url: str) -> bool:
|
|
||||||
"""Returns true if the current url is resolvable.
|
|
||||||
This can be used to check that an instance is online before
|
|
||||||
trying to send posts to it.
|
|
||||||
"""
|
|
||||||
if not url.startswith('http'):
|
|
||||||
return False
|
|
||||||
if '.onion/' in url or '.i2p/' in url or \
|
|
||||||
url.endswith('.onion') or \
|
|
||||||
url.endswith('.i2p'):
|
|
||||||
# skip this check for onion and i2p
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
req = urllib.request.Request(url)
|
|
||||||
urllib.request.urlopen(req, timeout=10) # nosec
|
|
||||||
return True
|
|
||||||
except SocketError as e:
|
|
||||||
if e.errno == errno.ECONNRESET:
|
|
||||||
print('WARN: connection was reset during siteIsActive')
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def weekDayOfMonthStart(monthNumber: int, year: int) -> int:
|
def weekDayOfMonthStart(monthNumber: int, year: int) -> int:
|
||||||
"""Gets the day number of the first day of the month
|
"""Gets the day number of the first day of the month
|
||||||
1=sun, 7=sat
|
1=sun, 7=sat
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ from happening import getCalendarEvents
|
||||||
from webapp_utils import htmlHeaderWithExternalStyle
|
from webapp_utils import htmlHeaderWithExternalStyle
|
||||||
from webapp_utils import htmlFooter
|
from webapp_utils import htmlFooter
|
||||||
from webapp_utils import getAltPath
|
from webapp_utils import getAltPath
|
||||||
|
from webapp_utils import htmlHideFromScreenReader
|
||||||
|
from webapp_utils import htmlKeyboardNavigation
|
||||||
|
|
||||||
|
|
||||||
def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str,
|
def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str,
|
||||||
|
|
@ -200,7 +202,8 @@ def _htmlCalendarDay(cssCache: {}, translate: {},
|
||||||
|
|
||||||
def htmlCalendar(cssCache: {}, translate: {},
|
def htmlCalendar(cssCache: {}, translate: {},
|
||||||
baseDir: str, path: str,
|
baseDir: str, path: str,
|
||||||
httpPrefix: str, domainFull: str) -> str:
|
httpPrefix: str, domainFull: str,
|
||||||
|
textModeBanner: str) -> str:
|
||||||
"""Show the calendar for a person
|
"""Show the calendar for a person
|
||||||
"""
|
"""
|
||||||
domain = domainFull
|
domain = domainFull
|
||||||
|
|
@ -297,8 +300,10 @@ def htmlCalendar(cssCache: {}, translate: {},
|
||||||
|
|
||||||
instanceTitle = \
|
instanceTitle = \
|
||||||
getConfigParam(baseDir, 'instanceTitle')
|
getConfigParam(baseDir, 'instanceTitle')
|
||||||
calendarStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
|
headerStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
|
||||||
calendarStr += '<main><table class="calendar">\n'
|
|
||||||
|
# the main graphical calendar as a table
|
||||||
|
calendarStr = '<main><table class="calendar">\n'
|
||||||
calendarStr += '<caption class="calendar__banner--month">\n'
|
calendarStr += '<caption class="calendar__banner--month">\n'
|
||||||
calendarStr += \
|
calendarStr += \
|
||||||
' <a href="' + calActor + '/calendar?year=' + str(prevYear) + \
|
' <a href="' + calActor + '/calendar?year=' + str(prevYear) + \
|
||||||
|
|
@ -320,24 +325,30 @@ def htmlCalendar(cssCache: {}, translate: {},
|
||||||
calendarStr += '</caption>\n'
|
calendarStr += '</caption>\n'
|
||||||
calendarStr += '<thead>\n'
|
calendarStr += '<thead>\n'
|
||||||
calendarStr += '<tr>\n'
|
calendarStr += '<tr>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Sun'] + '</th>\n'
|
translate['Sun'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Mon'] + '</th>\n'
|
translate['Mon'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Tue'] + '</th>\n'
|
translate['Tue'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Wed'] + '</th>\n'
|
translate['Wed'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Thu'] + '</th>\n'
|
translate['Thu'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Fri'] + '</th>\n'
|
translate['Fri'] + '</th>\n'
|
||||||
calendarStr += ' <th class="calendar__day__header">' + \
|
calendarStr += ' <th scope="col" class="calendar__day__header">' + \
|
||||||
translate['Sat'] + '</th>\n'
|
translate['Sat'] + '</th>\n'
|
||||||
calendarStr += '</tr>\n'
|
calendarStr += '</tr>\n'
|
||||||
calendarStr += '</thead>\n'
|
calendarStr += '</thead>\n'
|
||||||
calendarStr += '<tbody>\n'
|
calendarStr += '<tbody>\n'
|
||||||
|
|
||||||
|
# beginning of the links used for accessibility
|
||||||
|
navLinks = {}
|
||||||
|
timelineLinkStr = htmlHideFromScreenReader('🏠') + ' ' + \
|
||||||
|
translate['Switch to timeline view']
|
||||||
|
navLinks[timelineLinkStr] = calActor + '/inbox'
|
||||||
|
|
||||||
dayOfMonth = 0
|
dayOfMonth = 0
|
||||||
dow = weekDayOfMonthStart(monthNumber, year)
|
dow = weekDayOfMonthStart(monthNumber, year)
|
||||||
for weekOfMonth in range(1, 7):
|
for weekOfMonth in range(1, 7):
|
||||||
|
|
@ -358,8 +369,15 @@ def htmlCalendar(cssCache: {}, translate: {},
|
||||||
url = calActor + '/calendar?year=' + \
|
url = calActor + '/calendar?year=' + \
|
||||||
str(year) + '?month=' + \
|
str(year) + '?month=' + \
|
||||||
str(monthNumber) + '?day=' + str(dayOfMonth)
|
str(monthNumber) + '?day=' + str(dayOfMonth)
|
||||||
dayLink = '<a href="' + url + '">' + \
|
dayDescription = monthName + ' ' + str(dayOfMonth)
|
||||||
|
dayLink = '<a href="' + url + '" ' + \
|
||||||
|
'title="' + dayDescription + '">' + \
|
||||||
str(dayOfMonth) + '</a>'
|
str(dayOfMonth) + '</a>'
|
||||||
|
# accessibility menu links
|
||||||
|
menuOptionStr = \
|
||||||
|
htmlHideFromScreenReader('📅') + ' ' + \
|
||||||
|
dayDescription
|
||||||
|
navLinks[menuOptionStr] = url
|
||||||
# there are events for this day
|
# there are events for this day
|
||||||
if not isToday:
|
if not isToday:
|
||||||
calendarStr += \
|
calendarStr += \
|
||||||
|
|
@ -387,5 +405,17 @@ def htmlCalendar(cssCache: {}, translate: {},
|
||||||
|
|
||||||
calendarStr += '</tbody>\n'
|
calendarStr += '</tbody>\n'
|
||||||
calendarStr += '</table></main>\n'
|
calendarStr += '</table></main>\n'
|
||||||
calendarStr += htmlFooter()
|
|
||||||
return calendarStr
|
# end of the links used for accessibility
|
||||||
|
nextMonthStr = \
|
||||||
|
htmlHideFromScreenReader('→') + ' ' + translate['Next month']
|
||||||
|
navLinks[nextMonthStr] = calActor + '/calendar?year=' + str(nextYear) + \
|
||||||
|
'?month=' + str(nextMonthNumber)
|
||||||
|
prevMonthStr = \
|
||||||
|
htmlHideFromScreenReader('←') + ' ' + translate['Previous month']
|
||||||
|
navLinks[prevMonthStr] = calActor + '/calendar?year=' + str(prevYear) + \
|
||||||
|
'?month=' + str(prevMonthNumber)
|
||||||
|
screenReaderCal = \
|
||||||
|
htmlKeyboardNavigation(textModeBanner, navLinks, monthName)
|
||||||
|
|
||||||
|
return headerStr + screenReaderCal + calendarStr + htmlFooter()
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,10 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str:
|
||||||
categoryStr = \
|
categoryStr = \
|
||||||
getHashtagCategory(baseDir, hashTagName)
|
getHashtagCategory(baseDir, hashTagName)
|
||||||
if len(categoryStr) < maxTagLength:
|
if len(categoryStr) < maxTagLength:
|
||||||
|
if '#' not in categoryStr and \
|
||||||
|
'&' not in categoryStr and \
|
||||||
|
'"' not in categoryStr and \
|
||||||
|
"'" not in categoryStr:
|
||||||
if categoryStr not in categorySwarm:
|
if categoryStr not in categorySwarm:
|
||||||
categorySwarm.append(categoryStr)
|
categorySwarm.append(categoryStr)
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from utils import isDormant
|
||||||
from utils import removeHtml
|
from utils import removeHtml
|
||||||
from utils import getDomainFromActor
|
from utils import getDomainFromActor
|
||||||
from utils import getNicknameFromActor
|
from utils import getNicknameFromActor
|
||||||
|
from utils import isFeaturedWriter
|
||||||
from blocking import isBlocked
|
from blocking import isBlocked
|
||||||
from follow import isFollowerOfPerson
|
from follow import isFollowerOfPerson
|
||||||
from follow import isFollowingActor
|
from follow import isFollowingActor
|
||||||
|
|
@ -24,6 +25,7 @@ from followingCalendar import receivingCalendarEvents
|
||||||
from webapp_utils import htmlHeaderWithExternalStyle
|
from webapp_utils import htmlHeaderWithExternalStyle
|
||||||
from webapp_utils import htmlFooter
|
from webapp_utils import htmlFooter
|
||||||
from webapp_utils import getBrokenLinkSubstitute
|
from webapp_utils import getBrokenLinkSubstitute
|
||||||
|
from webapp_utils import htmlKeyboardNavigation
|
||||||
|
|
||||||
|
|
||||||
def htmlPersonOptions(defaultTimeline: str,
|
def htmlPersonOptions(defaultTimeline: str,
|
||||||
|
|
@ -49,7 +51,9 @@ def htmlPersonOptions(defaultTimeline: str,
|
||||||
backToPath: str,
|
backToPath: str,
|
||||||
lockedAccount: bool,
|
lockedAccount: bool,
|
||||||
movedTo: str,
|
movedTo: str,
|
||||||
alsoKnownAs: []) -> str:
|
alsoKnownAs: [],
|
||||||
|
textModeBanner: str,
|
||||||
|
newsInstance: bool) -> str:
|
||||||
"""Show options for a person: view/follow/block/report
|
"""Show options for a person: view/follow/block/report
|
||||||
"""
|
"""
|
||||||
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
|
optionsDomain, optionsPort = getDomainFromActor(optionsActor)
|
||||||
|
|
@ -108,12 +112,13 @@ def htmlPersonOptions(defaultTimeline: str,
|
||||||
if donateUrl:
|
if donateUrl:
|
||||||
donateStr = \
|
donateStr = \
|
||||||
' <a href="' + donateUrl + \
|
' <a href="' + donateUrl + \
|
||||||
'"><button class="button" name="submitDonate">' + \
|
' tabindex="-1""><button class="button" name="submitDonate">' + \
|
||||||
translate['Donate'] + '</button></a>\n'
|
translate['Donate'] + '</button></a>\n'
|
||||||
|
|
||||||
instanceTitle = \
|
instanceTitle = \
|
||||||
getConfigParam(baseDir, 'instanceTitle')
|
getConfigParam(baseDir, 'instanceTitle')
|
||||||
optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
|
optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
|
||||||
|
optionsStr += htmlKeyboardNavigation(textModeBanner, {})
|
||||||
optionsStr += '<br><br>\n'
|
optionsStr += '<br><br>\n'
|
||||||
optionsStr += '<div class="options">\n'
|
optionsStr += '<div class="options">\n'
|
||||||
optionsStr += ' <div class="optionsAvatar">\n'
|
optionsStr += ' <div class="optionsAvatar">\n'
|
||||||
|
|
@ -284,6 +289,24 @@ def htmlPersonOptions(defaultTimeline: str,
|
||||||
checkboxStr = checkboxStr.replace(' checked>', '>')
|
checkboxStr = checkboxStr.replace(' checked>', '>')
|
||||||
optionsStr += checkboxStr
|
optionsStr += checkboxStr
|
||||||
|
|
||||||
|
# checkbox for permission to post to featured articles
|
||||||
|
if newsInstance and optionsDomainFull == domainFull:
|
||||||
|
adminNickname = getConfigParam(baseDir, 'admin')
|
||||||
|
if (nickname == adminNickname or
|
||||||
|
(isModerator(baseDir, nickname) and
|
||||||
|
not isModerator(baseDir, optionsNickname))):
|
||||||
|
checkboxStr = \
|
||||||
|
' <input type="checkbox" ' + \
|
||||||
|
'class="profilecheckbox" name="postsToFeatures" checked> ' + \
|
||||||
|
translate['Featured writer'] + \
|
||||||
|
'\n <button type="submit" class="buttonsmall" ' + \
|
||||||
|
'name="submitPostToFeatures">' + \
|
||||||
|
translate['Submit'] + '</button><br>\n'
|
||||||
|
if not isFeaturedWriter(baseDir, optionsNickname,
|
||||||
|
optionsDomain):
|
||||||
|
checkboxStr = checkboxStr.replace(' checked>', '>')
|
||||||
|
optionsStr += checkboxStr
|
||||||
|
|
||||||
optionsStr += optionsLinkStr
|
optionsStr += optionsLinkStr
|
||||||
backPath = '/'
|
backPath = '/'
|
||||||
if nickname:
|
if nickname:
|
||||||
|
|
|
||||||
|
|
@ -327,9 +327,13 @@ def _getEditIconHtml(baseDir: str, nickname: str, domainFull: str,
|
||||||
"""
|
"""
|
||||||
editStr = ''
|
editStr = ''
|
||||||
actor = postJsonObject['actor']
|
actor = postJsonObject['actor']
|
||||||
if (actor.endswith(domainFull + '/users/' + nickname) or
|
# This should either be a post which you created,
|
||||||
|
# or it could be generated from the newswire (see
|
||||||
|
# _addBlogsToNewswire) in which case anyone with
|
||||||
|
# editor status should be able to alter it
|
||||||
|
if (actor.endswith('/' + domainFull + '/users/' + nickname) or
|
||||||
(isEditor(baseDir, nickname) and
|
(isEditor(baseDir, nickname) and
|
||||||
actor.endswith(domainFull + '/users/news'))):
|
actor.endswith('/' + domainFull + '/users/news'))):
|
||||||
|
|
||||||
postId = postJsonObject['object']['id']
|
postId = postJsonObject['object']['id']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1278,6 +1278,16 @@ def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
|
||||||
' <input type="checkbox" class="profilecheckbox" ' + \
|
' <input type="checkbox" class="profilecheckbox" ' + \
|
||||||
'name="verifyallsignatures"> ' + \
|
'name="verifyallsignatures"> ' + \
|
||||||
translate['Verify all signatures'] + '<br>\n'
|
translate['Verify all signatures'] + '<br>\n'
|
||||||
|
if getConfigParam(baseDir, "brochMode"):
|
||||||
|
instanceStr += \
|
||||||
|
' <input type="checkbox" class="profilecheckbox" ' + \
|
||||||
|
'name="brochMode" checked> ' + \
|
||||||
|
translate['Broch mode'] + '<br>\n'
|
||||||
|
else:
|
||||||
|
instanceStr += \
|
||||||
|
' <input type="checkbox" class="profilecheckbox" ' + \
|
||||||
|
'name="brochMode"> ' + \
|
||||||
|
translate['Broch mode'] + '<br>\n'
|
||||||
instanceStr += '</div>'
|
instanceStr += '</div>'
|
||||||
|
|
||||||
moderators = ''
|
moderators = ''
|
||||||
|
|
@ -1745,6 +1755,10 @@ def _individualFollowAsHtml(translate: {},
|
||||||
if avatarUrl2:
|
if avatarUrl2:
|
||||||
avatarUrl = avatarUrl2
|
avatarUrl = avatarUrl2
|
||||||
if displayName:
|
if displayName:
|
||||||
|
displayName = \
|
||||||
|
addEmojiToDisplayName(baseDir, httpPrefix,
|
||||||
|
actorNickname, domain,
|
||||||
|
displayName, False)
|
||||||
titleStr = displayName
|
titleStr = displayName
|
||||||
|
|
||||||
if dormant:
|
if dormant:
|
||||||
|
|
|
||||||
|
|
@ -416,23 +416,23 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
|
||||||
translate['Mod']
|
translate['Mod']
|
||||||
navLinks = {
|
navLinks = {
|
||||||
menuProfile: '/users/' + nickname,
|
menuProfile: '/users/' + nickname,
|
||||||
menuInbox: usersPath + '/inbox#timeline',
|
menuInbox: usersPath + '/inbox#timelineposts',
|
||||||
menuSearch: usersPath + '/search',
|
menuSearch: usersPath + '/search',
|
||||||
menuNewPost: usersPath + '/newpost',
|
menuNewPost: usersPath + '/newpost',
|
||||||
menuCalendar: usersPath + '/calendar',
|
menuCalendar: usersPath + '/calendar',
|
||||||
menuDM: usersPath + '/dm#timeline',
|
menuDM: usersPath + '/dm#timelineposts',
|
||||||
menuReplies: usersPath + '/tlreplies#timeline',
|
menuReplies: usersPath + '/tlreplies#timelineposts',
|
||||||
menuOutbox: usersPath + '/inbox#timeline',
|
menuOutbox: usersPath + '/inbox#timelineposts',
|
||||||
menuBookmarks: usersPath + '/tlbookmarks#timeline',
|
menuBookmarks: usersPath + '/tlbookmarks#timelineposts',
|
||||||
menuShares: usersPath + '/tlshares#timeline',
|
menuShares: usersPath + '/tlshares#timelineposts',
|
||||||
menuBlogs: usersPath + '/tlblogs#timeline',
|
menuBlogs: usersPath + '/tlblogs#timelineposts',
|
||||||
# menuEvents: usersPath + '/tlevents#timeline',
|
# menuEvents: usersPath + '/tlevents#timelineposts',
|
||||||
menuNewswire: '#newswire',
|
menuNewswire: usersPath + '/newswiremobile',
|
||||||
menuLinks: '#links'
|
menuLinks: usersPath + '/linksmobile'
|
||||||
}
|
}
|
||||||
if moderator:
|
if moderator:
|
||||||
navLinks[menuModeration] = usersPath + '/moderation#modtimeline'
|
navLinks[menuModeration] = usersPath + '/moderation#modtimeline'
|
||||||
tlStr += htmlKeyboardNavigation(textModeBanner, navLinks,
|
tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, None,
|
||||||
usersPath, translate, followApprovals)
|
usersPath, translate, followApprovals)
|
||||||
|
|
||||||
# banner and row of buttons
|
# banner and row of buttons
|
||||||
|
|
@ -502,7 +502,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
|
||||||
calendarImage, followApprovals,
|
calendarImage, followApprovals,
|
||||||
iconsAsButtons)
|
iconsAsButtons)
|
||||||
|
|
||||||
tlStr += ' <div id="timeline" class="timeline-posts">\n'
|
tlStr += ' <div id="timelineposts" class="timeline-posts">\n'
|
||||||
|
|
||||||
# second row of buttons for moderator actions
|
# second row of buttons for moderator actions
|
||||||
if moderator and boxName == 'moderation':
|
if moderator and boxName == 'moderation':
|
||||||
|
|
|
||||||
|
|
@ -887,26 +887,31 @@ def htmlHideFromScreenReader(htmlStr: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def htmlKeyboardNavigation(banner: str, links: {},
|
def htmlKeyboardNavigation(banner: str, links: {},
|
||||||
|
subHeading=None,
|
||||||
usersPath=None, translate=None,
|
usersPath=None, translate=None,
|
||||||
followApprovals=False) -> str:
|
followApprovals=False) -> str:
|
||||||
"""Given a set of links return the html for keyboard navigation
|
"""Given a set of links return the html for keyboard navigation
|
||||||
"""
|
"""
|
||||||
htmlStr = '<div class="transparent"><ul>'
|
htmlStr = '<div class="transparent"><ul>\n'
|
||||||
|
|
||||||
if banner:
|
if banner:
|
||||||
htmlStr += '<pre aria-label="">' + banner + '<br><br></pre>'
|
htmlStr += '<pre aria-label="">\n' + banner + '\n<br><br></pre>\n'
|
||||||
|
|
||||||
|
if subHeading:
|
||||||
|
htmlStr += '<strong><label class="transparent">' + \
|
||||||
|
subHeading + '</label></strong><br>\n'
|
||||||
|
|
||||||
# show new follower approvals
|
# show new follower approvals
|
||||||
if usersPath and translate and followApprovals:
|
if usersPath and translate and followApprovals:
|
||||||
htmlStr += '<strong><label class="transparent">' + \
|
htmlStr += '<strong><label class="transparent">' + \
|
||||||
'<a href="' + usersPath + '/followers#timeline">' + \
|
'<a href="' + usersPath + '/followers#timeline">' + \
|
||||||
translate['Approve follow requests'] + '</a>' + \
|
translate['Approve follow requests'] + '</a>' + \
|
||||||
'</label></strong><br><br>'
|
'</label></strong><br><br>\n'
|
||||||
|
|
||||||
# show the list of links
|
# show the list of links
|
||||||
for title, url in links.items():
|
for title, url in links.items():
|
||||||
htmlStr += '<li><label class="transparent">' + \
|
htmlStr += '<li><label class="transparent">' + \
|
||||||
'<a href="' + str(url) + '">' + \
|
'<a href="' + str(url) + '">' + \
|
||||||
str(title) + '</a></label></li>'
|
str(title) + '</a></label></li>\n'
|
||||||
htmlStr += '</ul></div>'
|
htmlStr += '</ul></div>\n'
|
||||||
return htmlStr
|
return htmlStr
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="description" content="ActivityPub server written in Python, HTML and CSS, and suitable for self-hosting on single board computers">
|
||||||
|
<meta name="keywords" content="ActivityPub, Fediverse, Python, HTML, CSS">
|
||||||
|
<meta name="author" content="Bob Mottram">
|
||||||
<style>
|
<style>
|
||||||
@charset "UTF-8";
|
@charset "UTF-8";
|
||||||
|
|
||||||
|
|
@ -1141,6 +1144,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
<head>
|
||||||
|
<title>Epicyon ActivityPub server</title>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="timeline-banner"></div>
|
<div class="timeline-banner"></div>
|
||||||
<center>
|
<center>
|
||||||
|
|
|
||||||