diff --git a/content.py b/content.py
index 6bf97dc80..960d3f033 100644
--- a/content.py
+++ b/content.py
@@ -202,35 +202,35 @@ def dangerousCSS(filename: str, allowLocalNetworkAccess: bool) -> bool:
return False
-def switchWords(baseDir: str, nickname: str, domain: str, content: str) -> str:
+def switchWords(baseDir: str, nickname: str, domain: str, content: str,
+ rules: [] = []) -> str:
"""Performs word replacements. eg. Trump -> The Orange Menace
"""
if isPGPEncrypted(content) or containsPGPPublicKey(content):
return content
- switchWordsFilename = baseDir + '/accounts/' + \
- nickname + '@' + domain + '/replacewords.txt'
- if not os.path.isfile(switchWordsFilename):
- return content
- with open(switchWordsFilename, 'r') as fp:
- for line in fp:
- replaceStr = line.replace('\n', '').replace('\r', '')
- wordTransform = None
- if '->' in replaceStr:
- wordTransform = replaceStr.split('->')
- elif ':' in replaceStr:
- wordTransform = replaceStr.split(':')
- elif ',' in replaceStr:
- wordTransform = replaceStr.split(',')
- elif ';' in replaceStr:
- wordTransform = replaceStr.split(';')
- elif '-' in replaceStr:
- wordTransform = replaceStr.split('-')
- if not wordTransform:
- continue
- if len(wordTransform) == 2:
- replaceStr1 = wordTransform[0].strip().replace('"', '')
- replaceStr2 = wordTransform[1].strip().replace('"', '')
- content = content.replace(replaceStr1, replaceStr2)
+
+ if not rules:
+ switchWordsFilename = baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/replacewords.txt'
+ if not os.path.isfile(switchWordsFilename):
+ return content
+ with open(switchWordsFilename, 'r') as fp:
+ rules = fp.readlines()
+
+ for line in rules:
+ replaceStr = line.replace('\n', '').replace('\r', '')
+ splitters = ('->', ':', ',', ';', '-')
+ wordTransform = None
+ for splitStr in splitters:
+ if splitStr in replaceStr:
+ wordTransform = replaceStr.split(splitStr)
+ break
+ if not wordTransform:
+ continue
+ if len(wordTransform) == 2:
+ replaceStr1 = wordTransform[0].strip().replace('"', '')
+ replaceStr2 = wordTransform[1].strip().replace('"', '')
+ content = content.replace(replaceStr1, replaceStr2)
return content
diff --git a/daemon.py b/daemon.py
index aa4b55576..4b8f7354f 100644
--- a/daemon.py
+++ b/daemon.py
@@ -286,6 +286,8 @@ from bookmarks import undoBookmark
from petnames import setPetName
from followingCalendar import addPersonToCalendar
from followingCalendar import removePersonFromCalendar
+from notifyOnPost import addNotifyOnPost
+from notifyOnPost import removeNotifyOnPost
from devices import E2EEdevicesCollection
from devices import E2EEvalidDevice
from devices import E2EEaddDevice
@@ -2087,6 +2089,34 @@ class PubServer(BaseHTTPRequestHandler):
self.server.POSTbusy = False
return
+ # person options screen, on notify checkbox
+ # See htmlPersonOptions
+ if '&submitNotifyOnPost=' in optionsConfirmParams:
+ notify = None
+ if 'notifyOnPost=' in optionsConfirmParams:
+ notify = optionsConfirmParams.split('notifyOnPost=')[1]
+ if '&' in notify:
+ notify = notify.split('&')[0]
+ if notify == 'on':
+ addNotifyOnPost(baseDir,
+ chooserNickname,
+ domain,
+ optionsNickname,
+ optionsDomainFull)
+ else:
+ removeNotifyOnPost(baseDir,
+ chooserNickname,
+ domain,
+ optionsNickname,
+ optionsDomainFull)
+ usersPathStr = \
+ usersPath + '/' + self.server.defaultTimeline + \
+ '?page=' + str(pageNumber)
+ self._redirect_headers(usersPathStr, cookie,
+ callingDomain)
+ self.server.POSTbusy = False
+ return
+
# person options screen, permission to post to newswire
# See htmlPersonOptions
if '&submitPostToNews=' in optionsConfirmParams:
@@ -7670,111 +7700,106 @@ class PubServer(BaseHTTPRequestHandler):
if '/' not in namedStatus:
# show actor
nickname = namedStatus
+ return False
+
+ postSections = namedStatus.split('/')
+ if len(postSections) != 2:
+ return False
+ nickname = postSections[0]
+ statusNumber = postSections[1]
+ if len(statusNumber) <= 10 or not statusNumber.isdigit():
+ return False
+
+ postFilename = \
+ baseDir + '/accounts/' + nickname + '@' + domain + '/outbox/' + \
+ httpPrefix + ':##' + domainFull + '#users#' + nickname + \
+ '#statuses#' + statusNumber + '.json'
+
+ return self._showPostFromFile(postFilename, likedBy,
+ authorized, callingDomain, path,
+ baseDir, httpPrefix, nickname,
+ domain, domainFull, port,
+ onionDomain, i2pDomain,
+ GETstartTime, GETtimings,
+ proxyType, cookie, debug)
+
+ def _showPostFromFile(self, postFilename: str, likedBy: str,
+ authorized: bool,
+ callingDomain: str, path: str,
+ baseDir: str, httpPrefix: str, nickname: str,
+ domain: str, domainFull: str, port: int,
+ onionDomain: str, i2pDomain: str,
+ GETstartTime, GETtimings: {},
+ proxyType: str, cookie: str,
+ debug: str) -> bool:
+ """Shows an individual post from its filename
+ """
+ if not os.path.isfile(postFilename):
+ self._404()
+ self.server.GETbusy = False
+ return True
+
+ postJsonObject = loadJson(postFilename)
+ if not postJsonObject:
+ self.send_response(429)
+ self.end_headers()
+ self.server.GETbusy = False
+ return True
+
+ # Only authorized viewers get to see likes on posts
+ # Otherwize marketers could gain more social graph info
+ if not authorized:
+ pjo = postJsonObject
+ if not isPublicPost(pjo):
+ self._404()
+ self.server.GETbusy = False
+ return True
+ removePostInteractions(pjo, True)
+ if self._requestHTTP():
+ msg = \
+ htmlIndividualPost(self.server.cssCache,
+ self.server.recentPostsCache,
+ self.server.maxRecentPosts,
+ self.server.translate,
+ baseDir,
+ self.server.session,
+ self.server.cachedWebfingers,
+ self.server.personCache,
+ nickname, domain, port,
+ authorized,
+ postJsonObject,
+ httpPrefix,
+ self.server.projectVersion,
+ likedBy,
+ self.server.YTReplacementDomain,
+ self.server.showPublishedDateOnly,
+ self.server.peertubeInstances,
+ self.server.allowLocalNetworkAccess,
+ self.server.themeName)
+ msg = msg.encode('utf-8')
+ msglen = len(msg)
+ self._set_headers('text/html', msglen,
+ cookie, callingDomain)
+ self._write(msg)
+ self._benchmarkGETtimings(GETstartTime,
+ GETtimings,
+ 'show skills ' +
+ 'done',
+ 'show status')
else:
- postSections = namedStatus.split('/')
- if len(postSections) == 2:
- nickname = postSections[0]
- statusNumber = postSections[1]
- if len(statusNumber) > 10 and statusNumber.isdigit():
- postFilename = \
- baseDir + '/accounts/' + \
- nickname + '@' + \
- domain + '/outbox/' + \
- httpPrefix + ':##' + \
- domainFull + '#users#' + \
- nickname + '#statuses#' + \
- statusNumber + '.json'
- if os.path.isfile(postFilename):
- postJsonObject = loadJson(postFilename)
- loadedPost = False
- if postJsonObject:
- loadedPost = True
- else:
- postJsonObject = {}
- if loadedPost:
- # Only authorized viewers get to see likes
- # on posts. Otherwize marketers could gain
- # more social graph info
- if not authorized:
- pjo = postJsonObject
- if not isPublicPost(pjo):
- self._404()
- self.server.GETbusy = False
- return True
- removePostInteractions(pjo, True)
- if self._requestHTTP():
- recentPostsCache = \
- self.server.recentPostsCache
- maxRecentPosts = \
- self.server.maxRecentPosts
- translate = \
- self.server.translate
- cachedWebfingers = \
- self.server.cachedWebfingers
- personCache = \
- self.server.personCache
- projectVersion = \
- self.server.projectVersion
- ytDomain = \
- self.server.YTReplacementDomain
- showPublishedDateOnly = \
- self.server.showPublishedDateOnly
- peertubeInstances = \
- self.server.peertubeInstances
- cssCache = self.server.cssCache
- allowLocalNetworkAccess = \
- self.server.allowLocalNetworkAccess
- themeName = \
- self.server.themeName
- msg = \
- htmlIndividualPost(cssCache,
- recentPostsCache,
- maxRecentPosts,
- translate,
- self.server.baseDir,
- self.server.session,
- cachedWebfingers,
- personCache,
- nickname,
- domain,
- port,
- authorized,
- postJsonObject,
- httpPrefix,
- projectVersion,
- likedBy,
- ytDomain,
- showPublishedDateOnly,
- peertubeInstances,
- allowLocalNetworkAccess,
- themeName)
- msg = msg.encode('utf-8')
- msglen = len(msg)
- self._set_headers('text/html', msglen,
- cookie, callingDomain)
- self._write(msg)
- else:
- if self._fetchAuthenticated():
- msg = json.dumps(postJsonObject,
- ensure_ascii=False)
- msg = msg.encode('utf-8')
- msglen = len(msg)
- self._set_headers('application/json',
- msglen,
- None, callingDomain)
- self._write(msg)
- else:
- self._404()
- self.server.GETbusy = False
- self._benchmarkGETtimings(GETstartTime, GETtimings,
- 'new post done',
- 'individual post shown')
- return True
- else:
- self._404()
- self.server.GETbusy = False
- return True
- return False
+ if self._fetchAuthenticated():
+ msg = json.dumps(postJsonObject,
+ ensure_ascii=False)
+ msg = msg.encode('utf-8')
+ msglen = len(msg)
+ self._set_headers('application/json',
+ msglen,
+ None, callingDomain)
+ self._write(msg)
+ else:
+ self._404()
+ self.server.GETbusy = False
+ return True
def _showIndividualPost(self, authorized: bool,
callingDomain: str, path: str,
@@ -7802,108 +7827,51 @@ class PubServer(BaseHTTPRequestHandler):
statusNumber = postSections[2]
if len(statusNumber) <= 10 or (not statusNumber.isdigit()):
return False
- postFilename = \
- baseDir + '/accounts/' + \
- nickname + '@' + \
- domain + '/outbox/' + \
- httpPrefix + ':##' + \
- domainFull + '#users#' + \
- nickname + '#statuses#' + \
- statusNumber + '.json'
- if os.path.isfile(postFilename):
- postJsonObject = loadJson(postFilename)
- if not postJsonObject:
- self.send_response(429)
- self.end_headers()
- self.server.GETbusy = False
- return True
- else:
- # Only authorized viewers get to see likes
- # on posts
- # Otherwize marketers could gain more social
- # graph info
- if not authorized:
- pjo = postJsonObject
- if not isPublicPost(pjo):
- self._404()
- self.server.GETbusy = False
- return True
- removePostInteractions(pjo, True)
- if self._requestHTTP():
- recentPostsCache = \
- self.server.recentPostsCache
- maxRecentPosts = \
- self.server.maxRecentPosts
- translate = \
- self.server.translate
- cachedWebfingers = \
- self.server.cachedWebfingers
- personCache = \
- self.server.personCache
- projectVersion = \
- self.server.projectVersion
- ytDomain = \
- self.server.YTReplacementDomain
- showPublishedDateOnly = \
- self.server.showPublishedDateOnly
- peertubeInstances = \
- self.server.peertubeInstances
- allowLocalNetworkAccess = \
- self.server.allowLocalNetworkAccess
- themeName = \
- self.server.themeName
- msg = \
- htmlIndividualPost(self.server.cssCache,
- recentPostsCache,
- maxRecentPosts,
- translate,
- baseDir,
- self.server.session,
- cachedWebfingers,
- personCache,
- nickname,
- domain,
- port,
- authorized,
- postJsonObject,
- httpPrefix,
- projectVersion,
- likedBy,
- ytDomain,
- showPublishedDateOnly,
- peertubeInstances,
- allowLocalNetworkAccess,
- themeName)
- msg = msg.encode('utf-8')
- msglen = len(msg)
- self._set_headers('text/html', msglen,
- cookie, callingDomain)
- self._write(msg)
- self._benchmarkGETtimings(GETstartTime,
- GETtimings,
- 'show skills ' +
- 'done',
- 'show status')
- else:
- if self._fetchAuthenticated():
- msg = json.dumps(postJsonObject,
- ensure_ascii=False)
- msg = msg.encode('utf-8')
- msglen = len(msg)
- self._set_headers('application/json',
- msglen,
- None, callingDomain)
- self._write(msg)
- else:
- self._404()
- self.server.GETbusy = False
- return True
- else:
- self._404()
- self.server.GETbusy = False
- return True
- return False
+ postFilename = \
+ baseDir + '/accounts/' + nickname + '@' + domain + '/outbox/' + \
+ httpPrefix + ':##' + domainFull + '#users#' + nickname + \
+ '#statuses#' + statusNumber + '.json'
+
+ return self._showPostFromFile(postFilename, likedBy,
+ authorized, callingDomain, path,
+ baseDir, httpPrefix, nickname,
+ domain, domainFull, port,
+ onionDomain, i2pDomain,
+ GETstartTime, GETtimings,
+ proxyType, cookie, debug)
+
+ def _showNotifyPost(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 an individual post from an account which you are following
+ and where you have the notify checkbox set on person options
+ """
+ likedBy = None
+ postId = path.split('?notifypost=')[1].strip()
+ postId = postId.replace('-', '/')
+ path = path.split('?notifypost=')[0]
+ nickname = path.split('/users/')[1]
+ if '/' in nickname:
+ return False
+ replies = False
+
+ postFilename = locatePost(baseDir, nickname, domain, postId, replies)
+ if not postFilename:
+ return False
+
+ return self._showPostFromFile(postFilename, likedBy,
+ authorized, callingDomain, path,
+ baseDir, httpPrefix, nickname,
+ domain, domainFull, port,
+ onionDomain, i2pDomain,
+ GETstartTime, GETtimings,
+ proxyType, cookie, debug)
def _showInbox(self, authorized: bool,
callingDomain: str, path: str,
@@ -12483,6 +12451,21 @@ class PubServer(BaseHTTPRequestHandler):
'post roles done',
'show skills done')
+ if '?notifypost=' in self.path and usersInPath and authorized:
+ if self._showNotifyPost(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
+
# get an individual post from the path
# /users/nickname/statuses/number
if '/statuses/' in self.path and usersInPath:
diff --git a/inbox.py b/inbox.py
index 8fbaccf50..24ed00f90 100644
--- a/inbox.py
+++ b/inbox.py
@@ -86,6 +86,7 @@ from categories import guessHashtagCategory
from context import hasValidContext
from speaker import updateSpeaker
from announce import isSelfAnnounce
+from notifyOnPost import notifyWhenPersonPosts
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
@@ -1850,6 +1851,25 @@ def _likeNotify(baseDir: str, domain: str, onionDomain: str,
pass
+def _notifyPostArrival(baseDir: str, handle: str, url: str) -> None:
+ """Creates a notification that a new post has arrived.
+ This is for followed accounts with the notify checkbox enabled
+ on the person options screen
+ """
+ accountDir = baseDir + '/accounts/' + handle
+ if not os.path.isdir(accountDir):
+ return
+ notifyFile = accountDir + '/.newNotifiedPost'
+ if os.path.isfile(notifyFile):
+ # check that the same notification is not repeatedly sent
+ with open(notifyFile, 'r') as fp:
+ existingNotificationMessage = fp.read()
+ if url in existingNotificationMessage:
+ return
+ with open(notifyFile, 'w+') as fp:
+ fp.write(url)
+
+
def _replyNotify(baseDir: str, handle: str, url: str) -> None:
"""Creates a notification that a new reply has arrived
"""
@@ -2275,6 +2295,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
_updateLastSeen(baseDir, handle, actor)
+ postIsDM = False
isGroup = _groupHandle(baseDir, handle)
if _receiveLike(recentPostsCache,
@@ -2392,6 +2413,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
postJsonObject = messageJson
nickname = handle.split('@')[0]
+ jsonObj = None
if _validPostContent(baseDir, nickname, domain,
postJsonObject, maxMentions, maxEmoji,
allowLocalNetworkAccess, debug):
@@ -2473,8 +2495,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
# get the actor being replied to
domainFull = getFullDomain(domain, port)
- actor = httpPrefix + '://' + domainFull + \
- '/users/' + handle.split('@')[0]
+ actor = httpPrefix + '://' + domainFull + '/users/' + nickname
# create a reply notification file if needed
if not postIsDM and isReply(postJsonObject, actor):
@@ -2512,6 +2533,26 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
# save the post to file
if saveJson(postJsonObject, destinationFilename):
+ # should we notify that a post from this person has arrived?
+ # This is for cases where the notify checkbox is enabled
+ # on the person options screen
+ if not postIsDM and jsonObj:
+ if jsonObj.get('attributedTo') and jsonObj.get('id'):
+ attributedTo = jsonObj['attributedTo']
+ if isinstance(attributedTo, str):
+ fromNickname = getNicknameFromActor(attributedTo)
+ fromDomain, fromPort = getDomainFromActor(attributedTo)
+ fromDomainFull = getFullDomain(fromDomain, fromPort)
+ if notifyWhenPersonPosts(baseDir, nickname, domain,
+ fromNickname, fromDomainFull):
+ postId = removeIdEnding(jsonObj['id'])
+ postLink = \
+ httpPrefix + '://' + \
+ getFullDomain(domain, port) + \
+ '/users/' + nickname + \
+ '?notifypost=' + postId.replace('/', '-')
+ _notifyPostArrival(baseDir, handle, postLink)
+
# If this is a reply to a muted post then also mute it.
# This enables you to ignore a threat that's getting boring
if isReplyToMutedPost:
diff --git a/notifyOnPost.py b/notifyOnPost.py
new file mode 100644
index 000000000..2c40e7d2e
--- /dev/null
+++ b/notifyOnPost.py
@@ -0,0 +1,104 @@
+__filename__ = "notifyOnPost.py"
+__author__ = "Bob Mottram"
+__license__ = "AGPL3+"
+__version__ = "1.2.0"
+__maintainer__ = "Bob Mottram"
+__email__ = "bob@freedombone.net"
+__status__ = "Production"
+__module_group__ = "Calendar"
+
+import os
+from utils import removeDomainPort
+
+
+def _notifyOnPostArrival(baseDir: str, nickname: str, domain: str,
+ followingNickname: str,
+ followingDomain: str,
+ add: bool) -> None:
+ """Adds or removes a handle from the following.txt list into a list
+ indicating whether to notify when a new post arrives from that account
+ """
+ # check that a following file exists
+ domain = removeDomainPort(domain)
+ followingFilename = baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/following.txt'
+ if not os.path.isfile(followingFilename):
+ print("WARN: following.txt doesn't exist for " +
+ nickname + '@' + domain)
+ return
+ handle = followingNickname + '@' + followingDomain
+
+ # check that you are following this handle
+ if handle + '\n' not in open(followingFilename).read():
+ print('WARN: ' + handle + ' is not in ' + followingFilename)
+ return
+
+ notifyOnPostFilename = baseDir + '/accounts/' + \
+ nickname + '@' + domain + '/notifyOnPost.txt'
+
+ # get the contents of the notifyOnPost file, which is
+ # a set of handles
+ followingHandles = ''
+ if os.path.isfile(notifyOnPostFilename):
+ print('notify file exists')
+ with open(notifyOnPostFilename, 'r') as calendarFile:
+ followingHandles = calendarFile.read()
+ else:
+ # create a new notifyOnPost file from the following file
+ print('Creating notifyOnPost file ' + notifyOnPostFilename)
+ followingHandles = ''
+ with open(followingFilename, 'r') as followingFile:
+ followingHandles = followingFile.read()
+ if add:
+ with open(notifyOnPostFilename, 'w+') as fp:
+ fp.write(followingHandles + handle + '\n')
+
+ # already in the notifyOnPost file?
+ if handle + '\n' in followingHandles:
+ print(handle + ' exists in notifyOnPost.txt')
+ if add:
+ # already added
+ return
+ # remove from calendar file
+ followingHandles = followingHandles.replace(handle + '\n', '')
+ with open(notifyOnPostFilename, 'w+') as fp:
+ fp.write(followingHandles)
+ else:
+ print(handle + ' not in notifyOnPost.txt')
+ # not already in the notifyOnPost file
+ if add:
+ # append to the list of handles
+ followingHandles += handle + '\n'
+ with open(notifyOnPostFilename, 'w+') as fp:
+ fp.write(followingHandles)
+
+
+def addNotifyOnPost(baseDir: str, nickname: str, domain: str,
+ followingNickname: str,
+ followingDomain: str) -> None:
+ _notifyOnPostArrival(baseDir, nickname, domain,
+ followingNickname, followingDomain, True)
+
+
+def removeNotifyOnPost(baseDir: str, nickname: str, domain: str,
+ followingNickname: str,
+ followingDomain: str) -> None:
+ _notifyOnPostArrival(baseDir, nickname, domain,
+ followingNickname, followingDomain, False)
+
+
+def notifyWhenPersonPosts(baseDir: str, nickname: str, domain: str,
+ followingNickname: str,
+ followingDomain: str) -> bool:
+ """Returns true if receiving notifications when the given publishes a post
+ """
+ if followingNickname == nickname and followingDomain == domain:
+ return False
+ notifyOnPostFilename = \
+ baseDir + '/accounts/' + nickname + '@' + domain + '/notifyOnPost.txt'
+ handle = followingNickname + '@' + followingDomain
+ if not os.path.isfile(notifyOnPostFilename):
+ # create a new notifyOnPost file
+ with open(notifyOnPostFilename, 'w+') as fp:
+ fp.write('')
+ return handle + '\n' in open(notifyOnPostFilename).read()
diff --git a/scripts/epicyon-notification b/scripts/epicyon-notification
index 5b4f4ec89..d2f2c6301 100755
--- a/scripts/epicyon-notification
+++ b/scripts/epicyon-notification
@@ -215,6 +215,21 @@ function notifications {
fi
fi
+ # send notifications for posts arriving from a particular person
+ epicyonNotifyFile="$epicyonDir/.newNotifiedPost"
+ if [ -f "$epicyonNotifyFile" ]; then
+ if ! grep -q "##sent##" "$epicyonNotifyFile"; then
+ epicyonNotifyMessage=$(notification_translate_text 'New post')
+ epicyonNotifyFileContent=$(echo "$epicyonNotifyMessage")" "$(cat "$epicyonNotifyFile")
+ if [[ "$epicyonNotifyFileContent" == *':'* ]]; then
+ epicyonNotifyMessage="Epicyon: $epicyonNotifyFileContent"
+ fi
+ sendNotification "$USERNAME" "Epicyon" "$epicyonNotifyMessage"
+ echo "##sent##" > "$epicyonNotifyFile"
+ chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonNotifyFile"
+ fi
+ fi
+
# send notifications for replies to XMPP/email users
epicyonReplyFile="$epicyonDir/.newReply"
if [ -f "$epicyonReplyFile" ]; then
diff --git a/tests.py b/tests.py
index c6ac118db..9fe956d83 100644
--- a/tests.py
+++ b/tests.py
@@ -94,6 +94,7 @@ from inbox import jsonPostAllowsComments
from inbox import validInbox
from inbox import validInboxFilenames
from categories import guessHashtagCategory
+from content import switchWords
from content import extractTextFieldsInPOST
from content import validHashTag
from content import htmlReplaceEmailQuote
@@ -3384,62 +3385,6 @@ def _testFunctions():
_diagramGroups(['Core', 'Accessibility'], ['utils'],
modules, modGroups, maxModuleCalls)
- callGraphStr = 'digraph Epicyon {\n\n'
- callGraphStr += ' size="8,6"; ratio=fill;\n'
- callGraphStr += ' graph [fontsize=10 fontname="Verdana" compound=true];\n'
- callGraphStr += ' node [shape=record fontsize=10 fontname="Verdana"];\n\n'
-
- for modName, modProperties in modules.items():
- callGraphStr += ' subgraph cluster_' + modName + ' {\n'
- callGraphStr += ' label = "' + modName + '";\n'
- callGraphStr += ' node [style=filled];\n'
- moduleFunctionsStr = ''
- for name in modProperties['functions']:
- if name.startswith('test'):
- continue
- if name not in excludeFuncs:
- if not functionProperties[name]['calls']:
- moduleFunctionsStr += \
- ' "' + name + '" [fillcolor=yellow style=filled];\n'
- continue
- noOfCalls = len(functionProperties[name]['calls'])
- if noOfCalls < int(maxFunctionCalls / 4):
- moduleFunctionsStr += ' "' + name + \
- '" [fillcolor=orange style=filled];\n'
- else:
- moduleFunctionsStr += ' "' + name + \
- '" [fillcolor=red style=filled];\n'
-
- if moduleFunctionsStr:
- callGraphStr += moduleFunctionsStr + '\n'
- callGraphStr += ' color=blue;\n'
- callGraphStr += ' }\n\n'
-
- for name, properties in functionProperties.items():
- if not properties['calls']:
- continue
- noOfCalls = len(properties['calls'])
- if noOfCalls <= int(maxFunctionCalls / 8):
- modColor = 'blue'
- elif noOfCalls < int(maxFunctionCalls / 4):
- modColor = 'green'
- else:
- modColor = 'red'
- for calledFunc in properties['calls']:
- if calledFunc.startswith('test'):
- continue
- if calledFunc not in excludeFuncs:
- callGraphStr += ' "' + name + '" -> "' + calledFunc + \
- '" [color=' + modColor + '];\n'
-
- callGraphStr += '\n}\n'
- with open('epicyon.dot', 'w+') as fp:
- fp.write(callGraphStr)
- print('Call graph saved to epicyon.dot')
- print('Plot using: ' +
- 'sfdp -x -Goverlap=prism -Goverlap_scaling=8 ' +
- '-Gsep=+120 -Tx11 epicyon.dot')
-
def _testLinksWithinPost() -> None:
baseDir = os.getcwd()
@@ -4173,9 +4118,33 @@ def _testUserAgentDomain() -> None:
assert userAgentDomain(userAgent, False) is None
+def _testSwitchWords() -> None:
+ print('testSwitchWords')
+ rules = [
+ "rock -> hamster",
+ "orange -> lemon"
+ ]
+ baseDir = os.getcwd()
+ nickname = 'testuser'
+ domain = 'testdomain.com'
+
+ content = 'This is a test'
+ result = switchWords(baseDir, nickname, domain, content, rules)
+ assert result == content
+
+ content = 'This is orange test'
+ result = switchWords(baseDir, nickname, domain, content, rules)
+ assert result == 'This is lemon test'
+
+ content = 'This is a test rock'
+ result = switchWords(baseDir, nickname, domain, content, rules)
+ assert result == 'This is a test hamster'
+
+
def runAllTests():
print('Running tests...')
updateDefaultThemesList(os.getcwd())
+ _testSwitchWords()
_testFunctions()
_testUserAgentDomain()
_testRoles()
diff --git a/translations/ar.json b/translations/ar.json
index 270d3b503..50cca5a13 100644
--- a/translations/ar.json
+++ b/translations/ar.json
@@ -449,5 +449,6 @@
"Import Theme": "استيراد الموضوع",
"Export Theme": "موضوع التصدير",
"Custom post submit button text": "عرف نشر إرسال نص زر",
- "Blocked User Agents": "عوامل المستخدم المحظورة"
+ "Blocked User Agents": "عوامل المستخدم المحظورة",
+ "Notify me when this account posts": "أعلمني عندما ينشر الحساب هذا"
}
diff --git a/translations/ca.json b/translations/ca.json
index b445702f3..c9a15be88 100644
--- a/translations/ca.json
+++ b/translations/ca.json
@@ -449,5 +449,6 @@
"Import Theme": "Importació temàtica",
"Export Theme": "Tema d'exportació",
"Custom post submit button text": "Text de botó d'enviament de publicacions personalitzades",
- "Blocked User Agents": "Agents d'usuari bloquejats"
+ "Blocked User Agents": "Agents d'usuari bloquejats",
+ "Notify me when this account posts": "Aviseu-me quan publiqui aquest compte"
}
diff --git a/translations/cy.json b/translations/cy.json
index 680fa4c72..0cf0e9980 100644
--- a/translations/cy.json
+++ b/translations/cy.json
@@ -449,5 +449,6 @@
"Import Theme": "Thema Mewnforio",
"Export Theme": "Thema Allforio",
"Custom post submit button text": "Testun Post Post Post",
- "Blocked User Agents": "Asiantau defnyddwyr wedi'u blocio"
+ "Blocked User Agents": "Asiantau defnyddwyr wedi'u blocio",
+ "Notify me when this account posts": "Rhoi gwybod i mi pan fydd y cyfrifon cyfrif hwn"
}
diff --git a/translations/de.json b/translations/de.json
index 2ec7c24b6..df10a2e6e 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -449,5 +449,6 @@
"Import Theme": "Theme importieren",
"Export Theme": "Theme exportieren",
"Custom post submit button text": "Benutzerdefinierte Post-Senden Schaltfläche Text",
- "Blocked User Agents": "Blockierte Benutzeragenten"
+ "Blocked User Agents": "Blockierte Benutzeragenten",
+ "Notify me when this account posts": "Benachrichtigen Sie mich, wenn dieses Konto postet"
}
diff --git a/translations/en.json b/translations/en.json
index 25dd4a83a..31d1bbf40 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -449,5 +449,6 @@
"Import Theme": "Import Theme",
"Export Theme": "Export Theme",
"Custom post submit button text": "Custom post submit button text",
- "Blocked User Agents": "Blocked User Agents"
+ "Blocked User Agents": "Blocked User Agents",
+ "Notify me when this account posts": "Notify me when this account posts"
}
diff --git a/translations/es.json b/translations/es.json
index 1502da0e3..ac096a01d 100644
--- a/translations/es.json
+++ b/translations/es.json
@@ -449,5 +449,6 @@
"Import Theme": "Tema de importación",
"Export Theme": "Tema de exportación",
"Custom post submit button text": "POST POST PERSONALIZADO Botón Texto",
- "Blocked User Agents": "Agentes de usuario bloqueados"
+ "Blocked User Agents": "Agentes de usuario bloqueados",
+ "Notify me when this account posts": "Notifíqueme cuando se publique esta cuenta"
}
diff --git a/translations/fr.json b/translations/fr.json
index 57d4d369a..6defd9956 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -449,5 +449,6 @@
"Import Theme": "Import thème",
"Export Theme": "Thème d'exportation",
"Custom post submit button text": "Texte de bouton d'envoi postal personnalisé",
- "Blocked User Agents": "Agents d'utilisateur bloqués"
+ "Blocked User Agents": "Agents d'utilisateur bloqués",
+ "Notify me when this account posts": "Avertissez-moi quand ce compte publie"
}
diff --git a/translations/ga.json b/translations/ga.json
index ea04687fe..632b57ab0 100644
--- a/translations/ga.json
+++ b/translations/ga.json
@@ -449,5 +449,6 @@
"Import Theme": "Téama Iompórtáil",
"Export Theme": "Téama Easpórtála",
"Custom post submit button text": "Post saincheaptha Cuir isteach an cnaipe Téacs",
- "Blocked User Agents": "Gníomhairí úsáideora blocáilte"
+ "Blocked User Agents": "Gníomhairí úsáideora blocáilte",
+ "Notify me when this account posts": "Cuir in iúl dom nuair a phostófar an cuntas seo"
}
diff --git a/translations/hi.json b/translations/hi.json
index 00ea6df4d..2f3e13f0e 100644
--- a/translations/hi.json
+++ b/translations/hi.json
@@ -449,5 +449,6 @@
"Import Theme": "आयात विषय",
"Export Theme": "निर्यात विषय",
"Custom post submit button text": "कस्टम पोस्ट सबमिट बटन टेक्स्ट",
- "Blocked User Agents": "अवरुद्ध उपयोगकर्ता एजेंट"
+ "Blocked User Agents": "अवरुद्ध उपयोगकर्ता एजेंट",
+ "Notify me when this account posts": "यह खाता पोस्ट होने पर मुझे सूचित करें"
}
diff --git a/translations/it.json b/translations/it.json
index 12994e2ea..8406ccbda 100644
--- a/translations/it.json
+++ b/translations/it.json
@@ -449,5 +449,6 @@
"Import Theme": "Tema dell'importazione",
"Export Theme": "Esportare tema",
"Custom post submit button text": "Pulsante di invio del post personalizzato",
- "Blocked User Agents": "Agenti utente bloccati"
+ "Blocked User Agents": "Agenti utente bloccati",
+ "Notify me when this account posts": "Avvisami quando questo account messaggi"
}
diff --git a/translations/ja.json b/translations/ja.json
index fdb96a967..494661a86 100644
--- a/translations/ja.json
+++ b/translations/ja.json
@@ -449,5 +449,6 @@
"Import Theme": "輸入テーマ",
"Export Theme": "テーマをエクスポートします",
"Custom post submit button text": "カスタムポスト送信ボタンテキスト",
- "Blocked User Agents": "ブロックされたユーザーエージェント"
+ "Blocked User Agents": "ブロックされたユーザーエージェント",
+ "Notify me when this account posts": "この口座投稿を通知する"
}
diff --git a/translations/ku.json b/translations/ku.json
index cf71e2f19..8d99ddd72 100644
--- a/translations/ku.json
+++ b/translations/ku.json
@@ -449,5 +449,6 @@
"Import Theme": "Mijara Import",
"Export Theme": "Mijara Export",
"Custom post submit button text": "Nivîsa bişkojka paşîn a paşîn",
- "Blocked User Agents": "Karmendên bikarhêner asteng kirin"
+ "Blocked User Agents": "Karmendên bikarhêner asteng kirin",
+ "Notify me when this account posts": "Dema ku ev postên hesabê min agahdar bikin"
}
diff --git a/translations/oc.json b/translations/oc.json
index 7c2df85d6..411216f8a 100644
--- a/translations/oc.json
+++ b/translations/oc.json
@@ -445,5 +445,6 @@
"Import Theme": "Import Theme",
"Export Theme": "Export Theme",
"Custom post submit button text": "Custom post submit button text",
- "Blocked User Agents": "Blocked User Agents"
+ "Blocked User Agents": "Blocked User Agents",
+ "Notify me when this account posts": "Notify me when this account posts"
}
diff --git a/translations/pt.json b/translations/pt.json
index d7d303d1f..8d05812de 100644
--- a/translations/pt.json
+++ b/translations/pt.json
@@ -449,5 +449,6 @@
"Import Theme": "Importar tema",
"Export Theme": "Exportar tema",
"Custom post submit button text": "Texto de botão de envio de post personalizado",
- "Blocked User Agents": "Agentes de usuário bloqueados"
+ "Blocked User Agents": "Agentes de usuário bloqueados",
+ "Notify me when this account posts": "Notifique-me quando esta conta posts"
}
diff --git a/translations/ru.json b/translations/ru.json
index cfee8a392..4663cef78 100644
--- a/translations/ru.json
+++ b/translations/ru.json
@@ -449,5 +449,6 @@
"Import Theme": "Импортировать тему",
"Export Theme": "Экспортная тема",
"Custom post submit button text": "Пользовательский пост Отправить кнопку текста",
- "Blocked User Agents": "Заблокированные пользовательские агенты"
+ "Blocked User Agents": "Заблокированные пользовательские агенты",
+ "Notify me when this account posts": "Сообщите мне, когда эта учетная запись"
}
diff --git a/translations/sw.json b/translations/sw.json
index 0ac58b643..4f2fa1bb7 100644
--- a/translations/sw.json
+++ b/translations/sw.json
@@ -449,5 +449,6 @@
"Import Theme": "Ingiza mandhari",
"Export Theme": "Tuma mandhari",
"Custom post submit button text": "Ujumbe wa Desturi Wasilisha Nakala ya kifungo",
- "Blocked User Agents": "Wakala wa watumiaji waliozuiwa"
+ "Blocked User Agents": "Wakala wa watumiaji waliozuiwa",
+ "Notify me when this account posts": "Nijulishe wakati akaunti hii ya akaunti."
}
diff --git a/translations/zh.json b/translations/zh.json
index 07b9e10c1..eb143dc04 100644
--- a/translations/zh.json
+++ b/translations/zh.json
@@ -449,5 +449,6 @@
"Import Theme": "进口主题",
"Export Theme": "出口主题",
"Custom post submit button text": "自定义发布提交按钮文本",
- "Blocked User Agents": "阻止用户代理商"
+ "Blocked User Agents": "阻止用户代理商",
+ "Notify me when this account posts": "此帐户帖子时通知我"
}
diff --git a/webapp_person_options.py b/webapp_person_options.py
index 3c3309196..cebdba87d 100644
--- a/webapp_person_options.py
+++ b/webapp_person_options.py
@@ -23,6 +23,7 @@ from blocking import isBlocked
from follow import isFollowerOfPerson
from follow import isFollowingActor
from followingCalendar import receivingCalendarEvents
+from notifyOnPost import notifyWhenPersonPosts
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from webapp_utils import getBrokenLinkSubstitute
@@ -246,8 +247,21 @@ def htmlPersonOptions(defaultTimeline: str,
'name="submitPetname">' + \
translate['Submit'] + '
\n'
- # checkbox for receiving calendar events
+ # Notify when a post arrives from this person
if isFollowingActor(baseDir, nickname, domain, optionsActor):
+ checkboxStr = \
+ ' 🔔' + \
+ translate['Notify me when this account posts'] + \
+ '\n
\n'
+ if not notifyWhenPersonPosts(baseDir, nickname, domain,
+ optionsNickname,
+ optionsDomainFull):
+ checkboxStr = checkboxStr.replace(' checked>', '>')
+ optionsStr += checkboxStr
+
checkboxStr = \
' ' + \