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