From f4cb24490af06bf2088df5223bdbc3f91035db1d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 09:58:44 +0100 Subject: [PATCH 001/263] Tidying --- config.py | 46 ------------------------------------------- daemon.py | 6 +++--- epicyon.py | 4 ++-- newswire.py | 13 +++---------- person.py | 21 ++------------------ posts.py | 2 +- utils.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ webinterface.py | 2 +- 8 files changed, 64 insertions(+), 82 deletions(-) delete mode 100644 config.py diff --git a/config.py b/config.py deleted file mode 100644 index b290797bd..000000000 --- a/config.py +++ /dev/null @@ -1,46 +0,0 @@ -__filename__ = "config.py" -__author__ = "Bob Mottram" -__license__ = "AGPL3+" -__version__ = "1.1.0" -__maintainer__ = "Bob Mottram" -__email__ = "bob@freedombone.net" -__status__ = "Production" - -import os -from utils import loadJson -from utils import saveJson - - -def createConfig(baseDir: str) -> None: - """Creates a configuration file - """ - configFilename = baseDir + '/config.json' - if os.path.isfile(configFilename): - return - configJson = { - } - saveJson(configJson, configFilename) - - -def setConfigParam(baseDir: str, variableName: str, variableValue) -> None: - """Sets a configuration value - """ - createConfig(baseDir) - configFilename = baseDir + '/config.json' - configJson = {} - if os.path.isfile(configFilename): - configJson = loadJson(configFilename) - configJson[variableName] = variableValue - saveJson(configJson, configFilename) - - -def getConfigParam(baseDir: str, variableName: str): - """Gets a configuration value - """ - createConfig(baseDir) - configFilename = baseDir + '/config.json' - configJson = loadJson(configFilename) - if configJson: - if configJson.get(variableName): - return configJson[variableName] - return None diff --git a/daemon.py b/daemon.py index 2b242ed2b..f159f9e9e 100644 --- a/daemon.py +++ b/daemon.py @@ -54,7 +54,6 @@ from person import registerAccount from person import personLookup from person import personBoxJson from person import createSharedInbox -from person import isSuspended from person import suspendAccount from person import unsuspendAccount from person import removeAccount @@ -101,8 +100,6 @@ from blocking import removeGlobalBlock from blocking import isBlockedHashtag from blocking import isBlockedDomain from blocking import getDomainBlocklist -from config import setConfigParam -from config import getConfigParam from roles import setRole from roles import clearModeratorStatus from blog import htmlBlogPageRSS2 @@ -159,6 +156,8 @@ from shares import getSharesFeedForPerson from shares import addShare from shares import removeShare from shares import expireShares +from utils import setConfigParam +from utils import getConfigParam from utils import removeIdEnding from utils import updateLikesCollection from utils import undoLikesCollectionEntry @@ -174,6 +173,7 @@ from utils import getStatusNumber from utils import urlPermitted from utils import loadJson from utils import saveJson +from utils import isSuspended from manualapprove import manualDenyFollowRequest from manualapprove import manualApproveFollowRequest from announce import createAnnounce diff --git a/epicyon.py b/epicyon.py index 55cb19c7a..1c0912c6e 100644 --- a/epicyon.py +++ b/epicyon.py @@ -45,10 +45,10 @@ from tests import testPostMessageBetweenServers from tests import testFollowBetweenServers from tests import testClientToServer from tests import runAllTests -from config import setConfigParam -from config import getConfigParam from auth import storeBasicCredentials from auth import createPassword +from utils import setConfigParam +from utils import getConfigParam from utils import getDomainFromActor from utils import getNicknameFromActor from utils import followPerson diff --git a/newswire.py b/newswire.py index bda5e8b06..b2d08f4d9 100644 --- a/newswire.py +++ b/newswire.py @@ -15,6 +15,7 @@ from datetime import datetime from collections import OrderedDict from utils import locatePost from utils import loadJson +from utils import isSuspended def rss2Header(httpPrefix: str, @@ -245,16 +246,8 @@ def addLocalBlogsToNewswire(baseDir: str, newswire: {}, # has this account been suspended? nickname = handle.split('@')[0] - if os.path.isfile(suspendedFilename): - with open(suspendedFilename, "r") as f: - lines = f.readlines() - foundSuspended = False - for nick in lines: - if nick == nickname + '\n': - foundSuspended = True - break - if foundSuspended: - continue + if isSuspended(baseDir, nickname): + continue # is there a blogs timeline for this account? blogsIndex = accountDir + '/tlblogs.index' diff --git a/person.py b/person.py index d00724294..19e8243d4 100644 --- a/person.py +++ b/person.py @@ -37,8 +37,8 @@ from utils import validNickname from utils import noOfAccounts from utils import loadJson from utils import saveJson -from config import setConfigParam -from config import getConfigParam +from utils import setConfigParam +from utils import getConfigParam def generateRSAKey() -> (str, str): @@ -754,23 +754,6 @@ def setBio(baseDir: str, nickname: str, domain: str, bio: str) -> bool: return True -def isSuspended(baseDir: str, nickname: str) -> bool: - """Returns true if the given nickname is suspended - """ - adminNickname = getConfigParam(baseDir, 'admin') - if nickname == adminNickname: - return False - - suspendedFilename = baseDir + '/accounts/suspended.txt' - if os.path.isfile(suspendedFilename): - with open(suspendedFilename, "r") as f: - lines = f.readlines() - for suspended in lines: - if suspended.strip('\n').strip('\r') == nickname: - return True - return False - - def unsuspendAccount(baseDir: str, nickname: str) -> None: """Removes an account suspention """ diff --git a/posts.py b/posts.py index 53c5d3bae..37d947371 100644 --- a/posts.py +++ b/posts.py @@ -45,6 +45,7 @@ from utils import validNickname from utils import locatePost from utils import loadJson from utils import saveJson +from utils import getConfigParam from media import attachMedia from media import replaceYouTube from content import removeHtml @@ -53,7 +54,6 @@ from content import addHtmlTags from content import replaceEmojiFromTags from content import removeTextFormatting from auth import createBasicAuthHeader -from config import getConfigParam from blocking import isBlocked from filters import isFiltered from git import convertPostToPatch diff --git a/utils.py b/utils.py index ce3f7012f..a3881df97 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,58 @@ from calendar import monthrange from followingCalendar import addPersonToCalendar +def createConfig(baseDir: str) -> None: + """Creates a configuration file + """ + configFilename = baseDir + '/config.json' + if os.path.isfile(configFilename): + return + configJson = { + } + saveJson(configJson, configFilename) + + +def setConfigParam(baseDir: str, variableName: str, variableValue) -> None: + """Sets a configuration value + """ + createConfig(baseDir) + configFilename = baseDir + '/config.json' + configJson = {} + if os.path.isfile(configFilename): + configJson = loadJson(configFilename) + configJson[variableName] = variableValue + saveJson(configJson, configFilename) + + +def getConfigParam(baseDir: str, variableName: str): + """Gets a configuration value + """ + createConfig(baseDir) + configFilename = baseDir + '/config.json' + configJson = loadJson(configFilename) + if configJson: + if configJson.get(variableName): + return configJson[variableName] + return None + + +def isSuspended(baseDir: str, nickname: str) -> bool: + """Returns true if the given nickname is suspended + """ + adminNickname = getConfigParam(baseDir, 'admin') + if nickname == adminNickname: + return False + + suspendedFilename = baseDir + '/accounts/suspended.txt' + if os.path.isfile(suspendedFilename): + with open(suspendedFilename, "r") as f: + lines = f.readlines() + for suspended in lines: + if suspended.strip('\n').strip('\r') == nickname: + return True + return False + + def getFollowersList(baseDir: str, nickname: str, domain: str, followFile='following.txt') -> []: diff --git a/webinterface.py b/webinterface.py index 3afe1c2d1..68c7bb7b7 100644 --- a/webinterface.py +++ b/webinterface.py @@ -41,6 +41,7 @@ from utils import getDisplayName from utils import getCachedPostDirectory from utils import getCachedPostFilename from utils import loadJson +from utils import getConfigParam from follow import isFollowingActor from webfinger import webfingerHandle from posts import isDM @@ -67,7 +68,6 @@ from content import addHtmlTags from content import replaceEmojiFromTags from content import removeLongWords from content import removeHtml -from config import getConfigParam from skills import getSkills from cache import getPersonFromCache from cache import storePersonInCache From 3c98dd7cf535c70d5222330978f61fc88b42f524 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:01:22 +0100 Subject: [PATCH 002/263] Change urls for readme screenshots --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f2106266..0b00d2a34 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
Epicyon, meaning "more than a dog". Largest of the Borophaginae which lived in North America 20-5 million years ago.
- + - + Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend. @@ -12,7 +12,7 @@ Matrix room: **#epicyon:matrix.freedombone.net** Includes emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0). Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). - + ## Package Dependencies From bff785aa24d046bb64f0966d2b1b5b89560712a0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:15:27 +0100 Subject: [PATCH 003/263] Another screenshot --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0b00d2a34..55e8c0a28 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Matrix room: **#epicyon:matrix.freedombone.net** Includes emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0). Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). + + ## Package Dependencies From 8acf9b2f3e02036df7d083e1c21c9eedca1c8fa7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:22:23 +0100 Subject: [PATCH 004/263] Comment --- newswire.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/newswire.py b/newswire.py index b2d08f4d9..5cef10c9c 100644 --- a/newswire.py +++ b/newswire.py @@ -21,6 +21,8 @@ from utils import isSuspended def rss2Header(httpPrefix: str, nickname: str, domainFull: str, title: str, translate: {}) -> str: + """Header for an RSS 2.0 feed + """ rssStr = "" rssStr += "" rssStr += '' @@ -38,6 +40,8 @@ def rss2Header(httpPrefix: str, def rss2Footer() -> str: + """Footer for an RSS 2.0 feed + """ rssStr = '' rssStr += '' return rssStr From c26cfb3c935de71653f7fbfb450c17b952ca6894 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:37:22 +0100 Subject: [PATCH 005/263] Tidying --- newswire.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/newswire.py b/newswire.py index 5cef10c9c..5cf8600d3 100644 --- a/newswire.py +++ b/newswire.py @@ -16,6 +16,7 @@ from collections import OrderedDict from utils import locatePost from utils import loadJson from utils import isSuspended +from utils import getConfigParam def rss2Header(httpPrefix: str, @@ -222,17 +223,28 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, break +def isTrustedByNewswire(baseDir: str, nickname: str) -> bool: + """Returns true if the given nickname is trusted to post + blog entries to the newswire + """ + adminNickname = getConfigParam(baseDir, 'admin') + if nickname == adminNickname: + return True + + newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' + if os.path.isfile(newswireTrustedFilename): + with open(newswireTrustedFilename, "r") as f: + lines = f.readlines() + for trusted in lines: + if trusted.strip('\n').strip('\r') == nickname: + return True + return False + + def addLocalBlogsToNewswire(baseDir: str, newswire: {}, maxBlogsPerAccount: int) -> None: """Adds blogs from this instance into the newswire """ - # get the list of handles who are trusted to post to the newswire - newswireTrusted = '' - newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' - if os.path.isfile(newswireTrustedFilename): - with open(newswireTrustedFilename, "r") as trustFile: - newswireTrusted = trustFile.read() - # file containing suspended account nicknames suspendedFilename = baseDir + '/accounts/suspended.txt' @@ -243,13 +255,12 @@ def addLocalBlogsToNewswire(baseDir: str, newswire: {}, continue if 'inbox@' in handle: continue - if handle not in newswireTrusted: - if handle.split('@')[0] + '\n' not in newswireTrusted: - continue + nickname = handle.split('@')[0] + if not isTrustedByNewswire(baseDir, nickname): + continue accountDir = os.path.join(baseDir + '/accounts', handle) # has this account been suspended? - nickname = handle.split('@')[0] if isSuspended(baseDir, nickname): continue From 561640abfe55a12e68784880933220b11f870432 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:38:41 +0100 Subject: [PATCH 006/263] Tidying --- newswire.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/newswire.py b/newswire.py index 5cf8600d3..ea6c62c81 100644 --- a/newswire.py +++ b/newswire.py @@ -245,9 +245,6 @@ def addLocalBlogsToNewswire(baseDir: str, newswire: {}, maxBlogsPerAccount: int) -> None: """Adds blogs from this instance into the newswire """ - # file containing suspended account nicknames - suspendedFilename = baseDir + '/accounts/suspended.txt' - # go through each account for subdir, dirs, files in os.walk(baseDir + '/accounts'): for handle in dirs: From 77dc42c3436a7b84ffc2000faedc3f9f0ef17928 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:41:04 +0100 Subject: [PATCH 007/263] Logic sequence --- newswire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newswire.py b/newswire.py index ea6c62c81..835b462e4 100644 --- a/newswire.py +++ b/newswire.py @@ -255,13 +255,13 @@ def addLocalBlogsToNewswire(baseDir: str, newswire: {}, nickname = handle.split('@')[0] if not isTrustedByNewswire(baseDir, nickname): continue - accountDir = os.path.join(baseDir + '/accounts', handle) # has this account been suspended? if isSuspended(baseDir, nickname): continue # is there a blogs timeline for this account? + accountDir = os.path.join(baseDir + '/accounts', handle) blogsIndex = accountDir + '/tlblogs.index' if os.path.isfile(blogsIndex): domain = handle.split('@')[1] From 2d7e6f4f432cc0fb4c6e84add11fdb4e1bae8eb4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 10:47:58 +0100 Subject: [PATCH 008/263] Blog posts going into the newswire may not always be local. They may be whatever federated to each users blog timeline. --- newswire.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/newswire.py b/newswire.py index 835b462e4..46638c5bf 100644 --- a/newswire.py +++ b/newswire.py @@ -241,9 +241,9 @@ def isTrustedByNewswire(baseDir: str, nickname: str) -> bool: return False -def addLocalBlogsToNewswire(baseDir: str, newswire: {}, - maxBlogsPerAccount: int) -> None: - """Adds blogs from this instance into the newswire +def addBlogsToNewswire(baseDir: str, newswire: {}, + maxBlogsPerAccount: int) -> None: + """Adds blogs from each user account into the newswire """ # go through each account for subdir, dirs, files in os.walk(baseDir + '/accounts'): @@ -292,8 +292,8 @@ def getDictFromNewswire(session, baseDir: str) -> {}: for dateStr, item in itemsList.items(): result[dateStr] = item - # add local content - addLocalBlogsToNewswire(baseDir, result, 5) + # add blogs from each user account + addBlogsToNewswire(baseDir, result, 5) # sort into chronological order, latest first sortedResult = OrderedDict(sorted(result.items(), reverse=True)) From ebb760a3c3b93872e2ffee0558eb118a9481b3ea Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 11:34:56 +0100 Subject: [PATCH 009/263] Create a dictionary of blog posts to be moderated --- newswire.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++-- utils.py | 5 +++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/newswire.py b/newswire.py index 46638c5bf..24ce4fc3e 100644 --- a/newswire.py +++ b/newswire.py @@ -170,6 +170,56 @@ def getRSSfromDict(baseDir: str, newswire: {}, return rssStr +def updateNewswireModerationQueue(baseDir: str, handle: str, + maxBlogsPerAccount: int, + moderationDict: {}) -> None: + """Puts new blog posts by untrusted accounts into a moderation queue + """ + accountDir = os.path.join(baseDir + '/accounts', handle) + indexFilename = accountDir + '/tlblogs.index' + if not os.path.isfile(indexFilename): + return + nickname = handle.split('@')[0] + domain = handle.split('@')[1] + with open(indexFilename, 'r') as indexFile: + postFilename = 'start' + ctr = 0 + while postFilename: + postFilename = indexFile.readline() + if postFilename: + # if this is a full path then remove the directories + if '/' in postFilename: + postFilename = postFilename.split('/')[-1] + + # filename of the post without any extension or path + # This should also correspond to any index entry in + # the posts cache + postUrl = \ + postFilename.replace('\n', '').replace('\r', '') + postUrl = postUrl.replace('.json', '').strip() + + # read the post from file + fullPostFilename = \ + locatePost(baseDir, nickname, + domain, postUrl, False) + moderationStatusFilename = fullPostFilename + '.moderate' + if not os.path.isfile(moderationStatusFilename): + statusFile = open(moderationStatusFilename, "w+") + if statusFile: + statusFile.write('[waiting]') + statusFile.close() + + if '[accepted]' not in \ + open(moderationStatusFilename).read(): + if moderationDict.get(nickname): + moderationDict[nickname] = [] + moderationDict[nickname].append(fullPostFilename) + + ctr += 1 + if ctr >= maxBlogsPerAccount: + break + + def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, newswire: {}, maxBlogsPerAccount: int, @@ -245,6 +295,8 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, maxBlogsPerAccount: int) -> None: """Adds blogs from each user account into the newswire """ + moderationDict = {} + # go through each account for subdir, dirs, files in os.walk(baseDir + '/accounts'): for handle in dirs: @@ -252,14 +304,19 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, continue if 'inbox@' in handle: continue + nickname = handle.split('@')[0] - if not isTrustedByNewswire(baseDir, nickname): - continue # has this account been suspended? if isSuspended(baseDir, nickname): continue + # is this account trusted? + if not isTrustedByNewswire(baseDir, nickname): + updateNewswireModerationQueue(baseDir, handle, 5, + moderationDict) + continue + # is there a blogs timeline for this account? accountDir = os.path.join(baseDir + '/accounts', handle) blogsIndex = accountDir + '/tlblogs.index' diff --git a/utils.py b/utils.py index a3881df97..8f4a02a6e 100644 --- a/utils.py +++ b/utils.py @@ -633,6 +633,11 @@ def deletePost(baseDir: str, httpPrefix: str, if os.path.isfile(muteFilename): os.remove(muteFilename) + # remove any moderation file + moderationFilename = postFilename + '.moderate' + if os.path.isfile(moderationFilename): + os.remove(moderationFilename) + # remove cached html version of the post cachedPostFilename = \ getCachedPostFilename(baseDir, nickname, domain, postJsonObject) From fc267f8b9179397ddab5ad2a048d238abe7d4913 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 11:37:06 +0100 Subject: [PATCH 010/263] Unique entries in list --- newswire.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/newswire.py b/newswire.py index 24ce4fc3e..5becaefcf 100644 --- a/newswire.py +++ b/newswire.py @@ -213,7 +213,8 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, open(moderationStatusFilename).read(): if moderationDict.get(nickname): moderationDict[nickname] = [] - moderationDict[nickname].append(fullPostFilename) + if fullPostFilename not in moderationDict[nickname]: + moderationDict[nickname].append(fullPostFilename) ctr += 1 if ctr >= maxBlogsPerAccount: From d5248ea2d658206347ecf558abe41af92f211d01 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 12:28:32 +0100 Subject: [PATCH 011/263] Save the current newswire moderation state to a file --- newswire.py | 69 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/newswire.py b/newswire.py index 5becaefcf..2b4df192c 100644 --- a/newswire.py +++ b/newswire.py @@ -15,6 +15,7 @@ from datetime import datetime from collections import OrderedDict from utils import locatePost from utils import loadJson +from utils import saveJson from utils import isSuspended from utils import getConfigParam @@ -170,6 +171,22 @@ def getRSSfromDict(baseDir: str, newswire: {}, return rssStr +def isaBlogPost(postJsonObject: {}) -> bool: + """Is the given object a blog post? + """ + if not postJsonObject: + return False + if not postJsonObject.get('object'): + return False + if not isinstance(postJsonObject['object'], dict): + return False + if postJsonObject['object'].get('summary') and \ + postJsonObject['object'].get('url') and \ + postJsonObject['object'].get('published'): + return True + return False + + def updateNewswireModerationQueue(baseDir: str, handle: str, maxBlogsPerAccount: int, moderationDict: {}) -> None: @@ -203,18 +220,34 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, locatePost(baseDir, nickname, domain, postUrl, False) moderationStatusFilename = fullPostFilename + '.moderate' + moderationStatusStr = '' if not os.path.isfile(moderationStatusFilename): statusFile = open(moderationStatusFilename, "w+") if statusFile: statusFile.write('[waiting]') statusFile.close() + moderationStatusStr = '[waiting]' + else: + statusFile = open(moderationStatusFilename, "r") + if statusFile: + moderationStatusStr = statusFile.read() + statusFile.close() if '[accepted]' not in \ open(moderationStatusFilename).read(): - if moderationDict.get(nickname): - moderationDict[nickname] = [] - if fullPostFilename not in moderationDict[nickname]: - moderationDict[nickname].append(fullPostFilename) + + postJsonObject = None + if fullPostFilename: + postJsonObject = loadJson(fullPostFilename) + if isaBlogPost(postJsonObject): + published = postJsonObject['object']['published'] + published = published.replace('T', ' ') + published = published.replace('Z', '+00:00') + moderationDict[published] = \ + [postJsonObject['object']['summary'], + postJsonObject['object']['url'], + nickname, moderationStatusStr, + fullPostFilename] ctr += 1 if ctr >= maxBlogsPerAccount: @@ -250,24 +283,16 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, fullPostFilename = \ locatePost(baseDir, nickname, domain, postUrl, False) - isAPost = False postJsonObject = None if fullPostFilename: postJsonObject = loadJson(fullPostFilename) - if postJsonObject: - if postJsonObject.get('object'): - if isinstance(postJsonObject['object'], dict): - isAPost = True - if isAPost: - if postJsonObject['object'].get('summary') and \ - postJsonObject['object'].get('url') and \ - postJsonObject['object'].get('published'): - published = postJsonObject['object']['published'] - published = published.replace('T', ' ') - published = published.replace('Z', '+00:00') - newswire[published] = \ - [postJsonObject['object']['summary'], - postJsonObject['object']['url']] + if isaBlogPost(postJsonObject): + published = postJsonObject['object']['published'] + published = published.replace('T', ' ') + published = published.replace('Z', '+00:00') + newswire[published] = \ + [postJsonObject['object']['summary'], + postJsonObject['object']['url']] ctr += 1 if ctr >= maxBlogsPerAccount: @@ -327,6 +352,12 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, newswire, maxBlogsPerAccount, blogsIndex) + # sort the moderation dict into chronological order, latest first + sortedModerationDict = \ + OrderedDict(sorted(moderationDict.items(), reverse=True)) + newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' + saveJson(sortedModerationDict, newswireModerationFilename) + def getDictFromNewswire(session, baseDir: str) -> {}: """Gets rss feeds as a dictionary from newswire file From d0085e1c332676cf381c9278bb2841415d6718ac Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 13:15:35 +0100 Subject: [PATCH 012/263] Comments --- newswire.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/newswire.py b/newswire.py index 2b4df192c..19d62fb1a 100644 --- a/newswire.py +++ b/newswire.py @@ -222,20 +222,24 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, moderationStatusFilename = fullPostFilename + '.moderate' moderationStatusStr = '' if not os.path.isfile(moderationStatusFilename): + # create a file used to keep track of moderation status + moderationStatusStr = '[waiting]' statusFile = open(moderationStatusFilename, "w+") if statusFile: - statusFile.write('[waiting]') + statusFile.write(moderationStatusStr) statusFile.close() - moderationStatusStr = '[waiting]' else: + # read the moderation status file statusFile = open(moderationStatusFilename, "r") if statusFile: moderationStatusStr = statusFile.read() statusFile.close() + # if the post is still in the moderation queue if '[accepted]' not in \ open(moderationStatusFilename).read(): + # load the post and add its details to the moderation queue postJsonObject = None if fullPostFilename: postJsonObject = loadJson(fullPostFilename) @@ -355,6 +359,7 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, # sort the moderation dict into chronological order, latest first sortedModerationDict = \ OrderedDict(sorted(moderationDict.items(), reverse=True)) + # save the moderation queue details for later display newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' saveJson(sortedModerationDict, newswireModerationFilename) From 3e666a4a91253e84b916c653efb5647d6c6382aa Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 13:33:53 +0100 Subject: [PATCH 013/263] Moderated posts can be rejected --- newswire.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/newswire.py b/newswire.py index 19d62fb1a..380900801 100644 --- a/newswire.py +++ b/newswire.py @@ -238,20 +238,22 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, # if the post is still in the moderation queue if '[accepted]' not in \ open(moderationStatusFilename).read(): - - # load the post and add its details to the moderation queue - postJsonObject = None - if fullPostFilename: - postJsonObject = loadJson(fullPostFilename) - if isaBlogPost(postJsonObject): - published = postJsonObject['object']['published'] - published = published.replace('T', ' ') - published = published.replace('Z', '+00:00') - moderationDict[published] = \ - [postJsonObject['object']['summary'], - postJsonObject['object']['url'], - nickname, moderationStatusStr, - fullPostFilename] + if '[rejected]' not in \ + open(moderationStatusFilename).read(): + # load the post and add its details to the + # moderation queue + postJsonObject = None + if fullPostFilename: + postJsonObject = loadJson(fullPostFilename) + if isaBlogPost(postJsonObject): + published = postJsonObject['object']['published'] + published = published.replace('T', ' ') + published = published.replace('Z', '+00:00') + moderationDict[published] = \ + [postJsonObject['object']['summary'], + postJsonObject['object']['url'], + nickname, moderationStatusStr, + fullPostFilename] ctr += 1 if ctr >= maxBlogsPerAccount: From f26d50f4d22521672d143be049ff78d6cffe3962 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 13:39:50 +0100 Subject: [PATCH 014/263] Mention news --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55e8c0a28..45a998d6f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ -Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend. +Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and sutable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions, news feed and perimeter defense against adversaries. It contains *no javascript* and uses HTML+CSS with a Python backend. [Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Code of Conduct](code-of-conduct.md) From df7382af1982cb1ddb8de556b4bab3cf9537c28e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 13:45:35 +0100 Subject: [PATCH 015/263] Test without admin trust --- newswire.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/newswire.py b/newswire.py index 380900801..194d8220a 100644 --- a/newswire.py +++ b/newswire.py @@ -309,9 +309,9 @@ def isTrustedByNewswire(baseDir: str, nickname: str) -> bool: """Returns true if the given nickname is trusted to post blog entries to the newswire """ - adminNickname = getConfigParam(baseDir, 'admin') - if nickname == adminNickname: - return True + # adminNickname = getConfigParam(baseDir, 'admin') + # if nickname == adminNickname: + # return True newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' if os.path.isfile(newswireTrustedFilename): From bee236a44fd2c33348d5026168c3e766b277025a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 14:05:15 +0100 Subject: [PATCH 016/263] Check that post is located --- newswire.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/newswire.py b/newswire.py index 194d8220a..1a59c0bfa 100644 --- a/newswire.py +++ b/newswire.py @@ -219,6 +219,12 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, fullPostFilename = \ locatePost(baseDir, nickname, domain, postUrl, False) + if not fullPostFilename: + print('Unable to locate post ' + postUrl) + ctr += 1 + if ctr >= maxBlogsPerAccount: + break + moderationStatusFilename = fullPostFilename + '.moderate' moderationStatusStr = '' if not os.path.isfile(moderationStatusFilename): @@ -289,6 +295,12 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, fullPostFilename = \ locatePost(baseDir, nickname, domain, postUrl, False) + if not fullPostFilename: + print('Unable to locate post ' + postUrl) + ctr += 1 + if ctr >= maxBlogsPerAccount: + break + postJsonObject = None if fullPostFilename: postJsonObject = loadJson(fullPostFilename) From f040362ef81bb9824fcdc9c3eb9e15458e02bb40 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 14:17:32 +0100 Subject: [PATCH 017/263] Three params --- daemon.py | 2 +- newswire.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index f159f9e9e..b9e32a5dd 100644 --- a/daemon.py +++ b/daemon.py @@ -11286,7 +11286,7 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool, print('Creating newswire thread') httpd.thrNewswireDaemon = \ threadWithTrace(target=runNewswireDaemon, - args=(baseDir, httpd), daemon=True) + args=(baseDir, httpd, 'newswire'), daemon=True) # flags used when restarting the inbox queue httpd.restartInboxQueueInProgress = False diff --git a/newswire.py b/newswire.py index 1a59c0bfa..ecef8c59c 100644 --- a/newswire.py +++ b/newswire.py @@ -17,7 +17,7 @@ from utils import locatePost from utils import loadJson from utils import saveJson from utils import isSuspended -from utils import getConfigParam +# from utils import getConfigParam def rss2Header(httpPrefix: str, @@ -408,7 +408,7 @@ def getDictFromNewswire(session, baseDir: str) -> {}: return sortedResult -def runNewswireDaemon(baseDir: str, httpd): +def runNewswireDaemon(baseDir: str, httpd, unused: str): """Periodically updates RSS feeds """ # initial sleep to allow the system to start up From 96828b6727156bc6b08d9fd86e0e5a2ac7a3fc8a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 14:31:34 +0100 Subject: [PATCH 018/263] Show exception --- newswire.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/newswire.py b/newswire.py index ecef8c59c..7c8550af2 100644 --- a/newswire.py +++ b/newswire.py @@ -412,7 +412,7 @@ def runNewswireDaemon(baseDir: str, httpd, unused: str): """Periodically updates RSS feeds """ # initial sleep to allow the system to start up - time.sleep(70) + time.sleep(50) while True: # has the session been created yet? if not httpd.session: @@ -424,8 +424,8 @@ def runNewswireDaemon(baseDir: str, httpd, unused: str): newNewswire = None try: newNewswire = getDictFromNewswire(httpd.session, baseDir) - except BaseException: - print('WARN: unable to update newswire') + except Exception as e: + print('WARN: unable to update newswire ' + str(e)) time.sleep(120) continue From a91f7bbb75bf195eddd9e5c373d3a4fac17fe0f7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 14:34:04 +0100 Subject: [PATCH 019/263] Continue if post is not located --- newswire.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/newswire.py b/newswire.py index 7c8550af2..0c055905e 100644 --- a/newswire.py +++ b/newswire.py @@ -224,6 +224,7 @@ def updateNewswireModerationQueue(baseDir: str, handle: str, ctr += 1 if ctr >= maxBlogsPerAccount: break + continue moderationStatusFilename = fullPostFilename + '.moderate' moderationStatusStr = '' @@ -300,6 +301,7 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, ctr += 1 if ctr >= maxBlogsPerAccount: break + continue postJsonObject = None if fullPostFilename: From ef94e153ea10f9f0cfc94ef85ef46f2bbc51619a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 15:11:38 +0100 Subject: [PATCH 020/263] Remove newswire moderation color --- epicyon-profile.css | 8 -------- webinterface.py | 15 ++------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 614f114a9..ebf9d99a4 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -67,7 +67,6 @@ --quote-font-size: 120%; --line-spacing: 130%; --line-spacing-newswire: 100%; - --newswire-moderate-color: yellow; --column-left-width: 10vw; --column-center-width: 80vw; --column-right-width: 10vw; @@ -232,13 +231,6 @@ a:focus { line-height: var(--line-spacing-newswire); } -.newswireItemModerate { - font-size: var(--font-size-newswire); - color: var(--newswire-moderate-color); - font-weight: bold; - line-height: var(--line-spacing-newswire); -} - .newswireDate { font-size: var(--font-size-newswire); color: var(--newswire-date-color); diff --git a/webinterface.py b/webinterface.py index 68c7bb7b7..0eec71b30 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5358,19 +5358,8 @@ def htmlNewswire(newswire: str) -> str: """ htmlStr = '' for dateStr, item in newswire.items(): - # if the item is to be moderated then show it in a different style - shown = False - if len(item) > 2: - if item[2].startswith('moderate'): - moderationUrl = '/moderate?' + item[1] - htmlStr += '

' + \ - '' + item[0] + '' - shown = True - - if not shown: - # unmoderated item - htmlStr += '

' + \ - '' + item[0] + '' + htmlStr += '

' + \ + '' + item[0] + '' htmlStr += '

' return htmlStr From fdaf02b5c7a508ea11253b8e01d6262364acbb33 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 15:32:53 +0100 Subject: [PATCH 021/263] Edit button changes color when there are newswire items to be moderated --- img/icons/edit_notify.png | Bin 0 -> 1440 bytes img/icons/hacker/edit_notify.png | Bin 0 -> 1441 bytes img/icons/henge/edit_notify.png | Bin 0 -> 1442 bytes img/icons/indymedia/edit_notify.png | Bin 0 -> 1476 bytes img/icons/lcd/edit_notify.png | Bin 0 -> 1439 bytes img/icons/light/edit_notify.png | Bin 0 -> 1444 bytes img/icons/night/edit_notify.png | Bin 0 -> 1438 bytes img/icons/purple/edit_notify.png | Bin 0 -> 1445 bytes img/icons/solidaric/edit_notify.png | Bin 0 -> 1444 bytes img/icons/starlight/edit_notify.png | Bin 0 -> 1444 bytes img/icons/zen/edit_notify.png | Bin 0 -> 1433 bytes newswire.py | 7 ++++++- webinterface.py | 29 +++++++++++++++++++--------- 13 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 img/icons/edit_notify.png create mode 100644 img/icons/hacker/edit_notify.png create mode 100644 img/icons/henge/edit_notify.png create mode 100644 img/icons/indymedia/edit_notify.png create mode 100644 img/icons/lcd/edit_notify.png create mode 100644 img/icons/light/edit_notify.png create mode 100644 img/icons/night/edit_notify.png create mode 100644 img/icons/purple/edit_notify.png create mode 100644 img/icons/solidaric/edit_notify.png create mode 100644 img/icons/starlight/edit_notify.png create mode 100644 img/icons/zen/edit_notify.png diff --git a/img/icons/edit_notify.png b/img/icons/edit_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..03eb4644a86300db2e1f9a7b210f5ecd8be90b06 GIT binary patch literal 1440 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 v2hf3J@dgA9&?x zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=KJmgFW3h2L34mVhLL#Bwk{=gbbW{9LeIUDeZ* z%uMo-hcd^21s3xC1l1kJpWkNq0~eK?LQ?Zwa*nuCNrfvq9?z@nnqu1ZzS1>>=NEf; z3@}WBR?bJO*Zc~*e!Ot6LC32-_%*_QIl2aJhq6{?boz6Uu=8oBE0R#g*~!K2In;yC zw%sB7$2}Wwe|nrl>p46dl0l`eSV)+MkVMohR|In2b=grz$(To?=(A8jBXUOqL0&%Z zZex5F=tbna7e1^1=zS8O>t&ZuTINe5biN!Q{aXAj@zcWch=^YYOmA0Th&;Q`InJ-E zDMQhGWmF8eI$QnOA$yYjT1GM4ioWeqP(0SyfV{^R50plQ80ybJi8@u9Gldmosz$+zC^e;EeFf4@>yF!Cxt1 z&bB0?=nt(B7q7fV7>nGhF$+LwUfgsGe3uJi#lWUW1+0`5`D4kELsdnSs%ACyphatzoU-PeEw4kanpiTmY-Vo7s*5LA z&u;EsycRBkGf+z|R=ku_D~F1ODtuMUudp0^$dL{`@?nP`<){tm(^Auxn>BBx)y`cy zj)|VTb?>FuL1Eye7-{Ip!-kJC>O`(hnQ7|G)27cd>x0@w^}F&5)aatdo79?T4{ERm zv)cu&<3txT5aUE3Zi@gCnisR^loBs;i&-p;O`(h+b+HMZ7BL`9gIFg$*nN=uDQ-di zr?~MykqZmme?cw)-S^zSpw`##T-(IXUAQ!jg57t_q?CeM(}KC!9P2s$ZX5osgSVr% zqqn2Cqqn2Cqqn2~g(Li}K0E&L41WPNbUo-v7T*T|00D(*LqkwWLqi~Na&Km7Y-Iod zc$|HaJxIeq9K~Nhv{I@LRuFN>P@OD@ia2T&iclfc3avVrT>2q2X-HCB90k{cgCC1k z2N!2u9b5%L@B_rj(Mi!oO8j3^Xc6PVaX;SOd)&PP{Pi+Z&EOcIYL<~sCWLHmMF_nj zj862RA3=$k`m89X;5okT;p6LFoM(BT`*UNtRyMK z=ftB1U6A;Z>$1yloQn?od1lner00mE#6q!+^TBd-1_M1200006P)t-sVg&#w*C^#Fbd&%900DGTPE!Ct=GbNc0004E zOGiWihy@);00009a7bBm001r{001r{0eGc9b^rhX2XskIMF->x1`ZV>#*sd=0001i zNklK_P+1M{@hY&wPlZ4ZJtfX`0@Ba~Re% zl-qE2!!-;td?eez0<8Qgz$xckaR literal 0 HcmV?d00001 diff --git a/img/icons/indymedia/edit_notify.png b/img/icons/indymedia/edit_notify.png new file mode 100644 index 0000000000000000000000000000000000000000..5687aab1be299cb499fbcd6c9b36aaa195cf31e9 GIT binary patch literal 1476 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 n&?x {}: diff --git a/webinterface.py b/webinterface.py index 0eec71b30..7eb2bd901 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5406,15 +5406,26 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, htmlStr += '\n
\n' if moderator: - # show the edit icon - htmlStr += \ - ' ' + \ - '' + \
-            translate['Edit newswire'] + '\n' + if os.path.isfile(baseDir + '/accounts/newswiremoderation.txt'): + # show the edit icon highlighted + htmlStr += \ + ' ' + \ + '' + \
+                translate['Edit newswire'] + '\n' + else: + # show the edit icon + htmlStr += \ + ' ' + \ + '' + \
+                translate['Edit newswire'] + '\n' htmlStr += \ ' ' + \ From 06f3c10cb270aa150d003ef0af27872826e3e273 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:18:22 +0100 Subject: [PATCH 022/263] Newswire moderation items listed --- translations/ar.json | 5 +++- translations/ca.json | 5 +++- translations/cy.json | 5 +++- translations/de.json | 5 +++- translations/en.json | 5 +++- translations/es.json | 5 +++- translations/fr.json | 5 +++- translations/ga.json | 5 +++- translations/hi.json | 5 +++- translations/it.json | 5 +++- translations/ja.json | 5 +++- translations/oc.json | 5 +++- translations/pt.json | 5 +++- translations/ru.json | 5 +++- translations/zh.json | 5 +++- webinterface.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 16 files changed, 130 insertions(+), 15 deletions(-) diff --git a/translations/ar.json b/translations/ar.json index 096198c27..ac05265f4 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -297,5 +297,8 @@ "Edit newswire": "تحرير الأخبار", "Add RSS feed links below.": "إضافة روابط تغذية RSS أدناه.", "Newswire RSS Feed": "Newswire موجز RSS", - "Nicknames whose blog entries appear on the newswire.": "الألقاب التي تظهر إدخالات المدونة الخاصة بها على موقع الأخبار." + "Nicknames whose blog entries appear on the newswire.": "الألقاب التي تظهر إدخالات المدونة الخاصة بها على موقع الأخبار.", + "Posts to be approved": "الوظائف المطلوب الموافقة عليها", + "Discuss": "مناقشة", + "Moderator Discussion": "مناقشة المنسق" } diff --git a/translations/ca.json b/translations/ca.json index 0e2fb495e..78fbff613 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -297,5 +297,8 @@ "Edit newswire": "Editeu newswire", "Add RSS feed links below.": "Afegiu enllaços de canals RSS a continuació.", "Newswire RSS Feed": "Feed RSS de Newswire", - "Nicknames whose blog entries appear on the newswire.": "Sobrenoms les entrades del bloc apareixen a newswire." + "Nicknames whose blog entries appear on the newswire.": "Sobrenoms les entrades del bloc apareixen a newswire.", + "Posts to be approved": "Missatges per aprovar", + "Discuss": "Discuteix", + "Moderator Discussion": "Discussió sobre moderadors" } diff --git a/translations/cy.json b/translations/cy.json index 8c0b0192a..c4ed63de5 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -297,5 +297,8 @@ "Edit newswire": "Golygu newyddion", "Add RSS feed links below.": "Ychwanegwch ddolenni porthiant RSS isod.", "Newswire RSS Feed": "Newswire RSS Feed", - "Nicknames whose blog entries appear on the newswire.": "Llysenwau y mae eu cofnodion blog yn ymddangos ar y we newyddion." + "Nicknames whose blog entries appear on the newswire.": "Llysenwau y mae eu cofnodion blog yn ymddangos ar y we newyddion.", + "Posts to be approved": "Swyddi i'w cymeradwyo", + "Discuss": "Trafodwch", + "Moderator Discussion": "Trafodaeth Cymedrolwr" } diff --git a/translations/de.json b/translations/de.json index b428e1a72..a5f935f93 100644 --- a/translations/de.json +++ b/translations/de.json @@ -297,5 +297,8 @@ "Edit newswire": "Newswire bearbeiten", "Add RSS feed links below.": "Fügen Sie unten RSS-Feed-Links hinzu.", "Newswire RSS Feed": "Newswire RSS Feed", - "Nicknames whose blog entries appear on the newswire.": "Spitznamen, deren Blogeinträge im Newswire erscheinen." + "Nicknames whose blog entries appear on the newswire.": "Spitznamen, deren Blogeinträge im Newswire erscheinen.", + "Posts to be approved": "Zu genehmigende Beiträge", + "Discuss": "Diskutieren", + "Moderator Discussion": "Moderatorendiskussion" } diff --git a/translations/en.json b/translations/en.json index 644161721..3f61e3366 100644 --- a/translations/en.json +++ b/translations/en.json @@ -297,5 +297,8 @@ "Edit newswire": "Edit newswire", "Add RSS feed links below.": "Add RSS feed links below.", "Newswire RSS Feed": "Newswire RSS Feed", - "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire." + "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", + "Posts to be approved": "Posts to be approved", + "Discuss": "Discuss", + "Moderator Discussion": "Moderator Discussion" } diff --git a/translations/es.json b/translations/es.json index 457acde58..1eaf08769 100644 --- a/translations/es.json +++ b/translations/es.json @@ -297,5 +297,8 @@ "Edit newswire": "Editar newswire", "Add RSS feed links below.": "Agregue los enlaces de fuentes RSS a continuación.", "Newswire RSS Feed": "Canal RSS de Newswire", - "Nicknames whose blog entries appear on the newswire.": "Apodos cuyas entradas de blog aparecen en el newswire." + "Nicknames whose blog entries appear on the newswire.": "Apodos cuyas entradas de blog aparecen en el newswire.", + "Posts to be approved": "Publicaciones a aprobar", + "Discuss": "Discutir", + "Moderator Discussion": "Discusión del moderador" } diff --git a/translations/fr.json b/translations/fr.json index b9b0c00e2..4fa45c874 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -297,5 +297,8 @@ "Edit newswire": "Modifier le fil d'actualité", "Add RSS feed links below.": "Ajoutez des liens de flux RSS ci-dessous.", "Newswire RSS Feed": "Flux RSS de Newswire", - "Nicknames whose blog entries appear on the newswire.": "Surnoms dont les entrées de blog apparaissent sur le fil de presse." + "Nicknames whose blog entries appear on the newswire.": "Surnoms dont les entrées de blog apparaissent sur le fil de presse.", + "Posts to be approved": "Postes à approuver", + "Discuss": "Discuter", + "Moderator Discussion": "Discussion du modérateur" } diff --git a/translations/ga.json b/translations/ga.json index 1853bd945..4bd8edfb2 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -297,5 +297,8 @@ "Edit newswire": "Cuir sreang nuachta in eagar", "Add RSS feed links below.": "Cuir naisc beatha RSS thíos.", "Newswire RSS Feed": "Newswire RSS Feed", - "Nicknames whose blog entries appear on the newswire.": "Leasainmneacha a bhfuil a n-iontrálacha blag le feiceáil ar an sreang nuachta." + "Nicknames whose blog entries appear on the newswire.": "Leasainmneacha a bhfuil a n-iontrálacha blag le feiceáil ar an sreang nuachta.", + "Posts to be approved": "Poist le ceadú", + "Discuss": "Pléigh", + "Moderator Discussion": "Plé Modhnóir" } diff --git a/translations/hi.json b/translations/hi.json index 45f6af94a..1df55af62 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -297,5 +297,8 @@ "Edit newswire": "नवांश संपादित करें", "Add RSS feed links below.": "नीचे आरएसएस फ़ीड लिंक जोड़ें।", "Newswire RSS Feed": "Newswire RSS फ़ीड", - "Nicknames whose blog entries appear on the newswire.": "उपनाम जिनकी ब्लॉग प्रविष्टियाँ न्यूज़वायर पर दिखाई देती हैं।" + "Nicknames whose blog entries appear on the newswire.": "उपनाम जिनकी ब्लॉग प्रविष्टियाँ न्यूज़वायर पर दिखाई देती हैं।", + "Posts to be approved": "स्वीकृत किए जाने वाले पद", + "Discuss": "चर्चा करें", + "Moderator Discussion": "मॉडरेटर चर्चा" } diff --git a/translations/it.json b/translations/it.json index eba3c33fe..390e7fce4 100644 --- a/translations/it.json +++ b/translations/it.json @@ -297,5 +297,8 @@ "Edit newswire": "Modifica newswire", "Add RSS feed links below.": "Aggiungi i link ai feed RSS di seguito.", "Newswire RSS Feed": "Feed RSS di Newswire", - "Nicknames whose blog entries appear on the newswire.": "Soprannomi le cui voci di blog compaiono nel newswire." + "Nicknames whose blog entries appear on the newswire.": "Soprannomi le cui voci di blog compaiono nel newswire.", + "Posts to be approved": "Post da approvare", + "Discuss": "Discutere", + "Moderator Discussion": "Discussione del moderatore" } diff --git a/translations/ja.json b/translations/ja.json index 4b9a9cbb4..34e7afbb9 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -297,5 +297,8 @@ "Edit newswire": "ニュースワイヤーを編集", "Add RSS feed links below.": "以下にRSSフィードリンクを追加します。", "Newswire RSS Feed": "NewswireRSSフィード", - "Nicknames whose blog entries appear on the newswire.": "ブログエントリがニュースワイヤーに表示されるニックネーム。" + "Nicknames whose blog entries appear on the newswire.": "ブログエントリがニュースワイヤーに表示されるニックネーム。", + "Posts to be approved": "承認される投稿", + "Discuss": "議論する", + "Moderator Discussion": "モデレーターディスカッション" } diff --git a/translations/oc.json b/translations/oc.json index 6f39c0ae5..14e8e3888 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -293,5 +293,8 @@ "Edit newswire": "Edit newswire", "Add RSS feed links below.": "Add RSS feed links below.", "Newswire RSS Feed": "Newswire RSS Feed", - "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire." + "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", + "Posts to be approved": "Posts to be approved", + "Discuss": "Discuss", + "Moderator Discussion": "Moderator Discussion" } diff --git a/translations/pt.json b/translations/pt.json index af16daa72..d4d23a91a 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -297,5 +297,8 @@ "Edit newswire": "Editar notícias", "Add RSS feed links below.": "Adicione links de feed RSS abaixo.", "Newswire RSS Feed": "Feed RSS da Newswire", - "Nicknames whose blog entries appear on the newswire.": "Apelidos cujas entradas de blog aparecem nos jornais." + "Nicknames whose blog entries appear on the newswire.": "Apelidos cujas entradas de blog aparecem nos jornais.", + "Posts to be approved": "Postagens a serem aprovadas", + "Discuss": "Discutir", + "Moderator Discussion": "Discussão do moderador" } diff --git a/translations/ru.json b/translations/ru.json index 37f5d6659..f41695c62 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -297,5 +297,8 @@ "Edit newswire": "Редактировать ленту новостей", "Add RSS feed links below.": "Добавьте ссылки на RSS-канал ниже.", "Newswire RSS Feed": "Лента новостей RSS", - "Nicknames whose blog entries appear on the newswire.": "Псевдонимы, чьи записи блога появляются в ленте новостей." + "Nicknames whose blog entries appear on the newswire.": "Псевдонимы, чьи записи блога появляются в ленте новостей.", + "Posts to be approved": "Посты на утверждение", + "Discuss": "Обсудить", + "Moderator Discussion": "Обсуждение модератором" } diff --git a/translations/zh.json b/translations/zh.json index 3c2711739..30417890a 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -297,5 +297,8 @@ "Edit newswire": "编辑新闻专线", "Add RSS feed links below.": "在下面添加RSS feed链接。", "Newswire RSS Feed": "Newswire RSS提要", - "Nicknames whose blog entries appear on the newswire.": "博客条目出现在新闻专线上的昵称。" + "Nicknames whose blog entries appear on the newswire.": "博客条目出现在新闻专线上的昵称。", + "Posts to be approved": "职位待批准", + "Discuss": "讨论", + "Moderator Discussion": "主持人讨论" } diff --git a/webinterface.py b/webinterface.py index 7eb2bd901..19d392952 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1274,6 +1274,67 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, return editLinksForm +def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: + """Get a list of newswire items to be moderated + """ + if '/users/' not in path: + return '' + + # load the file containing newswire posts to be moderated + newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' + moderateJson = loadJson(newswireModerationFilename) + if not newswireJson: + return '' + + # get the nickname and actor path of the moderator + nickname = path.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + basePath = path.split('/users/')[0] + '/users/' + nickname + + resultStr = '' + + # for each post to be moderated + for dateStr, item in moderateJson.items(): + # details of this post + title = item[0] + url = item[1] + nick = item[2] + status = item[3] + postFilename = item[4].replace('/', '#') + + # create the html for this post + resultStr += '' + return resultStr + + def htmlEditNewswire(translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit newswire screen @@ -1335,6 +1396,15 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, editNewswireForm += \ '
' + + newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' + if os.path.isfile(newswireModerationFilename): + editNewswireForm += \ + ' ' + \ + translate['Posts to be approved'] + ':
' + editNewswireForm += \ + htmlNewswireModeration(baseDir, path, translate) + '
' + editNewswireForm += \ ' ' + \ translate['Add RSS feed links below.'] + \ From 20c55ef06cebefac24fae91184981e2a44d03b4f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:20:37 +0100 Subject: [PATCH 023/263] Json variable --- webinterface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webinterface.py b/webinterface.py index 19d392952..2e404e278 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1283,7 +1283,7 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: # load the file containing newswire posts to be moderated newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' moderateJson = loadJson(newswireModerationFilename) - if not newswireJson: + if not moderateJson: return '' # get the nickname and actor path of the moderator @@ -1299,8 +1299,8 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: # details of this post title = item[0] url = item[1] - nick = item[2] - status = item[3] + # nick = item[2] + # status = item[3] postFilename = item[4].replace('/', '#') # create the html for this post From 506ea825d21929676772c2489b820153f48827eb Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:25:53 +0100 Subject: [PATCH 024/263] Buttons on same row --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 2e404e278..a4fa5b9ca 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1316,7 +1316,7 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: '/newswireapprove=' + postFilename + '">' resultStr += \ '

' + translate['Approve'] + '' resultStr += \ '' + resultStr += \ - '' + '' + resultStr += \ + '' + \ + nick + ': ' + + resultStr += \ + '' resultStr += \ '' + \ title + '' From 164e94912595e6fa66bd6ea9c4b8235d0d0b35a3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:30:37 +0100 Subject: [PATCH 026/263] # not permitted in nickname --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 8f4a02a6e..a8aa5416f 100644 --- a/utils.py +++ b/utils.py @@ -718,7 +718,7 @@ def deletePost(baseDir: str, httpPrefix: str, def validNickname(domain: str, nickname: str) -> bool: - forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@') + forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@', '#') for c in forbiddenChars: if c in nickname: return False From de67a2afe478a26dfd36324133ba6c13258fbd21 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:33:42 +0100 Subject: [PATCH 027/263] Less space --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index b972d0e5a..0f86c7f2e 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1310,7 +1310,7 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: '' resultStr += \ '' + \ - nick + ': ' + nick + ':' resultStr += \ '' From b26a42d798e975ef053b605c8efefdc4767d76b1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 17:48:49 +0100 Subject: [PATCH 028/263] Highlight selected button --- webinterface.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/webinterface.py b/webinterface.py index 0f86c7f2e..9baaf060c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1300,7 +1300,7 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: title = item[0] url = item[1] nick = item[2] - # status = item[3] + status = item[3] postFilename = item[4].replace('/', '#') # create the html for this post @@ -1321,16 +1321,26 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: resultStr += \ '' - resultStr += \ - '' + if '[vote:' + nickname + ':approve]' in status: + resultStr += \ + '' + else: + resultStr += \ + '' resultStr += \ '' - resultStr += \ - '' + if '[vote:' + nickname + ':deny]' in status: + resultStr += \ + '' + else: + resultStr += \ + '' resultStr += \ '' @@ -1320,7 +1321,7 @@ def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: resultStr += \ '' + '/newswireapprove=' + postLink + '">' if '[vote:' + nickname + ':approve]' in status: resultStr += \ '' + '/newswirediscuss=' + postLink + '">' + if os.path.isfile(postFilename + '.discuss'): + resultStr += \ + '' + else: + resultStr += \ + '' resultStr += '
' return resultStr From 1d7a0192170725dfc662b35c016fa1ec26da74e6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 19:26:08 +0100 Subject: [PATCH 030/263] Backtracking --- daemon.py | 28 +++++++-------- newswire.py | 82 ------------------------------------------- webinterface.py | 92 ------------------------------------------------- 3 files changed, 14 insertions(+), 188 deletions(-) diff --git a/daemon.py b/daemon.py index b9e32a5dd..1de1cef12 100644 --- a/daemon.py +++ b/daemon.py @@ -8688,21 +8688,21 @@ class PubServer(BaseHTTPRequestHandler): 'show announce done', 'unannounce done') - # send a follow request approval from the web interface - if authorized and '/followapprove=' in self.path and \ + # send a newswire moderation approval from the web interface + if authorized and '/newswireapprove=' in self.path and \ self.path.startswith('/users/'): - self._followApproveButton(callingDomain, self.path, - cookie, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - GETstartTime, GETtimings, - self.server.proxyType, - self.server.debug) + self._moderationApproveButton(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, diff --git a/newswire.py b/newswire.py index 3a9f044f4..0b0e9ffa8 100644 --- a/newswire.py +++ b/newswire.py @@ -187,86 +187,6 @@ def isaBlogPost(postJsonObject: {}) -> bool: return False -def updateNewswireModerationQueue(baseDir: str, handle: str, - maxBlogsPerAccount: int, - moderationDict: {}) -> None: - """Puts new blog posts by untrusted accounts into a moderation queue - """ - accountDir = os.path.join(baseDir + '/accounts', handle) - indexFilename = accountDir + '/tlblogs.index' - if not os.path.isfile(indexFilename): - return - nickname = handle.split('@')[0] - domain = handle.split('@')[1] - with open(indexFilename, 'r') as indexFile: - postFilename = 'start' - ctr = 0 - while postFilename: - postFilename = indexFile.readline() - if postFilename: - # if this is a full path then remove the directories - if '/' in postFilename: - postFilename = postFilename.split('/')[-1] - - # filename of the post without any extension or path - # This should also correspond to any index entry in - # the posts cache - postUrl = \ - postFilename.replace('\n', '').replace('\r', '') - postUrl = postUrl.replace('.json', '').strip() - - # read the post from file - fullPostFilename = \ - locatePost(baseDir, nickname, - domain, postUrl, False) - if not fullPostFilename: - print('Unable to locate post ' + postUrl) - ctr += 1 - if ctr >= maxBlogsPerAccount: - break - continue - - moderationStatusFilename = fullPostFilename + '.moderate' - moderationStatusStr = '' - if not os.path.isfile(moderationStatusFilename): - # create a file used to keep track of moderation status - moderationStatusStr = '[waiting]' - statusFile = open(moderationStatusFilename, "w+") - if statusFile: - statusFile.write(moderationStatusStr) - statusFile.close() - else: - # read the moderation status file - statusFile = open(moderationStatusFilename, "r") - if statusFile: - moderationStatusStr = statusFile.read() - statusFile.close() - - # if the post is still in the moderation queue - if '[accepted]' not in \ - open(moderationStatusFilename).read(): - if '[rejected]' not in \ - open(moderationStatusFilename).read(): - # load the post and add its details to the - # moderation queue - postJsonObject = None - if fullPostFilename: - postJsonObject = loadJson(fullPostFilename) - if isaBlogPost(postJsonObject): - published = postJsonObject['object']['published'] - published = published.replace('T', ' ') - published = published.replace('Z', '+00:00') - moderationDict[published] = \ - [postJsonObject['object']['summary'], - postJsonObject['object']['url'], - nickname, moderationStatusStr, - fullPostFilename] - - ctr += 1 - if ctr >= maxBlogsPerAccount: - break - - def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, newswire: {}, maxBlogsPerAccount: int, @@ -359,8 +279,6 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, # is this account trusted? if not isTrustedByNewswire(baseDir, nickname): - updateNewswireModerationQueue(baseDir, handle, 5, - moderationDict) continue # is there a blogs timeline for this account? diff --git a/webinterface.py b/webinterface.py index 0cc17ee84..de5087003 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1274,90 +1274,6 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, return editLinksForm -def htmlNewswireModeration(baseDir: str, path: str, translate: {}) -> str: - """Get a list of newswire items to be moderated - """ - if '/users/' not in path: - return '' - - # load the file containing newswire posts to be moderated - newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' - moderateJson = loadJson(newswireModerationFilename) - if not moderateJson: - return '' - - # get the nickname and actor path of the moderator - nickname = path.split('/users/')[1] - if '/' in nickname: - nickname = nickname.split('/')[0] - basePath = path.split('/users/')[0] + '/users/' + nickname - - resultStr = '' - - # for each post to be moderated - for dateStr, item in moderateJson.items(): - # details of this post - title = item[0] - url = item[1] - nick = item[2] - status = item[3] - postFilename = item[4] - postLink = postFilename.replace(baseDir + '/accounts', '') - - # create the html for this post - resultStr += '
' - - resultStr += \ - '' - resultStr += \ - '' + \ - nick + ':' - - resultStr += \ - '' - resultStr += \ - '' + \ - title + '' - - resultStr += \ - '' - if '[vote:' + nickname + ':approve]' in status: - resultStr += \ - '' - else: - resultStr += \ - '' - - resultStr += \ - '' - if '[vote:' + nickname + ':deny]' in status: - resultStr += \ - '' - else: - resultStr += \ - '' - - resultStr += \ - '' - if os.path.isfile(postFilename + '.discuss'): - resultStr += \ - '' - else: - resultStr += \ - '' - resultStr += '
' - return resultStr - - def htmlEditNewswire(translate: {}, baseDir: str, path: str, domain: str, port: int, httpPrefix: str) -> str: """Shows the edit newswire screen @@ -1420,14 +1336,6 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, editNewswireForm += \ '
' - newswireModerationFilename = baseDir + '/accounts/newswiremoderation.txt' - if os.path.isfile(newswireModerationFilename): - editNewswireForm += \ - ' ' + \ - translate['Posts to be approved'] + ':
' - editNewswireForm += \ - htmlNewswireModeration(baseDir, path, translate) + '
' - editNewswireForm += \ ' ' + \ translate['Add RSS feed links below.'] + \ From 454b4f3348d1a917defb20a1569645c985eee972 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 19:32:24 +0100 Subject: [PATCH 031/263] Backtracking --- newswire.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/newswire.py b/newswire.py index 0b0e9ffa8..ff2274738 100644 --- a/newswire.py +++ b/newswire.py @@ -239,24 +239,6 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, break -def isTrustedByNewswire(baseDir: str, nickname: str) -> bool: - """Returns true if the given nickname is trusted to post - blog entries to the newswire - """ - # adminNickname = getConfigParam(baseDir, 'admin') - # if nickname == adminNickname: - # return True - - newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' - if os.path.isfile(newswireTrustedFilename): - with open(newswireTrustedFilename, "r") as f: - lines = f.readlines() - for trusted in lines: - if trusted.strip('\n').strip('\r') == nickname: - return True - return False - - def addBlogsToNewswire(baseDir: str, newswire: {}, maxBlogsPerAccount: int) -> None: """Adds blogs from each user account into the newswire @@ -277,10 +259,6 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, if isSuspended(baseDir, nickname): continue - # is this account trusted? - if not isTrustedByNewswire(baseDir, nickname): - continue - # is there a blogs timeline for this account? accountDir = os.path.join(baseDir + '/accounts', handle) blogsIndex = accountDir + '/tlblogs.index' From 6481e9f5283622db1f144276294906841d9191d4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 19:34:14 +0100 Subject: [PATCH 032/263] Backtracking --- webinterface.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/webinterface.py b/webinterface.py index de5087003..6cbaa2e29 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1326,13 +1326,6 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, with open(newswireFilename, 'r') as fp: newswireStr = fp.read() - # get the list of handles who are trusted to post to the newswire - newswireTrusted = '' - newswireTrustedFilename = baseDir + '/accounts/newswiretrusted.txt' - if os.path.isfile(newswireTrustedFilename): - with open(newswireTrustedFilename, "r") as trustFile: - newswireTrusted = trustFile.read() - editNewswireForm += \ '
' @@ -1344,14 +1337,6 @@ def htmlEditNewswire(translate: {}, baseDir: str, path: str, ' ' - editNewswireForm += \ - ' ' + \ - translate['Nicknames whose blog entries appear on the newswire.'] + \ - '
' - editNewswireForm += \ - ' ' - editNewswireForm += \ '
' From 143354d5376f7d7a4a8b2d30d7c299685e3f7fc6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 20:13:02 +0100 Subject: [PATCH 033/263] Show approved newswire items in a different style --- epicyon-profile.css | 15 +++++++++++++++ newswire.py | 3 ++- webinterface.py | 32 ++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index ebf9d99a4..8a59c6386 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -15,6 +15,7 @@ --main-fg-color: #dddddd; --column-left-fg-color: #dddddd; --column-right-fg-color: #dddddd; + --column-right-fg-color-approved: yellow; --main-link-color: #999; --main-link-color-hover: #bbb; --main-visited-color: #888; @@ -231,12 +232,26 @@ a:focus { line-height: var(--line-spacing-newswire); } +.newswireItemApproved { + font-size: var(--font-size-newswire); + font-weight: bold; + color: var(--column-right-fg-color-approved); + line-height: var(--line-spacing-newswire); +} + .newswireDate { font-size: var(--font-size-newswire); color: var(--newswire-date-color); float: right; } +.newswireDateApproved { + font-size: var(--font-size-newswire); + font-weight: bold; + color: var(--newswire-date-color); + float: right; +} + .new-post-text { font-size: var(--font-size2); font-family: Arial, Helvetica, sans-serif; diff --git a/newswire.py b/newswire.py index ff2274738..d47222c0d 100644 --- a/newswire.py +++ b/newswire.py @@ -232,7 +232,8 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, published = published.replace('Z', '+00:00') newswire[published] = \ [postJsonObject['object']['summary'], - postJsonObject['object']['url']] + postJsonObject['object']['url'], + ['votes:0']] ctr += 1 if ctr >= maxBlogsPerAccount: diff --git a/webinterface.py b/webinterface.py index 6cbaa2e29..50ed03082 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5339,15 +5339,35 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, return htmlStr -def htmlNewswire(newswire: str) -> str: +def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: """Converts a newswire dict into html """ htmlStr = '' for dateStr, item in newswire.items(): - htmlStr += '

' + \ - '' + item[0] + '' - htmlStr += '

' + if 'vote:' + nickname in item[2]: + htmlStr += '

' + \ + '' + item[0] + '' + if moderator: + htmlStr += \ + '

' + else: + htmlStr += '

' + else: + htmlStr += '

' + \ + '' + item[0] + '' + if moderator: + htmlStr += \ + '

' + else: + htmlStr += '

' return htmlStr @@ -5426,7 +5446,7 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, else: htmlStr += '
\n' - htmlStr += htmlNewswire(newswire) + htmlStr += htmlNewswire(newswire, nickname, moderator) return htmlStr From 83bfd0b40a67e7ad16f49ee294e3824977dc063c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 20:19:12 +0100 Subject: [PATCH 034/263] Three fields in item --- newswire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newswire.py b/newswire.py index d47222c0d..6418560cf 100644 --- a/newswire.py +++ b/newswire.py @@ -79,7 +79,7 @@ def xml2StrToDict(xmlStr: str) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z") - result[str(publishedDate)] = [title, link] + result[str(publishedDate)] = [title, link, ['votes:0']] parsed = True except BaseException: pass From 92bac79367cb393deebe7ec08b916c15df78340d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 20:25:41 +0100 Subject: [PATCH 035/263] Switch label order --- webinterface.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/webinterface.py b/webinterface.py index 50ed03082..743c05892 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5349,10 +5349,11 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: '' + item[0] + '' if moderator: htmlStr += \ - '

' + '/newswireunvote=' + dateStr.replace(' ', 'T') + '">' + \ + '

' else: htmlStr += '

' @@ -5361,10 +5362,11 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: '' + item[0] + '' if moderator: htmlStr += \ - '

' + '/newswirevote=' + dateStr.replace(' ', 'T') + '">' + \ + '

' else: htmlStr += '

' From 20d3a93be8c96769063e44e9b4d3815d722884b9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 20:32:31 +0100 Subject: [PATCH 036/263] Less verbose date link --- webinterface.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webinterface.py b/webinterface.py index 743c05892..8d4eb8c9f 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5344,6 +5344,8 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: """ htmlStr = '' for dateStr, item in newswire.items(): + dateStrLink = dateStr.replace(' ', 'T') + dateStrLink = dateStrLink.replace('+00:00', '') if 'vote:' + nickname in item[2]: htmlStr += '

' + \ '' + item[0] + '' @@ -5351,9 +5353,9 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: htmlStr += \ ' ' + \ '' + \ + '/newswireunvote=' + dateStrLink + '">' + \ '

' + htmlStr += dateStr + '

' else: htmlStr += '

' @@ -5364,7 +5366,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: htmlStr += \ ' ' + \ '' + \ + '/newswirevote=' + dateStrLink + '">' + \ '

' else: From adc6cd93176cd5b728db413757411367528a38f1 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 20:46:22 +0100 Subject: [PATCH 037/263] Show total moderator votes on newswire items --- newswire.py | 5 ++--- webinterface.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/newswire.py b/newswire.py index 6418560cf..1c1aee157 100644 --- a/newswire.py +++ b/newswire.py @@ -79,7 +79,7 @@ def xml2StrToDict(xmlStr: str) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z") - result[str(publishedDate)] = [title, link, ['votes:0']] + result[str(publishedDate)] = [title, link, []] parsed = True except BaseException: pass @@ -232,8 +232,7 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, published = published.replace('Z', '+00:00') newswire[published] = \ [postJsonObject['object']['summary'], - postJsonObject['object']['url'], - ['votes:0']] + postJsonObject['object']['url'], []] ctr += 1 if ctr >= maxBlogsPerAccount: diff --git a/webinterface.py b/webinterface.py index 8d4eb8c9f..4e57e0127 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5347,8 +5347,19 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: dateStrLink = dateStr.replace(' ', 'T') dateStrLink = dateStrLink.replace('+00:00', '') if 'vote:' + nickname in item[2]: + totalVotesStr = '' + if moderator: + # count the total votes for this item + totalVotes = 0 + for line in item[2]: + if '[vote:' in line: + totalVotes += 1 + if totalVotes > 0: + totalVotesStr = ' +' + str(totalVotes) + htmlStr += '

' + \ - '' + item[0] + '' + '' + item[0] + '' + \ + totalVotesStr if moderator: htmlStr += \ ' ' + \ @@ -5360,8 +5371,19 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: htmlStr += '

' else: + totalVotesStr = '' + if moderator: + # count the total votes for this item + totalVotes = 0 + for line in item[2]: + if '[vote:' in line: + totalVotes += 1 + if totalVotes > 0: + totalVotesStr = ' +' + str(totalVotes) + htmlStr += '

' + \ - '' + item[0] + '' + '' + item[0] + '' + \ + totalVotesStr if moderator: htmlStr += \ ' ' + \ From 576a2555e2934efa1174bcc20584a8f454871c80 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 21:17:34 +0100 Subject: [PATCH 038/263] Voting on newswire items --- daemon.py | 138 +++++++++++++++++++++++++++++++++++++++++++----- newswire.py | 8 ++- webinterface.py | 4 +- 3 files changed, 132 insertions(+), 18 deletions(-) diff --git a/daemon.py b/daemon.py index 1de1cef12..fbee5112d 100644 --- a/daemon.py +++ b/daemon.py @@ -4657,6 +4657,82 @@ class PubServer(BaseHTTPRequestHandler): 'follow approve shown') self.server.GETbusy = False + def _newswireVote(self, callingDomain: str, path: str, + cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, debug: bool): + """Vote for a newswire item + """ + originPathStr = path.split('/newswirevote=')[0] + dateStr = path.split('/newswirevote=')[1].replace('T', ' ') + '+00:00' + nickname = originPathStr.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if self.newswire.get(dateStr): + if isModerator(baseDir, nickname): + if 'vote:' + nickname not in self.newswire[dateStr][2]: + self.newswire[dateStr][2].append('vote:' + nickname) + filename = self.newswire[dateStr][3] + if filename: + saveJson(self.newswire[dateStr][2], + filename + '.votes') + + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + self._redirect_headers(originPathStrAbsolute, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unannounce done', + 'follow approve shown') + self.server.GETbusy = False + + def _newswireUnvote(self, callingDomain: str, path: str, + cookie: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, debug: bool): + """Remove vote for a newswire item + """ + originPathStr = path.split('/newswirevote=')[0] + dateStr = path.split('/newswirevote=')[1].replace('T', ' ') + '+00:00' + nickname = originPathStr.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if self.newswire.get(dateStr): + if isModerator(baseDir, nickname): + if 'vote:' + nickname in self.newswire[dateStr][2]: + self.newswire[dateStr][2].remove('vote:' + nickname) + filename = self.newswire[dateStr][3] + if filename: + saveJson(self.newswire[dateStr][2], + filename + '.votes') + + originPathStrAbsolute = \ + httpPrefix + '://' + domainFull + originPathStr + if callingDomain.endswith('.onion') and onionDomain: + originPathStrAbsolute = \ + 'http://' + onionDomain + originPathStr + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStrAbsolute = \ + 'http://' + i2pDomain + originPathStr + self._redirect_headers(originPathStrAbsolute, + cookie, callingDomain) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'unannounce done', + 'follow approve shown') + self.server.GETbusy = False + def _followDenyButton(self, callingDomain: str, path: str, cookie: str, baseDir: str, httpPrefix: str, @@ -8688,21 +8764,55 @@ class PubServer(BaseHTTPRequestHandler): 'show announce done', 'unannounce done') - # send a newswire moderation approval from the web interface - if authorized and '/newswireapprove=' in self.path and \ + # send a newswire moderation vote from the web interface + if authorized and '/newswirevote=' in self.path and \ self.path.startswith('/users/'): - self._moderationApproveButton(callingDomain, self.path, - cookie, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - GETstartTime, GETtimings, - self.server.proxyType, - self.server.debug) + self._newswireVote(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) + return + + # send a newswire moderation unvote from the web interface + if authorized and '/newswireunvote=' in self.path and \ + self.path.startswith('/users/'): + self._newswireUnvote(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) + return + + # send a follow request approval from the web interface + if authorized and '/followapprove=' in self.path and \ + self.path.startswith('/users/'): + self._followApproveButton(callingDomain, self.path, + cookie, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + self.server.debug) return self._benchmarkGETtimings(GETstartTime, GETtimings, diff --git a/newswire.py b/newswire.py index 1c1aee157..7b720b69f 100644 --- a/newswire.py +++ b/newswire.py @@ -79,7 +79,7 @@ def xml2StrToDict(xmlStr: str) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z") - result[str(publishedDate)] = [title, link, []] + result[str(publishedDate)] = [title, link, [], ''] parsed = True except BaseException: pass @@ -230,9 +230,13 @@ def addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, published = postJsonObject['object']['published'] published = published.replace('T', ' ') published = published.replace('Z', '+00:00') + votes = [] + if os.path.isfile(fullPostFilename + '.votes'): + votes = loadJson(fullPostFilename + '.votes') newswire[published] = \ [postJsonObject['object']['summary'], - postJsonObject['object']['url'], []] + postJsonObject['object']['url'], votes, + fullPostFilename] ctr += 1 if ctr >= maxBlogsPerAccount: diff --git a/webinterface.py b/webinterface.py index 4e57e0127..a235a409c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5352,7 +5352,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: # count the total votes for this item totalVotes = 0 for line in item[2]: - if '[vote:' in line: + if 'vote:' in line: totalVotes += 1 if totalVotes > 0: totalVotesStr = ' +' + str(totalVotes) @@ -5376,7 +5376,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: # count the total votes for this item totalVotes = 0 for line in item[2]: - if '[vote:' in line: + if 'vote:' in line: totalVotes += 1 if totalVotes > 0: totalVotesStr = ' +' + str(totalVotes) From 673fd6ef199cea2c64d7c02b6758323c0eff49b0 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 21:22:48 +0100 Subject: [PATCH 039/263] Pass newswire as parameter --- daemon.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/daemon.py b/daemon.py index fbee5112d..b9b849525 100644 --- a/daemon.py +++ b/daemon.py @@ -4663,7 +4663,8 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, GETstartTime, GETtimings: {}, - proxyType: str, debug: bool): + proxyType: str, debug: bool, + newswire: {}): """Vote for a newswire item """ originPathStr = path.split('/newswirevote=')[0] @@ -4671,13 +4672,13 @@ class PubServer(BaseHTTPRequestHandler): nickname = originPathStr.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] - if self.newswire.get(dateStr): + if newswire.get(dateStr): if isModerator(baseDir, nickname): - if 'vote:' + nickname not in self.newswire[dateStr][2]: - self.newswire[dateStr][2].append('vote:' + nickname) - filename = self.newswire[dateStr][3] + if 'vote:' + nickname not in newswire[dateStr][2]: + newswire[dateStr][2].append('vote:' + nickname) + filename = newswire[dateStr][3] if filename: - saveJson(self.newswire[dateStr][2], + saveJson(newswire[dateStr][2], filename + '.votes') originPathStrAbsolute = \ @@ -4701,7 +4702,8 @@ class PubServer(BaseHTTPRequestHandler): domain: str, domainFull: str, port: int, onionDomain: str, i2pDomain: str, GETstartTime, GETtimings: {}, - proxyType: str, debug: bool): + proxyType: str, debug: bool, + newswire: {}): """Remove vote for a newswire item """ originPathStr = path.split('/newswirevote=')[0] @@ -4709,13 +4711,13 @@ class PubServer(BaseHTTPRequestHandler): nickname = originPathStr.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] - if self.newswire.get(dateStr): + if newswire.get(dateStr): if isModerator(baseDir, nickname): - if 'vote:' + nickname in self.newswire[dateStr][2]: - self.newswire[dateStr][2].remove('vote:' + nickname) - filename = self.newswire[dateStr][3] + if 'vote:' + nickname in newswire[dateStr][2]: + newswire[dateStr][2].remove('vote:' + nickname) + filename = newswire[dateStr][3] if filename: - saveJson(self.newswire[dateStr][2], + saveJson(newswire[dateStr][2], filename + '.votes') originPathStrAbsolute = \ @@ -8778,7 +8780,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2pDomain, GETstartTime, GETtimings, self.server.proxyType, - self.server.debug) + self.server.debug, + self.server.newswire) return # send a newswire moderation unvote from the web interface @@ -8795,7 +8798,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2pDomain, GETstartTime, GETtimings, self.server.proxyType, - self.server.debug) + self.server.debug, + self.server.newswire) return # send a follow request approval from the web interface From b2a07a52982ea3d3e38a20ea1c75dca4a1b7b859 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 21:26:25 +0100 Subject: [PATCH 040/263] unvote --- daemon.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/daemon.py b/daemon.py index b9b849525..06af3595c 100644 --- a/daemon.py +++ b/daemon.py @@ -4668,7 +4668,8 @@ class PubServer(BaseHTTPRequestHandler): """Vote for a newswire item """ originPathStr = path.split('/newswirevote=')[0] - dateStr = path.split('/newswirevote=')[1].replace('T', ' ') + '+00:00' + dateStr = \ + path.split('/newswirevote=')[1].replace('T', ' ') + '+00:00' nickname = originPathStr.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] @@ -4706,8 +4707,9 @@ class PubServer(BaseHTTPRequestHandler): newswire: {}): """Remove vote for a newswire item """ - originPathStr = path.split('/newswirevote=')[0] - dateStr = path.split('/newswirevote=')[1].replace('T', ' ') + '+00:00' + originPathStr = path.split('/newswireunvote=')[0] + dateStr = \ + path.split('/newswireunvote=')[1].replace('T', ' ') + '+00:00' nickname = originPathStr.split('/users/')[1] if '/' in nickname: nickname = nickname.split('/')[0] From 1457e966fec7880601b8b9a902feb7dd3e0d05dc Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 21:31:11 +0100 Subject: [PATCH 041/263] Return to default timeline after voting on newswire item --- daemon.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/daemon.py b/daemon.py index 06af3595c..592a07116 100644 --- a/daemon.py +++ b/daemon.py @@ -4683,7 +4683,8 @@ class PubServer(BaseHTTPRequestHandler): filename + '.votes') originPathStrAbsolute = \ - httpPrefix + '://' + domainFull + originPathStr + httpPrefix + '://' + domainFull + originPathStr + '/' + \ + self.server.defaultTimeline if callingDomain.endswith('.onion') and onionDomain: originPathStrAbsolute = \ 'http://' + onionDomain + originPathStr @@ -4694,7 +4695,7 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self._benchmarkGETtimings(GETstartTime, GETtimings, 'unannounce done', - 'follow approve shown') + 'vote for newswite item') self.server.GETbusy = False def _newswireUnvote(self, callingDomain: str, path: str, @@ -4723,7 +4724,8 @@ class PubServer(BaseHTTPRequestHandler): filename + '.votes') originPathStrAbsolute = \ - httpPrefix + '://' + domainFull + originPathStr + httpPrefix + '://' + domainFull + originPathStr + '/' + \ + self.server.defaultTimeline if callingDomain.endswith('.onion') and onionDomain: originPathStrAbsolute = \ 'http://' + onionDomain + originPathStr @@ -4734,7 +4736,7 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self._benchmarkGETtimings(GETstartTime, GETtimings, 'unannounce done', - 'follow approve shown') + 'unvote for newswite item') self.server.GETbusy = False def _followDenyButton(self, callingDomain: str, path: str, From 092be5c5c542532d01149e69304a3e13fb4a1843 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 22:00:53 +0100 Subject: [PATCH 042/263] Remove votes file if post is deleted --- utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/utils.py b/utils.py index a8aa5416f..59124cd8f 100644 --- a/utils.py +++ b/utils.py @@ -633,10 +633,10 @@ def deletePost(baseDir: str, httpPrefix: str, if os.path.isfile(muteFilename): os.remove(muteFilename) - # remove any moderation file - moderationFilename = postFilename + '.moderate' - if os.path.isfile(moderationFilename): - os.remove(moderationFilename) + # remove any votes file + votesFilename = postFilename + '.votes' + if os.path.isfile(votesFilename): + os.remove(votesFilename) # remove cached html version of the post cachedPostFilename = \ From f46f7e4480de0a3497190db4dc3127fc4569e9bb Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 22:28:40 +0100 Subject: [PATCH 043/263] No slash --- newswire.py | 5 ++++- translations/ar.json | 4 +++- translations/ca.json | 4 +++- translations/cy.json | 4 +++- translations/de.json | 4 +++- translations/en.json | 4 +++- translations/es.json | 4 +++- translations/fr.json | 4 +++- translations/ga.json | 4 +++- translations/hi.json | 4 +++- translations/it.json | 4 +++- translations/ja.json | 4 +++- translations/oc.json | 4 +++- translations/pt.json | 4 +++- translations/ru.json | 4 +++- translations/zh.json | 4 +++- webinterface.py | 11 +++++++---- 17 files changed, 56 insertions(+), 20 deletions(-) diff --git a/newswire.py b/newswire.py index 7b720b69f..f5d9eaa41 100644 --- a/newswire.py +++ b/newswire.py @@ -17,7 +17,6 @@ from utils import locatePost from utils import loadJson from utils import saveJson from utils import isSuspended -# from utils import getConfigParam def rss2Header(httpPrefix: str, @@ -263,6 +262,10 @@ def addBlogsToNewswire(baseDir: str, newswire: {}, if isSuspended(baseDir, nickname): continue + if os.path.isfile(baseDir + '/accounts/' + handle + + '/.nonewswire'): + continue + # is there a blogs timeline for this account? accountDir = os.path.join(baseDir + '/accounts', handle) blogsIndex = accountDir + '/tlblogs.index' diff --git a/translations/ar.json b/translations/ar.json index ac05265f4..0b9ffa554 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "الألقاب التي تظهر إدخالات المدونة الخاصة بها على موقع الأخبار.", "Posts to be approved": "الوظائف المطلوب الموافقة عليها", "Discuss": "مناقشة", - "Moderator Discussion": "مناقشة المنسق" + "Moderator Discussion": "مناقشة المنسق", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/ca.json b/translations/ca.json index 78fbff613..7d1daca4d 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Sobrenoms les entrades del bloc apareixen a newswire.", "Posts to be approved": "Missatges per aprovar", "Discuss": "Discuteix", - "Moderator Discussion": "Discussió sobre moderadors" + "Moderator Discussion": "Discussió sobre moderadors", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/cy.json b/translations/cy.json index c4ed63de5..67522c007 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Llysenwau y mae eu cofnodion blog yn ymddangos ar y we newyddion.", "Posts to be approved": "Swyddi i'w cymeradwyo", "Discuss": "Trafodwch", - "Moderator Discussion": "Trafodaeth Cymedrolwr" + "Moderator Discussion": "Trafodaeth Cymedrolwr", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/de.json b/translations/de.json index a5f935f93..0f1819f6e 100644 --- a/translations/de.json +++ b/translations/de.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Spitznamen, deren Blogeinträge im Newswire erscheinen.", "Posts to be approved": "Zu genehmigende Beiträge", "Discuss": "Diskutieren", - "Moderator Discussion": "Moderatorendiskussion" + "Moderator Discussion": "Moderatorendiskussion", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/en.json b/translations/en.json index 3f61e3366..a5a9a06b5 100644 --- a/translations/en.json +++ b/translations/en.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", "Posts to be approved": "Posts to be approved", "Discuss": "Discuss", - "Moderator Discussion": "Moderator Discussion" + "Moderator Discussion": "Moderator Discussion", + "Vote": "Vote", + "Remove Vote": "Remove Vote" } diff --git a/translations/es.json b/translations/es.json index 1eaf08769..ec0d739fc 100644 --- a/translations/es.json +++ b/translations/es.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Apodos cuyas entradas de blog aparecen en el newswire.", "Posts to be approved": "Publicaciones a aprobar", "Discuss": "Discutir", - "Moderator Discussion": "Discusión del moderador" + "Moderator Discussion": "Discusión del moderador", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/fr.json b/translations/fr.json index 4fa45c874..ed75d08c7 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Surnoms dont les entrées de blog apparaissent sur le fil de presse.", "Posts to be approved": "Postes à approuver", "Discuss": "Discuter", - "Moderator Discussion": "Discussion du modérateur" + "Moderator Discussion": "Discussion du modérateur", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/ga.json b/translations/ga.json index 4bd8edfb2..230498980 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Leasainmneacha a bhfuil a n-iontrálacha blag le feiceáil ar an sreang nuachta.", "Posts to be approved": "Poist le ceadú", "Discuss": "Pléigh", - "Moderator Discussion": "Plé Modhnóir" + "Moderator Discussion": "Plé Modhnóir", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/hi.json b/translations/hi.json index 1df55af62..e11084c3f 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "उपनाम जिनकी ब्लॉग प्रविष्टियाँ न्यूज़वायर पर दिखाई देती हैं।", "Posts to be approved": "स्वीकृत किए जाने वाले पद", "Discuss": "चर्चा करें", - "Moderator Discussion": "मॉडरेटर चर्चा" + "Moderator Discussion": "मॉडरेटर चर्चा", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/it.json b/translations/it.json index 390e7fce4..ff891a91d 100644 --- a/translations/it.json +++ b/translations/it.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Soprannomi le cui voci di blog compaiono nel newswire.", "Posts to be approved": "Post da approvare", "Discuss": "Discutere", - "Moderator Discussion": "Discussione del moderatore" + "Moderator Discussion": "Discussione del moderatore", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/ja.json b/translations/ja.json index 34e7afbb9..c79ceaf4d 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "ブログエントリがニュースワイヤーに表示されるニックネーム。", "Posts to be approved": "承認される投稿", "Discuss": "議論する", - "Moderator Discussion": "モデレーターディスカッション" + "Moderator Discussion": "モデレーターディスカッション", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/oc.json b/translations/oc.json index 14e8e3888..a2304146a 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -296,5 +296,7 @@ "Nicknames whose blog entries appear on the newswire.": "Nicknames whose blog entries appear on the newswire.", "Posts to be approved": "Posts to be approved", "Discuss": "Discuss", - "Moderator Discussion": "Moderator Discussion" + "Moderator Discussion": "Moderator Discussion", + "Vote": "Vote", + "Remove Vote": "Remove Vote" } diff --git a/translations/pt.json b/translations/pt.json index d4d23a91a..2c8dc07b5 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Apelidos cujas entradas de blog aparecem nos jornais.", "Posts to be approved": "Postagens a serem aprovadas", "Discuss": "Discutir", - "Moderator Discussion": "Discussão do moderador" + "Moderator Discussion": "Discussão do moderador", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/ru.json b/translations/ru.json index f41695c62..e9465e880 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "Псевдонимы, чьи записи блога появляются в ленте новостей.", "Posts to be approved": "Посты на утверждение", "Discuss": "Обсудить", - "Moderator Discussion": "Обсуждение модератором" + "Moderator Discussion": "Обсуждение модератором", + "Vote": "", + "Remove Vote": "" } diff --git a/translations/zh.json b/translations/zh.json index 30417890a..9da0fbfe5 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -300,5 +300,7 @@ "Nicknames whose blog entries appear on the newswire.": "博客条目出现在新闻专线上的昵称。", "Posts to be approved": "职位待批准", "Discuss": "讨论", - "Moderator Discussion": "主持人讨论" + "Moderator Discussion": "主持人讨论", + "Vote": "", + "Remove Vote": "" } diff --git a/webinterface.py b/webinterface.py index a235a409c..ea0c18fcd 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5339,7 +5339,8 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, return htmlStr -def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: +def htmlNewswire(newswire: str, nickname: str, moderator: bool, + translate: {}) -> str: """Converts a newswire dict into html """ htmlStr = '' @@ -5364,7 +5365,8 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: htmlStr += \ ' ' + \ '' + \ + '/newswireunvote=' + dateStrLink + '" ' + \ + 'title="' + translate['Remove Vote'] + '">' + \ '

' else: @@ -5388,7 +5390,8 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool) -> str: htmlStr += \ ' ' + \ '' + \ + '/newswirevote=' + dateStrLink + '" ' + \ + 'title="' + translate['Vote'] + '">' + \ '

' else: @@ -5472,7 +5475,7 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, else: htmlStr += '
\n' - htmlStr += htmlNewswire(newswire, nickname, moderator) + htmlStr += htmlNewswire(newswire, nickname, moderator, translate) return htmlStr From 81834dfcaadd0d0f1e07a1d45e852a561ee4aaf8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 6 Oct 2020 22:33:54 +0100 Subject: [PATCH 044/263] Remove date ending --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index ea0c18fcd..e987271d9 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5368,7 +5368,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, '/newswireunvote=' + dateStrLink + '" ' + \ 'title="' + translate['Remove Vote'] + '">' + \ '

' + htmlStr += dateStr.replace('+00:00', '') + '

' else: htmlStr += '

' From 9d3015861804dddd8cfa1a599c0a430f728ea064 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 10:10:42 +0100 Subject: [PATCH 045/263] News instance type --- daemon.py | 48 +++++++++++++++++++++++++++++++++++++++++--- epicyon.py | 16 ++++++++++++++- person.py | 7 ++++++- posts.py | 19 ++++++++++++++---- tests.py | 9 ++++++--- translations/ar.json | 4 +++- translations/ca.json | 4 +++- translations/cy.json | 4 +++- translations/de.json | 4 +++- translations/en.json | 4 +++- translations/es.json | 4 +++- translations/fr.json | 4 +++- translations/ga.json | 4 +++- translations/hi.json | 4 +++- translations/it.json | 4 +++- translations/ja.json | 4 +++- translations/oc.json | 4 +++- translations/pt.json | 4 +++- translations/ru.json | 4 +++- translations/zh.json | 4 +++- utils.py | 2 +- webinterface.py | 40 ++++++++++++++++++++++++++++++++++++ 22 files changed, 173 insertions(+), 28 deletions(-) diff --git a/daemon.py b/daemon.py index 592a07116..e78989fea 100644 --- a/daemon.py +++ b/daemon.py @@ -3436,6 +3436,7 @@ class PubServer(BaseHTTPRequestHandler): if fields['mediaInstance'] == 'on': self.server.mediaInstance = True self.server.blogsInstance = False + self.server.newsInstance = False self.server.defaultTimeline = 'tlmedia' setConfigParam(baseDir, "mediaInstance", @@ -3443,6 +3444,9 @@ class PubServer(BaseHTTPRequestHandler): setConfigParam(baseDir, "blogsInstance", self.server.blogsInstance) + setConfigParam(baseDir, + "newsInstance", + self.server.newsInstance) else: if self.server.mediaInstance: self.server.mediaInstance = False @@ -3451,6 +3455,32 @@ class PubServer(BaseHTTPRequestHandler): "mediaInstance", self.server.mediaInstance) + # change news instance status + if fields.get('newsInstance'): + self.server.newsInstance = False + self.server.defaultTimeline = 'inbox' + if fields['mediaInstance'] == '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 @@ -3458,6 +3488,7 @@ class PubServer(BaseHTTPRequestHandler): if fields['blogsInstance'] == 'on': self.server.blogsInstance = True self.server.mediaInstance = False + self.server.newsInstance = False self.server.defaultTimeline = 'tlblogs' setConfigParam(baseDir, "blogsInstance", @@ -3465,6 +3496,9 @@ class PubServer(BaseHTTPRequestHandler): setConfigParam(baseDir, "mediaInstance", self.server.mediaInstance) + setConfigParam(baseDir, + "newsInstance", + self.server.newsInstance) else: if self.server.blogsInstance: self.server.blogsInstance = False @@ -8632,13 +8666,16 @@ class PubServer(BaseHTTPRequestHandler): nickname = nickname.split('/')[0] self._setMinimal(nickname, not self._isMinimal(nickname)) if not (self.server.mediaInstance or - self.server.blogsInstance): + self.server.blogsInstance or + self.server.newsInstance): self.path = '/users/' + nickname + '/inbox' else: if self.server.blogsInstance: self.path = '/users/' + nickname + '/tlblogs' - else: + elif self.server.mediaInstance: self.path = '/users/' + nickname + '/tlmedia' + else: + self.path = '/users/' + nickname + '/tlnews' # search for a fediverse address, shared item or emoji # from the web interface by selecting search icon @@ -11166,7 +11203,9 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(blogsInstance: bool, mediaInstance: bool, +def runDaemon(newsInstance: bool, + blogsInstance: bool, + mediaInstance: bool, maxRecentPosts: int, enableSharedInbox: bool, registration: bool, language: str, projectVersion: str, @@ -11230,11 +11269,14 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool, httpd.useBlurHash = useBlurHash httpd.mediaInstance = mediaInstance httpd.blogsInstance = blogsInstance + httpd.newsInstance = newsInstance httpd.defaultTimeline = 'inbox' if mediaInstance: httpd.defaultTimeline = 'tlmedia' if blogsInstance: httpd.defaultTimeline = 'tlblogs' + if newsInstance: + httpd.defaultTimeline = 'tlnews' # load translations dictionary httpd.translate = {} diff --git a/epicyon.py b/epicyon.py index 1c0912c6e..9cb4eb300 100644 --- a/epicyon.py +++ b/epicyon.py @@ -195,6 +195,9 @@ parser.add_argument("--mediainstance", type=str2bool, nargs='?', parser.add_argument("--blogsinstance", type=str2bool, nargs='?', const=True, default=False, help="Blogs Instance - favor blogs over microblogging") +parser.add_argument("--newsinstance", type=str2bool, nargs='?', + const=True, default=False, + help="News Instance - favor news over microblogging") parser.add_argument("--debug", type=str2bool, nargs='?', const=True, default=False, help="Show debug messages") @@ -626,6 +629,15 @@ if not args.mediainstance: args.mediainstance = mediaInstance if args.mediainstance: args.blogsinstance = False + args.newsinstance = False + +if not args.newsinstance: + newsInstance = getConfigParam(baseDir, 'newsInstance') + if newsInstance is not None: + args.newsinstance = newsInstance + if args.newsinstance: + args.blogsinstance = False + args.mediainstance = False if not args.blogsinstance: blogsInstance = getConfigParam(baseDir, 'blogsInstance') @@ -633,6 +645,7 @@ if not args.blogsinstance: args.blogsinstance = blogsInstance if args.blogsinstance: args.mediainstance = False + args.newsinstance = False # set the instance title in config.json title = getConfigParam(baseDir, 'instanceTitle') @@ -1898,7 +1911,8 @@ if setTheme(baseDir, themeName): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.blogsinstance, args.mediainstance, + runDaemon(args.newsinstance, + args.blogsinstance, args.mediainstance, args.maxRecentPosts, not args.nosharedinbox, registration, args.language, __version__, diff --git a/person.py b/person.py index 19e8243d4..6eb196708 100644 --- a/person.py +++ b/person.py @@ -23,6 +23,7 @@ from webfinger import storeWebfingerEndpoint from posts import createDMTimeline from posts import createRepliesTimeline from posts import createMediaTimeline +from posts import createNewsTimeline from posts import createBlogsTimeline from posts import createBookmarksTimeline from posts import createEventsTimeline @@ -591,7 +592,7 @@ def personBoxJson(recentPostsCache: {}, """ if boxname != 'inbox' and boxname != 'dm' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \ - boxname != 'tlblogs' and \ + boxname != 'tlblogs' and boxname != 'tlnews' and \ boxname != 'outbox' and boxname != 'moderation' and \ boxname != 'tlbookmarks' and boxname != 'bookmarks' and \ boxname != 'tlevents': @@ -659,6 +660,10 @@ def personBoxJson(recentPostsCache: {}, return createMediaTimeline(session, baseDir, nickname, domain, port, httpPrefix, noOfItems, headerOnly, pageNumber) + elif boxname == 'tlnews': + return createNewsTimeline(session, baseDir, nickname, domain, port, + httpPrefix, noOfItems, headerOnly, + pageNumber) elif boxname == 'tlblogs': return createBlogsTimeline(session, baseDir, nickname, domain, port, httpPrefix, noOfItems, headerOnly, diff --git a/posts.py b/posts.py index 37d947371..739f565ce 100644 --- a/posts.py +++ b/posts.py @@ -505,7 +505,8 @@ def deleteAllPosts(baseDir: str, """Deletes all posts for a person from inbox or outbox """ if boxname != 'inbox' and boxname != 'outbox' and \ - boxname != 'tlblogs' and boxname != 'tlevents': + boxname != 'tlblogs' and boxname != 'tlnews' and \ + boxname != 'tlevents': return boxDir = createPersonDir(nickname, domain, baseDir, boxname) for deleteFilename in os.scandir(boxDir): @@ -527,7 +528,8 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str, Returns the filename """ if boxname != 'inbox' and boxname != 'outbox' and \ - boxname != 'tlblogs' and boxname != 'tlevents' and \ + boxname != 'tlblogs' and boxname != 'tlnews' and \ + boxname != 'tlevents' and \ boxname != 'scheduled': return None originalDomain = domain @@ -2504,6 +2506,15 @@ def createMediaTimeline(session, baseDir: str, nickname: str, domain: str, pageNumber) +def createNewsTimeline(session, baseDir: str, nickname: str, domain: str, + port: int, httpPrefix: str, itemsPerPage: int, + headerOnly: bool, pageNumber=None) -> {}: + return createBoxIndexed({}, session, baseDir, 'tlnews', nickname, + domain, port, httpPrefix, + itemsPerPage, headerOnly, True, + pageNumber) + + def createOutbox(session, baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, itemsPerPage: int, headerOnly: bool, authorized: bool, @@ -2775,7 +2786,7 @@ def addPostStringToTimeline(postStr: str, boxname: str, elif boxname == 'tlreplies': if boxActor not in postStr: return False - elif boxname == 'tlblogs': + elif boxname == 'tlblogs' or boxname == 'tlnews': if '"Create"' not in postStr: return False if '"Article"' not in postStr: @@ -2812,7 +2823,7 @@ def createBoxIndexed(recentPostsCache: {}, if boxname != 'inbox' and boxname != 'dm' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \ - boxname != 'tlblogs' and \ + boxname != 'tlblogs' and boxname != 'tlnews' and \ boxname != 'outbox' and boxname != 'tlbookmarks' and \ boxname != 'bookmarks' and \ boxname != 'tlevents': diff --git a/tests.py b/tests.py index cc0a472c8..8d9a2bd5e 100644 --- a/tests.py +++ b/tests.py @@ -287,7 +287,8 @@ def createServerAlice(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Alice') - runDaemon(False, False, 5, True, True, 'en', __version__, + runDaemon(False, False, False, + 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, httpPrefix, federationList, maxMentions, maxEmoji, False, @@ -349,7 +350,8 @@ def createServerBob(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Bob') - runDaemon(False, False, 5, True, True, 'en', __version__, + runDaemon(False, False, False, + 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, httpPrefix, federationList, maxMentions, maxEmoji, False, @@ -385,7 +387,8 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], onionDomain = None i2pDomain = None print('Server running: Eve') - runDaemon(False, False, 5, True, True, 'en', __version__, + runDaemon(False, False, False, + 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, httpPrefix, federationList, maxMentions, maxEmoji, False, diff --git a/translations/ar.json b/translations/ar.json index 0b9ffa554..ed00b4e6d 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -302,5 +302,7 @@ "Discuss": "مناقشة", "Moderator Discussion": "مناقشة المنسق", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/ca.json b/translations/ca.json index 7d1daca4d..1a2521d80 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -302,5 +302,7 @@ "Discuss": "Discuteix", "Moderator Discussion": "Discussió sobre moderadors", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/cy.json b/translations/cy.json index 67522c007..5d32efd9e 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -302,5 +302,7 @@ "Discuss": "Trafodwch", "Moderator Discussion": "Trafodaeth Cymedrolwr", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/de.json b/translations/de.json index 0f1819f6e..736c0dfd2 100644 --- a/translations/de.json +++ b/translations/de.json @@ -302,5 +302,7 @@ "Discuss": "Diskutieren", "Moderator Discussion": "Moderatorendiskussion", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/en.json b/translations/en.json index a5a9a06b5..420dfec52 100644 --- a/translations/en.json +++ b/translations/en.json @@ -302,5 +302,7 @@ "Discuss": "Discuss", "Moderator Discussion": "Moderator Discussion", "Vote": "Vote", - "Remove Vote": "Remove Vote" + "Remove Vote": "Remove Vote", + "This is a news instance": "This is a news instance", + "News": "News" } diff --git a/translations/es.json b/translations/es.json index ec0d739fc..0ddbbfaa1 100644 --- a/translations/es.json +++ b/translations/es.json @@ -302,5 +302,7 @@ "Discuss": "Discutir", "Moderator Discussion": "Discusión del moderador", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/fr.json b/translations/fr.json index ed75d08c7..8768d0de6 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -302,5 +302,7 @@ "Discuss": "Discuter", "Moderator Discussion": "Discussion du modérateur", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/ga.json b/translations/ga.json index 230498980..0b76ea9a1 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -302,5 +302,7 @@ "Discuss": "Pléigh", "Moderator Discussion": "Plé Modhnóir", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/hi.json b/translations/hi.json index e11084c3f..8da90263a 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -302,5 +302,7 @@ "Discuss": "चर्चा करें", "Moderator Discussion": "मॉडरेटर चर्चा", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/it.json b/translations/it.json index ff891a91d..4963e9326 100644 --- a/translations/it.json +++ b/translations/it.json @@ -302,5 +302,7 @@ "Discuss": "Discutere", "Moderator Discussion": "Discussione del moderatore", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/ja.json b/translations/ja.json index c79ceaf4d..e3dc6f87e 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -302,5 +302,7 @@ "Discuss": "議論する", "Moderator Discussion": "モデレーターディスカッション", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/oc.json b/translations/oc.json index a2304146a..99eae9ad6 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -298,5 +298,7 @@ "Discuss": "Discuss", "Moderator Discussion": "Moderator Discussion", "Vote": "Vote", - "Remove Vote": "Remove Vote" + "Remove Vote": "Remove Vote", + "This is a news instance": "This is a news instance", + "News": "News" } diff --git a/translations/pt.json b/translations/pt.json index 2c8dc07b5..b11639e0f 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -302,5 +302,7 @@ "Discuss": "Discutir", "Moderator Discussion": "Discussão do moderador", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/ru.json b/translations/ru.json index e9465e880..a1b5e091f 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -302,5 +302,7 @@ "Discuss": "Обсудить", "Moderator Discussion": "Обсуждение модератором", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/translations/zh.json b/translations/zh.json index 9da0fbfe5..2d3a7e0cc 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -302,5 +302,7 @@ "Discuss": "讨论", "Moderator Discussion": "主持人讨论", "Vote": "", - "Remove Vote": "" + "Remove Vote": "", + "This is a news instance": "", + "News": "" } diff --git a/utils.py b/utils.py index 59124cd8f..323292acc 100644 --- a/utils.py +++ b/utils.py @@ -728,7 +728,7 @@ def validNickname(domain: str, nickname: str) -> bool: 'public', 'followers', 'channel', 'calendar', 'tlreplies', 'tlmedia', 'tlblogs', - 'tlevents', + 'tlevents', 'tlblogs', 'moderation', 'activity', 'undo', 'reply', 'replies', 'question', 'like', 'likes', 'users', 'statuses', diff --git a/webinterface.py b/webinterface.py index e987271d9..1198a248d 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1374,6 +1374,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str, hideLikeButton = '' mediaInstanceStr = '' blogsInstanceStr = '' + newsInstanceStr = '' displayNickname = nickname bioStr = '' donateUrl = '' @@ -1432,12 +1433,21 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str, if mediaInstance is True: mediaInstanceStr = 'checked' blogsInstanceStr = '' + newsInstanceStr = '' + + newsInstance = getConfigParam(baseDir, "newsInstance") + if newsInstance: + if newsInstance is True: + newsInstanceStr = 'checked' + blogsInstanceStr = '' + mediaInstanceStr = '' blogsInstance = getConfigParam(baseDir, "blogsInstance") if blogsInstance: if blogsInstance is True: blogsInstanceStr = 'checked' mediaInstanceStr = '' + newsInstanceStr = '' filterStr = '' filterFilename = \ @@ -1781,6 +1791,10 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str, ' ' + \ translate['This is a blogging instance'] + '
\n' + editProfileForm += \ + ' ' + \ + translate['This is a news instance'] + '
\n' editProfileForm += '
\n' editProfileForm += '
\n' @@ -5596,6 +5610,7 @@ def htmlTimeline(defaultTimeline: str, # the appearance of buttons - highlighted or not inboxButton = 'button' blogsButton = 'button' + newsButton = 'button' dmButton = 'button' if newDM: dmButton = 'buttonhighlighted' @@ -5616,6 +5631,8 @@ def htmlTimeline(defaultTimeline: str, inboxButton = 'buttonselected' elif boxName == 'tlblogs': blogsButton = 'buttonselected' + elif boxName == 'tlnews': + newsButton = 'buttonselected' elif boxName == 'dm': dmButton = 'buttonselected' if newDM: @@ -5812,6 +5829,12 @@ def htmlTimeline(defaultTimeline: str, '/tlblogs">\n' + elif defaultTimeline == 'tlnews': + tlStr += \ + ' \n' else: tlStr += \ ' ' + translate['Inbox'] + \ '\n' + # typically the news button + # but may change if this is a news oriented instance + if defaultTimeline != 'tlnews': + if not minimal: + tlStr += \ + ' \n' + else: + if not minimal: + tlStr += \ + ' \n' + # button for the outbox tlStr += \ ' Date: Wed, 7 Oct 2020 10:39:18 +0100 Subject: [PATCH 047/263] News timeline placeholder --- daemon.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ webinterface.py | 15 +++++++++ 2 files changed, 103 insertions(+) diff --git a/daemon.py b/daemon.py index e78989fea..9b75dde13 100644 --- a/daemon.py +++ b/daemon.py @@ -119,6 +119,7 @@ from webinterface import htmlInboxDMs from webinterface import htmlInboxReplies from webinterface import htmlInboxMedia from webinterface import htmlInboxBlogs +from webinterface import htmlInboxNews from webinterface import htmlUnblockConfirm from webinterface import htmlPersonOptions from webinterface import htmlIndividualPost @@ -6479,6 +6480,73 @@ class PubServer(BaseHTTPRequestHandler): return True return False + def _showNewsTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the news timeline + """ + if '/users/' in path: + if authorized: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlnews', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + msg = \ + htmlInboxNews(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInBlogsFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain, + self.server.newswire) + msg = msg.encode('utf-8') + self._set_headers('text/html', len(msg), + cookie, callingDomain) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show blogs 2 done', + 'show news 2') + self.server.GETbusy = False + return True + else: + if debug: + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlnews', '') + print('DEBUG: ' + nickname + + ' was not authorized to access ' + path) + if path != '/tlnews': + # not the news inbox + if debug: + print('DEBUG: GET access to news is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + return False + def _showSharesTimeline(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -9399,6 +9467,26 @@ class PubServer(BaseHTTPRequestHandler): 'show media 2 done', 'show blogs 2 done') + # get the news for a given person + if self.path.endswith('/tlnews') or '/tlnews?page=' in self.path: + if self._showNewsTimeline(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return + + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show blogs 2 done', + 'show news 2 done') + # get the shared items timeline for a given person if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path: if self._showSharesTimeline(authorized, diff --git a/webinterface.py b/webinterface.py index 1198a248d..37bc7bec1 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6340,6 +6340,21 @@ def htmlInboxBlogs(defaultTimeline: str, minimal, YTReplacementDomain, newswire) +def htmlInboxNews(defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + newswire: {}) -> str: + """Show the news timeline as html + """ + # TODO + return '' + + def htmlModeration(defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, From 187053bd1ca4186eeecc5b5ddc3771e7522d52ef Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 10:42:59 +0100 Subject: [PATCH 048/263] Blank news timeline --- webinterface.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 37bc7bec1..a8f5d8594 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6351,8 +6351,12 @@ def htmlInboxNews(defaultTimeline: str, newswire: {}) -> str: """Show the news timeline as html """ - # TODO - return '' + return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, {}, 'tlnews', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, newswire) def htmlModeration(defaultTimeline: str, From 72bf620eb7f9ce54151eac370610c23e890f8328 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 10:55:28 +0100 Subject: [PATCH 049/263] Create blog post from news timeline --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index a8f5d8594..f652fe128 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5741,7 +5741,7 @@ def htmlTimeline(defaultTimeline: str, translate['Create a new DM'] + \ '" alt="| ' + translate['Create a new DM'] + \ '" class="timelineicon"/>\n' - elif boxName == 'tlblogs': + elif boxName == 'tlblogs' or boxName == 'tlnews': newPostButtonStr = \ ' None: + """This tries to keep the newswire update thread running even if it dies + """ + print('Starting newswire watchdog') + newswireOriginal = \ + httpd.thrPostSchedule.clone(runNewswireDaemon) + httpd.thrNewswireDaemon.start() + while True: + time.sleep(50) + if not httpd.thrNewswireDaemon.isAlive(): + httpd.thrNewswireDaemon.kill() + httpd.thrNewswireDaemon = \ + newswireOriginal.clone(runNewswireDaemon) + httpd.thrNewswireDaemon.start() + print('Restarting newswire daemon...') diff --git a/newswire.py b/newswire.py index f5d9eaa41..1b86ef6ec 100644 --- a/newswire.py +++ b/newswire.py @@ -70,6 +70,10 @@ def xml2StrToDict(xmlStr: str) -> {}: continue title = rssItem.split('')[1] title = title.split('')[0] + description = '' + if '' in rssItem and '' in rssItem: + description = rssItem.split('')[1] + description = description.split('')[0] link = rssItem.split('')[1] link = link.split('')[0] pubDate = rssItem.split('')[1] @@ -78,7 +82,7 @@ def xml2StrToDict(xmlStr: str) -> {}: try: publishedDate = \ datetime.strptime(pubDate, "%a, %d %b %Y %H:%M:%S %z") - result[str(publishedDate)] = [title, link, [], ''] + result[str(publishedDate)] = [title, link, [], '', description] parsed = True except BaseException: pass @@ -316,47 +320,3 @@ def getDictFromNewswire(session, baseDir: str) -> {}: # sort into chronological order, latest first sortedResult = OrderedDict(sorted(result.items(), reverse=True)) return sortedResult - - -def runNewswireDaemon(baseDir: str, httpd, unused: str): - """Periodically updates RSS feeds - """ - # initial sleep to allow the system to start up - time.sleep(50) - while True: - # has the session been created yet? - if not httpd.session: - print('Newswire daemon waiting for session') - time.sleep(60) - continue - - # try to update the feeds - newNewswire = None - try: - newNewswire = getDictFromNewswire(httpd.session, baseDir) - except Exception as e: - print('WARN: unable to update newswire ' + str(e)) - time.sleep(120) - continue - - httpd.newswire = newNewswire - print('Newswire updated') - # wait a while before the next feeds update - time.sleep(1200) - - -def runNewswireWatchdog(projectVersion: str, httpd) -> None: - """This tries to keep the newswire update thread running even if it dies - """ - print('Starting newswire watchdog') - newswireOriginal = \ - httpd.thrPostSchedule.clone(runNewswireDaemon) - httpd.thrNewswireDaemon.start() - while True: - time.sleep(50) - if not httpd.thrNewswireDaemon.isAlive(): - httpd.thrNewswireDaemon.kill() - httpd.thrNewswireDaemon = \ - newswireOriginal.clone(runNewswireDaemon) - httpd.thrNewswireDaemon.start() - print('Restarting newswire daemon...') diff --git a/posts.py b/posts.py index 739f565ce..1e265ff6f 100644 --- a/posts.py +++ b/posts.py @@ -1193,6 +1193,41 @@ def createBlogPost(baseDir: str, return blog +def createNewsPost(baseDir: str, + nickname: str, domain: str, port: int, httpPrefix: str, + rssTitle: str, rssDescription: str, + attachImageFilename: str, mediaType: str, + imageDescription: str, useBlurhash: bool) -> {}: + """Converts title and description from an rss feed into a post + """ + inReplyTo = None + inReplyToAtomUri = None + schedulePost = False + eventDate = None + eventTime = None + location = None + schedulePost = False + eventDate = None + eventTime = None + location = None + clientToServer = False + saveToFile = False + followersOnly = False + + blog = \ + createPublicPost(baseDir, + nickname, domain, port, httpPrefix, + rssDescription, followersOnly, saveToFile, + clientToServer, + attachImageFilename, mediaType, + imageDescription, useBlurhash, + inReplyTo, inReplyToAtomUri, rssTitle, + schedulePost, + eventDate, eventTime, location) + blog['object']['type'] = 'Article' + return blog + + def createQuestionPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, content: str, qOptions: [], From 6208a3f00f4eecda5fb473064b9b634b1f8959b6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 14:51:29 +0100 Subject: [PATCH 051/263] Convert rss feed items to activitypub posts --- daemon.py | 4 ++- newsdaemon.py | 73 +++++++++++++++++++++++++++++++++++++++++++- newswire.py | 1 - posts.py | 3 +- translations/ar.json | 3 +- translations/ca.json | 3 +- translations/cy.json | 3 +- translations/de.json | 3 +- translations/en.json | 3 +- translations/es.json | 3 +- translations/fr.json | 3 +- translations/ga.json | 3 +- translations/hi.json | 3 +- translations/it.json | 3 +- translations/ja.json | 3 +- translations/oc.json | 3 +- translations/pt.json | 3 +- translations/ru.json | 3 +- translations/zh.json | 3 +- 19 files changed, 107 insertions(+), 19 deletions(-) diff --git a/daemon.py b/daemon.py index a36513705..0c23ee96f 100644 --- a/daemon.py +++ b/daemon.py @@ -11534,7 +11534,9 @@ def runDaemon(newsInstance: bool, print('Creating newswire thread') httpd.thrNewswireDaemon = \ threadWithTrace(target=runNewswireDaemon, - args=(baseDir, httpd, 'newswire'), daemon=True) + args=(baseDir, httpd, + httpPrefix, domain, port, + httpd.translate), daemon=True) # flags used when restarting the inbox queue httpd.restartInboxQueueInProgress = False diff --git a/newsdaemon.py b/newsdaemon.py index 708a7dacb..6df1600fa 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -6,11 +6,76 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" +import os import time from newswire import getDictFromNewswire +from posts import createNewsPost +from utils import saveJson -def runNewswireDaemon(baseDir: str, httpd, unused: str): +def updateFeedsIndex(baseDir: str, filename: str) -> None: + """Updates the index used for imported RSS feeds + """ + indexFilename = baseDir + '/accounts/feeds.index' + + if os.path.isfile(indexFilename): + if filename not in open(indexFilename).read(): + try: + with open(indexFilename, 'r+') as feedsFile: + content = feedsFile.read() + feedsFile.seek(0, 0) + feedsFile.write(filename + '\n' + content) + print('DEBUG: feeds post added to index') + except Exception as e: + print('WARN: Failed to write entry to feeds posts index ' + + indexFilename + ' ' + str(e)) + else: + feedsFile = open(indexFilename, 'w+') + if feedsFile: + feedsFile.write(filename + '\n') + feedsFile.close() + + +def convertRSStoActivityPub(baseDir: str, httpPrefix: str, + domain: str, port: int, + newswire: {}, + translate: {}) -> None: + """Converts rss items in a newswire into posts + """ + basePath = baseDir + '/accounts/feeds' + if not os.path.isdir(basePath): + os.mkdir(basePath) + + nickname = 'feeds' + + for dateStr, item in newswire.items(): + dateStr = dateStr.replace(' ', 'T') + dateStr = dateStr.replace('+00:00', 'Z') + + filename = basePath + '/' + dateStr + '.json' + if os.path.isfile(filename): + continue + + rssTitle = item[0] + url = item[1] + rssDescription = item[4] + if rssDescription: + rssDescription += \ + '\n\n' + translate['Read more...'] + '\n' + url + else: + rssDescription = url + blog = createNewsPost(baseDir, + nickname, domain, port, + httpPrefix, dateStr, + rssTitle, rssDescription, + None, None, None, False) + if saveJson(blog, filename): + updateFeedsIndex(baseDir, filename) + + +def runNewswireDaemon(baseDir: str, httpd, + httpPrefix: str, domain: str, port: int, + translate: {}) -> None: """Periodically updates RSS feeds """ # initial sleep to allow the system to start up @@ -33,6 +98,12 @@ def runNewswireDaemon(baseDir: str, httpd, unused: str): httpd.newswire = newNewswire print('Newswire updated') + + convertRSStoActivityPub(baseDir, + httpPrefix, domain, port, + newNewswire, translate) + print('Newswire feed converted to ActivityPub') + # wait a while before the next feeds update time.sleep(1200) diff --git a/newswire.py b/newswire.py index 1b86ef6ec..0d3ddceea 100644 --- a/newswire.py +++ b/newswire.py @@ -7,7 +7,6 @@ __email__ = "bob@freedombone.net" __status__ = "Production" import os -import time import requests from socket import error as SocketError import errno diff --git a/posts.py b/posts.py index 1e265ff6f..991fd3c84 100644 --- a/posts.py +++ b/posts.py @@ -1195,7 +1195,7 @@ def createBlogPost(baseDir: str, def createNewsPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, - rssTitle: str, rssDescription: str, + published: str, rssTitle: str, rssDescription: str, attachImageFilename: str, mediaType: str, imageDescription: str, useBlurhash: bool) -> {}: """Converts title and description from an rss feed into a post @@ -1225,6 +1225,7 @@ def createNewsPost(baseDir: str, schedulePost, eventDate, eventTime, location) blog['object']['type'] = 'Article' + blog['object']['published'] = published return blog diff --git a/translations/ar.json b/translations/ar.json index f24ac3992..74ce376c6 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -304,5 +304,6 @@ "Vote": "تصويت", "Remove Vote": "إزالة التصويت", "This is a news instance": "هذا مثال أخبار", - "News": "أخبار" + "News": "أخبار", + "Read more...": "" } diff --git a/translations/ca.json b/translations/ca.json index d59073906..40f7b8154 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -304,5 +304,6 @@ "Vote": "Notícies", "Remove Vote": "Elimina el vot", "This is a news instance": "Aquesta és una instància de notícies", - "News": "Notícies" + "News": "Notícies", + "Read more...": "" } diff --git a/translations/cy.json b/translations/cy.json index 6129a816e..053d523be 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -304,5 +304,6 @@ "Vote": "Newyddion", "Remove Vote": "Tynnwch y Bleidlais", "This is a news instance": "Dyma enghraifft newyddion", - "News": "Newyddion" + "News": "Newyddion", + "Read more...": "" } diff --git a/translations/de.json b/translations/de.json index d0710d5a9..21c688451 100644 --- a/translations/de.json +++ b/translations/de.json @@ -304,5 +304,6 @@ "Vote": "Abstimmung", "Remove Vote": "Abstimmung entfernen", "This is a news instance": "Dies ist eine Nachrichteninstanz", - "News": "Nachrichten" + "News": "Nachrichten", + "Read more...": "" } diff --git a/translations/en.json b/translations/en.json index 420dfec52..36add8b2f 100644 --- a/translations/en.json +++ b/translations/en.json @@ -304,5 +304,6 @@ "Vote": "Vote", "Remove Vote": "Remove Vote", "This is a news instance": "This is a news instance", - "News": "News" + "News": "News", + "Read more...": "Read more..." } diff --git a/translations/es.json b/translations/es.json index a296bf69d..010138ebf 100644 --- a/translations/es.json +++ b/translations/es.json @@ -304,5 +304,6 @@ "Vote": "Votar", "Remove Vote": "Eliminar voto", "This is a news instance": "Esta es una instancia de noticias", - "News": "Noticias" + "News": "Noticias", + "Read more...": "" } diff --git a/translations/fr.json b/translations/fr.json index ec2955833..a9e3c739b 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -304,5 +304,6 @@ "Vote": "Voter", "Remove Vote": "Supprimer le vote", "This is a news instance": "Ceci est une instance d'actualité", - "News": "Nouvelles" + "News": "Nouvelles", + "Read more...": "" } diff --git a/translations/ga.json b/translations/ga.json index 49b67041d..429efb4d8 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -304,5 +304,6 @@ "Vote": "Vóta", "Remove Vote": "Bain Vóta", "This is a news instance": "Is sampla nuachta é seo", - "News": "Nuacht" + "News": "Nuacht", + "Read more...": "" } diff --git a/translations/hi.json b/translations/hi.json index b8e555726..428b8b1ec 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -304,5 +304,6 @@ "Vote": "वोट", "Remove Vote": "वोट हटा दें", "This is a news instance": "यह एक समाचार का उदाहरण है", - "News": "समाचार" + "News": "समाचार", + "Read more...": "" } diff --git a/translations/it.json b/translations/it.json index a8e22bd98..32788a6b8 100644 --- a/translations/it.json +++ b/translations/it.json @@ -304,5 +304,6 @@ "Vote": "Votazione", "Remove Vote": "Rimuovi voto", "This is a news instance": "Questa è un'istanza di notizie", - "News": "Notizia" + "News": "Notizia", + "Read more...": "" } diff --git a/translations/ja.json b/translations/ja.json index 299964631..b57fe63d9 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -304,5 +304,6 @@ "Vote": "投票", "Remove Vote": "投票を削除", "This is a news instance": "これはニュースインスタンスです", - "News": "ニュース" + "News": "ニュース", + "Read more...": "" } diff --git a/translations/oc.json b/translations/oc.json index 99eae9ad6..23780037b 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -300,5 +300,6 @@ "Vote": "Vote", "Remove Vote": "Remove Vote", "This is a news instance": "This is a news instance", - "News": "News" + "News": "News", + "Read more...": "Read more..." } diff --git a/translations/pt.json b/translations/pt.json index 015fa1859..67a076516 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -304,5 +304,6 @@ "Vote": "Voto", "Remove Vote": "Remover voto", "This is a news instance": "Esta é uma instância de notícias", - "News": "Notícia" + "News": "Notícia", + "Read more...": "" } diff --git a/translations/ru.json b/translations/ru.json index ca7623b34..726df9bea 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -304,5 +304,6 @@ "Vote": "Голос", "Remove Vote": "Удалить голос", "This is a news instance": "Это новостной экземпляр", - "News": "Новости" + "News": "Новости", + "Read more...": "" } diff --git a/translations/zh.json b/translations/zh.json index fb24bc136..dafc27164 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -304,5 +304,6 @@ "Vote": "投票", "Remove Vote": "删除投票", "This is a news instance": "这是一个新闻实例", - "News": "新闻" + "News": "新闻", + "Read more...": "" } From c0beeb3e917622e4bb2f877e9cdef7f4510dfa2a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 14:55:27 +0100 Subject: [PATCH 052/263] Check list length --- newsdaemon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/newsdaemon.py b/newsdaemon.py index 6df1600fa..cdae44f2e 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -58,7 +58,9 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, rssTitle = item[0] url = item[1] - rssDescription = item[4] + rssDescription = '' + if len(item) >= 4: + rssDescription = item[4] if rssDescription: rssDescription += \ '\n\n' + translate['Read more...'] + '\n' + url From b353caceb85a50738b37b2b61b08a75a42b96eab Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 14:58:39 +0100 Subject: [PATCH 053/263] Check list length --- newsdaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsdaemon.py b/newsdaemon.py index cdae44f2e..c8e450430 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -59,7 +59,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, rssTitle = item[0] url = item[1] rssDescription = '' - if len(item) >= 4: + if len(item) >= 5: rssDescription = item[4] if rssDescription: rssDescription += \ From f9517367bb22c60aa3ad13713bbf2b4a66e31298 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 15:10:06 +0100 Subject: [PATCH 054/263] Change newswire link to local if post exists locally --- newsdaemon.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/newsdaemon.py b/newsdaemon.py index c8e450430..ed8f3eb50 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -49,28 +49,44 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, nickname = 'feeds' for dateStr, item in newswire.items(): + # convert the date to the format used by ActivityPub dateStr = dateStr.replace(' ', 'T') dateStr = dateStr.replace('+00:00', 'Z') + # file where the post is stored filename = basePath + '/' + dateStr + '.json' if os.path.isfile(filename): + # if a local post exists as html then change the link + # to the local one + htmlFilename = basePath + '/' + dateStr + '.html' + if os.path.isfile(htmlFilename): + item[1] = '/feeds/' + dateStr + '.html' + # don't create the post if it already exists continue rssTitle = item[0] url = item[1] rssDescription = '' + + # get the rss description if it exists if len(item) >= 5: rssDescription = item[4] + + # add the off-site link to the description if rssDescription: rssDescription += \ '\n\n' + translate['Read more...'] + '\n' + url else: rssDescription = url + + # create the activitypub post blog = createNewsPost(baseDir, nickname, domain, port, httpPrefix, dateStr, rssTitle, rssDescription, None, None, None, False) + + # save the post and update the index if saveJson(blog, filename): updateFeedsIndex(baseDir, filename) From a7e22c7590999d38eca6310310fc57e2c93b79b9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 17:01:45 +0100 Subject: [PATCH 055/263] Add a user to handle news items --- daemon.py | 10 ++++++++++ person.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/daemon.py b/daemon.py index 0c23ee96f..cde615e7f 100644 --- a/daemon.py +++ b/daemon.py @@ -54,6 +54,7 @@ from person import registerAccount from person import personLookup from person import personBoxJson from person import createSharedInbox +from person import createNewsInbox from person import suspendAccount from person import unsuspendAccount from person import removeAccount @@ -1229,6 +1230,11 @@ class PubServer(BaseHTTPRequestHandler): loginNickname, loginPassword, register = \ htmlGetLoginCredentials(loginParams, self.server.lastLoginTime) if loginNickname: + if loginNickname == 'news' or loginNickname == 'inbox': + print('Invalid username login: ' + loginNickname) + self._clearLoginDetails(loginNickname, callingDomain) + self.server.POSTbusy = False + return self.server.lastLoginTime = int(time.time()) if register: if not registerAccount(baseDir, httpPrefix, domain, port, @@ -11455,6 +11461,10 @@ def runDaemon(newsInstance: bool, print('Creating shared inbox: inbox@' + domain) createSharedInbox(baseDir, 'inbox', domain, port, httpPrefix) + if not os.path.isdir(baseDir + '/accounts/news@' + domain): + print('Creating news inbox: news@' + domain) + createNewsInbox(baseDir, domain, port, httpPrefix) + if not os.path.isdir(baseDir + '/cache'): os.mkdir(baseDir + '/cache') if not os.path.isdir(baseDir + '/cache/actors'): diff --git a/person.py b/person.py index 6eb196708..bea86e265 100644 --- a/person.py +++ b/person.py @@ -504,6 +504,14 @@ def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int, True, True, None) +def createNewsInbox(baseDir: str, domain: str, port: int, + httpPrefix: str) -> (str, str, {}, {}): + """Generates the news inbox + """ + return createPersonBase(baseDir, 'news', domain, port, httpPrefix, + True, True, None) + + def personUpgradeActor(baseDir: str, personJson: {}, handle: str, filename: str) -> None: """Alter the actor to add any new properties From 3928340dc9a909e5c26099bb92792339c23c49c4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 17:55:15 +0100 Subject: [PATCH 056/263] Move rss posts to news account --- newsdaemon.py | 64 ++++++++++++++++++++++++++++++++++++--------------- posts.py | 36 ----------------------------- utils.py | 8 +++++-- 3 files changed, 51 insertions(+), 57 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index ed8f3eb50..74e3e1a49 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -9,22 +9,24 @@ __status__ = "Production" import os import time from newswire import getDictFromNewswire -from posts import createNewsPost +from posts import createBlogPost from utils import saveJson +from utils import getStatusNumber -def updateFeedsIndex(baseDir: str, filename: str) -> None: +def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: """Updates the index used for imported RSS feeds """ - indexFilename = baseDir + '/accounts/feeds.index' + basePath = baseDir + '/accounts/news@' + domain + indexFilename = basePath + '/outbox.index' if os.path.isfile(indexFilename): - if filename not in open(indexFilename).read(): + if postId not in open(indexFilename).read(): try: with open(indexFilename, 'r+') as feedsFile: content = feedsFile.read() feedsFile.seek(0, 0) - feedsFile.write(filename + '\n' + content) + feedsFile.write(postId + '\n' + content) print('DEBUG: feeds post added to index') except Exception as e: print('WARN: Failed to write entry to feeds posts index ' + @@ -32,7 +34,7 @@ def updateFeedsIndex(baseDir: str, filename: str) -> None: else: feedsFile = open(indexFilename, 'w+') if feedsFile: - feedsFile.write(filename + '\n') + feedsFile.write(postId + '\n') feedsFile.close() @@ -42,25 +44,28 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, translate: {}) -> None: """Converts rss items in a newswire into posts """ - basePath = baseDir + '/accounts/feeds' + basePath = baseDir + '/accounts/news@' + domain + '/outbox' if not os.path.isdir(basePath): os.mkdir(basePath) - nickname = 'feeds' - for dateStr, item in newswire.items(): # convert the date to the format used by ActivityPub dateStr = dateStr.replace(' ', 'T') dateStr = dateStr.replace('+00:00', 'Z') + statusNumber, published = getStatusNumber(dateStr) + newPostId = \ + httpPrefix + '://' + domain + \ + '/users/news/statuses/' + statusNumber + # file where the post is stored - filename = basePath + '/' + dateStr + '.json' + filename = basePath + '/' + newPostId.replace('/', '#') + '.json' if os.path.isfile(filename): # if a local post exists as html then change the link # to the local one - htmlFilename = basePath + '/' + dateStr + '.html' + htmlFilename = filename.replace('.json', '.html') if os.path.isfile(htmlFilename): - item[1] = '/feeds/' + dateStr + '.html' + item[1] = '/users/news/statuses/' + statusNumber + '.html' # don't create the post if it already exists continue @@ -79,16 +84,37 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, else: rssDescription = url - # create the activitypub post - blog = createNewsPost(baseDir, - nickname, domain, port, - httpPrefix, dateStr, - rssTitle, rssDescription, - None, None, None, False) + followersOnly = False + useBlurhash = False + blog = createBlogPost(baseDir, + 'news', domain, port, httpPrefix, + rssDescription, followersOnly, False, + False, + None, None, None, useBlurhash, + None, None, rssTitle, + False, + None, None, None) + if not blog: + continue + + idStr = \ + httpPrefix + '://' + domain + '/users/news' + \ + '/statuses/' + statusNumber + '/replies' + blog['object']['replies']['id'] = idStr + blog['object']['replies']['first']['partOf'] = idStr + + blog['id'] = newPostId + '/activity' + blog['object']['id'] = newPostId + blog['object']['atomUri'] = newPostId + blog['object']['url'] = \ + httpPrefix + '://' + domain + '/@news/' + statusNumber + blog['object']['published'] = dateStr + + postId = newPostId.replace('/', '#') # save the post and update the index if saveJson(blog, filename): - updateFeedsIndex(baseDir, filename) + updateFeedsIndex(baseDir, domain, postId + '.json') def runNewswireDaemon(baseDir: str, httpd, diff --git a/posts.py b/posts.py index 991fd3c84..739f565ce 100644 --- a/posts.py +++ b/posts.py @@ -1193,42 +1193,6 @@ def createBlogPost(baseDir: str, return blog -def createNewsPost(baseDir: str, - nickname: str, domain: str, port: int, httpPrefix: str, - published: str, rssTitle: str, rssDescription: str, - attachImageFilename: str, mediaType: str, - imageDescription: str, useBlurhash: bool) -> {}: - """Converts title and description from an rss feed into a post - """ - inReplyTo = None - inReplyToAtomUri = None - schedulePost = False - eventDate = None - eventTime = None - location = None - schedulePost = False - eventDate = None - eventTime = None - location = None - clientToServer = False - saveToFile = False - followersOnly = False - - blog = \ - createPublicPost(baseDir, - nickname, domain, port, httpPrefix, - rssDescription, followersOnly, saveToFile, - clientToServer, - attachImageFilename, mediaType, - imageDescription, useBlurhash, - inReplyTo, inReplyToAtomUri, rssTitle, - schedulePost, - eventDate, eventTime, location) - blog['object']['type'] = 'Article' - blog['object']['published'] = published - return blog - - def createQuestionPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, content: str, qOptions: [], diff --git a/utils.py b/utils.py index 323292acc..0d3977e7e 100644 --- a/utils.py +++ b/utils.py @@ -220,10 +220,14 @@ def loadJsonOnionify(filename: str, domain: str, onionDomain: str, return jsonObject -def getStatusNumber() -> (str, str): +def getStatusNumber(publishedStr=None) -> (str, str): """Returns the status number and published date """ - currTime = datetime.datetime.utcnow() + if not publishedStr: + currTime = datetime.datetime.utcnow() + else: + currTime = \ + datetime.datetime.strptime(publishedStr, '%Y-%m-%dT%H:%M:%SZ') daysSinceEpoch = (currTime - datetime.datetime(1970, 1, 1)).days # status is the number of seconds since epoch statusNumber = \ From 92c210175c1b3fc2dbf7601aa7363ac47a578d8e Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 18:36:38 +0100 Subject: [PATCH 057/263] Get news timeline from news account outbox --- daemon.py | 84 +++++++++++++++++++++++++++++++++++-------------- webinterface.py | 19 ----------- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/daemon.py b/daemon.py index cde615e7f..075e9d44a 100644 --- a/daemon.py +++ b/daemon.py @@ -120,7 +120,6 @@ from webinterface import htmlInboxDMs from webinterface import htmlInboxReplies from webinterface import htmlInboxMedia from webinterface import htmlInboxBlogs -from webinterface import htmlInboxNews from webinterface import htmlUnblockConfirm from webinterface import htmlPersonOptions from webinterface import htmlIndividualPost @@ -220,6 +219,8 @@ maxPostsInMediaFeed = 6 # Blogs can be longer, so don't show many per page maxPostsInBlogsFeed = 4 +maxPostsInNewsFeed = 10 + # Maximum number of entries in returned rss.xml maxPostsInRSSFeed = 10 @@ -6498,9 +6499,24 @@ class PubServer(BaseHTTPRequestHandler): """ if '/users/' in path: if authorized: + currNickname = path.split('/users/')[1] + if '/' in currNickname: + currNickname = currNickname.split('/')[0] + newsPath = path.replace('/' + currNickname + '/', '/news/') + inboxNewsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + newsPath, + httpPrefix, + maxPostsInNewsFeed, 'outbox', + True) + if not inboxNewsFeed: + inboxNewsFeed = [] if self._requestHTTP(): - nickname = path.replace('/users/', '') - nickname = nickname.replace('/tlnews', '') + nickname = 'news' pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] @@ -6509,25 +6525,38 @@ class PubServer(BaseHTTPRequestHandler): pageNumber = int(pageNumber) else: pageNumber = 1 + if 'page=' not in path: + # if no page was specified then show the first + inboxNewsFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, + domain, + port, + newsPath + '?page=1', + httpPrefix, + maxPostsInBlogsFeed, 'outbox', + True) msg = \ - htmlInboxNews(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInBlogsFeed, - self.server.session, - baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - domain, - port, - self.server.allowDeletion, - httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain, - self.server.newswire) + htmlInboxBlogs(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInNewsFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxNewsFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain, + self.server.newswire) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6535,12 +6564,21 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', 'show news 2') + else: + # don't need authenticated fetch here because there is + # already the authorization check + msg = json.dumps(inboxNewsFeed, + ensure_ascii=False) + msg = msg.encode('utf-8') + self._set_headers('application/json', + len(msg), + None, callingDomain) + self._write(msg) self.server.GETbusy = False return True else: if debug: - nickname = path.replace('/users/', '') - nickname = nickname.replace('/tlnews', '') + nickname = 'news' print('DEBUG: ' + nickname + ' was not authorized to access ' + path) if path != '/tlnews': diff --git a/webinterface.py b/webinterface.py index f652fe128..c0d0ed9cf 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6340,25 +6340,6 @@ def htmlInboxBlogs(defaultTimeline: str, minimal, YTReplacementDomain, newswire) -def htmlInboxNews(defaultTimeline: str, - recentPostsCache: {}, maxRecentPosts: int, - translate: {}, pageNumber: int, itemsPerPage: int, - session, baseDir: str, wfRequest: {}, personCache: {}, - nickname: str, domain: str, port: int, - allowDeletion: bool, - httpPrefix: str, projectVersion: str, - minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: - """Show the news timeline as html - """ - return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, - translate, pageNumber, - itemsPerPage, session, baseDir, wfRequest, personCache, - nickname, domain, port, {}, 'tlnews', - allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire) - - def htmlModeration(defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, From ae8c7c6e309e43b236e7ad648be698facfaaf7d3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 19:05:08 +0100 Subject: [PATCH 058/263] Create news timeline --- daemon.py | 12 ++++-------- posts.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/daemon.py b/daemon.py index 075e9d44a..a2eea4fd1 100644 --- a/daemon.py +++ b/daemon.py @@ -6499,19 +6499,15 @@ class PubServer(BaseHTTPRequestHandler): """ if '/users/' in path: if authorized: - currNickname = path.split('/users/')[1] - if '/' in currNickname: - currNickname = currNickname.split('/')[0] - newsPath = path.replace('/' + currNickname + '/', '/news/') inboxNewsFeed = \ personBoxJson(self.server.recentPostsCache, self.server.session, baseDir, domain, port, - newsPath, + path, httpPrefix, - maxPostsInNewsFeed, 'outbox', + maxPostsInNewsFeed, 'tlnews', True) if not inboxNewsFeed: inboxNewsFeed = [] @@ -6533,9 +6529,9 @@ class PubServer(BaseHTTPRequestHandler): baseDir, domain, port, - newsPath + '?page=1', + path + '?page=1', httpPrefix, - maxPostsInBlogsFeed, 'outbox', + maxPostsInBlogsFeed, 'tlnews', True) msg = \ htmlInboxBlogs(self.server.defaultTimeline, diff --git a/posts.py b/posts.py index 739f565ce..a5331e83f 100644 --- a/posts.py +++ b/posts.py @@ -2509,7 +2509,7 @@ def createMediaTimeline(session, baseDir: str, nickname: str, domain: str, def createNewsTimeline(session, baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, itemsPerPage: int, headerOnly: bool, pageNumber=None) -> {}: - return createBoxIndexed({}, session, baseDir, 'tlnews', nickname, + return createBoxIndexed({}, session, baseDir, 'outbox', 'news', domain, port, httpPrefix, itemsPerPage, headerOnly, True, pageNumber) From cbab604accebce41070c05aa61d303337773d803 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 19:18:26 +0100 Subject: [PATCH 059/263] Append --- newsdaemon.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 74e3e1a49..39b06b179 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -21,16 +21,10 @@ def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: indexFilename = basePath + '/outbox.index' if os.path.isfile(indexFilename): - if postId not in open(indexFilename).read(): - try: - with open(indexFilename, 'r+') as feedsFile: - content = feedsFile.read() - feedsFile.seek(0, 0) - feedsFile.write(postId + '\n' + content) - print('DEBUG: feeds post added to index') - except Exception as e: - print('WARN: Failed to write entry to feeds posts index ' + - indexFilename + ' ' + str(e)) + feedsFile = open(indexFilename, 'a+') + if feedsFile: + feedsFile.write(postId + '\n') + feedsFile.close() else: feedsFile = open(indexFilename, 'w+') if feedsFile: From 404912cd31a787a40b5f719d22dee4f247cd394a Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 19:46:42 +0100 Subject: [PATCH 060/263] Convert RSS to AP in reverse order --- newsdaemon.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 39b06b179..71a21643c 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -8,6 +8,7 @@ __status__ = "Production" import os import time +from collections import OrderedDict from newswire import getDictFromNewswire from posts import createBlogPost from utils import saveJson @@ -21,10 +22,16 @@ def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: indexFilename = basePath + '/outbox.index' if os.path.isfile(indexFilename): - feedsFile = open(indexFilename, 'a+') - if feedsFile: - feedsFile.write(postId + '\n') - feedsFile.close() + if postId not in open(indexFilename).read(): + try: + with open(indexFilename, 'r+') as feedsFile: + content = feedsFile.read() + feedsFile.seek(0, 0) + feedsFile.write(postId + '\n' + content) + print('DEBUG: feeds post added to index') + except Exception as e: + print('WARN: Failed to write entry to feeds posts index ' + + indexFilename + ' ' + str(e)) else: feedsFile = open(indexFilename, 'w+') if feedsFile: @@ -42,7 +49,10 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if not os.path.isdir(basePath): os.mkdir(basePath) - for dateStr, item in newswire.items(): + newswireReverse = \ + OrderedDict(sorted(newswire.items(), reverse=True)) + + for dateStr, item in newswireReverse.items(): # convert the date to the format used by ActivityPub dateStr = dateStr.replace(' ', 'T') dateStr = dateStr.replace('+00:00', 'Z') From 7c9b9d426307a1e747bfe9ab44c6e55bd0c7a328 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 19:51:08 +0100 Subject: [PATCH 061/263] Convert from news to nickname --- daemon.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/daemon.py b/daemon.py index a2eea4fd1..fbca32417 100644 --- a/daemon.py +++ b/daemon.py @@ -6533,6 +6533,9 @@ class PubServer(BaseHTTPRequestHandler): httpPrefix, maxPostsInBlogsFeed, 'tlnews', True) + currNickname = path.split('/users/')[1] + if '/' in currNickname: + currNickname = currNickname.split('/')[0] msg = \ htmlInboxBlogs(self.server.defaultTimeline, self.server.recentPostsCache, @@ -6553,6 +6556,7 @@ class PubServer(BaseHTTPRequestHandler): self._isMinimal(nickname), self.server.YTReplacementDomain, self.server.newswire) + msg = msg.replace('/news/', '/' + currNickname + '/') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) From 5d53593c83c7ca8c967955a312c0e0bc72097fee Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 19:55:59 +0100 Subject: [PATCH 062/263] News timeline --- daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon.py b/daemon.py index fbca32417..b23ed9170 100644 --- a/daemon.py +++ b/daemon.py @@ -6557,6 +6557,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.newswire) msg = msg.replace('/news/', '/' + currNickname + '/') + msg = msg.replace('tlblogs', 'tlnews') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) From b42d7c14e4c15e53eaf7384741cba494f241592b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 20:04:15 +0100 Subject: [PATCH 063/263] News inbox --- daemon.py | 40 ++++++++++++++++++++-------------------- webinterface.py | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/daemon.py b/daemon.py index b23ed9170..e49a947a5 100644 --- a/daemon.py +++ b/daemon.py @@ -120,6 +120,7 @@ from webinterface import htmlInboxDMs from webinterface import htmlInboxReplies from webinterface import htmlInboxMedia from webinterface import htmlInboxBlogs +from webinterface import htmlInboxNews from webinterface import htmlUnblockConfirm from webinterface import htmlPersonOptions from webinterface import htmlIndividualPost @@ -6537,27 +6538,26 @@ class PubServer(BaseHTTPRequestHandler): if '/' in currNickname: currNickname = currNickname.split('/')[0] msg = \ - htmlInboxBlogs(self.server.defaultTimeline, - self.server.recentPostsCache, - self.server.maxRecentPosts, - self.server.translate, - pageNumber, maxPostsInNewsFeed, - self.server.session, - baseDir, - self.server.cachedWebfingers, - self.server.personCache, - nickname, - domain, - port, - inboxNewsFeed, - self.server.allowDeletion, - httpPrefix, - self.server.projectVersion, - self._isMinimal(nickname), - self.server.YTReplacementDomain, - self.server.newswire) + htmlInboxNews(self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInNewsFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + inboxNewsFeed, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self._isMinimal(nickname), + self.server.YTReplacementDomain, + self.server.newswire) msg = msg.replace('/news/', '/' + currNickname + '/') - msg = msg.replace('tlblogs', 'tlnews') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) diff --git a/webinterface.py b/webinterface.py index c0d0ed9cf..2f13e586c 100644 --- a/webinterface.py +++ b/webinterface.py @@ -6340,6 +6340,25 @@ def htmlInboxBlogs(defaultTimeline: str, minimal, YTReplacementDomain, newswire) +def htmlInboxNews(defaultTimeline: str, + recentPostsCache: {}, maxRecentPosts: int, + translate: {}, pageNumber: int, itemsPerPage: int, + session, baseDir: str, wfRequest: {}, personCache: {}, + nickname: str, domain: str, port: int, inboxJson: {}, + allowDeletion: bool, + httpPrefix: str, projectVersion: str, + minimal: bool, YTReplacementDomain: str, + newswire: {}) -> str: + """Show the news timeline as html + """ + return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, + translate, pageNumber, + itemsPerPage, session, baseDir, wfRequest, personCache, + nickname, domain, port, inboxJson, 'tlnews', + allowDeletion, httpPrefix, projectVersion, False, + minimal, YTReplacementDomain, newswire) + + def htmlModeration(defaultTimeline: str, recentPostsCache: {}, maxRecentPosts: int, translate: {}, pageNumber: int, itemsPerPage: int, From f2fbccd0144b8b35d560169b5efea3f31fe46b7b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 20:41:55 +0100 Subject: [PATCH 064/263] Sorting order --- newsdaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsdaemon.py b/newsdaemon.py index 71a21643c..b2f4f5abd 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -50,7 +50,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, os.mkdir(basePath) newswireReverse = \ - OrderedDict(sorted(newswire.items(), reverse=True)) + OrderedDict(sorted(newswire.items(), reverse=False)) for dateStr, item in newswireReverse.items(): # convert the date to the format used by ActivityPub From 6c71349c0bc1a8b467befaa5fb8150817102eb9c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 20:49:57 +0100 Subject: [PATCH 065/263] Getting page number --- daemon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index e49a947a5..44ba890b8 100644 --- a/daemon.py +++ b/daemon.py @@ -6513,7 +6513,8 @@ class PubServer(BaseHTTPRequestHandler): if not inboxNewsFeed: inboxNewsFeed = [] if self._requestHTTP(): - nickname = 'news' + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlnews', '') pageNumber = 1 if '?page=' in nickname: pageNumber = nickname.split('?page=')[1] @@ -6522,6 +6523,7 @@ class PubServer(BaseHTTPRequestHandler): pageNumber = int(pageNumber) else: pageNumber = 1 + nickname = 'news' if 'page=' not in path: # if no page was specified then show the first inboxNewsFeed = \ From 8982a470acca4c5fba1403298cc54e4a34efada3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 21:03:39 +0100 Subject: [PATCH 066/263] Cached html location --- newsdaemon.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index b2f4f5abd..382a130bd 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -53,6 +53,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, OrderedDict(sorted(newswire.items(), reverse=False)) for dateStr, item in newswireReverse.items(): + originalDateStr = dateStr # convert the date to the format used by ActivityPub dateStr = dateStr.replace(' ', 'T') dateStr = dateStr.replace('+00:00', 'Z') @@ -67,9 +68,12 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if os.path.isfile(filename): # if a local post exists as html then change the link # to the local one - htmlFilename = filename.replace('.json', '.html') + htmlFilename = \ + baseDir + '/accounts/news@' + domain + \ + '/postcache/' + newPostId.replace('/', '#') + '.html' if os.path.isfile(htmlFilename): - item[1] = '/users/news/statuses/' + statusNumber + '.html' + newswire[originalDateStr][1] = \ + '/users/news/statuses/' + statusNumber + '.html' # don't create the post if it already exists continue From 251a7efcd695ad2bd3a0c9cbfa54efbeeb831c0c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 21:31:18 +0100 Subject: [PATCH 067/263] Banner file format --- daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon.py b/daemon.py index 44ba890b8..66baf6a5a 100644 --- a/daemon.py +++ b/daemon.py @@ -6560,6 +6560,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.newswire) msg = msg.replace('/news/', '/' + currNickname + '/') + msg = msg.replace('/banner.webp', '/banner.png') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) From da8e2733be5ec94d1f2b4ad717b1b7f08c4b4fd7 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 21:46:37 +0100 Subject: [PATCH 068/263] Override moderator flag for news timeline --- daemon.py | 3 ++- webinterface.py | 29 +++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/daemon.py b/daemon.py index 66baf6a5a..7a49fef99 100644 --- a/daemon.py +++ b/daemon.py @@ -6539,6 +6539,7 @@ class PubServer(BaseHTTPRequestHandler): currNickname = path.split('/users/')[1] if '/' in currNickname: currNickname = currNickname.split('/')[0] + moderator = isModerator(baseDir, currNickname) msg = \ htmlInboxNews(self.server.defaultTimeline, self.server.recentPostsCache, @@ -6558,7 +6559,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, moderator) msg = msg.replace('/news/', '/' + currNickname + '/') msg = msg.replace('/banner.webp', '/banner.png') msg = msg.encode('utf-8') diff --git a/webinterface.py b/webinterface.py index 2f13e586c..1590aab5b 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5504,7 +5504,7 @@ def htmlTimeline(defaultTimeline: str, manuallyApproveFollowers: bool, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, moderator: bool) -> str: """Show the timeline as html """ timelineStartTime = time.time() @@ -5600,7 +5600,8 @@ def htmlTimeline(defaultTimeline: str, httpPrefix + '://') # is the user a moderator? - moderator = isModerator(baseDir, nickname) + if not moderator: + moderator = isModerator(baseDir, nickname) # benchmark 2 timeDiff = int((time.time() - timelineStartTime) * 1000) @@ -6192,7 +6193,7 @@ def htmlShares(defaultTimeline: str, nickname, domain, port, None, 'tlshares', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - False, YTReplacementDomain, newswire) + False, YTReplacementDomain, newswire, False) def htmlInbox(defaultTimeline: str, @@ -6215,7 +6216,7 @@ def htmlInbox(defaultTimeline: str, nickname, domain, port, inboxJson, 'inbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlBookmarks(defaultTimeline: str, @@ -6238,7 +6239,7 @@ def htmlBookmarks(defaultTimeline: str, nickname, domain, port, bookmarksJson, 'tlbookmarks', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlEvents(defaultTimeline: str, @@ -6261,7 +6262,7 @@ def htmlEvents(defaultTimeline: str, nickname, domain, port, bookmarksJson, 'tlevents', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlInboxDMs(defaultTimeline: str, @@ -6280,7 +6281,7 @@ def htmlInboxDMs(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'dm', allowDeletion, httpPrefix, projectVersion, False, minimal, - YTReplacementDomain, newswire) + YTReplacementDomain, newswire, False) def htmlInboxReplies(defaultTimeline: str, @@ -6299,7 +6300,7 @@ def htmlInboxReplies(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlreplies', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlInboxMedia(defaultTimeline: str, @@ -6318,7 +6319,7 @@ def htmlInboxMedia(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlmedia', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlInboxBlogs(defaultTimeline: str, @@ -6337,7 +6338,7 @@ def htmlInboxBlogs(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlblogs', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, False) def htmlInboxNews(defaultTimeline: str, @@ -6348,7 +6349,7 @@ def htmlInboxNews(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, moderator: bool) -> str: """Show the news timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6356,7 +6357,7 @@ def htmlInboxNews(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlnews', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire) + minimal, YTReplacementDomain, newswire, moderator) def htmlModeration(defaultTimeline: str, @@ -6375,7 +6376,7 @@ def htmlModeration(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'moderation', allowDeletion, httpPrefix, projectVersion, True, False, - YTReplacementDomain, newswire) + YTReplacementDomain, newswire, False) def htmlOutbox(defaultTimeline: str, @@ -6397,7 +6398,7 @@ def htmlOutbox(defaultTimeline: str, nickname, domain, port, outboxJson, 'outbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, - YTReplacementDomain, newswire) + YTReplacementDomain, newswire, False) def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int, From dd0721a0ad2a566dd07cb27979342379f36fd42d Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 21:50:18 +0100 Subject: [PATCH 069/263] news --- daemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 7a49fef99..8647b8a18 100644 --- a/daemon.py +++ b/daemon.py @@ -3468,7 +3468,7 @@ class PubServer(BaseHTTPRequestHandler): if fields.get('newsInstance'): self.server.newsInstance = False self.server.defaultTimeline = 'inbox' - if fields['mediaInstance'] == 'on': + if fields['newsInstance'] == 'on': self.server.newsInstance = True self.server.blogsInstance = False self.server.mediaInstance = False From 007dc311bedb7c56cb5c2ef51aa57b4b8858cf95 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 21:56:58 +0100 Subject: [PATCH 070/263] Extra replacement --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index 8647b8a18..f974b03eb 100644 --- a/daemon.py +++ b/daemon.py @@ -6561,6 +6561,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.newswire, moderator) msg = msg.replace('/news/', '/' + currNickname + '/') + msg = msg.replace('/users/news"', + '/users/' + currNickname + '"') msg = msg.replace('/banner.webp', '/banner.png') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), From d1e9f113e3d1a1e78c5e6659ec499310e2a0dd23 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:10:34 +0100 Subject: [PATCH 071/263] Create public post rather than blog post --- newsdaemon.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 382a130bd..4fb34c856 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -94,14 +94,15 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, followersOnly = False useBlurhash = False - blog = createBlogPost(baseDir, - 'news', domain, port, httpPrefix, - rssDescription, followersOnly, False, - False, - None, None, None, useBlurhash, - None, None, rssTitle, - False, - None, None, None) + commentsEnabled = False + blog = createPublicPost(baseDir, + 'news', domain, port, httpPrefix, + rssDescription, followersOnly, False, + False, commentsEnabled, + None, None, None, useBlurhash, + None, None, rssTitle, + False, + None, None, None) if not blog: continue From e090a3c047922a63d0f14287c8b7a21531571ced Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:11:01 +0100 Subject: [PATCH 072/263] Create public post rather than blog post --- newsdaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsdaemon.py b/newsdaemon.py index 4fb34c856..8e490ec8e 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -10,7 +10,7 @@ import os import time from collections import OrderedDict from newswire import getDictFromNewswire -from posts import createBlogPost +from posts import createPublicPost from utils import saveJson from utils import getStatusNumber From c440e31bc95b98f1c75be6c27a9b7f1b7bb640f8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:26:03 +0100 Subject: [PATCH 073/263] Simplify --- newsdaemon.py | 16 ++++++---------- posts.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 8e490ec8e..f7edd161f 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -10,7 +10,7 @@ import os import time from collections import OrderedDict from newswire import getDictFromNewswire -from posts import createPublicPost +from posts import createNewsPost from utils import saveJson from utils import getStatusNumber @@ -94,15 +94,11 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, followersOnly = False useBlurhash = False - commentsEnabled = False - blog = createPublicPost(baseDir, - 'news', domain, port, httpPrefix, - rssDescription, followersOnly, False, - False, commentsEnabled, - None, None, None, useBlurhash, - None, None, rssTitle, - False, - None, None, None) + blog = createNewsPost(baseDir, + 'news', domain, port, httpPrefix, + rssDescription, followersOnly, False, + None, None, None, useBlurhash, + rssTitle) if not blog: continue diff --git a/posts.py b/posts.py index a5331e83f..a101c3dcb 100644 --- a/posts.py +++ b/posts.py @@ -1193,6 +1193,33 @@ def createBlogPost(baseDir: str, return blog +def createNewsPost(baseDir: str, + nickname: str, domain: str, port: int, httpPrefix: str, + content: str, followersOnly: bool, saveToFile: bool, + attachImageFilename: str, mediaType: str, + imageDescription: str, useBlurhash: bool, + subject: str) -> {}: + clientToServer = False + inReplyTo = None + inReplyToAtomUri = None + schedulePost = False, + eventDate = None + eventTime = None + location = None + blog = \ + createPublicPost(baseDir, + nickname, domain, port, httpPrefix, + content, followersOnly, saveToFile, + clientToServer, + attachImageFilename, mediaType, + imageDescription, useBlurhash, + inReplyTo, inReplyToAtomUri, subject, + schedulePost, + eventDate, eventTime, location) + blog['object']['type'] = 'Article' + return blog + + def createQuestionPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, content: str, qOptions: [], From 28b127d99ba87ec5284e3c2874b0ff4abdaf60c5 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:33:23 +0100 Subject: [PATCH 074/263] String --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 1590aab5b..7ca718794 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5134,7 +5134,7 @@ def individualPostAsHtml(allowDownloads: bool, contentStr = '' if postJsonObject['object'].get('summary'): contentStr += \ - '' + postJsonObject['object']['summary'] + '\n ' + '' + str(postJsonObject['object']['summary']) + '\n ' if isModerationPost: containerClass = 'container report' # get the content warning text From 15b15c3a076aa8fb7b61bcdc99d25f4d9c211f1b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:40:52 +0100 Subject: [PATCH 075/263] Stray comma --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index a101c3dcb..a16816085 100644 --- a/posts.py +++ b/posts.py @@ -1202,7 +1202,7 @@ def createNewsPost(baseDir: str, clientToServer = False inReplyTo = None inReplyToAtomUri = None - schedulePost = False, + schedulePost = False eventDate = None eventTime = None location = None From dd2c1c99b2f41fbf8bdc5ea631224105f8707289 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 22:49:19 +0100 Subject: [PATCH 076/263] Debug --- newsdaemon.py | 1 + posts.py | 1 + 2 files changed, 2 insertions(+) diff --git a/newsdaemon.py b/newsdaemon.py index f7edd161f..23aa390a1 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -94,6 +94,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, followersOnly = False useBlurhash = False + print('RSS title 1: ' + str(rssTitle)) blog = createNewsPost(baseDir, 'news', domain, port, httpPrefix, rssDescription, followersOnly, False, diff --git a/posts.py b/posts.py index a16816085..166190542 100644 --- a/posts.py +++ b/posts.py @@ -1206,6 +1206,7 @@ def createNewsPost(baseDir: str, eventDate = None eventTime = None location = None + print('RSS title 2: ' + str(subject)) blog = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, From e7897d32261d80aa818e7eb6cb51edd1977fa043 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 23:05:26 +0100 Subject: [PATCH 077/263] Debug --- newsdaemon.py | 1 - posts.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 23aa390a1..f7edd161f 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -94,7 +94,6 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, followersOnly = False useBlurhash = False - print('RSS title 1: ' + str(rssTitle)) blog = createNewsPost(baseDir, 'news', domain, port, httpPrefix, rssDescription, followersOnly, False, diff --git a/posts.py b/posts.py index 166190542..4f7ff8ade 100644 --- a/posts.py +++ b/posts.py @@ -657,7 +657,7 @@ def appendEventFields(newPost: {}, newPost['sensitive'] = False -def validContentWarning(cw: str) -> str: +def validContentWarnings(cw: str) -> str: """Returns a validated content warning """ cw = removeHtml(cw) @@ -722,7 +722,9 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, eventStatus=None, ticketUrl=None) -> {}: """Creates a message """ + print("Subject 1: " + subject) subject = addAutoCW(baseDir, nickname, domain, subject, content) + print("Subject 2: " + subject) mentionedRecipients = \ getMentionedPeople(baseDir, httpPrefix, content, domain, False) @@ -766,6 +768,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, if subject: summary = validContentWarning(subject) sensitive = True + print("Subject 3: " + summary) toRecipients = [] toCC = [] @@ -1206,7 +1209,6 @@ def createNewsPost(baseDir: str, eventDate = None eventTime = None location = None - print('RSS title 2: ' + str(subject)) blog = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, From bdd6e756e95542d2d13e7259bce094cc746da53f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 23:08:15 +0100 Subject: [PATCH 078/263] Typo --- posts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posts.py b/posts.py index 4f7ff8ade..f4ee5b9bb 100644 --- a/posts.py +++ b/posts.py @@ -657,7 +657,7 @@ def appendEventFields(newPost: {}, newPost['sensitive'] = False -def validContentWarnings(cw: str) -> str: +def validContentWarning(cw: str) -> str: """Returns a validated content warning """ cw = removeHtml(cw) From 7469c390172b9f23612d1e6bece9349b2f19e290 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 23:17:40 +0100 Subject: [PATCH 079/263] Strings --- posts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/posts.py b/posts.py index f4ee5b9bb..41bc7e218 100644 --- a/posts.py +++ b/posts.py @@ -722,9 +722,9 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, eventStatus=None, ticketUrl=None) -> {}: """Creates a message """ - print("Subject 1: " + subject) + print("Subject 1: " + str(subject)) subject = addAutoCW(baseDir, nickname, domain, subject, content) - print("Subject 2: " + subject) + print("Subject 2: " + str(subject)) mentionedRecipients = \ getMentionedPeople(baseDir, httpPrefix, content, domain, False) @@ -768,7 +768,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int, if subject: summary = validContentWarning(subject) sensitive = True - print("Subject 3: " + summary) + print("Subject 3: " + str(summary)) toRecipients = [] toCC = [] From 0e370f22292a48a8c8164c33ee2387a49c755a50 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 23:25:30 +0100 Subject: [PATCH 080/263] Missing parameter --- newsdaemon.py | 2 +- posts.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index f7edd161f..bb2e6d415 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -95,7 +95,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, followersOnly = False useBlurhash = False blog = createNewsPost(baseDir, - 'news', domain, port, httpPrefix, + domain, port, httpPrefix, rssDescription, followersOnly, False, None, None, None, useBlurhash, rssTitle) diff --git a/posts.py b/posts.py index 41bc7e218..eaa47ec33 100644 --- a/posts.py +++ b/posts.py @@ -1197,7 +1197,7 @@ def createBlogPost(baseDir: str, def createNewsPost(baseDir: str, - nickname: str, domain: str, port: int, httpPrefix: str, + domain: str, port: int, httpPrefix: str, content: str, followersOnly: bool, saveToFile: bool, attachImageFilename: str, mediaType: str, imageDescription: str, useBlurhash: bool, @@ -1211,14 +1211,14 @@ def createNewsPost(baseDir: str, location = None blog = \ createPublicPost(baseDir, - nickname, domain, port, httpPrefix, - content, followersOnly, saveToFile, - clientToServer, - attachImageFilename, mediaType, - imageDescription, useBlurhash, - inReplyTo, inReplyToAtomUri, subject, - schedulePost, - eventDate, eventTime, location) + 'news', domain, port, httpPrefix, + content, followersOnly, saveToFile, + clientToServer, False, + attachImageFilename, mediaType, + imageDescription, useBlurhash, + inReplyTo, inReplyToAtomUri, subject, + schedulePost, + eventDate, eventTime, location) blog['object']['type'] = 'Article' return blog From 9e802d52eb4df33cb700b61da25987dc7f70fec4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Wed, 7 Oct 2020 23:29:05 +0100 Subject: [PATCH 081/263] Tidying --- posts.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/posts.py b/posts.py index eaa47ec33..e8cb1fc11 100644 --- a/posts.py +++ b/posts.py @@ -1182,11 +1182,12 @@ def createBlogPost(baseDir: str, inReplyTo=None, inReplyToAtomUri=None, subject=None, schedulePost=False, eventDate=None, eventTime=None, location=None) -> {}: + commentsEnabled = True blog = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, content, followersOnly, saveToFile, - clientToServer, + clientToServer, commentsEnabled, attachImageFilename, mediaType, imageDescription, useBlurhash, inReplyTo, inReplyToAtomUri, subject, @@ -1211,14 +1212,14 @@ def createNewsPost(baseDir: str, location = None blog = \ createPublicPost(baseDir, - 'news', domain, port, httpPrefix, - content, followersOnly, saveToFile, - clientToServer, False, - attachImageFilename, mediaType, - imageDescription, useBlurhash, - inReplyTo, inReplyToAtomUri, subject, - schedulePost, - eventDate, eventTime, location) + 'news', domain, port, httpPrefix, + content, followersOnly, saveToFile, + clientToServer, False, + attachImageFilename, mediaType, + imageDescription, useBlurhash, + inReplyTo, inReplyToAtomUri, subject, + schedulePost, + eventDate, eventTime, location) blog['object']['type'] = 'Article' return blog From eca0cbe9827d6ffc896359128e036ab1c9564c96 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 10:07:45 +0100 Subject: [PATCH 082/263] Indicate that imported posts contain news --- newsdaemon.py | 1 + utils.py | 6 ++++++ webinterface.py | 27 +++++++++++++++------------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index bb2e6d415..bc7b02398 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -105,6 +105,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, idStr = \ httpPrefix + '://' + domain + '/users/news' + \ '/statuses/' + statusNumber + '/replies' + blog['news'] = True blog['object']['replies']['id'] = idStr blog['object']['replies']['first']['partOf'] = idStr diff --git a/utils.py b/utils.py index 0d3977e7e..74f58ba52 100644 --- a/utils.py +++ b/utils.py @@ -979,6 +979,12 @@ def isBlogPost(postJsonObject: {}) -> bool: return True +def isNewsPost(postJsonObject: {}) -> bool: + """Is the given post a blog post? + """ + return postJsonObject.get('news') + + def searchBoxPosts(baseDir: str, nickname: str, domain: str, searchStr: str, maxResults: int, boxName='outbox') -> []: diff --git a/webinterface.py b/webinterface.py index 7ca718794..9a8c7ed91 100644 --- a/webinterface.py +++ b/webinterface.py @@ -30,6 +30,7 @@ from utils import getProtocolPrefixes from utils import searchBoxPosts from utils import isEventPost from utils import isBlogPost +from utils import isNewsPost from utils import updateRecentPostsCache from utils import getNicknameFromActor from utils import getDomainFromActor @@ -4512,18 +4513,20 @@ def individualPostAsHtml(allowDownloads: bool, if fullDomain + '/users/' + nickname in postJsonObject['actor']: if '/statuses/' in postJsonObject['object']['id']: if isBlogPost(postJsonObject): - blogPostId = postJsonObject['object']['id'] - editStr += \ - ' ' + \ - '' + \ - '' + \
-                    translate['Edit blog post'] + \
-                    ' |\n' + if not isNewsPost(postJsonObject): + blogPostId = postJsonObject['object']['id'] + editStr += \ + ' ' + \ + '' + \ + '' + \
+                        translate['Edit blog post'] + \
+                        ' |\n' elif isEvent: eventPostId = postJsonObject['object']['id'] editStr += \ From a7cd3d160e845991f2efc1a9eaaa86b50ef7a56b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 10:14:21 +0100 Subject: [PATCH 083/263] No delete button for news posts --- webinterface.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/webinterface.py b/webinterface.py index 9a8c7ed91..392b78c8d 100644 --- a/webinterface.py +++ b/webinterface.py @@ -4686,15 +4686,18 @@ def individualPostAsHtml(allowDownloads: bool, ('/' + fullDomain + '/' in postActor and messageId.startswith(postActor))): if '/users/' + nickname + '/' in messageId: - deleteStr = \ - ' \n' - deleteStr += \ - ' ' + \ - '' + translate['Delete this post'] + \
-                ' |\n' + if not isNewsPost(postJsonObject): + deleteStr = \ + ' \n' + deleteStr += \ + ' ' + \ + '' + \
+                    translate['Delete this post'] + \
+                    ' |\n' else: if not isMuted: muteStr = \ From 014336606d982d36733008cf09808efa8c7a2f57 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 10:33:14 +0100 Subject: [PATCH 084/263] Don't allow the news or shared inbox accounts to be followed --- follow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/follow.py b/follow.py index dc8a7cbda..1616c0fd5 100644 --- a/follow.py +++ b/follow.py @@ -574,6 +574,10 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, print('DEBUG: follow request does not contain a ' + 'nickname for the account followed') return True + if nicknameToFollow == 'news' or nicknameToFollow == 'inbox': + if debug: + print('DEBUG: Cannot follow the news or inbox accounts') + return True handleToFollow = nicknameToFollow + '@' + domainToFollow if domainToFollow == domain: if not os.path.isdir(baseDir + '/accounts/' + handleToFollow): From 8b222151c648952a5cbc9e4a1038e1b1d2921db9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 10:55:58 +0100 Subject: [PATCH 085/263] Extra replacement --- daemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/daemon.py b/daemon.py index f974b03eb..278670d59 100644 --- a/daemon.py +++ b/daemon.py @@ -6563,6 +6563,8 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.replace('/news/', '/' + currNickname + '/') msg = msg.replace('/users/news"', '/users/' + currNickname + '"') + msg = msg.replace('/users/news?', + '/users/' + currNickname + '?') msg = msg.replace('/banner.webp', '/banner.png') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), From 52626eb564324c382e6864dd6cb2ed4e659a11ff Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 11:19:48 +0100 Subject: [PATCH 086/263] Retain news link on date --- daemon.py | 1 + webinterface.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/daemon.py b/daemon.py index 278670d59..37401417c 100644 --- a/daemon.py +++ b/daemon.py @@ -6566,6 +6566,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.replace('/users/news?', '/users/' + currNickname + '?') msg = msg.replace('/banner.webp', '/banner.png') + msg = msg.replace('/news2/', '/news/') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) diff --git a/webinterface.py b/webinterface.py index 392b78c8d..e93be3aa6 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5053,7 +5053,10 @@ def individualPostAsHtml(allowDownloads: bool, if timeDiff > 100: print('TIMING INDIV ' + boxName + ' 15 = ' + str(timeDiff)) - publishedLink = messageId + if '/users/news/' not in messageId: + publishedLink = messageId + else: + publishedLink = messageId.replace('/users/news/', '/users/news2/') # blog posts should have no /statuses/ in their link if isBlogPost(postJsonObject): # is this a post to the local domain? From d1f252aed6319b39bd3eadefd74e3858a84ecb87 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 11:28:14 +0100 Subject: [PATCH 087/263] Published link --- webinterface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webinterface.py b/webinterface.py index e93be3aa6..6ba8bd7fe 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5053,15 +5053,15 @@ def individualPostAsHtml(allowDownloads: bool, if timeDiff > 100: print('TIMING INDIV ' + boxName + ' 15 = ' + str(timeDiff)) - if '/users/news/' not in messageId: - publishedLink = messageId - else: - publishedLink = messageId.replace('/users/news/', '/users/news2/') + publishedLink = messageId # blog posts should have no /statuses/ in their link if isBlogPost(postJsonObject): # is this a post to the local domain? if '://' + domain in messageId: publishedLink = messageId.replace('/statuses/', '/') + # this retains the news account reference for news posts + if '/users/news/' in publishedLink: + publishedLink = publishedLink.replace('/users/news/', '/users/news2/') # if this is a local link then make it relative so that it works # on clearnet or onion address if domain + '/users/' in publishedLink or \ From 585a1340e913b1dce2270e21e205bcd7efbea898 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 11:32:59 +0100 Subject: [PATCH 088/263] No date link on news posts --- daemon.py | 1 - webinterface.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/daemon.py b/daemon.py index 37401417c..278670d59 100644 --- a/daemon.py +++ b/daemon.py @@ -6566,7 +6566,6 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.replace('/users/news?', '/users/' + currNickname + '?') msg = msg.replace('/banner.webp', '/banner.png') - msg = msg.replace('/news2/', '/news/') msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) diff --git a/webinterface.py b/webinterface.py index 6ba8bd7fe..bcb297be7 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5059,17 +5059,17 @@ def individualPostAsHtml(allowDownloads: bool, # is this a post to the local domain? if '://' + domain in messageId: publishedLink = messageId.replace('/statuses/', '/') - # this retains the news account reference for news posts - if '/users/news/' in publishedLink: - publishedLink = publishedLink.replace('/users/news/', '/users/news2/') # if this is a local link then make it relative so that it works # on clearnet or onion address if domain + '/users/' in publishedLink or \ domain + ':' + str(port) + '/users/' in publishedLink: publishedLink = '/users/' + publishedLink.split('/users/')[1] - footerStr = '' + publishedStr + '\n' + if not isNewsPost(postJsonObject): + footerStr = '' + publishedStr + '\n' + else: + footerStr = publishedStr + '\n' # change the background color for DMs in inbox timeline if showDMicon: From dc49f057882c7616139087b23f1d579d9f05c80c Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 11:40:45 +0100 Subject: [PATCH 089/263] Footer with icons --- webinterface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index bcb297be7..22f62f756 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5080,8 +5080,11 @@ def individualPostAsHtml(allowDownloads: bool, footerStr = '\n
\n' footerStr += replyStr + announceStr + likeStr + bookmarkStr + \ deleteStr + muteStr + editStr - footerStr += ' ' + publishedStr + '\n' + if not isNewsPost(postJsonObject): + footerStr += ' ' + publishedStr + '\n' + else: + footerStr += publishedStr + '\n' footerStr += '
\n' postIsSensitive = False From c362daba79a2c3dee7a792db3cc9e4bd6b2e0257 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 11:44:39 +0100 Subject: [PATCH 090/263] Use label on date --- webinterface.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 22f62f756..ce43a75ee 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5069,7 +5069,8 @@ def individualPostAsHtml(allowDownloads: bool, footerStr = '' + publishedStr + '\n' else: - footerStr = publishedStr + '\n' + footerStr = ' \n' # change the background color for DMs in inbox timeline if showDMicon: @@ -5084,7 +5085,8 @@ def individualPostAsHtml(allowDownloads: bool, footerStr += ' ' + publishedStr + '\n' else: - footerStr += publishedStr + '\n' + footerStr += ' \n' footerStr += '
\n' postIsSensitive = False From 9d758c8704f7c781723400dea58f89d6c919f1c2 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 12:14:40 +0100 Subject: [PATCH 091/263] Translations --- translations/ar.json | 2 +- translations/ca.json | 2 +- translations/cy.json | 2 +- translations/de.json | 2 +- translations/en.json | 4 ++-- translations/es.json | 2 +- translations/fr.json | 2 +- translations/ga.json | 2 +- translations/hi.json | 2 +- translations/it.json | 2 +- translations/ja.json | 2 +- translations/pt.json | 2 +- translations/ru.json | 2 +- translations/zh.json | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/translations/ar.json b/translations/ar.json index 74ce376c6..8250b98ce 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -305,5 +305,5 @@ "Remove Vote": "إزالة التصويت", "This is a news instance": "هذا مثال أخبار", "News": "أخبار", - "Read more...": "" + "Read more...": "اقرأ أكثر..." } diff --git a/translations/ca.json b/translations/ca.json index 40f7b8154..c8c0e18ad 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -305,5 +305,5 @@ "Remove Vote": "Elimina el vot", "This is a news instance": "Aquesta és una instància de notícies", "News": "Notícies", - "Read more...": "" + "Read more...": "Llegeix més..." } diff --git a/translations/cy.json b/translations/cy.json index 053d523be..98dc73a77 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -305,5 +305,5 @@ "Remove Vote": "Tynnwch y Bleidlais", "This is a news instance": "Dyma enghraifft newyddion", "News": "Newyddion", - "Read more...": "" + "Read more...": "Darllen mwy..." } diff --git a/translations/de.json b/translations/de.json index 21c688451..b53008f74 100644 --- a/translations/de.json +++ b/translations/de.json @@ -305,5 +305,5 @@ "Remove Vote": "Abstimmung entfernen", "This is a news instance": "Dies ist eine Nachrichteninstanz", "News": "Nachrichten", - "Read more...": "" + "Read more...": "Weiterlesen..." } diff --git a/translations/en.json b/translations/en.json index 36add8b2f..c8bb47660 100644 --- a/translations/en.json +++ b/translations/en.json @@ -212,8 +212,8 @@ "Remove Twitter posts": "Remove Twitter posts", "Sensitive": "Sensitive", "Word Replacements": "Word Replacements", - "Happening Today": "Happening Today", - "Happening This Week": "Happening This Week", + "Happening Today": "Today", + "Happening This Week": "This Week", "Blog": "Blog", "Blogs": "Blogs", "Title": "Title", diff --git a/translations/es.json b/translations/es.json index 010138ebf..e55383321 100644 --- a/translations/es.json +++ b/translations/es.json @@ -305,5 +305,5 @@ "Remove Vote": "Eliminar voto", "This is a news instance": "Esta es una instancia de noticias", "News": "Noticias", - "Read more...": "" + "Read more...": "Lee mas..." } diff --git a/translations/fr.json b/translations/fr.json index a9e3c739b..87851e3d8 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -305,5 +305,5 @@ "Remove Vote": "Supprimer le vote", "This is a news instance": "Ceci est une instance d'actualité", "News": "Nouvelles", - "Read more...": "" + "Read more...": "Lire la suite..." } diff --git a/translations/ga.json b/translations/ga.json index 429efb4d8..8e9ece95c 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -305,5 +305,5 @@ "Remove Vote": "Bain Vóta", "This is a news instance": "Is sampla nuachta é seo", "News": "Nuacht", - "Read more...": "" + "Read more...": "Leigh Nios mo..." } diff --git a/translations/hi.json b/translations/hi.json index 428b8b1ec..53af28fba 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -305,5 +305,5 @@ "Remove Vote": "वोट हटा दें", "This is a news instance": "यह एक समाचार का उदाहरण है", "News": "समाचार", - "Read more...": "" + "Read more...": "अधिक पढ़ें..." } diff --git a/translations/it.json b/translations/it.json index 32788a6b8..6fbda6588 100644 --- a/translations/it.json +++ b/translations/it.json @@ -305,5 +305,5 @@ "Remove Vote": "Rimuovi voto", "This is a news instance": "Questa è un'istanza di notizie", "News": "Notizia", - "Read more...": "" + "Read more...": "Leggi di più..." } diff --git a/translations/ja.json b/translations/ja.json index b57fe63d9..f68c8fcf4 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -305,5 +305,5 @@ "Remove Vote": "投票を削除", "This is a news instance": "これはニュースインスタンスです", "News": "ニュース", - "Read more...": "" + "Read more...": "続きを読む..." } diff --git a/translations/pt.json b/translations/pt.json index 67a076516..3562ad311 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -305,5 +305,5 @@ "Remove Vote": "Remover voto", "This is a news instance": "Esta é uma instância de notícias", "News": "Notícia", - "Read more...": "" + "Read more...": "Consulte Mais informação..." } diff --git a/translations/ru.json b/translations/ru.json index 726df9bea..871742e17 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -305,5 +305,5 @@ "Remove Vote": "Удалить голос", "This is a news instance": "Это новостной экземпляр", "News": "Новости", - "Read more...": "" + "Read more...": "Подробнее..." } diff --git a/translations/zh.json b/translations/zh.json index dafc27164..13844c25d 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -305,5 +305,5 @@ "Remove Vote": "删除投票", "This is a news instance": "这是一个新闻实例", "News": "新闻", - "Read more...": "" + "Read more...": "阅读更多..." } From 5ad810e6946f199ded9477f42603c82714ffc989 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 12:33:15 +0100 Subject: [PATCH 092/263] No trailing html --- newsdaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsdaemon.py b/newsdaemon.py index bc7b02398..c71e9da11 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -73,7 +73,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, '/postcache/' + newPostId.replace('/', '#') + '.html' if os.path.isfile(htmlFilename): newswire[originalDateStr][1] = \ - '/users/news/statuses/' + statusNumber + '.html' + '/users/news/statuses/' + statusNumber # don't create the post if it already exists continue From bd4db02b3a0f18e8a8aa55798352c956ceb1d2a6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 12:58:48 +0100 Subject: [PATCH 093/263] Link style --- newsdaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index c71e9da11..3187fa001 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -72,8 +72,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, baseDir + '/accounts/news@' + domain + \ '/postcache/' + newPostId.replace('/', '#') + '.html' if os.path.isfile(htmlFilename): - newswire[originalDateStr][1] = \ - '/users/news/statuses/' + statusNumber + newswire[originalDateStr][1] = '/@news/' + statusNumber # don't create the post if it already exists continue @@ -121,6 +120,7 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # save the post and update the index if saveJson(blog, filename): updateFeedsIndex(baseDir, domain, postId + '.json') + newswire[originalDateStr][1] = '/@news/' + statusNumber def runNewswireDaemon(baseDir: str, httpd, From 936b752a0e189d98f8908d6d53cdc0ca8b6eb895 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:13:42 +0100 Subject: [PATCH 094/263] Populate post cache when importing rss items --- newsdaemon.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 3187fa001..02d2d68ff 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -11,6 +11,7 @@ import time from collections import OrderedDict from newswire import getDictFromNewswire from posts import createNewsPost +from inbox import inboxStorePostToHtmlCache from utils import saveJson from utils import getStatusNumber @@ -42,7 +43,10 @@ def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: def convertRSStoActivityPub(baseDir: str, httpPrefix: str, domain: str, port: int, newswire: {}, - translate: {}) -> None: + translate: {}, + recentPostsCache: {}, maxRecentPosts: int, + session, cachedWebfingers: {}, + personCache: {}) -> None: """Converts rss items in a newswire into posts """ basePath = baseDir + '/accounts/news@' + domain + '/outbox' @@ -120,6 +124,12 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # save the post and update the index if saveJson(blog, filename): updateFeedsIndex(baseDir, domain, postId + '.json') + # convert json to html + inboxStorePostToHtmlCache(recentPostsCache, maxRecentPosts, + translate, baseDir, httpPrefix, + session, cachedWebfingers, personCache, + 'news', domain, port, + blog, False, 'outbox') newswire[originalDateStr][1] = '/@news/' + statusNumber @@ -151,7 +161,12 @@ def runNewswireDaemon(baseDir: str, httpd, convertRSStoActivityPub(baseDir, httpPrefix, domain, port, - newNewswire, translate) + newNewswire, translate, + httpd.recentPostsCache, + httpd.maxRecentPosts, + httpd.session, + httpd.cachedWebfingers, + httpd.personCache) print('Newswire feed converted to ActivityPub') # wait a while before the next feeds update From 90283d57072f3eaa248a5f182ac29e8a46ffa661 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:16:44 +0100 Subject: [PATCH 095/263] Boxname --- inbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inbox.py b/inbox.py index c61b9320d..c7309a7d3 100644 --- a/inbox.py +++ b/inbox.py @@ -142,7 +142,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, baseDir, session, cachedWebfingers, personCache, nickname, domain, port, postJsonObject, avatarUrl, True, allowDeletion, - httpPrefix, __version__, boxName, + httpPrefix, __version__, boxname, not isDM(postJsonObject), True, True, False, True) From bbbf470149740739b194ea18a98c5c95cfea2f14 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:28:02 +0100 Subject: [PATCH 096/263] Boxname --- inbox.py | 2 +- newsdaemon.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/inbox.py b/inbox.py index c7309a7d3..45478e77d 100644 --- a/inbox.py +++ b/inbox.py @@ -136,7 +136,7 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, pageNumber = -999 avatarUrl = None if boxname != 'tlevents' and boxname != 'outbox': - boxName = 'inbox' + boxname = 'inbox' individualPostAsHtml(True, recentPostsCache, maxRecentPosts, getIconsDir(baseDir), translate, pageNumber, baseDir, session, cachedWebfingers, personCache, diff --git a/newsdaemon.py b/newsdaemon.py index 02d2d68ff..2b5535fb7 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -11,10 +11,10 @@ import time from collections import OrderedDict from newswire import getDictFromNewswire from posts import createNewsPost -from inbox import inboxStorePostToHtmlCache +from inbox import individualPostAsHtml from utils import saveJson from utils import getStatusNumber - +from webinterface import getIconsDir def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: """Updates the index used for imported RSS feeds @@ -125,11 +125,17 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if saveJson(blog, filename): updateFeedsIndex(baseDir, domain, postId + '.json') # convert json to html - inboxStorePostToHtmlCache(recentPostsCache, maxRecentPosts, - translate, baseDir, httpPrefix, - session, cachedWebfingers, personCache, - 'news', domain, port, - blog, False, 'outbox') + iconsDir = getIconsDir(baseDir) + pageNumber = -999 + avatarUrl = None + individualPostAsHtml(True, recentPostsCache, maxRecentPosts, + iconsDir, translate, pageNumber, + baseDir, session, cachedWebfingers, + personCache, + 'news', domain, port, blog, + avatarUrl, True, False, + httpPrefix, __version__, 'outbox', + True, True, True, False, True) newswire[originalDateStr][1] = '/@news/' + statusNumber From aa84d0081f028d7ef27af112f88e057805f385af Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:29:40 +0100 Subject: [PATCH 097/263] Spacing --- newsdaemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/newsdaemon.py b/newsdaemon.py index 2b5535fb7..ca466a481 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -16,6 +16,7 @@ from utils import saveJson from utils import getStatusNumber from webinterface import getIconsDir + def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: """Updates the index used for imported RSS feeds """ From 12b37b81d3cc9e0c160c4fadf445f35a6c58be6f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:37:14 +0100 Subject: [PATCH 098/263] Test without html generation --- newsdaemon.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index ca466a481..3d2fbbe02 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -73,11 +73,12 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, if os.path.isfile(filename): # if a local post exists as html then change the link # to the local one - htmlFilename = \ - baseDir + '/accounts/news@' + domain + \ - '/postcache/' + newPostId.replace('/', '#') + '.html' - if os.path.isfile(htmlFilename): - newswire[originalDateStr][1] = '/@news/' + statusNumber + #htmlFilename = \ + # baseDir + '/accounts/news@' + domain + \ + # '/postcache/' + newPostId.replace('/', '#') + '.html' + #if os.path.isfile(htmlFilename): + newswire[originalDateStr][1] = \ + '/users/news/statuses/' + statusNumber # don't create the post if it already exists continue @@ -128,16 +129,16 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # convert json to html iconsDir = getIconsDir(baseDir) pageNumber = -999 - avatarUrl = None - individualPostAsHtml(True, recentPostsCache, maxRecentPosts, - iconsDir, translate, pageNumber, - baseDir, session, cachedWebfingers, - personCache, - 'news', domain, port, blog, - avatarUrl, True, False, - httpPrefix, __version__, 'outbox', - True, True, True, False, True) - newswire[originalDateStr][1] = '/@news/' + statusNumber + #individualPostAsHtml(True, recentPostsCache, maxRecentPosts, + # iconsDir, translate, pageNumber, + # baseDir, session, cachedWebfingers, + # personCache, + # 'news', domain, port, blog, + # None, True, False, + # httpPrefix, __version__, 'outbox', + # True, True, True, False, True) + newswire[originalDateStr][1] = \ + '/users/news/statuses/' + statusNumber def runNewswireDaemon(baseDir: str, httpd, From e465b91647e78bd16356ed3c1464723e67da925f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 13:52:15 +0100 Subject: [PATCH 099/263] Tidying --- newsdaemon.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/newsdaemon.py b/newsdaemon.py index 3d2fbbe02..beaeef7a4 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -11,10 +11,8 @@ import time from collections import OrderedDict from newswire import getDictFromNewswire from posts import createNewsPost -from inbox import individualPostAsHtml from utils import saveJson from utils import getStatusNumber -from webinterface import getIconsDir def updateFeedsIndex(baseDir: str, domain: str, postId: str) -> None: @@ -71,15 +69,9 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # file where the post is stored filename = basePath + '/' + newPostId.replace('/', '#') + '.json' if os.path.isfile(filename): - # if a local post exists as html then change the link - # to the local one - #htmlFilename = \ - # baseDir + '/accounts/news@' + domain + \ - # '/postcache/' + newPostId.replace('/', '#') + '.html' - #if os.path.isfile(htmlFilename): + # don't create the post if it already exists newswire[originalDateStr][1] = \ '/users/news/statuses/' + statusNumber - # don't create the post if it already exists continue rssTitle = item[0] @@ -126,17 +118,6 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # save the post and update the index if saveJson(blog, filename): updateFeedsIndex(baseDir, domain, postId + '.json') - # convert json to html - iconsDir = getIconsDir(baseDir) - pageNumber = -999 - #individualPostAsHtml(True, recentPostsCache, maxRecentPosts, - # iconsDir, translate, pageNumber, - # baseDir, session, cachedWebfingers, - # personCache, - # 'news', domain, port, blog, - # None, True, False, - # httpPrefix, __version__, 'outbox', - # True, True, True, False, True) newswire[originalDateStr][1] = \ '/users/news/statuses/' + statusNumber From b07ce27a03b00da46d88e57fc0021be982c56213 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 14:07:17 +0100 Subject: [PATCH 100/263] Locate news posts --- utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils.py b/utils.py index 74f58ba52..e91091d6a 100644 --- a/utils.py +++ b/utils.py @@ -523,6 +523,12 @@ def locatePost(baseDir: str, nickname: str, domain: str, if os.path.isfile(postFilename): return postFilename + # check news posts + accountDir = baseDir + '/accounts/news' + '@' + domain + '/' + postFilename = accountDir + boxName + '/' + postUrl + if os.path.isfile(postFilename): + return postFilename + # is it in the announce cache? postFilename = baseDir + '/cache/announce/' + nickname + '/' + postUrl if os.path.isfile(postFilename): From 72c9efdd76331a2dea4f4900987514521166809b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 14:18:57 +0100 Subject: [PATCH 101/263] debug --- daemon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/daemon.py b/daemon.py index 278670d59..6c286a367 100644 --- a/daemon.py +++ b/daemon.py @@ -4721,6 +4721,7 @@ class PubServer(BaseHTTPRequestHandler): if 'vote:' + nickname not in newswire[dateStr][2]: newswire[dateStr][2].append('vote:' + nickname) filename = newswire[dateStr][3] + print('VOTE filename ' + str(filename)) if filename: saveJson(newswire[dateStr][2], filename + '.votes') From ea3211e8ed8d3302ebf4e2df33308e83e25f75e8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 15:35:26 +0100 Subject: [PATCH 102/263] Set filename for rss posts --- newsdaemon.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/newsdaemon.py b/newsdaemon.py index beaeef7a4..a7245c007 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -70,8 +70,11 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, filename = basePath + '/' + newPostId.replace('/', '#') + '.json' if os.path.isfile(filename): # don't create the post if it already exists + # set the url newswire[originalDateStr][1] = \ '/users/news/statuses/' + statusNumber + # set the filename + newswire[originalDateStr][3] = filename continue rssTitle = item[0] @@ -118,8 +121,11 @@ def convertRSStoActivityPub(baseDir: str, httpPrefix: str, # save the post and update the index if saveJson(blog, filename): updateFeedsIndex(baseDir, domain, postId + '.json') + # set the url newswire[originalDateStr][1] = \ '/users/news/statuses/' + statusNumber + # set the filename + newswire[originalDateStr][3] = filename def runNewswireDaemon(baseDir: str, httpd, From 71a36ec3ee57754fb771712828823fdee9c852b9 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 16:07:06 +0100 Subject: [PATCH 103/263] Full url in rss link --- newswire.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/newswire.py b/newswire.py index 0d3ddceea..d97969677 100644 --- a/newswire.py +++ b/newswire.py @@ -164,7 +164,10 @@ def getRSSfromDict(baseDir: str, newswire: {}, continue rssStr += '\n' rssStr += ' ' + fields[0] + '\n' - rssStr += ' ' + fields[1] + '\n' + url = fields[1] + if domainFull not in url: + url = httpPrefix + '://' + domainFull + url + rssStr += ' ' + url + '\n' rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") rssStr += ' ' + rssDateStr + '\n' From 321aea4f891a225445750a9bb6fa98980addedc4 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 16:25:06 +0100 Subject: [PATCH 104/263] Script to clear the newswire --- scripts/clearnewswire | 4 ++++ 1 file changed, 4 insertions(+) create mode 100755 scripts/clearnewswire diff --git a/scripts/clearnewswire b/scripts/clearnewswire new file mode 100755 index 000000000..1815e9015 --- /dev/null +++ b/scripts/clearnewswire @@ -0,0 +1,4 @@ +#!/bin/bash +rm accounts/news@*/outbox/* +rm accounts/news@*/postcache/* +rm accounts/news@*/outbox.index From f3432d96f72ba6b8530c4fc63eaa318674c9a490 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 17:31:13 +0100 Subject: [PATCH 105/263] Positive or negative voting on newswire items --- daemon.py | 41 ++++++++++++++++++--------- epicyon.py | 7 ++++- tests.py | 6 ++-- webinterface.py | 74 ++++++++++++++++++++++++++++++------------------- 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/daemon.py b/daemon.py index 6c286a367..ebe93ad56 100644 --- a/daemon.py +++ b/daemon.py @@ -4721,7 +4721,6 @@ class PubServer(BaseHTTPRequestHandler): if 'vote:' + nickname not in newswire[dateStr][2]: newswire[dateStr][2].append('vote:' + nickname) filename = newswire[dateStr][3] - print('VOTE filename ' + str(filename)) if filename: saveJson(newswire[dateStr][2], filename + '.votes') @@ -6043,7 +6042,8 @@ class PubServer(BaseHTTPRequestHandler): projectVersion, self._isMinimal(nickname), YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) if GETstartTime: self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', @@ -6150,7 +6150,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6251,7 +6252,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6352,7 +6354,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6453,7 +6456,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6560,7 +6564,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire, moderator) + self.server.newswire, moderator, + self.server.positiveVoting) msg = msg.replace('/news/', '/' + currNickname + '/') msg = msg.replace('/users/news"', '/users/' + currNickname + '"') @@ -6641,7 +6646,8 @@ class PubServer(BaseHTTPRequestHandler): httpPrefix, self.server.projectVersion, self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6726,7 +6732,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6830,7 +6837,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -6924,7 +6932,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, self._isMinimal(nickname), self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -7010,7 +7019,8 @@ class PubServer(BaseHTTPRequestHandler): httpPrefix, self.server.projectVersion, self.server.YTReplacementDomain, - self.server.newswire) + self.server.newswire, + self.server.positiveVoting) msg = msg.encode('utf-8') self._set_headers('text/html', len(msg), cookie, callingDomain) @@ -11345,7 +11355,8 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: tokensLookup[token] = nickname -def runDaemon(newsInstance: bool, +def runDaemon(positiveVoting: bool, + newsInstance: bool, blogsInstance: bool, mediaInstance: bool, maxRecentPosts: int, @@ -11452,6 +11463,10 @@ def runDaemon(newsInstance: bool, print('ERROR: no translations loaded from ' + translationsFile) sys.exit() + # on the newswire, whether moderators vote positively for items + # or against them (veto) + httpd.positiveVoting = positiveVoting + if registration == 'open': httpd.registration = True else: diff --git a/epicyon.py b/epicyon.py index 9cb4eb300..ea13307d2 100644 --- a/epicyon.py +++ b/epicyon.py @@ -198,6 +198,10 @@ parser.add_argument("--blogsinstance", type=str2bool, nargs='?', parser.add_argument("--newsinstance", type=str2bool, nargs='?', const=True, default=False, help="News Instance - favor news over microblogging") +parser.add_argument("--positivevoting", type=str2bool, nargs='?', + const=True, default=False, + help="On newswire, whether moderators vote " + + "positively for or veto against items") parser.add_argument("--debug", type=str2bool, nargs='?', const=True, default=False, help="Show debug messages") @@ -1911,7 +1915,8 @@ if setTheme(baseDir, themeName): print('Theme set to ' + themeName) if __name__ == "__main__": - runDaemon(args.newsinstance, + runDaemon(args.positivevoting, + args.newsinstance, args.blogsinstance, args.mediainstance, args.maxRecentPosts, not args.nosharedinbox, diff --git a/tests.py b/tests.py index 8d9a2bd5e..8a8989e0c 100644 --- a/tests.py +++ b/tests.py @@ -287,7 +287,7 @@ def createServerAlice(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Alice') - runDaemon(False, False, False, + runDaemon(False, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -350,7 +350,7 @@ def createServerBob(path: str, domain: str, port: int, onionDomain = None i2pDomain = None print('Server running: Bob') - runDaemon(False, False, False, + runDaemon(False, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, @@ -387,7 +387,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], onionDomain = None i2pDomain = None print('Server running: Eve') - runDaemon(False, False, False, + runDaemon(False, False, False, False, 5, True, True, 'en', __version__, "instanceId", False, path, domain, onionDomain, i2pDomain, None, port, port, diff --git a/webinterface.py b/webinterface.py index ce43a75ee..778921dd8 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5368,7 +5368,7 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, def htmlNewswire(newswire: str, nickname: str, moderator: bool, - translate: {}) -> str: + translate: {}, positiveVoting: bool) -> str: """Converts a newswire dict into html """ htmlStr = '' @@ -5408,8 +5408,15 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, for line in item[2]: if 'vote:' in line: totalVotes += 1 + # show a number of ticks or crosses for how many + # votes for or against if totalVotes > 0: - totalVotesStr = ' +' + str(totalVotes) + totalVotesStr = ' ' + for v in range(totalVotes): + if positiveVoting: + totalVotesStr += '✓' + else: + totalVotesStr += '✗' htmlStr += '

