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

main
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 binascii
import os import os
import secrets import secrets
from utils import isSystemAccount
def hashPassword(password: str) -> str: def hashPassword(password: str) -> str:
@ -85,7 +86,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
""" """
if ' ' not in authHeader: if ' ' not in authHeader:
if debug: if debug:
print('DEBUG: Authorixation header does not ' + print('DEBUG: basic auth - Authorixation header does not ' +
'contain a space character') 'contain a space character')
return False return False
if '/users/' not in path and \ if '/users/' not in path and \
@ -93,23 +94,32 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
'/channel/' not in path and \ '/channel/' not in path and \
'/profile/' not in path: '/profile/' not in path:
if debug: 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 return False
pathUsersSection = path.split('/users/')[1] pathUsersSection = path.split('/users/')[1]
if '/' not in pathUsersSection: if '/' not in pathUsersSection:
if debug: if debug:
print('DEBUG: This is not a users endpoint') print('DEBUG: basic auth - this is not a users endpoint')
return False return False
nicknameFromPath = pathUsersSection.split('/')[0] nicknameFromPath = pathUsersSection.split('/')[0]
if isSystemAccount(nicknameFromPath):
print('basic auth - attempted login using system account ' +
nicknameFromPath + ' in path')
return False
base64Str = \ base64Str = \
authHeader.split(' ')[1].replace('\n', '').replace('\r', '') authHeader.split(' ')[1].replace('\n', '').replace('\r', '')
plain = base64.b64decode(base64Str).decode('utf-8') plain = base64.b64decode(base64Str).decode('utf-8')
if ':' not in plain: if ':' not in plain:
if debug: if debug:
print('DEBUG: Basic Auth header does not contain a ":" ' + print('DEBUG: basic auth header does not contain a ":" ' +
'separator for username:password') 'separator for username:password')
return False return False
nickname = plain.split(':')[0] 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 nickname != nicknameFromPath:
if debug: if debug:
print('DEBUG: Nickname given in the path (' + nicknameFromPath + 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' globalBlockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(globalBlockingFilename): if os.path.isfile(globalBlockingFilename):
hashtag = hashtag.strip('\n').strip('\r') hashtag = hashtag.strip('\n').strip('\r')
if not hashtag.startswith('#'):
hashtag = '#' + hashtag
if hashtag + '\n' in open(globalBlockingFilename).read(): if hashtag + '\n' in open(globalBlockingFilename).read():
return True return True
return False return False

View File

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

View File

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

529
daemon.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -689,12 +689,6 @@ if args.json:
pprint(testJson) pprint(testJson)
sys.exit() sys.exit()
if args.rss:
session = createSession(None)
testRSS = getRSS(session, args.rss)
pprint(testRSS)
sys.exit()
# create cache for actors # create cache for actors
if not os.path.isdir(baseDir + '/cache'): if not os.path.isdir(baseDir + '/cache'):
os.mkdir(baseDir + '/cache') os.mkdir(baseDir + '/cache')
@ -756,6 +750,13 @@ if args.domain:
domain = args.domain domain = args.domain
setConfigParam(baseDir, 'domain', 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 args.onion:
if not args.onion.endswith('.onion'): if not args.onion.endswith('.onion'):
print(args.onion + ' does not look like an onion domain') 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 isImageMedia
from posts import sendSignedJson from posts import sendSignedJson
from posts import sendToFollowersThread from posts import sendToFollowersThread
from webapp import individualPostAsHtml from webapp_utils import getIconsWebPath
from webapp import getIconsWebPath from webapp_post import individualPostAsHtml
from question import questionUpdateVotes from question import questionUpdateVotes
from media import replaceYouTube from media import replaceYouTube
from git import isGitPatch 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? # if this post in the outbox of the person?
messageId = removeIdEnding(messageJson['object']) messageId = removeIdEnding(messageJson['object'])
removeModerationPostFromIndex(baseDir, messageId, debug) removeModerationPostFromIndex(baseDir, messageId, debug)
postFilename = locatePost(baseDir, handle.split('@')[0], handleNickname = handle.split('@')[0]
handle.split('@')[1], messageId) handleDomain = handle.split('@')[1]
postFilename = locatePost(baseDir, handleNickname,
handleDomain, messageId)
if not postFilename: if not postFilename:
if debug: if debug:
print('DEBUG: delete post not found in inbox or outbox') print('DEBUG: delete post not found in inbox or outbox')
print(messageId) print(messageId)
return True return True
deletePost(baseDir, httpPrefix, handle.split('@')[0], deletePost(baseDir, httpPrefix, handleNickname,
handle.split('@')[1], postFilename, debug, handleDomain, postFilename, debug,
recentPostsCache) recentPostsCache)
if debug: if debug:
print('DEBUG: post deleted - ' + postFilename) 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 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.maxNewswireFeedSizeKb,
httpd.maxTags, httpd.maxTags,
httpd.maxFeedItemSizeKb, httpd.maxFeedItemSizeKb,
httpd.maxNewswirePosts) httpd.maxNewswirePosts,
httpd.maxCategoriesFeedItemSizeKb)
if not httpd.newswire: if not httpd.newswire:
if os.path.isfile(newswireStateFilename): if os.path.isfile(newswireStateFilename):

View File

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

View File

@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import os import os
from shutil import copyfile
from session import createSession from session import createSession
from auth import createPassword from auth import createPassword
from posts import outboxMessageCreateWrap from posts import outboxMessageCreateWrap
@ -116,24 +117,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
fileExtension = 'png' fileExtension = 'png'
mediaTypeStr = \ mediaTypeStr = \
attach['mediaType'] attach['mediaType']
if mediaTypeStr.endswith('jpeg'):
fileExtension = 'jpg' extensions = {
elif mediaTypeStr.endswith('gif'): "jpeg": "jpg",
fileExtension = 'gif' "gif": "gif",
elif mediaTypeStr.endswith('webp'): "webp": "webp",
fileExtension = 'webp' "avif": "avif",
elif mediaTypeStr.endswith('avif'): "audio/mpeg": "mp3",
fileExtension = 'avif' "ogg": "ogg",
elif mediaTypeStr.endswith('audio/mpeg'): "mp4": "mp4",
fileExtension = 'mp3' "webm": "webm",
elif mediaTypeStr.endswith('ogg'): "ogv": "ogv"
fileExtension = 'ogg' }
elif mediaTypeStr.endswith('mp4'): for matchExt, ext in extensions.items():
fileExtension = 'mp4' if mediaTypeStr.endswith(matchExt):
elif mediaTypeStr.endswith('webm'): fileExtension = ext
fileExtension = 'webm' break
elif mediaTypeStr.endswith('ogv'):
fileExtension = 'ogv'
mediaDir = \ mediaDir = \
baseDir + '/accounts/' + \ baseDir + '/accounts/' + \
postToNickname + '@' + domain postToNickname + '@' + domain
@ -188,11 +188,31 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
savePostToBox(baseDir, savePostToBox(baseDir,
httpPrefix, httpPrefix,
postId, postId,
postToNickname, postToNickname, domainFull,
domainFull, messageJson, outboxName) messageJson, outboxName)
if not savedFilename: if not savedFilename:
print('WARN: post not saved to outbox ' + outboxName) print('WARN: post not saved to outbox ' + outboxName)
return False 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 \ if messageJson['type'] == 'Create' or \
messageJson['type'] == 'Question' or \ messageJson['type'] == 'Question' or \
messageJson['type'] == 'Note' or \ messageJson['type'] == 'Note' or \

View File

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

View File

@ -92,34 +92,6 @@ def isModerator(baseDir: str, nickname: str) -> bool:
return False 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, def noOfFollowersOnDomain(baseDir: str, handle: str,
domain: str, followFile='followers.txt') -> int: domain: str, followFile='followers.txt') -> int:
"""Returns the number of followers of the given handle from the given domain """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) boxDir = createPersonDir(nickname, domain, baseDir, boxname)
filename = boxDir + '/' + postId.replace('/', '#') + '.json' filename = boxDir + '/' + postId.replace('/', '#') + '.json'
saveJson(postJsonObject, filename) saveJson(postJsonObject, filename)
return filename return filename
@ -2587,6 +2560,15 @@ def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
0, False, 0, pageNumber) 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, def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, pageNumber=None) -> {}: headerOnly: bool, pageNumber=None) -> {}:
@ -2879,7 +2861,9 @@ def addPostStringToTimeline(postStr: str, boxname: str,
elif boxname == 'tlreplies': elif boxname == 'tlreplies':
if boxActor not in postStr: if boxActor not in postStr:
return False return False
elif boxname == 'tlblogs' or boxname == 'tlnews': elif (boxname == 'tlblogs' or
boxname == 'tlnews' or
boxname == 'tlfeatures'):
if '"Create"' not in postStr: if '"Create"' not in postStr:
return False return False
if '"Article"' not in postStr: if '"Article"' not in postStr:
@ -2900,6 +2884,13 @@ def addPostToTimeline(filePath: str, boxname: str,
""" """
with open(filePath, 'r') as postFile: with open(filePath, 'r') as postFile:
postStr = postFile.read() 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 addPostStringToTimeline(postStr, boxname, postsInBox, boxActor)
return False return False
@ -2918,6 +2909,7 @@ def createBoxIndexed(recentPostsCache: {},
if boxname != 'inbox' and boxname != 'dm' and \ if boxname != 'inbox' and boxname != 'dm' and \
boxname != 'tlreplies' and boxname != 'tlmedia' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and boxname != 'tlnews' and \ boxname != 'tlblogs' and boxname != 'tlnews' and \
boxname != 'tlfeatures' and \
boxname != 'outbox' and boxname != 'tlbookmarks' and \ boxname != 'outbox' and boxname != 'tlbookmarks' and \
boxname != 'bookmarks' and \ boxname != 'bookmarks' and \
boxname != 'tlevents': boxname != 'tlevents':
@ -2926,9 +2918,14 @@ def createBoxIndexed(recentPostsCache: {},
# bookmarks and events timelines are like the inbox # bookmarks and events timelines are like the inbox
# but have their own separate index # but have their own separate index
indexBoxName = boxname indexBoxName = boxname
timelineNickname = nickname
if boxname == "tlbookmarks": if boxname == "tlbookmarks":
boxname = "bookmarks" boxname = "bookmarks"
indexBoxName = boxname indexBoxName = boxname
elif boxname == "tlfeatures":
boxname = "tlblogs"
indexBoxName = boxname
timelineNickname = 'news'
if port: if port:
if port != 80 and port != 443: if port != 80 and port != 443:
@ -2966,7 +2963,7 @@ def createBoxIndexed(recentPostsCache: {},
postsInBox = [] postsInBox = []
indexFilename = \ indexFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \ baseDir + '/accounts/' + timelineNickname + '@' + domain + \
'/' + indexBoxName + '.index' '/' + indexBoxName + '.index'
postsCtr = 0 postsCtr = 0
if os.path.isfile(indexFilename): if os.path.isfile(indexFilename):
@ -3051,7 +3048,18 @@ def createBoxIndexed(recentPostsCache: {},
addPostToTimeline(fullPostFilename, boxname, addPostToTimeline(fullPostFilename, boxname,
postsInBox, boxActor) postsInBox, boxActor)
else: 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 postsCtr += 1
@ -3080,12 +3088,24 @@ def createBoxIndexed(recentPostsCache: {},
return boxHeader return boxHeader
for postStr in postsInBox: 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 p = None
try: try:
p = json.loads(postStr) p = json.loads(postStr)
except BaseException: except BaseException:
continue 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 # Don't show likes, replies or shares (announces) to
# unauthorized viewers # unauthorized viewers
if not authorized: if not authorized:

View File

@ -46,15 +46,20 @@ def clearEditorStatus(baseDir: str) -> None:
for f in os.scandir(directory): for f in os.scandir(directory):
f = f.name f = f.name
filename = os.fsdecode(f) filename = os.fsdecode(f)
if filename.endswith(".json") and '@' in filename: if '@' not in filename:
filename = os.path.join(baseDir + '/accounts/', filename) continue
if '"editor"' in open(filename).read(): if not filename.endswith(".json"):
actorJson = loadJson(filename) continue
if actorJson: filename = os.path.join(baseDir + '/accounts/', filename)
if actorJson['roles'].get('instance'): if '"editor"' not in open(filename).read():
if 'editor' in actorJson['roles']['instance']: continue
actorJson['roles']['instance'].remove('editor') actorJson = loadJson(filename)
saveJson(actorJson, 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: 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 clearFollowers
from follow import sendFollowRequestViaServer from follow import sendFollowRequestViaServer
from follow import sendUnfollowRequestViaServer from follow import sendUnfollowRequestViaServer
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 utils import siteIsActive
@ -1387,6 +1388,8 @@ def testClientToServer():
httpPrefix, httpPrefix,
cachedWebfingers, personCache, cachedWebfingers, personCache,
True, __version__) True, __version__)
alicePetnamesFilename = aliceDir + '/accounts/' + \
'alice@' + aliceDomain + '/petnames.txt'
aliceFollowingFilename = \ aliceFollowingFilename = \
aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt' aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt'
bobFollowersFilename = \ bobFollowersFilename = \
@ -1395,7 +1398,8 @@ def testClientToServer():
if os.path.isfile(bobFollowersFilename): if os.path.isfile(bobFollowersFilename):
if 'alice@' + aliceDomain + ':' + str(alicePort) in \ if 'alice@' + aliceDomain + ':' + str(alicePort) in \
open(bobFollowersFilename).read(): open(bobFollowersFilename).read():
if os.path.isfile(aliceFollowingFilename): if os.path.isfile(aliceFollowingFilename) and \
os.path.isfile(alicePetnamesFilename):
if 'bob@' + bobDomain + ':' + str(bobPort) in \ if 'bob@' + bobDomain + ':' + str(bobPort) in \
open(aliceFollowingFilename).read(): open(aliceFollowingFilename).read():
break break
@ -1403,6 +1407,9 @@ def testClientToServer():
assert os.path.isfile(bobFollowersFilename) assert os.path.isfile(bobFollowersFilename)
assert os.path.isfile(aliceFollowingFilename) assert os.path.isfile(aliceFollowingFilename)
assert os.path.isfile(alicePetnamesFilename)
assert 'bob bob@' + bobDomain in \
open(alicePetnamesFilename).read()
print('alice@' + aliceDomain + ':' + str(alicePort) + ' in ' + print('alice@' + aliceDomain + ':' + str(alicePort) + ' in ' +
bobFollowersFilename) bobFollowersFilename)
assert 'alice@' + aliceDomain + ':' + str(alicePort) in \ assert 'alice@' + aliceDomain + ':' + str(alicePort) in \
@ -2397,8 +2404,26 @@ def testParseFeedDate():
assert publishedDate == "2020-11-22 18:51:33+00:00" 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(): def runAllTests():
print('Running tests...') print('Running tests...')
testValidNickname()
testParseFeedDate() testParseFeedDate()
testFirstParagraphFromString() testFirstParagraphFromString()
testGetNewswireTags() 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": "35px",
"gallery-font-size-mobile": "55px", "gallery-font-size-mobile": "55px",
"main-bg-color": "#002365", "main-bg-color": "#002365",
"login-bg-color": "#002365",
"options-bg-color": "#002365",
"post-bg-color": "#002365", "post-bg-color": "#002365",
"timeline-posts-background-color": "#002365", "timeline-posts-background-color": "#002365",
"header-bg-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", "publish-button-at-top": "False",
"focus-color": "green", "focus-color": "green",
"main-bg-color": "black", "main-bg-color": "black",
"login-bg-color": "black",
"options-bg-color": "black",
"post-bg-color": "black", "post-bg-color": "black",
"timeline-posts-background-color": "black", "timeline-posts-background-color": "black",
"header-bg-color": "black", "header-bg-color": "black",
@ -16,6 +18,8 @@
"main-bg-color-report": "#050202", "main-bg-color-report": "#050202",
"main-header-color-roles": "#1f192d", "main-header-color-roles": "#1f192d",
"main-fg-color": "#00ff00", "main-fg-color": "#00ff00",
"login-fg-color": "#00ff00",
"options-fg-color": "#00ff00",
"column-left-fg-color": "#00ff00", "column-left-fg-color": "#00ff00",
"border-color": "#035103", "border-color": "#035103",
"main-link-color": "#2fff2f", "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", "newswire-publish-icon": "True",
"full-width-timeline-buttons": "False", "full-width-timeline-buttons": "False",
"icons-as-buttons": "False", "icons-as-buttons": "False",
@ -25,6 +30,7 @@
"title-color": "white", "title-color": "white",
"main-visited-color": "#e1c4bc", "main-visited-color": "#e1c4bc",
"main-fg-color": "white", "main-fg-color": "white",
"options-fg-color": "white",
"column-left-fg-color": "white", "column-left-fg-color": "white",
"main-bg-color-dm": "#343335", "main-bg-color-dm": "#343335",
"border-color": "#222", "border-color": "#222",
@ -49,7 +55,7 @@
"lines-color": "#c5d2b9", "lines-color": "#c5d2b9",
"day-number": "#c5d2b9", "day-number": "#c5d2b9",
"day-number2": "#ccc", "day-number2": "#ccc",
"event-background": "#333", "event-background": "#555",
"timeline-border-radius": "20px", "timeline-border-radius": "20px",
"image-corners": "8%", "image-corners": "8%",
"quote-right-margin": "0.1em", "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-size4": "24px",
"font-size5": "22px", "font-size5": "22px",
"main-bg-color": "black", "main-bg-color": "black",
"login-bg-color": "black",
"options-bg-color": "black",
"post-bg-color": "black", "post-bg-color": "black",
"timeline-posts-background-color": "black", "timeline-posts-background-color": "black",
"header-bg-color": "black", "header-bg-color": "black",
@ -40,6 +42,8 @@
"main-link-color-hover": "#d09338", "main-link-color-hover": "#d09338",
"main-visited-color": "#ffb900", "main-visited-color": "#ffb900",
"main-fg-color": "white", "main-fg-color": "white",
"login-fg-color": "white",
"options-fg-color": "white",
"column-left-fg-color": "white", "column-left-fg-color": "white",
"main-bg-color-dm": "#0b0a0a", "main-bg-color-dm": "#0b0a0a",
"border-color": "#003366", "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", "button-bottom-margin": "0",
"header-vertical-offset": "10px", "header-vertical-offset": "10px",
"header-bg-color": "#efefef", "header-bg-color": "#efefef",
@ -15,7 +17,6 @@
"hashtag-size2": "30px", "hashtag-size2": "30px",
"font-size-calendar-header": "2rem", "font-size-calendar-header": "2rem",
"font-size-calendar-cell": "2rem", "font-size-calendar-cell": "2rem",
"calendar-horizontal-padding": "20%",
"time-vertical-align": "10px", "time-vertical-align": "10px",
"publish-button-vertical-offset": "15px", "publish-button-vertical-offset": "15px",
"vertical-between-posts": "0", "vertical-between-posts": "0",
@ -52,7 +53,7 @@
"container-button-padding": "0px", "container-button-padding": "0px",
"container-button-margin": "0px", "container-button-margin": "0px",
"column-left-icon-size": "15%", "column-left-icon-size": "15%",
"column-right-icon-size": "8%", "column-right-icon-size": "9.5%",
"button-margin": "2px", "button-margin": "2px",
"button-height-padding": "5px", "button-height-padding": "5px",
"icon-brightness-change": "70%", "icon-brightness-change": "70%",
@ -64,8 +65,8 @@
"login-button-color": "#25408f", "login-button-color": "#25408f",
"login-button-fg-color": "white", "login-button-fg-color": "white",
"column-left-width": "10vw", "column-left-width": "10vw",
"column-center-width": "70vw", "column-center-width": "75vw",
"column-right-width": "20vw", "column-right-width": "15vw",
"column-right-fg-color": "#25408f", "column-right-fg-color": "#25408f",
"column-right-fg-color-voted-on": "red", "column-right-fg-color-voted-on": "red",
"newswire-item-moderated-color": "red", "newswire-item-moderated-color": "red",
@ -88,6 +89,8 @@
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)", "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#efefef", "column-left-color": "#efefef",
"main-bg-color": "#efefef", "main-bg-color": "#efefef",
"login-bg-color": "#efefef",
"options-bg-color": "#efefef",
"post-bg-color": "white", "post-bg-color": "white",
"timeline-posts-background-color": "white", "timeline-posts-background-color": "white",
"main-bg-color-dm": "white", "main-bg-color-dm": "white",
@ -96,6 +99,8 @@
"main-bg-color-report": "white", "main-bg-color-report": "white",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"main-fg-color": "black", "main-fg-color": "black",
"login-fg-color": "black",
"options-fg-color": "black",
"column-left-fg-color": "#25408f", "column-left-fg-color": "#25408f",
"border-color": "#c0cdd9", "border-color": "#c0cdd9",
"main-link-color": "#25408f", "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-background": "#9fb42b",
"column-left-header-color": "#33390d", "column-left-header-color": "#33390d",
"main-bg-color": "#9fb42b", "main-bg-color": "#9fb42b",
"login-bg-color": "#9fb42b",
"options-bg-color": "#9fb42b",
"post-bg-color": "#9fb42b", "post-bg-color": "#9fb42b",
"timeline-posts-background-color": "#9fb42b", "timeline-posts-background-color": "#9fb42b",
"header-bg-color": "#9fb42b", "header-bg-color": "#9fb42b",
@ -21,6 +23,8 @@
"main-bg-color-dm": "#5fb42b", "main-bg-color-dm": "#5fb42b",
"main-header-color-roles": "#9fb42b", "main-header-color-roles": "#9fb42b",
"main-fg-color": "#33390d", "main-fg-color": "#33390d",
"login-fg-color": "#33390d",
"options-fg-color": "#33390d",
"border-color": "#33390d", "border-color": "#33390d",
"border-width": "5px", "border-width": "5px",
"border-width-header": "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)", "rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
"column-left-color": "#e6ebf0", "column-left-color": "#e6ebf0",
"main-bg-color": "#e6ebf0", "main-bg-color": "#e6ebf0",
"login-bg-color": "#e6ebf0",
"options-bg-color": "#e6ebf0",
"post-bg-color": "#e6ebf0", "post-bg-color": "#e6ebf0",
"timeline-posts-background-color": "#e6ebf0", "timeline-posts-background-color": "#e6ebf0",
"header-bg-color": "#e6ebf0", "header-bg-color": "#e6ebf0",
"main-bg-color-dm": "#e3dbf0", "main-bg-color-dm": "#e3dbf0",
"link-bg-color": "#e6ebf0", "link-bg-color": "#e6ebf0",
"main-bg-color-reply": "#e0dbf0", "main-bg-color-reply": "white",
"main-bg-color-report": "#e3dbf0", "main-bg-color-report": "#e3dbf0",
"main-header-color-roles": "#ebebf0", "main-header-color-roles": "#ebebf0",
"main-fg-color": "#2d2c37", "main-fg-color": "#2d2c37",
"login-fg-color": "#2d2c37",
"options-fg-color": "#2d2c37",
"column-left-fg-color": "#2d2c37", "column-left-fg-color": "#2d2c37",
"border-color": "#c0cdd9", "border-color": "#c0cdd9",
"main-link-color": "#2a2c37", "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-size4": "24px",
"font-size5": "22px", "font-size5": "22px",
"main-bg-color": "#0f0d10", "main-bg-color": "#0f0d10",
"login-bg-color": "#0f0d10",
"options-bg-color": "#0f0d10",
"post-bg-color": "#0f0d10", "post-bg-color": "#0f0d10",
"timeline-posts-background-color": "#0f0d10", "timeline-posts-background-color": "#0f0d10",
"header-bg-color": "#0f0d10", "header-bg-color": "#0f0d10",
@ -29,6 +31,8 @@
"main-link-color": "#6481f5", "main-link-color": "#6481f5",
"main-link-color-hover": "#d09338", "main-link-color-hover": "#d09338",
"main-fg-color": "#0481f5", "main-fg-color": "#0481f5",
"login-fg-color": "#0481f5",
"options-fg-color": "#0481f5",
"column-left-fg-color": "#0481f5", "column-left-fg-color": "#0481f5",
"main-bg-color-dm": "#0b0a0a", "main-bg-color-dm": "#0b0a0a",
"border-color": "#606984", "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