diff --git a/blog.py b/blog.py index 8a1251298..1a4c98753 100644 --- a/blog.py +++ b/blog.py @@ -365,7 +365,7 @@ def htmlBlogPost(authorized: bool, blogStr += 'RSS 2.0' + iconsDir + '/logorss.png" />' # blogStr += '' @@ -461,7 +461,7 @@ def htmlBlogPage(authorized: bool, session, domainFull + '/blog/' + nickname + '/rss.xml">' blogStr += 'RSS 2.0' + iconsDir + '/logorss.png" />' # blogStr += '' @@ -478,7 +478,8 @@ def htmlBlogPage(authorized: bool, session, def htmlBlogPageRSS2(authorized: bool, session, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, port: int, - noOfItems: int, pageNumber: int) -> str: + noOfItems: int, pageNumber: int, + includeHeader: bool) -> str: """Returns an RSS version 2 feed containing posts """ if ' ' in nickname or '@' in nickname or \ @@ -490,12 +491,18 @@ def htmlBlogPageRSS2(authorized: bool, session, if port != 80 and port != 443: domainFull = domain + ':' + str(port) - blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, 'Blog', translate) + blogRSS2 = '' + if includeHeader: + blogRSS2 = rss2Header(httpPrefix, nickname, domainFull, + 'Blog', translate) blogsIndex = baseDir + '/accounts/' + \ nickname + '@' + domain + '/tlblogs.index' if not os.path.isfile(blogsIndex): - return blogRSS2 + rss2Footer() + if includeHeader: + return blogRSS2 + rss2Footer() + else: + return blogRSS2 timelineJson = createBlogsTimeline(session, baseDir, nickname, domain, port, @@ -504,7 +511,10 @@ def htmlBlogPageRSS2(authorized: bool, session, pageNumber) if not timelineJson: - return blogRSS2 + rss2Footer() + if includeHeader: + return blogRSS2 + rss2Footer() + else: + return blogRSS2 if pageNumber is not None: for item in timelineJson['orderedItems']: @@ -518,7 +528,10 @@ def htmlBlogPageRSS2(authorized: bool, session, domainFull, item, None, True) - return blogRSS2 + rss2Footer() + if includeHeader: + return blogRSS2 + rss2Footer() + else: + return blogRSS2 def htmlBlogPageRSS3(authorized: bool, session, diff --git a/daemon.py b/daemon.py index c5a815729..86cb9be8f 100644 --- a/daemon.py +++ b/daemon.py @@ -164,6 +164,8 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import containsInvalidChars +from utils import isSystemAccount from utils import setConfigParam from utils import getConfigParam from utils import removeIdEnding @@ -194,6 +196,7 @@ from media import removeMetaData from cache import storePersonInCache from cache import getPersonFromCache from httpsig import verifyPostHeaders +from theme import setNewsAvatar from theme import setTheme from theme import getTheme from theme import enableGrayscale @@ -212,6 +215,8 @@ from devices import E2EEdevicesCollection from devices import E2EEvalidDevice from devices import E2EEaddDevice from newswire import getRSSfromDict +from newswire import rss2Header +from newswire import rss2Footer from newsdaemon import runNewswireWatchdog from newsdaemon import runNewswireDaemon import os @@ -300,11 +305,11 @@ class PubServer(BaseHTTPRequestHandler): accountDir = self.server.baseDir + '/accounts/' + \ nickname + '@' + self.server.domain if not os.path.isdir(accountDir): - return False - minimalFilename = accountDir + '/minimal' - if os.path.isfile(minimalFilename): return True - return False + minimalFilename = accountDir + '/.notminimal' + if os.path.isfile(minimalFilename): + return False + return True def _setMinimal(self, nickname: str, minimal: bool) -> None: """Sets whether an account should display minimal buttons @@ -313,11 +318,11 @@ class PubServer(BaseHTTPRequestHandler): nickname + '@' + self.server.domain if not os.path.isdir(accountDir): return - minimalFilename = accountDir + '/minimal' + minimalFilename = accountDir + '/.notminimal' minimalFileExists = os.path.isfile(minimalFilename) - if not minimal and minimalFileExists: + if minimal and minimalFileExists: os.remove(minimalFilename) - elif minimal and not minimalFileExists: + elif not minimal and not minimalFileExists: with open(minimalFilename, 'w+') as fp: fp.write('\n') @@ -521,6 +526,21 @@ class PubServer(BaseHTTPRequestHandler): self.send_header('X-Robots-Tag', 'noindex') self.end_headers() + def _logout_redirect(self, redirect: str, cookie: str, + callingDomain: str) -> None: + if '://' not in redirect: + print('REDIRECT ERROR: redirect is not an absolute url ' + + redirect) + + self.send_response(303) + self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict') + self.send_header('Location', redirect) + self.send_header('Host', callingDomain) + self.send_header('InstanceID', self.server.instanceId) + self.send_header('Content-Length', '0') + self.send_header('X-Robots-Tag', 'noindex') + self.end_headers() + def _set_headers_base(self, fileFormat: str, length: int, cookie: str, callingDomain: str) -> None: self.send_response(200) @@ -1240,8 +1260,9 @@ class PubServer(BaseHTTPRequestHandler): loginNickname, loginPassword, register = \ htmlGetLoginCredentials(loginParams, self.server.lastLoginTime) if loginNickname: - if loginNickname == 'news' or loginNickname == 'inbox': - print('Invalid username login: ' + loginNickname) + if isSystemAccount(loginNickname): + print('Invalid username login: ' + loginNickname + + ' (system account)') self._clearLoginDetails(loginNickname, callingDomain) self.server.POSTbusy = False return @@ -1625,7 +1646,8 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('You cannot perform an option action on yourself') - # view button on person option screen + # person options screen, view button + # See htmlPersonOptions if '&submitView=' in optionsConfirmParams: if debug: print('Viewing ' + optionsActor) @@ -1634,7 +1656,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # petname submit button on person option screen + # person options screen, petname submit button + # See htmlPersonOptions if '&submitPetname=' in optionsConfirmParams and petname: if debug: print('Change petname to ' + petname) @@ -1650,7 +1673,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # person notes submit button on person option screen + # person options screen, person notes submit button + # See htmlPersonOptions if '&submitPersonNotes=' in optionsConfirmParams: if debug: print('Change person notes') @@ -1668,7 +1692,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # person on calendar checkbox on person option screen + # person options screen, on calendar checkbox + # See htmlPersonOptions if '&submitOnCalendar=' in optionsConfirmParams: onCalendar = None if 'onCalendar=' in optionsConfirmParams: @@ -1694,7 +1719,35 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # block person button on person option screen + # person options screen, permission to post to newswire + # See htmlPersonOptions + if '&submitPostToNews=' in optionsConfirmParams: + if isModerator(self.server.baseDir, chooserNickname): + postsToNews = None + if 'postsToNews=' in optionsConfirmParams: + postsToNews = optionsConfirmParams.split('postsToNews=')[1] + if '&' in postsToNews: + postsToNews = postsToNews.split('&')[0] + newswireBlockedFilename = \ + self.server.baseDir + '/accounts/' + \ + optionsNickname + '@' + optionsDomain + '/.nonewswire' + if postsToNews == 'on': + if os.path.isfile(newswireBlockedFilename): + os.remove(newswireBlockedFilename) + else: + noNewswireFile = open(newswireBlockedFilename, "w+") + if noNewswireFile: + noNewswireFile.write('\n') + noNewswireFile.close() + self._redirect_headers(usersPath + '/' + + self.server.defaultTimeline + + '?page='+str(pageNumber), cookie, + callingDomain) + self.server.POSTbusy = False + return + + # person options screen, block button + # See htmlPersonOptions if '&submitBlock=' in optionsConfirmParams: if debug: print('Adding block by ' + chooserNickname + @@ -1703,7 +1756,8 @@ class PubServer(BaseHTTPRequestHandler): domain, optionsNickname, optionsDomainFull) - # unblock button on person option screen + # person options screen, unblock button + # See htmlPersonOptions if '&submitUnblock=' in optionsConfirmParams: if debug: print('Unblocking ' + optionsActor) @@ -1719,7 +1773,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # follow button on person option screen + # person options screen, follow button + # See htmlPersonOptions if '&submitFollow=' in optionsConfirmParams: if debug: print('Following ' + optionsActor) @@ -1735,7 +1790,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # unfollow button on person option screen + # person options screen, unfollow button + # See htmlPersonOptions if '&submitUnfollow=' in optionsConfirmParams: if debug: print('Unfollowing ' + optionsActor) @@ -1751,7 +1807,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # DM button on person option screen + # person options screen, DM button + # See htmlPersonOptions if '&submitDM=' in optionsConfirmParams: if debug: print('Sending DM to ' + optionsActor) @@ -1771,7 +1828,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # snooze button on person option screen + # person options screen, snooze button + # See htmlPersonOptions if '&submitSnooze=' in optionsConfirmParams: usersPath = path.split('/personoptions')[0] thisActor = httpPrefix + '://' + domainFull + usersPath @@ -1792,7 +1850,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # unsnooze button on person option screen + # person options screen, unsnooze button + # See htmlPersonOptions if '&submitUnSnooze=' in optionsConfirmParams: usersPath = path.split('/personoptions')[0] thisActor = httpPrefix + '://' + domainFull + usersPath @@ -1813,7 +1872,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - # report button on person option screen + # person options screen, report button + # See htmlPersonOptions if '&submitReport=' in optionsConfirmParams: if debug: print('Reporting ' + optionsActor) @@ -3309,10 +3369,95 @@ class PubServer(BaseHTTPRequestHandler): if fields['displayNickname'] != actorJson['name']: actorJson['name'] = fields['displayNickname'] actorChanged = True + + # change media instance status + if fields.get('mediaInstance'): + self.server.mediaInstance = False + self.server.defaultTimeline = 'inbox' + if fields['mediaInstance'] == 'on': + self.server.mediaInstance = True + self.server.blogsInstance = False + self.server.newsInstance = False + self.server.defaultTimeline = 'tlmedia' + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + setConfigParam(baseDir, + "newsInstance", + self.server.newsInstance) + else: + if self.server.mediaInstance: + self.server.mediaInstance = False + self.server.defaultTimeline = 'inbox' + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + + # change news instance status + if fields.get('newsInstance'): + self.server.newsInstance = False + self.server.defaultTimeline = 'inbox' + if fields['newsInstance'] == 'on': + self.server.newsInstance = True + self.server.blogsInstance = False + self.server.mediaInstance = False + self.server.defaultTimeline = 'tlnews' + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + setConfigParam(baseDir, + "newsInstance", + self.server.newsInstance) + else: + if self.server.newsInstance: + self.server.newsInstance = False + self.server.defaultTimeline = 'inbox' + setConfigParam(baseDir, + "newsInstance", + self.server.mediaInstance) + + # change blog instance status + if fields.get('blogsInstance'): + self.server.blogsInstance = False + self.server.defaultTimeline = 'inbox' + if fields['blogsInstance'] == 'on': + self.server.blogsInstance = True + self.server.mediaInstance = False + self.server.newsInstance = False + self.server.defaultTimeline = 'tlblogs' + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + setConfigParam(baseDir, + "mediaInstance", + self.server.mediaInstance) + setConfigParam(baseDir, + "newsInstance", + self.server.newsInstance) + else: + if self.server.blogsInstance: + self.server.blogsInstance = False + self.server.defaultTimeline = 'inbox' + setConfigParam(baseDir, + "blogsInstance", + self.server.blogsInstance) + + # change theme if fields.get('themeDropdown'): setTheme(baseDir, fields['themeDropdown'], domain) + setNewsAvatar(baseDir, + fields['themeDropdown'], + httpPrefix, + domain, + domainFull) # change email address currentEmailAddress = getEmailAddress(actorJson) @@ -3653,84 +3798,6 @@ class PubServer(BaseHTTPRequestHandler): if currTheme: setTheme(baseDir, currTheme, domain) - # change media instance status - if fields.get('mediaInstance'): - self.server.mediaInstance = False - self.server.defaultTimeline = 'inbox' - if fields['mediaInstance'] == 'on': - self.server.mediaInstance = True - self.server.blogsInstance = False - self.server.newsInstance = False - self.server.defaultTimeline = 'tlmedia' - setConfigParam(baseDir, - "mediaInstance", - self.server.mediaInstance) - setConfigParam(baseDir, - "blogsInstance", - self.server.blogsInstance) - setConfigParam(baseDir, - "newsInstance", - self.server.newsInstance) - else: - if self.server.mediaInstance: - self.server.mediaInstance = False - self.server.defaultTimeline = 'inbox' - setConfigParam(baseDir, - "mediaInstance", - self.server.mediaInstance) - - # change news instance status - if fields.get('newsInstance'): - self.server.newsInstance = False - self.server.defaultTimeline = 'inbox' - if fields['newsInstance'] == 'on': - self.server.newsInstance = True - self.server.blogsInstance = False - self.server.mediaInstance = False - self.server.defaultTimeline = 'tlnews' - setConfigParam(baseDir, - "mediaInstance", - self.server.mediaInstance) - setConfigParam(baseDir, - "blogsInstance", - self.server.blogsInstance) - setConfigParam(baseDir, - "newsInstance", - self.server.newsInstance) - else: - if self.server.newsInstance: - self.server.newsInstance = False - self.server.defaultTimeline = 'inbox' - setConfigParam(baseDir, - "newsInstance", - self.server.mediaInstance) - - # change blog instance status - if fields.get('blogsInstance'): - self.server.blogsInstance = False - self.server.defaultTimeline = 'inbox' - if fields['blogsInstance'] == 'on': - self.server.blogsInstance = True - self.server.mediaInstance = False - self.server.newsInstance = False - self.server.defaultTimeline = 'tlblogs' - setConfigParam(baseDir, - "blogsInstance", - self.server.blogsInstance) - setConfigParam(baseDir, - "mediaInstance", - self.server.mediaInstance) - setConfigParam(baseDir, - "newsInstance", - self.server.newsInstance) - else: - if self.server.blogsInstance: - self.server.blogsInstance = False - self.server.defaultTimeline = 'inbox' - setConfigParam(baseDir, - "blogsInstance", - self.server.blogsInstance) - # only receive DMs from accounts you follow followDMsFilename = \ baseDir + '/accounts/' + \ @@ -4211,7 +4278,8 @@ class PubServer(BaseHTTPRequestHandler): nickname, domain, port, - maxPostsInRSSFeed, 1) + maxPostsInRSSFeed, 1, + True) if msg is not None: msg = msg.encode('utf-8') self._set_headers('text/xml', len(msg), @@ -4229,6 +4297,66 @@ class PubServer(BaseHTTPRequestHandler): path + ' ' + callingDomain) self._404() + def _getRSS2site(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domainFull: str, port: int, proxyType: str, + translate: {}, + GETstartTime, GETtimings: {}, + debug: bool): + """Returns an RSS2 feed for all blogs on this instance + """ + if not self.server.session: + print('Starting new session during RSS request') + self.server.session = \ + createSession(proxyType) + if not self.server.session: + print('ERROR: GET failed to create session ' + + 'during RSS request') + self._404() + return + + msg = '' + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct or 'news@' in acct: + continue + nickname = acct.split('@')[0] + domain = acct.split('@')[1] + msg += \ + htmlBlogPageRSS2(authorized, + self.server.session, + baseDir, + httpPrefix, + self.server.translate, + nickname, + domain, + port, + maxPostsInRSSFeed, 1, + False) + if msg: + msg = rss2Header(httpPrefix, + 'news', domainFull, + 'Site', translate) + msg + rss2Footer() + + msg = msg.encode('utf-8') + self._set_headers('text/xml', len(msg), + None, callingDomain) + self._write(msg) + if debug: + print('Sent rss2 feed: ' + + path + ' ' + callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'sharedInbox enabled', + 'blog rss2') + return + if debug: + print('Failed to get rss2 feed: ' + + path + ' ' + callingDomain) + self._404() + def _getNewswireFeed(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -4358,6 +4486,7 @@ class PubServer(BaseHTTPRequestHandler): PGPfingerprint = getPGPfingerprint(actorJson) msg = htmlPersonOptions(self.server.translate, baseDir, domain, + domainFull, originPathStr, optionsActor, optionsProfileUrl, @@ -5870,6 +5999,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, YTReplacementDomain, self.server.showPublishedDateOnly, + self.server.newswire, actorJson['roles'], None, None) msg = msg.encode('utf-8') @@ -5943,6 +6073,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, YTReplacementDomain, showPublishedDateOnly, + self.server.newswire, actorJson['skills'], None, None) msg = msg.encode('utf-8') @@ -7405,6 +7536,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, self.server.YTReplacementDomain, self.server.showPublishedDateOnly, + self.server.newswire, shares, pageNumber, sharesPerPage) msg = msg.encode('utf-8') @@ -7492,6 +7624,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, self.server.YTReplacementDomain, self.server.showPublishedDateOnly, + self.server.newswire, following, pageNumber, followsPerPage).encode('utf-8') @@ -7579,6 +7712,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, self.server.YTReplacementDomain, self.server.showPublishedDateOnly, + self.server.newswire, followers, pageNumber, followsPerPage).encode('utf-8') @@ -7641,6 +7775,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, self.server.YTReplacementDomain, self.server.showPublishedDateOnly, + self.server.newswire, None, None).encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7748,23 +7883,28 @@ class PubServer(BaseHTTPRequestHandler): divertToLoginScreen = False if divertToLoginScreen and not authorized: - if debug: - print('DEBUG: divertToLoginScreen=' + - str(divertToLoginScreen)) - print('DEBUG: authorized=' + str(authorized)) - print('DEBUG: path=' + path) + divertPath = '/login' + if self.server.newsInstance: + # for news instances if not logged in then show the + # front page + divertPath = '/users/news' + # if debug: + print('DEBUG: divertToLoginScreen=' + + str(divertToLoginScreen)) + print('DEBUG: authorized=' + str(authorized)) + print('DEBUG: path=' + path) if callingDomain.endswith('.onion') and onionDomain: self._redirect_headers('http://' + - onionDomain + '/login', + onionDomain + divertPath, None, callingDomain) elif callingDomain.endswith('.i2p') and i2pDomain: self._redirect_headers('http://' + - i2pDomain + '/login', + i2pDomain + divertPath, None, callingDomain) else: self._redirect_headers(httpPrefix + '://' + domainFull + - '/login', None, callingDomain) + divertPath, None, callingDomain) self._benchmarkGETtimings(GETstartTime, GETtimings, 'robots txt', 'show login screen') @@ -8328,11 +8468,31 @@ class PubServer(BaseHTTPRequestHandler): '_mastoApi(callingDomain)') if self.path == '/logout': - msg = \ - htmlLogin(self.server.translate, - self.server.baseDir, False).encode('utf-8') - self._logout_headers('text/html', len(msg), callingDomain) - self._write(msg) + if not self.server.newsInstance: + msg = \ + htmlLogin(self.server.translate, + self.server.baseDir, False).encode('utf-8') + self._logout_headers('text/html', len(msg), callingDomain) + self._write(msg) + else: + if callingDomain.endswith('.onion') and \ + self.server.onionDomain: + self._logout_redirect('http://' + + self.server.onionDomain + + '/users/news', None, + callingDomain) + elif (callingDomain.endswith('.i2p') and + self.server.i2pDomain): + self._logout_redirect('http://' + + self.server.i2pDomain + + '/users/news', None, + callingDomain) + else: + self._logout_redirect(self.server.httpPrefix + + '://' + + self.server.domainFull + + '/users/news', + None, callingDomain) self._benchmarkGETtimings(GETstartTime, GETtimings, '_nodeinfo(callingDomain)', 'logout') @@ -8465,15 +8625,27 @@ class PubServer(BaseHTTPRequestHandler): # RSS 2.0 if self.path.startswith('/blog/') and \ self.path.endswith('/rss.xml'): - self._getRSS2feed(authorized, - callingDomain, self.path, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.port, - self.server.proxyType, - GETstartTime, GETtimings, - self.server.debug) + if not self.path == '/blog/rss.xml': + self._getRSS2feed(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.port, + self.server.proxyType, + GETstartTime, GETtimings, + self.server.debug) + else: + self._getRSS2site(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.server.port, + self.server.proxyType, + self.server.translate, + GETstartTime, GETtimings, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -9067,8 +9239,11 @@ class PubServer(BaseHTTPRequestHandler): 'GET busy time', 'permitted directory') - if self.path.startswith('/login') or \ - (self.path == '/' and not authorized): + # show the login screen + if (self.path.startswith('/login') or + (self.path == '/' and + not authorized and + not self.server.newsInstance)): # request basic auth msg = htmlLogin(self.server.translate, self.server.baseDir).encode('utf-8') @@ -9080,6 +9255,33 @@ class PubServer(BaseHTTPRequestHandler): 'login shown') return + # show the news front page + if self.path == '/' and \ + not authorized and \ + self.server.newsInstance: + if callingDomain.endswith('.onion') and \ + self.server.onionDomain: + self._logout_redirect('http://' + + self.server.onionDomain + + '/users/news', None, + callingDomain) + elif (callingDomain.endswith('.i2p') and + self.server.i2pDomain): + self._logout_redirect('http://' + + self.server.i2pDomain + + '/users/news', None, + callingDomain) + else: + self._logout_redirect(self.server.httpPrefix + + '://' + + self.server.domainFull + + '/users/news', + None, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'permitted directory', + 'news front page shown') + return + self._benchmarkGETtimings(GETstartTime, GETtimings, 'permitted directory', 'login shown done') @@ -11558,6 +11760,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return + if containsInvalidChars(messageBytes.decode("utf-8")): + self._400() + self.server.POSTbusy = False + return + # convert the raw bytes to json messageJson = json.loads(messageBytes) @@ -11744,7 +11951,9 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(showPublishedDateOnly: bool, +def runDaemon(maxNewswireFeedSizeKb: int, + maxNewswirePostsPerSource: int, + showPublishedDateOnly: bool, votingTimeMins: int, positiveVoting: bool, newswireVotesThreshold: int, @@ -11864,6 +12073,15 @@ def runDaemon(showPublishedDateOnly: bool, # number of votes needed to remove a newswire item from the news timeline # or if positive voting is anabled to add the item to the news timeline httpd.newswireVotesThreshold = newswireVotesThreshold + # maximum overall size of an rss/atom feed read by the newswire daemon + # If the feed is too large then this is probably a DoS attempt + httpd.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb + + # For each newswire source (account or rss feed) + # this is the maximum number of posts to show for each. + # This avoids one or two sources from dominating the news, + # and also prevents big feeds from slowing down page load times + httpd.maxNewswirePostsPerSource = maxNewswirePostsPerSource # Show only the date at the bottom of posts, and not the time httpd.showPublishedDateOnly = showPublishedDateOnly @@ -11929,6 +12147,16 @@ def runDaemon(showPublishedDateOnly: bool, print('Creating news inbox: news@' + domain) createNewsInbox(baseDir, domain, port, httpPrefix) + # set the avatar for the news account + themeName = getConfigParam(baseDir, 'theme') + if not themeName: + themeName = 'default' + setNewsAvatar(baseDir, + themeName, + httpPrefix, + domain, + httpd.domainFull) + if not os.path.isdir(baseDir + '/cache'): os.mkdir(baseDir + '/cache') if not os.path.isdir(baseDir + '/cache/actors'): diff --git a/epicyon-profile.css b/epicyon-profile.css index d691a90c3..a73bf58b7 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -41,7 +41,9 @@ --font-size-tox2: 8px; --time-color: #aaa; --time-vertical-align: 4px; + --publish-button-text: #FFFFFF; --button-text: #FFFFFF; + --publish-button-background: #999; --button-background: #999; --button-background-hover: #777; --button-selected: #666; @@ -50,6 +52,7 @@ --button-selected-highlighted: darkgreen; --button-approve: darkgreen; --button-deny: darkred; + --button-width-chars: 10ch; --button-height: 10px; --button-height-padding-mobile: 20px; --button-height-padding: 10px; @@ -68,7 +71,7 @@ --quote-font-weight: normal; --quote-font-size: 120%; --line-spacing: 130%; - --line-spacing-newswire: 100%; + --line-spacing-newswire: 120%; --newswire-item-moderated-color: white; --newswire-date-moderated-color: white; --column-left-width: 10vw; @@ -81,9 +84,13 @@ --column-left-icon-size: 20%; --column-left-icon-size-mobile: 10%; --column-left-image-width-mobile: 40vw; + --column-right-image-width-mobile: 100vw; --column-right-icon-size: 20%; + --column-right-icon-size-mobile: 10%; --newswire-date-color: white; --newswire-voted-background-color: black; + --login-button-color: #2965; + --login-button-fg-color: black; } @font-face { @@ -577,8 +584,8 @@ input[type=submit] { } .loginButton { - background-color: #2965; - color: #000; + background-color: var(--login-button-color); + color: var(--login-button-fg-color); float: none; margin: 0px 10px; padding: 12px 40px; @@ -1224,11 +1231,27 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 200px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 5px; } + .publishbtn { + border-radius: var(--button-corner-radius); + background-color: var(--publish-button-background); + border: none; + color: var(--publish-button-text); + text-align: center; + font-size: var(--font-size-header); + font-family: Arial, Helvetica, sans-serif; + padding: var(--button-height-padding); + width: 10%; + max-width: 200px; + min-width: var(--button-width-chars); + transition: all 0.5s; + cursor: pointer; + margin: -20px 0px; + } .buttonhighlighted { border-radius: var(--button-corner-radius); background-color: var(--button-highlighted); @@ -1240,7 +1263,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1256,7 +1279,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1272,7 +1295,7 @@ aside .toggle-inside li { padding: var(--button-height-padding); width: 10%; max-width: 100px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 5px; @@ -1566,13 +1589,14 @@ aside .toggle-inside li { } .rightColEditImage { background: var(--main-bg-color); - width: var(--column-right-icon-size); + width: var(--column-right-icon-size-mobile); float: right; margin: 20px 0px; } .rightColImg { background: var(--main-bg-color); - width: 100vw; + width: var(--column-right-image-width-mobile); + float: right; margin: 0 0; padding: 0 0; } @@ -1790,7 +1814,23 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 10ch; + min-width: var(--button-width-chars); + transition: all 0.5s; + cursor: pointer; + margin: 15px; + } + .publishbtn { + border-radius: var(--button-corner-radius); + background-color: var(--publish-button-background); + border: none; + color: var(--publish-button-text); + text-align: center; + font-size: var(--font-size-newswire-mobile); + font-family: Arial, Helvetica, sans-serif; + padding: var(--button-height-padding-mobile); + width: 20%; + max-width: 400px; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1806,7 +1846,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1822,7 +1862,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 15px; @@ -1838,7 +1878,7 @@ aside .toggle-inside li { padding: var(--button-height-padding-mobile); width: 20%; max-width: 400px; - min-width: 10ch; + min-width: var(--button-width-chars); transition: all 0.5s; cursor: pointer; margin: 15px; diff --git a/epicyon.py b/epicyon.py index 775d68682..5b4787869 100644 --- a/epicyon.py +++ b/epicyon.py @@ -112,6 +112,14 @@ parser.add_argument('--i2pDomain', dest='i2pDomain', type=str, parser.add_argument('-p', '--port', dest='port', type=int, default=None, help='Port number to run on') +parser.add_argument('--postsPerSource', + dest='maxNewswirePostsPerSource', type=int, + default=5, + help='Maximum newswire posts per feed or account') +parser.add_argument('--maxFeedSize', + dest='maxNewswireFeedSizeKb', type=int, + default=2048, + help='Maximum newswire rss/atom feed size in K') parser.add_argument('--postcache', dest='maxRecentPosts', type=int, default=512, help='The maximum number of recent posts to store in RAM') @@ -1925,6 +1933,20 @@ dateonly = getConfigParam(baseDir, 'dateonly') if dateonly: args.dateonly = dateonly +# set the maximum number of newswire posts per account or rss feed +maxNewswirePostsPerSource = \ + getConfigParam(baseDir, 'maxNewswirePostsPerSource') +if maxNewswirePostsPerSource: + if maxNewswirePostsPerSource.isdigit(): + args.maxNewswirePostsPerSource = maxNewswirePostsPerSource + +# set the maximum size of a newswire rss/atom feed in Kilobytes +maxNewswireFeedSizeKb = \ + getConfigParam(baseDir, 'maxNewswireFeedSizeKb') +if maxNewswireFeedSizeKb: + if maxNewswireFeedSizeKb.isdigit(): + args.maxNewswireFeedSizeKb = maxNewswireFeedSizeKb + YTDomain = getConfigParam(baseDir, 'youtubedomain') if YTDomain: if '://' in YTDomain: @@ -1938,7 +1960,9 @@ if setTheme(baseDir, themeName, domain): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.dateonly, + runDaemon(args.maxNewswireFeedSizeKb, + args.maxNewswirePostsPerSource, + args.dateonly, args.votingtime, args.positivevoting, args.minimumvotes, diff --git a/img/banner_blue.png b/img/banner_blue.png index f4e664dca..0a17439f1 100644 Binary files a/img/banner_blue.png and b/img/banner_blue.png differ diff --git a/img/banner_night.png b/img/banner_night.png index 921ec44d7..3391f4b48 100644 Binary files a/img/banner_night.png and b/img/banner_night.png differ diff --git a/img/banner_purple.png b/img/banner_purple.png index f6cb9802c..1ca5e48e4 100644 Binary files a/img/banner_purple.png and b/img/banner_purple.png differ diff --git a/img/banner_solidaric.png b/img/banner_solidaric.png index dda908424..4be517046 100644 Binary files a/img/banner_solidaric.png and b/img/banner_solidaric.png differ diff --git a/img/icons/avatar_news.png b/img/icons/avatar_news.png new file mode 100644 index 000000000..95f4fdd10 Binary files /dev/null and b/img/icons/avatar_news.png differ diff --git a/img/icons/blue/add.png b/img/icons/blue/add.png new file mode 100644 index 000000000..02a632a51 Binary files /dev/null and b/img/icons/blue/add.png differ diff --git a/img/icons/blue/avatar_news.png b/img/icons/blue/avatar_news.png new file mode 100644 index 000000000..9c69b7d64 Binary files /dev/null and b/img/icons/blue/avatar_news.png differ diff --git a/img/icons/blue/bookmark.png b/img/icons/blue/bookmark.png new file mode 100644 index 000000000..581db83d5 Binary files /dev/null and b/img/icons/blue/bookmark.png differ diff --git a/img/icons/blue/bookmark_inactive.png b/img/icons/blue/bookmark_inactive.png new file mode 100644 index 000000000..698025b0e Binary files /dev/null and b/img/icons/blue/bookmark_inactive.png differ diff --git a/img/icons/blue/calendar.png b/img/icons/blue/calendar.png new file mode 100644 index 000000000..6d5789c3a Binary files /dev/null and b/img/icons/blue/calendar.png differ diff --git a/img/icons/blue/calendar_notify.png b/img/icons/blue/calendar_notify.png new file mode 100644 index 000000000..ad4be5454 Binary files /dev/null and b/img/icons/blue/calendar_notify.png differ diff --git a/img/icons/blue/delete.png b/img/icons/blue/delete.png new file mode 100644 index 000000000..9904774e3 Binary files /dev/null and b/img/icons/blue/delete.png differ diff --git a/img/icons/blue/dm.png b/img/icons/blue/dm.png new file mode 100644 index 000000000..c0493f82e Binary files /dev/null and b/img/icons/blue/dm.png differ diff --git a/img/icons/blue/download.png b/img/icons/blue/download.png new file mode 100644 index 000000000..3a9605ab7 Binary files /dev/null and b/img/icons/blue/download.png differ diff --git a/img/icons/blue/edit.png b/img/icons/blue/edit.png new file mode 100644 index 000000000..c07dc2dec Binary files /dev/null and b/img/icons/blue/edit.png differ diff --git a/img/icons/blue/edit_notify.png b/img/icons/blue/edit_notify.png new file mode 100644 index 000000000..4b7d3554a Binary files /dev/null and b/img/icons/blue/edit_notify.png differ diff --git a/img/icons/blue/like.png b/img/icons/blue/like.png new file mode 100644 index 000000000..4246ba762 Binary files /dev/null and b/img/icons/blue/like.png differ diff --git a/img/icons/blue/like_inactive.png b/img/icons/blue/like_inactive.png new file mode 100644 index 000000000..dbbe24f5f Binary files /dev/null and b/img/icons/blue/like_inactive.png differ diff --git a/img/icons/blue/links.png b/img/icons/blue/links.png new file mode 100644 index 000000000..4ef5fe6a4 Binary files /dev/null and b/img/icons/blue/links.png differ diff --git a/img/icons/blue/logorss.png b/img/icons/blue/logorss.png new file mode 100644 index 000000000..1490090e2 Binary files /dev/null and b/img/icons/blue/logorss.png differ diff --git a/img/icons/blue/mute.png b/img/icons/blue/mute.png new file mode 100644 index 000000000..5fe808e0f Binary files /dev/null and b/img/icons/blue/mute.png differ diff --git a/img/icons/blue/new.png b/img/icons/blue/new.png new file mode 100644 index 000000000..3b106a6f6 Binary files /dev/null and b/img/icons/blue/new.png differ diff --git a/img/icons/blue/newpost.png b/img/icons/blue/newpost.png new file mode 100644 index 000000000..3bc33deb8 Binary files /dev/null and b/img/icons/blue/newpost.png differ diff --git a/img/icons/blue/newswire.png b/img/icons/blue/newswire.png new file mode 100644 index 000000000..f3521130d Binary files /dev/null and b/img/icons/blue/newswire.png differ diff --git a/img/icons/blue/pagedown.png b/img/icons/blue/pagedown.png new file mode 100644 index 000000000..da7b236d8 Binary files /dev/null and b/img/icons/blue/pagedown.png differ diff --git a/img/icons/blue/pageup.png b/img/icons/blue/pageup.png new file mode 100644 index 000000000..8adf6a8b8 Binary files /dev/null and b/img/icons/blue/pageup.png differ diff --git a/img/icons/blue/person.png b/img/icons/blue/person.png new file mode 100644 index 000000000..ec5ce54a1 Binary files /dev/null and b/img/icons/blue/person.png differ diff --git a/img/icons/blue/prev.png b/img/icons/blue/prev.png new file mode 100644 index 000000000..daa14c763 Binary files /dev/null and b/img/icons/blue/prev.png differ diff --git a/img/icons/blue/qrcode.png b/img/icons/blue/qrcode.png new file mode 100644 index 000000000..933a2671c Binary files /dev/null and b/img/icons/blue/qrcode.png differ diff --git a/img/icons/blue/repeat.png b/img/icons/blue/repeat.png new file mode 100644 index 000000000..62bc3ed0c Binary files /dev/null and b/img/icons/blue/repeat.png differ diff --git a/img/icons/blue/repeat_inactive.png b/img/icons/blue/repeat_inactive.png new file mode 100644 index 000000000..59ec9d794 Binary files /dev/null and b/img/icons/blue/repeat_inactive.png differ diff --git a/img/icons/blue/reply.png b/img/icons/blue/reply.png new file mode 100644 index 000000000..030be3d73 Binary files /dev/null and b/img/icons/blue/reply.png differ diff --git a/img/icons/blue/rss3.png b/img/icons/blue/rss3.png new file mode 100644 index 000000000..83521cd1b Binary files /dev/null and b/img/icons/blue/rss3.png differ diff --git a/img/icons/blue/scope_blog.png b/img/icons/blue/scope_blog.png new file mode 100644 index 000000000..59713257c Binary files /dev/null and b/img/icons/blue/scope_blog.png differ diff --git a/img/icons/blue/scope_dm.png b/img/icons/blue/scope_dm.png new file mode 100644 index 000000000..7c485959c Binary files /dev/null and b/img/icons/blue/scope_dm.png differ diff --git a/img/icons/blue/scope_event.png b/img/icons/blue/scope_event.png new file mode 100644 index 000000000..6d5789c3a Binary files /dev/null and b/img/icons/blue/scope_event.png differ diff --git a/img/icons/blue/scope_followers.png b/img/icons/blue/scope_followers.png new file mode 100644 index 000000000..2e420954c Binary files /dev/null and b/img/icons/blue/scope_followers.png differ diff --git a/img/icons/blue/scope_public.png b/img/icons/blue/scope_public.png new file mode 100644 index 000000000..7f8633ff0 Binary files /dev/null and b/img/icons/blue/scope_public.png differ diff --git a/img/icons/blue/scope_question.png b/img/icons/blue/scope_question.png new file mode 100644 index 000000000..a811b21c6 Binary files /dev/null and b/img/icons/blue/scope_question.png differ diff --git a/img/icons/blue/scope_reminder.png b/img/icons/blue/scope_reminder.png new file mode 100644 index 000000000..809376840 Binary files /dev/null and b/img/icons/blue/scope_reminder.png differ diff --git a/img/icons/blue/scope_report.png b/img/icons/blue/scope_report.png new file mode 100644 index 000000000..7fbd60b74 Binary files /dev/null and b/img/icons/blue/scope_report.png differ diff --git a/img/icons/blue/scope_share.png b/img/icons/blue/scope_share.png new file mode 100644 index 000000000..07fe95502 Binary files /dev/null and b/img/icons/blue/scope_share.png differ diff --git a/img/icons/blue/scope_unlisted.png b/img/icons/blue/scope_unlisted.png new file mode 100644 index 000000000..b3ce02e69 Binary files /dev/null and b/img/icons/blue/scope_unlisted.png differ diff --git a/img/icons/blue/search.png b/img/icons/blue/search.png new file mode 100644 index 000000000..6d3b05c83 Binary files /dev/null and b/img/icons/blue/search.png differ diff --git a/img/icons/blue/showhide.png b/img/icons/blue/showhide.png new file mode 100644 index 000000000..38e6d3275 Binary files /dev/null and b/img/icons/blue/showhide.png differ diff --git a/img/icons/blue/unmute.png b/img/icons/blue/unmute.png new file mode 100644 index 000000000..8adfd15a4 Binary files /dev/null and b/img/icons/blue/unmute.png differ diff --git a/img/icons/blue/vote.png b/img/icons/blue/vote.png new file mode 100644 index 000000000..c810092b0 Binary files /dev/null and b/img/icons/blue/vote.png differ diff --git a/img/icons/hacker/avatar_news.png b/img/icons/hacker/avatar_news.png new file mode 100644 index 000000000..cc9b1a71c Binary files /dev/null and b/img/icons/hacker/avatar_news.png differ diff --git a/img/icons/henge/avatar_news.png b/img/icons/henge/avatar_news.png new file mode 100644 index 000000000..34e898654 Binary files /dev/null and b/img/icons/henge/avatar_news.png differ diff --git a/img/icons/lcd/avatar_news.png b/img/icons/lcd/avatar_news.png new file mode 100644 index 000000000..74cc49969 Binary files /dev/null and b/img/icons/lcd/avatar_news.png differ diff --git a/img/icons/lcd/edit.png b/img/icons/lcd/edit.png index 52be8ef52..18fd100a9 100644 Binary files a/img/icons/lcd/edit.png and b/img/icons/lcd/edit.png differ diff --git a/img/icons/light/avatar_news.png b/img/icons/light/avatar_news.png new file mode 100644 index 000000000..0196a526e Binary files /dev/null and b/img/icons/light/avatar_news.png differ diff --git a/img/icons/night/avatar_news.png b/img/icons/night/avatar_news.png new file mode 100644 index 000000000..7b770cd0b Binary files /dev/null and b/img/icons/night/avatar_news.png differ diff --git a/img/icons/night/logorss.png b/img/icons/night/logorss.png index c5fad44cb..1490090e2 100644 Binary files a/img/icons/night/logorss.png and b/img/icons/night/logorss.png differ diff --git a/img/icons/purple/avatar_news.png b/img/icons/purple/avatar_news.png new file mode 100644 index 000000000..db87e722a Binary files /dev/null and b/img/icons/purple/avatar_news.png differ diff --git a/img/icons/solidaric/avatar_news.png b/img/icons/solidaric/avatar_news.png new file mode 100644 index 000000000..bb95103d5 Binary files /dev/null and b/img/icons/solidaric/avatar_news.png differ diff --git a/img/icons/starlight/avatar_news.png b/img/icons/starlight/avatar_news.png new file mode 100644 index 000000000..1a173f348 Binary files /dev/null and b/img/icons/starlight/avatar_news.png differ diff --git a/img/icons/zen/avatar_news.png b/img/icons/zen/avatar_news.png new file mode 100644 index 000000000..31df35fc6 Binary files /dev/null and b/img/icons/zen/avatar_news.png differ diff --git a/img/image_night.png b/img/image_night.png index ce7b85431..5e9e61b08 100644 Binary files a/img/image_night.png and b/img/image_night.png differ diff --git a/img/left_col_image_henge.png b/img/left_col_image_henge.png new file mode 100644 index 000000000..8fc170604 Binary files /dev/null and b/img/left_col_image_henge.png differ diff --git a/img/left_col_image_night.png b/img/left_col_image_night.png new file mode 100644 index 000000000..5f9c820ce Binary files /dev/null and b/img/left_col_image_night.png differ diff --git a/img/left_col_image_solidaric.png b/img/left_col_image_solidaric.png new file mode 100644 index 000000000..e449f997b Binary files /dev/null and b/img/left_col_image_solidaric.png differ diff --git a/img/left_col_image_starlight.png b/img/left_col_image_starlight.png new file mode 100644 index 000000000..ca846a67a Binary files /dev/null and b/img/left_col_image_starlight.png differ diff --git a/img/login_background_indymedia.jpg b/img/login_background_indymedia.jpg index c71a8a03a..d6927f895 100644 Binary files a/img/login_background_indymedia.jpg and b/img/login_background_indymedia.jpg differ diff --git a/img/right_col_image_henge.png b/img/right_col_image_henge.png new file mode 100644 index 000000000..451b30e8e Binary files /dev/null and b/img/right_col_image_henge.png differ diff --git a/img/right_col_image_night.png b/img/right_col_image_night.png new file mode 100644 index 000000000..b07ce5af1 Binary files /dev/null and b/img/right_col_image_night.png differ diff --git a/img/right_col_image_purple.png b/img/right_col_image_purple.png index 2797c7db3..b910f7429 100644 Binary files a/img/right_col_image_purple.png and b/img/right_col_image_purple.png differ diff --git a/img/right_col_image_solidaric.png b/img/right_col_image_solidaric.png new file mode 100644 index 000000000..2658ce7a8 Binary files /dev/null and b/img/right_col_image_solidaric.png differ diff --git a/img/right_col_image_starlight.png b/img/right_col_image_starlight.png new file mode 100644 index 000000000..9aa486359 Binary files /dev/null and b/img/right_col_image_starlight.png differ diff --git a/img/screenshot_purple.jpg b/img/screenshot_purple.jpg index ce8e3cba5..b2b947721 100644 Binary files a/img/screenshot_purple.jpg and b/img/screenshot_purple.jpg differ diff --git a/img/search_banner_night.png b/img/search_banner_night.png index 921ec44d7..3391f4b48 100644 Binary files a/img/search_banner_night.png and b/img/search_banner_night.png differ diff --git a/img/search_banner_purple.png b/img/search_banner_purple.png index f6cb9802c..1ca5e48e4 100644 Binary files a/img/search_banner_purple.png and b/img/search_banner_purple.png differ diff --git a/newsdaemon.py b/newsdaemon.py index d4d63ee40..0e861bf06 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -202,9 +202,8 @@ def mergeWithPreviousNewswire(oldNewswire: {}, newNewswire: {}) -> None: for published, fields in oldNewswire.items(): if not newNewswire.get(published): continue - newNewswire[published][1] = fields[1] - newNewswire[published][2] = fields[2] - newNewswire[published][3] = fields[3] + for i in range(1, 5): + newNewswire[published][i] = fields[i] def runNewswireDaemon(baseDir: str, httpd, @@ -226,7 +225,10 @@ def runNewswireDaemon(baseDir: str, httpd, # try to update the feeds newNewswire = None try: - newNewswire = getDictFromNewswire(httpd.session, baseDir) + newNewswire = \ + getDictFromNewswire(httpd.session, baseDir, + httpd.maxNewswirePostsPerSource, + httpd.maxNewswireFeedSizeKb) except Exception as e: print('WARN: unable to update newswire ' + str(e)) time.sleep(120) diff --git a/newswire.py b/newswire.py index 2d3045b20..bd964580b 100644 --- a/newswire.py +++ b/newswire.py @@ -16,6 +16,8 @@ from utils import locatePost from utils import loadJson from utils import saveJson from utils import isSuspended +from utils import containsInvalidChars +from blocking import isBlockedDomain def rss2Header(httpPrefix: str, @@ -26,14 +28,17 @@ def rss2Header(httpPrefix: str, rssStr = "" rssStr += "" rssStr += '' + if title.startswith('News'): rssStr += ' Newswire' - else: - rssStr += ' ' + translate[title] + '' - if title.startswith('News'): rssStr += ' ' + httpPrefix + '://' + domainFull + \ '/newswire.xml' + '' + elif title.startswith('Site'): + rssStr += ' ' + domainFull + '' + rssStr += ' ' + httpPrefix + '://' + domainFull + \ + '/blog/rss.xml' + '' else: + rssStr += ' ' + translate[title] + '' rssStr += ' ' + httpPrefix + '://' + domainFull + \ '/users/' + nickname + '/rss.xml' + '' return rssStr @@ -47,13 +52,15 @@ def rss2Footer() -> str: return rssStr -def xml2StrToDict(xmlStr: str, moderated: bool) -> {}: +def xml2StrToDict(baseDir: str, xmlStr: str, moderated: bool, + maxPostsPerSource: int) -> {}: """Converts an xml 2.0 string to a dictionary """ if '' not in xmlStr: return {} result = {} rssItems = xmlStr.split('') + postCtr = 0 for rssItem in rssItems: if '' not in rssItem: continue @@ -75,6 +82,13 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}: description = description.split('</description>')[0] link = rssItem.split('<link>')[1] link = link.split('</link>')[0] + if '://' not in link: + continue + domain = link.split('://')[1] + if '/' in domain: + domain = domain.split('/')[0] + if isBlockedDomain(baseDir, domain): + continue pubDate = rssItem.split('<pubDate>')[1] pubDate = pubDate.split('</pubDate>')[0] parsed = False @@ -86,6 +100,9 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}: result[str(publishedDate)] = [title, link, votesStatus, postFilename, description, moderated] + postCtr += 1 + if postCtr >= maxPostsPerSource: + break parsed = True except BaseException: pass @@ -93,7 +110,15 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT") - result[str(publishedDate) + '+00:00'] = [title, link] + postFilename = '' + votesStatus = [] + result[str(publishedDate) + '+00:00'] = \ + [title, link, + votesStatus, postFilename, + description, moderated] + postCtr += 1 + if postCtr >= maxPostsPerSource: + break parsed = True except BaseException: print('WARN: unrecognized RSS date format: ' + pubDate) @@ -101,13 +126,15 @@ def xml2StrToDict(xmlStr: str, moderated: bool) -> {}: return result -def atomFeedToDict(xmlStr: str, moderated: bool) -> {}: +def atomFeedToDict(baseDir: str, xmlStr: str, moderated: bool, + maxPostsPerSource: int) -> {}: """Converts an atom feed string to a dictionary """ if '<entry>' not in xmlStr: return {} result = {} rssItems = xmlStr.split('<entry>') + postCtr = 0 for rssItem in rssItems: if '<title>' not in rssItem: continue @@ -129,6 +156,13 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}: description = description.split('</summary>')[0] link = rssItem.split('<link>')[1] link = link.split('</link>')[0] + if '://' not in link: + continue + domain = link.split('://')[1] + if '/' in domain: + domain = domain.split('/')[0] + if isBlockedDomain(baseDir, domain): + continue pubDate = rssItem.split('<updated>')[1] pubDate = pubDate.split('</updated>')[0] parsed = False @@ -140,6 +174,9 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}: result[str(publishedDate)] = [title, link, votesStatus, postFilename, description, moderated] + postCtr += 1 + if postCtr >= maxPostsPerSource: + break parsed = True except BaseException: pass @@ -147,7 +184,15 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S UT") - result[str(publishedDate) + '+00:00'] = [title, link] + postFilename = '' + votesStatus = [] + result[str(publishedDate) + '+00:00'] = \ + [title, link, + votesStatus, postFilename, + description, moderated] + postCtr += 1 + if postCtr >= maxPostsPerSource: + break parsed = True except BaseException: print('WARN: unrecognized atom feed date format: ' + pubDate) @@ -155,17 +200,20 @@ def atomFeedToDict(xmlStr: str, moderated: bool) -> {}: return result -def xmlStrToDict(xmlStr: str, moderated: bool) -> {}: +def xmlStrToDict(baseDir: str, xmlStr: str, moderated: bool, + maxPostsPerSource: int) -> {}: """Converts an xml string to a dictionary """ if 'rss version="2.0"' in xmlStr: - return xml2StrToDict(xmlStr, moderated) + return xml2StrToDict(baseDir, xmlStr, moderated, maxPostsPerSource) elif 'xmlns="http://www.w3.org/2005/Atom"' in xmlStr: - return atomFeedToDict(xmlStr, moderated) + return atomFeedToDict(baseDir, xmlStr, moderated, maxPostsPerSource) return {} -def getRSS(session, url: str, moderated: bool) -> {}: +def getRSS(baseDir: str, session, url: str, moderated: bool, + maxPostsPerSource: int, + maxFeedSizeKb: int) -> {}: """Returns an RSS url as a dict """ if not isinstance(url, str): @@ -188,7 +236,13 @@ def getRSS(session, url: str, moderated: bool) -> {}: print('WARN: no session specified for getRSS') try: result = session.get(url, headers=sessionHeaders, params=sessionParams) - return xmlStrToDict(result.text, moderated) + if result: + if int(len(result.text) / 1024) < maxFeedSizeKb and \ + not containsInvalidChars(result.text): + return xmlStrToDict(baseDir, result.text, moderated, + maxPostsPerSource) + else: + print('WARN: feed is too large: ' + url) except requests.exceptions.RequestException as e: print('ERROR: getRSS failed\nurl: ' + str(url) + '\n' + 'headers: ' + str(sessionHeaders) + '\n' + @@ -365,13 +419,16 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, os.remove(newswireModerationFilename) -def getDictFromNewswire(session, baseDir: str) -> {}: +def getDictFromNewswire(session, baseDir: str, + maxPostsPerSource: int, maxFeedSizeKb: int) -> {}: """Gets rss feeds as a dictionary from newswire file """ subscriptionsFilename = baseDir + '/accounts/newswire.txt' if not os.path.isfile(subscriptionsFilename): return {} + maxPostsPerSource = 5 + # add rss feeds rssFeed = [] with open(subscriptionsFilename, 'r') as fp: @@ -394,12 +451,13 @@ def getDictFromNewswire(session, baseDir: str) -> {}: moderated = True url = url.replace('*', '').strip() - itemsList = getRSS(session, url, moderated) + itemsList = getRSS(baseDir, session, url, moderated, + maxPostsPerSource, maxFeedSizeKb) for dateStr, item in itemsList.items(): result[dateStr] = item # add blogs from each user account - addBlogsToNewswire(baseDir, result, 5) + addBlogsToNewswire(baseDir, result, maxPostsPerSource) # sort into chronological order, latest first sortedResult = OrderedDict(sorted(result.items(), reverse=True)) diff --git a/posts.py b/posts.py index 8ef55044b..cd85388d5 100644 --- a/posts.py +++ b/posts.py @@ -1211,13 +1211,12 @@ def createPublicPost(baseDir: str, def createBlogPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, content: str, followersOnly: bool, saveToFile: bool, - clientToServer: bool, + clientToServer: bool, commentsEnabled: bool, attachImageFilename: str, mediaType: str, imageDescription: str, useBlurhash: bool, inReplyTo=None, inReplyToAtomUri=None, subject=None, schedulePost=False, eventDate=None, eventTime=None, location=None) -> {}: - commentsEnabled = True blog = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, @@ -3532,8 +3531,9 @@ def rejectAnnounce(announceFilename: str): """ if not os.path.isfile(announceFilename + '.reject'): rejectAnnounceFile = open(announceFilename + '.reject', "w+") - rejectAnnounceFile.write('\n') - rejectAnnounceFile.close() + if rejectAnnounceFile: + rejectAnnounceFile.write('\n') + rejectAnnounceFile.close() def downloadAnnounce(session, baseDir: str, httpPrefix: str, diff --git a/tests.py b/tests.py index 1f1bc86d6..082108b61 100644 --- a/tests.py +++ b/tests.py @@ -288,7 +288,7 @@ def createServerAlice(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Alice') - runDaemon(False, 0, False, 1, False, False, False, + runDaemon(1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -351,7 +351,7 @@ def createServerBob(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Bob') - runDaemon(False, 0, False, 1, False, False, False, + runDaemon(1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -388,7 +388,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], onionDomain = None i2pDomain = None print('Server running: Eve') - runDaemon(False, 0, False, 1, False, False, False, + runDaemon(1024, 5, False, 0, False, 1, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, diff --git a/theme.py b/theme.py index e236af711..8e5a59a10 100644 --- a/theme.py +++ b/theme.py @@ -293,6 +293,8 @@ def setThemeIndymedia(baseDir: str): "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "darkblue", + "publish-button-background": "#ff9900", + "publish-button-text": "#003366", "button-background": "#003366", "button-selected": "blue", "calendar-bg-color": "#0f0d10", @@ -310,7 +312,9 @@ def setThemeIndymedia(baseDir: str): "column-left-width": "10vw", "column-center-width": "70vw", "column-right-width": "20vw", - "column-right-icon-size": "11%" + "column-right-icon-size": "11%", + "login-button-color": "red", + "login-button-fg-color": "white" } setThemeFromDict(baseDir, name, themeParams, bgParams) @@ -320,6 +324,7 @@ def setThemeBlue(baseDir: str): removeTheme(baseDir) setThemeInConfig(baseDir, name) themeParams = { + "newswire-date-color": "blue", "font-size-header": "22px", "font-size-header-mobile": "32px", "font-size": "45px", @@ -373,17 +378,18 @@ def setThemeNight(baseDir: str): "link-bg-color": "#0f0d10", "main-link-color": "ff9900", "main-link-color-hover": "#d09338", - "main-fg-color": "#a961ab", - "column-left-fg-color": "#a961ab", + "main-fg-color": "#0481f5", + "column-left-fg-color": "#0481f5", "main-bg-color-dm": "#0b0a0a", "border-color": "#606984", "main-bg-color-reply": "#0f0d10", "main-bg-color-report": "#0f0d10", "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", - "button-background-hover": "#6961ab", - "button-background": "#a961ab", - "button-selected": "#86579d", + "button-background-hover": "#0481f5", + "publish-button-background": "#07447c", + "button-background": "#07447c", + "button-selected": "#0481f5", "calendar-bg-color": "#0f0d10", "lines-color": "#a961ab", "day-number": "#a961ab", @@ -412,6 +418,8 @@ def setThemeStarlight(baseDir: str): removeTheme(baseDir) setThemeInConfig(baseDir, name) themeParams = { + "column-left-image-width-mobile": "40vw", + "line-spacing-newswire": "120%", "focus-color": "darkred", "font-size-button-mobile": "36px", "font-size": "32px", @@ -437,6 +445,7 @@ def setThemeStarlight(baseDir: str): "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "#a9282c", + "publish-button-background": "#69282c", "button-background": "#69282c", "button-small-background": "darkblue", "button-selected": "#a34046", @@ -474,6 +483,8 @@ def setThemeHenge(baseDir: str): removeTheme(baseDir) setThemeInConfig(baseDir, name) themeParams = { + "column-left-image-width-mobile": "40vw", + "column-right-image-width-mobile": "40vw", "font-size-button-mobile": "36px", "font-size": "32px", "font-size2": "26px", @@ -498,6 +509,7 @@ def setThemeHenge(baseDir: str): "hashtag-vertical-spacing3": "100px", "hashtag-vertical-spacing4": "150px", "button-background-hover": "#444", + "publish-button-background": "#222", "button-background": "#222", "button-selected": "black", "dropdown-fg-color": "#dddddd", @@ -545,6 +557,7 @@ def setThemeZen(baseDir: str): "title-color": "#dddddd", "main-visited-color": "#dddddd", "button-background-hover": "#a63b35", + "publish-button-background": "#463b35", "button-background": "#463b35", "button-selected": "#26201d", "main-bg-color-dm": "#5c4a40", @@ -591,8 +604,12 @@ def setThemeHighVis(baseDir: str): def setThemeLCD(baseDir: str): name = 'lcd' themeParams = { + "newswire-date-color": "#cfb42b", + "column-left-header-background": "#9fb42b", + "column-left-header-color": "#33390d", "main-bg-color": "#9fb42b", - "column-left-color": "#9fb42b", + "column-left-color": "#33390d", + "column-left-fg-color": "#9fb42b", "link-bg-color": "#33390d", "text-entry-foreground": "#33390d", "text-entry-background": "#9fb42b", @@ -601,7 +618,6 @@ def setThemeLCD(baseDir: str): "main-bg-color-dm": "#5fb42b", "main-header-color-roles": "#9fb42b", "main-fg-color": "#33390d", - "column-left-fg-color": "#33390d", "border-color": "#33390d", "border-width": "5px", "main-link-color": "#9fb42b", @@ -611,9 +627,11 @@ def setThemeLCD(baseDir: str): "button-selected": "black", "button-highlighted": "green", "button-background-hover": "#a3390d", + "publish-button-background": "#33390d", "button-background": "#33390d", "button-small-background": "#33390d", "button-text": "#9fb42b", + "publish-button-text": "#9fb42b", "button-small-text": "#9fb42b", "color: #FFFFFE;": "color: #9fb42b;", "calendar-bg-color": "#eee", @@ -684,9 +702,11 @@ def setThemePurple(baseDir: str): "main-visited-color": "#f93bb0", "button-selected": "#c042a0", "button-background-hover": "#af42a0", + "publish-button-background": "#ff42a0", "button-background": "#ff42a0", "button-small-background": "#ff42a0", "button-text": "white", + "publish-button-text": "white", "button-small-text": "white", "color: #FFFFFE;": "color: #1f152d;", "calendar-bg-color": "#eee", @@ -735,9 +755,11 @@ def setThemeHacker(baseDir: str): "main-visited-color": "#3c8234", "button-selected": "#063200", "button-background-hover": "#a62200", + "publish-button-background": "#062200", "button-background": "#062200", "button-small-background": "#062200", "button-text": "#00ff00", + "publish-button-text": "#00ff00", "button-small-text": "#00ff00", "button-corner-radius": "4px", "timeline-border-radius": "4px", @@ -830,6 +852,9 @@ def setThemeLight(baseDir: str): def setThemeSolidaric(baseDir: str): name = 'solidaric' themeParams = { + "button-highlighted": "darkred", + "button-selected-highlighted": "darkred", + "newswire-date-color": "grey", "focus-color": "grey", "font-size-button-mobile": "36px", "font-size": "32px", @@ -1006,6 +1031,31 @@ def setThemeImages(baseDir: str, name: str) -> None: pass +def setNewsAvatar(baseDir: str, name: str, + httpPrefix: str, + domain: str, domainFull: str) -> None: + """Sets the avatar for the news account + """ + nickname = 'news' + newFilename = baseDir + '/img/icons/' + name + '/avatar_news.png' + if not os.path.isfile(newFilename): + newFilename = baseDir + '/img/icons/avatar_news.png' + if not os.path.isfile(newFilename): + return + avatarFilename = \ + httpPrefix + '://' + domainFull + '/users/' + nickname + '.png' + avatarFilename = avatarFilename.replace('/', '-') + filename = baseDir + '/cache/avatars/' + avatarFilename + + if os.path.isfile(filename): + os.remove(filename) + if os.path.isdir(baseDir + '/cache/avatars'): + copyfile(newFilename, filename) + copyfile(newFilename, + baseDir + '/accounts/' + + nickname + '@' + domain + '/avatar.png') + + def setTheme(baseDir: str, name: str, domain: str) -> bool: result = False diff --git a/translations/ar.json b/translations/ar.json index ca37a9949..de49c1547 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -308,5 +308,8 @@ "Read more...": "اقرأ أكثر...", "Edit News Post": "تحرير منشور الأخبار", "A list of editor nicknames. One per line.": "قائمة بأسماء المحرر. واحد في كل سطر.", - "Site Editors": "محررو الموقع" + "Site Editors": "محررو الموقع", + "Allow news posts": "السماح بنشر الأخبار", + "Publish": "ينشر", + "Publish a news article": "انشر مقالة إخبارية" } diff --git a/translations/ca.json b/translations/ca.json index 78a90aff9..3499c9de7 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -308,5 +308,8 @@ "Read more...": "Llegeix més...", "Edit News Post": "Edita la publicació de notícies", "A list of editor nicknames. One per line.": "Una llista de sobrenoms de l'editor. Un per línia.", - "Site Editors": "Editors de llocs" + "Site Editors": "Editors de llocs", + "Allow news posts": "Permet publicacions de notícies", + "Publish": "Publica", + "Publish a news article": "Publicar un article de notícies" } diff --git a/translations/cy.json b/translations/cy.json index a68b2456f..2ab1795cc 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -308,5 +308,8 @@ "Read more...": "Darllen mwy...", "Edit News Post": "Golygu News News", "A list of editor nicknames. One per line.": "Rhestr o lysenwau golygydd. Un i bob llinell.", - "Site Editors": "Golygyddion Safle" + "Site Editors": "Golygyddion Safle", + "Allow news posts": "Caniatáu swyddi newyddion", + "Publish": "Cyhoeddi", + "Publish a news article": "Cyhoeddi erthygl newyddion" } diff --git a/translations/de.json b/translations/de.json index 06bdd7e98..ceb6179e6 100644 --- a/translations/de.json +++ b/translations/de.json @@ -308,5 +308,8 @@ "Read more...": "Weiterlesen...", "Edit News Post": "Nachrichtenbeitrag bearbeiten", "A list of editor nicknames. One per line.": "Eine Liste der Editor-Spitznamen. Eine pro Zeile.", - "Site Editors": "Site-Editoren" + "Site Editors": "Site-Editoren", + "Allow news posts": "Nachrichtenbeiträge zulassen", + "Publish": "Veröffentlichen", + "Publish a news article": "Veröffentlichen Sie einen Nachrichtenartikel" } diff --git a/translations/en.json b/translations/en.json index 9a10f674f..e5a65517c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -308,5 +308,8 @@ "Read more...": "Read more...", "Edit News Post": "Edit News Post", "A list of editor nicknames. One per line.": "A list of editor nicknames. One per line.", - "Site Editors": "Site Editors" + "Site Editors": "Site Editors", + "Allow news posts": "Allow news posts", + "Publish": "Publish", + "Publish a news article": "Publish a news article" } diff --git a/translations/es.json b/translations/es.json index 6970b49bd..32c7ac5f8 100644 --- a/translations/es.json +++ b/translations/es.json @@ -308,5 +308,8 @@ "Read more...": "Lee mas...", "Edit News Post": "Editar publicación de noticias", "A list of editor nicknames. One per line.": "Una lista de apodos de los editores. Uno por línea.", - "Site Editors": "Editores del sitio" + "Site Editors": "Editores del sitio", + "Allow news posts": "Permitir publicaciones de noticias", + "Publish": "Publicar", + "Publish a news article": "Publica un artículo de noticias" } diff --git a/translations/fr.json b/translations/fr.json index 65d770121..068971bde 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -308,5 +308,8 @@ "Read more...": "Lire la suite...", "Edit News Post": "Modifier l'article d'actualité", "A list of editor nicknames. One per line.": "Une liste de surnoms d'éditeur. Un par ligne.", - "Site Editors": "Éditeurs du site" + "Site Editors": "Éditeurs du site", + "Allow news posts": "Autoriser les articles d'actualité", + "Publish": "Publier", + "Publish a news article": "Publier un article de presse" } diff --git a/translations/ga.json b/translations/ga.json index d97ad2671..547f6b56a 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -308,5 +308,8 @@ "Read more...": "Leigh Nios mo...", "Edit News Post": "Cuir News Post in eagar", "A list of editor nicknames. One per line.": "Liosta leasainmneacha eagarthóra. Ceann in aghaidh na líne.", - "Site Editors": "Eagarthóirí Suímh" + "Site Editors": "Eagarthóirí Suímh", + "Allow news posts": "Ceadaigh poist nuachta", + "Publish": "Fhoilsiú", + "Publish a news article": "Foilsigh alt nuachta" } diff --git a/translations/hi.json b/translations/hi.json index f1ed564bb..758a933ab 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -308,5 +308,8 @@ "Read more...": "अधिक पढ़ें...", "Edit News Post": "समाचार पोस्ट संपादित करें", "A list of editor nicknames. One per line.": "संपादक उपनामों की एक सूची। प्रति पंक्ति एक।", - "Site Editors": "साइट संपादकों" + "Site Editors": "साइट संपादकों", + "Allow news posts": "समाचार पोस्ट की अनुमति दें", + "Publish": "प्रकाशित करना", + "Publish a news article": "एक समाचार लेख प्रकाशित करें" } diff --git a/translations/it.json b/translations/it.json index d01e1fd46..e103f50c9 100644 --- a/translations/it.json +++ b/translations/it.json @@ -308,5 +308,8 @@ "Read more...": "Leggi di più...", "Edit News Post": "Modifica post di notizie", "A list of editor nicknames. One per line.": "Un elenco di soprannomi dell'editor. Uno per riga.", - "Site Editors": "Editori del sito" + "Site Editors": "Editori del sito", + "Allow news posts": "Consenti post di notizie", + "Publish": "Pubblicare", + "Publish a news article": "Pubblica un articolo di notizie" } diff --git a/translations/ja.json b/translations/ja.json index 1867a618d..ac44cd3e0 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -308,5 +308,8 @@ "Read more...": "続きを読む...", "Edit News Post": "ニュース投稿を編集する", "A list of editor nicknames. One per line.": "編集者のニックネームのリスト。 1行に1つ。", - "Site Editors": "サイト編集者" + "Site Editors": "サイト編集者", + "Allow news posts": "ニュース投稿を許可する", + "Publish": "公開する", + "Publish a news article": "ニュース記事を公開する" } diff --git a/translations/oc.json b/translations/oc.json index 5f54ec75d..3e7c8007c 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -304,5 +304,8 @@ "Read more...": "Read more...", "Edit News Post": "Edit News Post", "A list of editor nicknames. One per line.": "A list of editor nicknames. One per line.", - "Site Editors": "Site Editors" + "Site Editors": "Site Editors", + "Allow news posts": "Allow news posts", + "Publish": "Publish", + "Publish a news article": "Publish a news article" } diff --git a/translations/pt.json b/translations/pt.json index 78832177e..a894c6c02 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -308,5 +308,8 @@ "Read more...": "Consulte Mais informação...", "Edit News Post": "Editar Postagem de Notícias", "A list of editor nicknames. One per line.": "Uma lista de apelidos de editores. Um por linha.", - "Site Editors": "Editores do site" + "Site Editors": "Editores do site", + "Allow news posts": "Permitir postagens de notícias", + "Publish": "Publicar", + "Publish a news article": "Publique um artigo de notícias" } diff --git a/translations/ru.json b/translations/ru.json index 798d51b18..6ae4b03f5 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -308,5 +308,8 @@ "Read more...": "Подробнее...", "Edit News Post": "Редактировать новость", "A list of editor nicknames. One per line.": "Список ников редакторов. По одному на строку.", - "Site Editors": "Редакторы сайта" + "Site Editors": "Редакторы сайта", + "Allow news posts": "Разрешить публикации новостей", + "Publish": "Публиковать", + "Publish a news article": "Опубликовать новостную статью" } diff --git a/translations/zh.json b/translations/zh.json index ffd2dd28c..d2a33c46b 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -308,5 +308,8 @@ "Read more...": "阅读更多...", "Edit News Post": "编辑新闻帖子", "A list of editor nicknames. One per line.": "编辑者昵称列表。 每行一个。", - "Site Editors": "网站编辑" + "Site Editors": "网站编辑", + "Allow news posts": "允许新闻发布", + "Publish": "发布", + "Publish a news article": "发布新闻文章" } diff --git a/utils.py b/utils.py index da689e60f..6ed6c1c62 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,14 @@ from calendar import monthrange from followingCalendar import addPersonToCalendar +def isSystemAccount(nickname: str) -> bool: + """Returns true if the given nickname is a system account + """ + if nickname == 'news' or nickname == 'inbox': + return True + return False + + def createConfig(baseDir: str) -> None: """Creates a configuration file """ @@ -49,7 +57,7 @@ def getConfigParam(baseDir: str, variableName: str): configFilename = baseDir + '/config.json' configJson = loadJson(configFilename) if configJson: - if configJson.get(variableName): + if variableName in configJson: return configJson[variableName] return None @@ -265,6 +273,19 @@ def isEvil(domain: str) -> bool: return False +def containsInvalidChars(jsonStr: str) -> bool: + """Does the given json string contain invalid characters? + e.g. dubious clacks/admin dogwhistles + """ + invalidStrings = { + '卐', '卍', '࿕', '࿖', '࿗', '࿘' + } + for isInvalid in invalidStrings: + if isInvalid in jsonStr: + return True + return False + + def createPersonDir(nickname: str, domain: str, baseDir: str, dirname: str) -> str: """Create a directory for a person diff --git a/webinterface.py b/webinterface.py index 849313626..14b4aa121 100644 --- a/webinterface.py +++ b/webinterface.py @@ -25,6 +25,7 @@ from ssb import getSSBAddress from tox import getToxAddress from matrix import getMatrixAddress from donate import getDonationUrl +from utils import isSystemAccount from utils import removeIdEnding from utils import getProtocolPrefixes from utils import searchBoxPosts @@ -3232,7 +3233,7 @@ def htmlProfile(defaultTimeline: str, session, wfRequest: {}, personCache: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, - extraJson=None, + newswire: {}, extraJson=None, pageNumber=None, maxItemsPerPage=None) -> str: """Show the profile page as html """ @@ -3296,7 +3297,7 @@ def htmlProfile(defaultTimeline: str, PGPfingerprint or emailAddress: donateSection = '<div class="container">\n' donateSection += ' <center>\n' - if donateUrl: + if donateUrl and not isSystemAccount(nickname): donateSection += \ ' <p><a href="' + donateUrl + \ '"><button class="donateButton">' + translate['Donate'] + \ @@ -3415,55 +3416,82 @@ def htmlProfile(defaultTimeline: str, avatarDescription = profileJson['summary'].replace('<br>', '\n') avatarDescription = avatarDescription.replace('<p>', '') avatarDescription = avatarDescription.replace('</p>', '') - profileHeaderStr = '<div class="hero-image">' - profileHeaderStr += ' <div class="hero-text">' - profileHeaderStr += \ - ' <img loading="lazy" src="' + profileJson['icon']['url'] + \ - '" title="' + avatarDescription + '" alt="' + \ - avatarDescription + '" class="title">' - profileHeaderStr += ' <h1>' + displayName + '</h1>' - iconsDir = getIconsDir(baseDir) - profileHeaderStr += \ - '<p><b>@' + nickname + '@' + domainFull + '</b><br>' - profileHeaderStr += \ - '<a href="/users/' + nickname + \ - '/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \ - translate['QR Code'] + '">' + \ - '<img class="qrcode" src="/' + iconsDir + '/qrcode.png" /></a></p>' - profileHeaderStr += ' <p>' + profileDescriptionShort + '</p>' - profileHeaderStr += loginButton - profileHeaderStr += ' </div>' - profileHeaderStr += '</div>' + + # If this is the news account then show a different banner + if isSystemAccount(nickname): + profileHeaderStr = '<div class="timeline-banner"></div>\n' + profileHeaderStr += '<center>' + loginButton + '</center>\n' + + profileHeaderStr += '<table class="timeline">\n' + profileHeaderStr += ' <colgroup>\n' + profileHeaderStr += ' <col span="1" class="column-left">\n' + profileHeaderStr += ' <col span="1" class="column-center">\n' + profileHeaderStr += ' <col span="1" class="column-right">\n' + profileHeaderStr += ' </colgroup>\n' + profileHeaderStr += ' <tbody>\n' + profileHeaderStr += ' <tr>\n' + profileHeaderStr += ' <td valign="top" class="col-left">\n' + iconsDir = getIconsDir(baseDir) + profileHeaderStr += \ + getLeftColumnContent(baseDir, 'news', domainFull, + httpPrefix, translate, + iconsDir, False, + False, None) + profileHeaderStr += ' </td>\n' + profileHeaderStr += ' <td valign="top" class="col-center">\n' + else: + profileHeaderStr = '<div class="hero-image">\n' + profileHeaderStr += ' <div class="hero-text">\n' + profileHeaderStr += \ + ' <img loading="lazy" src="' + profileJson['icon']['url'] + \ + '" title="' + avatarDescription + '" alt="' + \ + avatarDescription + '" class="title">\n' + profileHeaderStr += ' <h1>' + displayName + '</h1>\n' + iconsDir = getIconsDir(baseDir) + profileHeaderStr += \ + '<p><b>@' + nickname + '@' + domainFull + '</b><br>' + profileHeaderStr += \ + '<a href="/users/' + nickname + \ + '/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \ + translate['QR Code'] + '">' + \ + '<img class="qrcode" src="/' + iconsDir + \ + '/qrcode.png" /></a></p>\n' + profileHeaderStr += ' <p>' + profileDescriptionShort + '</p>\n' + profileHeaderStr += loginButton + profileHeaderStr += ' </div>\n' + profileHeaderStr += '</div>\n' profileStr = \ linkToTimelineStart + profileHeaderStr + \ linkToTimelineEnd + donateSection profileStr += '<div class="container" id="buttonheader">\n' profileStr += ' <center>' - profileStr += \ - ' <a href="' + usersPath + '#buttonheader"><button class="' + \ - postsButton + '"><span>' + translate['Posts'] + \ - ' </span></button></a>' - profileStr += \ - ' <a href="' + usersPath + '/following#buttonheader">' + \ - '<button class="' + followingButton + '"><span>' + \ - translate['Following'] + ' </span></button></a>' - profileStr += \ - ' <a href="' + usersPath + '/followers#buttonheader">' + \ - '<button class="' + followersButton + \ - '"><span>' + translate['Followers'] + ' </span></button></a>' - profileStr += \ - ' <a href="' + usersPath + '/roles#buttonheader">' + \ - '<button class="' + rolesButton + '"><span>' + translate['Roles'] + \ - ' </span></button></a>' - profileStr += \ - ' <a href="' + usersPath + '/skills#buttonheader">' + \ - '<button class="' + skillsButton + '"><span>' + \ - translate['Skills'] + ' </span></button></a>' - profileStr += \ - ' <a href="' + usersPath + '/shares#buttonheader">' + \ - '<button class="' + sharesButton + '"><span>' + \ - translate['Shares'] + ' </span></button></a>' + if not isSystemAccount(nickname): + profileStr += \ + ' <a href="' + usersPath + '#buttonheader"><button class="' + \ + postsButton + '"><span>' + translate['Posts'] + \ + ' </span></button></a>' + profileStr += \ + ' <a href="' + usersPath + '/following#buttonheader">' + \ + '<button class="' + followingButton + '"><span>' + \ + translate['Following'] + ' </span></button></a>' + profileStr += \ + ' <a href="' + usersPath + '/followers#buttonheader">' + \ + '<button class="' + followersButton + \ + '"><span>' + translate['Followers'] + ' </span></button></a>' + profileStr += \ + ' <a href="' + usersPath + '/roles#buttonheader">' + \ + '<button class="' + rolesButton + '"><span>' + \ + translate['Roles'] + \ + ' </span></button></a>' + profileStr += \ + ' <a href="' + usersPath + '/skills#buttonheader">' + \ + '<button class="' + skillsButton + '"><span>' + \ + translate['Skills'] + ' </span></button></a>' + profileStr += \ + ' <a href="' + usersPath + '/shares#buttonheader">' + \ + '<button class="' + sharesButton + '"><span>' + \ + translate['Shares'] + ' </span></button></a>' profileStr += editProfileStr + logoutStr profileStr += ' </center>' profileStr += '</div>' @@ -3477,6 +3505,12 @@ def htmlProfile(defaultTimeline: str, profileStyle = \ cssFile.read().replace('image.png', profileJson['image']['url']) + if isSystemAccount(nickname): + bannerFile, bannerFilename = \ + getBannerFile(baseDir, nickname, domain) + profileStyle = \ + profileStyle.replace('banner.png', + '/users/' + nickname + '/' + bannerFile) licenseStr = \ '<a href="https://gitlab.com/bashrc2/epicyon">' + \ @@ -3522,8 +3556,27 @@ def htmlProfile(defaultTimeline: str, htmlProfileShares(actor, translate, nickname, domainFull, extraJson) + licenseStr + + # Footer which is only used for system accounts + profileFooterStr = '' + if isSystemAccount(nickname): + profileFooterStr = ' </td>\n' + profileFooterStr += ' <td valign="top" class="col-right">\n' + iconsDir = getIconsDir(baseDir) + profileFooterStr += \ + getRightColumnContent(baseDir, 'news', domainFull, + httpPrefix, translate, + iconsDir, False, False, + newswire, False, + False, None, False) + profileFooterStr += ' </td>\n' + profileFooterStr += ' </tr>\n' + profileFooterStr += ' </tbody>\n' + profileFooterStr += '</table>\n' + profileStr = \ - htmlHeader(cssFilename, profileStyle) + profileStr + htmlFooter() + htmlHeader(cssFilename, profileStyle) + \ + profileStr + profileFooterStr + htmlFooter() return profileStr @@ -4426,21 +4479,35 @@ def individualPostAsHtml(allowDownloads: bool, if timeDiff > 100: print('TIMING INDIV ' + boxName + ' 7 = ' + str(timeDiff)) - avatarLink = ' <a class="imageAnchor" href="' + postActor + '">' - avatarLink += \ - ' <img loading="lazy" src="' + avatarUrl + '" title="' + \ - translate['Show profile'] + '" alt=" "' + avatarPosition + '/></a>\n' + if '/users/news/' not in avatarUrl: + avatarLink = ' <a class="imageAnchor" href="' + postActor + '">' + avatarLink += \ + ' <img loading="lazy" src="' + avatarUrl + '" title="' + \ + translate['Show profile'] + '" alt=" "' + avatarPosition + \ + '/></a>\n' + else: + avatarLink += \ + ' <img loading="lazy" src="' + avatarUrl + '" title="' + \ + translate['Show profile'] + '" alt=" "' + avatarPosition + \ + '/>\n' if showAvatarOptions and \ fullDomain + '/users/' + nickname not in postActor: - avatarLink = \ - ' <a class="imageAnchor" href="/users/' + \ - nickname + '?options=' + postActor + \ - ';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">\n' - avatarLink += \ - ' <img loading="lazy" title="' + \ - translate['Show options for this person'] + \ - '" src="' + avatarUrl + '" ' + avatarPosition + '/></a>\n' + if '/users/news/' not in avatarUrl: + avatarLink = \ + ' <a class="imageAnchor" href="/users/' + \ + nickname + '?options=' + postActor + \ + ';' + str(pageNumber) + ';' + avatarUrl + messageIdStr + '">\n' + avatarLink += \ + ' <img loading="lazy" title="' + \ + translate['Show options for this person'] + \ + '" src="' + avatarUrl + '" ' + avatarPosition + '/></a>\n' + else: + # don't link to the person options for the news account + avatarLink += \ + ' <img loading="lazy" title="' + \ + translate['Show options for this person'] + \ + '" src="' + avatarUrl + '" ' + avatarPosition + '/>\n' avatarImageInPost = \ ' <div class="timeline-avatar">' + avatarLink.strip() + '</div>\n' @@ -4929,20 +4996,24 @@ def individualPostAsHtml(allowDownloads: bool, if announceAvatarUrl: idx = 'Show options for this person' - replyAvatarImageInPost = \ - ' ' \ - '<div class="timeline-avatar-reply">\n' \ - ' <a class="imageAnchor" ' + \ - 'href="/users/' + nickname + \ - '?options=' + \ - announceActor + ';' + str(pageNumber) + \ - ';' + announceAvatarUrl + \ - messageIdStr + '">' \ - '<img loading="lazy" src="' + \ - announceAvatarUrl + '" ' \ - 'title="' + translate[idx] + \ - '" alt=" "' + avatarPosition + \ - '/></a>\n </div>\n' + if '/users/news/' not in announceAvatarUrl: + replyAvatarImageInPost = \ + ' ' \ + '<div class=' + \ + '"timeline-avatar-reply">\n' \ + ' ' + \ + '<a class="imageAnchor" ' + \ + 'href="/users/' + nickname + \ + '?options=' + \ + announceActor + ';' + \ + str(pageNumber) + \ + ';' + announceAvatarUrl + \ + messageIdStr + '">' \ + '<img loading="lazy" src="' + \ + announceAvatarUrl + '" ' \ + 'title="' + translate[idx] + \ + '" alt=" "' + avatarPosition + \ + '/></a>\n </div>\n' else: titleStr += \ ' <img loading="lazy" title="' + \ @@ -5437,10 +5508,15 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, iconsDir + '/edit.png" /></a>\n' # RSS icon + if nickname != 'news': + # rss feed for this account + rssUrl = httpPrefix + '://' + domainFull + \ + '/blog/' + nickname + '/rss.xml' + else: + # rss feed for all accounts on the instance + rssUrl = httpPrefix + '://' + domainFull + '/blog/rss.xml' htmlStr += \ - ' <a href="' + \ - httpPrefix + '://' + domainFull + \ - '/blog/' + nickname + '/rss.xml">' + \ + ' <a href="' + rssUrl + '">' + \ '<img class="' + editImageClass + \ '" loading="lazy" alt="' + \ translate['RSS feed for this site'] + \ @@ -5513,7 +5589,7 @@ def votesIndicator(totalVotes: int, positiveVoting: bool) -> str: return totalVotesStr -def htmlNewswire(newswire: str, nickname: str, moderator: bool, +def htmlNewswire(newswire: {}, nickname: str, moderator: bool, translate: {}, positiveVoting: bool, iconsDir: str) -> str: """Converts a newswire dict into html """ @@ -5521,7 +5597,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, for dateStr, item in newswire.items(): publishedDate = \ datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S+00:00") - dateShown = publishedDate.strftime("%Y-%m-%d") + dateShown = publishedDate.strftime("%Y-%m-%d %H:%M") dateStrLink = dateStr.replace('T', ' ') dateStrLink = dateStrLink.replace('Z', '') @@ -5584,7 +5660,8 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsDir: str, moderator: bool, editor: bool, newswire: {}, positiveVoting: bool, - showBackButton: bool, timelinePath: str) -> str: + showBackButton: bool, timelinePath: str, + showPublishButton: bool) -> str: """Returns html content for the right column """ htmlStr = '' @@ -5627,6 +5704,14 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, '<button class="cancelbtn">' + \ translate['Go Back'] + '</button></a>\n' + if showPublishButton: + htmlStr += \ + ' <a href="' + \ + '/users/' + nickname + '/newblog" ' + \ + 'title="' + translate['Publish a news article'] + '">' + \ + '<button class="publishbtn">' + \ + translate['Publish'] + '</button></a>\n' + if editor: if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'): # show the edit icon highlighted @@ -5744,11 +5829,36 @@ def htmlNewswireMobile(baseDir: str, nickname: str, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, - True, timelinePath) + True, timelinePath, True) htmlStr += htmlFooter() return htmlStr +def getBannerFile(baseDir: str, nickname: str, domain: str) -> (str, str): + """ + returns the banner filename + """ + # filename of the banner shown at the top + bannerFile = 'banner.png' + bannerFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + bannerFile + if not os.path.isfile(bannerFilename): + bannerFile = 'banner.jpg' + bannerFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + bannerFile + if not os.path.isfile(bannerFilename): + bannerFile = 'banner.gif' + bannerFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + bannerFile + if not os.path.isfile(bannerFilename): + bannerFile = 'banner.avif' + bannerFilename = baseDir + '/accounts/' + \ + nickname + '@' + domain + '/' + bannerFile + if not os.path.isfile(bannerFilename): + bannerFile = 'banner.webp' + return bannerFile, bannerFilename + + def htmlTimeline(defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, @@ -5824,23 +5934,7 @@ def htmlTimeline(defaultTimeline: str, cssFilename = baseDir + '/epicyon.css' # filename of the banner shown at the top - bannerFile = 'banner.png' - bannerFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/' + bannerFile - if not os.path.isfile(bannerFilename): - bannerFile = 'banner.jpg' - bannerFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/' + bannerFile - if not os.path.isfile(bannerFilename): - bannerFile = 'banner.gif' - bannerFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/' + bannerFile - if not os.path.isfile(bannerFilename): - bannerFile = 'banner.avif' - bannerFilename = baseDir + '/accounts/' + \ - nickname + '@' + domain + '/' + bannerFile - if not os.path.isfile(bannerFilename): - bannerFile = 'banner.webp' + bannerFile, bannerFilename = getBannerFile(baseDir, nickname, domain) # benchmark 1 timeDiff = int((time.time() - timelineStartTime) * 1000) @@ -6136,7 +6230,7 @@ def htmlTimeline(defaultTimeline: str, # typically the blogs button # but may change if this is a blogging oriented instance if defaultTimeline != 'tlblogs': - if not minimal: + if not minimal or defaultTimeline == 'tlnews': tlStr += \ ' <a href="' + usersPath + \ '/tlblogs"><button class="' + \ @@ -6422,7 +6516,7 @@ def htmlTimeline(defaultTimeline: str, httpPrefix, translate, iconsDir, moderator, editor, newswire, positiveVoting, - False, None) + False, None, True) tlStr += ' <td valign="top" class="col-right">' + \ rightColumnStr + ' </td>\n' tlStr += ' </tr>\n' @@ -7190,7 +7284,8 @@ def htmlUnfollowConfirm(translate: {}, baseDir: str, def htmlPersonOptions(translate: {}, baseDir: str, - domain: str, originPathStr: str, + domain: str, domainFull: str, + originPathStr: str, optionsActor: str, optionsProfileUrl: str, optionsLink: str, @@ -7328,24 +7423,37 @@ def htmlPersonOptions(translate: {}, baseDir: str, 'name="submitPetname">' + \ translate['Submit'] + '</button><br>\n' + # checkbox for receiving calendar events if isFollowingActor(baseDir, nickname, domain, optionsActor): - if receivingCalendarEvents(baseDir, nickname, domain, - optionsNickname, optionsDomainFull): - optionsStr += \ + checkboxStr = \ + ' <input type="checkbox" ' + \ + 'class="profilecheckbox" name="onCalendar" checked> ' + \ + translate['Receive calendar events from this account'] + \ + '\n <button type="submit" class="buttonsmall" ' + \ + 'name="submitOnCalendar">' + \ + translate['Submit'] + '</button><br>\n' + if not receivingCalendarEvents(baseDir, nickname, domain, + optionsNickname, optionsDomainFull): + checkboxStr = checkboxStr.replace(' checked>', '>') + optionsStr += checkboxStr + + # checkbox for permission to post to newswire + if optionsDomainFull == domainFull: + if isModerator(baseDir, nickname) and \ + not isModerator(baseDir, optionsNickname): + newswireBlockedFilename = \ + baseDir + '/accounts/' + \ + optionsNickname + '@' + optionsDomain + '/.nonewswire' + checkboxStr = \ ' <input type="checkbox" ' + \ - 'class="profilecheckbox" name="onCalendar" checked> ' + \ - translate['Receive calendar events from this account'] + \ + 'class="profilecheckbox" name="postsToNews" checked> ' + \ + translate['Allow news posts'] + \ '\n <button type="submit" class="buttonsmall" ' + \ - 'name="submitOnCalendar">' + \ - translate['Submit'] + '</button><br>\n' - else: - optionsStr += \ - ' <input type="checkbox" ' + \ - 'class="profilecheckbox" name="onCalendar"> ' + \ - translate['Receive calendar events from this account'] + \ - '\n <button type="submit" class="buttonsmall" ' + \ - 'name="submitOnCalendar">' + \ + 'name="submitPostToNews">' + \ translate['Submit'] + '</button><br>\n' + if os.path.isfile(newswireBlockedFilename): + checkboxStr = checkboxStr.replace(' checked>', '>') + optionsStr += checkboxStr optionsStr += optionsLinkStr optionsStr += \