main
Bob Mottram 2021-10-19 23:49:48 +01:00
commit b03a6a6104
10 changed files with 1337 additions and 777 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@ -31,23 +31,6 @@ def _getConversationFilename(baseDir: str, nickname: str, domain: str,
return conversationDir + '/' + conversationId return conversationDir + '/' + conversationId
def previousConversationPostId(baseDir: str, nickname: str, domain: str,
postJsonObject: {}) -> str:
"""Returns the previous conversation post id
"""
conversationFilename = \
_getConversationFilename(baseDir, nickname, domain, postJsonObject)
if not conversationFilename:
return False
if not os.path.isfile(conversationFilename):
return False
with open(conversationFilename, 'r') as fp:
lines = fp.readlines()
if lines:
return lines[-1].replace('\n', '')
return False
def updateConversation(baseDir: str, nickname: str, domain: str, def updateConversation(baseDir: str, nickname: str, domain: str,
postJsonObject: {}) -> bool: postJsonObject: {}) -> bool:
"""Ads a post to a conversation index in the /conversation subdirectory """Ads a post to a conversation index in the /conversation subdirectory

1625
daemon.py

File diff suppressed because it is too large Load Diff

141
epicyon-graph.css 100644
View File

@ -0,0 +1,141 @@
body, table, input, select, textarea {
}
.graph {
margin-bottom:1em;
font:normal 100%/150% arial,helvetica,sans-serif;
}
.graph caption {
font:bold 150%/120% arial,helvetica,sans-serif;
padding-bottom:0.33em;
}
.graph tbody th {
text-align:right;
}
@supports (display:grid) {
@media (min-width:32em) {
.graph {
display:block;
width:600px;
height:300px;
}
.graph caption {
display:block;
}
.graph thead {
display:none;
}
.graph tbody {
position:relative;
display:grid;
grid-template-columns:repeat(auto-fit, minmax(2em, 1fr));
column-gap:2.5%;
align-items:end;
height:100%;
margin:3em 0 1em 2.8em;
padding:0 1em;
border-bottom:2px solid rgba(0,0,0,0.5);
background:repeating-linear-gradient(
180deg,
rgba(170,170,170,0.7) 0,
rgba(170,170,170,0.7) 1px,
transparent 1px,
transparent 20%
);
}
.graph tbody:before,
.graph tbody:after {
position:absolute;
left:-3.2em;
width:2.8em;
text-align:right;
font:bold 80%/120% arial,helvetica,sans-serif;
}
.graph tbody:before {
content:"100%";
top:-0.6em;
}
.graph tbody:after {
content:"0%";
bottom:-0.6em;
}
.graph tr {
position:relative;
display:block;
}
.graph tr:hover {
z-index:999;
}
.graph th,
.graph td {
display:block;
text-align:center;
}
.graph tbody th {
position:absolute;
top:-3em;
left:0;
width:100%;
font-weight:normal;
text-align:center;
white-space:nowrap;
text-indent:0;
transform:rotate(-45deg);
}
.graph tbody th:after {
content:"";
}
.graph td {
width:100%;
height:100%;
background:#F63;
border-radius:0.5em 0.5em 0 0;
transition:background 0.5s;
}
.graph tr:hover td {
opacity:0.7;
}
.graph td span {
overflow:hidden;
position:absolute;
left:50%;
top:50%;
width:0;
padding:0.5em 0;
margin:-1em 0 0;
font:normal 85%/120% arial,helvetica,sans-serif;
/* background:white; */
/* box-shadow:0 0 0.25em rgba(0,0,0,0.6); */
font-weight:bold;
opacity:0;
transition:opacity 0.5s;
color:white;
}
.toggleGraph:checked + table td span,
.graph tr:hover td span {
width:4em;
margin-left:-2em;
opacity:1;
}
}
}

126
fitnessFunctions.py 100644
View File

@ -0,0 +1,126 @@
__filename__ = "fitnessFunctions.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Core"
import os
import time
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter
from utils import getConfigParam
from utils import saveJson
def fitnessPerformance(startTime, fitnessState: {},
fitnessId: str, watchPoint: str, debug: bool) -> None:
"""Log a performance watchpoint
"""
if 'performance' not in fitnessState:
fitnessState['performance'] = {}
if fitnessId not in fitnessState['performance']:
fitnessState['performance'][fitnessId] = {}
if watchPoint not in fitnessState['performance'][fitnessId]:
fitnessState['performance'][fitnessId][watchPoint] = {
"total": float(0),
"ctr": int(0)
}
timeDiff = float(time.time() - startTime)
fitnessState['performance'][fitnessId][watchPoint]['total'] += timeDiff
fitnessState['performance'][fitnessId][watchPoint]['ctr'] += 1
if fitnessState['performance'][fitnessId][watchPoint]['ctr'] >= 1024:
fitnessState['performance'][fitnessId][watchPoint]['total'] /= 2
fitnessState['performance'][fitnessId][watchPoint]['ctr'] = \
int(fitnessState['performance'][fitnessId][watchPoint]['ctr'] / 2)
if debug:
ctr = fitnessState['performance'][fitnessId][watchPoint]['ctr']
total = fitnessState['performance'][fitnessId][watchPoint]['total']
print('FITNESS: performance/' + fitnessId + '/' +
watchPoint + '/' + str(total * 1000 / ctr))
def sortedWatchPoints(fitness: {}, fitnessId: str) -> []:
"""Returns a sorted list of watchpoints
times are in mS
"""
if not fitness.get('performance'):
return []
if not fitness['performance'].get(fitnessId):
return []
result = []
for watchPoint, item in fitness['performance'][fitnessId].items():
if not item.get('total'):
continue
averageTime = item['total'] * 1000 / item['ctr']
result.append(str(averageTime) + ' ' + watchPoint)
result.sort(reverse=True)
return result
def htmlWatchPointsGraph(baseDir: str, fitness: {}, fitnessId: str,
maxEntries: int) -> str:
"""Returns the html for a graph of watchpoints
"""
watchPointsList = sortedWatchPoints(fitness, fitnessId)
cssFilename = baseDir + '/epicyon-graph.css'
if os.path.isfile(baseDir + '/graph.css'):
cssFilename = baseDir + '/graph.css'
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
htmlStr += \
'<table class="graph">\n' + \
'<caption>Watchpoints for ' + fitnessId + '</caption>\n' + \
'<thead>\n' + \
' <tr>\n' + \
' <th scope="col">Item</th>\n' + \
' <th scope="col">Percent</th>\n' + \
' </tr>\n' + \
'</thead><tbody>\n'
# get the maximum time
maxAverageTime = float(1)
if len(watchPointsList) > 0:
maxAverageTime = float(watchPointsList[0].split(' ')[0])
for watchPoint in watchPointsList:
averageTime = float(watchPoint.split(' ')[0])
if averageTime > maxAverageTime:
maxAverageTime = averageTime
ctr = 0
for watchPoint in watchPointsList:
name = watchPoint.split(' ')[1]
averageTime = float(watchPoint.split(' ')[0])
heightPercent = int(averageTime * 100 / maxAverageTime)
timeMS = int(averageTime)
if heightPercent == 0:
continue
htmlStr += \
'<tr style="height:' + str(heightPercent) + '%">\n' + \
' <th scope="row">' + name + '</th>\n' + \
' <td><span>' + str(timeMS) + 'mS</span></td>\n' + \
'</tr>\n'
ctr += 1
if ctr >= maxEntries:
break
htmlStr += '</tbody></table>\n' + htmlFooter()
return htmlStr
def fitnessThread(baseDir: str, fitness: {}):
"""Thread used to save fitness function scores
"""
fitnessFilename = baseDir + '/accounts/fitness.json'
while True:
# every 10 mins
time.sleep(60 * 10)
saveJson(fitness, fitnessFilename)

View File

@ -107,6 +107,37 @@ from conversation import updateConversation
from content import validHashTag from content import validHashTag
def _storeLastPostId(baseDir: str, nickname: str, domain: str,
postJsonObject: {}) -> None:
"""Stores the id of the last post made by an actor
When a new post arrives this allows it to be compared against the last
to see if it is an edited post.
It would be great if edited posts contained a back reference id to the
source but we don't live in that ideal world.
"""
actor = postId = None
if hasObjectDict(postJsonObject):
if postJsonObject['object'].get('attributedTo'):
if isinstance(postJsonObject['object']['attributedTo'], str):
actor = postJsonObject['object']['attributedTo']
postId = removeIdEnding(postJsonObject['object']['id'])
if not actor:
actor = postJsonObject['actor']
postId = removeIdEnding(postJsonObject['id'])
if not actor:
return
lastpostDir = acctDir(baseDir, nickname, domain) + '/lastpost'
if not os.path.isdir(lastpostDir):
os.mkdir(lastpostDir)
actorFilename = lastpostDir + '/' + actor.replace('/', '#')
try:
with open(actorFilename, 'w+') as fp:
fp.write(postId)
except BaseException:
print('Unable to write last post id to ' + actorFilename)
pass
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
"""Extracts hashtags from an incoming post and updates the """Extracts hashtags from an incoming post and updates the
relevant tags files. relevant tags files.
@ -2889,6 +2920,9 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, editedFilename, nickname, domain, editedFilename,
debug, recentPostsCache) debug, recentPostsCache)
# store the id of the last post made by this actor
_storeLastPostId(baseDir, nickname, domain, postJsonObject)
_inboxUpdateCalendar(baseDir, handle, postJsonObject) _inboxUpdateCalendar(baseDir, handle, postJsonObject)
storeHashTags(baseDir, handleName, postJsonObject) storeHashTags(baseDir, handleName, postJsonObject)

