diff --git a/blog.py b/blog.py index ca6d5a6a2..d14c72951 100644 --- a/blog.py +++ b/blog.py @@ -15,6 +15,7 @@ from webapp import htmlHeaderWithExternalStyle from webapp import htmlFooter from webapp_media import addEmbeddedElements from webapp_utils import getPostAttachmentsAsHtml +from utils import getMediaFormats from utils import getNicknameFromActor from utils import getDomainFromActor from utils import locatePost @@ -724,12 +725,11 @@ def htmlEditBlog(mediaInstance: bool, translate: {}, iconsPath = getIconsWebPath(baseDir) - editBlogText = '
' + \ - translate['Write your post text below.'] + '
' + editBlogText = '' + file.read() + '
' + editBlogText = '' + file.read() + '
' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): @@ -746,8 +746,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {}, editBlogImageSection += \ ' ' + ' accept="' + getMediaFormats() + '">' editBlogImageSection += ' ' placeholderMessage = translate['Write something'] + '...' diff --git a/content.py b/content.py index 44113ac59..efac10f97 100644 --- a/content.py +++ b/content.py @@ -9,6 +9,7 @@ __status__ = "Production" import os import email.parser from shutil import copyfile +from utils import getImageExtensions from utils import loadJson from utils import fileLastModified from utils import getLinkPrefixes @@ -939,7 +940,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool, break # remove any existing image files with a different format - extensionTypes = ('png', 'jpg', 'jpeg', 'gif', 'webp', 'avif') + extensionTypes = getImageExtensions() for ex in extensionTypes: if ex == detectedExtension: continue diff --git a/daemon.py b/daemon.py index 5a160d9c6..7ffaba474 100644 --- a/daemon.py +++ b/daemon.py @@ -166,6 +166,7 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import getImageExtensions from utils import mediaFileMimeType from utils import getCSS from utils import firstParagraphFromString @@ -8412,7 +8413,8 @@ class PubServer(BaseHTTPRequestHandler): GETstartTime, GETtimings: {}) -> bool: """Show a background image """ - for ext in ('webp', 'gif', 'jpg', 'png', 'avif'): + imageExtensions = getImageExtensions() + for ext in imageExtensions: for bg in ('follow', 'options', 'login'): # follow screen background image if path.endswith('/' + bg + '-background.' + ext): @@ -12386,7 +12388,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(allowLocalNetworkAccess: bool, +def runDaemon(maxNewswirePosts: int, + allowLocalNetworkAccess: bool, maxFeedItemSizeKb: int, publishButtonAtTop: bool, rssIconAtTop: bool, @@ -12461,6 +12464,9 @@ def runDaemon(allowLocalNetworkAccess: bool, # newswire storing rss feeds httpd.newswire = {} + # maximum number of posts to appear in the newswire on the right column + httpd.maxNewswirePosts = maxNewswirePosts + # This counter is used to update the list of blocked domains in memory. # It helps to avoid touching the disk and so improves flooding resistance httpd.blocklistUpdateCtr = 0 diff --git a/epicyon-links.css b/epicyon-links.css index 849c2cf90..8cc051baa 100644 --- a/epicyon-links.css +++ b/epicyon-links.css @@ -66,6 +66,7 @@ --column-right-width: 10vw; --banner-height: 15vh; --banner-height-mobile: 10vh; + --header-font: 'Arial, Helvetica, sans-serif'; } @font-face { @@ -129,10 +130,6 @@ blockquote p { border: 2px solid var(--focus-color); } -h1 { - color: var(--title-color); -} - a, u { color: var(--main-fg-color); } @@ -214,10 +211,9 @@ a:focus { transform: translateY(30%) scaleX(-1); } -.new-post-text { - font-size: var(--font-size2); - font-family: Arial, Helvetica, sans-serif; - padding: 4px 0; +h1 { + font-family: var(--header-font); + color: var(--title-color); } .new-post-subtext { diff --git a/epicyon.py b/epicyon.py index 605661f1b..ecc051bb6 100644 --- a/epicyon.py +++ b/epicyon.py @@ -116,6 +116,10 @@ parser.add_argument('--postsPerSource', dest='maxNewswirePostsPerSource', type=int, default=4, help='Maximum newswire posts per feed or account') +parser.add_argument('--maxNewswirePosts', + dest='maxNewswirePosts', type=int, + default=20, + help='Maximum newswire posts in the right column') parser.add_argument('--maxFeedSize', dest='maxNewswireFeedSizeKb', type=int, default=10240, @@ -2001,6 +2005,12 @@ maxNewswirePostsPerSource = \ if maxNewswirePostsPerSource: args.maxNewswirePostsPerSource = int(maxNewswirePostsPerSource) +# set the maximum number of newswire posts appearing in the right column +maxNewswirePosts = \ + getConfigParam(baseDir, 'maxNewswirePosts') +if maxNewswirePosts: + args.maxNewswirePosts = int(maxNewswirePosts) + # set the maximum size of a newswire rss/atom feed in Kilobytes maxNewswireFeedSizeKb = \ getConfigParam(baseDir, 'maxNewswireFeedSizeKb') @@ -2075,7 +2085,8 @@ if setTheme(baseDir, themeName, domain, args.allowLocalNetworkAccess): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.allowLocalNetworkAccess, + runDaemon(args.maxNewswirePosts, + args.allowLocalNetworkAccess, args.maxFeedItemSizeKb, args.publishButtonAtTop, args.rssIconAtTop, diff --git a/media.py b/media.py index a231c7906..25532ce22 100644 --- a/media.py +++ b/media.py @@ -13,6 +13,10 @@ import os import datetime from hashlib import sha1 from auth import createPassword +from utils import getImageExtensions +from utils import getVideoExtensions +from utils import getAudioExtensions +from utils import getMediaExtensions from shutil import copyfile from shutil import rmtree from shutil import move @@ -56,8 +60,7 @@ def getImageHash(imageFilename: str) -> str: def isMedia(imageFilename: str) -> bool: - permittedMedia = ('png', 'jpg', 'gif', 'webp', 'avif', - 'mp4', 'ogv', 'mp3', 'ogg') + permittedMedia = getMediaExtensions() for m in permittedMedia: if imageFilename.endswith('.' + m): return True @@ -83,16 +86,15 @@ def getAttachmentMediaType(filename: str) -> str: image, video or audio """ mediaType = None - imageTypes = ('png', 'jpg', 'jpeg', - 'gif', 'webp', 'avif') + imageTypes = getImageExtensions() for mType in imageTypes: if filename.endswith('.' + mType): return 'image' - videoTypes = ('mp4', 'webm', 'ogv') + videoTypes = getVideoExtensions() for mType in videoTypes: if filename.endswith('.' + mType): return 'video' - audioTypes = ('mp3', 'ogg') + audioTypes = getAudioExtensions() for mType in audioTypes: if filename.endswith('.' + mType): return 'audio' @@ -143,8 +145,7 @@ def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int, return postJson fileExtension = None - acceptedTypes = ('png', 'jpg', 'gif', 'webp', 'avif', - 'mp4', 'webm', 'ogv', 'mp3', 'ogg') + acceptedTypes = getMediaExtensions() for mType in acceptedTypes: if imageFilename.endswith('.' + mType): if mType == 'jpg': diff --git a/newsdaemon.py b/newsdaemon.py index 899866179..a08f0e200 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -711,18 +711,13 @@ def runNewswireDaemon(baseDir: str, httpd, print('Newswire daemon session established') # try to update the feeds - newNewswire = None - try: - newNewswire = \ - getDictFromNewswire(httpd.session, baseDir, domain, - httpd.maxNewswirePostsPerSource, - httpd.maxNewswireFeedSizeKb, - httpd.maxTags, - httpd.maxFeedItemSizeKb) - except Exception as e: - print('WARN: unable to update newswire ' + str(e)) - time.sleep(120) - continue + newNewswire = \ + getDictFromNewswire(httpd.session, baseDir, domain, + httpd.maxNewswirePostsPerSource, + httpd.maxNewswireFeedSizeKb, + httpd.maxTags, + httpd.maxFeedItemSizeKb, + httpd.maxNewswirePosts) if not httpd.newswire: if os.path.isfile(newswireStateFilename): diff --git a/newswire.py b/newswire.py index 7874ecb9a..2e0ab8aaf 100644 --- a/newswire.py +++ b/newswire.py @@ -11,6 +11,8 @@ import requests from socket import error as SocketError import errno from datetime import datetime +from datetime import timedelta +from datetime import timezone from collections import OrderedDict from utils import firstParagraphFromString from utils import isPublicPost @@ -25,6 +27,16 @@ from blocking import isBlockedHashtag from filters import isFiltered +def removeCDATA(text: str) -> str: + """Removes any CDATA from the given text + """ + if 'CDATA[' in text: + text = text.split('CDATA[')[1] + if ']' in text: + text = text.split(']')[0] + return text + + def rss2Header(httpPrefix: str, nickname: str, domainFull: str, title: str, translate: {}) -> str: @@ -125,6 +137,71 @@ def addNewswireDictEntry(baseDir: str, domain: str, ] +def parseFeedDate(pubDate: str) -> str: + """Returns a UTC date string based on the given date string + This tries a number of formats to see which work + """ + formats = ("%a, %d %b %Y %H:%M:%S %z", + "%a, %d %b %Y %H:%M:%S EST", + "%a, %d %b %Y %H:%M:%S UT", + "%Y-%m-%dT%H:%M:%SZ", + "%Y-%m-%dT%H:%M:%S%z") + + publishedDate = None + for dateFormat in formats: + if ',' in pubDate and ',' not in dateFormat: + continue + if ',' not in pubDate and ',' in dateFormat: + continue + if '-' in pubDate and '-' not in dateFormat: + continue + if '-' not in pubDate and '-' in dateFormat: + continue + if 'T' in pubDate and 'T' not in dateFormat: + continue + if 'T' not in pubDate and 'T' in dateFormat: + continue + if 'Z' in pubDate and 'Z' not in dateFormat: + continue + if 'Z' not in pubDate and 'Z' in dateFormat: + continue + if 'EST' not in pubDate and 'EST' in dateFormat: + continue + if 'EST' in pubDate and 'EST' not in dateFormat: + continue + if 'UT' not in pubDate and 'UT' in dateFormat: + continue + if 'UT' in pubDate and 'UT' not in dateFormat: + continue + + try: + publishedDate = \ + datetime.strptime(pubDate, dateFormat) + except BaseException: + print('WARN: unrecognized date format: ' + + pubDate + ' ' + dateFormat) + continue + + if publishedDate: + if pubDate.endswith(' EST'): + hoursAdded = timedelta(hours=5) + publishedDate = publishedDate + hoursAdded + break + + pubDateStr = None + if publishedDate: + offset = publishedDate.utcoffset() + if offset: + publishedDate = publishedDate - offset + # convert local date to UTC + publishedDate = publishedDate.replace(tzinfo=timezone.utc) + pubDateStr = str(publishedDate) + if not pubDateStr.endswith('+00:00'): + pubDateStr += '+00:00' + + return pubDateStr + + def xml2StrToDict(baseDir: str, domain: str, xmlStr: str, moderated: bool, mirrored: bool, maxPostsPerSource: int, @@ -154,11 +231,17 @@ def xml2StrToDict(baseDir: str, domain: str, xmlStr: str, if '' not in rssItem: continue title = rssItem.split('' + translate['Edit Links'] + '
' + '' + translate['Edit newswire'] + '
' + '' + translate['Edit News Post'] + '
' + '' + \ - translate['Write your post text below.'] + '
\n' + newPostText = '' + \ @@ -208,8 +210,8 @@ def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, showPublicOnDropdown = False else: newPostText = \ - '
' + \ - translate['Write your report below.'] + '
\n' + '' + \ + '
' + \ + '
' + file.read() + '
\n' + '' + file.read() + '
\n' cssFilename = baseDir + '/epicyon-profile.css' if os.path.isfile(baseDir + '/epicyon.css'): @@ -280,13 +282,12 @@ def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {}, newPostImageSection += \ ' \n' + ' accept="' + getImageFormats() + '">\n' else: newPostImageSection += \ ' \n' + ' accept="' + getMediaFormats() + '">\n' newPostImageSection += '' + translate['Profile for'] + \ - ' ' + nickname + '@' + domainFull + '
' + '