' + \ '' + item[0] + '' + \ @@ -5431,7 +5438,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, iconsDir: str, moderator: bool, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Returns html content for the right column """ htmlStr = '' @@ -5503,7 +5510,8 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, else: htmlStr += '
\n' - htmlStr += htmlNewswire(newswire, nickname, moderator, translate) + htmlStr += htmlNewswire(newswire, nickname, moderator, translate, + positiveVoting) return htmlStr @@ -5518,7 +5526,8 @@ def htmlTimeline(defaultTimeline: str, manuallyApproveFollowers: bool, minimal: bool, YTReplacementDomain: str, - newswire: {}, moderator: bool) -> str: + newswire: {}, moderator: bool, + positiveVoting: bool) -> str: """Show the timeline as html """ timelineStartTime = time.time() @@ -6153,7 +6162,7 @@ def htmlTimeline(defaultTimeline: str, # right column rightColumnStr = getRightColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, - moderator, newswire) + moderator, newswire, positiveVoting) tlStr += ' ' + \ rightColumnStr + ' \n' tlStr += ' \n' @@ -6195,7 +6204,7 @@ def htmlShares(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the shares timeline as html """ manuallyApproveFollowers = \ @@ -6207,7 +6216,8 @@ def htmlShares(defaultTimeline: str, nickname, domain, port, None, 'tlshares', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - False, YTReplacementDomain, newswire, False) + False, YTReplacementDomain, newswire, False, + positiveVoting) def htmlInbox(defaultTimeline: str, @@ -6218,7 +6228,7 @@ def htmlInbox(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the inbox as html """ manuallyApproveFollowers = \ @@ -6230,7 +6240,8 @@ def htmlInbox(defaultTimeline: str, nickname, domain, port, inboxJson, 'inbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlBookmarks(defaultTimeline: str, @@ -6241,7 +6252,7 @@ def htmlBookmarks(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the bookmarks as html """ manuallyApproveFollowers = \ @@ -6253,7 +6264,8 @@ def htmlBookmarks(defaultTimeline: str, nickname, domain, port, bookmarksJson, 'tlbookmarks', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlEvents(defaultTimeline: str, @@ -6264,7 +6276,7 @@ def htmlEvents(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the events as html """ manuallyApproveFollowers = \ @@ -6276,7 +6288,8 @@ def htmlEvents(defaultTimeline: str, nickname, domain, port, bookmarksJson, 'tlevents', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlInboxDMs(defaultTimeline: str, @@ -6287,7 +6300,7 @@ def htmlInboxDMs(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the DM timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6295,7 +6308,7 @@ def htmlInboxDMs(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'dm', allowDeletion, httpPrefix, projectVersion, False, minimal, - YTReplacementDomain, newswire, False) + YTReplacementDomain, newswire, False, positiveVoting) def htmlInboxReplies(defaultTimeline: str, @@ -6306,7 +6319,7 @@ def htmlInboxReplies(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the replies timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6314,7 +6327,8 @@ def htmlInboxReplies(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlreplies', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlInboxMedia(defaultTimeline: str, @@ -6325,7 +6339,7 @@ def htmlInboxMedia(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the media timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6333,7 +6347,8 @@ def htmlInboxMedia(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlmedia', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlInboxBlogs(defaultTimeline: str, @@ -6344,7 +6359,7 @@ def htmlInboxBlogs(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the blogs timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6352,7 +6367,8 @@ def htmlInboxBlogs(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlblogs', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire, False) + minimal, YTReplacementDomain, newswire, False, + positiveVoting) def htmlInboxNews(defaultTimeline: str, @@ -6363,7 +6379,8 @@ def htmlInboxNews(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}, moderator: bool) -> str: + newswire: {}, moderator: bool, + positiveVoting: bool) -> str: """Show the news timeline as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6371,7 +6388,8 @@ def htmlInboxNews(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'tlnews', allowDeletion, httpPrefix, projectVersion, False, - minimal, YTReplacementDomain, newswire, moderator) + minimal, YTReplacementDomain, newswire, moderator, + positiveVoting) def htmlModeration(defaultTimeline: str, @@ -6382,7 +6400,7 @@ def htmlModeration(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the moderation feed as html """ return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, @@ -6390,7 +6408,7 @@ def htmlModeration(defaultTimeline: str, itemsPerPage, session, baseDir, wfRequest, personCache, nickname, domain, port, inboxJson, 'moderation', allowDeletion, httpPrefix, projectVersion, True, False, - YTReplacementDomain, newswire, False) + YTReplacementDomain, newswire, False, positiveVoting) def htmlOutbox(defaultTimeline: str, @@ -6401,7 +6419,7 @@ def htmlOutbox(defaultTimeline: str, allowDeletion: bool, httpPrefix: str, projectVersion: str, minimal: bool, YTReplacementDomain: str, - newswire: {}) -> str: + newswire: {}, positiveVoting: bool) -> str: """Show the Outbox as html """ manuallyApproveFollowers = \ @@ -6412,7 +6430,7 @@ def htmlOutbox(defaultTimeline: str, nickname, domain, port, outboxJson, 'outbox', allowDeletion, httpPrefix, projectVersion, manuallyApproveFollowers, minimal, - YTReplacementDomain, newswire, False) + YTReplacementDomain, newswire, False, positiveVoting) def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int, From 68758e95f5c6e1611c3f7ba1b624e8c8535ec608 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 17:39:30 +0100 Subject: [PATCH 106/263] Displaying votes on newswire --- webinterface.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 778921dd8..f69a680bd 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5384,7 +5384,12 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, if 'vote:' in line: totalVotes += 1 if totalVotes > 0: - totalVotesStr = ' +' + str(totalVotes) + totalVotesStr = ' ' + for v in range(totalVotes): + if positiveVoting: + totalVotesStr += '✓' + else: + totalVotesStr += '✗' htmlStr += '

' + \ '' + item[0] + '' + \ From c5ecdaf5c3887657964929ac7820fc2b31202118 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 18:05:01 +0100 Subject: [PATCH 107/263] Tidying --- webinterface.py | 56 ++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/webinterface.py b/webinterface.py index f69a680bd..93be55b57 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5367,6 +5367,30 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, return htmlStr +def votesOnNewswireItem(status: []) -> int: + """Returns the number of votes on a newswire item + """ + totalVotes = 0 + for line in status: + if 'vote:' in line: + totalVotes += 1 + return totalVotes + + +def votesIndicator(totalVotes: int, positiveVoting: bool) -> str: + """Returns an indicator of the number of votes on a newswire item + """ + if totalVotes <= 0: + return '' + totalVotesStr = ' ' + for v in range(totalVotes): + if positiveVoting: + totalVotesStr += '✓' + else: + totalVotesStr += '✗' + return totalVotesStr + + def htmlNewswire(newswire: str, nickname: str, moderator: bool, translate: {}, positiveVoting: bool) -> str: """Converts a newswire dict into html @@ -5377,19 +5401,11 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, dateStrLink = dateStrLink.replace('+00:00', '') if 'vote:' + nickname in item[2]: totalVotesStr = '' + totalVotes = 0 if moderator: - # count the total votes for this item - totalVotes = 0 - for line in item[2]: - if 'vote:' in line: - totalVotes += 1 - if totalVotes > 0: - totalVotesStr = ' ' - for v in range(totalVotes): - if positiveVoting: - totalVotesStr += '✓' - else: - totalVotesStr += '✗' + totalVotes = votesOnNewswireItem(item[2]) + totalVotesStr = \ + votesIndicator(totalVotes, positiveVoting) htmlStr += '

' + \ '' + item[0] + '' + \ @@ -5407,21 +5423,13 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, htmlStr += dateStr.replace('+00:00', '') + '

' else: totalVotesStr = '' + totalVotes = 0 if moderator: - # count the total votes for this item - totalVotes = 0 - for line in item[2]: - if 'vote:' in line: - totalVotes += 1 + totalVotes = votesOnNewswireItem(item[2]) # show a number of ticks or crosses for how many # votes for or against - if totalVotes > 0: - totalVotesStr = ' ' - for v in range(totalVotes): - if positiveVoting: - totalVotesStr += '✓' - else: - totalVotesStr += '✗' + totalVotesStr = \ + votesIndicator(totalVotes, positiveVoting) htmlStr += '

' + \ '' + item[0] + '' + \ From b0839c4c3266e903cb75eedbe966cc1197632887 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 8 Oct 2020 18:18:13 +0100 Subject: [PATCH 108/263] Better terminology --- epicyon-profile.css | 4 ++-- webinterface.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 8a59c6386..4e797fce8 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -232,7 +232,7 @@ a:focus { line-height: var(--line-spacing-newswire); } -.newswireItemApproved { +.newswireItemVotedOn { font-size: var(--font-size-newswire); font-weight: bold; color: var(--column-right-fg-color-approved); @@ -245,7 +245,7 @@ a:focus { float: right; } -.newswireDateApproved { +.newswireDateVotedOn { font-size: var(--font-size-newswire); font-weight: bold; color: var(--newswire-date-color); diff --git a/webinterface.py b/webinterface.py index 93be55b57..37c5bf1ca 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5407,7 +5407,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, totalVotesStr = \ votesIndicator(totalVotes, positiveVoting) - htmlStr += '

' + \ + htmlStr += '

' + \ '' + item[0] + '' + \ totalVotesStr if moderator: @@ -5416,10 +5416,10 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, '' + \ - '

' else: - htmlStr += '
' + iconsDir + '/logorss.png" />' if startIndex > 0: # previous page link @@ -5437,7 +5437,7 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, '" loading="lazy" alt="' + \ translate['RSS feed for this site'] + \ '" title="' + translate['RSS feed for this site'] + \ - '" src="/' + iconsDir + '/rss.png" />\n' + '" src="/' + iconsDir + '/logorss.png" />\n' if editImageClass == 'leftColEdit': htmlStr += ' \n' @@ -5644,7 +5644,7 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, '" loading="lazy" alt="' + \ translate['Newswire RSS Feed'] + '" title="' + \ translate['Newswire RSS Feed'] + '" src="/' + \ - iconsDir + '/rss.png" />\n' + iconsDir + '/logorss.png" />\n' if editImageClass == 'rightColEdit': htmlStr += ' \n' From 3ebb8d0b99fa845f29909a33ed2d3b3f8da899c8 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 16:14:50 +0100 Subject: [PATCH 251/263] rss logos --- img/icons/hacker/logorss.png | Bin 0 -> 5784 bytes img/icons/henge/logorss.png | Bin 0 -> 5777 bytes img/icons/indymedia/logorss.png | Bin 0 -> 9023 bytes img/icons/lcd/logorss.png | Bin 0 -> 7988 bytes img/icons/light/logorss.png | Bin 0 -> 9023 bytes img/icons/logorss.png | Bin 0 -> 9023 bytes img/icons/night/logorss.png | Bin 0 -> 9023 bytes img/icons/purple/logorss.png | Bin 0 -> 5756 bytes img/icons/solidaric/logorss.png | Bin 0 -> 5703 bytes img/icons/starlight/logorss.png | Bin 0 -> 5762 bytes img/icons/zen/logorss.png | Bin 0 -> 5775 bytes 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 img/icons/hacker/logorss.png create mode 100644 img/icons/henge/logorss.png create mode 100644 img/icons/indymedia/logorss.png create mode 100644 img/icons/lcd/logorss.png create mode 100644 img/icons/light/logorss.png create mode 100644 img/icons/logorss.png create mode 100644 img/icons/night/logorss.png create mode 100644 img/icons/purple/logorss.png create mode 100644 img/icons/solidaric/logorss.png create mode 100644 img/icons/starlight/logorss.png create mode 100644 img/icons/zen/logorss.png diff --git a/img/icons/hacker/logorss.png b/img/icons/hacker/logorss.png new file mode 100644 index 0000000000000000000000000000000000000000..153fe1b6254f7b7e1f0c37ddaf1668615a5fe6b6 GIT binary patch literal 5784 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 literal 0 HcmV?d00001 diff --git a/img/icons/henge/logorss.png b/img/icons/henge/logorss.png new file mode 100644 index 0000000000000000000000000000000000000000..6d20602fbc4909a557abb6c1b82b7fbe1a5616a2 GIT binary patch literal 5777 zcmeHLX;f3!7EVyhAOd2wVx`2ONWqYiOcH^F08yd{2q;pxx%Vcdklc`5NPt!mMNuot zP>U!E4qXc3fVQaAY89x+Lrb*|bwZ>dQWT#0w8fz(0WrM#>~p=>e`c)=XP<9>d!Id> zth+LFcF-uB2{r@*VU#2|APju_8Q&Jez~@EF4N?MO7&Sd28V`f?B#jo6E0a(Xo~A)b zs6igpI9beluHu$Q`au0N` zE!lqGw!-no&(){8QgFjw&(BWyENk;?I?-W)Z1F#RWPf74pQCt-e$BJ59Jy^wUuT!d zu^;WaD){}xGYj#r2Q9~ME>Y_Hc9lHPb*$)awP((%Q7o!jy7j2KBl*nb@FLx&8qemI zup0KmOQ-Kb@!5^2uiQO1y!lZqW2Q~c^>_GB6N1Fb!j(6lO7i$)i&?2xc0w!jET=DM zudh-`HZiNkYbNgz7Q|NJlK2amQSrZIP!4u9WgH2#9-}`I?3UAPlfAF}o_m~4(_!J} z+ria)_j!HfpMy@*R@A!_%6`jny=J$l@FJW%*5Q12`?68%w{3Qu*SV^4Y*3Z|$ag$` zzK{7|rxFiY={(1YEG$+`YH7Hwkx$KX8T0sEt10VGjkOB?e0pTV+*-oa-s1Pqa=Z8M z{-ikX06|z+yTol4u_C8-`QzRq2a#C3t-Sb%ThoG&X~o4GuFPCiSGTQVW6jjFALVZ^ zV$J_&q44xB@Y`^NFRE7M`B>@$Lnc=~C~DKRV2yuvHhn+3|zc zv!n)ZTdBU5ap&+C{=>>xtv^5~gC?XEt_XbOIKZ zSbF1p&(1zkRwTc<&bF58nA1nz=Xgn$I(b#zNnv%7^M>BNq4gdq-*q0$SrR|7mftCV z!;dv!+Hg^O3dbarHyJ>)E$EkO1}!5*7dIc?hcxU#z8 z?3JF1VK;m{GxIQXv^!&WAx{BjRqi$tg z?OeZu{L|W!a*r8>sftgR$Egh7Pt<+m(nlW6{&rqMYoF_i#YAO$=9I}T@b?4)F#lPP7E$zZ>Y8EY za;Hv2c&a@zyTwyzwatF+XEXnCz9WD>;%sk;Qx`+ry_M5+CG8IR(Y1~NR_o@9oW@4yoGZ_hk)?gs_RmXi zz1J|Mpu#pVcSJ;)Q2V51aZgRm0UGb7TlN^gN2^tfZHIjqTROkG`yPBny_v8?(K0i! zuVPeud%=VBnAOh2r&o^GrNk6G%4~Ed{l0zH^0r`N@iQUO;JXbwO&nJ3m(bK*`PJHU zmLyT^4t3^IQT+Dc%UvJbyW9C|_YG#{vqc2CM!WTW+>S{gIK(KyEQ^+g@L^2l1tFLW z^)je5VAc@`KE4JG1Sg_6NrozvY61CP%`q}bi3rG1-cp)WBSI6D!RcBwJbiWqoSq2t z5VG$K8y^E75U5ZbA{kUkY8~GoAe(sk;I~msC6i1LJW)W7mWGlEUa$AkGrcgaf=cJ{cvKpL%3x3cf}%@P4jWZ7INNFog#)G>xpp;T#18b-7^5`hPC zniOr434_2?GznD!M|6OhJ_L^|F!d@S?qxgeF0_9Mh@< zE@GA=4((wH&|gM~m87DGm($m9r*g3x6MlOgBIWODXU zb{!_i^^g|zSAZG-c?PXz$}`E`RDB-~Me7q#P$Ql+Iv>^;;uv6yLYudK<1ciohFz-&WV# zaJ^9k-U$4*y8dtBvU&N$09AuOu6po1;7j*lH}EXMIy59g48E77W;{Kn$UAt%4=jjy z*z9nEU;8ES+>}6+%?b)2ONmk1yF>!~!{lo2lTM5YVqV&&+g2_ zQOCDs&G#qk9aa)%_)R>Y>tC2bSXU9ddy0nQ^ws3={pJm?K6UQeE~|{S#JQsP_P1L_ zi>oA6^l|pZ9x*G^9)7c6cz6J*Dfy?*m2u^Q?q`?s*5xFXW*jWZ%Nb7c`f%CT0kq6i zi|d)uv0H;sJ-uq-*yHKggp++;6Lf?Heip98`(uh^cSer4=&@t99xYkA$1g6*f=S#L i*PZ#`Xq)X~9(8qo>dt@Fuh|MtMv#bS2ka5XXZ->DO=IH# literal 0 HcmV?d00001 diff --git a/img/icons/indymedia/logorss.png b/img/icons/indymedia/logorss.png new file mode 100644 index 0000000000000000000000000000000000000000..30e55f8c1c6077ee7d2977eebf211ee7504249c9 GIT binary patch literal 9023 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

PHm{I`Z*ms1c!Rk~DOxyled%lmqsEmae!ejFg zh|5>N5TA>%Aw;S0f)pH3;9R8;L6m}_?0SuoFu*#@FBQ}P$TMgyQ=aM5P1QGJDB6&Q zff{+6%@MKLjK1{tr~B{mN8rWhdBO++p`Jy%rJY8aPgvw+h^Ms1Y5i63Cog~lHYE?L<8=C0nh5sLLX7c?__E+NOus-PkjW!z$ zp)?}gpnj?P7XbSh!f+H*>oqSk-5k;v7PEU4*xa`V?q_h_vj*<_{xT;g!Qc4mZ=b(W z1E7AbA^-2+VCGhL&dL6D;ioh#@Usu=vEnKw0CkB`r{Bbpa=K*c8X=}l= z1lx$Ps37p&yWz?2$5T&^s+s^6R3dVYoZ@@;GI&Hvp(5{u1XA*cO$a67+R%7Cg<|JI zzNwVbvN2%OnvlwZtlMbz4z9zGS>UI@)_G~5UzG66>Y^6sfJrualX!&F)BRSCUoU%m z%b9}QX`7lhbV=e$mRS6D=V4WDeeCBSuZZ_!7@Ss7ynV;Fe&AQUh_e3Eo(tlf`JuiF7Uqez411lIV^U-2)F C^iY@p literal 0 HcmV?d00001 diff --git a/img/icons/solidaric/logorss.png b/img/icons/solidaric/logorss.png new file mode 100644 index 0000000000000000000000000000000000000000..eca0f532adcc9321be1e4d622cafdbf5bea7df97 GIT binary patch literal 5703 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 N$Mp~KtMgrz`!A0`Sw#Q< literal 0 HcmV?d00001 diff --git a/img/icons/zen/logorss.png b/img/icons/zen/logorss.png new file mode 100644 index 0000000000000000000000000000000000000000..e1991047eb0da4c6581c4c7000f8f9ed26a75ee3 GIT binary patch literal 5775 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 Date: Mon, 12 Oct 2020 17:18:27 +0100 Subject: [PATCH 252/263] rss icon border width --- img/icons/hacker/logorss.png | Bin 5784 -> 7738 bytes img/icons/henge/logorss.png | Bin 5777 -> 7330 bytes img/icons/indymedia/logorss.png | Bin 9023 -> 10510 bytes img/icons/lcd/logorss.png | Bin 7988 -> 8044 bytes img/icons/light/logorss.png | Bin 9023 -> 10510 bytes img/icons/logorss.png | Bin 9023 -> 10510 bytes img/icons/night/logorss.png | Bin 9023 -> 10510 bytes img/icons/purple/logorss.png | Bin 5756 -> 8218 bytes img/icons/solidaric/logorss.png | Bin 5703 -> 8271 bytes img/icons/starlight/logorss.png | Bin 5762 -> 8002 bytes img/icons/zen/logorss.png | Bin 5775 -> 7016 bytes 11 files changed, 0 insertions(+), 0 deletions(-) diff --git a/img/icons/hacker/logorss.png b/img/icons/hacker/logorss.png index 153fe1b6254f7b7e1f0c37ddaf1668615a5fe6b6..4d9db882cc09899ddd71528b620719c8832fcba9 100644 GIT binary patch delta 3349 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 delta 1525 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 KMNUMnLSTYzIEJDC delta 1566 zcmV+(2I2XlIgu@pBYy)gdQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+U=HGlH@21 zMgLjFEWwhH1k2$WF*}&$&jmX@mDyF%9o_XeY0`jYd;uM$>@fcMdxpPo@Nrm(>dCU% zfg|RaxuBDd^C-KfBkp=1@w$cUCwsVWFie70&dX%i_=lZtfq%0p=;;Tc)UF%#`9{8G z;PfJNO(bhY*wfK3p?nDmpEu|dDCf2g%gH}(p^mfdkDa9R4672~+PBE-b#!>ID~!TC zgycZY!sh~HpFzsHv1`Ua5=EbPjsCR~IOH=w9UoQ|Ua8zY`ojJKMeMW(7@nPJcl)WG z^mFHj?cxtJOMkm(ezJq2y>91m&e>C#bDX;?(cn_MHuc!b?qOhs%Vj@iD2`Ln_gykd z3W(kYI{j!zMQN0Y;r&UGIu)8UcH5|7iIE38TIR;GZAqLnw?&O4u0oStvUEvc)__-F z<$Eo(=goV%MhO#l!bm2F(F%pIs_!UY3eCAfp6E!ouYa&EUO|XuEOM*HC;*}Ph$Gz& zew5c^aTO5M?dF6HmZP2$Rq2YY6-Tl`3kE9M^KU`kF9Em+b1N1@0s@C2;K}H4W+#Fl z6ZjMeC6>q)2uPJXR>@Js04MR98_ziHv8;TWk+T3o#l(RoO988h#D@6sm=HrnMWd>k zx+cw9On)qznwi^5e#OP3tDCzg&t8(Gm^4}PltRj>WX^)*sM)gTlyfcxlNKl!JY6uL zl*(0VtXi#lO*Plj5I!w7ZPvV{&~huCyL9Arq%uTaJ+b;0Shh=E`n#5(C__aC_@ZXw1$;>HhhVWImDa$%wSg4~bXKB3mv+ol~7 zHlg)5Tuwb?u2*9!oQfHosQ9h7$08$Y|aTc@bHWQpKyly;tLc-07C3BGr$bswzH|c4vKtUu}3b{BHXedJDaUzEx=A z`y+>c0cqxdB_!Zu#{d8Ug_Fn#9DiG-QmPJiDB_Tz3W5bu5l5{;5h{dQp;ZTyOTVB= zLz3d+D7Y3J{8+3yxH#+T;3^1$A0SSSPKqv4;{TFDix>}%`|kLm3rVh|{W(Vj@lZ2@n6E;}^*# zldB9yjs;YqLUR1zfAG6ovwtu-;UK=+Gne+&b`U7%UF?eAmTZk_;vXW&X}`>PFL z=9Bb#TZmKj!>Fn*_Gp+u90A_G!+Av1Ik`>_NM(tj(N$d7;~m&B9vJy@1~0{r9ftbn;!z@vg$K_Yh$i2y!8 z9Mq0MlzRY>t6i)ma4i|NB*#Ei1h>f3-KhwBj{#9b7qR9DSVgi)vN0A9nkrj~dG)+n z1XDyH&2etkm1$q9J?;M3k&uXNpnJ5Pk%$B!E-8ezL@I)bG@D4QMy`6)uKwnn^GXbz)?SS;X0PsP@K< Qq5uE@07*qoM6N<$f}MKYkN^Mx diff --git a/img/icons/indymedia/logorss.png b/img/icons/indymedia/logorss.png index 30e55f8c1c6077ee7d2977eebf211ee7504249c9..38708c5d9f19506219d861623a672d31ede3ba9f 100644 GIT binary patch delta 6151 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 Z*)R1vFvS8)iuV8j002ovPDHLkV1j_MxUT>J delta 4827 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 B9sB?Q diff --git a/img/icons/lcd/logorss.png b/img/icons/lcd/logorss.png index 312f8248f3a6ead4d01a670caaa200273fbe8a86..8ca232194b2cef8d49e850f0fe4cbe011d5ea90b 100644 GIT binary patch delta 3666 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 delta 3779 zcmV;!4m|PfKD0iN7=H)?0000b3+9Oc01F0sR9JLUVRs;Ka&Km7Y-J#Hd2nSQWq4_3 z004N}tyyW3!z>Q{&ne~zW)XtpAeO4R!5n{{z;@h;)17=XNhMBu!JxNDa-062e-HC7 zyn^Fk0&$8t#tpBKLv{r}(zae@e%md~@A(S*A^iPgzc~*$?0@pu`t@gu`}xOydk(NR z1;6MrP^$X@`FSAkcW~)xp#4T(XXNms?E~d~An(=zyAM#-vbLBLUzZ?#ZQHSuv_3|^ zJkPPuL7x8(jOaDOPVZdz)^GTB-mMAv`7yref%)xlqVtS?(FM%=O6py*v9j6j?Wk^nKAlWT?r8( zjr*dGrOaPAQ4n%JFN=6HK7`*q-x=>@z}^aU+EzEgyV`6WqCae~%@#XspWA9HrWjmz zjIDFk`MEE7cG*=$%>p63?Qra~Z$1ID3V1#Cx^pge-hVgG+f|!z<7RQDvlz@O6^wrU zKi+Q#8bc{>?Uv41VO~y2h-oNt`i-*yh#d!yXa@LrKfe@L4+Lq3IbnfilQo2p-MOVa z@)Kx}6A9ZT3(IXE03pJ?36rrN2y7ALx6y9w99xSxCg4*rP=X$u00K+o8fNK&_YvSE zr}xG+Y=7suj@;S}&jJwSqh~;qeF0W#Y~T-v1P&GQ6o@DiQ=&{I`WPa`7&)r>74{sM zI5KnM%q9605~Y|pC6jV0*<}xNq#QHnlyfcxH!T>s;Oc@CN)absf{;XENs^^h(5Hq< zHCC?4)Lct*%{TZ&i;Y`qxs`4^sMJHJ9y|BcbAK-bR2zPTK_dl_u4yquBS(1X&tRe;k zL${8GJ+XTr_X9T*!vBPuJCHL9-TwnQqtLxU?iIHi)S70Sx_L7ZQW!gW6SX;MQS$`CpBW8@3u|$+S!~#NPK9`sLu{=L&KbIGg z#gUHKccDf&7u+jVKts`Nt-z(y5vqw{`eSR-hbN}ZH(7&^d~=d{xeANw4mq(_k7zV- zuGhT6SdigzuS)eO4S}@2XnbI>-qhe>KDeR}@jMEGV!`K$N{%ezbA=u>Fd=FzNqNvIkCDCAnLulEdwYbDnh7q@LP-qubyv3z_(!)< ztAyYt;av2(62-J2(esI>|6vI3u|AV&J&bvZQTMc>7{&{y0J6KvZ;vz1ku0m*U{DeR4-Bn+55m4u3IrP^kga zh{%1l`zlV!AFFz8=-&+l^MPAb7|F1_j3n8Pl4Ft|O~wnCU%ds)zSzrQF3VdIqTBLY z@Q^Al@R0th2?81S&@GH**X=ZxrCT)?=vIv-8t7c0TiP;1w+!iv9DfNxqq8Rjjn0`6 zG&(GKiEG7FOnH-K5!5FU{H_7hOF%1tl%w^-UX@U|P(k=$f`)qM!3s4-S01RpG5lzS z+7etTM64OrABcy@FOPii={n9FW$|f_(@dI1i7bPPGNB+;o5ZyDkPD=Lc+<+Ou$9{g zns*^A^p$lEqcPW8Lx1jn8tQLFlew*$mP9@#!GOD(oCKcHJA4QQ$fmBV6O{C&Z<|AS zZ+-hky&AA{oKwB#lrv6OQen@b*aQIbr|qED>0*)2O__?#a}v~ zGWXH-G(?+z1Oj$pRQYQr+r#hSu{EIj%t_5o2yx6#Ql6`vp)i0z`&DNBY< z$EE4I%mZsktViwa2@KXfFdKA_Z7%;JqSxx-xpwdgbvG)#+{kF)sG-Y{%i_OnwY2dQ zW%4g(t#wwn1Yb87d8vKv&37j#kw97NWsZySeo$EDDHl$6gciWMfh46Q8w~IrSFo9n8>`) zcNMojhJRM*_33WzdL2MIx;8L(@7iWEU0s_J7hRhY7fG->@&&2gwmA?SH!==c5*Ll+|i(Bdo?PN{C%fo90V3c-XF;i8)}F4Q*{lf>gdTZaGS>&G#U{xU{nX z-99ZT%~MVJT2g8^33}g11}=|51H=OM8JF8iHh*74S-k)ynA7sfRc&6^W1rhK&n4H< za6gr3w=Cy^Yua9xY2KI4;Os1wQU+*Ku6_e&Bu;4vs^H=^i|Iep$QhnC61MqxK}&Xe zn87!X0#HMK-@s@E!~rr2NS86p($%cmHEgRDo>kw}am7potS}E|6 z@PCnKPu!scHA~PR#FI%ptbnR{1V_u-b4pLvemeWsea|SFeTL!id~P98FE_X@{xg^U zY0~Bq7Tw|nR*ac6!x5Y+mpk1o6NIxK9i%?BiWbX=ji-5r(o3~GZ1?KN>9Kka4{FSs zS2@NLTQh?&Lx4!E^fuJMGSWnrME%j1xPLdeOeLCoDWWJYz<%qzKb57UxtYmUFMSe~ zfvU7tqfDx*7B^KD?alTK>2*Et8zi&W44=10Co}Y)zK^gNjf?eH8~@?rS*(l-cLK-# z!5IK)pDAPh31<$N?Yv^|A_ebx;Q}|fQaq978chAUuK8QL}GS8H(aVD4g=3}NX)bF--+KR&AdcB%T)dj4uq z#b{oPDHN+uBxpMFg!jyjH$+}MzA+R!=4Yi7t5#2_$(%O0nBWS{_C)SUi2Ng7=F115 zZ@1qKG?ea;!+!y=uM6ZgPXAStnF}EsMJZJWi-KF$!-8NH+xR}YVie0Y=A&g!OK$Mwf%t=xbzT@j20lwbFc$WXUKSz(6 zvltK%iD#K%+Qb{g)0?)zd7n7Kin2<4PCRDP1&JTIu6X>$x!|(EGb3g?HBTHN77J~x zv@t818u1iyRMm9K7cw5JoVPe@5=1DdqJ%PR#Aww? zv5=zuxQ~Cx^-JVZ$W;O(#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@Or{kK3Z zXwB=bxsTHaAWdB*Z-9eCV6;Hl>pt)9Ztv~iGtK^f080&Wmvyi9Qvd)ETeB?<{sJUo zG&yBCVl-qeGcjc{Ei_>@Vl81bGBPb>H!?LaF*IXlHDh6uPY{s`GBz|ZIWjOaG&Hm7 z5KIHJ1{GEb7ENEQF#rGn24YJ`L;(K){{a7>y{D6te;y%!4j2Kx<00Fp3L_t(& z-tAh;l>#vgBYtzG1)AWrJ<$PYI-o)-;lPERPsSg~$?PUK-eZhpS&1E{Tz}4|BQH4} zd<_Dv*Yy42cE2pY@qD}vLtu3+S#?Eeay&Dm)Rqsx1FMDCwE}ex$%6c?UFe8va1}_f z2fc)D_Y0MOMo~6%aS)_(D!y*i$A`f|GSkVZr2?4;9LP{=U`*^lgHofwCo&VVxDNPj z)PyOnL*dQDdTA1;3P}gCf^dsN z(m{N=^fBqE7vgmg)|fUAq@7>-Xs~kt+d=4~ArG>D3`}$gq=C|eL6yTJ5gL|Pa6maCS{s=>ynSYuofby|Ka@IpTBwcyev8F9#6^Xlg t=}#&sSAkm5Z+#z?g)-iq9t8e5fj6)w-6Ynd!UzBW002ovPDHLkV1jty4-5bR diff --git a/img/icons/light/logorss.png b/img/icons/light/logorss.png index 30e55f8c1c6077ee7d2977eebf211ee7504249c9..c5fad44cbe9016599ce44beb353fe70701c02c10 100644 GIT binary patch delta 6152 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 delta 1519 zcmV^CMGD|Jhi|-NwC1w%X~vla5Gq9sXsR@@ih~XL zW7&~IO@BqRsuneMt=fu|T(p#8B@Vx4V%gM+nYmSKE}mVzxVa~<-YQqY;i$D#t6pnu z4Ne=B8{Tf1(Av(s?0MI{?6&(}_tud1Own+!MEy^B-~J2e}mJ{sy@e=)NKM zncElCM*Q0JP0}J1pJ4=b6MTM{aCl>H@uT+3ZTj68-a>Dox6oVYE%X+83;lnDCjLBB z{C`RR59~>zx<`qxAOHXXgK0xUP)S2WAaHVTW@&6?004NLeUUv#!$2IxUt6V8st#rl zamY}eEQpFYY88r5A=C=3I+$Gg1x*@~6c! zA{5k6Mg=zFwCbc-NYj48$3NuyC2}d`Dua<@0aa*_T|f9A{O;B&Oip-7(FD-_;y528 zKzJ8u)*a{j*m0UCKw3Vt|3x4z`-FfTBPiCpLh3k_V(|YR)0TcXmX^Q#M2P=X(IOP(jG&?9y@$2=-3B;kxEe_6-&l;=V%zX zS1~bPwGZZ-qLM^MDOmd6B5032mZx@}l(ZxEO1ghu6fY}C=h`o1K<;6Ca1VxZV<<9m zgY@8vTwHu`cMRl~CU+w4MsD)*2hg8bi2KC_qz9xsFboxt2i)iek~dIfRFuOnzAw$0 VtGqbWQpf-R002ovPDHLkV1maX!rA}; diff --git a/img/icons/starlight/logorss.png b/img/icons/starlight/logorss.png index d355e1654468d33c93004034ca47bd927ccd58ed..90ab9a7e8289e5718816687a0b638b784e6f170a 100644 GIT binary patch delta 3641 zcmV-94#x3aB^>EX>4U6ba`-PAZ2)IW&i+q+SQp^cHBA= zME|jhUIH-#%fWa~@1U38Hvy6=RjFk8**{NZ6)6rNkP#UfNvrX{|4#KEe%#@baz3Vz zLpbsudgeXk#Pp5pIM$JKqpaLBT?`_B~4>wmA~`W|3!3VZ%xpw!M2 z_4A2*KEb4?fzAh6nUP_qp9AG{AZzvseGX9evW}2bd|W~ud)u#-r2VVp%f>bKJ;?Ll zE)u=&IcSX;R@;MbW6hp`UBAi}IkCPC7it^VzI+>dEtPX`zCWufOqmJJ6g$d99Ch z$BKCwAtBQch;Mj;gMZHTQ2l$cUtJ! z&jAP#&-P44TM#&cwq6H)L}Q}X#4&+SVW4EJGZhH5_og#TA;y?ujwRXT zl20MUlvK*8WRor8=yS+1r<`*sJhU)!;qJl(rBqXGb=B8UV@);J(x6Xs&9~5EORD8o zy6Lu~6Fv0UQ_sB&J)qKX!;diHNF$Fjq1v?5O@BYbj5EzV%Z-{=8`hW4&r#DCYGxrN z7seYkZjJF8LW`Vu!3@NV(;yxN0TD34>`2-0jFz03~>FgqyjLQv%)JKu!sCZ;*S%?F-bpRGW5KwGfgtl%O7#^y7k&ib+L^+K;#D zcYkmAw+%GQ5!SKc?8xalKkb+^`C#YtI!@x(a-dqYkzS6geWG?5sg9U+>(g6Qztx$l zZLu~thfZ>Na?uWskDO;nAh-vqrst|+7=0r9vv%Jm&>t2`nv#2K2F}3|(lrLpIqtX2 z!H;ICCi7@Kjny-}LA#sElMSF0bf4|PW$;D03g zJNPaF;|_l7a?`9CObL9Ul(%V04LU?u#*D5O&Z!-rfLQ%mw8{PzZNC|kV?`>k5_3@E z#+bvd050Z>Zlq$n2v!tE&KRi(6MNHfBRKXvWo>Q9Jq7v#KwI|*?cSd8x}nZwL(L8v zsydGl%gKNP#eIkSuzfrS^dX!u`+qf}09Z#t=qvhdp+O|PWM1~DCMXtGzd$CrYp{*_ zWRxU2(>A}mt>uu}C1^&Ou~StaX9=;xR+L)SZ(5CGNXGRREPK%jjk%juF^x@MxWpE1 zcks{`t1N@+QrfT$?1~<75`L$@i^o^tP^>kV7&}=lr7-aTa^(#9TK!s#-G4EY$c#iz ztOv~{P~B53qS^{Y0<`MUCz5TpvGbiVzV)-pg8bMkzLSd!bTD$5vcf`|w zh43#@20~Ft>08wB_9)ZQz!V!|-5MyI>L#X-hx~G@_RdyVcB3I>+VgcW&3LO&BH2$B z>R>ZAii*hEAYhLXQS(xz5Pz@K>fSpJGRIaSBE4@V5l3xpCmkpdLAn9t2_6m9fZVQsz0oIJW*^#l$>V{0BETQ@**!4j6L#8|-igClBx%)P_8-K?`82+$izuWF$ zL!qhdJkwU$Syvj~i90_fZoCo~hDX3cov{*L@7Y$h^zcC;x0e^@DT?bx`qt?$Sp)>6w+16jY;1{;rk${ zpF}~1=Z|mwmRiquhkq33%roencX7^AoK1Rjyf!hcX}FXLgg75HF;=u}k}i+gvT}u9 zLqLf?iJ^K7BAh06DCS7sSuI(Mw~mSp?^5jcSpi{7aPZJgOPdUiuwq^ zJVRUlMB)3U(`&xVWZypHCkmH6mw7U@9SR+)xOgY|C;>bvhU2*V6}=sAKfWv5>TDif z%JwL3Ol*eq*vFD*41e2F7QV2o>e|`0G4|KhpNns%*jg*zC>$nF8h&~eX)oeJgcPy1 zepE6h^M4}K4VKU>IJBrKi_jk~UL+;UD-C7_VLk+0UTN3|4M*lq2|uo(?-jkSr*&LXq$8?sb> zyz3-S5F`QB&Q40=N{ur=uXd8`yqd{$Wq-Q#Tb`ms`F19*FrqJq+Vnv}o*%Y`mjt|3 zU`zd_1M`y(M0E+lGJZ?1^~$6h=^KmPf}Usq^oa%?c`;h6YH-QT`v8Y5X1AkA=7XaB zXMT+teUt=CMfH|Yj|g&6M~Wk^a0#Hao#JRdTP;aukBg9N)Ws}WjC3$%FJ|>Gn}1lV ze}P!~?mgFBWzsDaGYa~bqiTa@W|))s$JxI$a^|V^5GquP%9{>HiQr!m!SH6Ll16Fr z{}3H@boFcze$^qUH!VUqHd*S~Av`q*^@SYjdv;vp$h$dy@GT-ymv33M|HHb}%et^g zM#Z(DlRh*?(iqC^k&gOFM7N_ALtCp}9qsiV<~Qn^Yh5Avuy*iFoxJykb`=RZMVa04 zhB{~h+P#XR>;&bRo*tQ+2eol><OEDhhTGamY}eEQpFaY88r5A=C=3I+$Gg2TdB16c-nOVl1BqiZHzU~p=`(2D@`Pcn9deof7fPa8UJj)EzCf*>P+O!SM z`@|7elvUz$;!%?>Nc_lk#p5^51(yY$88OqTdEyAMSZHIVjakvuh$o4os-{!EknvdM zyv127S6TC({DtA1zOu}9T0=-+5lfIDLO~TJlwl)AyH1LQ6rIO?{DZDvB9}t05*Rra zP=N;7^@IPx?|Gn4P0EeG-VIC+yMrj4B3=jDM(Am=YjV#`lc+cG2uuT$8w*?{WHe$j zVP!QkEjMO4Wi2#jGBqt?F*0N=Vr4TjH!@{2F)=b?vzrU12^_yMR*e7v01Qw}R7C&) z00000y=H6uzh`8BW8ITl6+3?r5gBUpe2oAA0HsMpK~y-))sjIH#2^R+DULkFSNar= z7OzYRx*2dcdyY3dVXB9O=tW1&4qyl971#kr1$F?%p;l&sP$gPj@(kG*|71%HA+lmW zA<|=JPyf=u?`UJd70`E8%wP=$6Oq%dS00000 LNkvXXu0mjf6T#Au delta 1567 zcmV+)2H^R^K7uWfBYy)RdQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+U=HIlH4i` zh2L4lU4kVcB$mT(Rd$f&=YZY5)18^hR3(pX%0LTz0DZFBVf_3t!+*FaXAe=$Q%>o? z6?4p7(DCEE%C6~%yWUs4hVc8z9_|5#Nzlsq)6+HoVW(T*Y<~)R`a#IsHBg@i^0fl{ zi_o=@tQ|2r{SwNTknlM`mq0m>b)@X}@d$OCZGW7^&pWJ2Y-`^lufLAo~td*1)bA6G;?(-WHa}QQ?r!{B(TSRd}Ux_vj1z3lvGGJ;3nn%yhS(+DSim ze%LPlu(Gs!<$otT=%&}>JnlKWhdIZ2x)Kvynyy1Vj8N~yl zw}DPS+EGy&Wny@LQlw6WCXL-TYS?1r!G@N(v22@*aptzDk;GMKvPqUM4q6R(1y;WI zLVMl3muuu$xD#eFL5x-?gjM~E@}rsQwpJX;1}&JVXwTPztX~3f5!P00h6DtTgy5${hcO!w{OI6Q zAml8OD-e(>ckGg*NCF(^wKkqH+H+a?G$UsLgh~6O={|z zwIoR~X@5$|(n@~C#GA9<9CP@6L2)S0GD zpLv#>8if<c=s}G)sWs7VYIxM_Awesh=zn4cVjKy?Z4p=rY%x2WV&t&UEoMh% zycNpWr7k#~7BLWvgIFis?EWYB#4W`5SKRnPE-ZBaf?QbWz99EIw@;|`^>5P-37gP* z4VP07!Rx1my>QLPEa}*NB*8zn;ky&xLT{nB&|7GwF0QI`v^J$qlaXc)xvm_Q@U5C> zl7CTE@WrE4n`%dru2Bie7g>r{@9Q@FX~K8gx6oVYE%aAH__psk`~u5!gg0O;Y8C(h z0flKpLr_UWLm+T+Z)Rz1WdHzpoPCi!NW(xJ#a~;cQmPK^AmWgrih~7F5l5{;5h{dQ zp;ZTyOTVB=Lz3d+D7Y3J{8+3yxH#+T;D0Izf*&AGj!ud$QsV!TLW>v=j{EWM-sA2a z;BQozYIcnSs%9DKWJ1X1R)ydzLI|S|5sXUA)aOJo1<&zy4K=+Gne+&b`U7%UF?eAmTZk_;v zXW&X}`>PFL=9Bb#TZhO4E_QnI5#mj zFk>)eEn#M3G%YkZVKpsbW@2P5V>x7EGh|~hGcz+elNu0_3Nkb@H8VIeI59c1wGd1L zv(Xe*2^kTWAkA@Z)s<;qsy*%g*pZNkY@mCzosozHATB9{wnQ+1b^_jjSY4t=Wp^>MTou52 zZyLk<>l$+aT0jCL0rL}>V(JSR0`Lwr;=P@Nu0UT`0y6=yU+sYOz2ppkIWLQncBuBo RjiLYm002ovPDHLkV1j%2*|Got diff --git a/img/icons/zen/logorss.png b/img/icons/zen/logorss.png index e1991047eb0da4c6581c4c7000f8f9ed26a75ee3..64257681b41a880edb5453f96e8d4dd9a3d216ad 100644 GIT binary patch delta 2655 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 Date: Mon, 12 Oct 2020 17:24:44 +0100 Subject: [PATCH 253/263] Vertical alignment of icons on mobile newswire --- epicyon-profile.css | 89 +++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 4925b4fd6..cccbdf5b2 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -957,40 +957,40 @@ aside .toggle-inside li { line-height: var(--line-spacing); } .newswireItem { - font-size: var(--font-size-newswire); - color: var(--column-right-fg-color); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire); + color: var(--column-right-fg-color); + line-height: var(--line-spacing-newswire); } .newswireItemModerated { - font-size: var(--font-size-newswire); - color: var(--newswire-item-moderated-color); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire); + color: var(--newswire-item-moderated-color); + line-height: var(--line-spacing-newswire); } .newswireDateModerated { - font-size: var(--font-size-newswire); - font-weight: bold; - color: var(--newswire-date-moderated-color); - float: right; + font-size: var(--font-size-newswire); + font-weight: bold; + color: var(--newswire-date-moderated-color); + float: right; } .newswireItemVotedOn a:link { - background: var(--newswire-voted-background-color); + background: var(--newswire-voted-background-color); } .newswireItemVotedOn { - font-size: var(--font-size-newswire); - font-weight: bold; - color: var(--column-right-fg-color-voted-on); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire); + font-weight: bold; + color: var(--column-right-fg-color-voted-on); + line-height: var(--line-spacing-newswire); } .newswireDate { - font-size: var(--font-size-newswire); - color: var(--newswire-date-color); - float: right; + font-size: var(--font-size-newswire); + color: var(--newswire-date-color); + float: right; } .newswireDateVotedOn { - font-size: var(--font-size-newswire); - font-weight: bold; - color: var(--column-right-fg-color-voted-on); - float: right; + font-size: var(--font-size-newswire); + font-weight: bold; + color: var(--column-right-fg-color-voted-on); + float: right; } .imageAnchorMobile img{ display: none; @@ -1545,6 +1545,7 @@ aside .toggle-inside li { background: var(--main-bg-color); width: var(--column-right-icon-size); float: right; + margin: 20px 0px; } .rightColImg { background: var(--main-bg-color); @@ -1553,40 +1554,40 @@ aside .toggle-inside li { padding: 0 0; } .newswireItem { - font-size: var(--font-size-newswire-mobile); - color: var(--column-right-fg-color); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire-mobile); + color: var(--column-right-fg-color); + line-height: var(--line-spacing-newswire); } .newswireItemModerated { - font-size: var(--font-size-newswire-mobile); - color: var(--newswire-item-moderated-color); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire-mobile); + color: var(--newswire-item-moderated-color); + line-height: var(--line-spacing-newswire); } .newswireDateModerated { - font-size: var(--font-size-newswire-mobile); - font-weight: bold; - color: var(--newswire-date-moderated-color); - float: right; + font-size: var(--font-size-newswire-mobile); + font-weight: bold; + color: var(--newswire-date-moderated-color); + float: right; } .newswireItemVotedOn a:link { - background: var(--newswire-voted-background-color); + background: var(--newswire-voted-background-color); } .newswireItemVotedOn { - font-size: var(--font-size-newswire-mobile); - font-weight: bold; - color: var(--column-right-fg-color-voted-on); - line-height: var(--line-spacing-newswire); + font-size: var(--font-size-newswire-mobile); + font-weight: bold; + color: var(--column-right-fg-color-voted-on); + line-height: var(--line-spacing-newswire); } .newswireDate { - font-size: var(--font-size-newswire-mobile); - color: var(--newswire-date-color); - float: right; + font-size: var(--font-size-newswire-mobile); + color: var(--newswire-date-color); + float: right; } .newswireDateVotedOn { - font-size: var(--font-size-newswire-mobile); - font-weight: bold; - color: var(--column-right-fg-color-voted-on); - float: right; + font-size: var(--font-size-newswire-mobile); + font-weight: bold; + color: var(--column-right-fg-color-voted-on); + float: right; } .imageAnchorMobile img{ display: inline; From b4226d9e7dc2c109b470ee6825d43dce3d3153fe Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 17:30:56 +0100 Subject: [PATCH 254/263] Date format in newswire --- webinterface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 5d134953e..ac2234455 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5511,8 +5511,9 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, """ htmlStr = '' for dateStr, item in newswire.items(): - dateStrLink = dateStr.replace(' ', 'T') - dateStrLink = dateStrLink.replace('+00:00', '') + publishedDate = \ + datetime.strptime(dateStr, "%Y-%m-%dT%H:%M:%SZ") + dateStrLink = publishedDate.strftime("%Y-%m-%d") moderatedItem = item[5] if moderatedItem and 'vote:' + nickname in item[2]: totalVotesStr = '' From 2d38065e42ec90da3615efd77e84350a5a08a818 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 17:34:56 +0100 Subject: [PATCH 255/263] Date format --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index ac2234455..da9dc37e2 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5512,7 +5512,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, htmlStr = '' for dateStr, item in newswire.items(): publishedDate = \ - datetime.strptime(dateStr, "%Y-%m-%dT%H:%M:%SZ") + datetime.strptime(dateStr, "%Y-%m-%d %H:%M:%S+00:00") dateStrLink = publishedDate.strftime("%Y-%m-%d") moderatedItem = item[5] if moderatedItem and 'vote:' + nickname in item[2]: From b04d37c21912e8e7d99382dbba5423597f5d9726 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 17:40:00 +0100 Subject: [PATCH 256/263] Date shown on newswire --- webinterface.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/webinterface.py b/webinterface.py index da9dc37e2..9e34d999f 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5513,7 +5513,10 @@ 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") - dateStrLink = publishedDate.strftime("%Y-%m-%d") + dateShown = publishedDate.strftime("%Y-%m-%d") + + dateStrLink = dateStr.replace('T', ' ') + dateStrLink = dateStrLink.replace('Z', '') moderatedItem = item[5] if moderatedItem and 'vote:' + nickname in item[2]: totalVotesStr = '' @@ -5529,8 +5532,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, '' + totalVotesStr if moderator: htmlStr += \ - ' ' + dateStr.replace('+00:00', '') + \ - '' htmlStr += '' + \ '' + \ item[0] + '' + totalVotesStr - htmlStr += ' ' + dateStr.replace('+00:00', '') + htmlStr += ' ' + dateShown htmlStr += '' From 53d9cadfdfb94fbf7a982e44dbdbf960722f626b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 17:43:59 +0100 Subject: [PATCH 257/263] Date shown on newswire --- webinterface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webinterface.py b/webinterface.py index 9e34d999f..510056e26 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5539,7 +5539,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, iconsDir + '/vote.png" />

' else: htmlStr += ' ' - htmlStr += dateStr.replace('+00:00', '') + '

' + htmlStr += dateShown + '

' else: totalVotesStr = '' totalVotes = 0 @@ -5568,7 +5568,7 @@ def htmlNewswire(newswire: str, nickname: str, moderator: bool, item[0] + '' + \ totalVotesStr htmlStr += ' ' - htmlStr += dateStr.replace('+00:00', '') + '

' + htmlStr += dateShown + '

' return htmlStr From 0c3ec272b1237e66733dfa858811a0cd40eb1c86 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 20:41:53 +0100 Subject: [PATCH 258/263] Mobile links screen --- daemon.py | 24 ++++++++++++--- epicyon-profile.css | 12 ++++++++ webinterface.py | 72 ++++++++++++++++++++++++++++++--------------- 3 files changed, 80 insertions(+), 28 deletions(-) diff --git a/daemon.py b/daemon.py index 4b0ae2af3..c5a815729 100644 --- a/daemon.py +++ b/daemon.py @@ -143,6 +143,7 @@ from webinterface import htmlFollowConfirm from webinterface import htmlCalendar from webinterface import htmlSearch from webinterface import htmlNewswireMobile +from webinterface import htmlLinksMobile from webinterface import htmlSearchEmoji from webinterface import htmlSearchEmojiTextEntry from webinterface import htmlUnfollowConfirm @@ -9083,19 +9084,15 @@ class PubServer(BaseHTTPRequestHandler): 'permitted directory', 'login shown done') - print('TEST1 ' + self.path) if authorized and htmlGET and '/users/' in self.path and \ self.path.endswith('/newswiremobile'): - print('TEST2 ' + self.path) nickname = getNicknameFromActor(self.path) if not nickname: self._404() self.server.GETbusy = False return - print('TEST3 ' + nickname) timelinePath = \ '/users/' + nickname + '/' + self.server.defaultTimeline - print('TEST4 ' + timelinePath) msg = htmlNewswireMobile(self.server.baseDir, nickname, self.server.domain, @@ -9110,6 +9107,25 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return + if authorized and htmlGET and '/users/' in self.path and \ + self.path.endswith('/linksmobile'): + nickname = getNicknameFromActor(self.path) + if not nickname: + self._404() + self.server.GETbusy = False + return + timelinePath = \ + '/users/' + nickname + '/' + self.server.defaultTimeline + msg = htmlLinksMobile(self.server.baseDir, nickname, + self.server.domainFull, + self.server.httpPrefix, + self.server.translate, + timelinePath).encode('utf-8') + self._set_headers('text/html', len(msg), cookie, callingDomain) + self._write(msg) + self.server.GETbusy = False + return + # hashtag search if self.path.startswith('/tags/') or \ (authorized and '/tags/' in self.path): diff --git a/epicyon-profile.css b/epicyon-profile.css index cccbdf5b2..71f57ded7 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -1541,12 +1541,24 @@ aside .toggle-inside li { font-size: var(--font-size); line-height: var(--line-spacing); } + .leftColEditImage { + background: var(--main-bg-color); + width: var(--column-left-icon-size); + float: right; + margin: 20px 0px; + } .rightColEditImage { background: var(--main-bg-color); width: var(--column-right-icon-size); float: right; margin: 20px 0px; } + .leftColImg { + background: var(--main-bg-color); + width: 100vw; + margin: 0 0; + padding: 0 0; + } .rightColImg { background: var(--main-bg-color); width: 100vw; diff --git a/webinterface.py b/webinterface.py index 510056e26..0a116ee61 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5381,7 +5381,8 @@ def htmlHighlightLabel(label: str, highlight: bool) -> str: def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, - iconsDir: str, editor: bool) -> str: + iconsDir: str, editor: bool, + showBackButton: bool, timelinePath: str) -> str: """Returns html content for the left column """ htmlStr = '' @@ -5414,6 +5415,12 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, nickname + '/left_col_image.png" />\n' + \ ' \n' + if showBackButton: + htmlStr += \ + ' ' + \ + '\n' + if editImageClass == 'leftColEdit': htmlStr += '\n
\n' @@ -5659,6 +5666,43 @@ def getRightColumnContent(baseDir: str, nickname: str, domainFull: str, return htmlStr +def htmlLinksMobile(baseDir: str, nickname: str, domainFull: str, + httpPrefix: str, translate, + timelinePath: str) -> str: + """Show the left column links within mobile view + """ + htmlStr = '' + + # the css filename + cssFilename = baseDir + '/epicyon-profile.css' + if os.path.isfile(baseDir + '/epicyon.css'): + cssFilename = baseDir + '/epicyon.css' + + profileStyle = None + with open(cssFilename, 'r') as cssFile: + # load css + profileStyle = \ + cssFile.read() + # replace any https within the css with whatever prefix is needed + if httpPrefix != 'https': + profileStyle = \ + profileStyle.replace('https://', httpPrefix + '://') + + iconsDir = getIconsDir(baseDir) + + # is the user a site editor? + editor = isEditor(baseDir, nickname) + + htmlStr = htmlHeader(cssFilename, profileStyle) + htmlStr += \ + getLeftColumnContent(baseDir, nickname, domainFull, + httpPrefix, translate, + iconsDir, editor, + True, timelinePath) + htmlStr += htmlFooter() + return htmlStr + + def htmlNewswireMobile(baseDir: str, nickname: str, domain: str, domainFull: str, httpPrefix: str, translate: {}, @@ -5674,31 +5718,11 @@ def htmlNewswireMobile(baseDir: str, nickname: str, if os.path.isfile(baseDir + '/epicyon.css'): 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' - profileStyle = None with open(cssFilename, 'r') as cssFile: # load css profileStyle = \ - cssFile.read().replace('banner.png', - '/users/' + nickname + '/' + bannerFile) + cssFile.read() # replace any https within the css with whatever prefix is needed if httpPrefix != 'https': profileStyle = \ @@ -6047,7 +6071,7 @@ def htmlTimeline(defaultTimeline: str, leftColumnStr = \ getLeftColumnContent(baseDir, nickname, domainFull, httpPrefix, translate, iconsDir, - editor) + editor, False, None) tlStr += ' ' + \ leftColumnStr + ' \n' # center column containing posts @@ -6222,7 +6246,7 @@ def htmlTimeline(defaultTimeline: str, # the links button to show left column links tlStr += \ ' ' + \ + usersPath + '/linksmobile">' + \ '| ' + translate['Edit Links'] + \

From 3b8398f002f93afec659a56b44b44308238661a9 Mon Sep 17 00:00:00 2001
From: Bob Mottram <bob@freedombone.net>
Date: Mon, 12 Oct 2020 20:54:09 +0100
Subject: [PATCH 259/263] Extra div

---
 webinterface.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/webinterface.py b/webinterface.py
index 0a116ee61..30a8af232 100644
--- a/webinterface.py
+++ b/webinterface.py
@@ -5417,6 +5417,7 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str,
 
     if showBackButton:
         htmlStr += \
+            '      <div>' + \
             '      <a href=' + \ '\n' @@ -5699,7 +5700,7 @@ def htmlLinksMobile(baseDir: str, nickname: str, domainFull: str, httpPrefix, translate, iconsDir, editor, True, timelinePath) - htmlStr += htmlFooter() + htmlStr += '\n' + htmlFooter() return htmlStr From 39df0b8b7195524719c5fd3f10fd2e3334c058e6 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 21:00:39 +0100 Subject: [PATCH 260/263] Mobile links screen style --- epicyon-profile.css | 17 ++++++++++------- theme.py | 1 + 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index 71f57ded7..aee9896d0 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -78,6 +78,8 @@ --column-left-header-color: #fff; --column-left-header-size: 20px; --column-left-icon-size: 20%; + --column-left-icon-size-mobile: 10%; + --column-left-image-width-mobile: 40vw; --column-right-icon-size: 20%; --newswire-date-color: white; --newswire-voted-background-color: black; @@ -1543,22 +1545,23 @@ aside .toggle-inside li { } .leftColEditImage { background: var(--main-bg-color); - width: var(--column-left-icon-size); + width: var(--column-left-icon-size-mobile); float: right; margin: 20px 0px; } + .leftColImg { + background: var(--main-bg-color); + width: var(--column-left-image-width-mobile); + float: right; + margin: 0 0; + padding: 0 0; + } .rightColEditImage { background: var(--main-bg-color); width: var(--column-right-icon-size); float: right; margin: 20px 0px; } - .leftColImg { - background: var(--main-bg-color); - width: 100vw; - margin: 0 0; - padding: 0 0; - } .rightColImg { background: var(--main-bg-color); width: 100vw; diff --git a/theme.py b/theme.py index 9f2511316..e236af711 100644 --- a/theme.py +++ b/theme.py @@ -261,6 +261,7 @@ def setThemeIndymedia(baseDir: str): "newswire-date-moderated-color": "white", "newswire-date-color": "white", "newswire-voted-background-color": "black", + "column-left-image-width-mobile": "40vw", "column-right-fg-color": "#ff9900", "column-right-fg-color-voted-on": "red", "button-corner-radius": "5px", From 2b7ba3bdc920b04f60088703fff229b19bbd5523 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 21:18:58 +0100 Subject: [PATCH 261/263] Links textarea height --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 30a8af232..01ee5c2af 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1272,7 +1272,7 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, translate['One link per line. Description followed by the link.'] + \ '
' editLinksForm += \ - ' ' editLinksForm += \ '' From ad29df38f5860a42b75fb640c55fab99e303319f Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 21:21:19 +0100 Subject: [PATCH 262/263] Links textarea height --- webinterface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webinterface.py b/webinterface.py index 01ee5c2af..849313626 100644 --- a/webinterface.py +++ b/webinterface.py @@ -1272,7 +1272,7 @@ def htmlEditLinks(translate: {}, baseDir: str, path: str, translate['One link per line. Description followed by the link.'] + \ '
' editLinksForm += \ - ' ' editLinksForm += \ '' From f79689800d21402c5c4e28c328bf6e606e60ec58 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Mon, 12 Oct 2020 21:43:55 +0100 Subject: [PATCH 263/263] Mobile header size --- epicyon-profile.css | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/epicyon-profile.css b/epicyon-profile.css index aee9896d0..d691a90c3 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -77,6 +77,7 @@ --column-left-header-background: #555; --column-left-header-color: #fff; --column-left-header-size: 20px; + --column-left-header-size-mobile: 50px; --column-left-icon-size: 20%; --column-left-icon-size-mobile: 10%; --column-left-image-width-mobile: 40vw; @@ -151,15 +152,6 @@ h1 { color: var(--title-color); } -h3.linksHeader { - background-color: var(--column-left-header-background); - color: var(--column-left-header-color); - font-size: var(--column-left-header-size); - text-transform: uppercase; - padding: 4px; - border: none; -} - a, u { color: var(--main-fg-color); } @@ -958,6 +950,14 @@ aside .toggle-inside li { font-size: var(--font-size); line-height: var(--line-spacing); } + h3.linksHeader { + background-color: var(--column-left-header-background); + color: var(--column-left-header-color); + font-size: var(--column-left-header-size); + text-transform: uppercase; + padding: 4px; + border: none; + } .newswireItem { font-size: var(--font-size-newswire); color: var(--column-right-fg-color); @@ -1543,6 +1543,14 @@ aside .toggle-inside li { font-size: var(--font-size); line-height: var(--line-spacing); } + h3.linksHeader { + background-color: var(--column-left-header-background); + color: var(--column-left-header-color); + font-size: var(--column-left-header-size-mobile); + text-transform: uppercase; + padding: 4px; + border: none; + } .leftColEditImage { background: var(--main-bg-color); width: var(--column-left-icon-size-mobile);