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

merge-requests/30/head
Bob Mottram 2020-12-03 23:02:10 +00:00
commit a464759806
140 changed files with 3470 additions and 1537 deletions

18
auth.py
View File

@ -11,6 +11,7 @@ import hashlib
import binascii
import os
import secrets
from utils import isSystemAccount
def hashPassword(password: str) -> str:
@ -85,7 +86,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
"""
if ' ' not in authHeader:
if debug:
print('DEBUG: Authorixation header does not ' +
print('DEBUG: basic auth - Authorixation header does not ' +
'contain a space character')
return False
if '/users/' not in path and \
@ -93,23 +94,32 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
'/channel/' not in path and \
'/profile/' not in path:
if debug:
print('DEBUG: Path for Authorization does not contain a user')
print('DEBUG: basic auth - ' +
'path for Authorization does not contain a user')
return False
pathUsersSection = path.split('/users/')[1]
if '/' not in pathUsersSection:
if debug:
print('DEBUG: This is not a users endpoint')
print('DEBUG: basic auth - this is not a users endpoint')
return False
nicknameFromPath = pathUsersSection.split('/')[0]
if isSystemAccount(nicknameFromPath):
print('basic auth - attempted login using system account ' +
nicknameFromPath + ' in path')
return False
base64Str = \
authHeader.split(' ')[1].replace('\n', '').replace('\r', '')
plain = base64.b64decode(base64Str).decode('utf-8')
if ':' not in plain:
if debug:
print('DEBUG: Basic Auth header does not contain a ":" ' +
print('DEBUG: basic auth header does not contain a ":" ' +
'separator for username:password')
return False
nickname = plain.split(':')[0]
if isSystemAccount(nickname):
print('basic auth - attempted login using system account ' + nickname +
' in Auth header')
return False
if nickname != nicknameFromPath:
if debug:
print('DEBUG: Nickname given in the path (' + nicknameFromPath +

View File

@ -131,6 +131,8 @@ def isBlockedHashtag(baseDir: str, hashtag: str) -> bool:
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(globalBlockingFilename):
hashtag = hashtag.strip('\n').strip('\r')
if not hashtag.startswith('#'):
hashtag = '#' + hashtag
if hashtag + '\n' in open(globalBlockingFilename).read():
return True
return False

View File

@ -10,11 +10,11 @@ import os
from datetime import datetime
from content import replaceEmojiFromTags
from webapp import getIconsWebPath
from webapp import htmlHeaderWithExternalStyle
from webapp import htmlFooter
from webapp_media import addEmbeddedElements
from webapp_utils import getIconsWebPath
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from webapp_utils import getPostAttachmentsAsHtml
from webapp_media import addEmbeddedElements
from utils import getMediaFormats
from utils import getNicknameFromActor
from utils import getDomainFromActor

View File

@ -8,6 +8,7 @@ __status__ = "Production"
import os
import email.parser
import urllib.parse
from shutil import copyfile
from utils import getImageExtensions
from utils import loadJson
@ -991,5 +992,5 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool) -> {}:
if line > 2:
postValue += '\n'
postValue += postLines[line]
fields[postKey] = postValue
fields[postKey] = urllib.parse.unquote_plus(postValue)
return fields

529
daemon.py
View File

@ -40,6 +40,8 @@ from ssb import getSSBAddress
from ssb import setSSBAddress
from tox import getToxAddress
from tox import setToxAddress
from jami import getJamiAddress
from jami import setJamiAddress
from matrix import getMatrixAddress
from matrix import setMatrixAddress
from donate import getDonationUrl
@ -63,7 +65,6 @@ from person import canRemovePost
from person import personSnooze
from person import personUnsnooze
from posts import isModerator
from posts import isEditor
from posts import mutePost
from posts import unmutePost
from posts import createQuestionPost
@ -113,15 +114,16 @@ from blog import htmlBlogView
from blog import htmlBlogPage
from blog import htmlBlogPost
from blog import htmlEditBlog
from webapp_utils import htmlHashtagBlocked
from webapp_utils import htmlFollowingList
from webapp_utils import setBlogAddress
from webapp_utils import getBlogAddress
from webapp_calendar import htmlCalendarDeleteConfirm
from webapp_calendar import htmlCalendar
from webapp_about import htmlAbout
from webapp import htmlFollowingList
from webapp import htmlDeletePost
from webapp import htmlRemoveSharedItem
from webapp import htmlUnblockConfirm
from webapp_confirm import htmlConfirmDelete
from webapp_confirm import htmlConfirmRemoveSharedItem
from webapp_confirm import htmlConfirmUnblock
from webapp_person_options import htmlPersonOptions
from webapp_timeline import htmlShares
from webapp_timeline import htmlInbox
@ -132,6 +134,7 @@ from webapp_timeline import htmlInboxReplies
from webapp_timeline import htmlInboxMedia
from webapp_timeline import htmlInboxBlogs
from webapp_timeline import htmlInboxNews
from webapp_timeline import htmlInboxFeatures
from webapp_timeline import htmlOutbox
from webapp_moderation import htmlModeration
from webapp_moderation import htmlModerationInfo
@ -140,9 +143,8 @@ from webapp_login import htmlLogin
from webapp_login import htmlGetLoginCredentials
from webapp_suspended import htmlSuspended
from webapp_tos import htmlTermsOfService
from webapp import htmlFollowConfirm
from webapp import htmlUnfollowConfirm
from webapp import htmlHashtagBlocked
from webapp_confirm import htmlConfirmFollow
from webapp_confirm import htmlConfirmUnfollow
from webapp_post import htmlPostReplies
from webapp_post import htmlIndividualPost
from webapp_profile import htmlEditProfile
@ -162,10 +164,14 @@ from webapp_search import htmlSearchEmoji
from webapp_search import htmlSearchSharedItems
from webapp_search import htmlSearchEmojiTextEntry
from webapp_search import htmlSearch
from webapp_hashtagswarm import getHashtagCategoriesFeed
from webapp_hashtagswarm import htmlSearchHashtagCategory
from shares import getSharesFeedForPerson
from shares import addShare
from shares import removeShare
from shares import expireShares
from utils import setHashtagCategory
from utils import isEditor
from utils import getImageExtensions
from utils import mediaFileMimeType
from utils import getCSS
@ -227,6 +233,7 @@ from newswire import rss2Header
from newswire import rss2Footer
from newsdaemon import runNewswireWatchdog
from newsdaemon import runNewswireDaemon
from filters import isFiltered
import os
@ -1109,6 +1116,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.path.startswith('/icons/') or \
self.path.startswith('/avatars/') or \
self.path.startswith('/favicon.ico') or \
self.path.startswith('/categories.xml') or \
self.path.startswith('/newswire.xml'):
return False
@ -1120,21 +1128,22 @@ class PubServer(BaseHTTPRequestHandler):
tokenStr = tokenStr.split(';')[0].strip()
if self.server.tokensLookup.get(tokenStr):
nickname = self.server.tokensLookup[tokenStr]
self.authorizedNickname = nickname
# default to the inbox of the person
if self.path == '/':
self.path = '/users/' + nickname + '/inbox'
# check that the path contains the same nickname
# as the cookie otherwise it would be possible
# to be authorized to use an account you don't own
if '/' + nickname + '/' in self.path:
return True
elif '/' + nickname + '?' in self.path:
return True
elif self.path.endswith('/' + nickname):
return True
print('AUTH: nickname ' + nickname +
' was not found in path ' + self.path)
if not isSystemAccount(nickname):
self.authorizedNickname = nickname
# default to the inbox of the person
if self.path == '/':
self.path = '/users/' + nickname + '/inbox'
# check that the path contains the same nickname
# as the cookie otherwise it would be possible
# to be authorized to use an account you don't own
if '/' + nickname + '/' in self.path:
return True
elif '/' + nickname + '?' in self.path:
return True
elif self.path.endswith('/' + nickname):
return True
print('AUTH: nickname ' + nickname +
' was not found in path ' + self.path)
return False
print('AUTH: epicyon cookie ' +
'authorization failed, header=' +
@ -1144,13 +1153,13 @@ class PubServer(BaseHTTPRequestHandler):
return False
print('AUTH: Header cookie was not authorized')
return False
# basic auth
# basic auth for c2s
if self.headers.get('Authorization'):
if authorize(self.server.baseDir, self.path,
self.headers['Authorization'],
self.server.debug):
return True
print('AUTH: Basic auth did not authorize ' +
print('AUTH: C2S Basic auth did not authorize ' +
self.headers['Authorization'])
return False
@ -1518,9 +1527,7 @@ class PubServer(BaseHTTPRequestHandler):
moderationText)
if postFilename:
if canRemovePost(baseDir,
nickname,
domain,
port,
nickname, domain, port,
moderationText):
deletePost(baseDir,
httpPrefix,
@ -1528,6 +1535,23 @@ class PubServer(BaseHTTPRequestHandler):
postFilename,
debug,
self.server.recentPostsCache)
if nickname != 'news':
# if this is a local blog post then also remove it
# from the news actor
postFilename = \
locatePost(baseDir, 'news', domain,
moderationText)
if postFilename:
if canRemovePost(baseDir,
'news', domain, port,
moderationText):
deletePost(baseDir,
httpPrefix,
'news', domain,
postFilename,
debug,
self.server.recentPostsCache)
if callingDomain.endswith('.onion') and onionDomain:
actorStr = 'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and i2pDomain):
@ -1773,7 +1797,7 @@ class PubServer(BaseHTTPRequestHandler):
if debug:
print('Unblocking ' + optionsActor)
msg = \
htmlUnblockConfirm(self.server.cssCache,
htmlConfirmUnblock(self.server.cssCache,
self.server.translate,
baseDir,
usersPath,
@ -1791,7 +1815,7 @@ class PubServer(BaseHTTPRequestHandler):
if debug:
print('Following ' + optionsActor)
msg = \
htmlFollowConfirm(self.server.cssCache,
htmlConfirmFollow(self.server.cssCache,
self.server.translate,
baseDir,
usersPath,
@ -1808,7 +1832,7 @@ class PubServer(BaseHTTPRequestHandler):
if '&submitUnfollow=' in optionsConfirmParams:
print('Unfollowing ' + optionsActor)
msg = \
htmlUnfollowConfirm(self.server.cssCache,
htmlConfirmUnfollow(self.server.cssCache,
self.server.translate,
baseDir,
usersPath,
@ -2732,7 +2756,7 @@ class PubServer(BaseHTTPRequestHandler):
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str,
debug: bool) -> None:
"""Endpoint for removing posts
"""Endpoint for removing posts after confirmation
"""
pageNumber = 1
usersPath = path.split('/rmpost')[0]
@ -2796,7 +2820,7 @@ class PubServer(BaseHTTPRequestHandler):
'actor': removePostActor,
'object': removeMessageId,
'to': toList,
'cc': [removePostActor+'/followers'],
'cc': [removePostActor + '/followers'],
'type': 'Delete'
}
self.postToNickname = getNicknameFromActor(removePostActor)
@ -2959,6 +2983,129 @@ class PubServer(BaseHTTPRequestHandler):
cookie, callingDomain)
self.server.POSTbusy = False
def _setHashtagCategory(self, callingDomain: str, cookie: str,
authorized: bool, path: str,
baseDir: str, httpPrefix: str,
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str, debug: bool,
defaultTimeline: str,
allowLocalNetworkAccess: bool) -> None:
"""On the screen after selecting a hashtag from the swarm, this sets
the category for that tag
"""
usersPath = path.replace('/sethashtagcategory', '')
hashtag = ''
if '/tags/' not in usersPath:
# no hashtag is specified within the path
self._404()
return
hashtag = usersPath.split('/tags/')[1].strip()
hashtag = urllib.parse.unquote_plus(hashtag)
if not hashtag:
# no hashtag was given in the path
self._404()
return
hashtagFilename = baseDir + '/tags/' + hashtag + '.txt'
if not os.path.isfile(hashtagFilename):
# the hashtag does not exist
self._404()
return
usersPath = usersPath.split('/tags/')[0]
actorStr = httpPrefix + '://' + domainFull + usersPath
tagScreenStr = actorStr + '/tags/' + hashtag
if ' boundary=' in self.headers['Content-type']:
boundary = self.headers['Content-type'].split('boundary=')[1]
if ';' in boundary:
boundary = boundary.split(';')[0]
# get the nickname
nickname = getNicknameFromActor(actorStr)
editor = None
if nickname:
editor = isEditor(baseDir, nickname)
if not hashtag or not editor:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
if not nickname:
print('WARN: nickname not found in ' + actorStr)
else:
print('WARN: nickname is not a moderator' + actorStr)
self._redirect_headers(tagScreenStr, cookie, callingDomain)
self.server.POSTbusy = False
return
length = int(self.headers['Content-length'])
# check that the POST isn't too large
if length > self.server.maxPostLength:
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
print('Maximum links data length exceeded ' + str(length))
self._redirect_headers(tagScreenStr, cookie, callingDomain)
self.server.POSTbusy = False
return
try:
# read the bytes of the http form POST
postBytes = self.rfile.read(length)
except SocketError as e:
if e.errno == errno.ECONNRESET:
print('WARN: connection was reset while ' +
'reading bytes from http form POST')
else:
print('WARN: error while reading bytes ' +
'from http form POST')
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
except ValueError as e:
print('ERROR: failed to read bytes for POST')
print(e)
self.send_response(400)
self.end_headers()
self.server.POSTbusy = False
return
# extract all of the text fields into a dict
fields = \
extractTextFieldsInPOST(postBytes, boundary, debug)
if fields.get('hashtagCategory'):
categoryStr = fields['hashtagCategory'].lower()
if not isBlockedHashtag(baseDir, categoryStr) and \
not isFiltered(baseDir, nickname, domain, categoryStr):
setHashtagCategory(baseDir, hashtag, categoryStr)
else:
categoryFilename = baseDir + '/tags/' + hashtag + '.category'
if os.path.isfile(categoryFilename):
os.remove(categoryFilename)
# redirect back to the default timeline
if callingDomain.endswith('.onion') and \
onionDomain:
actorStr = \
'http://' + onionDomain + usersPath
elif (callingDomain.endswith('.i2p') and
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
self._redirect_headers(tagScreenStr,
cookie, callingDomain)
self.server.POSTbusy = False
def _newswireUpdate(self, callingDomain: str, cookie: str,
authorized: bool, path: str,
baseDir: str, httpPrefix: str,
@ -3207,7 +3354,7 @@ class PubServer(BaseHTTPRequestHandler):
domain: str, domainFull: str,
onionDomain: str, i2pDomain: str, debug: bool,
defaultTimeline: str) -> None:
"""edits a news post
"""edits a news post after receiving POST
"""
usersPath = path.replace('/newseditdata', '')
usersPath = usersPath.replace('/editnewspost', '')
@ -3235,8 +3382,12 @@ class PubServer(BaseHTTPRequestHandler):
print('WARN: nickname not found in ' + actorStr)
else:
print('WARN: nickname is not an editor' + actorStr)
self._redirect_headers(actorStr + '/tlnews',
cookie, callingDomain)
if self.server.newsInstance:
self._redirect_headers(actorStr + '/tlfeatures',
cookie, callingDomain)
else:
self._redirect_headers(actorStr + '/tlnews',
cookie, callingDomain)
self.server.POSTbusy = False
return
@ -3253,8 +3404,12 @@ class PubServer(BaseHTTPRequestHandler):
actorStr = \
'http://' + i2pDomain + usersPath
print('Maximum news data length exceeded ' + str(length))
self._redirect_headers(actorStr + 'tlnews',
cookie, callingDomain)
if self.server.newsInstance:
self._redirect_headers(actorStr + '/tlfeatures',
cookie, callingDomain)
else:
self._redirect_headers(actorStr + '/tlnews',
cookie, callingDomain)
self.server.POSTbusy = False
return
@ -3342,8 +3497,12 @@ class PubServer(BaseHTTPRequestHandler):
i2pDomain):
actorStr = \
'http://' + i2pDomain + usersPath
self._redirect_headers(actorStr + '/tlnews',
cookie, callingDomain)
if self.server.newsInstance:
self._redirect_headers(actorStr + '/tlfeatures',
cookie, callingDomain)
else:
self._redirect_headers(actorStr + '/tlnews',
cookie, callingDomain)
self.server.POSTbusy = False
def _profileUpdate(self, callingDomain: str, cookie: str,
@ -3616,7 +3775,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.newsInstance = True
self.server.blogsInstance = False
self.server.mediaInstance = False
self.server.defaultTimeline = 'tlnews'
self.server.defaultTimeline = 'tlfeatures'
setConfigParam(baseDir,
"mediaInstance",
self.server.mediaInstance)
@ -3758,6 +3917,18 @@ class PubServer(BaseHTTPRequestHandler):
setToxAddress(actorJson, '')
actorChanged = True
# change jami address
currentJamiAddress = getJamiAddress(actorJson)
if fields.get('jamiAddress'):
if fields['jamiAddress'] != currentJamiAddress:
setJamiAddress(actorJson,
fields['jamiAddress'])
actorChanged = True
else:
if currentJamiAddress:
setJamiAddress(actorJson, '')
actorChanged = True
# change PGP public key
currentPGPpubKey = getPGPpubKey(actorJson)
if fields.get('pgp'):
@ -4643,6 +4814,41 @@ class PubServer(BaseHTTPRequestHandler):
path + ' ' + callingDomain)
self._404()
def _getHashtagCategoriesFeed(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
domain: str, port: int, proxyType: str,
GETstartTime, GETtimings: {},
debug: bool) -> None:
"""Returns the hashtag categories feed
"""
if not self.server.session:
print('Starting new session during RSS categories request')
self.server.session = \
createSession(proxyType)
if not self.server.session:
print('ERROR: GET failed to create session ' +
'during RSS categories request')
self._404()
return
hashtagCategories = None
msg = \
getHashtagCategoriesFeed(baseDir, hashtagCategories)
if msg:
msg = msg.encode('utf-8')
self._set_headers('text/xml', len(msg),
None, callingDomain)
self._write(msg)
if debug:
print('Sent rss2 categories feed: ' +
path + ' ' + callingDomain)
return
if debug:
print('Failed to get rss2 categories feed: ' +
path + ' ' + callingDomain)
self._404()
def _getRSS3feed(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
@ -4718,6 +4924,7 @@ class PubServer(BaseHTTPRequestHandler):
matrixAddress = None
blogAddress = None
toxAddress = None
jamiAddress = None
ssbAddress = None
emailAddress = None
actorJson = getPersonFromCache(baseDir,
@ -4731,6 +4938,7 @@ class PubServer(BaseHTTPRequestHandler):
ssbAddress = getSSBAddress(actorJson)
blogAddress = getBlogAddress(actorJson)
toxAddress = getToxAddress(actorJson)
jamiAddress = getJamiAddress(actorJson)
emailAddress = getEmailAddress(actorJson)
PGPpubKey = getPGPpubKey(actorJson)
PGPfingerprint = getPGPfingerprint(actorJson)
@ -4746,7 +4954,7 @@ class PubServer(BaseHTTPRequestHandler):
pageNumber, donateUrl,
xmppAddress, matrixAddress,
ssbAddress, blogAddress,
toxAddress,
toxAddress, jamiAddress,
PGPpubKey, PGPfingerprint,
emailAddress).encode('utf-8')
self._set_headers('text/html', len(msg),
@ -4758,7 +4966,7 @@ class PubServer(BaseHTTPRequestHandler):
return
if '/users/news/' in path:
self._redirect_headers(originPathStr + '/tlnews',
self._redirect_headers(originPathStr + '/tlfeatures',
cookie, callingDomain)
return
@ -4929,7 +5137,9 @@ class PubServer(BaseHTTPRequestHandler):
hashtag = path.split('/tags/')[1]
if '?page=' in hashtag:
hashtag = hashtag.split('?page=')[0]
hashtag = urllib.parse.unquote_plus(hashtag)
if isBlockedHashtag(baseDir, hashtag):
print('BLOCK: hashtag #' + hashtag)
msg = htmlHashtagBlocked(self.server.cssCache, baseDir,
self.server.translate).encode('utf-8')
self._login_headers('text/html', len(msg), callingDomain)
@ -5791,7 +6001,7 @@ class PubServer(BaseHTTPRequestHandler):
GETstartTime, GETtimings: {},
proxyType: str, cookie: str,
debug: str):
"""Delete button is pressed
"""Delete button is pressed on a post
"""
if not cookie:
print('ERROR: no cookie given when deleting')
@ -5855,16 +6065,16 @@ class PubServer(BaseHTTPRequestHandler):
return
deleteStr = \
htmlDeletePost(self.server.cssCache,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate, pageNumber,
self.server.session, baseDir,
deleteUrl, httpPrefix,
__version__, self.server.cachedWebfingers,
self.server.personCache, callingDomain,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly)
htmlConfirmDelete(self.server.cssCache,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate, pageNumber,
self.server.session, baseDir,
deleteUrl, httpPrefix,
__version__, self.server.cachedWebfingers,
self.server.personCache, callingDomain,
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly)
if deleteStr:
self._set_headers('text/html', len(deleteStr),
cookie, callingDomain)
@ -7299,6 +7509,127 @@ class PubServer(BaseHTTPRequestHandler):
return True
return False
def _showFeaturesTimeline(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
domain: str, domainFull: str, port: int,
onionDomain: str, i2pDomain: str,
GETstartTime, GETtimings: {},
proxyType: str, cookie: str,
debug: str) -> bool:
"""Shows the features timeline (all local blogs)
"""
if '/users/' in path:
if authorized:
inboxFeaturesFeed = \
personBoxJson(self.server.recentPostsCache,
self.server.session,
baseDir,
domain,
port,
path,
httpPrefix,
maxPostsInNewsFeed, 'tlfeatures',
True,
self.server.newswireVotesThreshold,
self.server.positiveVoting,
self.server.votingTimeMins)
if not inboxFeaturesFeed:
inboxFeaturesFeed = []
if self._requestHTTP():
nickname = path.replace('/users/', '')
nickname = nickname.replace('/tlfeatures', '')
pageNumber = 1
if '?page=' in nickname:
pageNumber = nickname.split('?page=')[1]
nickname = nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber = int(pageNumber)
else:
pageNumber = 1
if 'page=' not in path:
# if no page was specified then show the first
inboxFeaturesFeed = \
personBoxJson(self.server.recentPostsCache,
self.server.session,
baseDir,
domain,
port,
path + '?page=1',
httpPrefix,
maxPostsInBlogsFeed, 'tlfeatures',
True,
self.server.newswireVotesThreshold,
self.server.positiveVoting,
self.server.votingTimeMins)
currNickname = path.split('/users/')[1]
if '/' in currNickname:
currNickname = currNickname.split('/')[0]
fullWidthTimelineButtonHeader = \
self.server.fullWidthTimelineButtonHeader
msg = \
htmlInboxFeatures(self.server.cssCache,
self.server.defaultTimeline,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate,
pageNumber, maxPostsInBlogsFeed,
self.server.session,
baseDir,
self.server.cachedWebfingers,
self.server.personCache,
nickname,
domain,
port,
inboxFeaturesFeed,
self.server.allowDeletion,
httpPrefix,
self.server.projectVersion,
self._isMinimal(nickname),
self.server.YTReplacementDomain,
self.server.showPublishedDateOnly,
self.server.newswire,
self.server.positiveVoting,
self.server.showPublishAsIcon,
fullWidthTimelineButtonHeader,
self.server.iconsAsButtons,
self.server.rssIconAtTop,
self.server.publishButtonAtTop,
authorized)
msg = msg.encode('utf-8')
self._set_headers('text/html', len(msg),
cookie, callingDomain)
self._write(msg)
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show blogs 2 done',
'show news 2')
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg = json.dumps(inboxFeaturesFeed,
ensure_ascii=False)
msg = msg.encode('utf-8')
self._set_headers('application/json',
len(msg),
None, callingDomain)
self._write(msg)
self.server.GETbusy = False
return True
else:
if debug:
nickname = 'news'
print('DEBUG: ' + nickname +
' was not authorized to access ' + path)
if path != '/tlfeatures':
# not the features inbox
if debug:
print('DEBUG: GET access to features is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy = False
return True
return False
def _showSharesTimeline(self, authorized: bool,
callingDomain: str, path: str,
baseDir: str, httpPrefix: str,
@ -8730,11 +9061,16 @@ class PubServer(BaseHTTPRequestHandler):
"""Show the edit screen for a news post
"""
if '/users/' in path and '/editnewspost=' in path:
postActor = 'news'
if '?actor=' in path:
postActor = path.split('?actor=')[1]
if '?' in postActor:
postActor = postActor.split('?')[0]
postId = path.split('/editnewspost=')[1]
if '?' in postId:
postId = postId.split('?')[0]
postUrl = httpPrefix + '://' + domainFull + \
'/users/news/statuses/' + postId
'/users/' + postActor + '/statuses/' + postId
path = path.split('/editnewspost=')[0]
msg = htmlEditNewsPost(self.server.cssCache,
translate, baseDir,
@ -8979,6 +9315,18 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings,
'fonts', 'sharedInbox enabled')
if self.path == '/categories.xml':
self._getHashtagCategoriesFeed(authorized,
callingDomain, self.path,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.port,
self.server.proxyType,
GETstartTime, GETtimings,
self.server.debug)
return
if self.path == '/newswire.xml':
self._getNewswireFeed(authorized,
callingDomain, self.path,
@ -9178,11 +9526,11 @@ class PubServer(BaseHTTPRequestHandler):
actor = \
self.server.httpPrefix + '://' + \
self.server.domainFull + usersPath
msg = htmlRemoveSharedItem(self.server.cssCache,
self.server.translate,
self.server.baseDir,
actor, shareName,
callingDomain).encode('utf-8')
msg = htmlConfirmRemoveSharedItem(self.server.cssCache,
self.server.translate,
self.server.baseDir,
actor, shareName,
callingDomain).encode('utf-8')
if not msg:
if callingDomain.endswith('.onion') and \
self.server.onionDomain:
@ -9772,7 +10120,7 @@ class PubServer(BaseHTTPRequestHandler):
elif self.server.mediaInstance:
self.path = '/users/' + nickname + '/tlmedia'
else:
self.path = '/users/' + nickname + '/tlnews'
self.path = '/users/' + nickname + '/tlfeatures'
# search for a fediverse address, shared item or emoji
# from the web interface by selecting search icon
@ -9795,6 +10143,22 @@ class PubServer(BaseHTTPRequestHandler):
'search screen shown')
return
# show a hashtag category from the search screen
if htmlGET and '/category/' in self.path:
msg = htmlSearchHashtagCategory(self.server.cssCache,
self.server.translate,
self.server.baseDir, self.path,
self.server.domain)
if msg:
msg = msg.encode('utf-8')
self._set_headers('text/html', len(msg), cookie, callingDomain)
self._write(msg)
self.server.GETbusy = False
self._benchmarkGETtimings(GETstartTime, GETtimings,
'hashtag category done',
'hashtag category screen shown')
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'hashtag search done',
'search screen shown done')
@ -10527,6 +10891,23 @@ class PubServer(BaseHTTPRequestHandler):
cookie, self.server.debug):
return
# get features (local blogs) for a given person
if self.path.endswith('/tlfeatures') or \
'/tlfeatures?page=' in self.path:
if self._showFeaturesTimeline(authorized,
callingDomain, self.path,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.domainFull,
self.server.port,
self.server.onionDomain,
self.server.i2pDomain,
GETstartTime, GETtimings,
self.server.proxyType,
cookie, self.server.debug):
return
self._benchmarkGETtimings(GETstartTime, GETtimings,
'show blogs 2 done',
'show news 2 done')
@ -11111,6 +11492,13 @@ class PubServer(BaseHTTPRequestHandler):
replaceYouTube(postJsonObject,
self.server.YTReplacementDomain)
saveJson(postJsonObject, postFilename)
# also save to the news actor
if nickname != 'news':
postFilename = \
postFilename.replace('#users#' +
nickname + '#',
'#users#news#')
saveJson(postJsonObject, postFilename)
print('Edited blog post, resaved ' + postFilename)
return 1
else:
@ -11759,6 +12147,20 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2)
if authorized and self.path.endswith('/sethashtagcategory'):
self._setHashtagCategory(callingDomain, cookie,
authorized, self.path,
self.server.baseDir,
self.server.httpPrefix,
self.server.domain,
self.server.domainFull,
self.server.onionDomain,
self.server.i2pDomain,
self.server.debug,
self.server.defaultTimeline,
self.server.allowLocalNetworkAccess)
return
# update of profile/avatar from web interface,
# after selecting Edit button then Submit
if authorized and self.path.endswith('/profiledata'):
@ -12486,7 +12888,7 @@ def runDaemon(maxNewswirePosts: int,
if blogsInstance:
httpd.defaultTimeline = 'tlblogs'
if newsInstance:
httpd.defaultTimeline = 'tlnews'
httpd.defaultTimeline = 'tlfeatures'
# load translations dictionary
httpd.translate = {}
@ -12579,6 +12981,9 @@ def runDaemon(maxNewswirePosts: int,
# maximum size of individual RSS feed items, in K
httpd.maxFeedItemSizeKb = maxFeedItemSizeKb
# maximum size of a hashtag category, in K
httpd.maxCategoriesFeedItemSizeKb = 256
if registration == 'open':
httpd.registration = True
else:

View File

@ -3,6 +3,7 @@
:root {
--main-bg-color: #282c37;
--link-bg-color: #282c37;
--title-color: #999;
--dropdown-bg-color: #111;
--dropdown-bg-color-hover: #333;
--main-bg-color-reply: #212c37;
@ -16,6 +17,7 @@
--font-size-header: 18px;
--font-color-header: #ccc;
--font-size-button-mobile: 34px;
--font-size-mobile: 50px;
--font-size: 30px;
--font-size2: 24px;
--font-size3: 38px;
@ -45,6 +47,7 @@
--focus-color: white;
--line-spacing: 130%;
--header-font: 'Bedstead';
--main-link-color-hover: #bbb;
}
@font-face {
@ -78,27 +81,33 @@ body, html {
a, u {
color: var(--main-fg-color);
}
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {
border: 2px solid var(--focus-color);
}
h1 {
font-family: var(--header-font);
}
.cwText {
display: none;
}
@ -689,6 +698,11 @@ div.gallery img {
font-size: var(--font-size4);
font-family: "Times New Roman", Roman, serif;
}
h1 {
font-family: var(--header-font);
font-size: var(--font-size);
color: var(--title-color);
}
.galleryContainer {
display: grid;
grid-template-columns: 50% 50%;
@ -1041,6 +1055,11 @@ div.gallery img {
font-size: var(--font-size3);
font-family: "Times New Roman", Roman, serif;
}
h1 {
font-family: var(--header-font);
font-size: var(--font-size-mobile);
color: var(--title-color);
}
div.gallerytext {
color: var(--gallery-text-color);
font-size: var(--gallery-font-size-mobile);

View File

@ -23,6 +23,7 @@
--font-size-calendar-cell-mobile: 4rem;
--calendar-header-font: 'Montserrat';
--calendar-header-font-style: italic;
--main-link-color-hover: #bbb;
}
@font-face {
@ -59,6 +60,7 @@ a:visited{
z-index: 1;
padding: 1rem;
margin: -1rem;
font-weight: normal;
}
a:link {
@ -67,6 +69,15 @@ a:link {
z-index: 1;
padding: 1rem;
margin: -1rem;
font-weight: normal;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {

View File

@ -31,6 +31,7 @@
--follow-text-size2: 40px;
--follow-text-entry-width: 90%;
--focus-color: white;
--main-link-color-hover: #bbb;
}
@font-face {
@ -66,13 +67,23 @@ a, u {
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {

View File

@ -154,13 +154,15 @@ a, u {
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
@ -213,6 +215,7 @@ a:focus {
h1 {
font-family: var(--header-font);
font-size: var(--font-size);
color: var(--title-color);
}

View File

@ -1,9 +1,9 @@
@charset "UTF-8";
:root {
--main-bg-color: #282c37;
--login-bg-color: #282c37;
--link-bg-color: #282c37;
--main-fg-color: #dddddd;
--login-fg-color: #dddddd;
--main-link-color: #999;
--main-visited-color: #888;
--border-color: #505050;
@ -22,6 +22,7 @@
--focus-color: white;
--line-spacing: 130%;
--login-logo-width: 20%;
--main-link-color-hover: #bbb;
}
@font-face {
@ -40,8 +41,8 @@
}
body, html {
background-color: var(--main-bg-color);
color: var(--main-fg-color);
background-color: var(--login-bg-color);
color: var(--login-fg-color);
background-image: url("/login-background.jpg");
background-size: cover;
@ -59,19 +60,29 @@ body, html {
}
a, u {
color: var(--main-fg-color);
color: var(--login-fg-color);
}
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {
@ -146,8 +157,8 @@ span.psw {
@media screen and (min-width: 400px) {
body, html {
background-color: var(--main-bg-color);
color: var(--main-fg-color);
background-color: var(--login-bg-color);
color: var(--login-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
max-width: 60%;
@ -186,8 +197,8 @@ span.psw {
@media screen and (max-width: 1000px) {
body, html {
background-color: var(--main-bg-color);
color: var(--main-fg-color);
background-color: var(--login-bg-color);
color: var(--login-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
max-width: 95%;

View File

@ -1,9 +1,9 @@
@charset "UTF-8";
:root {
--main-bg-color: #282c37;
--link-bg-color: #282c37;
--main-fg-color: #dddddd;
--options-bg-color: #282c37;
--options-link-bg-color: transparent;
--options-fg-color: #dddddd;
--main-link-color: #999;
--main-visited-color: #888;
--border-color: #505050;
@ -34,6 +34,7 @@
--follow-text-entry-width: 90%;
--focus-color: white;
--petname-width-chars: 16ch;
--main-link-color-hover: #bbb;
}
@font-face {
@ -58,8 +59,8 @@ body, html {
-moz-background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-color: var(--main-bg-color);
color: var(--main-fg-color);
background-color: var(--options-bg-color);
color: var(--options-fg-color);
height: 100%;
font-family: Arial, Helvetica, sans-serif;
@ -68,19 +69,29 @@ body, html {
}
a, u {
color: var(--main-fg-color);
color: var(--options-fg-color);
}
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
background: var(--options-link-bg-color);
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
background: var(--options-link-bg-color);
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {
@ -90,7 +101,7 @@ a:focus {
.follow {
height: 100%;
position: relative;
background-color: var(--main-bg-color);
background-color: var(--options-bg-color);
}
.followAvatar {
@ -111,7 +122,7 @@ a:focus {
.pgp {
font-size: var(--font-size5);
color: var(--main-link-color);
background: var(--link-bg-color);
background: var(--options-link-bg-color);
}
.button:hover {
@ -123,6 +134,7 @@ a:focus {
}
.options img {
background-color: var(--options-bg-color);
width: 15%;
}
@ -132,7 +144,7 @@ a:focus {
font-size: var(--font-size4);
width: 90%;
background-color: var(--text-entry-background);
color: white;
color: var(--text-entry-foreground);
}
.followText {
font-size: var(--follow-text-size1);
@ -209,7 +221,7 @@ a:focus {
font-size: var(--font-size);
width: 90%;
background-color: var(--text-entry-background);
color: white;
color: var(--text-entry-foreground);
}
.followText {
font-size: var(--follow-text-size2);

View File

@ -1,6 +1,8 @@
@charset "UTF-8";
:root {
--timeline-icon-width: 50px;
--timeline-icon-width-mobile: 100px;
--header-bg-color: #282c37;
--main-bg-color: #282c37;
--post-bg-color: #282c37;
@ -31,8 +33,9 @@
--font-size-links: 18px;
--font-size-publish-button: 18px;
--font-size-newswire: 18px;
--font-size-newswire-mobile: 40px;
--font-size-newswire-mobile: 38px;
--font-size-dropdown-header: 40px;
--font-size-mobile: 50px;
--font-size: 30px;
--font-size2: 24px;
--font-size3: 38px;
@ -183,7 +186,7 @@ body, html {
}
.postSeparatorImage img {
background-color: var(--post-bg-color);
background-color: transparent;
padding-top: var(--post-separator-margin-top);
padding-bottom: var(--post-separator-margin-bottom);
width: var(--post-separator-width);
@ -192,7 +195,7 @@ body, html {
}
.postSeparatorImageLeft img {
background-color: var(--column-left-color);
background-color: transparent;
padding-top: var(--post-separator-margin-top);
padding-bottom: var(--post-separator-margin-bottom);
width: var(--separator-width-left);
@ -201,7 +204,7 @@ body, html {
}
.postSeparatorImageRight img {
background-color: var(--column-left-color);
background-color: transparent;
padding-top: var(--post-separator-margin-top);
padding-bottom: var(--post-separator-margin-bottom);
width: var(--separator-width-right);
@ -254,25 +257,22 @@ blockquote p {
border: 2px solid var(--focus-color);
}
h1 {
font-family: var(--header-font);
color: var(--title-color);
}
a, u {
color: var(--main-fg-color);
}
a:visited{
a:visited {
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
@ -284,7 +284,7 @@ a:visited:hover {
}
.buttonevent:hover {
filter: brightness(var(----icon-brightness-change));
filter: brightness(var(--icon-brightness-change));
}
a:focus {
@ -317,8 +317,6 @@ a:focus {
.profileHeader * {
-webkit-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: all 0.25s ease;
transition: all 0.25s ease;
}
.profileHeader img.profileBackground {
@ -437,17 +435,7 @@ a:focus {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
.button span:after {
font-family: var(--header-font);
content: '\00bb';
position: absolute;
opacity: 0;
top: 0;
right: -20px;
transition: 0.5s;
transition: 1.0s;
}
.button:hover {
@ -465,17 +453,7 @@ a:focus {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
.buttonselected span:after {
font-family: var(--header-font);
content: '\00bb';
position: absolute;
opacity: 0;
top: 0;
right: -20px;
transition: 0.5s;
transition: 1.0s;
}
.buttonselected:hover {
@ -631,7 +609,7 @@ a:focus {
}
.containericons img:hover {
filter: brightness(var(----icon-brightness-change));
filter: brightness(var(--icon-brightness-change));
}
.post-title {
@ -940,14 +918,6 @@ div.gallery img {
li { list-style:none;}
/***********BUTTON CODE ******************************************************/
a, button, input:focus, input[type='button'], input[type='reset'], input[type='submit'], textarea:focus, .button {
-webkit-transition: all 0.1s ease-in-out;
-moz-transition: all 0.1s ease-in-out;
-ms-transition: all 0.1s ease-in-out;
-o-transition: all 0.1s ease-in-out;
transition: all 0.1s ease-in-out;
text-decoration: none;
}
.btn {
margin: -3px 0 0 0;
}
@ -971,6 +941,11 @@ div.container {
font-size: var(--font-size);
line-height: var(--line-spacing);
}
h1 {
font-family: var(--header-font);
font-size: var(--font-size);
color: var(--title-color);
}
.containerHeader {
border: var(--border-width-header) solid var(--border-color);
background-color: var(--header-bg-color);
@ -1067,12 +1042,14 @@ div.container {
float: left;
width: var(--column-left-width);
}
.col-left img.leftColEditImage:hover {
filter: brightness(var(--icon-brightness-change));
}
.col-left img.leftColEdit {
background: var(--column-left-color);
width: var(--column-left-icon-size);
}
.col-left img.leftColEditImage {
background: var(--column-left-color);
width: var(--column-left-icon-size);
float: right;
}
@ -1112,12 +1089,15 @@ div.container {
width: var(--column-right-width);
overflow: hidden;
}
.col-right img.rightColEditImage:hover {
filter: brightness(var(--icon-brightness-change));
}
.col-right img.rightColEdit {
background: var(--column-left-color);
background: transparent;
width: var(--column-right-icon-size);
}
.col-right img.rightColEditImage {
background: var(--column-left-color);
background: transparent;
width: var(--column-right-icon-size);
float: right;
}
@ -1212,7 +1192,7 @@ div.container {
margin-right: 0px;
padding: 0px 0px;
margin: 0px 0px;
width: 50px;
width: var(--timeline-icon-width);
}
.containerHeader img.timelineicon {
float: var(--icons-side);
@ -1220,7 +1200,7 @@ div.container {
margin-right:0;
padding: 0 0;
margin: 0 0;
width: 50px;
width: var(--timeline-icon-width);
}
.container img.emojiheader {
float: none;
@ -1612,6 +1592,11 @@ div.container {
font-size: var(--font-size);
line-height: var(--line-spacing);
}
h1 {
font-family: var(--header-font);
font-size: var(--font-size-mobile);
color: var(--title-color);
}
.containerHeader {
border: var(--border-width-header) solid var(--border-color);
background-color: var(--header-bg-color);
@ -1832,7 +1817,7 @@ div.container {
margin-right: 0px;
padding: 0px 0px;
margin: 0px 0px;
width: 100px;
width: var(--timeline-icon-width-mobile);
}
.containerHeader img.timelineicon {
float: var(--icons-side);
@ -1840,7 +1825,7 @@ div.container {
margin-right:0;
padding: 0 0;
margin: 0 0;
width: 100px;
width: var(--timeline-icon-width-mobile);
}
.container img.emojiheader {
float: none;

View File

@ -66,6 +66,7 @@ body, html {
h1 {
font-family: var(--header-font);
font-size: var(--font-size);
color: var(--title-color);
}
@ -76,13 +77,15 @@ a, u {
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
@ -206,6 +209,9 @@ input[type=text] {
}
@media screen and (min-width: 400px) {
details {
font-size: var(--hashtag-size1);
}
.domainHistogram {
border: 0;
font-size: var(--hashtag-size1);
@ -262,6 +268,9 @@ input[type=text] {
}
@media screen and (max-width: 1000px) {
details {
font-size: var(--hashtag-size2);
}
.domainHistogram {
border: 0;
font-size: var(--hashtag-size2);

View File

@ -20,6 +20,7 @@
--button-background: #999;
--button-selected: #666;
--focus-color: white;
--main-link-color-hover: #bbb;
}
@font-face {
@ -56,13 +57,23 @@ a, u {
a:visited{
color: var(--main-visited-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link {
color: var(--main-link-color);
background: var(--link-bg-color);
font-weight: bold;
font-weight: normal;
text-decoration: none;
}
a:link:hover {
color: var(--main-link-color-hover);
}
a:visited:hover {
color: var(--main-link-color-hover);
}
a:focus {

View File

@ -689,12 +689,6 @@ if args.json:
pprint(testJson)
sys.exit()
if args.rss:
session = createSession(None)
testRSS = getRSS(session, args.rss)
pprint(testRSS)
sys.exit()
# create cache for actors
if not os.path.isdir(baseDir + '/cache'):
os.mkdir(baseDir + '/cache')
@ -756,6 +750,13 @@ if args.domain:
domain = args.domain
setConfigParam(baseDir, 'domain', domain)
if args.rss:
session = createSession(None)
testRSS = getRSS(baseDir, domain, session, args.rss,
False, False, 1000, 1000, 1000, 1000)
pprint(testRSS)
sys.exit()
if args.onion:
if not args.onion.endswith('.onion'):
print(args.onion + ' does not look like an onion domain')

View File

@ -56,8 +56,8 @@ from posts import isMuted
from posts import isImageMedia
from posts import sendSignedJson
from posts import sendToFollowersThread
from webapp import individualPostAsHtml
from webapp import getIconsWebPath
from webapp_utils import getIconsWebPath
from webapp_post import individualPostAsHtml
from question import questionUpdateVotes
from media import replaceYouTube
from git import isGitPatch
@ -1251,18 +1251,31 @@ def receiveDelete(session, handle: str, isGroup: bool, baseDir: str,
# if this post in the outbox of the person?
messageId = removeIdEnding(messageJson['object'])
removeModerationPostFromIndex(baseDir, messageId, debug)
postFilename = locatePost(baseDir, handle.split('@')[0],
handle.split('@')[1], messageId)
handleNickname = handle.split('@')[0]
handleDomain = handle.split('@')[1]
postFilename = locatePost(baseDir, handleNickname,
handleDomain, messageId)
if not postFilename:
if debug:
print('DEBUG: delete post not found in inbox or outbox')
print(messageId)
return True
deletePost(baseDir, httpPrefix, handle.split('@')[0],
handle.split('@')[1], postFilename, debug,
deletePost(baseDir, httpPrefix, handleNickname,
handleDomain, postFilename, debug,
recentPostsCache)
if debug:
print('DEBUG: post deleted - ' + postFilename)
# also delete any local blogs saved to the news actor
if handleNickname != 'news' and handleDomain == domainFull:
postFilename = locatePost(baseDir, 'news',
handleDomain, messageId)
if postFilename:
deletePost(baseDir, httpPrefix, 'news',
handleDomain, postFilename, debug,
recentPostsCache)
if debug:
print('DEBUG: blog post deleted - ' + postFilename)
return True

93
jami.py 100644
View File

@ -0,0 +1,93 @@
__filename__ = "jami.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
def getJamiAddress(actorJson: {}) -> str:
"""Returns jami address for the given actor
"""
if not actorJson.get('attachment'):
return ''
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue['name'].lower().startswith('jami'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue.get('value'):
continue
if propertyValue['type'] != 'PropertyValue':
continue
propertyValue['value'] = propertyValue['value'].strip()
if len(propertyValue['value']) < 2:
continue
if '"' in propertyValue['value']:
continue
if ' ' in propertyValue['value']:
continue
if ',' in propertyValue['value']:
continue
if '.' in propertyValue['value']:
continue
return propertyValue['value']
return ''
def setJamiAddress(actorJson: {}, jamiAddress: str) -> None:
"""Sets an jami address for the given actor
"""
notJamiAddress = False
if len(jamiAddress) < 2:
notJamiAddress = True
if '"' in jamiAddress:
notJamiAddress = True
if ' ' in jamiAddress:
notJamiAddress = True
if '.' in jamiAddress:
notJamiAddress = True
if ',' in jamiAddress:
notJamiAddress = True
if not actorJson.get('attachment'):
actorJson['attachment'] = []
# remove any existing value
propertyFound = None
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue['name'].lower().startswith('jami'):
continue
propertyFound = propertyValue
break
if propertyFound:
actorJson['attachment'].remove(propertyFound)
if notJamiAddress:
return
for propertyValue in actorJson['attachment']:
if not propertyValue.get('name'):
continue
if not propertyValue.get('type'):
continue
if not propertyValue['name'].lower().startswith('jami'):
continue
if propertyValue['type'] != 'PropertyValue':
continue
propertyValue['value'] = jamiAddress
return
newJamiAddress = {
"name": "Jami",
"type": "PropertyValue",
"value": jamiAddress
}
actorJson['attachment'].append(newJamiAddress)

View File

@ -717,7 +717,8 @@ def runNewswireDaemon(baseDir: str, httpd,
httpd.maxNewswireFeedSizeKb,
httpd.maxTags,
httpd.maxFeedItemSizeKb,
httpd.maxNewswirePosts)
httpd.maxNewswirePosts,
httpd.maxCategoriesFeedItemSizeKb)
if not httpd.newswire:
if os.path.isfile(newswireStateFilename):

View File

@ -14,6 +14,7 @@ from datetime import datetime
from datetime import timedelta
from datetime import timezone
from collections import OrderedDict
from utils import setHashtagCategory
from utils import firstParagraphFromString
from utils import isPublicPost
from utils import locatePost
@ -122,7 +123,7 @@ def addNewswireDictEntry(baseDir: str, domain: str,
# check that no tags are blocked
for tag in postTags:
if isBlockedHashtag(baseDir, tag.replace('#', '')):
if isBlockedHashtag(baseDir, tag):
return
newswire[dateStr] = [
@ -202,19 +203,64 @@ def parseFeedDate(pubDate: str) -> str:
return pubDateStr
def xml2StrToHashtagCategories(baseDir: str, domain: str, xmlStr: str,
maxCategoriesFeedItemSizeKb: int) -> None:
"""Updates hashtag categories based upon an rss feed
"""
rssItems = xmlStr.split('<item>')
maxBytes = maxCategoriesFeedItemSizeKb * 1024
for rssItem in rssItems:
if not rssItem:
continue
if len(rssItem) > maxBytes:
print('WARN: rss categories feed item is too big')
continue
if '<title>' not in rssItem:
continue
if '</title>' not in rssItem:
continue
if '<description>' not in rssItem:
continue
if '</description>' not in rssItem:
continue
categoryStr = rssItem.split('<title>')[1]
categoryStr = categoryStr.split('</title>')[0].strip()
if not categoryStr:
continue
if 'CDATA' in categoryStr:
continue
hashtagListStr = rssItem.split('<description>')[1]
hashtagListStr = hashtagListStr.split('</description>')[0].strip()
if not hashtagListStr:
continue
if 'CDATA' in hashtagListStr:
continue
hashtagList = hashtagListStr.split(' ')
if not isBlockedHashtag(baseDir, categoryStr):
for hashtag in hashtagList:
setHashtagCategory(baseDir, hashtag, categoryStr)
def xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
moderated: bool, mirrored: bool,
maxPostsPerSource: int,
maxFeedItemSizeKb: int) -> {}:
maxFeedItemSizeKb: int,
maxCategoriesFeedItemSizeKb: int) -> {}:
"""Converts an xml 2.0 string to a dictionary
"""
if '<item>' not in xmlStr:
return {}
result = {}
if '<title>#categories</title>' in xmlStr:
xml2StrToHashtagCategories(baseDir, domain, xmlStr,
maxCategoriesFeedItemSizeKb)
return {}
rssItems = xmlStr.split('<item>')
postCtr = 0
maxBytes = maxFeedItemSizeKb * 1024
for rssItem in rssItems:
if not rssItem:
continue
if len(rssItem) > maxBytes:
print('WARN: rss feed item is too big')
continue
@ -266,6 +312,8 @@ def xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
postCtr += 1
if postCtr >= maxPostsPerSource:
break
if postCtr > 0:
print('Added ' + str(postCtr) + ' rss feed items to newswire')
return result
@ -282,6 +330,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
postCtr = 0
maxBytes = maxFeedItemSizeKb * 1024
for atomItem in atomItems:
if not atomItem:
continue
if len(atomItem) > maxBytes:
print('WARN: atom feed item is too big')
continue
@ -333,6 +383,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
postCtr += 1
if postCtr >= maxPostsPerSource:
break
if postCtr > 0:
print('Added ' + str(postCtr) + ' atom feed items to newswire')
return result
@ -351,7 +403,10 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
postCtr = 0
maxBytes = maxFeedItemSizeKb * 1024
for atomItem in atomItems:
print('YouTube feed item: ' + atomItem)
if not atomItem:
continue
if not atomItem.strip():
continue
if len(atomItem) > maxBytes:
print('WARN: atom feed item is too big')
continue
@ -359,9 +414,9 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
continue
if '</title>' not in atomItem:
continue
if '<updated>' not in atomItem:
if '<published>' not in atomItem:
continue
if '</updated>' not in atomItem:
if '</published>' not in atomItem:
continue
if '<yt:videoId>' not in atomItem:
continue
@ -382,8 +437,8 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
link = atomItem.split('<yt:videoId>')[1]
link = link.split('</yt:videoId>')[0]
link = 'https://www.youtube.com/watch?v=' + link.strip()
pubDate = atomItem.split('<updated>')[1]
pubDate = pubDate.split('</updated>')[0]
pubDate = atomItem.split('<published>')[1]
pubDate = pubDate.split('</published>')[0]
pubDateStr = parseFeedDate(pubDate)
if pubDateStr:
@ -397,13 +452,16 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
postCtr += 1
if postCtr >= maxPostsPerSource:
break
if postCtr > 0:
print('Added ' + str(postCtr) + ' YouTube feed items to newswire')
return result
def xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
moderated: bool, mirrored: bool,
maxPostsPerSource: int,
maxFeedItemSizeKb: int) -> {}:
maxFeedItemSizeKb: int,
maxCategoriesFeedItemSizeKb: int) -> {}:
"""Converts an xml string to a dictionary
"""
if '<yt:videoId>' in xmlStr and '<yt:channelId>' in xmlStr:
@ -414,7 +472,8 @@ def xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
elif 'rss version="2.0"' in xmlStr:
return xml2StrToDict(baseDir, domain,
xmlStr, moderated, mirrored,
maxPostsPerSource, maxFeedItemSizeKb)
maxPostsPerSource, maxFeedItemSizeKb,
maxCategoriesFeedItemSizeKb)
elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr:
return atomFeedToDict(baseDir, domain,
xmlStr, moderated, mirrored,
@ -437,7 +496,8 @@ def YTchannelToAtomFeed(url: str) -> str:
def getRSS(baseDir: str, domain: str, session, url: str,
moderated: bool, mirrored: bool,
maxPostsPerSource: int, maxFeedSizeKb: int,
maxFeedItemSizeKb: int) -> {}:
maxFeedItemSizeKb: int,
maxCategoriesFeedItemSizeKb: int) -> {}:
"""Returns an RSS url as a dict
"""
if not isinstance(url, str):
@ -467,7 +527,8 @@ def getRSS(baseDir: str, domain: str, session, url: str,
return xmlStrToDict(baseDir, domain, result.text,
moderated, mirrored,
maxPostsPerSource,
maxFeedItemSizeKb)
maxFeedItemSizeKb,
maxCategoriesFeedItemSizeKb)
else:
print('WARN: feed is too large, ' +
'or contains invalid characters: ' + url)
@ -701,7 +762,8 @@ def addBlogsToNewswire(baseDir: str, domain: str, newswire: {},
def getDictFromNewswire(session, baseDir: str, domain: str,
maxPostsPerSource: int, maxFeedSizeKb: int,
maxTags: int, maxFeedItemSizeKb: int,
maxNewswirePosts: int) -> {}:
maxNewswirePosts: int,
maxCategoriesFeedItemSizeKb: int) -> {}:
"""Gets rss feeds as a dictionary from newswire file
"""
subscriptionsFilename = baseDir + '/accounts/newswire.txt'
@ -741,7 +803,8 @@ def getDictFromNewswire(session, baseDir: str, domain: str,
itemsList = getRSS(baseDir, domain, session, url,
moderated, mirrored,
maxPostsPerSource, maxFeedSizeKb,
maxFeedItemSizeKb)
maxFeedItemSizeKb,
maxCategoriesFeedItemSizeKb)
if itemsList:
for dateStr, item in itemsList.items():
result[dateStr] = item

View File

@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
__status__ = "Production"
import os
from shutil import copyfile
from session import createSession
from auth import createPassword
from posts import outboxMessageCreateWrap
@ -116,24 +117,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
fileExtension = 'png'
mediaTypeStr = \
attach['mediaType']
if mediaTypeStr.endswith('jpeg'):
fileExtension = 'jpg'
elif mediaTypeStr.endswith('gif'):
fileExtension = 'gif'
elif mediaTypeStr.endswith('webp'):
fileExtension = 'webp'
elif mediaTypeStr.endswith('avif'):
fileExtension = 'avif'
elif mediaTypeStr.endswith('audio/mpeg'):
fileExtension = 'mp3'
elif mediaTypeStr.endswith('ogg'):
fileExtension = 'ogg'
elif mediaTypeStr.endswith('mp4'):
fileExtension = 'mp4'
elif mediaTypeStr.endswith('webm'):
fileExtension = 'webm'
elif mediaTypeStr.endswith('ogv'):
fileExtension = 'ogv'
extensions = {
"jpeg": "jpg",
"gif": "gif",
"webp": "webp",
"avif": "avif",
"audio/mpeg": "mp3",
"ogg": "ogg",
"mp4": "mp4",
"webm": "webm",
"ogv": "ogv"
}
for matchExt, ext in extensions.items():
if mediaTypeStr.endswith(matchExt):
fileExtension = ext
break
mediaDir = \
baseDir + '/accounts/' + \
postToNickname + '@' + domain
@ -188,11 +188,31 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
savePostToBox(baseDir,
httpPrefix,
postId,
postToNickname,
domainFull, messageJson, outboxName)
postToNickname, domainFull,
messageJson, outboxName)
if not savedFilename:
print('WARN: post not saved to outbox ' + outboxName)
return False
# save all instance blogs to the news actor
if postToNickname != 'news' and outboxName == 'tlblogs':
if '/' in savedFilename:
savedPostId = savedFilename.split('/')[-1]
blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs'
if not os.path.isdir(blogsDir):
os.mkdir(blogsDir)
copyfile(savedFilename, blogsDir + '/' + savedPostId)
inboxUpdateIndex('tlblogs', baseDir,
'news@' + domain,
savedFilename, debug)
# clear the citations file if it exists
citationsFilename = \
baseDir + '/accounts/' + \
postToNickname + '@' + domain + '/.citations.txt'
if os.path.isfile(citationsFilename):
os.remove(citationsFilename)
if messageJson['type'] == 'Create' or \
messageJson['type'] == 'Question' or \
messageJson['type'] == 'Note' or \

View File

@ -25,6 +25,7 @@ from posts import createRepliesTimeline
from posts import createMediaTimeline
from posts import createNewsTimeline
from posts import createBlogsTimeline
from posts import createFeaturesTimeline
from posts import createBookmarksTimeline
from posts import createEventsTimeline
from posts import createInbox
@ -236,7 +237,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
elif nickname == 'news':
personUrl = httpPrefix + '://' + domain + \
'/about/more?news_actor=true'
personName = originalDomain
approveFollowers = True
personType = 'Application'
@ -437,10 +437,16 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
# If a config.json file doesn't exist then don't decrement
# remaining registrations counter
remainingConfigExists = getConfigParam(baseDir, 'registrationsRemaining')
if remainingConfigExists:
registrationsRemaining = int(remainingConfigExists)
if registrationsRemaining <= 0:
if nickname != 'news':
remainingConfigExists = \
getConfigParam(baseDir, 'registrationsRemaining')
if remainingConfigExists:
registrationsRemaining = int(remainingConfigExists)
if registrationsRemaining <= 0:
return None, None, None, None
else:
if os.path.isdir(baseDir + '/accounts/news@' + domain):
# news account already exists
return None, None, None, None
(privateKeyPem, publicKeyPem,
@ -451,12 +457,13 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
manualFollowerApproval,
password)
if not getConfigParam(baseDir, 'admin'):
# print(nickname+' becomes the instance admin and a moderator')
setConfigParam(baseDir, 'admin', nickname)
setRole(baseDir, nickname, domain, 'instance', 'admin')
setRole(baseDir, nickname, domain, 'instance', 'moderator')
setRole(baseDir, nickname, domain, 'instance', 'editor')
setRole(baseDir, nickname, domain, 'instance', 'delegator')
if nickname != 'news':
# print(nickname+' becomes the instance admin and a moderator')
setConfigParam(baseDir, 'admin', nickname)
setRole(baseDir, nickname, domain, 'instance', 'admin')
setRole(baseDir, nickname, domain, 'instance', 'moderator')
setRole(baseDir, nickname, domain, 'instance', 'editor')
setRole(baseDir, nickname, domain, 'instance', 'delegator')
if not os.path.isdir(baseDir + '/accounts'):
os.mkdir(baseDir + '/accounts')
@ -470,22 +477,33 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
fFile.write('\n')
# notify when posts are liked
notifyLikesFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/.notifyLikes'
with open(notifyLikesFilename, 'w+') as nFile:
nFile.write('\n')
if nickname != 'news':
notifyLikesFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/.notifyLikes'
with open(notifyLikesFilename, 'w+') as nFile:
nFile.write('\n')
if os.path.isfile(baseDir + '/img/default-avatar.png'):
copyfile(baseDir + '/img/default-avatar.png',
baseDir + '/accounts/' + nickname + '@' + domain +
'/avatar.png')
theme = getConfigParam(baseDir, 'theme')
if not theme:
theme = 'default'
if nickname != 'news':
if os.path.isfile(baseDir + '/img/default-avatar.png'):
copyfile(baseDir + '/img/default-avatar.png',
baseDir + '/accounts/' + nickname + '@' + domain +
'/avatar.png')
else:
newsAvatar = baseDir + '/theme/' + theme + '/icons/avatar_news.png'
if os.path.isfile(newsAvatar):
copyfile(newsAvatar,
baseDir + '/accounts/' + nickname + '@' + domain +
'/avatar.png')
defaultProfileImageFilename = baseDir + '/theme/default/image.png'
if theme:
if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'):
defaultBannerFilename = baseDir + '/theme/' + theme + '/image.png'
defaultProfileImageFilename = \
baseDir + '/theme/' + theme + '/image.png'
if os.path.isfile(defaultProfileImageFilename):
copyfile(defaultProfileImageFilename, baseDir +
'/accounts/' + nickname + '@' + domain + '/image.png')
@ -496,7 +514,7 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
if os.path.isfile(defaultBannerFilename):
copyfile(defaultBannerFilename, baseDir + '/accounts/' +
nickname + '@' + domain + '/banner.png')
if remainingConfigExists:
if nickname != 'news' and remainingConfigExists:
registrationsRemaining -= 1
setConfigParam(baseDir, 'registrationsRemaining',
str(registrationsRemaining))
@ -516,8 +534,8 @@ def createNewsInbox(baseDir: str, domain: str, port: int,
httpPrefix: str) -> (str, str, {}, {}):
"""Generates the news inbox
"""
return createPersonBase(baseDir, 'news', domain, port, httpPrefix,
True, True, None)
return createPerson(baseDir, 'news', domain, port,
httpPrefix, True, True, None)
def personUpgradeActor(baseDir: str, personJson: {},
@ -611,6 +629,7 @@ def personBoxJson(recentPostsCache: {},
if boxname != 'inbox' and boxname != 'dm' and \
boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and boxname != 'tlnews' and \
boxname != 'tlfeatures' and \
boxname != 'outbox' and boxname != 'moderation' and \
boxname != 'tlbookmarks' and boxname != 'bookmarks' and \
boxname != 'tlevents':
@ -683,6 +702,10 @@ def personBoxJson(recentPostsCache: {},
httpPrefix, noOfItems, headerOnly,
newswireVotesThreshold, positiveVoting,
votingTimeMins, pageNumber)
elif boxname == 'tlfeatures':
return createFeaturesTimeline(session, baseDir, nickname, domain, port,
httpPrefix, noOfItems, headerOnly,
pageNumber)
elif boxname == 'tlblogs':
return createBlogsTimeline(session, baseDir, nickname, domain, port,
httpPrefix, noOfItems, headerOnly,

View File

@ -92,34 +92,6 @@ def isModerator(baseDir: str, nickname: str) -> bool:
return False
def isEditor(baseDir: str, nickname: str) -> bool:
"""Returns true if the given nickname is an editor
"""
editorsFile = baseDir + '/accounts/editors.txt'
if not os.path.isfile(editorsFile):
adminName = getConfigParam(baseDir, 'admin')
if not adminName:
return False
if adminName == nickname:
return True
return False
with open(editorsFile, "r") as f:
lines = f.readlines()
if len(lines) == 0:
adminName = getConfigParam(baseDir, 'admin')
if not adminName:
return False
if adminName == nickname:
return True
for editor in lines:
editor = editor.strip('\n').strip('\r')
if editor == nickname:
return True
return False
def noOfFollowersOnDomain(baseDir: str, handle: str,
domain: str, followFile='followers.txt') -> int:
"""Returns the number of followers of the given handle from the given domain
@ -582,6 +554,7 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str,
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
filename = boxDir + '/' + postId.replace('/', '#') + '.json'
saveJson(postJsonObject, filename)
return filename
@ -2587,6 +2560,15 @@ def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
0, False, 0, pageNumber)
def createFeaturesTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlfeatures', nickname,
domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
0, False, 0, pageNumber)
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, pageNumber=None) -> {}:
@ -2879,7 +2861,9 @@ def addPostStringToTimeline(postStr: str, boxname: str,
elif boxname == 'tlreplies':
if boxActor not in postStr:
return False
elif boxname == 'tlblogs' or boxname == 'tlnews':
elif (boxname == 'tlblogs' or
boxname == 'tlnews' or
boxname == 'tlfeatures'):
if '"Create"' not in postStr:
return False
if '"Article"' not in postStr:
@ -2900,6 +2884,13 @@ def addPostToTimeline(filePath: str, boxname: str,
"""
with open(filePath, 'r') as postFile:
postStr = postFile.read()
if filePath.endswith('.json'):
repliesFilename = filePath.replace('.json', '.replies')
if os.path.isfile(repliesFilename):
# append a replies identifier, which will later be removed
postStr += '<hasReplies>'
return addPostStringToTimeline(postStr, boxname, postsInBox, boxActor)
return False
@ -2918,6 +2909,7 @@ def createBoxIndexed(recentPostsCache: {},
if boxname != 'inbox' and boxname != 'dm' and \
boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and boxname != 'tlnews' and \
boxname != 'tlfeatures' and \
boxname != 'outbox' and boxname != 'tlbookmarks' and \
boxname != 'bookmarks' and \
boxname != 'tlevents':
@ -2926,9 +2918,14 @@ def createBoxIndexed(recentPostsCache: {},
# bookmarks and events timelines are like the inbox
# but have their own separate index
indexBoxName = boxname
timelineNickname = nickname
if boxname == "tlbookmarks":
boxname = "bookmarks"
indexBoxName = boxname
elif boxname == "tlfeatures":
boxname = "tlblogs"
indexBoxName = boxname
timelineNickname = 'news'
if port:
if port != 80 and port != 443:
@ -2966,7 +2963,7 @@ def createBoxIndexed(recentPostsCache: {},
postsInBox = []
indexFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
baseDir + '/accounts/' + timelineNickname + '@' + domain + \
'/' + indexBoxName + '.index'
postsCtr = 0
if os.path.isfile(indexFilename):
@ -3051,7 +3048,18 @@ def createBoxIndexed(recentPostsCache: {},
addPostToTimeline(fullPostFilename, boxname,
postsInBox, boxActor)
else:
print('WARN: unable to locate post ' + postUrl)
# if this is the features timeline
if timelineNickname != nickname:
fullPostFilename = \
locatePost(baseDir, timelineNickname,
domain, postUrl, False)
if fullPostFilename:
addPostToTimeline(fullPostFilename, boxname,
postsInBox, boxActor)
else:
print('WARN: unable to locate post ' + postUrl)
else:
print('WARN: unable to locate post ' + postUrl)
postsCtr += 1
@ -3080,12 +3088,24 @@ def createBoxIndexed(recentPostsCache: {},
return boxHeader
for postStr in postsInBox:
# Check if the post has replies
hasReplies = False
if postStr.endswith('<hasReplies>'):
hasReplies = True
# remove the replies identifier
postStr = postStr.replace('<hasReplies>', '')
p = None
try:
p = json.loads(postStr)
except BaseException:
continue
# Does this post have replies?
# This will be used to indicate that replies exist within the html
# created by individualPostAsHtml
p['hasReplies'] = hasReplies
# Don't show likes, replies or shares (announces) to
# unauthorized viewers
if not authorized:

View File

@ -46,15 +46,20 @@ def clearEditorStatus(baseDir: str) -> None:
for f in os.scandir(directory):
f = f.name
filename = os.fsdecode(f)
if filename.endswith(".json") and '@' in filename:
filename = os.path.join(baseDir + '/accounts/', filename)
if '"editor"' in open(filename).read():
actorJson = loadJson(filename)
if actorJson:
if actorJson['roles'].get('instance'):
if 'editor' in actorJson['roles']['instance']:
actorJson['roles']['instance'].remove('editor')
saveJson(actorJson, filename)
if '@' not in filename:
continue
if not filename.endswith(".json"):
continue
filename = os.path.join(baseDir + '/accounts/', filename)
if '"editor"' not in open(filename).read():
continue
actorJson = loadJson(filename)
if not actorJson:
continue
if actorJson['roles'].get('instance'):
if 'editor' in actorJson['roles']['instance']:
actorJson['roles']['instance'].remove('editor')
saveJson(actorJson, filename)
def addModerator(baseDir: str, nickname: str, domain: str) -> None:

View File

@ -32,6 +32,7 @@ from follow import clearFollows
from follow import clearFollowers
from follow import sendFollowRequestViaServer
from follow import sendUnfollowRequestViaServer
from utils import validNickname
from utils import firstParagraphFromString
from utils import removeIdEnding
from utils import siteIsActive
@ -1387,6 +1388,8 @@ def testClientToServer():
httpPrefix,
cachedWebfingers, personCache,
True, __version__)
alicePetnamesFilename = aliceDir + '/accounts/' + \
'alice@' + aliceDomain + '/petnames.txt'
aliceFollowingFilename = \
aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt'
bobFollowersFilename = \
@ -1395,7 +1398,8 @@ def testClientToServer():
if os.path.isfile(bobFollowersFilename):
if 'alice@' + aliceDomain + ':' + str(alicePort) in \
open(bobFollowersFilename).read():
if os.path.isfile(aliceFollowingFilename):
if os.path.isfile(aliceFollowingFilename) and \
os.path.isfile(alicePetnamesFilename):
if 'bob@' + bobDomain + ':' + str(bobPort) in \
open(aliceFollowingFilename).read():
break
@ -1403,6 +1407,9 @@ def testClientToServer():
assert os.path.isfile(bobFollowersFilename)
assert os.path.isfile(aliceFollowingFilename)
assert os.path.isfile(alicePetnamesFilename)
assert 'bob bob@' + bobDomain in \
open(alicePetnamesFilename).read()
print('alice@' + aliceDomain + ':' + str(alicePort) + ' in ' +
bobFollowersFilename)
assert 'alice@' + aliceDomain + ':' + str(alicePort) in \
@ -2397,8 +2404,26 @@ def testParseFeedDate():
assert publishedDate == "2020-11-22 18:51:33+00:00"
def testValidNickname():
print('testValidNickname')
domain = 'somedomain.net'
nickname = 'myvalidnick'
assert validNickname(domain, nickname)
nickname = 'my.invalid.nick'
assert not validNickname(domain, nickname)
nickname = 'myinvalidnick?'
assert not validNickname(domain, nickname)
nickname = 'my invalid nick?'
assert not validNickname(domain, nickname)
def runAllTests():
print('Running tests...')
testValidNickname()
testParseFeedDate()
testFirstParagraphFromString()
testGetNewswireTags()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -17,6 +17,8 @@
"gallery-font-size": "35px",
"gallery-font-size-mobile": "55px",
"main-bg-color": "#002365",
"login-bg-color": "#002365",
"options-bg-color": "#002365",
"post-bg-color": "#002365",
"timeline-posts-background-color": "#002365",
"header-bg-color": "#002365",

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -0,0 +1,87 @@
{
"today-circle": "#03a494",
"main-link-color-hover": "blue",
"font-size-newswire-mobile": "32px",
"newswire-date-color": "#00a594",
"column-right-fg-color": "black",
"button-highlighted": "#2b5c6d",
"button-selected-highlighted": "#2b5c6d",
"button-approve": "#2b5c6d",
"login-button-color": "#2b5c6d",
"button-event-background-color": "#2b5c6d",
"post-separator-margin-top": "10px",
"post-separator-margin-bottom": "10px",
"vertical-between-posts": "10px",
"time-vertical-align": "10px",
"button-corner-radius": "5px",
"timeline-border-radius": "5px",
"newswire-publish-icon": "True",
"full-width-timeline-buttons": "False",
"icons-as-buttons": "False",
"rss-icon-at-top": "True",
"publish-button-at-top": "False",
"newswire-item-moderated-color": "grey",
"newswire-date-moderated-color": "grey",
"search-banner-height": "25vh",
"search-banner-height-mobile": "15vh",
"banner-height": "20vh",
"banner-height-mobile": "10vh",
"hashtag-background-color": "grey",
"focus-color": "grey",
"font-size-button-mobile": "26px",
"font-size": "26px",
"font-size2": "20px",
"font-size3": "34px",
"font-size4": "18px",
"font-size5": "16px",
"font-size-likes": "14px",
"font-size-links": "14px",
"font-size-newswire": "14px",
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#e6ebf0",
"main-bg-color": "#e6ebf0",
"login-bg-color": "#010026",
"options-bg-color": "#010026",
"post-bg-color": "#e6ebf0",
"timeline-posts-background-color": "#e6ebf0",
"header-bg-color": "#e6ebf0",
"main-bg-color-dm": "#e3dbf0",
"link-bg-color": "#e6ebf0",
"main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0",
"main-fg-color": "#2d2c37",
"login-fg-color": "white",
"options-fg-color": "lightgrey",
"column-left-fg-color": "#2d2c37",
"border-color": "#c0cdd9",
"border-width": "1px",
"border-width-header": "1px",
"main-link-color": "darkblue",
"title-color": "#2a2c37",
"main-visited-color": "#232c37",
"text-entry-foreground": "#111",
"text-entry-background": "white",
"font-color-header": "black",
"dropdown-fg-color": "#222",
"dropdown-fg-color-hover": "#222",
"dropdown-bg-color": "white",
"dropdown-bg-color-hover": "lightgrey",
"color: #FFFFFE;": "color: black;",
"calendar-bg-color": "#e6ebf0",
"lines-color": "darkblue",
"day-number": "black",
"day-number2": "#282c37",
"place-color": "black",
"event-color": "#282c37",
"today-foreground": "white",
"event-background": "lightgrey",
"event-foreground": "white",
"title-text": "white",
"title-background": "#2b5c6d",
"gallery-text-color": "black",
"header-font": "'NimbusSanL'",
"*font-family": "'NimbusSanL'",
"*src": "url('./fonts/NimbusSanL.otf') format('opentype')",
"**src": "url('./fonts/NimbusSanL-italic.otf') format('opentype')"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -6,6 +6,8 @@
"publish-button-at-top": "False",
"focus-color": "green",
"main-bg-color": "black",
"login-bg-color": "black",
"options-bg-color": "black",
"post-bg-color": "black",
"timeline-posts-background-color": "black",
"header-bg-color": "black",
@ -16,6 +18,8 @@
"main-bg-color-report": "#050202",
"main-header-color-roles": "#1f192d",
"main-fg-color": "#00ff00",
"login-fg-color": "#00ff00",
"options-fg-color": "#00ff00",
"column-left-fg-color": "#00ff00",
"border-color": "#035103",
"main-link-color": "#2fff2f",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,4 +1,9 @@
{
"time-color": "grey",
"event-color": "white",
"login-bg-color": "#567726",
"login-fg-color": "black",
"options-bg-color": "black",
"newswire-publish-icon": "True",
"full-width-timeline-buttons": "False",
"icons-as-buttons": "False",
@ -25,6 +30,7 @@
"title-color": "white",
"main-visited-color": "#e1c4bc",
"main-fg-color": "white",
"options-fg-color": "white",
"column-left-fg-color": "white",
"main-bg-color-dm": "#343335",
"border-color": "#222",
@ -49,7 +55,7 @@
"lines-color": "#c5d2b9",
"day-number": "#c5d2b9",
"day-number2": "#ccc",
"event-background": "#333",
"event-background": "#555",
"timeline-border-radius": "20px",
"image-corners": "8%",
"quote-right-margin": "0.1em",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -27,6 +27,8 @@
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "black",
"login-bg-color": "black",
"options-bg-color": "black",
"post-bg-color": "black",
"timeline-posts-background-color": "black",
"header-bg-color": "black",
@ -40,6 +42,8 @@
"main-link-color-hover": "#d09338",
"main-visited-color": "#ffb900",
"main-fg-color": "white",
"login-fg-color": "white",
"options-fg-color": "white",
"column-left-fg-color": "white",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#003366",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -1,4 +1,6 @@
{
"timeline-icon-width": "30px",
"timeline-icon-width-mobile": "60px",
"button-bottom-margin": "0",
"header-vertical-offset": "10px",
"header-bg-color": "#efefef",
@ -15,7 +17,6 @@
"hashtag-size2": "30px",
"font-size-calendar-header": "2rem",
"font-size-calendar-cell": "2rem",
"calendar-horizontal-padding": "20%",
"time-vertical-align": "10px",
"publish-button-vertical-offset": "15px",
"vertical-between-posts": "0",
@ -52,7 +53,7 @@
"container-button-padding": "0px",
"container-button-margin": "0px",
"column-left-icon-size": "15%",
"column-right-icon-size": "8%",
"column-right-icon-size": "9.5%",
"button-margin": "2px",
"button-height-padding": "5px",
"icon-brightness-change": "70%",
@ -64,8 +65,8 @@
"login-button-color": "#25408f",
"login-button-fg-color": "white",
"column-left-width": "10vw",
"column-center-width": "70vw",
"column-right-width": "20vw",
"column-center-width": "75vw",
"column-right-width": "15vw",
"column-right-fg-color": "#25408f",
"column-right-fg-color-voted-on": "red",
"newswire-item-moderated-color": "red",
@ -88,6 +89,8 @@
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#efefef",
"main-bg-color": "#efefef",
"login-bg-color": "#efefef",
"options-bg-color": "#efefef",
"post-bg-color": "white",
"timeline-posts-background-color": "white",
"main-bg-color-dm": "white",
@ -96,6 +99,8 @@
"main-bg-color-report": "white",
"main-header-color-roles": "#ebebf0",
"main-fg-color": "black",
"login-fg-color": "black",
"options-fg-color": "black",
"column-left-fg-color": "#25408f",
"border-color": "#c0cdd9",
"main-link-color": "#25408f",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -8,6 +8,8 @@
"column-left-header-background": "#9fb42b",
"column-left-header-color": "#33390d",
"main-bg-color": "#9fb42b",
"login-bg-color": "#9fb42b",
"options-bg-color": "#9fb42b",
"post-bg-color": "#9fb42b",
"timeline-posts-background-color": "#9fb42b",
"header-bg-color": "#9fb42b",
@ -21,6 +23,8 @@
"main-bg-color-dm": "#5fb42b",
"main-header-color-roles": "#9fb42b",
"main-fg-color": "#33390d",
"login-fg-color": "#33390d",
"options-fg-color": "#33390d",
"border-color": "#33390d",
"border-width": "5px",
"border-width-header": "5px",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -22,15 +22,19 @@
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#e6ebf0",
"main-bg-color": "#e6ebf0",
"login-bg-color": "#e6ebf0",
"options-bg-color": "#e6ebf0",
"post-bg-color": "#e6ebf0",
"timeline-posts-background-color": "#e6ebf0",
"header-bg-color": "#e6ebf0",
"main-bg-color-dm": "#e3dbf0",
"link-bg-color": "#e6ebf0",
"main-bg-color-reply": "#e0dbf0",
"main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0",
"main-fg-color": "#2d2c37",
"login-fg-color": "#2d2c37",
"options-fg-color": "#2d2c37",
"column-left-fg-color": "#2d2c37",
"border-color": "#c0cdd9",
"main-link-color": "#2a2c37",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -20,6 +20,8 @@
"font-size4": "24px",
"font-size5": "22px",
"main-bg-color": "#0f0d10",
"login-bg-color": "#0f0d10",
"options-bg-color": "#0f0d10",
"post-bg-color": "#0f0d10",
"timeline-posts-background-color": "#0f0d10",
"header-bg-color": "#0f0d10",
@ -29,6 +31,8 @@
"main-link-color": "#6481f5",
"main-link-color-hover": "#d09338",
"main-fg-color": "#0481f5",
"login-fg-color": "#0481f5",
"options-fg-color": "#0481f5",
"column-left-fg-color": "#0481f5",
"main-bg-color-dm": "#0b0a0a",
"border-color": "#606984",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More