View File

@ -86,7 +86,6 @@ from linked_data_sig import generateJsonSignature
from petnames import resolvePetnames from petnames import resolvePetnames
from video import convertVideoToNote from video import convertVideoToNote
from context import getIndividualPostContext from context import getIndividualPostContext
from conversation import previousConversationPostId
def isModerator(baseDir: str, nickname: str) -> bool: def isModerator(baseDir: str, nickname: str) -> bool:
@ -2095,10 +2094,15 @@ def threadSendPost(session, postJsonStr: str, federationList: [],
if debug: if debug:
print('Getting postJsonString for ' + inboxUrl) print('Getting postJsonString for ' + inboxUrl)
try: try:
postResult, unauthorized = \ postResult, unauthorized, returnCode = \
postJsonString(session, postJsonStr, federationList, postJsonString(session, postJsonStr, federationList,
inboxUrl, signatureHeaderJson, inboxUrl, signatureHeaderJson,
debug) debug)
if returnCode >= 500 and returnCode < 600:
# if an instance is returning a code which indicates that
# it might have a runtime error, like 503, then don't
# continue to post to it
break
if debug: if debug:
print('Obtained postJsonString for ' + inboxUrl + print('Obtained postJsonString for ' + inboxUrl +
' unauthorized: ' + str(unauthorized)) ' unauthorized: ' + str(unauthorized))
@ -2410,12 +2414,17 @@ def sendPostViaServer(signingPrivateKeyPem: str, projectVersion: str,
'Authorization': authHeader 'Authorization': authHeader
} }
postDumps = json.dumps(postJsonObject) postDumps = json.dumps(postJsonObject)
postResult = \ postResult, unauthorized, returnCode = \
postJsonString(session, postDumps, [], postJsonString(session, postDumps, [],
inboxUrl, headers, debug, 5, True) inboxUrl, headers, debug, 5, True)
if not postResult: if not postResult:
if debug: if debug:
print('DEBUG: POST failed for c2s to ' + inboxUrl) if unauthorized:
print('DEBUG: POST failed for c2s to ' +
inboxUrl + ' unathorized')
else:
print('DEBUG: POST failed for c2s to '
+ inboxUrl + ' return code ' + str(returnCode))
return 5 return 5
if debug: if debug:
@ -4986,60 +4995,74 @@ def editedPostFilename(baseDir: str, nickname: str, domain: str,
""" """
if not hasObjectDict(postJsonObject): if not hasObjectDict(postJsonObject):
return '' return ''
if not postJsonObject.get('type'):
return ''
if not postJsonObject['object'].get('type'):
return ''
if not postJsonObject['object'].get('published'): if not postJsonObject['object'].get('published'):
return '' return ''
if not postJsonObject['object'].get('id'): if not postJsonObject['object'].get('id'):
return '' return ''
if not postJsonObject['object'].get('content'): if not postJsonObject['object'].get('content'):
return '' return ''
prevConvPostId = \ if not postJsonObject['object'].get('attributedTo'):
previousConversationPostId(baseDir, nickname, domain,
postJsonObject)
if not prevConvPostId:
return '' return ''
prevConvPostFilename = \ if not isinstance(postJsonObject['object']['attributedTo'], str):
locatePost(baseDir, nickname, domain, prevConvPostId, False)
if not prevConvPostFilename:
return '' return ''
prevPostJsonObject = loadJson(prevConvPostFilename, 0) actor = postJsonObject['object']['attributedTo']
if not prevPostJsonObject: actorFilename = \
acctDir(baseDir, nickname, domain) + '/lastpost/' + \
actor.replace('/', '#')
if not os.path.isfile(actorFilename):
return '' return ''
if not hasObjectDict(prevPostJsonObject): postId = removeIdEnding(postJsonObject['object']['id'])
lastpostId = None
try:
with open(actorFilename, 'r') as fp:
lastpostId = fp.read()
except BaseException:
return '' return ''
if not prevPostJsonObject['object'].get('published'): if not lastpostId:
return '' return ''
if not prevPostJsonObject['object'].get('id'): if lastpostId == postId:
return '' return ''
if not prevPostJsonObject['object'].get('content'): lastpostFilename = \
locatePost(baseDir, nickname, domain, lastpostId, False)
if not lastpostFilename:
return '' return ''
if prevPostJsonObject['object']['id'] == postJsonObject['object']['id']: lastpostJson = loadJson(lastpostFilename, 0)
if not lastpostJson:
return '' return ''
id1 = removeIdEnding(prevPostJsonObject['object']['id']) if not lastpostJson.get('type'):
if '/' not in id1:
return '' return ''
id2 = removeIdEnding(postJsonObject['object']['id']) if lastpostJson['type'] != postJsonObject['type']:
if '/' not in id2:
return '' return ''
ending1 = id1.split('/')[-1] if not lastpostJson['object'].get('type'):
if not ending1:
return '' return ''
ending2 = id2.split('/')[-1] if lastpostJson['object']['type'] != postJsonObject['object']['type']:
if not ending2: return
if not lastpostJson['object'].get('published'):
return '' return ''
if id1.replace(ending1, '') != id2.replace(ending2, ''): if not lastpostJson['object'].get('id'):
return ''
if not lastpostJson['object'].get('content'):
return ''
if not lastpostJson['object'].get('attributedTo'):
return ''
if not isinstance(lastpostJson['object']['attributedTo'], str):
return '' return ''
timeDiffSeconds = \ timeDiffSeconds = \
secondsBetweenPublished(prevPostJsonObject['object']['published'], secondsBetweenPublished(lastpostJson['object']['published'],
postJsonObject['object']['published']) postJsonObject['object']['published'])
if timeDiffSeconds > maxTimeDiffSeconds: if timeDiffSeconds > maxTimeDiffSeconds:
return '' return ''
if debug: if debug:
print(id2 + ' might be an edit of ' + id1) print(postId + ' might be an edit of ' + lastpostId)
if wordsSimilarity(prevPostJsonObject['object']['content'], if wordsSimilarity(lastpostJson['object']['content'],
postJsonObject['object']['content'], 10) < 70: postJsonObject['object']['content'], 10) < 70:
return '' return ''
print(id2 + ' is an edit of ' + id1) print(postId + ' is an edit of ' + lastpostId)
return prevConvPostFilename return lastpostFilename
def getOriginalPostFromAnnounceUrl(announceUrl: str, baseDir: str, def getOriginalPostFromAnnounceUrl(announceUrl: str, baseDir: str,

View File

@ -296,7 +296,7 @@ def postJsonString(session, postJsonStr: str,
headers: {}, headers: {},
debug: bool, debug: bool,
timeoutSec: int = 30, timeoutSec: int = 30,
quiet: bool = False) -> (bool, bool): quiet: bool = False) -> (bool, bool, int):
"""Post a json message string to the inbox of another person """Post a json message string to the inbox of another person
The second boolean returned is true if the send is unauthorized The second boolean returned is true if the send is unauthorized
NOTE: Here we post a string rather than the original json so that NOTE: Here we post a string rather than the original json so that
@ -310,18 +310,18 @@ def postJsonString(session, postJsonStr: str,
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
if not quiet: if not quiet:
print('WARN: error during postJsonString requests ' + str(e)) print('WARN: error during postJsonString requests ' + str(e))
return None, None return None, None, 0
except SocketError as e: except SocketError as e:
if not quiet and e.errno == errno.ECONNRESET: if not quiet and e.errno == errno.ECONNRESET:
print('WARN: connection was reset during postJsonString') print('WARN: connection was reset during postJsonString')
if not quiet: if not quiet:
print('ERROR: postJsonString failed ' + inboxUrl + ' ' + print('ERROR: postJsonString failed ' + inboxUrl + ' ' +
postJsonStr + ' ' + str(headers)) postJsonStr + ' ' + str(headers))
return None, None return None, None, 0
except ValueError as e: except ValueError as e:
if not quiet: if not quiet:
print('WARN: error during postJsonString ' + str(e)) print('WARN: error during postJsonString ' + str(e))
return None, None return None, None, 0
if postResult.status_code < 200 or postResult.status_code > 202: if postResult.status_code < 200 or postResult.status_code > 202:
if postResult.status_code >= 400 and \ if postResult.status_code >= 400 and \
postResult.status_code <= 405 and \ postResult.status_code <= 405 and \
@ -330,14 +330,14 @@ def postJsonString(session, postJsonStr: str,
print('WARN: Post to ' + inboxUrl + print('WARN: Post to ' + inboxUrl +
' is unauthorized. Code ' + ' is unauthorized. Code ' +
str(postResult.status_code)) str(postResult.status_code))
return False, True return False, True, postResult.status_code
else: else:
if not quiet: if not quiet:
print('WARN: Failed to post to ' + inboxUrl + print('WARN: Failed to post to ' + inboxUrl +
' with headers ' + str(headers)) ' with headers ' + str(headers) +
print('status code ' + str(postResult.status_code)) ' status code ' + str(postResult.status_code))
return False, False return False, False, postResult.status_code
return True, False return True, False, 0
def postImage(session, attachImageFilename: str, federationList: [], def postImage(session, attachImageFilename: str, federationList: [],

View File

@ -1976,6 +1976,18 @@ def testSharedItemsFederation(baseDir: str) -> None:
assert 'DFC:supplies' in catalogJson assert 'DFC:supplies' in catalogJson
assert len(catalogJson.get('DFC:supplies')) == 3 assert len(catalogJson.get('DFC:supplies')) == 3
# queue item removed
ctr = 0
while len([name for name in os.listdir(queuePath)
if os.path.isfile(os.path.join(queuePath, name))]) > 0:
ctr += 1
if ctr > 10:
break
time.sleep(1)
# assert len([name for name in os.listdir(queuePath)
# if os.path.isfile(os.path.join(queuePath, name))]) == 0
# stop the servers # stop the servers
thrAlice.kill() thrAlice.kill()
thrAlice.join() thrAlice.join()
@ -1985,11 +1997,6 @@ def testSharedItemsFederation(baseDir: str) -> None:
thrBob.join() thrBob.join()
assert thrBob.is_alive() is False assert thrBob.is_alive() is False
# queue item removed
time.sleep(4)
assert len([name for name in os.listdir(queuePath)
if os.path.isfile(os.path.join(queuePath, name))]) == 0
os.chdir(baseDir) os.chdir(baseDir)
shutil.rmtree(baseDir + '/.tests') shutil.rmtree(baseDir + '/.tests')
print('Testing federation of shared items between ' + print('Testing federation of shared items between ' +
@ -4523,6 +4530,7 @@ def _testFunctions():
'runNewswireWatchdog', 'runNewswireWatchdog',
'runFederatedSharesWatchdog', 'runFederatedSharesWatchdog',
'runFederatedSharesDaemon', 'runFederatedSharesDaemon',
'fitnessThread',
'threadSendPost', 'threadSendPost',
'sendToFollowers', 'sendToFollowers',
'expireCache', 'expireCache',
@ -5743,6 +5751,16 @@ def _testWordsSimilarity() -> None:
"The world of the electron and the webkit, the beauty of the baud" "The world of the electron and the webkit, the beauty of the baud"
similarity = wordsSimilarity(content1, content2, minWords) similarity = wordsSimilarity(content1, content2, minWords)
assert similarity > 70 assert similarity > 70
content1 = "<p>We&apos;re growing! </p><p>A new denizen " + \
"is frequenting HackBucket. You probably know him already " + \
"from her epic typos - but let&apos;s not spoil too much " + \
"\ud83d\udd2e</p>"
content2 = "<p>We&apos;re growing! </p><p>A new denizen " + \
"is frequenting HackBucket. You probably know them already " + \
"from their epic typos - but let&apos;s not spoil too much " + \
"\ud83d\udd2e</p>"
similarity = wordsSimilarity(content1, content2, minWords)
assert similarity > 80
def runAllTests(): def runAllTests():

View File

@ -104,7 +104,7 @@ def _getThemeFiles() -> []:
return ('epicyon.css', 'login.css', 'follow.css', return ('epicyon.css', 'login.css', 'follow.css',
'suspended.css', 'calendar.css', 'blog.css', 'suspended.css', 'calendar.css', 'blog.css',
'options.css', 'search.css', 'links.css', 'options.css', 'search.css', 'links.css',
'welcome.css') 'welcome.css', 'graph.css')
def isNewsThemeName(baseDir: str, themeName: str) -> bool: def isNewsThemeName(baseDir: str, themeName: str) -> bool: