Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
18
auth.py
|
@ -11,6 +11,7 @@ import hashlib
|
|||
import binascii
|
||||
import os
|
||||
import secrets
|
||||
from utils import isSystemAccount
|
||||
|
||||
|
||||
def hashPassword(password: str) -> str:
|
||||
|
@ -85,7 +86,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
|
|||
"""
|
||||
if ' ' not in authHeader:
|
||||
if debug:
|
||||
print('DEBUG: Authorixation header does not ' +
|
||||
print('DEBUG: basic auth - Authorixation header does not ' +
|
||||
'contain a space character')
|
||||
return False
|
||||
if '/users/' not in path and \
|
||||
|
@ -93,23 +94,32 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str,
|
|||
'/channel/' not in path and \
|
||||
'/profile/' not in path:
|
||||
if debug:
|
||||
print('DEBUG: Path for Authorization does not contain a user')
|
||||
print('DEBUG: basic auth - ' +
|
||||
'path for Authorization does not contain a user')
|
||||
return False
|
||||
pathUsersSection = path.split('/users/')[1]
|
||||
if '/' not in pathUsersSection:
|
||||
if debug:
|
||||
print('DEBUG: This is not a users endpoint')
|
||||
print('DEBUG: basic auth - this is not a users endpoint')
|
||||
return False
|
||||
nicknameFromPath = pathUsersSection.split('/')[0]
|
||||
if isSystemAccount(nicknameFromPath):
|
||||
print('basic auth - attempted login using system account ' +
|
||||
nicknameFromPath + ' in path')
|
||||
return False
|
||||
base64Str = \
|
||||
authHeader.split(' ')[1].replace('\n', '').replace('\r', '')
|
||||
plain = base64.b64decode(base64Str).decode('utf-8')
|
||||
if ':' not in plain:
|
||||
if debug:
|
||||
print('DEBUG: Basic Auth header does not contain a ":" ' +
|
||||
print('DEBUG: basic auth header does not contain a ":" ' +
|
||||
'separator for username:password')
|
||||
return False
|
||||
nickname = plain.split(':')[0]
|
||||
if isSystemAccount(nickname):
|
||||
print('basic auth - attempted login using system account ' + nickname +
|
||||
' in Auth header')
|
||||
return False
|
||||
if nickname != nicknameFromPath:
|
||||
if debug:
|
||||
print('DEBUG: Nickname given in the path (' + nicknameFromPath +
|
||||
|
|
|
@ -131,6 +131,8 @@ def isBlockedHashtag(baseDir: str, hashtag: str) -> bool:
|
|||
globalBlockingFilename = baseDir + '/accounts/blocking.txt'
|
||||
if os.path.isfile(globalBlockingFilename):
|
||||
hashtag = hashtag.strip('\n').strip('\r')
|
||||
if not hashtag.startswith('#'):
|
||||
hashtag = '#' + hashtag
|
||||
if hashtag + '\n' in open(globalBlockingFilename).read():
|
||||
return True
|
||||
return False
|
||||
|
|
8
blog.py
|
@ -10,11 +10,11 @@ import os
|
|||
from datetime import datetime
|
||||
|
||||
from content import replaceEmojiFromTags
|
||||
from webapp import getIconsWebPath
|
||||
from webapp import htmlHeaderWithExternalStyle
|
||||
from webapp import htmlFooter
|
||||
from webapp_media import addEmbeddedElements
|
||||
from webapp_utils import getIconsWebPath
|
||||
from webapp_utils import htmlHeaderWithExternalStyle
|
||||
from webapp_utils import htmlFooter
|
||||
from webapp_utils import getPostAttachmentsAsHtml
|
||||
from webapp_media import addEmbeddedElements
|
||||
from utils import getMediaFormats
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
|
|
|
@ -8,6 +8,7 @@ __status__ = "Production"
|
|||
|
||||
import os
|
||||
import email.parser
|
||||
import urllib.parse
|
||||
from shutil import copyfile
|
||||
from utils import getImageExtensions
|
||||
from utils import loadJson
|
||||
|
@ -991,5 +992,5 @@ def extractTextFieldsInPOST(postBytes, boundary, debug: bool) -> {}:
|
|||
if line > 2:
|
||||
postValue += '\n'
|
||||
postValue += postLines[line]
|
||||
fields[postKey] = postValue
|
||||
fields[postKey] = urllib.parse.unquote_plus(postValue)
|
||||
return fields
|
||||
|
|
529
daemon.py
|
@ -40,6 +40,8 @@ from ssb import getSSBAddress
|
|||
from ssb import setSSBAddress
|
||||
from tox import getToxAddress
|
||||
from tox import setToxAddress
|
||||
from jami import getJamiAddress
|
||||
from jami import setJamiAddress
|
||||
from matrix import getMatrixAddress
|
||||
from matrix import setMatrixAddress
|
||||
from donate import getDonationUrl
|
||||
|
@ -63,7 +65,6 @@ from person import canRemovePost
|
|||
from person import personSnooze
|
||||
from person import personUnsnooze
|
||||
from posts import isModerator
|
||||
from posts import isEditor
|
||||
from posts import mutePost
|
||||
from posts import unmutePost
|
||||
from posts import createQuestionPost
|
||||
|
@ -113,15 +114,16 @@ from blog import htmlBlogView
|
|||
from blog import htmlBlogPage
|
||||
from blog import htmlBlogPost
|
||||
from blog import htmlEditBlog
|
||||
from webapp_utils import htmlHashtagBlocked
|
||||
from webapp_utils import htmlFollowingList
|
||||
from webapp_utils import setBlogAddress
|
||||
from webapp_utils import getBlogAddress
|
||||
from webapp_calendar import htmlCalendarDeleteConfirm
|
||||
from webapp_calendar import htmlCalendar
|
||||
from webapp_about import htmlAbout
|
||||
from webapp import htmlFollowingList
|
||||
from webapp import htmlDeletePost
|
||||
from webapp import htmlRemoveSharedItem
|
||||
from webapp import htmlUnblockConfirm
|
||||
from webapp_confirm import htmlConfirmDelete
|
||||
from webapp_confirm import htmlConfirmRemoveSharedItem
|
||||
from webapp_confirm import htmlConfirmUnblock
|
||||
from webapp_person_options import htmlPersonOptions
|
||||
from webapp_timeline import htmlShares
|
||||
from webapp_timeline import htmlInbox
|
||||
|
@ -132,6 +134,7 @@ from webapp_timeline import htmlInboxReplies
|
|||
from webapp_timeline import htmlInboxMedia
|
||||
from webapp_timeline import htmlInboxBlogs
|
||||
from webapp_timeline import htmlInboxNews
|
||||
from webapp_timeline import htmlInboxFeatures
|
||||
from webapp_timeline import htmlOutbox
|
||||
from webapp_moderation import htmlModeration
|
||||
from webapp_moderation import htmlModerationInfo
|
||||
|
@ -140,9 +143,8 @@ from webapp_login import htmlLogin
|
|||
from webapp_login import htmlGetLoginCredentials
|
||||
from webapp_suspended import htmlSuspended
|
||||
from webapp_tos import htmlTermsOfService
|
||||
from webapp import htmlFollowConfirm
|
||||
from webapp import htmlUnfollowConfirm
|
||||
from webapp import htmlHashtagBlocked
|
||||
from webapp_confirm import htmlConfirmFollow
|
||||
from webapp_confirm import htmlConfirmUnfollow
|
||||
from webapp_post import htmlPostReplies
|
||||
from webapp_post import htmlIndividualPost
|
||||
from webapp_profile import htmlEditProfile
|
||||
|
@ -162,10 +164,14 @@ from webapp_search import htmlSearchEmoji
|
|||
from webapp_search import htmlSearchSharedItems
|
||||
from webapp_search import htmlSearchEmojiTextEntry
|
||||
from webapp_search import htmlSearch
|
||||
from webapp_hashtagswarm import getHashtagCategoriesFeed
|
||||
from webapp_hashtagswarm import htmlSearchHashtagCategory
|
||||
from shares import getSharesFeedForPerson
|
||||
from shares import addShare
|
||||
from shares import removeShare
|
||||
from shares import expireShares
|
||||
from utils import setHashtagCategory
|
||||
from utils import isEditor
|
||||
from utils import getImageExtensions
|
||||
from utils import mediaFileMimeType
|
||||
from utils import getCSS
|
||||
|
@ -227,6 +233,7 @@ from newswire import rss2Header
|
|||
from newswire import rss2Footer
|
||||
from newsdaemon import runNewswireWatchdog
|
||||
from newsdaemon import runNewswireDaemon
|
||||
from filters import isFiltered
|
||||
import os
|
||||
|
||||
|
||||
|
@ -1109,6 +1116,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if self.path.startswith('/icons/') or \
|
||||
self.path.startswith('/avatars/') or \
|
||||
self.path.startswith('/favicon.ico') or \
|
||||
self.path.startswith('/categories.xml') or \
|
||||
self.path.startswith('/newswire.xml'):
|
||||
return False
|
||||
|
||||
|
@ -1120,21 +1128,22 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
tokenStr = tokenStr.split(';')[0].strip()
|
||||
if self.server.tokensLookup.get(tokenStr):
|
||||
nickname = self.server.tokensLookup[tokenStr]
|
||||
self.authorizedNickname = nickname
|
||||
# default to the inbox of the person
|
||||
if self.path == '/':
|
||||
self.path = '/users/' + nickname + '/inbox'
|
||||
# check that the path contains the same nickname
|
||||
# as the cookie otherwise it would be possible
|
||||
# to be authorized to use an account you don't own
|
||||
if '/' + nickname + '/' in self.path:
|
||||
return True
|
||||
elif '/' + nickname + '?' in self.path:
|
||||
return True
|
||||
elif self.path.endswith('/' + nickname):
|
||||
return True
|
||||
print('AUTH: nickname ' + nickname +
|
||||
' was not found in path ' + self.path)
|
||||
if not isSystemAccount(nickname):
|
||||
self.authorizedNickname = nickname
|
||||
# default to the inbox of the person
|
||||
if self.path == '/':
|
||||
self.path = '/users/' + nickname + '/inbox'
|
||||
# check that the path contains the same nickname
|
||||
# as the cookie otherwise it would be possible
|
||||
# to be authorized to use an account you don't own
|
||||
if '/' + nickname + '/' in self.path:
|
||||
return True
|
||||
elif '/' + nickname + '?' in self.path:
|
||||
return True
|
||||
elif self.path.endswith('/' + nickname):
|
||||
return True
|
||||
print('AUTH: nickname ' + nickname +
|
||||
' was not found in path ' + self.path)
|
||||
return False
|
||||
print('AUTH: epicyon cookie ' +
|
||||
'authorization failed, header=' +
|
||||
|
@ -1144,13 +1153,13 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return False
|
||||
print('AUTH: Header cookie was not authorized')
|
||||
return False
|
||||
# basic auth
|
||||
# basic auth for c2s
|
||||
if self.headers.get('Authorization'):
|
||||
if authorize(self.server.baseDir, self.path,
|
||||
self.headers['Authorization'],
|
||||
self.server.debug):
|
||||
return True
|
||||
print('AUTH: Basic auth did not authorize ' +
|
||||
print('AUTH: C2S Basic auth did not authorize ' +
|
||||
self.headers['Authorization'])
|
||||
return False
|
||||
|
||||
|
@ -1518,9 +1527,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
moderationText)
|
||||
if postFilename:
|
||||
if canRemovePost(baseDir,
|
||||
nickname,
|
||||
domain,
|
||||
port,
|
||||
nickname, domain, port,
|
||||
moderationText):
|
||||
deletePost(baseDir,
|
||||
httpPrefix,
|
||||
|
@ -1528,6 +1535,23 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
postFilename,
|
||||
debug,
|
||||
self.server.recentPostsCache)
|
||||
if nickname != 'news':
|
||||
# if this is a local blog post then also remove it
|
||||
# from the news actor
|
||||
postFilename = \
|
||||
locatePost(baseDir, 'news', domain,
|
||||
moderationText)
|
||||
if postFilename:
|
||||
if canRemovePost(baseDir,
|
||||
'news', domain, port,
|
||||
moderationText):
|
||||
deletePost(baseDir,
|
||||
httpPrefix,
|
||||
'news', domain,
|
||||
postFilename,
|
||||
debug,
|
||||
self.server.recentPostsCache)
|
||||
|
||||
if callingDomain.endswith('.onion') and onionDomain:
|
||||
actorStr = 'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and i2pDomain):
|
||||
|
@ -1773,7 +1797,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if debug:
|
||||
print('Unblocking ' + optionsActor)
|
||||
msg = \
|
||||
htmlUnblockConfirm(self.server.cssCache,
|
||||
htmlConfirmUnblock(self.server.cssCache,
|
||||
self.server.translate,
|
||||
baseDir,
|
||||
usersPath,
|
||||
|
@ -1791,7 +1815,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if debug:
|
||||
print('Following ' + optionsActor)
|
||||
msg = \
|
||||
htmlFollowConfirm(self.server.cssCache,
|
||||
htmlConfirmFollow(self.server.cssCache,
|
||||
self.server.translate,
|
||||
baseDir,
|
||||
usersPath,
|
||||
|
@ -1808,7 +1832,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if '&submitUnfollow=' in optionsConfirmParams:
|
||||
print('Unfollowing ' + optionsActor)
|
||||
msg = \
|
||||
htmlUnfollowConfirm(self.server.cssCache,
|
||||
htmlConfirmUnfollow(self.server.cssCache,
|
||||
self.server.translate,
|
||||
baseDir,
|
||||
usersPath,
|
||||
|
@ -2732,7 +2756,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str,
|
||||
debug: bool) -> None:
|
||||
"""Endpoint for removing posts
|
||||
"""Endpoint for removing posts after confirmation
|
||||
"""
|
||||
pageNumber = 1
|
||||
usersPath = path.split('/rmpost')[0]
|
||||
|
@ -2796,7 +2820,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'actor': removePostActor,
|
||||
'object': removeMessageId,
|
||||
'to': toList,
|
||||
'cc': [removePostActor+'/followers'],
|
||||
'cc': [removePostActor + '/followers'],
|
||||
'type': 'Delete'
|
||||
}
|
||||
self.postToNickname = getNicknameFromActor(removePostActor)
|
||||
|
@ -2959,6 +2983,129 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _setHashtagCategory(self, callingDomain: str, cookie: str,
|
||||
authorized: bool, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str, debug: bool,
|
||||
defaultTimeline: str,
|
||||
allowLocalNetworkAccess: bool) -> None:
|
||||
"""On the screen after selecting a hashtag from the swarm, this sets
|
||||
the category for that tag
|
||||
"""
|
||||
usersPath = path.replace('/sethashtagcategory', '')
|
||||
hashtag = ''
|
||||
if '/tags/' not in usersPath:
|
||||
# no hashtag is specified within the path
|
||||
self._404()
|
||||
return
|
||||
hashtag = usersPath.split('/tags/')[1].strip()
|
||||
hashtag = urllib.parse.unquote_plus(hashtag)
|
||||
if not hashtag:
|
||||
# no hashtag was given in the path
|
||||
self._404()
|
||||
return
|
||||
hashtagFilename = baseDir + '/tags/' + hashtag + '.txt'
|
||||
if not os.path.isfile(hashtagFilename):
|
||||
# the hashtag does not exist
|
||||
self._404()
|
||||
return
|
||||
usersPath = usersPath.split('/tags/')[0]
|
||||
actorStr = httpPrefix + '://' + domainFull + usersPath
|
||||
tagScreenStr = actorStr + '/tags/' + hashtag
|
||||
if ' boundary=' in self.headers['Content-type']:
|
||||
boundary = self.headers['Content-type'].split('boundary=')[1]
|
||||
if ';' in boundary:
|
||||
boundary = boundary.split(';')[0]
|
||||
|
||||
# get the nickname
|
||||
nickname = getNicknameFromActor(actorStr)
|
||||
editor = None
|
||||
if nickname:
|
||||
editor = isEditor(baseDir, nickname)
|
||||
if not hashtag or not editor:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
if not nickname:
|
||||
print('WARN: nickname not found in ' + actorStr)
|
||||
else:
|
||||
print('WARN: nickname is not a moderator' + actorStr)
|
||||
self._redirect_headers(tagScreenStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
length = int(self.headers['Content-length'])
|
||||
|
||||
# check that the POST isn't too large
|
||||
if length > self.server.maxPostLength:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
print('Maximum links data length exceeded ' + str(length))
|
||||
self._redirect_headers(tagScreenStr, cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
try:
|
||||
# read the bytes of the http form POST
|
||||
postBytes = self.rfile.read(length)
|
||||
except SocketError as e:
|
||||
if e.errno == errno.ECONNRESET:
|
||||
print('WARN: connection was reset while ' +
|
||||
'reading bytes from http form POST')
|
||||
else:
|
||||
print('WARN: error while reading bytes ' +
|
||||
'from http form POST')
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
except ValueError as e:
|
||||
print('ERROR: failed to read bytes for POST')
|
||||
print(e)
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
# extract all of the text fields into a dict
|
||||
fields = \
|
||||
extractTextFieldsInPOST(postBytes, boundary, debug)
|
||||
|
||||
if fields.get('hashtagCategory'):
|
||||
categoryStr = fields['hashtagCategory'].lower()
|
||||
if not isBlockedHashtag(baseDir, categoryStr) and \
|
||||
not isFiltered(baseDir, nickname, domain, categoryStr):
|
||||
setHashtagCategory(baseDir, hashtag, categoryStr)
|
||||
else:
|
||||
categoryFilename = baseDir + '/tags/' + hashtag + '.category'
|
||||
if os.path.isfile(categoryFilename):
|
||||
os.remove(categoryFilename)
|
||||
|
||||
# redirect back to the default timeline
|
||||
if callingDomain.endswith('.onion') and \
|
||||
onionDomain:
|
||||
actorStr = \
|
||||
'http://' + onionDomain + usersPath
|
||||
elif (callingDomain.endswith('.i2p') and
|
||||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
self._redirect_headers(tagScreenStr,
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _newswireUpdate(self, callingDomain: str, cookie: str,
|
||||
authorized: bool, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
|
@ -3207,7 +3354,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
domain: str, domainFull: str,
|
||||
onionDomain: str, i2pDomain: str, debug: bool,
|
||||
defaultTimeline: str) -> None:
|
||||
"""edits a news post
|
||||
"""edits a news post after receiving POST
|
||||
"""
|
||||
usersPath = path.replace('/newseditdata', '')
|
||||
usersPath = usersPath.replace('/editnewspost', '')
|
||||
|
@ -3235,8 +3382,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
print('WARN: nickname not found in ' + actorStr)
|
||||
else:
|
||||
print('WARN: nickname is not an editor' + actorStr)
|
||||
self._redirect_headers(actorStr + '/tlnews',
|
||||
cookie, callingDomain)
|
||||
if self.server.newsInstance:
|
||||
self._redirect_headers(actorStr + '/tlfeatures',
|
||||
cookie, callingDomain)
|
||||
else:
|
||||
self._redirect_headers(actorStr + '/tlnews',
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
|
@ -3253,8 +3404,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
print('Maximum news data length exceeded ' + str(length))
|
||||
self._redirect_headers(actorStr + 'tlnews',
|
||||
cookie, callingDomain)
|
||||
if self.server.newsInstance:
|
||||
self._redirect_headers(actorStr + '/tlfeatures',
|
||||
cookie, callingDomain)
|
||||
else:
|
||||
self._redirect_headers(actorStr + '/tlnews',
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
return
|
||||
|
||||
|
@ -3342,8 +3497,12 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
i2pDomain):
|
||||
actorStr = \
|
||||
'http://' + i2pDomain + usersPath
|
||||
self._redirect_headers(actorStr + '/tlnews',
|
||||
cookie, callingDomain)
|
||||
if self.server.newsInstance:
|
||||
self._redirect_headers(actorStr + '/tlfeatures',
|
||||
cookie, callingDomain)
|
||||
else:
|
||||
self._redirect_headers(actorStr + '/tlnews',
|
||||
cookie, callingDomain)
|
||||
self.server.POSTbusy = False
|
||||
|
||||
def _profileUpdate(self, callingDomain: str, cookie: str,
|
||||
|
@ -3616,7 +3775,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.newsInstance = True
|
||||
self.server.blogsInstance = False
|
||||
self.server.mediaInstance = False
|
||||
self.server.defaultTimeline = 'tlnews'
|
||||
self.server.defaultTimeline = 'tlfeatures'
|
||||
setConfigParam(baseDir,
|
||||
"mediaInstance",
|
||||
self.server.mediaInstance)
|
||||
|
@ -3758,6 +3917,18 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
setToxAddress(actorJson, '')
|
||||
actorChanged = True
|
||||
|
||||
# change jami address
|
||||
currentJamiAddress = getJamiAddress(actorJson)
|
||||
if fields.get('jamiAddress'):
|
||||
if fields['jamiAddress'] != currentJamiAddress:
|
||||
setJamiAddress(actorJson,
|
||||
fields['jamiAddress'])
|
||||
actorChanged = True
|
||||
else:
|
||||
if currentJamiAddress:
|
||||
setJamiAddress(actorJson, '')
|
||||
actorChanged = True
|
||||
|
||||
# change PGP public key
|
||||
currentPGPpubKey = getPGPpubKey(actorJson)
|
||||
if fields.get('pgp'):
|
||||
|
@ -4643,6 +4814,41 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
path + ' ' + callingDomain)
|
||||
self._404()
|
||||
|
||||
def _getHashtagCategoriesFeed(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, port: int, proxyType: str,
|
||||
GETstartTime, GETtimings: {},
|
||||
debug: bool) -> None:
|
||||
"""Returns the hashtag categories feed
|
||||
"""
|
||||
if not self.server.session:
|
||||
print('Starting new session during RSS categories request')
|
||||
self.server.session = \
|
||||
createSession(proxyType)
|
||||
if not self.server.session:
|
||||
print('ERROR: GET failed to create session ' +
|
||||
'during RSS categories request')
|
||||
self._404()
|
||||
return
|
||||
|
||||
hashtagCategories = None
|
||||
msg = \
|
||||
getHashtagCategoriesFeed(baseDir, hashtagCategories)
|
||||
if msg:
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('text/xml', len(msg),
|
||||
None, callingDomain)
|
||||
self._write(msg)
|
||||
if debug:
|
||||
print('Sent rss2 categories feed: ' +
|
||||
path + ' ' + callingDomain)
|
||||
return
|
||||
if debug:
|
||||
print('Failed to get rss2 categories feed: ' +
|
||||
path + ' ' + callingDomain)
|
||||
self._404()
|
||||
|
||||
def _getRSS3feed(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
|
@ -4718,6 +4924,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
matrixAddress = None
|
||||
blogAddress = None
|
||||
toxAddress = None
|
||||
jamiAddress = None
|
||||
ssbAddress = None
|
||||
emailAddress = None
|
||||
actorJson = getPersonFromCache(baseDir,
|
||||
|
@ -4731,6 +4938,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
ssbAddress = getSSBAddress(actorJson)
|
||||
blogAddress = getBlogAddress(actorJson)
|
||||
toxAddress = getToxAddress(actorJson)
|
||||
jamiAddress = getJamiAddress(actorJson)
|
||||
emailAddress = getEmailAddress(actorJson)
|
||||
PGPpubKey = getPGPpubKey(actorJson)
|
||||
PGPfingerprint = getPGPfingerprint(actorJson)
|
||||
|
@ -4746,7 +4954,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
pageNumber, donateUrl,
|
||||
xmppAddress, matrixAddress,
|
||||
ssbAddress, blogAddress,
|
||||
toxAddress,
|
||||
toxAddress, jamiAddress,
|
||||
PGPpubKey, PGPfingerprint,
|
||||
emailAddress).encode('utf-8')
|
||||
self._set_headers('text/html', len(msg),
|
||||
|
@ -4758,7 +4966,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
if '/users/news/' in path:
|
||||
self._redirect_headers(originPathStr + '/tlnews',
|
||||
self._redirect_headers(originPathStr + '/tlfeatures',
|
||||
cookie, callingDomain)
|
||||
return
|
||||
|
||||
|
@ -4929,7 +5137,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
hashtag = path.split('/tags/')[1]
|
||||
if '?page=' in hashtag:
|
||||
hashtag = hashtag.split('?page=')[0]
|
||||
hashtag = urllib.parse.unquote_plus(hashtag)
|
||||
if isBlockedHashtag(baseDir, hashtag):
|
||||
print('BLOCK: hashtag #' + hashtag)
|
||||
msg = htmlHashtagBlocked(self.server.cssCache, baseDir,
|
||||
self.server.translate).encode('utf-8')
|
||||
self._login_headers('text/html', len(msg), callingDomain)
|
||||
|
@ -5791,7 +6001,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
GETstartTime, GETtimings: {},
|
||||
proxyType: str, cookie: str,
|
||||
debug: str):
|
||||
"""Delete button is pressed
|
||||
"""Delete button is pressed on a post
|
||||
"""
|
||||
if not cookie:
|
||||
print('ERROR: no cookie given when deleting')
|
||||
|
@ -5855,16 +6065,16 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return
|
||||
|
||||
deleteStr = \
|
||||
htmlDeletePost(self.server.cssCache,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate, pageNumber,
|
||||
self.server.session, baseDir,
|
||||
deleteUrl, httpPrefix,
|
||||
__version__, self.server.cachedWebfingers,
|
||||
self.server.personCache, callingDomain,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.showPublishedDateOnly)
|
||||
htmlConfirmDelete(self.server.cssCache,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate, pageNumber,
|
||||
self.server.session, baseDir,
|
||||
deleteUrl, httpPrefix,
|
||||
__version__, self.server.cachedWebfingers,
|
||||
self.server.personCache, callingDomain,
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.showPublishedDateOnly)
|
||||
if deleteStr:
|
||||
self._set_headers('text/html', len(deleteStr),
|
||||
cookie, callingDomain)
|
||||
|
@ -7299,6 +7509,127 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return True
|
||||
return False
|
||||
|
||||
def _showFeaturesTimeline(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
domain: str, domainFull: str, port: int,
|
||||
onionDomain: str, i2pDomain: str,
|
||||
GETstartTime, GETtimings: {},
|
||||
proxyType: str, cookie: str,
|
||||
debug: str) -> bool:
|
||||
"""Shows the features timeline (all local blogs)
|
||||
"""
|
||||
if '/users/' in path:
|
||||
if authorized:
|
||||
inboxFeaturesFeed = \
|
||||
personBoxJson(self.server.recentPostsCache,
|
||||
self.server.session,
|
||||
baseDir,
|
||||
domain,
|
||||
port,
|
||||
path,
|
||||
httpPrefix,
|
||||
maxPostsInNewsFeed, 'tlfeatures',
|
||||
True,
|
||||
self.server.newswireVotesThreshold,
|
||||
self.server.positiveVoting,
|
||||
self.server.votingTimeMins)
|
||||
if not inboxFeaturesFeed:
|
||||
inboxFeaturesFeed = []
|
||||
if self._requestHTTP():
|
||||
nickname = path.replace('/users/', '')
|
||||
nickname = nickname.replace('/tlfeatures', '')
|
||||
pageNumber = 1
|
||||
if '?page=' in nickname:
|
||||
pageNumber = nickname.split('?page=')[1]
|
||||
nickname = nickname.split('?page=')[0]
|
||||
if pageNumber.isdigit():
|
||||
pageNumber = int(pageNumber)
|
||||
else:
|
||||
pageNumber = 1
|
||||
if 'page=' not in path:
|
||||
# if no page was specified then show the first
|
||||
inboxFeaturesFeed = \
|
||||
personBoxJson(self.server.recentPostsCache,
|
||||
self.server.session,
|
||||
baseDir,
|
||||
domain,
|
||||
port,
|
||||
path + '?page=1',
|
||||
httpPrefix,
|
||||
maxPostsInBlogsFeed, 'tlfeatures',
|
||||
True,
|
||||
self.server.newswireVotesThreshold,
|
||||
self.server.positiveVoting,
|
||||
self.server.votingTimeMins)
|
||||
currNickname = path.split('/users/')[1]
|
||||
if '/' in currNickname:
|
||||
currNickname = currNickname.split('/')[0]
|
||||
fullWidthTimelineButtonHeader = \
|
||||
self.server.fullWidthTimelineButtonHeader
|
||||
msg = \
|
||||
htmlInboxFeatures(self.server.cssCache,
|
||||
self.server.defaultTimeline,
|
||||
self.server.recentPostsCache,
|
||||
self.server.maxRecentPosts,
|
||||
self.server.translate,
|
||||
pageNumber, maxPostsInBlogsFeed,
|
||||
self.server.session,
|
||||
baseDir,
|
||||
self.server.cachedWebfingers,
|
||||
self.server.personCache,
|
||||
nickname,
|
||||
domain,
|
||||
port,
|
||||
inboxFeaturesFeed,
|
||||
self.server.allowDeletion,
|
||||
httpPrefix,
|
||||
self.server.projectVersion,
|
||||
self._isMinimal(nickname),
|
||||
self.server.YTReplacementDomain,
|
||||
self.server.showPublishedDateOnly,
|
||||
self.server.newswire,
|
||||
self.server.positiveVoting,
|
||||
self.server.showPublishAsIcon,
|
||||
fullWidthTimelineButtonHeader,
|
||||
self.server.iconsAsButtons,
|
||||
self.server.rssIconAtTop,
|
||||
self.server.publishButtonAtTop,
|
||||
authorized)
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('text/html', len(msg),
|
||||
cookie, callingDomain)
|
||||
self._write(msg)
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show blogs 2 done',
|
||||
'show news 2')
|
||||
else:
|
||||
# don't need authenticated fetch here because there is
|
||||
# already the authorization check
|
||||
msg = json.dumps(inboxFeaturesFeed,
|
||||
ensure_ascii=False)
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('application/json',
|
||||
len(msg),
|
||||
None, callingDomain)
|
||||
self._write(msg)
|
||||
self.server.GETbusy = False
|
||||
return True
|
||||
else:
|
||||
if debug:
|
||||
nickname = 'news'
|
||||
print('DEBUG: ' + nickname +
|
||||
' was not authorized to access ' + path)
|
||||
if path != '/tlfeatures':
|
||||
# not the features inbox
|
||||
if debug:
|
||||
print('DEBUG: GET access to features is unauthorized')
|
||||
self.send_response(405)
|
||||
self.end_headers()
|
||||
self.server.GETbusy = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def _showSharesTimeline(self, authorized: bool,
|
||||
callingDomain: str, path: str,
|
||||
baseDir: str, httpPrefix: str,
|
||||
|
@ -8730,11 +9061,16 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
"""Show the edit screen for a news post
|
||||
"""
|
||||
if '/users/' in path and '/editnewspost=' in path:
|
||||
postActor = 'news'
|
||||
if '?actor=' in path:
|
||||
postActor = path.split('?actor=')[1]
|
||||
if '?' in postActor:
|
||||
postActor = postActor.split('?')[0]
|
||||
postId = path.split('/editnewspost=')[1]
|
||||
if '?' in postId:
|
||||
postId = postId.split('?')[0]
|
||||
postUrl = httpPrefix + '://' + domainFull + \
|
||||
'/users/news/statuses/' + postId
|
||||
'/users/' + postActor + '/statuses/' + postId
|
||||
path = path.split('/editnewspost=')[0]
|
||||
msg = htmlEditNewsPost(self.server.cssCache,
|
||||
translate, baseDir,
|
||||
|
@ -8979,6 +9315,18 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'fonts', 'sharedInbox enabled')
|
||||
|
||||
if self.path == '/categories.xml':
|
||||
self._getHashtagCategoriesFeed(authorized,
|
||||
callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.port,
|
||||
self.server.proxyType,
|
||||
GETstartTime, GETtimings,
|
||||
self.server.debug)
|
||||
return
|
||||
|
||||
if self.path == '/newswire.xml':
|
||||
self._getNewswireFeed(authorized,
|
||||
callingDomain, self.path,
|
||||
|
@ -9178,11 +9526,11 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
actor = \
|
||||
self.server.httpPrefix + '://' + \
|
||||
self.server.domainFull + usersPath
|
||||
msg = htmlRemoveSharedItem(self.server.cssCache,
|
||||
self.server.translate,
|
||||
self.server.baseDir,
|
||||
actor, shareName,
|
||||
callingDomain).encode('utf-8')
|
||||
msg = htmlConfirmRemoveSharedItem(self.server.cssCache,
|
||||
self.server.translate,
|
||||
self.server.baseDir,
|
||||
actor, shareName,
|
||||
callingDomain).encode('utf-8')
|
||||
if not msg:
|
||||
if callingDomain.endswith('.onion') and \
|
||||
self.server.onionDomain:
|
||||
|
@ -9772,7 +10120,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
elif self.server.mediaInstance:
|
||||
self.path = '/users/' + nickname + '/tlmedia'
|
||||
else:
|
||||
self.path = '/users/' + nickname + '/tlnews'
|
||||
self.path = '/users/' + nickname + '/tlfeatures'
|
||||
|
||||
# search for a fediverse address, shared item or emoji
|
||||
# from the web interface by selecting search icon
|
||||
|
@ -9795,6 +10143,22 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
'search screen shown')
|
||||
return
|
||||
|
||||
# show a hashtag category from the search screen
|
||||
if htmlGET and '/category/' in self.path:
|
||||
msg = htmlSearchHashtagCategory(self.server.cssCache,
|
||||
self.server.translate,
|
||||
self.server.baseDir, self.path,
|
||||
self.server.domain)
|
||||
if msg:
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('text/html', len(msg), cookie, callingDomain)
|
||||
self._write(msg)
|
||||
self.server.GETbusy = False
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'hashtag category done',
|
||||
'hashtag category screen shown')
|
||||
return
|
||||
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'hashtag search done',
|
||||
'search screen shown done')
|
||||
|
@ -10527,6 +10891,23 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
cookie, self.server.debug):
|
||||
return
|
||||
|
||||
# get features (local blogs) for a given person
|
||||
if self.path.endswith('/tlfeatures') or \
|
||||
'/tlfeatures?page=' in self.path:
|
||||
if self._showFeaturesTimeline(authorized,
|
||||
callingDomain, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.port,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain,
|
||||
GETstartTime, GETtimings,
|
||||
self.server.proxyType,
|
||||
cookie, self.server.debug):
|
||||
return
|
||||
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings,
|
||||
'show blogs 2 done',
|
||||
'show news 2 done')
|
||||
|
@ -11111,6 +11492,13 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
replaceYouTube(postJsonObject,
|
||||
self.server.YTReplacementDomain)
|
||||
saveJson(postJsonObject, postFilename)
|
||||
# also save to the news actor
|
||||
if nickname != 'news':
|
||||
postFilename = \
|
||||
postFilename.replace('#users#' +
|
||||
nickname + '#',
|
||||
'#users#news#')
|
||||
saveJson(postJsonObject, postFilename)
|
||||
print('Edited blog post, resaved ' + postFilename)
|
||||
return 1
|
||||
else:
|
||||
|
@ -11759,6 +12147,20 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
|
||||
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 2)
|
||||
|
||||
if authorized and self.path.endswith('/sethashtagcategory'):
|
||||
self._setHashtagCategory(callingDomain, cookie,
|
||||
authorized, self.path,
|
||||
self.server.baseDir,
|
||||
self.server.httpPrefix,
|
||||
self.server.domain,
|
||||
self.server.domainFull,
|
||||
self.server.onionDomain,
|
||||
self.server.i2pDomain,
|
||||
self.server.debug,
|
||||
self.server.defaultTimeline,
|
||||
self.server.allowLocalNetworkAccess)
|
||||
return
|
||||
|
||||
# update of profile/avatar from web interface,
|
||||
# after selecting Edit button then Submit
|
||||
if authorized and self.path.endswith('/profiledata'):
|
||||
|
@ -12486,7 +12888,7 @@ def runDaemon(maxNewswirePosts: int,
|
|||
if blogsInstance:
|
||||
httpd.defaultTimeline = 'tlblogs'
|
||||
if newsInstance:
|
||||
httpd.defaultTimeline = 'tlnews'
|
||||
httpd.defaultTimeline = 'tlfeatures'
|
||||
|
||||
# load translations dictionary
|
||||
httpd.translate = {}
|
||||
|
@ -12579,6 +12981,9 @@ def runDaemon(maxNewswirePosts: int,
|
|||
# maximum size of individual RSS feed items, in K
|
||||
httpd.maxFeedItemSizeKb = maxFeedItemSizeKb
|
||||
|
||||
# maximum size of a hashtag category, in K
|
||||
httpd.maxCategoriesFeedItemSizeKb = 256
|
||||
|
||||
if registration == 'open':
|
||||
httpd.registration = True
|
||||
else:
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
:root {
|
||||
--main-bg-color: #282c37;
|
||||
--link-bg-color: #282c37;
|
||||
--title-color: #999;
|
||||
--dropdown-bg-color: #111;
|
||||
--dropdown-bg-color-hover: #333;
|
||||
--main-bg-color-reply: #212c37;
|
||||
|
@ -16,6 +17,7 @@
|
|||
--font-size-header: 18px;
|
||||
--font-color-header: #ccc;
|
||||
--font-size-button-mobile: 34px;
|
||||
--font-size-mobile: 50px;
|
||||
--font-size: 30px;
|
||||
--font-size2: 24px;
|
||||
--font-size3: 38px;
|
||||
|
@ -45,6 +47,7 @@
|
|||
--focus-color: white;
|
||||
--line-spacing: 130%;
|
||||
--header-font: 'Bedstead';
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -78,27 +81,33 @@ body, html {
|
|||
a, u {
|
||||
color: var(--main-fg-color);
|
||||
}
|
||||
|
||||
|
||||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
border: 2px solid var(--focus-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
}
|
||||
|
||||
.cwText {
|
||||
display: none;
|
||||
}
|
||||
|
@ -689,6 +698,11 @@ div.gallery img {
|
|||
font-size: var(--font-size4);
|
||||
font-family: "Times New Roman", Roman, serif;
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size);
|
||||
color: var(--title-color);
|
||||
}
|
||||
.galleryContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
|
@ -1041,6 +1055,11 @@ div.gallery img {
|
|||
font-size: var(--font-size3);
|
||||
font-family: "Times New Roman", Roman, serif;
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size-mobile);
|
||||
color: var(--title-color);
|
||||
}
|
||||
div.gallerytext {
|
||||
color: var(--gallery-text-color);
|
||||
font-size: var(--gallery-font-size-mobile);
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
--font-size-calendar-cell-mobile: 4rem;
|
||||
--calendar-header-font: 'Montserrat';
|
||||
--calendar-header-font-style: italic;
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -59,6 +60,7 @@ a:visited{
|
|||
z-index: 1;
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a:link {
|
||||
|
@ -67,6 +69,15 @@ a:link {
|
|||
z-index: 1;
|
||||
padding: 1rem;
|
||||
margin: -1rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
--follow-text-size2: 40px;
|
||||
--follow-text-entry-width: 90%;
|
||||
--focus-color: white;
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -66,13 +67,23 @@ a, u {
|
|||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
|
|
@ -154,13 +154,15 @@ a, u {
|
|||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
|
@ -213,6 +215,7 @@ a:focus {
|
|||
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size);
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--main-bg-color: #282c37;
|
||||
--login-bg-color: #282c37;
|
||||
--link-bg-color: #282c37;
|
||||
--main-fg-color: #dddddd;
|
||||
--login-fg-color: #dddddd;
|
||||
--main-link-color: #999;
|
||||
--main-visited-color: #888;
|
||||
--border-color: #505050;
|
||||
|
@ -22,6 +22,7 @@
|
|||
--focus-color: white;
|
||||
--line-spacing: 130%;
|
||||
--login-logo-width: 20%;
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -40,8 +41,8 @@
|
|||
}
|
||||
|
||||
body, html {
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
background-color: var(--login-bg-color);
|
||||
color: var(--login-fg-color);
|
||||
|
||||
background-image: url("/login-background.jpg");
|
||||
background-size: cover;
|
||||
|
@ -59,19 +60,29 @@ body, html {
|
|||
}
|
||||
|
||||
a, u {
|
||||
color: var(--main-fg-color);
|
||||
color: var(--login-fg-color);
|
||||
}
|
||||
|
||||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
@ -146,8 +157,8 @@ span.psw {
|
|||
|
||||
@media screen and (min-width: 400px) {
|
||||
body, html {
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
background-color: var(--login-bg-color);
|
||||
color: var(--login-fg-color);
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
max-width: 60%;
|
||||
|
@ -186,8 +197,8 @@ span.psw {
|
|||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body, html {
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
background-color: var(--login-bg-color);
|
||||
color: var(--login-fg-color);
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
max-width: 95%;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--main-bg-color: #282c37;
|
||||
--link-bg-color: #282c37;
|
||||
--main-fg-color: #dddddd;
|
||||
--options-bg-color: #282c37;
|
||||
--options-link-bg-color: transparent;
|
||||
--options-fg-color: #dddddd;
|
||||
--main-link-color: #999;
|
||||
--main-visited-color: #888;
|
||||
--border-color: #505050;
|
||||
|
@ -34,6 +34,7 @@
|
|||
--follow-text-entry-width: 90%;
|
||||
--focus-color: white;
|
||||
--petname-width-chars: 16ch;
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -58,8 +59,8 @@ body, html {
|
|||
-moz-background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-color: var(--main-bg-color);
|
||||
color: var(--main-fg-color);
|
||||
background-color: var(--options-bg-color);
|
||||
color: var(--options-fg-color);
|
||||
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
@ -68,19 +69,29 @@ body, html {
|
|||
}
|
||||
|
||||
a, u {
|
||||
color: var(--main-fg-color);
|
||||
color: var(--options-fg-color);
|
||||
}
|
||||
|
||||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
background: var(--options-link-bg-color);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
background: var(--options-link-bg-color);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
@ -90,7 +101,7 @@ a:focus {
|
|||
.follow {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: var(--main-bg-color);
|
||||
background-color: var(--options-bg-color);
|
||||
}
|
||||
|
||||
.followAvatar {
|
||||
|
@ -111,7 +122,7 @@ a:focus {
|
|||
.pgp {
|
||||
font-size: var(--font-size5);
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
background: var(--options-link-bg-color);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
|
@ -123,6 +134,7 @@ a:focus {
|
|||
}
|
||||
|
||||
.options img {
|
||||
background-color: var(--options-bg-color);
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
|
@ -132,7 +144,7 @@ a:focus {
|
|||
font-size: var(--font-size4);
|
||||
width: 90%;
|
||||
background-color: var(--text-entry-background);
|
||||
color: white;
|
||||
color: var(--text-entry-foreground);
|
||||
}
|
||||
.followText {
|
||||
font-size: var(--follow-text-size1);
|
||||
|
@ -209,7 +221,7 @@ a:focus {
|
|||
font-size: var(--font-size);
|
||||
width: 90%;
|
||||
background-color: var(--text-entry-background);
|
||||
color: white;
|
||||
color: var(--text-entry-foreground);
|
||||
}
|
||||
.followText {
|
||||
font-size: var(--follow-text-size2);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--timeline-icon-width: 50px;
|
||||
--timeline-icon-width-mobile: 100px;
|
||||
--header-bg-color: #282c37;
|
||||
--main-bg-color: #282c37;
|
||||
--post-bg-color: #282c37;
|
||||
|
@ -31,8 +33,9 @@
|
|||
--font-size-links: 18px;
|
||||
--font-size-publish-button: 18px;
|
||||
--font-size-newswire: 18px;
|
||||
--font-size-newswire-mobile: 40px;
|
||||
--font-size-newswire-mobile: 38px;
|
||||
--font-size-dropdown-header: 40px;
|
||||
--font-size-mobile: 50px;
|
||||
--font-size: 30px;
|
||||
--font-size2: 24px;
|
||||
--font-size3: 38px;
|
||||
|
@ -183,7 +186,7 @@ body, html {
|
|||
}
|
||||
|
||||
.postSeparatorImage img {
|
||||
background-color: var(--post-bg-color);
|
||||
background-color: transparent;
|
||||
padding-top: var(--post-separator-margin-top);
|
||||
padding-bottom: var(--post-separator-margin-bottom);
|
||||
width: var(--post-separator-width);
|
||||
|
@ -192,7 +195,7 @@ body, html {
|
|||
}
|
||||
|
||||
.postSeparatorImageLeft img {
|
||||
background-color: var(--column-left-color);
|
||||
background-color: transparent;
|
||||
padding-top: var(--post-separator-margin-top);
|
||||
padding-bottom: var(--post-separator-margin-bottom);
|
||||
width: var(--separator-width-left);
|
||||
|
@ -201,7 +204,7 @@ body, html {
|
|||
}
|
||||
|
||||
.postSeparatorImageRight img {
|
||||
background-color: var(--column-left-color);
|
||||
background-color: transparent;
|
||||
padding-top: var(--post-separator-margin-top);
|
||||
padding-bottom: var(--post-separator-margin-bottom);
|
||||
width: var(--separator-width-right);
|
||||
|
@ -254,25 +257,22 @@ blockquote p {
|
|||
border: 2px solid var(--focus-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
a, u {
|
||||
color: var(--main-fg-color);
|
||||
}
|
||||
|
||||
a:visited{
|
||||
a:visited {
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
|
@ -284,7 +284,7 @@ a:visited:hover {
|
|||
}
|
||||
|
||||
.buttonevent:hover {
|
||||
filter: brightness(var(----icon-brightness-change));
|
||||
filter: brightness(var(--icon-brightness-change));
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
@ -317,8 +317,6 @@ a:focus {
|
|||
.profileHeader * {
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-webkit-transition: all 0.25s ease;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.profileHeader img.profileBackground {
|
||||
|
@ -437,17 +435,7 @@ a:focus {
|
|||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.button span:after {
|
||||
font-family: var(--header-font);
|
||||
content: '\00bb';
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
right: -20px;
|
||||
transition: 0.5s;
|
||||
transition: 1.0s;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
|
@ -465,17 +453,7 @@ a:focus {
|
|||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.buttonselected span:after {
|
||||
font-family: var(--header-font);
|
||||
content: '\00bb';
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
right: -20px;
|
||||
transition: 0.5s;
|
||||
transition: 1.0s;
|
||||
}
|
||||
|
||||
.buttonselected:hover {
|
||||
|
@ -631,7 +609,7 @@ a:focus {
|
|||
}
|
||||
|
||||
.containericons img:hover {
|
||||
filter: brightness(var(----icon-brightness-change));
|
||||
filter: brightness(var(--icon-brightness-change));
|
||||
}
|
||||
|
||||
.post-title {
|
||||
|
@ -940,14 +918,6 @@ div.gallery img {
|
|||
li { list-style:none;}
|
||||
/***********BUTTON CODE ******************************************************/
|
||||
|
||||
a, button, input:focus, input[type='button'], input[type='reset'], input[type='submit'], textarea:focus, .button {
|
||||
-webkit-transition: all 0.1s ease-in-out;
|
||||
-moz-transition: all 0.1s ease-in-out;
|
||||
-ms-transition: all 0.1s ease-in-out;
|
||||
-o-transition: all 0.1s ease-in-out;
|
||||
transition: all 0.1s ease-in-out;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn {
|
||||
margin: -3px 0 0 0;
|
||||
}
|
||||
|
@ -971,6 +941,11 @@ div.container {
|
|||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size);
|
||||
color: var(--title-color);
|
||||
}
|
||||
.containerHeader {
|
||||
border: var(--border-width-header) solid var(--border-color);
|
||||
background-color: var(--header-bg-color);
|
||||
|
@ -1067,12 +1042,14 @@ div.container {
|
|||
float: left;
|
||||
width: var(--column-left-width);
|
||||
}
|
||||
.col-left img.leftColEditImage:hover {
|
||||
filter: brightness(var(--icon-brightness-change));
|
||||
}
|
||||
.col-left img.leftColEdit {
|
||||
background: var(--column-left-color);
|
||||
width: var(--column-left-icon-size);
|
||||
}
|
||||
.col-left img.leftColEditImage {
|
||||
background: var(--column-left-color);
|
||||
width: var(--column-left-icon-size);
|
||||
float: right;
|
||||
}
|
||||
|
@ -1112,12 +1089,15 @@ div.container {
|
|||
width: var(--column-right-width);
|
||||
overflow: hidden;
|
||||
}
|
||||
.col-right img.rightColEditImage:hover {
|
||||
filter: brightness(var(--icon-brightness-change));
|
||||
}
|
||||
.col-right img.rightColEdit {
|
||||
background: var(--column-left-color);
|
||||
background: transparent;
|
||||
width: var(--column-right-icon-size);
|
||||
}
|
||||
.col-right img.rightColEditImage {
|
||||
background: var(--column-left-color);
|
||||
background: transparent;
|
||||
width: var(--column-right-icon-size);
|
||||
float: right;
|
||||
}
|
||||
|
@ -1212,7 +1192,7 @@ div.container {
|
|||
margin-right: 0px;
|
||||
padding: 0px 0px;
|
||||
margin: 0px 0px;
|
||||
width: 50px;
|
||||
width: var(--timeline-icon-width);
|
||||
}
|
||||
.containerHeader img.timelineicon {
|
||||
float: var(--icons-side);
|
||||
|
@ -1220,7 +1200,7 @@ div.container {
|
|||
margin-right:0;
|
||||
padding: 0 0;
|
||||
margin: 0 0;
|
||||
width: 50px;
|
||||
width: var(--timeline-icon-width);
|
||||
}
|
||||
.container img.emojiheader {
|
||||
float: none;
|
||||
|
@ -1612,6 +1592,11 @@ div.container {
|
|||
font-size: var(--font-size);
|
||||
line-height: var(--line-spacing);
|
||||
}
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size-mobile);
|
||||
color: var(--title-color);
|
||||
}
|
||||
.containerHeader {
|
||||
border: var(--border-width-header) solid var(--border-color);
|
||||
background-color: var(--header-bg-color);
|
||||
|
@ -1832,7 +1817,7 @@ div.container {
|
|||
margin-right: 0px;
|
||||
padding: 0px 0px;
|
||||
margin: 0px 0px;
|
||||
width: 100px;
|
||||
width: var(--timeline-icon-width-mobile);
|
||||
}
|
||||
.containerHeader img.timelineicon {
|
||||
float: var(--icons-side);
|
||||
|
@ -1840,7 +1825,7 @@ div.container {
|
|||
margin-right:0;
|
||||
padding: 0 0;
|
||||
margin: 0 0;
|
||||
width: 100px;
|
||||
width: var(--timeline-icon-width-mobile);
|
||||
}
|
||||
.container img.emojiheader {
|
||||
float: none;
|
||||
|
|
|
@ -66,6 +66,7 @@ body, html {
|
|||
|
||||
h1 {
|
||||
font-family: var(--header-font);
|
||||
font-size: var(--font-size);
|
||||
color: var(--title-color);
|
||||
}
|
||||
|
||||
|
@ -76,13 +77,15 @@ a, u {
|
|||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
|
@ -206,6 +209,9 @@ input[type=text] {
|
|||
}
|
||||
|
||||
@media screen and (min-width: 400px) {
|
||||
details {
|
||||
font-size: var(--hashtag-size1);
|
||||
}
|
||||
.domainHistogram {
|
||||
border: 0;
|
||||
font-size: var(--hashtag-size1);
|
||||
|
@ -262,6 +268,9 @@ input[type=text] {
|
|||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
details {
|
||||
font-size: var(--hashtag-size2);
|
||||
}
|
||||
.domainHistogram {
|
||||
border: 0;
|
||||
font-size: var(--hashtag-size2);
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
--button-background: #999;
|
||||
--button-selected: #666;
|
||||
--focus-color: white;
|
||||
--main-link-color-hover: #bbb;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
@ -56,13 +57,23 @@ a, u {
|
|||
a:visited{
|
||||
color: var(--main-visited-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: var(--main-link-color);
|
||||
background: var(--link-bg-color);
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:link:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:visited:hover {
|
||||
color: var(--main-link-color-hover);
|
||||
}
|
||||
|
||||
a:focus {
|
||||
|
|
13
epicyon.py
|
@ -689,12 +689,6 @@ if args.json:
|
|||
pprint(testJson)
|
||||
sys.exit()
|
||||
|
||||
if args.rss:
|
||||
session = createSession(None)
|
||||
testRSS = getRSS(session, args.rss)
|
||||
pprint(testRSS)
|
||||
sys.exit()
|
||||
|
||||
# create cache for actors
|
||||
if not os.path.isdir(baseDir + '/cache'):
|
||||
os.mkdir(baseDir + '/cache')
|
||||
|
@ -756,6 +750,13 @@ if args.domain:
|
|||
domain = args.domain
|
||||
setConfigParam(baseDir, 'domain', domain)
|
||||
|
||||
if args.rss:
|
||||
session = createSession(None)
|
||||
testRSS = getRSS(baseDir, domain, session, args.rss,
|
||||
False, False, 1000, 1000, 1000, 1000)
|
||||
pprint(testRSS)
|
||||
sys.exit()
|
||||
|
||||
if args.onion:
|
||||
if not args.onion.endswith('.onion'):
|
||||
print(args.onion + ' does not look like an onion domain')
|
||||
|
|
25
inbox.py
|
@ -56,8 +56,8 @@ from posts import isMuted
|
|||
from posts import isImageMedia
|
||||
from posts import sendSignedJson
|
||||
from posts import sendToFollowersThread
|
||||
from webapp import individualPostAsHtml
|
||||
from webapp import getIconsWebPath
|
||||
from webapp_utils import getIconsWebPath
|
||||
from webapp_post import individualPostAsHtml
|
||||
from question import questionUpdateVotes
|
||||
from media import replaceYouTube
|
||||
from git import isGitPatch
|
||||
|
@ -1251,18 +1251,31 @@ def receiveDelete(session, handle: str, isGroup: bool, baseDir: str,
|
|||
# if this post in the outbox of the person?
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
removeModerationPostFromIndex(baseDir, messageId, debug)
|
||||
postFilename = locatePost(baseDir, handle.split('@')[0],
|
||||
handle.split('@')[1], messageId)
|
||||
handleNickname = handle.split('@')[0]
|
||||
handleDomain = handle.split('@')[1]
|
||||
postFilename = locatePost(baseDir, handleNickname,
|
||||
handleDomain, messageId)
|
||||
if not postFilename:
|
||||
if debug:
|
||||
print('DEBUG: delete post not found in inbox or outbox')
|
||||
print(messageId)
|
||||
return True
|
||||
deletePost(baseDir, httpPrefix, handle.split('@')[0],
|
||||
handle.split('@')[1], postFilename, debug,
|
||||
deletePost(baseDir, httpPrefix, handleNickname,
|
||||
handleDomain, postFilename, debug,
|
||||
recentPostsCache)
|
||||
if debug:
|
||||
print('DEBUG: post deleted - ' + postFilename)
|
||||
|
||||
# also delete any local blogs saved to the news actor
|
||||
if handleNickname != 'news' and handleDomain == domainFull:
|
||||
postFilename = locatePost(baseDir, 'news',
|
||||
handleDomain, messageId)
|
||||
if postFilename:
|
||||
deletePost(baseDir, httpPrefix, 'news',
|
||||
handleDomain, postFilename, debug,
|
||||
recentPostsCache)
|
||||
if debug:
|
||||
print('DEBUG: blog post deleted - ' + postFilename)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -717,7 +717,8 @@ def runNewswireDaemon(baseDir: str, httpd,
|
|||
httpd.maxNewswireFeedSizeKb,
|
||||
httpd.maxTags,
|
||||
httpd.maxFeedItemSizeKb,
|
||||
httpd.maxNewswirePosts)
|
||||
httpd.maxNewswirePosts,
|
||||
httpd.maxCategoriesFeedItemSizeKb)
|
||||
|
||||
if not httpd.newswire:
|
||||
if os.path.isfile(newswireStateFilename):
|
||||
|
|
89
newswire.py
|
@ -14,6 +14,7 @@ from datetime import datetime
|
|||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from collections import OrderedDict
|
||||
from utils import setHashtagCategory
|
||||
from utils import firstParagraphFromString
|
||||
from utils import isPublicPost
|
||||
from utils import locatePost
|
||||
|
@ -122,7 +123,7 @@ def addNewswireDictEntry(baseDir: str, domain: str,
|
|||
|
||||
# check that no tags are blocked
|
||||
for tag in postTags:
|
||||
if isBlockedHashtag(baseDir, tag.replace('#', '')):
|
||||
if isBlockedHashtag(baseDir, tag):
|
||||
return
|
||||
|
||||
newswire[dateStr] = [
|
||||
|
@ -202,19 +203,64 @@ def parseFeedDate(pubDate: str) -> str:
|
|||
return pubDateStr
|
||||
|
||||
|
||||
def xml2StrToHashtagCategories(baseDir: str, domain: str, xmlStr: str,
|
||||
maxCategoriesFeedItemSizeKb: int) -> None:
|
||||
"""Updates hashtag categories based upon an rss feed
|
||||
"""
|
||||
rssItems = xmlStr.split('<item>')
|
||||
maxBytes = maxCategoriesFeedItemSizeKb * 1024
|
||||
for rssItem in rssItems:
|
||||
if not rssItem:
|
||||
continue
|
||||
if len(rssItem) > maxBytes:
|
||||
print('WARN: rss categories feed item is too big')
|
||||
continue
|
||||
if '<title>' not in rssItem:
|
||||
continue
|
||||
if '</title>' not in rssItem:
|
||||
continue
|
||||
if '<description>' not in rssItem:
|
||||
continue
|
||||
if '</description>' not in rssItem:
|
||||
continue
|
||||
categoryStr = rssItem.split('<title>')[1]
|
||||
categoryStr = categoryStr.split('</title>')[0].strip()
|
||||
if not categoryStr:
|
||||
continue
|
||||
if 'CDATA' in categoryStr:
|
||||
continue
|
||||
hashtagListStr = rssItem.split('<description>')[1]
|
||||
hashtagListStr = hashtagListStr.split('</description>')[0].strip()
|
||||
if not hashtagListStr:
|
||||
continue
|
||||
if 'CDATA' in hashtagListStr:
|
||||
continue
|
||||
hashtagList = hashtagListStr.split(' ')
|
||||
if not isBlockedHashtag(baseDir, categoryStr):
|
||||
for hashtag in hashtagList:
|
||||
setHashtagCategory(baseDir, hashtag, categoryStr)
|
||||
|
||||
|
||||
def xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||
moderated: bool, mirrored: bool,
|
||||
maxPostsPerSource: int,
|
||||
maxFeedItemSizeKb: int) -> {}:
|
||||
maxFeedItemSizeKb: int,
|
||||
maxCategoriesFeedItemSizeKb: int) -> {}:
|
||||
"""Converts an xml 2.0 string to a dictionary
|
||||
"""
|
||||
if '<item>' not in xmlStr:
|
||||
return {}
|
||||
result = {}
|
||||
if '<title>#categories</title>' in xmlStr:
|
||||
xml2StrToHashtagCategories(baseDir, domain, xmlStr,
|
||||
maxCategoriesFeedItemSizeKb)
|
||||
return {}
|
||||
rssItems = xmlStr.split('<item>')
|
||||
postCtr = 0
|
||||
maxBytes = maxFeedItemSizeKb * 1024
|
||||
for rssItem in rssItems:
|
||||
if not rssItem:
|
||||
continue
|
||||
if len(rssItem) > maxBytes:
|
||||
print('WARN: rss feed item is too big')
|
||||
continue
|
||||
|
@ -266,6 +312,8 @@ def xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
postCtr += 1
|
||||
if postCtr >= maxPostsPerSource:
|
||||
break
|
||||
if postCtr > 0:
|
||||
print('Added ' + str(postCtr) + ' rss feed items to newswire')
|
||||
return result
|
||||
|
||||
|
||||
|
@ -282,6 +330,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
postCtr = 0
|
||||
maxBytes = maxFeedItemSizeKb * 1024
|
||||
for atomItem in atomItems:
|
||||
if not atomItem:
|
||||
continue
|
||||
if len(atomItem) > maxBytes:
|
||||
print('WARN: atom feed item is too big')
|
||||
continue
|
||||
|
@ -333,6 +383,8 @@ def atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
postCtr += 1
|
||||
if postCtr >= maxPostsPerSource:
|
||||
break
|
||||
if postCtr > 0:
|
||||
print('Added ' + str(postCtr) + ' atom feed items to newswire')
|
||||
return result
|
||||
|
||||
|
||||
|
@ -351,7 +403,10 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
postCtr = 0
|
||||
maxBytes = maxFeedItemSizeKb * 1024
|
||||
for atomItem in atomItems:
|
||||
print('YouTube feed item: ' + atomItem)
|
||||
if not atomItem:
|
||||
continue
|
||||
if not atomItem.strip():
|
||||
continue
|
||||
if len(atomItem) > maxBytes:
|
||||
print('WARN: atom feed item is too big')
|
||||
continue
|
||||
|
@ -359,9 +414,9 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
continue
|
||||
if '</title>' not in atomItem:
|
||||
continue
|
||||
if '<updated>' not in atomItem:
|
||||
if '<published>' not in atomItem:
|
||||
continue
|
||||
if '</updated>' not in atomItem:
|
||||
if '</published>' not in atomItem:
|
||||
continue
|
||||
if '<yt:videoId>' not in atomItem:
|
||||
continue
|
||||
|
@ -382,8 +437,8 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
link = atomItem.split('<yt:videoId>')[1]
|
||||
link = link.split('</yt:videoId>')[0]
|
||||
link = 'https://www.youtube.com/watch?v=' + link.strip()
|
||||
pubDate = atomItem.split('<updated>')[1]
|
||||
pubDate = pubDate.split('</updated>')[0]
|
||||
pubDate = atomItem.split('<published>')[1]
|
||||
pubDate = pubDate.split('</published>')[0]
|
||||
|
||||
pubDateStr = parseFeedDate(pubDate)
|
||||
if pubDateStr:
|
||||
|
@ -397,13 +452,16 @@ def atomFeedYTToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
postCtr += 1
|
||||
if postCtr >= maxPostsPerSource:
|
||||
break
|
||||
if postCtr > 0:
|
||||
print('Added ' + str(postCtr) + ' YouTube feed items to newswire')
|
||||
return result
|
||||
|
||||
|
||||
def xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
|
||||
moderated: bool, mirrored: bool,
|
||||
maxPostsPerSource: int,
|
||||
maxFeedItemSizeKb: int) -> {}:
|
||||
maxFeedItemSizeKb: int,
|
||||
maxCategoriesFeedItemSizeKb: int) -> {}:
|
||||
"""Converts an xml string to a dictionary
|
||||
"""
|
||||
if '<yt:videoId>' in xmlStr and '<yt:channelId>' in xmlStr:
|
||||
|
@ -414,7 +472,8 @@ def xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
|
|||
elif 'rss version="2.0"' in xmlStr:
|
||||
return xml2StrToDict(baseDir, domain,
|
||||
xmlStr, moderated, mirrored,
|
||||
maxPostsPerSource, maxFeedItemSizeKb)
|
||||
maxPostsPerSource, maxFeedItemSizeKb,
|
||||
maxCategoriesFeedItemSizeKb)
|
||||
elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr:
|
||||
return atomFeedToDict(baseDir, domain,
|
||||
xmlStr, moderated, mirrored,
|
||||
|
@ -437,7 +496,8 @@ def YTchannelToAtomFeed(url: str) -> str:
|
|||
def getRSS(baseDir: str, domain: str, session, url: str,
|
||||
moderated: bool, mirrored: bool,
|
||||
maxPostsPerSource: int, maxFeedSizeKb: int,
|
||||
maxFeedItemSizeKb: int) -> {}:
|
||||
maxFeedItemSizeKb: int,
|
||||
maxCategoriesFeedItemSizeKb: int) -> {}:
|
||||
"""Returns an RSS url as a dict
|
||||
"""
|
||||
if not isinstance(url, str):
|
||||
|
@ -467,7 +527,8 @@ def getRSS(baseDir: str, domain: str, session, url: str,
|
|||
return xmlStrToDict(baseDir, domain, result.text,
|
||||
moderated, mirrored,
|
||||
maxPostsPerSource,
|
||||
maxFeedItemSizeKb)
|
||||
maxFeedItemSizeKb,
|
||||
maxCategoriesFeedItemSizeKb)
|
||||
else:
|
||||
print('WARN: feed is too large, ' +
|
||||
'or contains invalid characters: ' + url)
|
||||
|
@ -701,7 +762,8 @@ def addBlogsToNewswire(baseDir: str, domain: str, newswire: {},
|
|||
def getDictFromNewswire(session, baseDir: str, domain: str,
|
||||
maxPostsPerSource: int, maxFeedSizeKb: int,
|
||||
maxTags: int, maxFeedItemSizeKb: int,
|
||||
maxNewswirePosts: int) -> {}:
|
||||
maxNewswirePosts: int,
|
||||
maxCategoriesFeedItemSizeKb: int) -> {}:
|
||||
"""Gets rss feeds as a dictionary from newswire file
|
||||
"""
|
||||
subscriptionsFilename = baseDir + '/accounts/newswire.txt'
|
||||
|
@ -741,7 +803,8 @@ def getDictFromNewswire(session, baseDir: str, domain: str,
|
|||
itemsList = getRSS(baseDir, domain, session, url,
|
||||
moderated, mirrored,
|
||||
maxPostsPerSource, maxFeedSizeKb,
|
||||
maxFeedItemSizeKb)
|
||||
maxFeedItemSizeKb,
|
||||
maxCategoriesFeedItemSizeKb)
|
||||
if itemsList:
|
||||
for dateStr, item in itemsList.items():
|
||||
result[dateStr] = item
|
||||
|
|
60
outbox.py
|
@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
|
|||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
from shutil import copyfile
|
||||
from session import createSession
|
||||
from auth import createPassword
|
||||
from posts import outboxMessageCreateWrap
|
||||
|
@ -116,24 +117,23 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
fileExtension = 'png'
|
||||
mediaTypeStr = \
|
||||
attach['mediaType']
|
||||
if mediaTypeStr.endswith('jpeg'):
|
||||
fileExtension = 'jpg'
|
||||
elif mediaTypeStr.endswith('gif'):
|
||||
fileExtension = 'gif'
|
||||
elif mediaTypeStr.endswith('webp'):
|
||||
fileExtension = 'webp'
|
||||
elif mediaTypeStr.endswith('avif'):
|
||||
fileExtension = 'avif'
|
||||
elif mediaTypeStr.endswith('audio/mpeg'):
|
||||
fileExtension = 'mp3'
|
||||
elif mediaTypeStr.endswith('ogg'):
|
||||
fileExtension = 'ogg'
|
||||
elif mediaTypeStr.endswith('mp4'):
|
||||
fileExtension = 'mp4'
|
||||
elif mediaTypeStr.endswith('webm'):
|
||||
fileExtension = 'webm'
|
||||
elif mediaTypeStr.endswith('ogv'):
|
||||
fileExtension = 'ogv'
|
||||
|
||||
extensions = {
|
||||
"jpeg": "jpg",
|
||||
"gif": "gif",
|
||||
"webp": "webp",
|
||||
"avif": "avif",
|
||||
"audio/mpeg": "mp3",
|
||||
"ogg": "ogg",
|
||||
"mp4": "mp4",
|
||||
"webm": "webm",
|
||||
"ogv": "ogv"
|
||||
}
|
||||
for matchExt, ext in extensions.items():
|
||||
if mediaTypeStr.endswith(matchExt):
|
||||
fileExtension = ext
|
||||
break
|
||||
|
||||
mediaDir = \
|
||||
baseDir + '/accounts/' + \
|
||||
postToNickname + '@' + domain
|
||||
|
@ -188,11 +188,31 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
savePostToBox(baseDir,
|
||||
httpPrefix,
|
||||
postId,
|
||||
postToNickname,
|
||||
domainFull, messageJson, outboxName)
|
||||
postToNickname, domainFull,
|
||||
messageJson, outboxName)
|
||||
if not savedFilename:
|
||||
print('WARN: post not saved to outbox ' + outboxName)
|
||||
return False
|
||||
|
||||
# save all instance blogs to the news actor
|
||||
if postToNickname != 'news' and outboxName == 'tlblogs':
|
||||
if '/' in savedFilename:
|
||||
savedPostId = savedFilename.split('/')[-1]
|
||||
blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs'
|
||||
if not os.path.isdir(blogsDir):
|
||||
os.mkdir(blogsDir)
|
||||
copyfile(savedFilename, blogsDir + '/' + savedPostId)
|
||||
inboxUpdateIndex('tlblogs', baseDir,
|
||||
'news@' + domain,
|
||||
savedFilename, debug)
|
||||
|
||||
# clear the citations file if it exists
|
||||
citationsFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
postToNickname + '@' + domain + '/.citations.txt'
|
||||
if os.path.isfile(citationsFilename):
|
||||
os.remove(citationsFilename)
|
||||
|
||||
if messageJson['type'] == 'Create' or \
|
||||
messageJson['type'] == 'Question' or \
|
||||
messageJson['type'] == 'Note' or \
|
||||
|
|
69
person.py
|
@ -25,6 +25,7 @@ from posts import createRepliesTimeline
|
|||
from posts import createMediaTimeline
|
||||
from posts import createNewsTimeline
|
||||
from posts import createBlogsTimeline
|
||||
from posts import createFeaturesTimeline
|
||||
from posts import createBookmarksTimeline
|
||||
from posts import createEventsTimeline
|
||||
from posts import createInbox
|
||||
|
@ -236,7 +237,6 @@ def createPersonBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
elif nickname == 'news':
|
||||
personUrl = httpPrefix + '://' + domain + \
|
||||
'/about/more?news_actor=true'
|
||||
personName = originalDomain
|
||||
approveFollowers = True
|
||||
personType = 'Application'
|
||||
|
||||
|
@ -437,10 +437,16 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
|
|||
|
||||
# If a config.json file doesn't exist then don't decrement
|
||||
# remaining registrations counter
|
||||
remainingConfigExists = getConfigParam(baseDir, 'registrationsRemaining')
|
||||
if remainingConfigExists:
|
||||
registrationsRemaining = int(remainingConfigExists)
|
||||
if registrationsRemaining <= 0:
|
||||
if nickname != 'news':
|
||||
remainingConfigExists = \
|
||||
getConfigParam(baseDir, 'registrationsRemaining')
|
||||
if remainingConfigExists:
|
||||
registrationsRemaining = int(remainingConfigExists)
|
||||
if registrationsRemaining <= 0:
|
||||
return None, None, None, None
|
||||
else:
|
||||
if os.path.isdir(baseDir + '/accounts/news@' + domain):
|
||||
# news account already exists
|
||||
return None, None, None, None
|
||||
|
||||
(privateKeyPem, publicKeyPem,
|
||||
|
@ -451,12 +457,13 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
|
|||
manualFollowerApproval,
|
||||
password)
|
||||
if not getConfigParam(baseDir, 'admin'):
|
||||
# print(nickname+' becomes the instance admin and a moderator')
|
||||
setConfigParam(baseDir, 'admin', nickname)
|
||||
setRole(baseDir, nickname, domain, 'instance', 'admin')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'moderator')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'editor')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'delegator')
|
||||
if nickname != 'news':
|
||||
# print(nickname+' becomes the instance admin and a moderator')
|
||||
setConfigParam(baseDir, 'admin', nickname)
|
||||
setRole(baseDir, nickname, domain, 'instance', 'admin')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'moderator')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'editor')
|
||||
setRole(baseDir, nickname, domain, 'instance', 'delegator')
|
||||
|
||||
if not os.path.isdir(baseDir + '/accounts'):
|
||||
os.mkdir(baseDir + '/accounts')
|
||||
|
@ -470,22 +477,33 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
|
|||
fFile.write('\n')
|
||||
|
||||
# notify when posts are liked
|
||||
notifyLikesFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/.notifyLikes'
|
||||
with open(notifyLikesFilename, 'w+') as nFile:
|
||||
nFile.write('\n')
|
||||
if nickname != 'news':
|
||||
notifyLikesFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/.notifyLikes'
|
||||
with open(notifyLikesFilename, 'w+') as nFile:
|
||||
nFile.write('\n')
|
||||
|
||||
if os.path.isfile(baseDir + '/img/default-avatar.png'):
|
||||
copyfile(baseDir + '/img/default-avatar.png',
|
||||
baseDir + '/accounts/' + nickname + '@' + domain +
|
||||
'/avatar.png')
|
||||
theme = getConfigParam(baseDir, 'theme')
|
||||
if not theme:
|
||||
theme = 'default'
|
||||
|
||||
if nickname != 'news':
|
||||
if os.path.isfile(baseDir + '/img/default-avatar.png'):
|
||||
copyfile(baseDir + '/img/default-avatar.png',
|
||||
baseDir + '/accounts/' + nickname + '@' + domain +
|
||||
'/avatar.png')
|
||||
else:
|
||||
newsAvatar = baseDir + '/theme/' + theme + '/icons/avatar_news.png'
|
||||
if os.path.isfile(newsAvatar):
|
||||
copyfile(newsAvatar,
|
||||
baseDir + '/accounts/' + nickname + '@' + domain +
|
||||
'/avatar.png')
|
||||
|
||||
defaultProfileImageFilename = baseDir + '/theme/default/image.png'
|
||||
if theme:
|
||||
if os.path.isfile(baseDir + '/theme/' + theme + '/image.png'):
|
||||
defaultBannerFilename = baseDir + '/theme/' + theme + '/image.png'
|
||||
defaultProfileImageFilename = \
|
||||
baseDir + '/theme/' + theme + '/image.png'
|
||||
if os.path.isfile(defaultProfileImageFilename):
|
||||
copyfile(defaultProfileImageFilename, baseDir +
|
||||
'/accounts/' + nickname + '@' + domain + '/image.png')
|
||||
|
@ -496,7 +514,7 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
|
|||
if os.path.isfile(defaultBannerFilename):
|
||||
copyfile(defaultBannerFilename, baseDir + '/accounts/' +
|
||||
nickname + '@' + domain + '/banner.png')
|
||||
if remainingConfigExists:
|
||||
if nickname != 'news' and remainingConfigExists:
|
||||
registrationsRemaining -= 1
|
||||
setConfigParam(baseDir, 'registrationsRemaining',
|
||||
str(registrationsRemaining))
|
||||
|
@ -516,8 +534,8 @@ def createNewsInbox(baseDir: str, domain: str, port: int,
|
|||
httpPrefix: str) -> (str, str, {}, {}):
|
||||
"""Generates the news inbox
|
||||
"""
|
||||
return createPersonBase(baseDir, 'news', domain, port, httpPrefix,
|
||||
True, True, None)
|
||||
return createPerson(baseDir, 'news', domain, port,
|
||||
httpPrefix, True, True, None)
|
||||
|
||||
|
||||
def personUpgradeActor(baseDir: str, personJson: {},
|
||||
|
@ -611,6 +629,7 @@ def personBoxJson(recentPostsCache: {},
|
|||
if boxname != 'inbox' and boxname != 'dm' and \
|
||||
boxname != 'tlreplies' and boxname != 'tlmedia' and \
|
||||
boxname != 'tlblogs' and boxname != 'tlnews' and \
|
||||
boxname != 'tlfeatures' and \
|
||||
boxname != 'outbox' and boxname != 'moderation' and \
|
||||
boxname != 'tlbookmarks' and boxname != 'bookmarks' and \
|
||||
boxname != 'tlevents':
|
||||
|
@ -683,6 +702,10 @@ def personBoxJson(recentPostsCache: {},
|
|||
httpPrefix, noOfItems, headerOnly,
|
||||
newswireVotesThreshold, positiveVoting,
|
||||
votingTimeMins, pageNumber)
|
||||
elif boxname == 'tlfeatures':
|
||||
return createFeaturesTimeline(session, baseDir, nickname, domain, port,
|
||||
httpPrefix, noOfItems, headerOnly,
|
||||
pageNumber)
|
||||
elif boxname == 'tlblogs':
|
||||
return createBlogsTimeline(session, baseDir, nickname, domain, port,
|
||||
httpPrefix, noOfItems, headerOnly,
|
||||
|
|
82
posts.py
|
@ -92,34 +92,6 @@ def isModerator(baseDir: str, nickname: str) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def isEditor(baseDir: str, nickname: str) -> bool:
|
||||
"""Returns true if the given nickname is an editor
|
||||
"""
|
||||
editorsFile = baseDir + '/accounts/editors.txt'
|
||||
|
||||
if not os.path.isfile(editorsFile):
|
||||
adminName = getConfigParam(baseDir, 'admin')
|
||||
if not adminName:
|
||||
return False
|
||||
if adminName == nickname:
|
||||
return True
|
||||
return False
|
||||
|
||||
with open(editorsFile, "r") as f:
|
||||
lines = f.readlines()
|
||||
if len(lines) == 0:
|
||||
adminName = getConfigParam(baseDir, 'admin')
|
||||
if not adminName:
|
||||
return False
|
||||
if adminName == nickname:
|
||||
return True
|
||||
for editor in lines:
|
||||
editor = editor.strip('\n').strip('\r')
|
||||
if editor == nickname:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def noOfFollowersOnDomain(baseDir: str, handle: str,
|
||||
domain: str, followFile='followers.txt') -> int:
|
||||
"""Returns the number of followers of the given handle from the given domain
|
||||
|
@ -582,6 +554,7 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str,
|
|||
|
||||
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
|
||||
filename = boxDir + '/' + postId.replace('/', '#') + '.json'
|
||||
|
||||
saveJson(postJsonObject, filename)
|
||||
return filename
|
||||
|
||||
|
@ -2587,6 +2560,15 @@ def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
|
|||
0, False, 0, pageNumber)
|
||||
|
||||
|
||||
def createFeaturesTimeline(session, baseDir: str, nickname: str, domain: str,
|
||||
port: int, httpPrefix: str, itemsPerPage: int,
|
||||
headerOnly: bool, pageNumber=None) -> {}:
|
||||
return createBoxIndexed({}, session, baseDir, 'tlfeatures', nickname,
|
||||
domain, port, httpPrefix,
|
||||
itemsPerPage, headerOnly, True,
|
||||
0, False, 0, pageNumber)
|
||||
|
||||
|
||||
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
|
||||
port: int, httpPrefix: str, itemsPerPage: int,
|
||||
headerOnly: bool, pageNumber=None) -> {}:
|
||||
|
@ -2879,7 +2861,9 @@ def addPostStringToTimeline(postStr: str, boxname: str,
|
|||
elif boxname == 'tlreplies':
|
||||
if boxActor not in postStr:
|
||||
return False
|
||||
elif boxname == 'tlblogs' or boxname == 'tlnews':
|
||||
elif (boxname == 'tlblogs' or
|
||||
boxname == 'tlnews' or
|
||||
boxname == 'tlfeatures'):
|
||||
if '"Create"' not in postStr:
|
||||
return False
|
||||
if '"Article"' not in postStr:
|
||||
|
@ -2900,6 +2884,13 @@ def addPostToTimeline(filePath: str, boxname: str,
|
|||
"""
|
||||
with open(filePath, 'r') as postFile:
|
||||
postStr = postFile.read()
|
||||
|
||||
if filePath.endswith('.json'):
|
||||
repliesFilename = filePath.replace('.json', '.replies')
|
||||
if os.path.isfile(repliesFilename):
|
||||
# append a replies identifier, which will later be removed
|
||||
postStr += '<hasReplies>'
|
||||
|
||||
return addPostStringToTimeline(postStr, boxname, postsInBox, boxActor)
|
||||
return False
|
||||
|
||||
|
@ -2918,6 +2909,7 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
if boxname != 'inbox' and boxname != 'dm' and \
|
||||
boxname != 'tlreplies' and boxname != 'tlmedia' and \
|
||||
boxname != 'tlblogs' and boxname != 'tlnews' and \
|
||||
boxname != 'tlfeatures' and \
|
||||
boxname != 'outbox' and boxname != 'tlbookmarks' and \
|
||||
boxname != 'bookmarks' and \
|
||||
boxname != 'tlevents':
|
||||
|
@ -2926,9 +2918,14 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
# bookmarks and events timelines are like the inbox
|
||||
# but have their own separate index
|
||||
indexBoxName = boxname
|
||||
timelineNickname = nickname
|
||||
if boxname == "tlbookmarks":
|
||||
boxname = "bookmarks"
|
||||
indexBoxName = boxname
|
||||
elif boxname == "tlfeatures":
|
||||
boxname = "tlblogs"
|
||||
indexBoxName = boxname
|
||||
timelineNickname = 'news'
|
||||
|
||||
if port:
|
||||
if port != 80 and port != 443:
|
||||
|
@ -2966,7 +2963,7 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
postsInBox = []
|
||||
|
||||
indexFilename = \
|
||||
baseDir + '/accounts/' + nickname + '@' + domain + \
|
||||
baseDir + '/accounts/' + timelineNickname + '@' + domain + \
|
||||
'/' + indexBoxName + '.index'
|
||||
postsCtr = 0
|
||||
if os.path.isfile(indexFilename):
|
||||
|
@ -3051,7 +3048,18 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
addPostToTimeline(fullPostFilename, boxname,
|
||||
postsInBox, boxActor)
|
||||
else:
|
||||
print('WARN: unable to locate post ' + postUrl)
|
||||
# if this is the features timeline
|
||||
if timelineNickname != nickname:
|
||||
fullPostFilename = \
|
||||
locatePost(baseDir, timelineNickname,
|
||||
domain, postUrl, False)
|
||||
if fullPostFilename:
|
||||
addPostToTimeline(fullPostFilename, boxname,
|
||||
postsInBox, boxActor)
|
||||
else:
|
||||
print('WARN: unable to locate post ' + postUrl)
|
||||
else:
|
||||
print('WARN: unable to locate post ' + postUrl)
|
||||
|
||||
postsCtr += 1
|
||||
|
||||
|
@ -3080,12 +3088,24 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
return boxHeader
|
||||
|
||||
for postStr in postsInBox:
|
||||
# Check if the post has replies
|
||||
hasReplies = False
|
||||
if postStr.endswith('<hasReplies>'):
|
||||
hasReplies = True
|
||||
# remove the replies identifier
|
||||
postStr = postStr.replace('<hasReplies>', '')
|
||||
|
||||
p = None
|
||||
try:
|
||||
p = json.loads(postStr)
|
||||
except BaseException:
|
||||
continue
|
||||
|
||||
# Does this post have replies?
|
||||
# This will be used to indicate that replies exist within the html
|
||||
# created by individualPostAsHtml
|
||||
p['hasReplies'] = hasReplies
|
||||
|
||||
# Don't show likes, replies or shares (announces) to
|
||||
# unauthorized viewers
|
||||
if not authorized:
|
||||
|
|
23
roles.py
|
@ -46,15 +46,20 @@ def clearEditorStatus(baseDir: str) -> None:
|
|||
for f in os.scandir(directory):
|
||||
f = f.name
|
||||
filename = os.fsdecode(f)
|
||||
if filename.endswith(".json") and '@' in filename:
|
||||
filename = os.path.join(baseDir + '/accounts/', filename)
|
||||
if '"editor"' in open(filename).read():
|
||||
actorJson = loadJson(filename)
|
||||
if actorJson:
|
||||
if actorJson['roles'].get('instance'):
|
||||
if 'editor' in actorJson['roles']['instance']:
|
||||
actorJson['roles']['instance'].remove('editor')
|
||||
saveJson(actorJson, filename)
|
||||
if '@' not in filename:
|
||||
continue
|
||||
if not filename.endswith(".json"):
|
||||
continue
|
||||
filename = os.path.join(baseDir + '/accounts/', filename)
|
||||
if '"editor"' not in open(filename).read():
|
||||
continue
|
||||
actorJson = loadJson(filename)
|
||||
if not actorJson:
|
||||
continue
|
||||
if actorJson['roles'].get('instance'):
|
||||
if 'editor' in actorJson['roles']['instance']:
|
||||
actorJson['roles']['instance'].remove('editor')
|
||||
saveJson(actorJson, filename)
|
||||
|
||||
|
||||
def addModerator(baseDir: str, nickname: str, domain: str) -> None:
|
||||
|
|
27
tests.py
|
@ -32,6 +32,7 @@ from follow import clearFollows
|
|||
from follow import clearFollowers
|
||||
from follow import sendFollowRequestViaServer
|
||||
from follow import sendUnfollowRequestViaServer
|
||||
from utils import validNickname
|
||||
from utils import firstParagraphFromString
|
||||
from utils import removeIdEnding
|
||||
from utils import siteIsActive
|
||||
|
@ -1387,6 +1388,8 @@ def testClientToServer():
|
|||
httpPrefix,
|
||||
cachedWebfingers, personCache,
|
||||
True, __version__)
|
||||
alicePetnamesFilename = aliceDir + '/accounts/' + \
|
||||
'alice@' + aliceDomain + '/petnames.txt'
|
||||
aliceFollowingFilename = \
|
||||
aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt'
|
||||
bobFollowersFilename = \
|
||||
|
@ -1395,7 +1398,8 @@ def testClientToServer():
|
|||
if os.path.isfile(bobFollowersFilename):
|
||||
if 'alice@' + aliceDomain + ':' + str(alicePort) in \
|
||||
open(bobFollowersFilename).read():
|
||||
if os.path.isfile(aliceFollowingFilename):
|
||||
if os.path.isfile(aliceFollowingFilename) and \
|
||||
os.path.isfile(alicePetnamesFilename):
|
||||
if 'bob@' + bobDomain + ':' + str(bobPort) in \
|
||||
open(aliceFollowingFilename).read():
|
||||
break
|
||||
|
@ -1403,6 +1407,9 @@ def testClientToServer():
|
|||
|
||||
assert os.path.isfile(bobFollowersFilename)
|
||||
assert os.path.isfile(aliceFollowingFilename)
|
||||
assert os.path.isfile(alicePetnamesFilename)
|
||||
assert 'bob bob@' + bobDomain in \
|
||||
open(alicePetnamesFilename).read()
|
||||
print('alice@' + aliceDomain + ':' + str(alicePort) + ' in ' +
|
||||
bobFollowersFilename)
|
||||
assert 'alice@' + aliceDomain + ':' + str(alicePort) in \
|
||||
|
@ -2397,8 +2404,26 @@ def testParseFeedDate():
|
|||
assert publishedDate == "2020-11-22 18:51:33+00:00"
|
||||
|
||||
|
||||
def testValidNickname():
|
||||
print('testValidNickname')
|
||||
domain = 'somedomain.net'
|
||||
|
||||
nickname = 'myvalidnick'
|
||||
assert validNickname(domain, nickname)
|
||||
|
||||
nickname = 'my.invalid.nick'
|
||||
assert not validNickname(domain, nickname)
|
||||
|
||||
nickname = 'myinvalidnick?'
|
||||
assert not validNickname(domain, nickname)
|
||||
|
||||
nickname = 'my invalid nick?'
|
||||
assert not validNickname(domain, nickname)
|
||||
|
||||
|
||||
def runAllTests():
|
||||
print('Running tests...')
|
||||
testValidNickname()
|
||||
testParseFeedDate()
|
||||
testFirstParagraphFromString()
|
||||
testGetNewswireTags()
|
||||
|
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -17,6 +17,8 @@
|
|||
"gallery-font-size": "35px",
|
||||
"gallery-font-size-mobile": "55px",
|
||||
"main-bg-color": "#002365",
|
||||
"login-bg-color": "#002365",
|
||||
"options-bg-color": "#002365",
|
||||
"post-bg-color": "#002365",
|
||||
"timeline-posts-background-color": "#002365",
|
||||
"header-bg-color": "#002365",
|
||||
|
|
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 978 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 992 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 89 KiB |
|
@ -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')"
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -6,6 +6,8 @@
|
|||
"publish-button-at-top": "False",
|
||||
"focus-color": "green",
|
||||
"main-bg-color": "black",
|
||||
"login-bg-color": "black",
|
||||
"options-bg-color": "black",
|
||||
"post-bg-color": "black",
|
||||
"timeline-posts-background-color": "black",
|
||||
"header-bg-color": "black",
|
||||
|
@ -16,6 +18,8 @@
|
|||
"main-bg-color-report": "#050202",
|
||||
"main-header-color-roles": "#1f192d",
|
||||
"main-fg-color": "#00ff00",
|
||||
"login-fg-color": "#00ff00",
|
||||
"options-fg-color": "#00ff00",
|
||||
"column-left-fg-color": "#00ff00",
|
||||
"border-color": "#035103",
|
||||
"main-link-color": "#2fff2f",
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 11 KiB |
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"time-color": "grey",
|
||||
"event-color": "white",
|
||||
"login-bg-color": "#567726",
|
||||
"login-fg-color": "black",
|
||||
"options-bg-color": "black",
|
||||
"newswire-publish-icon": "True",
|
||||
"full-width-timeline-buttons": "False",
|
||||
"icons-as-buttons": "False",
|
||||
|
@ -25,6 +30,7 @@
|
|||
"title-color": "white",
|
||||
"main-visited-color": "#e1c4bc",
|
||||
"main-fg-color": "white",
|
||||
"options-fg-color": "white",
|
||||
"column-left-fg-color": "white",
|
||||
"main-bg-color-dm": "#343335",
|
||||
"border-color": "#222",
|
||||
|
@ -49,7 +55,7 @@
|
|||
"lines-color": "#c5d2b9",
|
||||
"day-number": "#c5d2b9",
|
||||
"day-number2": "#ccc",
|
||||
"event-background": "#333",
|
||||
"event-background": "#555",
|
||||
"timeline-border-radius": "20px",
|
||||
"image-corners": "8%",
|
||||
"quote-right-margin": "0.1em",
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -27,6 +27,8 @@
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "black",
|
||||
"login-bg-color": "black",
|
||||
"options-bg-color": "black",
|
||||
"post-bg-color": "black",
|
||||
"timeline-posts-background-color": "black",
|
||||
"header-bg-color": "black",
|
||||
|
@ -40,6 +42,8 @@
|
|||
"main-link-color-hover": "#d09338",
|
||||
"main-visited-color": "#ffb900",
|
||||
"main-fg-color": "white",
|
||||
"login-fg-color": "white",
|
||||
"options-fg-color": "white",
|
||||
"column-left-fg-color": "white",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#003366",
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.2 KiB |
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"timeline-icon-width": "30px",
|
||||
"timeline-icon-width-mobile": "60px",
|
||||
"button-bottom-margin": "0",
|
||||
"header-vertical-offset": "10px",
|
||||
"header-bg-color": "#efefef",
|
||||
|
@ -15,7 +17,6 @@
|
|||
"hashtag-size2": "30px",
|
||||
"font-size-calendar-header": "2rem",
|
||||
"font-size-calendar-cell": "2rem",
|
||||
"calendar-horizontal-padding": "20%",
|
||||
"time-vertical-align": "10px",
|
||||
"publish-button-vertical-offset": "15px",
|
||||
"vertical-between-posts": "0",
|
||||
|
@ -52,7 +53,7 @@
|
|||
"container-button-padding": "0px",
|
||||
"container-button-margin": "0px",
|
||||
"column-left-icon-size": "15%",
|
||||
"column-right-icon-size": "8%",
|
||||
"column-right-icon-size": "9.5%",
|
||||
"button-margin": "2px",
|
||||
"button-height-padding": "5px",
|
||||
"icon-brightness-change": "70%",
|
||||
|
@ -64,8 +65,8 @@
|
|||
"login-button-color": "#25408f",
|
||||
"login-button-fg-color": "white",
|
||||
"column-left-width": "10vw",
|
||||
"column-center-width": "70vw",
|
||||
"column-right-width": "20vw",
|
||||
"column-center-width": "75vw",
|
||||
"column-right-width": "15vw",
|
||||
"column-right-fg-color": "#25408f",
|
||||
"column-right-fg-color-voted-on": "red",
|
||||
"newswire-item-moderated-color": "red",
|
||||
|
@ -88,6 +89,8 @@
|
|||
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
|
||||
"column-left-color": "#efefef",
|
||||
"main-bg-color": "#efefef",
|
||||
"login-bg-color": "#efefef",
|
||||
"options-bg-color": "#efefef",
|
||||
"post-bg-color": "white",
|
||||
"timeline-posts-background-color": "white",
|
||||
"main-bg-color-dm": "white",
|
||||
|
@ -96,6 +99,8 @@
|
|||
"main-bg-color-report": "white",
|
||||
"main-header-color-roles": "#ebebf0",
|
||||
"main-fg-color": "black",
|
||||
"login-fg-color": "black",
|
||||
"options-fg-color": "black",
|
||||
"column-left-fg-color": "#25408f",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#25408f",
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -8,6 +8,8 @@
|
|||
"column-left-header-background": "#9fb42b",
|
||||
"column-left-header-color": "#33390d",
|
||||
"main-bg-color": "#9fb42b",
|
||||
"login-bg-color": "#9fb42b",
|
||||
"options-bg-color": "#9fb42b",
|
||||
"post-bg-color": "#9fb42b",
|
||||
"timeline-posts-background-color": "#9fb42b",
|
||||
"header-bg-color": "#9fb42b",
|
||||
|
@ -21,6 +23,8 @@
|
|||
"main-bg-color-dm": "#5fb42b",
|
||||
"main-header-color-roles": "#9fb42b",
|
||||
"main-fg-color": "#33390d",
|
||||
"login-fg-color": "#33390d",
|
||||
"options-fg-color": "#33390d",
|
||||
"border-color": "#33390d",
|
||||
"border-width": "5px",
|
||||
"border-width-header": "5px",
|
||||
|
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -22,15 +22,19 @@
|
|||
"rgba(0, 0, 0, 0.5)": "rgba(0, 0, 0, 0.0)",
|
||||
"column-left-color": "#e6ebf0",
|
||||
"main-bg-color": "#e6ebf0",
|
||||
"login-bg-color": "#e6ebf0",
|
||||
"options-bg-color": "#e6ebf0",
|
||||
"post-bg-color": "#e6ebf0",
|
||||
"timeline-posts-background-color": "#e6ebf0",
|
||||
"header-bg-color": "#e6ebf0",
|
||||
"main-bg-color-dm": "#e3dbf0",
|
||||
"link-bg-color": "#e6ebf0",
|
||||
"main-bg-color-reply": "#e0dbf0",
|
||||
"main-bg-color-reply": "white",
|
||||
"main-bg-color-report": "#e3dbf0",
|
||||
"main-header-color-roles": "#ebebf0",
|
||||
"main-fg-color": "#2d2c37",
|
||||
"login-fg-color": "#2d2c37",
|
||||
"options-fg-color": "#2d2c37",
|
||||
"column-left-fg-color": "#2d2c37",
|
||||
"border-color": "#c0cdd9",
|
||||
"main-link-color": "#2a2c37",
|
||||
|
|
After Width: | Height: | Size: 2.5 KiB |
|
@ -20,6 +20,8 @@
|
|||
"font-size4": "24px",
|
||||
"font-size5": "22px",
|
||||
"main-bg-color": "#0f0d10",
|
||||
"login-bg-color": "#0f0d10",
|
||||
"options-bg-color": "#0f0d10",
|
||||
"post-bg-color": "#0f0d10",
|
||||
"timeline-posts-background-color": "#0f0d10",
|
||||
"header-bg-color": "#0f0d10",
|
||||
|
@ -29,6 +31,8 @@
|
|||
"main-link-color": "#6481f5",
|
||||
"main-link-color-hover": "#d09338",
|
||||
"main-fg-color": "#0481f5",
|
||||
"login-fg-color": "#0481f5",
|
||||
"options-fg-color": "#0481f5",
|
||||
"column-left-fg-color": "#0481f5",
|
||||
"main-bg-color-dm": "#0b0a0a",
|
||||
"border-color": "#606984",
|
||||
|
|
After Width: | Height: | Size: 1.4 KiB |