Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2020-08-27 21:27:12 +01:00
commit afecdb80d8
44 changed files with 1769 additions and 310 deletions

View File

@ -130,7 +130,7 @@ def storeBasicCredentials(baseDir: str, nickname: str, password: str) -> bool:
os.rename(passwordFile + '.new', passwordFile) os.rename(passwordFile + '.new', passwordFile)
else: else:
# append to password file # append to password file
with open(passwordFile, "a") as passfile: with open(passwordFile, 'a+') as passfile:
passfile.write(storeStr + '\n') passfile.write(storeStr + '\n')
else: else:
with open(passwordFile, "w") as passfile: with open(passwordFile, "w") as passfile:

View File

@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import os import os
from utils import removeIdEnding
from utils import isEvil from utils import isEvil
from utils import locatePost from utils import locatePost
from utils import evilIncarnate from utils import evilIncarnate
@ -214,7 +215,7 @@ def outboxBlock(baseDir: str, httpPrefix: str,
if debug: if debug:
print('DEBUG: c2s block request arrived in outbox') print('DEBUG: c2s block request arrived in outbox')
messageId = messageJson['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object'])
if '/statuses/' not in messageId: if '/statuses/' not in messageId:
if debug: if debug:
print('DEBUG: c2s block object is not a status') print('DEBUG: c2s block object is not a status')
@ -293,7 +294,7 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str,
if debug: if debug:
print('DEBUG: c2s undo block request arrived in outbox') print('DEBUG: c2s undo block request arrived in outbox')
messageId = messageJson['object']['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object']['object'])
if '/statuses/' not in messageId: if '/statuses/' not in messageId:
if debug: if debug:
print('DEBUG: c2s undo block object is not a status') print('DEBUG: c2s undo block object is not a status')

View File

@ -8,6 +8,7 @@ __status__ = "Production"
import os import os
from pprint import pprint from pprint import pprint
from utils import removeIdEnding
from utils import removePostFromCache from utils import removePostFromCache
from utils import urlPermitted from utils import urlPermitted
from utils import getNicknameFromActor from utils import getNicknameFromActor
@ -607,7 +608,7 @@ def outboxBookmark(recentPostsCache: {},
if debug: if debug:
print('DEBUG: c2s bookmark request arrived in outbox') print('DEBUG: c2s bookmark request arrived in outbox')
messageId = messageJson['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object'])
if ':' in domain: if ':' in domain:
domain = domain.split(':')[0] domain = domain.split(':')[0]
postFilename = locatePost(baseDir, nickname, domain, messageId) postFilename = locatePost(baseDir, nickname, domain, messageId)
@ -667,7 +668,7 @@ def outboxUndoBookmark(recentPostsCache: {},
if debug: if debug:
print('DEBUG: c2s undo bookmark request arrived in outbox') print('DEBUG: c2s undo bookmark request arrived in outbox')
messageId = messageJson['object']['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object']['object'])
if ':' in domain: if ':' in domain:
domain = domain.split(':')[0] domain = domain.split(':')[0]
postFilename = locatePost(baseDir, nickname, domain, messageId) postFilename = locatePost(baseDir, nickname, domain, messageId)

261
daemon.py
View File

@ -6,7 +6,7 @@ __maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net" __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer
import sys import sys
import json import json
import time import time
@ -69,6 +69,7 @@ from posts import createBlogPost
from posts import createReportPost from posts import createReportPost
from posts import createUnlistedPost from posts import createUnlistedPost
from posts import createFollowersOnlyPost from posts import createFollowersOnlyPost
from posts import createEventPost
from posts import createDirectMessagePost from posts import createDirectMessagePost
from posts import populateRepliesJson from posts import populateRepliesJson
from posts import addToField from posts import addToField
@ -126,6 +127,7 @@ from webinterface import htmlIndividualPost
from webinterface import htmlProfile from webinterface import htmlProfile
from webinterface import htmlInbox from webinterface import htmlInbox
from webinterface import htmlBookmarks from webinterface import htmlBookmarks
from webinterface import htmlEvents
from webinterface import htmlShares from webinterface import htmlShares
from webinterface import htmlOutbox from webinterface import htmlOutbox
from webinterface import htmlModeration from webinterface import htmlModeration
@ -153,6 +155,7 @@ from shares import getSharesFeedForPerson
from shares import addShare from shares import addShare
from shares import removeShare from shares import removeShare
from shares import expireShares from shares import expireShares
from utils import removeIdEnding
from utils import updateLikesCollection from utils import updateLikesCollection
from utils import undoLikesCollectionEntry from utils import undoLikesCollectionEntry
from utils import deletePost from utils import deletePost
@ -315,12 +318,14 @@ class PubServer(BaseHTTPRequestHandler):
print('Voting on message ' + messageId) print('Voting on message ' + messageId)
print('Vote for: ' + answer) print('Vote for: ' + answer)
commentsEnabled = True
messageJson = \ messageJson = \
createPublicPost(self.server.baseDir, createPublicPost(self.server.baseDir,
nickname, nickname,
self.server.domain, self.server.port, self.server.domain, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
answer, False, False, False, answer, False, False, False,
commentsEnabled,
None, None, None, True, None, None, None, True,
messageId, messageId, None, messageId, messageId, None,
False, None, None, None) False, None, None, None)
@ -2731,10 +2736,9 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkGETtimings(GETstartTime, GETtimings, 32) self._benchmarkGETtimings(GETstartTime, GETtimings, 32)
# unrepeatPrivate = False
if htmlGET and '?unrepeatprivate=' in self.path: if htmlGET and '?unrepeatprivate=' in self.path:
self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=') self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=')
# unrepeatPrivate = True
# undo an announce/repeat from the web interface # undo an announce/repeat from the web interface
if htmlGET and '?unrepeat=' in self.path: if htmlGET and '?unrepeat=' in self.path:
pageNumber = 1 pageNumber = 1
@ -3595,6 +3599,40 @@ class PubServer(BaseHTTPRequestHandler):
self.server.GETbusy = False self.server.GETbusy = False
return return
# Edit an event
if authorized and \
'/tlevents' in self.path and \
'?editeventpost=' in self.path and \
'?actor=' in self.path:
messageId = self.path.split('?editeventpost=')[1]
if '?' in messageId:
messageId = messageId.split('?')[0]
actor = self.path.split('?actor=')[1]
if '?' in actor:
actor = actor.split('?')[0]
nickname = getNicknameFromActor(self.path)
if nickname == actor:
postUrl = \
self.server.httpPrefix + '://' + \
self.server.domainFull + '/users/' + nickname + \
'/statuses/' + messageId
msg = None
# TODO
# htmlEditEvent(self.server.mediaInstance,
# self.server.translate,
# self.server.baseDir,
# self.server.httpPrefix,
# self.path,
# nickname, self.server.domain,
# postUrl)
if msg:
msg = msg.encode('utf-8')
self._set_headers('text/html', len(msg),
cookie, callingDomain)
self._write(msg)
self.server.GETbusy = False
return
# edit profile in web interface # edit profile in web interface
if '/users/' in self.path and self.path.endswith('/editprofile'): if '/users/' in self.path and self.path.endswith('/editprofile'):
msg = htmlEditProfile(self.server.translate, msg = htmlEditProfile(self.server.translate,
@ -3619,6 +3657,7 @@ class PubServer(BaseHTTPRequestHandler):
self.path.endswith('/newfollowers') or self.path.endswith('/newfollowers') or
self.path.endswith('/newdm') or self.path.endswith('/newdm') or
self.path.endswith('/newreminder') or self.path.endswith('/newreminder') or
self.path.endswith('/newevent') or
self.path.endswith('/newreport') or self.path.endswith('/newreport') or
self.path.endswith('/newquestion') or self.path.endswith('/newquestion') or
self.path.endswith('/newshare'))): self.path.endswith('/newshare'))):
@ -4796,7 +4835,7 @@ class PubServer(BaseHTTPRequestHandler):
else: else:
# don't need authenticated fetch here because # don't need authenticated fetch here because
# there is already the authorization check # there is already the authorization check
msg = json.dumps(inboxFeed, msg = json.dumps(bookmarksFeed,
ensure_ascii=False) ensure_ascii=False)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('application/json', self._set_headers('application/json',
@ -4819,6 +4858,103 @@ class PubServer(BaseHTTPRequestHandler):
self.server.GETbusy = False self.server.GETbusy = False
return return
# get the events for a given person
if self.path.endswith('/tlevents') or \
'/tlevents?page=' in self.path or \
self.path.endswith('/events') or \
'/events?page=' in self.path:
if '/users/' in self.path:
if authorized:
# convert /events to /tlevents
if self.path.endswith('/events') or \
'/events?page=' in self.path:
self.path = self.path.replace('/events', '/tlevents')
eventsFeed = \
personBoxJson(self.server.recentPostsCache,
self.server.session,
self.server.baseDir,
self.server.domain,
self.server.port,
self.path,
self.server.httpPrefix,
maxPostsInFeed, 'tlevents',
authorized, self.server.ocapAlways)
print('eventsFeed: ' + str(eventsFeed))
if eventsFeed:
if self._requestHTTP():
nickname = self.path.replace('/users/', '')
nickname = nickname.replace('/tlevents', '')
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
if 'page=' not in self.path:
# if no page was specified then show the first
eventsFeed = \
personBoxJson(self.server.recentPostsCache,
self.server.session,
self.server.baseDir,
self.server.domain,
self.server.port,
self.path + '?page=1',
self.server.httpPrefix,
maxPostsInFeed,
'tlevents',
authorized,
self.server.ocapAlways)
msg = \
htmlEvents(self.server.defaultTimeline,
self.server.recentPostsCache,
self.server.maxRecentPosts,
self.server.translate,
pageNumber, maxPostsInFeed,
self.server.session,
self.server.baseDir,
self.server.cachedWebfingers,
self.server.personCache,
nickname,
self.server.domain,
self.server.port,
eventsFeed,
self.server.allowDeletion,
self.server.httpPrefix,
self.server.projectVersion,
self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8')
self._set_headers('text/html',
len(msg),
cookie, callingDomain)
self._write(msg)
else:
# don't need authenticated fetch here because
# there is already the authorization check
msg = json.dumps(eventsFeed,
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
else:
if self.server.debug:
nickname = self.path.replace('/users/', '')
nickname = nickname.replace('/tlevents', '')
print('DEBUG: ' + nickname +
' was not authorized to access ' + self.path)
if self.server.debug:
print('DEBUG: GET access to events is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy = False
return
self._benchmarkGETtimings(GETstartTime, GETtimings, 47) self._benchmarkGETtimings(GETstartTime, GETtimings, 47)
# get outbox feed for a person # get outbox feed for a person
@ -5486,11 +5622,13 @@ class PubServer(BaseHTTPRequestHandler):
fields['subject'] = None fields['subject'] = None
if not fields.get('replyTo'): if not fields.get('replyTo'):
fields['replyTo'] = None fields['replyTo'] = None
if not fields.get('schedulePost'): if not fields.get('schedulePost'):
fields['schedulePost'] = False fields['schedulePost'] = False
else: else:
fields['schedulePost'] = True fields['schedulePost'] = True
print('DEBUG: shedulePost ' + str(fields['schedulePost'])) print('DEBUG: shedulePost ' + str(fields['schedulePost']))
if not fields.get('eventDate'): if not fields.get('eventDate'):
fields['eventDate'] = None fields['eventDate'] = None
if not fields.get('eventTime'): if not fields.get('eventTime'):
@ -5515,6 +5653,14 @@ class PubServer(BaseHTTPRequestHandler):
mentionsStr = '' mentionsStr = ''
if fields.get('mentions'): if fields.get('mentions'):
mentionsStr = fields['mentions'].strip() + ' ' mentionsStr = fields['mentions'].strip() + ' '
if not fields.get('commentsEnabled'):
commentsEnabled = False
else:
commentsEnabled = True
if not fields.get('privateEvent'):
privateEvent = False
else:
privateEvent = True
if postType == 'newpost': if postType == 'newpost':
messageJson = \ messageJson = \
createPublicPost(self.server.baseDir, createPublicPost(self.server.baseDir,
@ -5523,7 +5669,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.port, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
mentionsStr + fields['message'], mentionsStr + fields['message'],
False, False, False, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5550,7 +5696,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.domain, self.server.port, self.server.domain, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
fields['message'], fields['message'],
False, False, False, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5653,7 +5799,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.domain, self.server.port, self.server.domain, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
mentionsStr + fields['message'], mentionsStr + fields['message'],
False, False, False, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5686,6 +5832,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.httpPrefix, self.server.httpPrefix,
mentionsStr + fields['message'], mentionsStr + fields['message'],
True, False, False, True, False, False,
commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5709,6 +5856,60 @@ class PubServer(BaseHTTPRequestHandler):
return 1 return 1
else: else:
return -1 return -1
elif postType == 'newevent':
# A Mobilizon-type event is posted
# if there is no image dscription then make it the same
# as the event title
if not fields.get('imageDescription'):
fields['imageDescription'] = fields['subject']
# Events are public by default, with opt-in
# followers only status
if not fields.get('followersOnlyEvent'):
fields['followersOnlyEvent'] = False
if not fields.get('anonymousParticipationEnabled'):
anonymousParticipationEnabled = False
else:
anonymousParticipationEnabled = True
maximumAttendeeCapacity = 999999
if fields.get('maximumAttendeeCapacity'):
maximumAttendeeCapacity = \
int(fields['maximumAttendeeCapacity'])
messageJson = \
createEventPost(self.server.baseDir,
nickname,
self.server.domain,
self.server.port,
self.server.httpPrefix,
mentionsStr + fields['message'],
privateEvent,
False, False, commentsEnabled,
filename, attachmentMediaType,
fields['imageDescription'],
self.server.useBlurHash,
fields['subject'],
fields['schedulePost'],
fields['eventDate'],
fields['eventTime'],
fields['location'],
fields['category'],
fields['joinMode'],
fields['endDate'],
fields['endTime'],
maximumAttendeeCapacity,
fields['repliesModerationOption'],
anonymousParticipationEnabled,
fields['eventStatus'],
fields['ticketUrl'])
if messageJson:
if fields['schedulePost']:
return 1
if self._postToOutbox(messageJson, __version__, nickname):
return 1
else:
return -1
elif postType == 'newdm': elif postType == 'newdm':
messageJson = None messageJson = None
print('A DM was posted') print('A DM was posted')
@ -5722,6 +5923,7 @@ class PubServer(BaseHTTPRequestHandler):
mentionsStr + mentionsStr +
fields['message'], fields['message'],
True, False, False, True, False, False,
commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5761,7 +5963,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.port, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
mentionsStr + fields['message'], mentionsStr + fields['message'],
True, False, False, True, False, False, False,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5794,7 +5996,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.domain, self.server.port, self.server.domain, self.server.port,
self.server.httpPrefix, self.server.httpPrefix,
mentionsStr + fields['message'], mentionsStr + fields['message'],
True, False, False, True, False, False, True,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -5825,6 +6027,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.httpPrefix, self.server.httpPrefix,
fields['message'], qOptions, fields['message'], qOptions,
False, False, False, False, False, False,
commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash, self.server.useBlurHash,
@ -6181,6 +6384,7 @@ class PubServer(BaseHTTPRequestHandler):
if not self.path.endswith('confirm'): if not self.path.endswith('confirm'):
self.path = self.path.replace('/outbox/', '/outbox') self.path = self.path.replace('/outbox/', '/outbox')
self.path = self.path.replace('/tlblogs/', '/tlblogs') self.path = self.path.replace('/tlblogs/', '/tlblogs')
self.path = self.path.replace('/tlevents/', '/tlevents')
self.path = self.path.replace('/inbox/', '/inbox') self.path = self.path.replace('/inbox/', '/inbox')
self.path = self.path.replace('/shares/', '/shares') self.path = self.path.replace('/shares/', '/shares')
self.path = self.path.replace('/sharedInbox/', '/sharedInbox') self.path = self.path.replace('/sharedInbox/', '/sharedInbox')
@ -6912,6 +7116,20 @@ class PubServer(BaseHTTPRequestHandler):
if not removeTwitterActive: if not removeTwitterActive:
if os.path.isfile(removeTwitterFilename): if os.path.isfile(removeTwitterFilename):
os.remove(removeTwitterFilename) os.remove(removeTwitterFilename)
# notify about new Likes
notifyLikesFilename = \
self.server.baseDir + '/accounts/' + \
nickname + '@' + self.server.domain + \
'/.notifyLikes'
notifyLikesActive = False
if fields.get('notifyLikes'):
if fields['notifyLikes'] == 'on':
notifyLikesActive = True
with open(notifyLikesFilename, "w") as rFile:
rFile.write('\n')
if not notifyLikesActive:
if os.path.isfile(notifyLikesFilename):
os.remove(notifyLikesFilename)
# this account is a bot # this account is a bot
if fields.get('isBot'): if fields.get('isBot'):
if fields['isBot'] == 'on': if fields['isBot'] == 'on':
@ -8369,15 +8587,16 @@ class PubServer(BaseHTTPRequestHandler):
# receive different types of post created by htmlNewPost # receive different types of post created by htmlNewPost
postTypes = ("newpost", "newblog", "newunlisted", "newfollowers", postTypes = ("newpost", "newblog", "newunlisted", "newfollowers",
"newdm", "newreport", "newshare", "newquestion", "newdm", "newreport", "newshare", "newquestion",
"editblogpost", "newreminder") "editblogpost", "newreminder", "newevent")
for currPostType in postTypes: for currPostType in postTypes:
if not authorized: if not authorized:
break break
if currPostType != 'newshare':
postRedirect = self.server.defaultTimeline postRedirect = self.server.defaultTimeline
else: if currPostType == 'newshare':
postRedirect = 'shares' postRedirect = 'shares'
elif currPostType == 'newevent':
postRedirect = 'tlevents'
pageNumber = self._receiveNewPost(currPostType, self.path) pageNumber = self._receiveNewPost(currPostType, self.path)
if pageNumber: if pageNumber:
@ -8612,8 +8831,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.outboxAuthenticated: if self.outboxAuthenticated:
if self._postToOutbox(messageJson, __version__): if self._postToOutbox(messageJson, __version__):
if messageJson.get('id'): if messageJson.get('id'):
locnStr = messageJson['id'].replace('/activity', '') locnStr = removeIdEnding(messageJson['id'])
locnStr = locnStr.replace('/undo', '')
self.headers['Location'] = locnStr self.headers['Location'] = locnStr
self.send_response(201) self.send_response(201)
self.end_headers() self.end_headers()
@ -8658,6 +8876,7 @@ class PubServer(BaseHTTPRequestHandler):
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 22) self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 22)
if not self.server.unitTest:
if not inboxPermittedMessage(self.server.domain, if not inboxPermittedMessage(self.server.domain,
messageJson, messageJson,
self.server.federationList): self.server.federationList):
@ -8711,6 +8930,17 @@ class PubServerUnitTest(PubServer):
protocol_version = 'HTTP/1.0' protocol_version = 'HTTP/1.0'
class EpicyonServer(ThreadingHTTPServer):
def handle_error(self, request, client_address):
# surpress connection reset errors
cls, e = sys.exc_info()[:2]
if cls is ConnectionResetError:
print('ERROR: ' + str(cls) + ", " + str(e))
pass
else:
return HTTPServer.handle_error(self, request, client_address)
def runPostsQueue(baseDir: str, sendThreads: [], debug: bool) -> None: def runPostsQueue(baseDir: str, sendThreads: [], debug: bool) -> None:
"""Manages the threads used to send posts """Manages the threads used to send posts
""" """
@ -8812,7 +9042,7 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
pubHandler = partial(PubServer) pubHandler = partial(PubServer)
try: try:
httpd = ThreadingHTTPServer(serverAddress, pubHandler) httpd = EpicyonServer(serverAddress, pubHandler)
except Exception as e: except Exception as e:
if e.errno == 98: if e.errno == 98:
print('ERROR: HTTP server address is already in use. ' + print('ERROR: HTTP server address is already in use. ' +
@ -8822,6 +9052,7 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
print('ERROR: HTTP server failed to start. ' + str(e)) print('ERROR: HTTP server failed to start. ' + str(e))
return False return False
httpd.unitTest = unitTest
httpd.YTReplacementDomain = YTReplacementDomain httpd.YTReplacementDomain = YTReplacementDomain
# This counter is used to update the list of blocked domains in memory. # This counter is used to update the list of blocked domains in memory.

View File

@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net" __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
from utils import removeIdEnding
from utils import getStatusNumber from utils import getStatusNumber
from utils import urlPermitted from utils import urlPermitted
from utils import getNicknameFromActor from utils import getNicknameFromActor
@ -257,7 +258,7 @@ def outboxDelete(baseDir: str, httpPrefix: str,
if debug: if debug:
print('DEBUG: delete not permitted from other instances') print('DEBUG: delete not permitted from other instances')
return return
messageId = messageJson['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object'])
if '/statuses/' not in messageId: if '/statuses/' not in messageId:
if debug: if debug:
print('DEBUG: c2s delete object is not a status') print('DEBUG: c2s delete object is not a status')

View File

@ -1269,6 +1269,26 @@ aside .toggle-inside li {
padding: 10px; padding: 10px;
margin: 20px 30px; margin: 20px 30px;
} }
input[type=radio]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 20px 30px;
}
input[type=number]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 20px 60px;
}
} }
@media screen and (min-width: 2200px) { @media screen and (min-width: 2200px) {
@ -1689,4 +1709,24 @@ aside .toggle-inside li {
padding: 20px; padding: 20px;
margin: 30px 40px; margin: 30px 40px;
} }
input[type=radio]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 20px;
margin: 30px 40px;
}
input[type=number]
{
-ms-transform: scale(2);
-moz-transform: scale(2);
-webkit-transform: scale(2);
-o-transform: scale(2);
transform: scale(2);
padding: 10px;
margin: 40px 80px;
}
} }

View File

@ -108,7 +108,7 @@ parser.add_argument('-p', '--port', dest='port', type=int,
default=None, default=None,
help='Port number to run on') help='Port number to run on')
parser.add_argument('--postcache', dest='maxRecentPosts', type=int, parser.add_argument('--postcache', dest='maxRecentPosts', type=int,
default=100, default=512,
help='The maximum number of recent posts to store in RAM') help='The maximum number of recent posts to store in RAM')
parser.add_argument('--proxy', dest='proxyPort', type=int, default=None, parser.add_argument('--proxy', dest='proxyPort', type=int, default=None,
help='Proxy port number to run on') help='Proxy port number to run on')
@ -166,6 +166,11 @@ parser.add_argument('--json', dest='json', type=str, default=None,
help='Show the json for a given activitypub url') help='Show the json for a given activitypub url')
parser.add_argument('-f', '--federate', nargs='+', dest='federationList', parser.add_argument('-f', '--federate', nargs='+', dest='federationList',
help='Specify federation list separated by spaces') help='Specify federation list separated by spaces')
parser.add_argument("--repliesEnabled", "--commentsEnabled",
dest='commentsEnabled',
type=str2bool, nargs='?',
const=True, default=True,
help="Enable replies to a post")
parser.add_argument("--noapproval", type=str2bool, nargs='?', parser.add_argument("--noapproval", type=str2bool, nargs='?',
const=True, default=False, const=True, default=False,
help="Allow followers without approval") help="Allow followers without approval")
@ -829,7 +834,7 @@ if args.message:
domain, port, domain, port,
toNickname, toDomain, toPort, ccUrl, toNickname, toDomain, toPort, ccUrl,
httpPrefix, sendMessage, followersOnly, httpPrefix, sendMessage, followersOnly,
attach, mediaType, args.commentsEnabled, attach, mediaType,
attachedImageDescription, useBlurhash, attachedImageDescription, useBlurhash,
cachedWebfingers, personCache, isArticle, cachedWebfingers, personCache, isArticle,
args.debug, replyTo, replyTo, subject) args.debug, replyTo, replyTo, subject)
@ -1751,30 +1756,31 @@ if args.testdata:
deleteAllPosts(baseDir, nickname, domain, 'outbox') deleteAllPosts(baseDir, nickname, domain, 'outbox')
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"like, this is totally just a #test, man", "like, this is totally just a #test, man",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Zoiks!!!", "Zoiks!!!",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Hey scoob we need like a hundred more #milkshakes", "Hey scoob we need like a hundred more #milkshakes",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Getting kinda spooky around here", "Getting kinda spooky around here",
False, True, False, None, None, useBlurhash, 'someone') False, True, False, True, None, None,
useBlurhash, 'someone')
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"And they would have gotten away with it too" + "And they would have gotten away with it too" +
"if it wasn't for those pesky hackers", "if it wasn't for those pesky hackers",
False, True, False, 'img/logo.png', False, True, False, True, 'img/logo.png',
'Description of image', useBlurhash) 'Description of image', useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"man, these centralized sites are, like, the worst!", "man, these centralized sites are, like, the worst!",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"another mystery solved #test", "another mystery solved #test",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"let's go bowling", "let's go bowling",
False, True, False, None, None, useBlurhash) False, True, False, True, None, None, useBlurhash)
domainFull = domain + ':' + str(port) domainFull = domain + ':' + str(port)
clearFollows(baseDir, nickname, domain) clearFollows(baseDir, nickname, domain)

View File

@ -202,14 +202,14 @@ def unfollowPerson(baseDir: str, nickname: str, domain: str,
if debug: if debug:
print('DEBUG: follow file ' + filename + ' was not found') print('DEBUG: follow file ' + filename + ' was not found')
return False return False
if handleToUnfollow.lower() not in open(filename).read().lower(): handleToUnfollowLower = handleToUnfollow.lower()
if handleToUnfollowLower not in open(filename).read().lower():
if debug: if debug:
print('DEBUG: handle to unfollow ' + handleToUnfollow + print('DEBUG: handle to unfollow ' + handleToUnfollow +
' is not in ' + filename) ' is not in ' + filename)
return return
with open(filename, "r") as f: with open(filename, "r") as f:
lines = f.readlines() lines = f.readlines()
handleToUnfollowLower = handleToUnfollow.lower()
with open(filename, "w") as f: with open(filename, "w") as f:
for line in lines: for line in lines:
if line.strip("\n").strip("\r").lower() != handleToUnfollowLower: if line.strip("\n").strip("\r").lower() != handleToUnfollowLower:
@ -520,7 +520,7 @@ def storeFollowRequest(baseDir: str,
approveFollowsFilename = accountsDir + '/followrequests.txt' approveFollowsFilename = accountsDir + '/followrequests.txt'
if os.path.isfile(approveFollowsFilename): if os.path.isfile(approveFollowsFilename):
if approveHandle not in open(approveFollowsFilename).read(): if approveHandle not in open(approveFollowsFilename).read():
with open(approveFollowsFilename, "a") as fp: with open(approveFollowsFilename, 'a+') as fp:
fp.write(approveHandle + '\n') fp.write(approveHandle + '\n')
else: else:
if debug: if debug:

View File

@ -43,12 +43,14 @@ def removeEventFromTimeline(eventId: str, tlEventsFilename: str) -> None:
pass pass
def saveEvent(baseDir: str, handle: str, postId: str, def saveEventPost(baseDir: str, handle: str, postId: str,
eventJson: {}) -> bool: eventJson: {}) -> bool:
"""Saves an event to the calendar and/or the events timeline """Saves an event to the calendar and/or the events timeline
If an event has extra fields, as per Mobilizon, If an event has extra fields, as per Mobilizon,
Then it is saved as a separate entity and added to the Then it is saved as a separate entity and added to the
events timeline events timeline
See https://framagit.org/framasoft/mobilizon/-/blob/
master/lib/federation/activity_stream/converter/event.ex
""" """
calendarPath = baseDir + '/accounts/' + handle + '/calendar' calendarPath = baseDir + '/accounts/' + handle + '/calendar'
if not os.path.isdir(calendarPath): if not os.path.isdir(calendarPath):
@ -71,6 +73,7 @@ def saveEvent(baseDir: str, handle: str, postId: str,
eventJson.get('uuid') and eventJson.get('content'): eventJson.get('uuid') and eventJson.get('content'):
if not validUuid(eventJson['uuid']): if not validUuid(eventJson['uuid']):
return False return False
print('Mobilizon type event')
# if this is a full description of an event then save it # if this is a full description of an event then save it
# as a separate json file # as a separate json file
eventsPath = baseDir + '/accounts/' + handle + '/events' eventsPath = baseDir + '/accounts/' + handle + '/events'

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

221
inbox.py
View File

@ -10,6 +10,8 @@ import json
import os import os
import datetime import datetime
import time import time
from utils import isEventPost
from utils import removeIdEnding
from utils import getProtocolPrefixes from utils import getProtocolPrefixes
from utils import isBlogPost from utils import isBlogPost
from utils import removeAvatarFromCache from utils import removeAvatarFromCache
@ -49,9 +51,11 @@ from filters import isFiltered
from announce import updateAnnounceCollection from announce import updateAnnounceCollection
from announce import undoAnnounceCollectionEntry from announce import undoAnnounceCollectionEntry
from httpsig import messageContentDigest from httpsig import messageContentDigest
from posts import validContentWarning
from posts import downloadAnnounce from posts import downloadAnnounce
from posts import isDM from posts import isDM
from posts import isReply from posts import isReply
from posts import isMuted
from posts import isImageMedia from posts import isImageMedia
from posts import sendSignedJson from posts import sendSignedJson
from posts import sendToFollowersThread from posts import sendToFollowersThread
@ -64,7 +68,7 @@ from git import isGitPatch
from git import receiveGitPatch from git import receiveGitPatch
from followingCalendar import receivingCalendarEvents from followingCalendar import receivingCalendarEvents
from content import dangerousMarkup from content import dangerousMarkup
from happening import saveEvent from happening import saveEventPost
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
@ -93,7 +97,7 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
continue continue
tagName = tag['name'].replace('#', '').strip() tagName = tag['name'].replace('#', '').strip()
tagsFilename = tagsDir + '/' + tagName + '.txt' tagsFilename = tagsDir + '/' + tagName + '.txt'
postUrl = postJsonObject['id'].replace('/activity', '') postUrl = removeIdEnding(postJsonObject['id'])
postUrl = postUrl.replace('/', '#') postUrl = postUrl.replace('/', '#')
daysDiff = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1) daysDiff = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
daysSinceEpoch = daysDiff.days daysSinceEpoch = daysDiff.days
@ -122,12 +126,13 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
session, cachedWebfingers: {}, personCache: {}, session, cachedWebfingers: {}, personCache: {},
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
postJsonObject: {}, postJsonObject: {},
allowDeletion: bool) -> None: allowDeletion: bool, boxname: str) -> None:
"""Converts the json post into html and stores it in a cache """Converts the json post into html and stores it in a cache
This enables the post to be quickly displayed later This enables the post to be quickly displayed later
""" """
pageNumber = -999 pageNumber = -999
avatarUrl = None avatarUrl = None
if boxname != 'tlevents' and boxname != 'outbox':
boxName = 'inbox' boxName = 'inbox'
individualPostAsHtml(recentPostsCache, maxRecentPosts, individualPostAsHtml(recentPostsCache, maxRecentPosts,
getIconsDir(baseDir), translate, pageNumber, getIconsDir(baseDir), translate, pageNumber,
@ -230,9 +235,11 @@ def getPersonPubKey(baseDir: str, session, personUrl: str,
def inboxMessageHasParams(messageJson: {}) -> bool: def inboxMessageHasParams(messageJson: {}) -> bool:
"""Checks whether an incoming message contains expected parameters """Checks whether an incoming message contains expected parameters
""" """
expectedParams = ['type', 'actor', 'object'] expectedParams = ['actor', 'type', 'object']
for param in expectedParams: for param in expectedParams:
if not messageJson.get(param): if not messageJson.get(param):
# print('inboxMessageHasParams: ' +
# param + ' ' + str(messageJson))
return False return False
if not messageJson.get('to'): if not messageJson.get('to'):
allowedWithoutToParam = ['Like', 'Follow', 'Request', allowedWithoutToParam = ['Like', 'Follow', 'Request',
@ -248,6 +255,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
""" """
if not messageJson.get('actor'): if not messageJson.get('actor'):
return False return False
actor = messageJson['actor'] actor = messageJson['actor']
# always allow the local domain # always allow the local domain
if domain in actor: if domain in actor:
@ -354,15 +362,13 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
return None return None
originalPostId = None originalPostId = None
if postJsonObject.get('id'): if postJsonObject.get('id'):
originalPostId = \ originalPostId = removeIdEnding(postJsonObject['id'])
postJsonObject['id'].replace('/activity', '').replace('/undo', '')
currTime = datetime.datetime.utcnow() currTime = datetime.datetime.utcnow()
postId = None postId = None
if postJsonObject.get('id'): if postJsonObject.get('id'):
postId = postJsonObject['id'].replace('/activity', '') postId = removeIdEnding(postJsonObject['id'])
postId = postId.replace('/undo', '')
published = currTime.strftime("%Y-%m-%dT%H:%M:%SZ") published = currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
if not postId: if not postId:
statusNumber, published = getStatusNumber() statusNumber, published = getStatusNumber()
@ -706,9 +712,8 @@ def receiveUndoFollow(session, baseDir: str, httpPrefix: str,
nicknameFollowing, domainFollowingFull, nicknameFollowing, domainFollowingFull,
nicknameFollower, domainFollowerFull, nicknameFollower, domainFollowerFull,
debug): debug):
if debug: print(nicknameFollowing + '@' + domainFollowingFull + ': '
print('DEBUG: Follower ' + 'Follower ' + nicknameFollower + '@' + domainFollowerFull +
nicknameFollower + '@' + domainFollowerFull +
' was removed') ' was removed')
return True return True
@ -771,6 +776,28 @@ def receiveUndo(session, baseDir: str, httpPrefix: str,
return False return False
def receiveEventPost(recentPostsCache: {}, session, baseDir: str,
httpPrefix: str, domain: str, port: int,
sendThreads: [], postLog: [], cachedWebfingers: {},
personCache: {}, messageJson: {}, federationList: [],
nickname: str, debug: bool) -> bool:
"""Receive a mobilizon-type event activity
See https://framagit.org/framasoft/mobilizon/-/blob/
master/lib/federation/activity_stream/converter/event.ex
"""
if not isEventPost(messageJson):
return
print('Receiving event: ' + str(messageJson['object']))
handle = nickname + '@' + domain
if port:
if port != 80 and port != 443:
handle += ':' + str(port)
postId = removeIdEnding(messageJson['id']).replace('/', '#')
saveEventPost(baseDir, handle, postId, messageJson['object'])
def personReceiveUpdate(baseDir: str, def personReceiveUpdate(baseDir: str,
domain: str, port: int, domain: str, port: int,
updateNickname: str, updateDomain: str, updateNickname: str, updateDomain: str,
@ -857,7 +884,7 @@ def receiveUpdateToQuestion(recentPostsCache: {}, messageJson: {},
return return
if not messageJson.get('actor'): if not messageJson.get('actor'):
return return
messageId = messageJson['id'].replace('/activity', '') messageId = removeIdEnding(messageJson['id'])
if '#' in messageId: if '#' in messageId:
messageId = messageId.split('#', 1)[0] messageId = messageId.split('#', 1)[0]
# find the question post # find the question post
@ -1314,8 +1341,7 @@ def receiveDelete(session, handle: str, isGroup: bool, baseDir: str,
if not os.path.isdir(baseDir + '/accounts/' + handle): if not os.path.isdir(baseDir + '/accounts/' + handle):
print('DEBUG: unknown recipient of like - ' + handle) print('DEBUG: unknown recipient of like - ' + handle)
# if this post in the outbox of the person? # if this post in the outbox of the person?
messageId = messageJson['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object'])
messageId = messageId.replace('/undo', '')
removeModerationPostFromIndex(baseDir, messageId, debug) removeModerationPostFromIndex(baseDir, messageId, debug)
postFilename = locatePost(baseDir, handle.split('@')[0], postFilename = locatePost(baseDir, handle.split('@')[0],
handle.split('@')[1], messageId) handle.split('@')[1], messageId)
@ -1532,6 +1558,28 @@ def receiveUndoAnnounce(recentPostsCache: {},
return True return True
def jsonPostAllowsComments(postJsonObject: {}) -> bool:
"""Returns true if the given post allows comments/replies
"""
if 'commentsEnabled' in postJsonObject:
return postJsonObject['commentsEnabled']
if postJsonObject.get('object'):
if not isinstance(postJsonObject['object'], dict):
return False
if 'commentsEnabled' in postJsonObject['object']:
return postJsonObject['object']['commentsEnabled']
return True
def postAllowsComments(postFilename: str) -> bool:
"""Returns true if the given post allows comments/replies
"""
postJsonObject = loadJson(postFilename)
if not postJsonObject:
return False
return jsonPostAllowsComments(postJsonObject)
def populateReplies(baseDir: str, httpPrefix: str, domain: str, def populateReplies(baseDir: str, httpPrefix: str, domain: str,
messageJson: {}, maxReplies: int, debug: bool) -> bool: messageJson: {}, maxReplies: int, debug: bool) -> bool:
"""Updates the list of replies for a post on this domain if """Updates the list of replies for a post on this domain if
@ -1572,16 +1620,19 @@ def populateReplies(baseDir: str, httpPrefix: str, domain: str,
if debug: if debug:
print('DEBUG: post may have expired - ' + replyTo) print('DEBUG: post may have expired - ' + replyTo)
return False return False
if not postAllowsComments(postFilename):
if debug:
print('DEBUG: post does not allow comments - ' + replyTo)
return False
# populate a text file containing the ids of replies # populate a text file containing the ids of replies
postRepliesFilename = postFilename.replace('.json', '.replies') postRepliesFilename = postFilename.replace('.json', '.replies')
messageId = messageJson['id'].replace('/activity', '') messageId = removeIdEnding(messageJson['id'])
messageId = messageId.replace('/undo', '')
if os.path.isfile(postRepliesFilename): if os.path.isfile(postRepliesFilename):
numLines = sum(1 for line in open(postRepliesFilename)) numLines = sum(1 for line in open(postRepliesFilename))
if numLines > maxReplies: if numLines > maxReplies:
return False return False
if messageId not in open(postRepliesFilename).read(): if messageId not in open(postRepliesFilename).read():
repliesFile = open(postRepliesFilename, "a") repliesFile = open(postRepliesFilename, 'a+')
repliesFile.write(messageId + '\n') repliesFile.write(messageId + '\n')
repliesFile.close() repliesFile.close()
else: else:
@ -1624,6 +1675,15 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
if 'Z' not in messageJson['object']['published']: if 'Z' not in messageJson['object']['published']:
return False return False
if messageJson['object'].get('summary'):
summary = messageJson['object']['summary']
if not isinstance(summary, str):
print('WARN: content warning is not a string')
return False
if summary != validContentWarning(summary):
print('WARN: invalid content warning ' + summary)
return False
if isGitPatch(baseDir, nickname, domain, if isGitPatch(baseDir, nickname, domain,
messageJson['object']['type'], messageJson['object']['type'],
messageJson['object']['summary'], messageJson['object']['summary'],
@ -1667,6 +1727,16 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
messageJson['object']['content']): messageJson['object']['content']):
print('REJECT: content filtered') print('REJECT: content filtered')
return False return False
if messageJson['object'].get('inReplyTo'):
if isinstance(messageJson['object']['inReplyTo'], str):
originalPostId = messageJson['object']['inReplyTo']
postPostFilename = locatePost(baseDir, nickname, domain,
originalPostId)
if postPostFilename:
if not postAllowsComments(postPostFilename):
print('REJECT: reply to post which does not ' +
'allow comments: ' + originalPostId)
return False
print('ACCEPT: post content is valid') print('ACCEPT: post content is valid')
return True return True
@ -1778,8 +1848,12 @@ def likeNotify(baseDir: str, domain: str, onionDomain: str,
return return
accountDir = baseDir + '/accounts/' + handle accountDir = baseDir + '/accounts/' + handle
if not os.path.isdir(accountDir):
# are like notifications enabled?
notifyLikesEnabledFilename = accountDir + '/.notifyLikes'
if not os.path.isfile(notifyLikesEnabledFilename):
return return
likeFile = accountDir + '/.newLike' likeFile = accountDir + '/.newLike'
if os.path.isfile(likeFile): if os.path.isfile(likeFile):
if '##sent##' not in open(likeFile).read(): if '##sent##' not in open(likeFile).read():
@ -1981,8 +2055,7 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None:
actorNickname, actorDomain): actorNickname, actorDomain):
return return
postId = \ postId = removeIdEnding(postJsonObject['id']).replace('/', '#')
postJsonObject['id'].replace('/activity', '').replace('/', '#')
# look for events within the tags list # look for events within the tags list
for tagDict in postJsonObject['object']['tag']: for tagDict in postJsonObject['object']['tag']:
@ -1992,7 +2065,7 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None:
continue continue
if not tagDict.get('startTime'): if not tagDict.get('startTime'):
continue continue
saveEvent(baseDir, handle, postId, tagDict) saveEventPost(baseDir, handle, postId, tagDict)
def inboxUpdateIndex(boxname: str, baseDir: str, handle: str, def inboxUpdateIndex(boxname: str, baseDir: str, handle: str,
@ -2171,12 +2244,18 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if validPostContent(baseDir, nickname, domain, if validPostContent(baseDir, nickname, domain,
postJsonObject, maxMentions, maxEmoji): postJsonObject, maxMentions, maxEmoji):
if postJsonObject.get('object'):
jsonObj = postJsonObject['object']
if not isinstance(jsonObj, dict):
jsonObj = None
else:
jsonObj = postJsonObject
# check for incoming git patches # check for incoming git patches
if isinstance(postJsonObject['object'], dict): if jsonObj:
if postJsonObject['object'].get('content') and \ if jsonObj.get('content') and \
postJsonObject['object'].get('summary') and \ jsonObj.get('summary') and \
postJsonObject['object'].get('attributedTo'): jsonObj.get('attributedTo'):
attributedTo = postJsonObject['object']['attributedTo'] attributedTo = jsonObj['attributedTo']
if isinstance(attributedTo, str): if isinstance(attributedTo, str):
fromNickname = getNicknameFromActor(attributedTo) fromNickname = getNicknameFromActor(attributedTo)
fromDomain, fromPort = getDomainFromActor(attributedTo) fromDomain, fromPort = getDomainFromActor(attributedTo)
@ -2184,17 +2263,17 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if fromPort != 80 and fromPort != 443: if fromPort != 80 and fromPort != 443:
fromDomain += ':' + str(fromPort) fromDomain += ':' + str(fromPort)
if receiveGitPatch(baseDir, nickname, domain, if receiveGitPatch(baseDir, nickname, domain,
postJsonObject['object']['type'], jsonObj['type'],
postJsonObject['object']['summary'], jsonObj['summary'],
postJsonObject['object']['content'], jsonObj['content'],
fromNickname, fromDomain): fromNickname, fromDomain):
gitPatchNotify(baseDir, handle, gitPatchNotify(baseDir, handle,
postJsonObject['object']['summary'], jsonObj['summary'],
postJsonObject['object']['content'], jsonObj['content'],
fromNickname, fromDomain) fromNickname, fromDomain)
elif '[PATCH]' in postJsonObject['object']['content']: elif '[PATCH]' in jsonObj['content']:
print('WARN: git patch not accepted - ' + print('WARN: git patch not accepted - ' +
postJsonObject['object']['summary']) jsonObj['summary'])
return False return False
# replace YouTube links, so they get less tracking data # replace YouTube links, so they get less tracking data
@ -2224,6 +2303,8 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
postJsonObject, debug, postJsonObject, debug,
__version__) __version__)
isReplyToMutedPost = False
if not isGroup: if not isGroup:
# create a DM notification file if needed # create a DM notification file if needed
postIsDM = isDM(postJsonObject) postIsDM = isDM(postJsonObject)
@ -2274,9 +2355,13 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if nickname != 'inbox': if nickname != 'inbox':
# replies index will be updated # replies index will be updated
updateIndexList.append('tlreplies') updateIndexList.append('tlreplies')
if not isMuted(baseDir, nickname, domain,
postJsonObject['object']['inReplyTo']):
replyNotify(baseDir, handle, replyNotify(baseDir, handle,
httpPrefix + '://' + domain + httpPrefix + '://' + domain +
'/users/' + nickname + '/tlreplies') '/users/' + nickname + '/tlreplies')
else:
isReplyToMutedPost = True
if isImageMedia(session, baseDir, httpPrefix, if isImageMedia(session, baseDir, httpPrefix,
nickname, domain, postJsonObject, nickname, domain, postJsonObject,
@ -2286,6 +2371,9 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if isBlogPost(postJsonObject): if isBlogPost(postJsonObject):
# blogs index will be updated # blogs index will be updated
updateIndexList.append('tlblogs') updateIndexList.append('tlblogs')
elif isEventPost(postJsonObject):
# events index will be updated
updateIndexList.append('tlevents')
# get the avatar for a reply/announce # get the avatar for a reply/announce
obtainAvatarForReplyPost(session, baseDir, obtainAvatarForReplyPost(session, baseDir,
@ -2294,32 +2382,49 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
# save the post to file # save the post to file
if saveJson(postJsonObject, destinationFilename): if saveJson(postJsonObject, destinationFilename):
# 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:
print('MUTE REPLY: ' + destinationFilename)
muteFile = open(destinationFilename + '.muted', "w")
if muteFile:
muteFile.write('\n')
muteFile.close()
# update the indexes for different timelines # update the indexes for different timelines
for boxname in updateIndexList: for boxname in updateIndexList:
if not inboxUpdateIndex(boxname, baseDir, handle, if not inboxUpdateIndex(boxname, baseDir, handle,
destinationFilename, debug): destinationFilename, debug):
print('ERROR: unable to update ' + boxname + ' index') print('ERROR: unable to update ' + boxname + ' index')
else:
if not unitTest:
if debug:
print('Saving inbox post as html to cache')
htmlCacheStartTime = time.time()
inboxStorePostToHtmlCache(recentPostsCache,
maxRecentPosts,
translate, baseDir,
httpPrefix,
session, cachedWebfingers,
personCache,
handle.split('@')[0],
domain, port,
postJsonObject,
allowDeletion,
boxname)
if debug:
timeDiff = \
str(int((time.time() - htmlCacheStartTime) *
1000))
print('Saved ' + boxname +
' post as html to cache in ' +
timeDiff + ' mS')
inboxUpdateCalendar(baseDir, handle, postJsonObject) inboxUpdateCalendar(baseDir, handle, postJsonObject)
storeHashTags(baseDir, handle.split('@')[0], postJsonObject) storeHashTags(baseDir, handle.split('@')[0], postJsonObject)
if not unitTest:
if debug:
print('DEBUG: saving inbox post as html to cache')
htmlCacheStartTime = time.time()
inboxStorePostToHtmlCache(recentPostsCache, maxRecentPosts,
translate, baseDir, httpPrefix,
session, cachedWebfingers,
personCache,
handle.split('@')[0], domain, port,
postJsonObject, allowDeletion)
if debug:
timeDiff = \
str(int((time.time() - htmlCacheStartTime) * 1000))
print('DEBUG: saved inbox post as html to cache in ' +
timeDiff + ' mS')
# send the post out to group members # send the post out to group members
if isGroup: if isGroup:
sendToGroupMembers(session, baseDir, handle, port, sendToGroupMembers(session, baseDir, handle, port,
@ -2594,6 +2699,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
if accountMaxPostsPerDay > 0 or domainMaxPostsPerDay > 0: if accountMaxPostsPerDay > 0 or domainMaxPostsPerDay > 0:
pprint(quotasDaily) pprint(quotasDaily)
if queueJson.get('actor'):
print('Obtaining public key for actor ' + queueJson['actor']) print('Obtaining public key for actor ' + queueJson['actor'])
# Try a few times to obtain the public key # Try a few times to obtain the public key
@ -2716,6 +2822,23 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
queue.pop(0) queue.pop(0)
continue continue
if receiveEventPost(recentPostsCache, session,
baseDir, httpPrefix,
domain, port,
sendThreads, postLog,
cachedWebfingers,
personCache,
queueJson['post'],
federationList,
queueJson['postNickname'],
debug):
print('Queue: Event activity accepted from ' + keyId)
if os.path.isfile(queueFilename):
os.remove(queueFilename)
if len(queue) > 0:
queue.pop(0)
continue
if receiveUpdate(recentPostsCache, session, if receiveUpdate(recentPostsCache, session,
baseDir, httpPrefix, baseDir, httpPrefix,
domain, port, domain, port,

View File

@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net" __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
from utils import removeIdEnding
from utils import urlPermitted from utils import urlPermitted
from utils import getNicknameFromActor from utils import getNicknameFromActor
from utils import getDomainFromActor from utils import getDomainFromActor
@ -411,7 +412,7 @@ def outboxLike(recentPostsCache: {},
if debug: if debug:
print('DEBUG: c2s like request arrived in outbox') print('DEBUG: c2s like request arrived in outbox')
messageId = messageJson['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object'])
if ':' in domain: if ':' in domain:
domain = domain.split(':')[0] domain = domain.split(':')[0]
postFilename = locatePost(baseDir, nickname, domain, messageId) postFilename = locatePost(baseDir, nickname, domain, messageId)
@ -462,7 +463,7 @@ def outboxUndoLike(recentPostsCache: {},
if debug: if debug:
print('DEBUG: c2s undo like request arrived in outbox') print('DEBUG: c2s undo like request arrived in outbox')
messageId = messageJson['object']['object'].replace('/activity', '') messageId = removeIdEnding(messageJson['object']['object'])
if ':' in domain: if ':' in domain:
domain = domain.split(':')[0] domain = domain.split(':')[0]
postFilename = locatePost(baseDir, nickname, domain, messageId) postFilename = locatePost(baseDir, nickname, domain, messageId)

View File

@ -13,6 +13,7 @@ from posts import outboxMessageCreateWrap
from posts import savePostToBox from posts import savePostToBox
from posts import sendToFollowersThread from posts import sendToFollowersThread
from posts import sendToNamedAddresses from posts import sendToNamedAddresses
from utils import removeIdEnding
from utils import getDomainFromActor from utils import getDomainFromActor
from blocking import isBlockedDomain from blocking import isBlockedDomain
from blocking import outboxBlock from blocking import outboxBlock
@ -152,15 +153,14 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo', permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo',
'Update', 'Add', 'Remove', 'Block', 'Delete', 'Update', 'Add', 'Remove', 'Block', 'Delete',
'Delegate', 'Skill', 'Bookmark') 'Delegate', 'Skill', 'Bookmark', 'Event')
if messageJson['type'] not in permittedOutboxTypes: if messageJson['type'] not in permittedOutboxTypes:
if debug: if debug:
print('DEBUG: POST to outbox - ' + messageJson['type'] + print('DEBUG: POST to outbox - ' + messageJson['type'] +
' is not a permitted activity type') ' is not a permitted activity type')
return False return False
if messageJson.get('id'): if messageJson.get('id'):
postId = \ postId = removeIdEnding(messageJson['id'])
messageJson['id'].replace('/activity', '').replace('/undo', '')
if debug: if debug:
print('DEBUG: id attribute exists within POST to outbox') print('DEBUG: id attribute exists within POST to outbox')
else: else:
@ -172,13 +172,15 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
if messageJson['type'] != 'Upgrade': if messageJson['type'] != 'Upgrade':
outboxName = 'outbox' outboxName = 'outbox'
# if this is a blog post then save to its own box # if this is a blog post or an event then save to its own box
if messageJson['type'] == 'Create': if messageJson['type'] == 'Create':
if messageJson.get('object'): if messageJson.get('object'):
if isinstance(messageJson['object'], dict): if isinstance(messageJson['object'], dict):
if messageJson['object'].get('type'): if messageJson['object'].get('type'):
if messageJson['object']['type'] == 'Article': if messageJson['object']['type'] == 'Article':
outboxName = 'tlblogs' outboxName = 'tlblogs'
elif messageJson['object']['type'] == 'Event':
outboxName = 'tlevents'
savedFilename = \ savedFilename = \
savePostToBox(baseDir, savePostToBox(baseDir,
@ -186,20 +188,25 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
postId, postId,
postToNickname, postToNickname,
domainFull, messageJson, outboxName) domainFull, messageJson, outboxName)
if not savedFilename:
print('WARN: post not saved to outbox ' + outboxName)
return False
if messageJson['type'] == 'Create' or \ if messageJson['type'] == 'Create' or \
messageJson['type'] == 'Question' or \ messageJson['type'] == 'Question' or \
messageJson['type'] == 'Note' or \ messageJson['type'] == 'Note' or \
messageJson['type'] == 'EncryptedMessage' or \ messageJson['type'] == 'EncryptedMessage' or \
messageJson['type'] == 'Article' or \ messageJson['type'] == 'Article' or \
messageJson['type'] == 'Event' or \
messageJson['type'] == 'Patch' or \ messageJson['type'] == 'Patch' or \
messageJson['type'] == 'Announce': messageJson['type'] == 'Announce':
indexes = [outboxName, "inbox"] indexes = [outboxName, "inbox"]
selfActor = \
httpPrefix + '://' + domainFull + '/users/' + postToNickname
for boxNameIndex in indexes: for boxNameIndex in indexes:
if not boxNameIndex:
continue
if boxNameIndex == 'inbox' and outboxName == 'tlblogs': if boxNameIndex == 'inbox' and outboxName == 'tlblogs':
continue continue
selfActor = \
httpPrefix + '://' + domainFull + \
'/users/' + postToNickname
# avoid duplicates of the message if already going # avoid duplicates of the message if already going
# back to the inbox of the same account # back to the inbox of the same account
if selfActor not in messageJson['to']: if selfActor not in messageJson['to']:

View File

@ -25,6 +25,7 @@ from posts import createRepliesTimeline
from posts import createMediaTimeline from posts import createMediaTimeline
from posts import createBlogsTimeline from posts import createBlogsTimeline
from posts import createBookmarksTimeline from posts import createBookmarksTimeline
from posts import createEventsTimeline
from posts import createInbox from posts import createInbox
from posts import createOutbox from posts import createOutbox
from posts import createModeration from posts import createModeration
@ -459,6 +460,12 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
with open(followDMsFilename, "w") as fFile: with open(followDMsFilename, "w") as fFile:
fFile.write('\n') fFile.write('\n')
# notify when posts are liked
notifyLikesFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/.notifyLikes'
with open(notifyLikesFilename, "w") as fFile:
fFile.write('\n')
if not os.path.isdir(baseDir + '/accounts'): if not os.path.isdir(baseDir + '/accounts'):
os.mkdir(baseDir + '/accounts') os.mkdir(baseDir + '/accounts')
if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain): if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain):
@ -598,7 +605,8 @@ def personBoxJson(recentPostsCache: {},
boxname != 'tlreplies' and boxname != 'tlmedia' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and \ boxname != 'tlblogs' and \
boxname != 'outbox' and boxname != 'moderation' and \ boxname != 'outbox' and boxname != 'moderation' and \
boxname != 'tlbookmarks' and boxname != 'bookmarks': boxname != 'tlbookmarks' and boxname != 'bookmarks' and \
boxname != 'tlevents':
return None return None
if not '/' + boxname in path: if not '/' + boxname in path:
@ -638,7 +646,8 @@ def personBoxJson(recentPostsCache: {},
httpPrefix, httpPrefix,
noOfItems, headerOnly, ocapAlways, pageNumber) noOfItems, headerOnly, ocapAlways, pageNumber)
elif boxname == 'dm': elif boxname == 'dm':
return createDMTimeline(session, baseDir, nickname, domain, port, return createDMTimeline(recentPostsCache,
session, baseDir, nickname, domain, port,
httpPrefix, httpPrefix,
noOfItems, headerOnly, ocapAlways, pageNumber) noOfItems, headerOnly, ocapAlways, pageNumber)
elif boxname == 'tlbookmarks' or boxname == 'bookmarks': elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
@ -646,8 +655,15 @@ def personBoxJson(recentPostsCache: {},
port, httpPrefix, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, noOfItems, headerOnly, ocapAlways,
pageNumber) pageNumber)
elif boxname == 'tlevents':
return createEventsTimeline(recentPostsCache,
session, baseDir, nickname, domain,
port, httpPrefix,
noOfItems, headerOnly, ocapAlways,
pageNumber)
elif boxname == 'tlreplies': elif boxname == 'tlreplies':
return createRepliesTimeline(session, baseDir, nickname, domain, return createRepliesTimeline(recentPostsCache,
session, baseDir, nickname, domain,
port, httpPrefix, port, httpPrefix,
noOfItems, headerOnly, ocapAlways, noOfItems, headerOnly, ocapAlways,
pageNumber) pageNumber)

299
posts.py
View File

@ -13,6 +13,7 @@ import os
import shutil import shutil
import sys import sys
import time import time
import uuid
from socket import error as SocketError from socket import error as SocketError
from time import gmtime, strftime from time import gmtime, strftime
from collections import OrderedDict from collections import OrderedDict
@ -28,6 +29,7 @@ from session import postJsonString
from session import postImage from session import postImage
from webfinger import webfingerHandle from webfinger import webfingerHandle
from httpsig import createSignedHeader from httpsig import createSignedHeader
from utils import removeIdEnding
from utils import siteIsActive from utils import siteIsActive
from utils import removePostFromCache from utils import removePostFromCache
from utils import getCachedPostFilename from utils import getCachedPostFilename
@ -45,6 +47,7 @@ from capabilities import getOcapFilename
from capabilities import capabilitiesUpdate from capabilities import capabilitiesUpdate
from media import attachMedia from media import attachMedia
from media import replaceYouTube from media import replaceYouTube
from content import removeHtml
from content import removeLongWords from content import removeLongWords
from content import addHtmlTags from content import addHtmlTags
from content import replaceEmojiFromTags from content import replaceEmojiFromTags
@ -501,7 +504,8 @@ def deleteAllPosts(baseDir: str,
nickname: str, domain: str, boxname: str) -> None: nickname: str, domain: str, boxname: str) -> None:
"""Deletes all posts for a person from inbox or outbox """Deletes all posts for a person from inbox or outbox
""" """
if boxname != 'inbox' and boxname != 'outbox' and boxname != 'tlblogs': if boxname != 'inbox' and boxname != 'outbox' and \
boxname != 'tlblogs' and boxname != 'tlevents':
return return
boxDir = createPersonDir(nickname, domain, baseDir, boxname) boxDir = createPersonDir(nickname, domain, baseDir, boxname)
for deleteFilename in os.scandir(boxDir): for deleteFilename in os.scandir(boxDir):
@ -523,7 +527,8 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str,
Returns the filename Returns the filename
""" """
if boxname != 'inbox' and boxname != 'outbox' and \ if boxname != 'inbox' and boxname != 'outbox' and \
boxname != 'tlblogs' and boxname != 'scheduled': boxname != 'tlblogs' and boxname != 'tlevents' and \
boxname != 'scheduled':
return None return None
originalDomain = domain originalDomain = domain
if ':' in domain: if ':' in domain:
@ -606,15 +611,78 @@ def addSchedulePost(baseDir: str, nickname: str, domain: str,
scheduleFile.close() scheduleFile.close()
def appendEventFields(newPost: {},
eventUUID: str, eventStatus: str,
anonymousParticipationEnabled: bool,
repliesModerationOption: str,
category: str,
joinMode: str,
eventDateStr: str,
endDateStr: str,
location: str,
maximumAttendeeCapacity: int,
ticketUrl: str,
subject: str) -> None:
"""Appends Mobilizon-type event fields to a post
"""
if not eventUUID:
return
# add attributes for Mobilizon-type events
newPost['uuid'] = eventUUID
if eventStatus:
newPost['ical:status'] = eventStatus
if anonymousParticipationEnabled:
newPost['anonymousParticipationEnabled'] = \
anonymousParticipationEnabled
if repliesModerationOption:
newPost['repliesModerationOption'] = repliesModerationOption
if category:
newPost['category'] = category
if joinMode:
newPost['joinMode'] = joinMode
newPost['startTime'] = eventDateStr
newPost['endTime'] = endDateStr
if location:
newPost['location'] = location
if maximumAttendeeCapacity:
newPost['maximumAttendeeCapacity'] = maximumAttendeeCapacity
if ticketUrl:
newPost['ticketUrl'] = ticketUrl
if subject:
newPost['name'] = subject
newPost['summary'] = None
newPost['sensitive'] = False
def validContentWarning(cw: str) -> str:
"""Returns a validated content warning
"""
cw = removeHtml(cw)
# hashtags within content warnings apparently cause a lot of trouble
# so remove them
if '#' in cw:
cw = cw.replace('#', '').replace(' ', ' ')
return cw
def createPostBase(baseDir: str, nickname: str, domain: str, port: int, def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str, content: str, toUrl: str, ccUrl: str, httpPrefix: str, content: str,
followersOnly: bool, saveToFile: bool, clientToServer: bool, followersOnly: bool, saveToFile: bool, clientToServer: bool,
commentsEnabled: bool,
attachImageFilename: str, attachImageFilename: str,
mediaType: str, imageDescription: str, mediaType: str, imageDescription: str,
useBlurhash: bool, isModerationReport: bool, useBlurhash: bool, isModerationReport: bool,
isArticle: bool, inReplyTo=None, isArticle: bool,
inReplyTo=None,
inReplyToAtomUri=None, subject=None, schedulePost=False, inReplyToAtomUri=None, subject=None, schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}: eventDate=None, eventTime=None, location=None,
eventUUID=None, category=None, joinMode=None,
endDate=None, endTime=None,
maximumAttendeeCapacity=None,
repliesModerationOption=None,
anonymousParticipationEnabled=None,
eventStatus=None, ticketUrl=None) -> {}:
"""Creates a message """Creates a message
""" """
mentionedRecipients = \ mentionedRecipients = \
@ -657,7 +725,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
sensitive = False sensitive = False
summary = None summary = None
if subject: if subject:
summary = subject summary = validContentWarning(subject)
sensitive = True sensitive = True
toRecipients = [] toRecipients = []
@ -703,6 +771,24 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
sensitive = True sensitive = True
if replyToJson['object'].get('summary'): if replyToJson['object'].get('summary'):
summary = replyToJson['object']['summary'] summary = replyToJson['object']['summary']
# get the ending date and time
endDateStr = None
if endDate:
eventName = summary
if not eventName:
eventName = content
endDateStr = endDate
if endTime:
if endTime.endswith('Z'):
endDateStr = endDate + 'T' + endTime
else:
endDateStr = endDate + 'T' + endTime + \
':00' + strftime("%z", gmtime())
else:
endDateStr = endDate + 'T12:00:00Z'
# get the starting date and time
eventDateStr = None eventDateStr = None
if eventDate: if eventDate:
eventName = summary eventName = summary
@ -717,15 +803,17 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
':00' + strftime("%z", gmtime()) ':00' + strftime("%z", gmtime())
else: else:
eventDateStr = eventDate + 'T12:00:00Z' eventDateStr = eventDate + 'T12:00:00Z'
if not schedulePost: if not endDateStr:
endDateStr = eventDateStr
if not schedulePost and not eventUUID:
tags.append({ tags.append({
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"type": "Event", "type": "Event",
"name": eventName, "name": eventName,
"startTime": eventDateStr, "startTime": eventDateStr,
"endTime": eventDateStr "endTime": endDateStr
}) })
if location: if location and not eventUUID:
tags.append({ tags.append({
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"type": "Place", "type": "Place",
@ -755,6 +843,11 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
for ccRemoval in removeFromCC: for ccRemoval in removeFromCC:
toCC.remove(ccRemoval) toCC.remove(ccRemoval)
# the type of post to be made
postObjectType = 'Note'
if eventUUID:
postObjectType = 'Event'
if not clientToServer: if not clientToServer:
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
@ -783,7 +876,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
'cc': toCC, 'cc': toCC,
'object': { 'object': {
'id': newPostId, 'id': newPostId,
'type': 'Note', 'type': postObjectType,
'summary': summary, 'summary': summary,
'inReplyTo': inReplyTo, 'inReplyTo': inReplyTo,
'published': published, 'published': published,
@ -794,6 +887,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
'sensitive': sensitive, 'sensitive': sensitive,
'atomUri': newPostId, 'atomUri': newPostId,
'inReplyToAtomUri': inReplyToAtomUri, 'inReplyToAtomUri': inReplyToAtomUri,
'commentsEnabled': commentsEnabled,
'mediaType': 'text/html', 'mediaType': 'text/html',
'content': content, 'content': content,
'contentMap': { 'contentMap': {
@ -817,6 +911,13 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
attachMedia(baseDir, httpPrefix, domain, port, attachMedia(baseDir, httpPrefix, domain, port,
newPost['object'], attachImageFilename, newPost['object'], attachImageFilename,
mediaType, imageDescription, useBlurhash) mediaType, imageDescription, useBlurhash)
appendEventFields(newPost['object'], eventUUID, eventStatus,
anonymousParticipationEnabled,
repliesModerationOption,
category, joinMode,
eventDateStr, endDateStr,
location, maximumAttendeeCapacity,
ticketUrl, subject)
else: else:
idStr = \ idStr = \
httpPrefix + '://' + domain + '/users/' + nickname + \ httpPrefix + '://' + domain + '/users/' + nickname + \
@ -824,7 +925,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
newPost = { newPost = {
"@context": postContext, "@context": postContext,
'id': newPostId, 'id': newPostId,
'type': 'Note', 'type': postObjectType,
'summary': summary, 'summary': summary,
'inReplyTo': inReplyTo, 'inReplyTo': inReplyTo,
'published': published, 'published': published,
@ -835,6 +936,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
'sensitive': sensitive, 'sensitive': sensitive,
'atomUri': newPostId, 'atomUri': newPostId,
'inReplyToAtomUri': inReplyToAtomUri, 'inReplyToAtomUri': inReplyToAtomUri,
'commentsEnabled': commentsEnabled,
'mediaType': 'text/html', 'mediaType': 'text/html',
'content': content, 'content': content,
'contentMap': { 'contentMap': {
@ -857,6 +959,13 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
attachMedia(baseDir, httpPrefix, domain, port, attachMedia(baseDir, httpPrefix, domain, port,
newPost, attachImageFilename, newPost, attachImageFilename,
mediaType, imageDescription, useBlurhash) mediaType, imageDescription, useBlurhash)
appendEventFields(newPost, eventUUID, eventStatus,
anonymousParticipationEnabled,
repliesModerationOption,
category, joinMode,
eventDateStr, endDateStr,
location, maximumAttendeeCapacity,
ticketUrl, subject)
if ccUrl: if ccUrl:
if len(ccUrl) > 0: if len(ccUrl) > 0:
newPost['cc'] = [ccUrl] newPost['cc'] = [ccUrl]
@ -892,12 +1001,15 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
'date and time values') 'date and time values')
return newPost return newPost
elif saveToFile: elif saveToFile:
if not isArticle: if isArticle:
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'outbox')
else:
savePostToBox(baseDir, httpPrefix, newPostId, savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'tlblogs') nickname, domain, newPost, 'tlblogs')
elif eventUUID:
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'tlevents')
else:
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'outbox')
return newPost return newPost
@ -1006,7 +1118,7 @@ def postIsAddressedToPublic(baseDir: str, postJsonObject: {}) -> bool:
def createPublicPost(baseDir: str, def createPublicPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str, nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None, inReplyTo=None, inReplyToAtomUri=None, subject=None,
@ -1024,11 +1136,13 @@ def createPublicPost(baseDir: str,
httpPrefix + '://' + domainFull + '/users/' + httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers', nickname + '/followers',
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location) schedulePost, eventDate, eventTime, location,
None, None, None, None, None,
None, None, None, None, None)
def createBlogPost(baseDir: str, def createBlogPost(baseDir: str,
@ -1058,7 +1172,7 @@ def createQuestionPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str, nickname: str, domain: str, port: int, httpPrefix: str,
content: str, qOptions: [], content: str, qOptions: [],
followersOnly: bool, saveToFile: bool, followersOnly: bool, saveToFile: bool,
clientToServer: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
subject: str, durationDays: int) -> {}: subject: str, durationDays: int) -> {}:
@ -1075,11 +1189,13 @@ def createQuestionPost(baseDir: str,
httpPrefix + '://' + domainFull + '/users/' + httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers', nickname + '/followers',
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, False, None, None, subject, False, False, None, None, subject,
False, None, None, None) False, None, None, None, None, None,
None, None, None,
None, None, None, None, None)
messageJson['object']['type'] = 'Question' messageJson['object']['type'] = 'Question'
messageJson['object']['oneOf'] = [] messageJson['object']['oneOf'] = []
messageJson['object']['votersCount'] = 0 messageJson['object']['votersCount'] = 0
@ -1104,7 +1220,7 @@ def createQuestionPost(baseDir: str,
def createUnlistedPost(baseDir: str, def createUnlistedPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str, nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None, inReplyTo=None, inReplyToAtomUri=None, subject=None,
@ -1122,11 +1238,13 @@ def createUnlistedPost(baseDir: str,
nickname + '/followers', nickname + '/followers',
'https://www.w3.org/ns/activitystreams#Public', 'https://www.w3.org/ns/activitystreams#Public',
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location) schedulePost, eventDate, eventTime, location,
None, None, None, None, None,
None, None, None, None, None)
def createFollowersOnlyPost(baseDir: str, def createFollowersOnlyPost(baseDir: str,
@ -1134,7 +1252,7 @@ def createFollowersOnlyPost(baseDir: str,
httpPrefix: str, httpPrefix: str,
content: str, followersOnly: bool, content: str, followersOnly: bool,
saveToFile: bool, saveToFile: bool,
clientToServer: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, inReplyTo=None, inReplyToAtomUri=None,
@ -1153,11 +1271,67 @@ def createFollowersOnlyPost(baseDir: str,
nickname + '/followers', nickname + '/followers',
None, None,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location) schedulePost, eventDate, eventTime, location,
None, None, None, None, None,
None, None, None, None, None)
def createEventPost(baseDir: str,
nickname: str, domain: str, port: int,
httpPrefix: str,
content: str, followersOnly: bool,
saveToFile: bool,
clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
subject=None, schedulePost=False,
eventDate=None, eventTime=None,
location=None, category=None, joinMode=None,
endDate=None, endTime=None,
maximumAttendeeCapacity=None,
repliesModerationOption=None,
anonymousParticipationEnabled=None,
eventStatus=None, ticketUrl=None) -> {}:
"""Mobilizon-type Event post
"""
if not attachImageFilename:
print('Event has no attached image')
return None
if not category:
print('Event has no category')
return None
domainFull = domain
if port:
if port != 80 and port != 443:
if ':' not in domain:
domainFull = domain + ':' + str(port)
# create event uuid
eventUUID = str(uuid.uuid1())
toStr1 = 'https://www.w3.org/ns/activitystreams#Public'
toStr2 = httpPrefix + '://' + domainFull + '/users/' + \
nickname + '/followers',
if followersOnly:
toStr1 = toStr2
toStr2 = None
return createPostBase(baseDir, nickname, domain, port,
toStr1, toStr2,
httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, None, None, subject,
schedulePost, eventDate, eventTime, location,
eventUUID, category, joinMode,
endDate, endTime, maximumAttendeeCapacity,
repliesModerationOption,
anonymousParticipationEnabled,
eventStatus, ticketUrl)
def getMentionedPeople(baseDir: str, httpPrefix: str, def getMentionedPeople(baseDir: str, httpPrefix: str,
@ -1200,6 +1374,7 @@ def createDirectMessagePost(baseDir: str,
httpPrefix: str, httpPrefix: str,
content: str, followersOnly: bool, content: str, followersOnly: bool,
saveToFile: bool, clientToServer: bool, saveToFile: bool, clientToServer: bool,
commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, inReplyTo=None, inReplyToAtomUri=None,
@ -1222,11 +1397,13 @@ def createDirectMessagePost(baseDir: str,
createPostBase(baseDir, nickname, domain, port, createPostBase(baseDir, nickname, domain, port,
postTo, postCc, postTo, postCc,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location) schedulePost, eventDate, eventTime, location,
None, None, None, None, None,
None, None, None, None, None)
# mentioned recipients go into To rather than Cc # mentioned recipients go into To rather than Cc
messageJson['to'] = messageJson['object']['cc'] messageJson['to'] = messageJson['object']['cc']
messageJson['object']['to'] = messageJson['to'] messageJson['object']['to'] = messageJson['to']
@ -1241,7 +1418,7 @@ def createDirectMessagePost(baseDir: str,
def createReportPost(baseDir: str, def createReportPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str, nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
debug: bool, subject=None) -> {}: debug: bool, subject=None) -> {}:
@ -1314,17 +1491,20 @@ def createReportPost(baseDir: str,
createPostBase(baseDir, nickname, domain, port, createPostBase(baseDir, nickname, domain, port,
toUrl, postCc, toUrl, postCc,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
True, False, None, None, subject, True, False, None, None, subject,
False, None, None, None) False, None, None, None, None, None,
None, None, None,
None, None, None, None, None)
if not postJsonObject: if not postJsonObject:
continue continue
# update the inbox index with the report filename # update the inbox index with the report filename
# indexFilename = baseDir+'/accounts/'+handle+'/inbox.index' # indexFilename = baseDir+'/accounts/'+handle+'/inbox.index'
# indexEntry=postJsonObject['id'].replace('/activity','').replace('/','#')+'.json' # indexEntry = \
# removeIdEnding(postJsonObject['id']).replace('/','#') + '.json'
# if indexEntry not in open(indexFilename).read(): # if indexEntry not in open(indexFilename).read():
# try: # try:
# with open(indexFilename, 'a+') as fp: # with open(indexFilename, 'a+') as fp:
@ -1402,6 +1582,7 @@ def sendPost(projectVersion: str,
toNickname: str, toDomain: str, toPort: int, cc: str, toNickname: str, toDomain: str, toPort: int, cc: str,
httpPrefix: str, content: str, followersOnly: bool, httpPrefix: str, content: str, followersOnly: bool,
saveToFile: bool, clientToServer: bool, saveToFile: bool, clientToServer: bool,
commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
federationList: [], sendThreads: [], postLog: [], federationList: [], sendThreads: [], postLog: [],
@ -1470,11 +1651,14 @@ def sendPost(projectVersion: str,
createPostBase(baseDir, nickname, domain, port, createPostBase(baseDir, nickname, domain, port,
toPersonId, cc, httpPrefix, content, toPersonId, cc, httpPrefix, content,
followersOnly, saveToFile, clientToServer, followersOnly, saveToFile, clientToServer,
commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, isArticle, inReplyTo, False, isArticle, inReplyTo,
inReplyToAtomUri, subject, inReplyToAtomUri, subject,
False, None, None, None) False, None, None, None, None, None,
None, None, None,
None, None, None, None, None)
# get the senders private key # get the senders private key
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private') privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private')
@ -1528,6 +1712,7 @@ def sendPostViaServer(projectVersion: str,
fromDomain: str, fromPort: int, fromDomain: str, fromPort: int,
toNickname: str, toDomain: str, toPort: int, cc: str, toNickname: str, toDomain: str, toPort: int, cc: str,
httpPrefix: str, content: str, followersOnly: bool, httpPrefix: str, content: str, followersOnly: bool,
commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str, useBlurhash: bool,
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
@ -1614,11 +1799,14 @@ def sendPostViaServer(projectVersion: str,
fromNickname, fromDomain, fromPort, fromNickname, fromDomain, fromPort,
toPersonId, cc, httpPrefix, content, toPersonId, cc, httpPrefix, content,
followersOnly, saveToFile, clientToServer, followersOnly, saveToFile, clientToServer,
commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription, useBlurhash,
False, isArticle, inReplyTo, False, isArticle, inReplyTo,
inReplyToAtomUri, subject, inReplyToAtomUri, subject,
False, None, None, None) False, None, None, None, None, None,
None, None, None,
None, None, None, None, None)
authHeader = createBasicAuthHeader(fromNickname, password) authHeader = createBasicAuthHeader(fromNickname, password)
@ -2261,20 +2449,34 @@ def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
True, ocapAlways, pageNumber) True, ocapAlways, pageNumber)
def createDMTimeline(session, baseDir: str, nickname: str, domain: str, def createEventsTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}: pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'dm', nickname, return createBoxIndexed(recentPostsCache, session, baseDir, 'tlevents',
nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber)
def createDMTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache,
session, baseDir, 'dm', nickname,
domain, port, httpPrefix, itemsPerPage, domain, port, httpPrefix, itemsPerPage,
headerOnly, True, ocapAlways, pageNumber) headerOnly, True, ocapAlways, pageNumber)
def createRepliesTimeline(session, baseDir: str, nickname: str, domain: str, def createRepliesTimeline(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int, port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool, headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}: pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlreplies', return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
nickname, domain, port, httpPrefix, nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True, itemsPerPage, headerOnly, True,
ocapAlways, pageNumber) ocapAlways, pageNumber)
@ -2441,6 +2643,7 @@ def isImageMedia(session, baseDir: str, httpPrefix: str,
if postJsonObject['object'].get('moderationStatus'): if postJsonObject['object'].get('moderationStatus'):
return False return False
if postJsonObject['object']['type'] != 'Note' and \ if postJsonObject['object']['type'] != 'Note' and \
postJsonObject['object']['type'] != 'Event' and \
postJsonObject['object']['type'] != 'Article': postJsonObject['object']['type'] != 'Article':
return False return False
if not postJsonObject['object'].get('attachment'): if not postJsonObject['object'].get('attachment'):
@ -2581,6 +2784,7 @@ def addPostStringToTimeline(postStr: str, boxname: str,
# must be a recognized ActivityPub type # must be a recognized ActivityPub type
if ('"Note"' in postStr or if ('"Note"' in postStr or
'"EncryptedMessage"' in postStr or '"EncryptedMessage"' in postStr or
'"Event"' in postStr or
'"Article"' in postStr or '"Article"' in postStr or
'"Patch"' in postStr or '"Patch"' in postStr or
'"Announce"' in postStr or '"Announce"' in postStr or
@ -2632,10 +2836,12 @@ def createBoxIndexed(recentPostsCache: {},
boxname != 'tlreplies' and boxname != 'tlmedia' and \ boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and \ boxname != 'tlblogs' and \
boxname != 'outbox' and boxname != 'tlbookmarks' and \ boxname != 'outbox' and boxname != 'tlbookmarks' and \
boxname != 'bookmarks': boxname != 'bookmarks' and \
boxname != 'tlevents':
return None return None
# bookmarks timeline is like the inbox but has its own separate index # bookmarks and events timelines are like the inbox
# but have their own separate index
indexBoxName = boxname indexBoxName = boxname
if boxname == "tlbookmarks": if boxname == "tlbookmarks":
boxname = "bookmarks" boxname = "bookmarks"
@ -3303,6 +3509,17 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str,
return None return None
def isMuted(baseDir: str, nickname: str, domain: str, postId: str) -> bool:
"""Returns true if the given post is muted
"""
postFilename = locatePost(baseDir, nickname, domain, postId)
if not postFilename:
return False
if os.path.isfile(postFilename + '.muted'):
return True
return False
def mutePost(baseDir: str, nickname: str, domain: str, postId: str, def mutePost(baseDir: str, nickname: str, domain: str, postId: str,
recentPostsCache: {}) -> None: recentPostsCache: {}) -> None:
""" Mutes the given post """ Mutes the given post
@ -3330,7 +3547,7 @@ def mutePost(baseDir: str, nickname: str, domain: str, postId: str,
# if the post is in the recent posts cache then mark it as muted # if the post is in the recent posts cache then mark it as muted
if recentPostsCache.get('index'): if recentPostsCache.get('index'):
postId = \ postId = \
postJsonObject['id'].replace('/activity', '').replace('/', '#') removeIdEnding(postJsonObject['id']).replace('/', '#')
if postId in recentPostsCache['index']: if postId in recentPostsCache['index']:
print('MUTE: ' + postId + ' is in recent posts cache') print('MUTE: ' + postId + ' is in recent posts cache')
if recentPostsCache['json'].get(postId): if recentPostsCache['json'].get(postId):

View File

@ -0,0 +1,7 @@
#!/bin/bash
journalctl -u epicyon | grep 'could not be' > .keyfailures.txt
if [ ! -f .keyfailures.txt ]; then
echo 'No key failures'
else
cat .keyfailures.txt
fi

130
tests.py
View File

@ -20,6 +20,7 @@ from cache import getPersonFromCache
from threads import threadWithTrace from threads import threadWithTrace
from daemon import runDaemon from daemon import runDaemon
from session import createSession from session import createSession
from posts import validContentWarning
from posts import deleteAllPosts from posts import deleteAllPosts
from posts import createPublicPost from posts import createPublicPost
from posts import sendPost from posts import sendPost
@ -31,6 +32,7 @@ from follow import clearFollows
from follow import clearFollowers from follow import clearFollowers
from follow import sendFollowRequestViaServer from follow import sendFollowRequestViaServer
from follow import sendUnfollowRequestViaServer from follow import sendUnfollowRequestViaServer
from utils import removeIdEnding
from utils import siteIsActive from utils import siteIsActive
from utils import updateRecentPostsCache from utils import updateRecentPostsCache
from utils import followPerson from utils import followPerson
@ -62,6 +64,7 @@ from announce import sendAnnounceViaServer
from media import getMediaPath from media import getMediaPath
from media import getAttachmentMediaType from media import getAttachmentMediaType
from delete import sendDeleteViaServer from delete import sendDeleteViaServer
from inbox import jsonPostAllowsComments
from inbox import validInbox from inbox import validInbox
from inbox import validInboxFilenames from inbox import validInboxFilenames
from content import htmlReplaceQuoteMarks from content import htmlReplaceQuoteMarks
@ -270,14 +273,16 @@ def createServerAlice(path: str, domain: str, port: int,
clientToServer = False clientToServer = False
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"No wise fish would go anywhere without a porpoise", "No wise fish would go anywhere without a porpoise",
False, True, clientToServer, None, None, useBlurhash) False, True, clientToServer, True,
None, None, useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"Curiouser and curiouser!", False, True, "Curiouser and curiouser!", False, True,
clientToServer, None, None, useBlurhash) clientToServer, True, None, None, useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"In the gardens of memory, in the palace " + "In the gardens of memory, in the palace " +
"of dreams, that is where you and I shall meet", "of dreams, that is where you and I shall meet",
False, True, clientToServer, None, None, useBlurhash) False, True, clientToServer, True,
None, None, useBlurhash)
global testServerAliceRunning global testServerAliceRunning
testServerAliceRunning = True testServerAliceRunning = True
maxMentions = 10 maxMentions = 10
@ -335,14 +340,17 @@ def createServerBob(path: str, domain: str, port: int,
if hasPosts: if hasPosts:
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"It's your life, live it your way.", "It's your life, live it your way.",
False, True, clientToServer, None, None, useBlurhash) False, True, clientToServer, True,
None, None, useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"One of the things I've realised is that " + "One of the things I've realised is that " +
"I am very simple", "I am very simple",
False, True, clientToServer, None, None, useBlurhash) False, True, clientToServer, True,
None, None, useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"Quantum physics is a bit of a passion of mine", "Quantum physics is a bit of a passion of mine",
False, True, clientToServer, None, None, useBlurhash) False, True, clientToServer, True,
None, None, useBlurhash)
global testServerBobRunning global testServerBobRunning
testServerBobRunning = True testServerBobRunning = True
maxMentions = 10 maxMentions = 10
@ -503,7 +511,8 @@ def testPostMessageBetweenServers():
'Why is a mouse when it spins? ' + 'Why is a mouse when it spins? ' +
'यह एक परीक्षण है #sillyquestion', 'यह एक परीक्षण है #sillyquestion',
followersOnly, followersOnly,
saveToFile, clientToServer, attachedImageFilename, mediaType, saveToFile, clientToServer, True,
attachedImageFilename, mediaType,
attachedImageDescription, useBlurhash, federationList, attachedImageDescription, useBlurhash, federationList,
aliceSendThreads, alicePostLog, aliceCachedWebfingers, aliceSendThreads, alicePostLog, aliceCachedWebfingers,
alicePersonCache, isArticle, inReplyTo, alicePersonCache, isArticle, inReplyTo,
@ -788,7 +797,8 @@ def testFollowBetweenServers():
sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, sessionAlice, aliceDir, 'alice', aliceDomain, alicePort,
'bob', bobDomain, bobPort, ccUrl, 'bob', bobDomain, bobPort, ccUrl,
httpPrefix, 'Alice message', followersOnly, saveToFile, httpPrefix, 'Alice message', followersOnly, saveToFile,
clientToServer, None, None, None, useBlurhash, federationList, clientToServer, True,
None, None, None, useBlurhash, federationList,
aliceSendThreads, alicePostLog, aliceCachedWebfingers, aliceSendThreads, alicePostLog, aliceCachedWebfingers,
alicePersonCache, isArticle, inReplyTo, alicePersonCache, isArticle, inReplyTo,
inReplyToAtomUri, subject) inReplyToAtomUri, subject)
@ -1092,7 +1102,7 @@ def testCreatePerson():
archivePostsForPerson(nickname, domain, baseDir, 'outbox', None, {}, 4) archivePostsForPerson(nickname, domain, baseDir, 'outbox', None, {}, 4)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"G'day world!", False, True, clientToServer, "G'day world!", False, True, clientToServer,
None, None, useBlurhash, None, None, True, None, None, useBlurhash, None, None,
'Not suitable for Vogons') 'Not suitable for Vogons')
os.chdir(currDir) os.chdir(currDir)
@ -1315,7 +1325,7 @@ def testClientToServer():
aliceDomain, alicePort, aliceDomain, alicePort,
'bob', bobDomain, bobPort, None, 'bob', bobDomain, bobPort, None,
httpPrefix, 'Sent from my ActivityPub client', httpPrefix, 'Sent from my ActivityPub client',
followersOnly, followersOnly, True,
attachedImageFilename, mediaType, attachedImageFilename, mediaType,
attachedImageDescription, useBlurhash, attachedImageDescription, useBlurhash,
cachedWebfingers, personCache, isArticle, cachedWebfingers, personCache, isArticle,
@ -1356,7 +1366,7 @@ def testClientToServer():
outboxPostFilename = outboxPath + '/' + name outboxPostFilename = outboxPath + '/' + name
postJsonObject = loadJson(outboxPostFilename, 0) postJsonObject = loadJson(outboxPostFilename, 0)
if postJsonObject: if postJsonObject:
outboxPostId = postJsonObject['id'].replace('/activity', '') outboxPostId = removeIdEnding(postJsonObject['id'])
assert outboxPostId assert outboxPostId
print('message id obtained: ' + outboxPostId) print('message id obtained: ' + outboxPostId)
assert validInbox(bobDir, 'bob', bobDomain) assert validInbox(bobDir, 'bob', bobDomain)
@ -1974,8 +1984,106 @@ def runHtmlReplaceQuoteMarks():
assert result == '“hello” <a href="somesite.html">“test” html</a>' assert result == '“hello” <a href="somesite.html">“test” html</a>'
def testJsonPostAllowsComments():
print('testJsonPostAllowsComments')
postJsonObject = {
"id": "123"
}
assert jsonPostAllowsComments(postJsonObject)
postJsonObject = {
"id": "123",
"commentsEnabled": False
}
assert not jsonPostAllowsComments(postJsonObject)
postJsonObject = {
"id": "123",
"commentsEnabled": True
}
assert jsonPostAllowsComments(postJsonObject)
postJsonObject = {
"id": "123",
"object": {
"commentsEnabled": True
}
}
assert jsonPostAllowsComments(postJsonObject)
postJsonObject = {
"id": "123",
"object": {
"commentsEnabled": False
}
}
assert not jsonPostAllowsComments(postJsonObject)
def testRemoveIdEnding():
print('testRemoveIdEnding')
testStr = 'https://activitypub.somedomain.net'
resultStr = removeIdEnding(testStr)
assert resultStr == 'https://activitypub.somedomain.net'
testStr = \
'https://activitypub.somedomain.net/users/foo/' + \
'statuses/34544814814/activity'
resultStr = removeIdEnding(testStr)
assert resultStr == \
'https://activitypub.somedomain.net/users/foo/statuses/34544814814'
testStr = \
'https://undo.somedomain.net/users/foo/statuses/34544814814/undo'
resultStr = removeIdEnding(testStr)
assert resultStr == \
'https://undo.somedomain.net/users/foo/statuses/34544814814'
testStr = \
'https://event.somedomain.net/users/foo/statuses/34544814814/event'
resultStr = removeIdEnding(testStr)
assert resultStr == \
'https://event.somedomain.net/users/foo/statuses/34544814814'
def testValidContentWarning():
print('testValidContentWarning')
resultStr = validContentWarning('Valid content warning')
assert resultStr == 'Valid content warning'
resultStr = validContentWarning('Invalid #content warning')
assert resultStr == 'Invalid content warning'
resultStr = \
validContentWarning('Invalid <a href="somesite">content warning</a>')
assert resultStr == 'Invalid content warning'
def testTranslations():
print('testTranslations')
languagesStr = ('ar', 'ca', 'cy', 'de', 'es', 'fr', 'ga',
'hi', 'it', 'ja', 'oc', 'pt', 'ru', 'zh')
# load all translations into a dict
langDict = {}
for lang in languagesStr:
langJson = loadJson('translations/' + lang + '.json')
assert langJson
langDict[lang] = langJson
# load english translations
translationsJson = loadJson('translations/en.json')
# test each english string exists in the other language files
for englishStr, translatedStr in translationsJson.items():
for lang in languagesStr:
langJson = langDict[lang]
if not langJson.get(englishStr):
print(englishStr + ' is missing from ' + lang + '.json')
assert langJson.get(englishStr)
def runAllTests(): def runAllTests():
print('Running tests...') print('Running tests...')
testTranslations()
testValidContentWarning()
testRemoveIdEnding()
testJsonPostAllowsComments()
runHtmlReplaceQuoteMarks() runHtmlReplaceQuoteMarks()
testDangerousMarkup() testDangerousMarkup()
testRemoveHtml() testRemoveHtml()

View File

@ -255,5 +255,32 @@
"Liked by": "نال إعجاب", "Liked by": "نال إعجاب",
"Solidaric": "تضامن", "Solidaric": "تضامن",
"YouTube Replacement Domain": "استبدال نطاق يوتيوب", "YouTube Replacement Domain": "استبدال نطاق يوتيوب",
"Notes": "ملاحظات" "Notes": "ملاحظات",
"Allow replies.": "السماح بالردود.",
"Event": "حدث",
"Event name": "اسم الحدث",
"Events": "الأحداث",
"Create an event": "أنشئ حدثًا",
"Describe the event": "صف الحدث",
"Start Date": "تاريخ البدء",
"End Date": "تاريخ الانتهاء",
"Categories": "التصنيفات",
"This is a private event.": "هذا هو الحدث الخاص.",
"Allow anonymous participation.": "السماح بالمشاركة المجهولة.",
"Anyone can join": "يمكن لأي شخص الانضمام",
"Apply to join": "تقديم طلب للانضمام",
"Invitation only": "المدعوون فقط",
"Joining": "انضمام",
"Status of the event": "حالة الحدث",
"Tentative": "مؤقت",
"Confirmed": "تم تأكيد",
"Cancelled": "ألغيت",
"Event banner image description": "وصف صورة شعار الحدث",
"Banner image": "صورة بانر",
"Maximum attendees": "الحد الأقصى للحضور",
"Ticket URL": "عنوان URL للتذكرة",
"Create a new event": "أنشئ حدثًا جديدًا",
"Moderation policy or code of conduct": "سياسة الوسطية أو قواعد السلوك",
"Edit event": "تحرير الحدث",
"Notify when posts are liked": "يخطر عندما يتم اعجاب المشاركات"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "M'agrada", "Liked by": "M'agrada",
"Solidaric": "Solidaritat", "Solidaric": "Solidaritat",
"YouTube Replacement Domain": "Domini de substitució de YouTube", "YouTube Replacement Domain": "Domini de substitució de YouTube",
"Notes": "Notes" "Notes": "Notes",
"Allow replies.": "Permetre respostes.",
"Event": "Esdeveniment",
"Event name": "Nom de lesdeveniment",
"Events": "Esdeveniments",
"Create an event": "Crea un esdeveniment",
"Describe the event": "Descriviu lesdeveniment",
"Start Date": "Data d'inici",
"End Date": "Data de finalització",
"Categories": "Categories",
"This is a private event.": "Aquest és un esdeveniment privat.",
"Allow anonymous participation.": "Permet una participació anònima.",
"Anyone can join": "Qualsevol persona shi pot apuntar",
"Apply to join": "Sol·liciteu participar",
"Invitation only": "Només invitació",
"Joining": "Unir-se",
"Status of the event": "Estat de lesdeveniment",
"Tentative": "Temptatiu",
"Confirmed": "Confirmat",
"Cancelled": "Cancel·lat",
"Event banner image description": "Descripció de la imatge del banner de lesdeveniment",
"Banner image": "Imatge de pancarta",
"Maximum attendees": "Màxim dassistents",
"Ticket URL": "URL de l'entrada",
"Create a new event": "Creeu un esdeveniment nou",
"Moderation policy or code of conduct": "Política de moderació o codi de conducta",
"Edit event": "Edita lesdeveniment",
"Notify when posts are liked": "Notifiqueu-ho quan us agradin les publicacions"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Hoffi", "Liked by": "Hoffi",
"Solidaric": "Undod", "Solidaric": "Undod",
"YouTube Replacement Domain": "Parth Amnewid YouTube", "YouTube Replacement Domain": "Parth Amnewid YouTube",
"Notes": "Nodiadau" "Notes": "Nodiadau",
"Allow replies.": "Caniatáu atebion.",
"Event": "Digwyddiad",
"Event name": "Enw'r digwyddiad",
"Events": "Digwyddiadau",
"Create an event": "Creu digwyddiad",
"Describe the event": "Disgrifiwch y digwyddiad",
"Start Date": "Dyddiad cychwyn",
"End Date": "Dyddiad Gorffen",
"Categories": "Categorïau",
"This is a private event.": "Digwyddiad preifat yw hwn.",
"Allow anonymous participation.": "Caniatáu cyfranogiad dienw.",
"Anyone can join": "Gall unrhyw un ymuno",
"Apply to join": "Gwnewch gais i ymuno",
"Invitation only": "Gwahoddiad yn unig",
"Joining": "Yn ymuno",
"Status of the event": "Statws y digwyddiad",
"Tentative": "Cynhyrfus",
"Confirmed": "Cadarnhawyd",
"Cancelled": "Wedi'i ganslo",
"Event banner image description": "Disgrifiad delwedd baner y digwyddiad",
"Banner image": "Delwedd baner",
"Maximum attendees": "Uchafswm mynychwyr",
"Ticket URL": "URL y tocyn",
"Create a new event": "Creu digwyddiad newydd",
"Moderation policy or code of conduct": "Polisi cymedroli neu god ymddygiad",
"Edit event": "Golygu digwyddiad",
"Notify when posts are liked": "Hysbysu pryd mae swyddi'n cael eu hoffi"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Gefallen von", "Liked by": "Gefallen von",
"Solidaric": "Solidarität", "Solidaric": "Solidarität",
"YouTube Replacement Domain": "YouTube-Ersatzdomain", "YouTube Replacement Domain": "YouTube-Ersatzdomain",
"Notes": "Anmerkungen" "Notes": "Anmerkungen",
"Allow replies.": "Antworten zulassen.",
"Event": "Veranstaltung",
"Event name": "Veranstaltungsname",
"Events": "Veranstaltungen",
"Create an event": "Erstellen Sie ein Ereignis",
"Describe the event": "Beschreiben Sie das Ereignis",
"Start Date": "Anfangsdatum",
"End Date": "Endtermin",
"Categories": "Kategorien",
"This is a private event.": "Dies ist eine private Veranstaltung.",
"Allow anonymous participation.": "Anonyme Teilnahme zulassen.",
"Anyone can join": "Jeder kann mitmachen",
"Apply to join": "Sich anmelden um teilzunehmen",
"Invitation only": "Nur Einladungen",
"Joining": "Beitritt",
"Status of the event": "Status des Ereignisses",
"Tentative": "Vorsichtig",
"Confirmed": "Bestätigt",
"Cancelled": "Abgesagt",
"Event banner image description": "Beschreibung des Ereignisbannerbildes",
"Banner image": "Bannerbild",
"Maximum attendees": "Maximale Teilnehmerzahl",
"Ticket URL": "Ticket URL",
"Create a new event": "Erstellen Sie ein neues Ereignis",
"Moderation policy or code of conduct": "Moderationsrichtlinie oder Verhaltenskodex",
"Edit event": "Ereignis bearbeiten",
"Notify when posts are liked": "Benachrichtigen, wenn Beiträge gefallen"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Liked by", "Liked by": "Liked by",
"Solidaric": "Solidaric", "Solidaric": "Solidaric",
"YouTube Replacement Domain": "YouTube Replacement Domain", "YouTube Replacement Domain": "YouTube Replacement Domain",
"Notes": "Notes" "Notes": "Notes",
"Allow replies.": "Allow replies.",
"Event": "Event",
"Event name": "Event name",
"Events": "Events",
"Create an event": "Create an event",
"Describe the event": "Describe the event",
"Start Date": "Start Date",
"End Date": "End Date",
"Categories": "Categories",
"This is a private event.": "This is a private event.",
"Allow anonymous participation.": "Allow anonymous participation.",
"Anyone can join": "Anyone can join",
"Apply to join": "Apply to join",
"Invitation only": "Invitation only",
"Joining": "Joining",
"Status of the event": "Status of the event",
"Tentative": "Tentative",
"Confirmed": "Confirmed",
"Cancelled": "Cancelled",
"Event banner image description": "Event banner image description",
"Banner image": "Banner image",
"Maximum attendees": "Maximum attendees",
"Ticket URL": "Ticket URL",
"Create a new event": "Create a new event",
"Moderation policy or code of conduct": "Moderation policy or code of conduct",
"Edit event": "Edit event",
"Notify when posts are liked": "Notify when posts are liked"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Apreciado por", "Liked by": "Apreciado por",
"Solidaric": "Solidaridad", "Solidaric": "Solidaridad",
"YouTube Replacement Domain": "Dominio de reemplazo de YouTube", "YouTube Replacement Domain": "Dominio de reemplazo de YouTube",
"Notes": "Notas" "Notes": "Notas",
"Allow replies.": "Permitir respuestas.",
"Event": "Evento",
"Event name": "Nombre del evento",
"Events": "Eventos",
"Create an event": "Crea un evento",
"Describe the event": "Describe el evento",
"Start Date": "Fecha de inicio",
"End Date": "Fecha final",
"Categories": "Categorías",
"This is a private event.": "Este es un evento privado.",
"Allow anonymous participation.": "Permitir la participación anónima.",
"Anyone can join": "Cualquiera puede unirse",
"Apply to join": "Aplica para unirte",
"Invitation only": "Sólo con Invitación",
"Joining": "Unión",
"Status of the event": "Estado del evento",
"Tentative": "Tentativa",
"Confirmed": "Confirmada",
"Cancelled": "Cancelada",
"Event banner image description": "Descripción de la imagen del banner del evento",
"Banner image": "Imagen de banner",
"Maximum attendees": "Asistentes máximos",
"Ticket URL": "URL del ticket",
"Create a new event": "Crea un nuevo evento",
"Moderation policy or code of conduct": "Política de moderación o código de conducta",
"Edit event": "Editar evento",
"Notify when posts are liked": "Notificar cuando les gusten las publicaciones"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Aimé par", "Liked by": "Aimé par",
"Solidaric": "Solidarité", "Solidaric": "Solidarité",
"YouTube Replacement Domain": "Domaine de remplacement YouTube", "YouTube Replacement Domain": "Domaine de remplacement YouTube",
"Notes": "Remarques" "Notes": "Remarques",
"Allow replies.": "Autoriser les réponses.",
"Event": "un événement",
"Event name": "Nom de l'événement",
"Events": "Événements",
"Create an event": "Créer un événement",
"Describe the event": "Décrivez l'événement",
"Start Date": "Date de début",
"End Date": "Date de fin",
"Categories": "Catégories",
"This is a private event.": "Ceci est un événement privé.",
"Allow anonymous participation.": "Autorisez la participation anonyme.",
"Anyone can join": "Tout le monde peut joindre",
"Apply to join": "Postuler pour rejoindre",
"Invitation only": "Invitation uniquement",
"Joining": "Joindre",
"Status of the event": "Statut de l'événement",
"Tentative": "Provisoire",
"Confirmed": "Confirmé",
"Cancelled": "Annulé",
"Event banner image description": "Description de l'image de la bannière de l'événement",
"Banner image": "Image de bannière",
"Maximum attendees": "Nombre maximum de participants",
"Ticket URL": "URL du ticket",
"Create a new event": "Créer un nouvel événement",
"Moderation policy or code of conduct": "Politique de modération ou code de conduite",
"Edit event": "Modifier l'événement",
"Notify when posts are liked": "Notifier lorsque les messages sont aimés"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Thaitin", "Liked by": "Thaitin",
"Solidaric": "Dlúthpháirtíocht", "Solidaric": "Dlúthpháirtíocht",
"YouTube Replacement Domain": "Fearann Athsholáthair YouTube", "YouTube Replacement Domain": "Fearann Athsholáthair YouTube",
"Notes": "Nótaí" "Notes": "Nótaí",
"Allow replies.": "Ceadaigh freagraí.",
"Event": "Imeacht",
"Event name": "Ainm na hócáide",
"Events": "Imeachtaí",
"Create an event": "Cruthaigh imeacht",
"Describe the event": "Déan cur síos ar an ócáid",
"Start Date": "Dáta tosaigh",
"End Date": "Dáta deiridh",
"Categories": "Catagóirí",
"This is a private event.": "Is ócáid phríobháideach é seo.",
"Allow anonymous participation.": "Lig rannpháirtíocht gan ainm.",
"Anyone can join": "Is féidir le duine ar bith a bheith páirteach",
"Apply to join": "Déan iarratas ar bhallraíocht",
"Invitation only": "Cuireadh amháin",
"Joining": "Ag teacht le chéile",
"Status of the event": "Stádas na hócáide",
"Tentative": "Sealadach",
"Confirmed": "Deimhnithe",
"Cancelled": "Cealaithe",
"Event banner image description": "Tuairisc íomhá meirge na hócáide",
"Banner image": "Íomhá meirge",
"Maximum attendees": "Uasmhéid freastail",
"Ticket URL": "URL na dticéad",
"Create a new event": "Cruthaigh imeacht nua",
"Moderation policy or code of conduct": "Beartas modhnóireachta nó cód iompair",
"Edit event": "Cuir imeacht in eagar",
"Notify when posts are liked": "Cuir in iúl cathain is maith poist"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "द्वारा पसंद किया गया", "Liked by": "द्वारा पसंद किया गया",
"Solidaric": "एकजुटता", "Solidaric": "एकजुटता",
"YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन", "YouTube Replacement Domain": "YouTube रिप्लेसमेंट डोमेन",
"Notes": "टिप्पणियाँ" "Notes": "टिप्पणियाँ",
"Allow replies.": "जवाब दें।",
"Event": "प्रतिस्पर्धा",
"Event name": "कार्यक्रम नाम",
"Events": "आयोजन",
"Create an event": "एक घटना बनाएँ",
"Describe the event": "घटना का वर्णन करें",
"Start Date": "आरंभ करने की तिथि",
"End Date": "अंतिम तिथि",
"Categories": "श्रेणियाँ",
"This is a private event.": "यह एक निजी कार्यक्रम है।",
"Allow anonymous participation.": "अनाम भागीदारी की अनुमति दें।",
"Anyone can join": "कोई भी शामिल हो सकता है",
"Apply to join": "जुड़ने के लिए आवेदन करें",
"Invitation only": "केवल आमंत्रण",
"Joining": "में शामिल होने से",
"Status of the event": "घटना की स्थिति",
"Tentative": "जांच का",
"Confirmed": "की पुष्टि",
"Cancelled": "रद्द",
"Event banner image description": "घटना बैनर छवि विवरण",
"Banner image": "बैनर की छवि",
"Maximum attendees": "अधिकतम उपस्थित",
"Ticket URL": "टिकट URL",
"Create a new event": "एक नई घटना बनाएँ",
"Moderation policy or code of conduct": "मॉडरेशन पॉलिसी या आचार संहिता",
"Edit event": "घटना संपादित करें",
"Notify when posts are liked": "पोस्ट पसंद आने पर सूचित करें"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Mi è piaciuto", "Liked by": "Mi è piaciuto",
"Solidaric": "Solidarietà", "Solidaric": "Solidarietà",
"YouTube Replacement Domain": "Dominio sostitutivo di YouTube", "YouTube Replacement Domain": "Dominio sostitutivo di YouTube",
"Notes": "Appunti" "Notes": "Appunti",
"Allow replies.": "Consenti risposte.",
"Event": "Evento",
"Event name": "Nome dell'evento",
"Events": "Eventi",
"Create an event": "Crea un evento",
"Describe the event": "Descrivi l'evento",
"Start Date": "Data d'inizio",
"End Date": "Data di fine",
"Categories": "Categorie",
"This is a private event.": "Questo è un evento privato.",
"Allow anonymous participation.": "Consenti la partecipazione anonima.",
"Anyone can join": "Chiunque può partecipare",
"Apply to join": "Richiedi di partecipare",
"Invitation only": "Solo su invito",
"Joining": "Partecipare",
"Status of the event": "Stato dell'evento",
"Tentative": "Tentativa",
"Confirmed": "Confermata",
"Cancelled": "Annullata",
"Event banner image description": "Descrizione dell'immagine del banner dell'evento",
"Banner image": "Immagine banner",
"Maximum attendees": "Numero massimo di partecipanti",
"Ticket URL": "URL del biglietto",
"Create a new event": "Crea un nuovo evento",
"Moderation policy or code of conduct": "Politica di moderazione o codice di condotta",
"Edit event": "Modifica evento",
"Notify when posts are liked": "Avvisa quando i post sono piaciuti"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "好き", "Liked by": "好き",
"Solidaric": "連帯", "Solidaric": "連帯",
"YouTube Replacement Domain": "YouTube交換ドメイン", "YouTube Replacement Domain": "YouTube交換ドメイン",
"Notes": "ノート" "Notes": "ノート",
"Allow replies.": "返信を許可します。",
"Event": "イベント",
"Event name": "イベント名",
"Events": "イベント",
"Create an event": "イベントを作成する",
"Describe the event": "イベントについて説明する",
"Start Date": "開始日",
"End Date": "終了日",
"Categories": "カテゴリー",
"This is a private event.": "これはプライベートイベントです。",
"Allow anonymous participation.": "匿名参加を許可します。",
"Anyone can join": "誰でも参加できます",
"Apply to join": "参加を申し込む",
"Invitation only": "招待のみ",
"Joining": "接合",
"Status of the event": "イベントのステータス",
"Tentative": "暫定の",
"Confirmed": "確認済み",
"Cancelled": "キャンセル",
"Event banner image description": "イベントバナー画像の説明",
"Banner image": "バナー画像",
"Maximum attendees": "最大参加者",
"Ticket URL": "チケットURL",
"Create a new event": "新しいイベントを作成する",
"Moderation policy or code of conduct": "モデレートポリシーまたは行動規範",
"Edit event": "イベントを編集",
"Notify when posts are liked": "投稿が高く評価されたときに通知する"
} }

View File

@ -251,5 +251,32 @@
"Liked by": "Liked by", "Liked by": "Liked by",
"Solidaric": "Solidaric", "Solidaric": "Solidaric",
"YouTube Replacement Domain": "YouTube Replacement Domain", "YouTube Replacement Domain": "YouTube Replacement Domain",
"Notes": "Notes" "Notes": "Notes",
"Allow replies.": "Allow replies.",
"Event": "Event",
"Event name": "Event name",
"Events": "Events",
"Create an event": "Create an event",
"Describe the event": "Describe the event",
"Start Date": "Start Date",
"End Date": "End Date",
"Categories": "Categories",
"This is a private event.": "This is a private event.",
"Allow anonymous participation.": "Allow anonymous participation.",
"Anyone can join": "Anyone can join",
"Apply to join": "Apply to join",
"Invitation only": "Invitation only",
"Joining": "Joining",
"Status of the event": "Status of the event",
"Tentative": "Tentative",
"Confirmed": "Confirmed",
"Cancelled": "Cancelled",
"Event banner image description": "Event banner image description",
"Banner image": "Banner image",
"Maximum attendees": "Maximum attendees",
"Ticket URL": "Ticket URL",
"Create a new event": "Create a new event",
"Moderation policy or code of conduct": "Moderation policy or code of conduct",
"Edit event": "Edit event",
"Notify when posts are liked": "Notify when posts are liked"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Curtida por", "Liked by": "Curtida por",
"Solidaric": "Solidariedade", "Solidaric": "Solidariedade",
"YouTube Replacement Domain": "Domínio de substituição do YouTube", "YouTube Replacement Domain": "Domínio de substituição do YouTube",
"Notes": "Notas" "Notes": "Notas",
"Allow replies.": "Permitir respostas.",
"Event": "Evento",
"Event name": "Nome do evento",
"Events": "Eventos",
"Create an event": "Crie um evento",
"Describe the event": "Descreva o evento",
"Start Date": "Data de início",
"End Date": "Data final",
"Categories": "Categorias",
"This is a private event.": "Este é um evento privado.",
"Allow anonymous participation.": "Permita a participação anônima.",
"Anyone can join": "Qualquer um pode participar",
"Apply to join": "Aplicar para participar",
"Invitation only": "Somente para convidados",
"Joining": "Juntando",
"Status of the event": "Status do evento",
"Tentative": "Provisório",
"Confirmed": "Confirmada",
"Cancelled": "Cancelada",
"Event banner image description": "Descrição da imagem do banner do evento",
"Banner image": "Imagem de banner",
"Maximum attendees": "Máximo de participantes",
"Ticket URL": "URL do bilhete",
"Create a new event": "Crie um novo evento",
"Moderation policy or code of conduct": "Política de moderação ou código de conduta",
"Edit event": "Editar evento",
"Notify when posts are liked": "Notificar quando as postagens forem curtidas"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "Понравилось", "Liked by": "Понравилось",
"Solidaric": "солидарность", "Solidaric": "солидарность",
"YouTube Replacement Domain": "Запасной домен YouTube", "YouTube Replacement Domain": "Запасной домен YouTube",
"Notes": "Ноты" "Notes": "Ноты",
"Allow replies.": "Разрешить ответы.",
"Event": "Мероприятие",
"Event name": "Название события",
"Events": "События",
"Create an event": "Создать мероприятие",
"Describe the event": "Опишите событие",
"Start Date": "Дата начала",
"End Date": "Дата окончания",
"Categories": "Категории",
"This is a private event.": "Это частное мероприятие.",
"Allow anonymous participation.": "Разрешить анонимное участие.",
"Anyone can join": "Каждый может присоединиться",
"Apply to join": "Подать заявку на присоединение",
"Invitation only": "Только приглашение",
"Joining": "Присоединение",
"Status of the event": "Статус мероприятия",
"Tentative": "Предварительно",
"Confirmed": "Подтверждено",
"Cancelled": "Отменено",
"Event banner image description": "Описание изображения баннера мероприятия",
"Banner image": "Изображение баннера",
"Maximum attendees": "Максимальное количество участников",
"Ticket URL": "URL билета",
"Create a new event": "Создать новое мероприятие",
"Moderation policy or code of conduct": "Политика модерации или кодекс поведения",
"Edit event": "Изменить мероприятие",
"Notify when posts are liked": "Уведомлять, когда публикации нравятся"
} }

View File

@ -255,5 +255,32 @@
"Liked by": "喜欢的人", "Liked by": "喜欢的人",
"Solidaric": "团结互助", "Solidaric": "团结互助",
"YouTube Replacement Domain": "YouTube替换域", "YouTube Replacement Domain": "YouTube替换域",
"Notes": "笔记" "Notes": "笔记",
"Allow replies.": "允许回复。",
"Event": "事件",
"Event name": "活动名称",
"Events": "大事记",
"Create an event": "建立活动",
"Describe the event": "描述事件",
"Start Date": "开始日期",
"End Date": "结束日期",
"Categories": "分类目录",
"This is a private event.": "这是私人活动。",
"Allow anonymous participation.": "允许匿名参与。",
"Anyone can join": "任何人都可以加入",
"Apply to join": "申请加入",
"Invitation only": "仅邀请",
"Joining": "加盟",
"Status of the event": "活动状态",
"Tentative": "暂定",
"Confirmed": "已确认",
"Cancelled": "取消",
"Event banner image description": "活动横幅图片说明",
"Banner image": "横幅图片",
"Maximum attendees": "参加人数上限",
"Ticket URL": "工单URL",
"Create a new event": "建立新活动",
"Moderation policy or code of conduct": "审核政策或行为准则",
"Edit event": "编辑活动",
"Notify when posts are liked": "通知喜欢的帖子"
} }

View File

@ -19,6 +19,20 @@ from calendar import monthrange
from followingCalendar import addPersonToCalendar from followingCalendar import addPersonToCalendar
def removeIdEnding(idStr: str) -> str:
"""Removes endings such as /activity and /undo
"""
if idStr.endswith('/activity'):
idStr = idStr[:-len('/activity')]
elif idStr.endswith('/undo'):
idStr = idStr[:-len('/undo')]
elif idStr.endswith('/event'):
idStr = idStr[:-len('/event')]
elif idStr.endswith('/replies'):
idStr = idStr[:-len('/replies')]
return idStr
def getProtocolPrefixes() -> []: def getProtocolPrefixes() -> []:
"""Returns a list of valid prefixes """Returns a list of valid prefixes
""" """
@ -384,13 +398,13 @@ def locatePost(baseDir: str, nickname: str, domain: str,
extension = 'replies' extension = 'replies'
# if this post in the shared inbox? # if this post in the shared inbox?
postUrl = postUrl.replace('/', '#').replace('/activity', '').strip() postUrl = removeIdEnding(postUrl.strip()).replace('/', '#')
# add the extension # add the extension
postUrl = postUrl + '.' + extension postUrl = postUrl + '.' + extension
# search boxes # search boxes
boxes = ('inbox', 'outbox', 'tlblogs') boxes = ('inbox', 'outbox', 'tlblogs', 'tlevents')
accountDir = baseDir + '/accounts/' + nickname + '@' + domain + '/' accountDir = baseDir + '/accounts/' + nickname + '@' + domain + '/'
for boxName in boxes: for boxName in boxes:
postFilename = accountDir + boxName + '/' + postUrl postFilename = accountDir + boxName + '/' + postUrl
@ -402,7 +416,7 @@ def locatePost(baseDir: str, nickname: str, domain: str,
if os.path.isfile(postFilename): if os.path.isfile(postFilename):
return postFilename return postFilename
print('WARN: unable to locate ' + nickname + ' ' + postUrl) # print('WARN: unable to locate ' + nickname + ' ' + postUrl)
return None return None
@ -435,7 +449,7 @@ def removeModerationPostFromIndex(baseDir: str, postUrl: str,
moderationIndexFile = baseDir + '/accounts/moderation.txt' moderationIndexFile = baseDir + '/accounts/moderation.txt'
if not os.path.isfile(moderationIndexFile): if not os.path.isfile(moderationIndexFile):
return return
postId = postUrl.replace('/activity', '') postId = removeIdEnding(postUrl)
if postId in open(moderationIndexFile).read(): if postId in open(moderationIndexFile).read():
with open(moderationIndexFile, "r") as f: with open(moderationIndexFile, "r") as f:
lines = f.readlines() lines = f.readlines()
@ -463,7 +477,7 @@ def isReplyToBlogPost(baseDir: str, nickname: str, domain: str,
nickname + '@' + domain + '/tlblogs.index' nickname + '@' + domain + '/tlblogs.index'
if not os.path.isfile(blogsIndexFilename): if not os.path.isfile(blogsIndexFilename):
return False return False
postId = postJsonObject['object']['inReplyTo'].replace('/activity', '') postId = removeIdEnding(postJsonObject['object']['inReplyTo'])
postId = postId.replace('/', '#') postId = postId.replace('/', '#')
if postId in open(blogsIndexFilename).read(): if postId in open(blogsIndexFilename).read():
return True return True
@ -494,7 +508,7 @@ def deletePost(baseDir: str, httpPrefix: str,
# remove from recent posts cache in memory # remove from recent posts cache in memory
if recentPostsCache: if recentPostsCache:
postId = \ postId = \
postJsonObject['id'].replace('/activity', '').replace('/', '#') removeIdEnding(postJsonObject['id']).replace('/', '#')
if recentPostsCache.get('index'): if recentPostsCache.get('index'):
if postId in recentPostsCache['index']: if postId in recentPostsCache['index']:
recentPostsCache['index'].remove(postId) recentPostsCache['index'].remove(postId)
@ -526,7 +540,7 @@ def deletePost(baseDir: str, httpPrefix: str,
if isinstance(postJsonObject['object'], dict): if isinstance(postJsonObject['object'], dict):
if postJsonObject['object'].get('moderationStatus'): if postJsonObject['object'].get('moderationStatus'):
if postJsonObject.get('id'): if postJsonObject.get('id'):
postId = postJsonObject['id'].replace('/activity', '') postId = removeIdEnding(postJsonObject['id'])
removeModerationPostFromIndex(baseDir, postId, debug) removeModerationPostFromIndex(baseDir, postId, debug)
# remove any hashtags index entries # remove any hashtags index entries
@ -540,8 +554,7 @@ def deletePost(baseDir: str, httpPrefix: str,
if postJsonObject['object'].get('id') and \ if postJsonObject['object'].get('id') and \
postJsonObject['object'].get('tag'): postJsonObject['object'].get('tag'):
# get the id of the post # get the id of the post
postId = \ postId = removeIdEnding(postJsonObject['object']['id'])
postJsonObject['object']['id'].replace('/activity', '')
for tag in postJsonObject['object']['tag']: for tag in postJsonObject['object']['tag']:
if tag['type'] != 'Hashtag': if tag['type'] != 'Hashtag':
continue continue
@ -600,6 +613,7 @@ def validNickname(domain: str, nickname: str) -> bool:
'public', 'followers', 'public', 'followers',
'channel', 'capabilities', 'calendar', 'channel', 'capabilities', 'calendar',
'tlreplies', 'tlmedia', 'tlblogs', 'tlreplies', 'tlmedia', 'tlblogs',
'tlevents',
'moderation', 'activity', 'undo', 'moderation', 'activity', 'undo',
'reply', 'replies', 'question', 'like', 'reply', 'replies', 'question', 'like',
'likes', 'users', 'statuses', 'likes', 'users', 'statuses',
@ -710,7 +724,7 @@ def getCachedPostFilename(baseDir: str, nickname: str, domain: str,
return None return None
cachedPostFilename = \ cachedPostFilename = \
cachedPostDir + \ cachedPostDir + \
'/' + postJsonObject['id'].replace('/activity', '').replace('/', '#') '/' + removeIdEnding(postJsonObject['id']).replace('/', '#')
cachedPostFilename = cachedPostFilename + '.html' cachedPostFilename = cachedPostFilename + '.html'
return cachedPostFilename return cachedPostFilename
@ -727,7 +741,7 @@ def removePostFromCache(postJsonObject: {}, recentPostsCache: {}):
postId = postJsonObject['id'] postId = postJsonObject['id']
if '#' in postId: if '#' in postId:
postId = postId.split('#', 1)[0] postId = postId.split('#', 1)[0]
postId = postId.replace('/activity', '').replace('/', '#') postId = removeIdEnding(postId).replace('/', '#')
if postId not in recentPostsCache['index']: if postId not in recentPostsCache['index']:
return return
@ -747,7 +761,7 @@ def updateRecentPostsCache(recentPostsCache: {}, maxRecentPosts: int,
postId = postJsonObject['id'] postId = postJsonObject['id']
if '#' in postId: if '#' in postId:
postId = postId.split('#', 1)[0] postId = postId.split('#', 1)[0]
postId = postId.replace('/activity', '').replace('/', '#') postId = removeIdEnding(postId).replace('/', '#')
if recentPostsCache.get('index'): if recentPostsCache.get('index'):
if postId in recentPostsCache['index']: if postId in recentPostsCache['index']:
return return
@ -757,6 +771,7 @@ def updateRecentPostsCache(recentPostsCache: {}, maxRecentPosts: int,
recentPostsCache['html'][postId] = htmlStr recentPostsCache['html'][postId] = htmlStr
while len(recentPostsCache['html'].items()) > maxRecentPosts: while len(recentPostsCache['html'].items()) > maxRecentPosts:
postId = recentPostsCache['index'][0]
recentPostsCache['index'].pop(0) recentPostsCache['index'].pop(0)
del recentPostsCache['json'][postId] del recentPostsCache['json'][postId]
del recentPostsCache['html'][postId] del recentPostsCache['html'][postId]
@ -792,6 +807,43 @@ def mergeDicts(dict1: {}, dict2: {}) -> {}:
return res return res
def isEventPost(messageJson: {}) -> bool:
"""Is the given post a mobilizon-type event activity?
See https://framagit.org/framasoft/mobilizon/-/blob/
master/lib/federation/activity_stream/converter/event.ex
"""
if not messageJson.get('id'):
return False
if not messageJson.get('actor'):
return False
if not messageJson.get('object'):
return False
if not isinstance(messageJson['object'], dict):
return False
if not messageJson['object'].get('type'):
return False
if messageJson['object']['type'] != 'Event':
return False
print('Event arriving')
if not messageJson['object'].get('startTime'):
print('No event start time')
return False
if not messageJson['object'].get('actor'):
print('No event actor')
return False
if not messageJson['object'].get('content'):
print('No event content')
return False
if not messageJson['object'].get('name'):
print('No event name')
return False
if not messageJson['object'].get('uuid'):
print('No event UUID')
return False
print('Event detected')
return True
def isBlogPost(postJsonObject: {}) -> bool: def isBlogPost(postJsonObject: {}) -> bool:
"""Is the given post a blog post? """Is the given post a blog post?
""" """
@ -1071,7 +1123,7 @@ def updateAnnounceCollection(recentPostsCache: {},
return return
if not isinstance(postJsonObject['object'], dict): if not isinstance(postJsonObject['object'], dict):
return return
postUrl = postJsonObject['id'].replace('/activity', '') + '/shares' postUrl = removeIdEnding(postJsonObject['id']) + '/shares'
if not postJsonObject['object'].get('shares'): if not postJsonObject['object'].get('shares'):
if debug: if debug:
print('DEBUG: Adding initial shares (announcements) to ' + print('DEBUG: Adding initial shares (announcements) to ' +

View File

@ -25,9 +25,11 @@ from ssb import getSSBAddress
from tox import getToxAddress from tox import getToxAddress
from matrix import getMatrixAddress from matrix import getMatrixAddress
from donate import getDonationUrl from donate import getDonationUrl
from utils import removeIdEnding
from utils import getProtocolPrefixes from utils import getProtocolPrefixes
from utils import getFileCaseInsensitive from utils import getFileCaseInsensitive
from utils import searchBoxPosts from utils import searchBoxPosts
from utils import isEventPost
from utils import isBlogPost from utils import isBlogPost
from utils import updateRecentPostsCache from utils import updateRecentPostsCache
from utils import getNicknameFromActor from utils import getNicknameFromActor
@ -1081,6 +1083,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
isGroup = '' isGroup = ''
followDMs = '' followDMs = ''
removeTwitter = '' removeTwitter = ''
notifyLikes = ''
mediaInstanceStr = '' mediaInstanceStr = ''
displayNickname = nickname displayNickname = nickname
bioStr = '' bioStr = ''
@ -1128,6 +1131,9 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
if os.path.isfile(baseDir + '/accounts/' + if os.path.isfile(baseDir + '/accounts/' +
nickname + '@' + domain + '/.removeTwitter'): nickname + '@' + domain + '/.removeTwitter'):
removeTwitter = 'checked' removeTwitter = 'checked'
if os.path.isfile(baseDir + '/accounts/' +
nickname + '@' + domain + '/.notifyLikes'):
notifyLikes = 'checked'
mediaInstance = getConfigParam(baseDir, "mediaInstance") mediaInstance = getConfigParam(baseDir, "mediaInstance")
if mediaInstance: if mediaInstance:
@ -1463,6 +1469,10 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
' <input type="checkbox" class="profilecheckbox" ' + \ ' <input type="checkbox" class="profilecheckbox" ' + \
'name="mediaInstance" ' + mediaInstanceStr + '> ' + \ 'name="mediaInstance" ' + mediaInstanceStr + '> ' + \
translate['This is a media instance'] + '<br>\n' translate['This is a media instance'] + '<br>\n'
editProfileForm += \
' <input type="checkbox" class="profilecheckbox" ' + \
'name="notifyLikes" ' + notifyLikes + '> ' + \
translate['Notify when posts are liked'] + '<br>\n'
editProfileForm += \ editProfileForm += \
' <br><b><label class="labels">' + \ ' <br><b><label class="labels">' + \
@ -1837,6 +1847,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
replyStr = '' replyStr = ''
showPublicOnDropdown = True showPublicOnDropdown = True
messageBoxHeight = 400
if not path.endswith('/newshare'): if not path.endswith('/newshare'):
if not path.endswith('/newreport'): if not path.endswith('/newreport'):
@ -1920,15 +1931,31 @@ def htmlNewPost(mediaInstance: bool, translate: {},
pathBase = path.replace('/newreport', '').replace('/newpost', '') pathBase = path.replace('/newreport', '').replace('/newpost', '')
pathBase = pathBase.replace('/newblog', '').replace('/newshare', '') pathBase = pathBase.replace('/newblog', '').replace('/newshare', '')
pathBase = pathBase.replace('/newunlisted', '') pathBase = pathBase.replace('/newunlisted', '')
pathBase = pathBase.replace('/newevent', '')
pathBase = pathBase.replace('/newreminder', '') pathBase = pathBase.replace('/newreminder', '')
pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '') pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '')
newPostImageSection = ' <div class="container">' newPostImageSection = ' <div class="container">'
if not path.endswith('/newevent'):
newPostImageSection += \ newPostImageSection += \
' <label class="labels">' + translate['Image description'] + \ ' <label class="labels">' + \
'</label>\n' translate['Image description'] + '</label>\n'
else:
newPostImageSection += \
' <label class="labels">' + \
translate['Event banner image description'] + '</label>\n'
newPostImageSection += \ newPostImageSection += \
' <input type="text" name="imageDescription">\n' ' <input type="text" name="imageDescription">\n'
if path.endswith('/newevent'):
newPostImageSection += \
' <label class="labels">' + \
translate['Banner image'] + '</label>\n'
newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \
' accept=".png, .jpg, .jpeg, .gif, .webp">\n'
else:
newPostImageSection += \ newPostImageSection += \
' <input type="file" id="attachpic" name="attachpic"' ' <input type="file" id="attachpic" name="attachpic"'
newPostImageSection += \ newPostImageSection += \
@ -1969,6 +1996,12 @@ def htmlNewPost(mediaInstance: bool, translate: {},
scopeIcon = 'scope_reminder.png' scopeIcon = 'scope_reminder.png'
scopeDescription = translate['Reminder'] scopeDescription = translate['Reminder']
endpoint = 'newreminder' endpoint = 'newreminder'
elif path.endswith('/newevent'):
scopeIcon = 'scope_event.png'
scopeDescription = translate['Event']
endpoint = 'newevent'
placeholderSubject = translate['Event name']
placeholderMessage = translate['Describe the event'] + '...'
elif path.endswith('/newreport'): elif path.endswith('/newreport'):
scopeIcon = 'scope_report.png' scopeIcon = 'scope_report.png'
scopeDescription = translate['Report'] scopeDescription = translate['Report']
@ -2027,29 +2060,139 @@ def htmlNewPost(mediaInstance: bool, translate: {},
if endpoint != 'newshare' and \ if endpoint != 'newshare' and \
endpoint != 'newreport' and \ endpoint != 'newreport' and \
endpoint != 'newquestion': endpoint != 'newquestion':
dateAndLocation = '<div class="container">' dateAndLocation = '<div class="container">\n'
if not inReplyTo: if endpoint == 'newevent':
# event status
dateAndLocation += '<label class="labels">' + \
translate['Status of the event'] + ':</label><br>\n'
dateAndLocation += '<input type="radio" id="tentative" ' + \
'name="eventStatus" value="tentative">\n'
dateAndLocation += '<label class="labels" for="tentative">' + \
translate['Tentative'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="confirmed" ' + \
'name="eventStatus" value="confirmed" checked>\n'
dateAndLocation += '<label class="labels" for="confirmed">' + \
translate['Confirmed'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="cancelled" ' + \
'name="eventStatus" value="cancelled">\n'
dateAndLocation += '<label class="labels" for="cancelled">' + \
translate['Cancelled'] + '</label><br>\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# maximum attendees
dateAndLocation += '<label class="labels" ' + \
'for="maximumAttendeeCapacity">' + \
translate['Maximum attendees'] + ':</label>\n'
dateAndLocation += '<input type="number" ' + \
'id="maximumAttendeeCapacity" ' + \
'name="maximumAttendeeCapacity" min="1" max="999999" ' + \
'value="100">\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# event joining options
dateAndLocation += '<label class="labels">' + \
translate['Joining'] + ':</label><br>\n'
dateAndLocation += '<input type="radio" id="free" ' + \
'name="joinMode" value="free" checked>\n'
dateAndLocation += '<label class="labels" for="free">' + \
translate['Anyone can join'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="restricted" ' + \
'name="joinMode" value="restricted">\n'
dateAndLocation += '<label class="labels" for="female">' + \
translate['Apply to join'] + '</label><br>\n'
dateAndLocation += '<input type="radio" id="invite" ' + \
'name="joinMode" value="invite">\n'
dateAndLocation += '<label class="labels" for="other">' + \
translate['Invitation only'] + '</label>\n'
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
# Event posts don't allow replies - they're just an announcement.
# They also have a few more checkboxes
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="privateEvent"><label class="labels"> ' + \
translate['This is a private event.'] + '</label></p>\n'
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="anonymousParticipationEnabled">' + \
'<label class="labels"> ' + \
translate['Allow anonymous participation.'] + '</label></p>\n'
else:
dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \
'name="commentsEnabled" checked><label class="labels"> ' + \
translate['Allow replies.'] + '</label></p>\n'
if not inReplyTo and endpoint != 'newevent':
dateAndLocation += \ dateAndLocation += \
'<p><input type="checkbox" class="profilecheckbox" ' + \ '<p><input type="checkbox" class="profilecheckbox" ' + \
'name="schedulePost"><label class="labels"> ' + \ 'name="schedulePost"><label class="labels"> ' + \
translate['This is a scheduled post.'] + '</label></p>\n' translate['This is a scheduled post.'] + '</label></p>\n'
if endpoint != 'newevent':
dateAndLocation += \ dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \ '<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \ 'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n' iconsDir + '/calendar.png"/>\n'
# select a date and time for this post
dateAndLocation += '<label class="labels">' + \ dateAndLocation += '<label class="labels">' + \
translate['Date'] + ': </label>\n' translate['Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="eventDate">\n' dateAndLocation += '<input type="date" name="eventDate">\n'
dateAndLocation += '<label class="labelsright">' + \ dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':' translate['Time'] + ':'
dateAndLocation += '<input type="time" name="eventTime"></label></p>\n' dateAndLocation += \
'<input type="time" name="eventTime"></label></p>\n'
else:
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
dateAndLocation += \
'<p><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n'
# select start time for the event
dateAndLocation += '<label class="labels">' + \
translate['Start Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="eventDate">\n'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += \
'<input type="time" name="eventTime"></label></p>\n'
# select end time for the event
dateAndLocation += \
'<br><img loading="lazy" alt="" title="" ' + \
'class="emojicalendar" src="/' + \
iconsDir + '/calendar.png"/>\n'
dateAndLocation += '<label class="labels">' + \
translate['End Date'] + ': </label>\n'
dateAndLocation += '<input type="date" name="endDate">\n'
dateAndLocation += '<label class="labelsright">' + \
translate['Time'] + ':'
dateAndLocation += \
'<input type="time" name="endTime"></label>\n'
if endpoint == 'newevent':
dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n'
dateAndLocation += '<br><label class="labels">' + \
translate['Moderation policy or code of conduct'] + \
': </label>\n'
dateAndLocation += \
' <textarea id="message" ' + \
'name="repliesModerationOption" style="height:' + \
str(messageBoxHeight) + 'px"></textarea>\n'
dateAndLocation += '</div>\n' dateAndLocation += '</div>\n'
dateAndLocation += '<div class="container">\n' dateAndLocation += '<div class="container">\n'
dateAndLocation += '<br><label class="labels">' + \ dateAndLocation += '<br><label class="labels">' + \
translate['Location'] + ': </label>\n' translate['Location'] + ': </label>\n'
dateAndLocation += '<input type="text" name="location">\n' dateAndLocation += '<input type="text" name="location">\n'
if endpoint == 'newevent':
dateAndLocation += '<br><label class="labels">' + \
translate['Ticket URL'] + ': </label>\n'
dateAndLocation += '<input type="text" name="ticketUrl">\n'
dateAndLocation += '<br><label class="labels">' + \
translate['Categories'] + ': </label>\n'
dateAndLocation += '<input type="text" name="category">\n'
dateAndLocation += '</div>\n' dateAndLocation += '</div>\n'
newPostForm = htmlHeader(cssFilename, newPostCSS) newPostForm = htmlHeader(cssFilename, newPostCSS)
@ -2093,6 +2236,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
dropdownUnlistedSuffix = '/newunlisted' dropdownUnlistedSuffix = '/newunlisted'
dropdownFollowersSuffix = '/newfollowers' dropdownFollowersSuffix = '/newfollowers'
dropdownDMSuffix = '/newdm' dropdownDMSuffix = '/newdm'
dropdownEventSuffix = '/newevent'
dropdownReminderSuffix = '/newreminder' dropdownReminderSuffix = '/newreminder'
dropdownReportSuffix = '/newreport' dropdownReportSuffix = '/newreport'
if inReplyTo or mentions: if inReplyTo or mentions:
@ -2101,6 +2245,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
dropdownUnlistedSuffix = '' dropdownUnlistedSuffix = ''
dropdownFollowersSuffix = '' dropdownFollowersSuffix = ''
dropdownDMSuffix = '' dropdownDMSuffix = ''
dropdownEventSuffix = ''
dropdownReminderSuffix = '' dropdownReminderSuffix = ''
dropdownReportSuffix = '' dropdownReportSuffix = ''
if inReplyTo: if inReplyTo:
@ -2176,6 +2321,12 @@ def htmlNewPost(mediaInstance: bool, translate: {},
iconsDir + '/scope_reminder.png"/><b>' + translate['Reminder'] + \ iconsDir + '/scope_reminder.png"/><b>' + translate['Reminder'] + \
'</b><br>' + translate['Scheduled note to yourself'] + \ '</b><br>' + translate['Scheduled note to yourself'] + \
'</li></a>\n' '</li></a>\n'
dropDownContent += " " \
'<a href="' + pathBase + dropdownEventSuffix + \
'"><li><img loading="lazy" alt="" title="" src="/' + \
iconsDir + '/scope_event.png"/><b>' + translate['Event'] + \
'</b><br>' + translate['Create an event'] + \
'</li></a>\n'
dropDownContent += " " \ dropDownContent += " " \
'<a href="' + pathBase + dropdownReportSuffix + \ '<a href="' + pathBase + dropdownReportSuffix + \
'"><li><img loading="lazy" alt="" title="" src="/' + iconsDir + \ '"><li><img loading="lazy" alt="" title="" src="/' + iconsDir + \
@ -2251,7 +2402,6 @@ def htmlNewPost(mediaInstance: bool, translate: {},
newPostForm += \ newPostForm += \
' <br><label class="labels">' + placeholderMessage + '</label>' ' <br><label class="labels">' + placeholderMessage + '</label>'
messageBoxHeight = 400
if mediaInstance: if mediaInstance:
messageBoxHeight = 200 messageBoxHeight = 200
@ -3186,7 +3336,7 @@ def insertQuestion(baseDir: str, translate: {},
return content return content
if len(postJsonObject['object']['oneOf']) == 0: if len(postJsonObject['object']['oneOf']) == 0:
return content return content
messageId = postJsonObject['id'].replace('/activity', '') messageId = removeIdEnding(postJsonObject['id'])
if '#' in messageId: if '#' in messageId:
messageId = messageId.split('#', 1)[0] messageId = messageId.split('#', 1)[0]
pageNumberStr = '' pageNumberStr = ''
@ -3654,7 +3804,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
avatarPosition = '' avatarPosition = ''
messageId = '' messageId = ''
if postJsonObject.get('id'): if postJsonObject.get('id'):
messageId = postJsonObject['id'].replace('/activity', '') messageId = removeIdEnding(postJsonObject['id'])
messageIdStr = '' messageIdStr = ''
if messageId: if messageId:
@ -3743,7 +3893,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
if boxName == 'tlbookmarks' or boxName == 'bookmarks': if boxName == 'tlbookmarks' or boxName == 'bookmarks':
return '' return ''
timelinePostBookmark = postJsonObject['id'].replace('/activity', '') timelinePostBookmark = removeIdEnding(postJsonObject['id'])
timelinePostBookmark = timelinePostBookmark.replace('://', '-') timelinePostBookmark = timelinePostBookmark.replace('://', '-')
timelinePostBookmark = timelinePostBookmark.replace('/', '-') timelinePostBookmark = timelinePostBookmark.replace('/', '-')
@ -3828,7 +3978,13 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
iconsDir + '/dm.png" class="DMicon"/>\n' iconsDir + '/dm.png" class="DMicon"/>\n'
replyStr = '' replyStr = ''
if showIcons: # check if replying is permitted
commentsEnabled = True
if 'commentsEnabled' in postJsonObject['object']:
if postJsonObject['object']['commentsEnabled'] is False:
commentsEnabled = False
if showIcons and commentsEnabled:
# reply is permitted - create reply icon
replyToLink = postJsonObject['object']['id'] replyToLink = postJsonObject['object']['id']
if postJsonObject['object'].get('attributedTo'): if postJsonObject['object'].get('attributedTo'):
if isinstance(postJsonObject['object']['attributedTo'], str): if isinstance(postJsonObject['object']['attributedTo'], str):
@ -3872,20 +4028,35 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
translate['Reply to this post'] + \ translate['Reply to this post'] + \
' |" src="/' + iconsDir + '/reply.png"/></a>\n' ' |" src="/' + iconsDir + '/reply.png"/></a>\n'
isEvent = isEventPost(postJsonObject)
editStr = '' editStr = ''
if fullDomain + '/users/' + nickname in postJsonObject['actor']: if fullDomain + '/users/' + nickname in postJsonObject['actor']:
if isBlogPost(postJsonObject):
if '/statuses/' in postJsonObject['object']['id']: if '/statuses/' in postJsonObject['object']['id']:
if isBlogPost(postJsonObject):
blogPostId = postJsonObject['object']['id']
editStr += \ editStr += \
'<a class="imageAnchor" href="/users/' + nickname + \ '<a class="imageAnchor" href="/users/' + nickname + \
'/tlblogs?editblogpost=' + \ '/tlblogs?editblogpost=' + \
postJsonObject['object']['id'].split('/statuses/')[1] + \ blogPostId.split('/statuses/')[1] + \
'?actor=' + actorNickname + \ '?actor=' + actorNickname + \
'" title="' + translate['Edit blog post'] + '">' + \ '" title="' + translate['Edit blog post'] + '">' + \
'<img loading="lazy" title="' + \ '<img loading="lazy" title="' + \
translate['Edit blog post'] + '" alt="' + \ translate['Edit blog post'] + '" alt="' + \
translate['Edit blog post'] + \ translate['Edit blog post'] + \
' |" src="/' + iconsDir + '/edit.png"/></a>\n' ' |" src="/' + iconsDir + '/edit.png"/></a>\n'
elif isEvent:
eventPostId = postJsonObject['object']['id']
editStr += \
'<a class="imageAnchor" href="/users/' + nickname + \
'/tlblogs?editeventpost=' + \
eventPostId.split('/statuses/')[1] + \
'?actor=' + actorNickname + \
'" title="' + translate['Edit event'] + '">' + \
'<img loading="lazy" title="' + \
translate['Edit event'] + '" alt="' + \
translate['Edit event'] + \
' |" src="/' + iconsDir + '/edit.png"/></a>\n'
announceStr = '' announceStr = ''
if not isModerationPost and showRepeatIcon: if not isModerationPost and showRepeatIcon:
@ -4448,7 +4619,7 @@ def htmlTimeline(defaultTimeline: str,
if boxName == 'tlshares': if boxName == 'tlshares':
os.remove(newShareFile) os.remove(newShareFile)
# should the Moderation button be highlighted? # should the Moderation/reports button be highlighted?
newReport = False newReport = False
newReportFile = accountDir + '/.newReport' newReportFile = accountDir + '/.newReport'
if os.path.isfile(newReportFile): if os.path.isfile(newReportFile):
@ -4456,11 +4627,16 @@ def htmlTimeline(defaultTimeline: str,
if boxName == 'moderation': if boxName == 'moderation':
os.remove(newReportFile) os.remove(newReportFile)
# directory where icons are found
# This changes depending upon theme
iconsDir = getIconsDir(baseDir) iconsDir = getIconsDir(baseDir)
# the css filename
cssFilename = baseDir + '/epicyon-profile.css' cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'): if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css' cssFilename = baseDir + '/epicyon.css'
# filename of the banner shown at the top
bannerFile = 'banner.png' bannerFile = 'banner.png'
bannerFilename = baseDir + '/accounts/' + \ bannerFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '/' + bannerFile nickname + '@' + domain + '/' + bannerFile
@ -4476,16 +4652,20 @@ def htmlTimeline(defaultTimeline: str,
bannerFile = 'banner.webp' bannerFile = 'banner.webp'
with open(cssFilename, 'r') as cssFile: with open(cssFilename, 'r') as cssFile:
# load css
profileStyle = \ profileStyle = \
cssFile.read().replace('banner.png', cssFile.read().replace('banner.png',
'/users/' + nickname + '/' + bannerFile) '/users/' + nickname + '/' + bannerFile)
# replace any https within the css with whatever prefix is needed
if httpPrefix != 'https': if httpPrefix != 'https':
profileStyle = \ profileStyle = \
profileStyle.replace('https://', profileStyle.replace('https://',
httpPrefix + '://') httpPrefix + '://')
# is the user a moderator?
moderator = isModerator(baseDir, nickname) moderator = isModerator(baseDir, nickname)
# the appearance of buttons - highlighted or not
inboxButton = 'button' inboxButton = 'button'
blogsButton = 'button' blogsButton = 'button'
dmButton = 'button' dmButton = 'button'
@ -4496,6 +4676,7 @@ def htmlTimeline(defaultTimeline: str,
repliesButton = 'buttonhighlighted' repliesButton = 'buttonhighlighted'
mediaButton = 'button' mediaButton = 'button'
bookmarksButton = 'button' bookmarksButton = 'button'
eventsButton = 'button'
sentButton = 'button' sentButton = 'button'
sharesButton = 'button' sharesButton = 'button'
if newShare: if newShare:
@ -4529,16 +4710,21 @@ def htmlTimeline(defaultTimeline: str,
sharesButton = 'buttonselectedhighlighted' sharesButton = 'buttonselectedhighlighted'
elif boxName == 'tlbookmarks' or boxName == 'bookmarks': elif boxName == 'tlbookmarks' or boxName == 'bookmarks':
bookmarksButton = 'buttonselected' bookmarksButton = 'buttonselected'
elif boxName == 'tlevents':
eventsButton = 'buttonselected'
# get the full domain, including any port number
fullDomain = domain fullDomain = domain
if port != 80 and port != 443: if port != 80 and port != 443:
if ':' not in domain: if ':' not in domain:
fullDomain = domain + ':' + str(port) fullDomain = domain + ':' + str(port)
usersPath = '/users/' + nickname usersPath = '/users/' + nickname
actor = httpPrefix + '://' + fullDomain + usersPath actor = httpPrefix + '://' + fullDomain + usersPath
showIndividualPostIcons = True showIndividualPostIcons = True
# show an icon for new follow approvals
followApprovals = '' followApprovals = ''
followRequestsFilename = \ followRequestsFilename = \
baseDir + '/accounts/' + \ baseDir + '/accounts/' + \
@ -4558,6 +4744,7 @@ def htmlTimeline(defaultTimeline: str,
'" src="/' + iconsDir + '/person.png"/></a>\n' '" src="/' + iconsDir + '/person.png"/></a>\n'
break break
# moderation / reports button
moderationButtonStr = '' moderationButtonStr = ''
if moderator and not minimal: if moderator and not minimal:
moderationButtonStr = \ moderationButtonStr = \
@ -4567,8 +4754,10 @@ def htmlTimeline(defaultTimeline: str,
htmlHighlightLabel(translate['Mod'], newReport) + \ htmlHighlightLabel(translate['Mod'], newReport) + \
' </span></button></a>\n' ' </span></button></a>\n'
# shares, bookmarks and events buttons
sharesButtonStr = '' sharesButtonStr = ''
bookmarksButtonStr = '' bookmarksButtonStr = ''
eventsButtonStr = ''
if not minimal: if not minimal:
sharesButtonStr = \ sharesButtonStr = \
'<a href="' + usersPath + '/tlshares"><button class="' + \ '<a href="' + usersPath + '/tlshares"><button class="' + \
@ -4581,10 +4770,39 @@ def htmlTimeline(defaultTimeline: str,
bookmarksButton + '"><span>' + translate['Bookmarks'] + \ bookmarksButton + '"><span>' + translate['Bookmarks'] + \
' </span></button></a>\n' ' </span></button></a>\n'
eventsButtonStr = \
'<a href="' + usersPath + '/tlevents"><button class="' + \
eventsButton + '"><span>' + translate['Events'] + \
' </span></button></a>\n'
tlStr = htmlHeader(cssFilename, profileStyle) tlStr = htmlHeader(cssFilename, profileStyle)
if boxName != 'dm': # what screen to go to when a new post is created
if boxName != 'tlblogs': if boxName == 'dm':
newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \
'/newdm"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new DM'] + \
'" alt="| ' + translate['Create a new DM'] + \
'" class="timelineicon"/></a>\n'
elif boxName == 'tlblogs':
newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \
'/newblog"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="| ' + \
translate['Create a new post'] + \
'" class="timelineicon"/></a>\n'
elif boxName == 'tlevents':
newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \
'/newevent"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new event'] + '" alt="| ' + \
translate['Create a new event'] + \
'" class="timelineicon"/></a>\n'
else:
if not manuallyApproveFollowers: if not manuallyApproveFollowers:
newPostButtonStr = \ newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \ '<a class="imageAnchor" href="' + usersPath + \
@ -4601,22 +4819,6 @@ def htmlTimeline(defaultTimeline: str,
translate['Create a new post'] + \ translate['Create a new post'] + \
'" alt="| ' + translate['Create a new post'] + \ '" alt="| ' + translate['Create a new post'] + \
'" class="timelineicon"/></a>\n' '" class="timelineicon"/></a>\n'
else:
newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \
'/newblog"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new post'] + '" alt="| ' + \
translate['Create a new post'] + \
'" class="timelineicon"/></a>\n'
else:
newPostButtonStr = \
'<a class="imageAnchor" href="' + usersPath + \
'/newdm"><img loading="lazy" src="/' + \
iconsDir + '/newpost.png" title="' + \
translate['Create a new DM'] + \
'" alt="| ' + translate['Create a new DM'] + \
'" class="timelineicon"/></a>\n'
# This creates a link to the profile page when viewed # This creates a link to the profile page when viewed
# in lynx, but should be invisible in a graphical web browser # in lynx, but should be invisible in a graphical web browser
@ -4681,6 +4883,7 @@ def htmlTimeline(defaultTimeline: str,
'</span></button></a>\n' '</span></button></a>\n'
# typically the blogs button # typically the blogs button
# but may change if this is a blogging oriented instance
if defaultTimeline != 'tlblogs': if defaultTimeline != 'tlblogs':
if not minimal: if not minimal:
tlStr += \ tlStr += \
@ -4696,14 +4899,19 @@ def htmlTimeline(defaultTimeline: str,
inboxButton + '"><span>' + translate['Inbox'] + \ inboxButton + '"><span>' + translate['Inbox'] + \
'</span></button></a>\n' '</span></button></a>\n'
# button for the outbox
tlStr += \ tlStr += \
' <a href="' + usersPath + \ ' <a href="' + usersPath + \
'/outbox"><button class="' + \ '/outbox"><button class="' + \
sentButton+'"><span>' + translate['Outbox'] + \ sentButton+'"><span>' + translate['Outbox'] + \
'</span></button></a>\n' '</span></button></a>\n'
# add other buttons
tlStr += \ tlStr += \
sharesButtonStr + bookmarksButtonStr + \ sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
moderationButtonStr + newPostButtonStr moderationButtonStr + newPostButtonStr
# the search button
tlStr += \ tlStr += \
' <a class="imageAnchor" href="' + usersPath + \ ' <a class="imageAnchor" href="' + usersPath + \
'/search"><img loading="lazy" src="/' + \ '/search"><img loading="lazy" src="/' + \
@ -4711,6 +4919,7 @@ def htmlTimeline(defaultTimeline: str,
translate['Search and follow'] + '" alt="| ' + \ translate['Search and follow'] + '" alt="| ' + \
translate['Search and follow'] + '" class="timelineicon"/></a>\n' translate['Search and follow'] + '" class="timelineicon"/></a>\n'
# the calendar button
calendarAltText = translate['Calendar'] calendarAltText = translate['Calendar']
if newCalendarEvent: if newCalendarEvent:
# indicate that the calendar icon is highlighted # indicate that the calendar icon is highlighted
@ -4721,6 +4930,7 @@ def htmlTimeline(defaultTimeline: str,
calendarImage + '" title="' + translate['Calendar'] + \ calendarImage + '" title="' + translate['Calendar'] + \
'" alt="| ' + calendarAltText + '" class="timelineicon"/></a>\n' '" alt="| ' + calendarAltText + '" class="timelineicon"/></a>\n'
# the show/hide button, for a simpler header appearance
tlStr += \ tlStr += \
' <a class="imageAnchor" href="' + usersPath + '/minimal' + \ ' <a class="imageAnchor" href="' + usersPath + '/minimal' + \
'"><img loading="lazy" src="/' + iconsDir + \ '"><img loading="lazy" src="/' + iconsDir + \
@ -4781,11 +4991,15 @@ def htmlTimeline(defaultTimeline: str,
if boxName == 'inbox' and pageNumber == 1: if boxName == 'inbox' and pageNumber == 1:
if todaysEventsCheck(baseDir, nickname, domain): if todaysEventsCheck(baseDir, nickname, domain):
now = datetime.now() now = datetime.now()
# happening today button
tlStr += \ tlStr += \
'<center>\n<a href="' + usersPath + '/calendar?year=' + \ '<center>\n<a href="' + usersPath + '/calendar?year=' + \
str(now.year) + '?month=' + str(now.month) + \ str(now.year) + '?month=' + str(now.month) + \
'?day=' + str(now.day) + '"><button class="buttonevent">' + \ '?day=' + str(now.day) + '"><button class="buttonevent">' + \
translate['Happening Today'] + '</button></a>\n' translate['Happening Today'] + '</button></a>\n'
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain): if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \ tlStr += \
'<a href="' + usersPath + \ '<a href="' + usersPath + \
@ -4793,6 +5007,7 @@ def htmlTimeline(defaultTimeline: str,
translate['Happening This Week'] + '</button></a>\n' translate['Happening This Week'] + '</button></a>\n'
tlStr += '</center>\n' tlStr += '</center>\n'
else: else:
# happening this week button
if thisWeeksEventsCheck(baseDir, nickname, domain): if thisWeeksEventsCheck(baseDir, nickname, domain):
tlStr += \ tlStr += \
'<center>\n<a href="' + usersPath + \ '<center>\n<a href="' + usersPath + \
@ -4813,10 +5028,13 @@ def htmlTimeline(defaultTimeline: str,
# show the posts # show the posts
itemCtr = 0 itemCtr = 0
if timelineJson: if timelineJson:
# if this is the media timeline then add an extra gallery container
if boxName == 'tlmedia': if boxName == 'tlmedia':
if pageNumber > 1: if pageNumber > 1:
tlStr += '<br>' tlStr += '<br>'
tlStr += '<div class="galleryContainer">\n' tlStr += '<div class="galleryContainer">\n'
# show each post in the timeline
for item in timelineJson['orderedItems']: for item in timelineJson['orderedItems']:
if item['type'] == 'Create' or \ if item['type'] == 'Create' or \
item['type'] == 'Announce' or \ item['type'] == 'Announce' or \
@ -4830,7 +5048,7 @@ def htmlTimeline(defaultTimeline: str,
if boxName != 'tlmedia' and \ if boxName != 'tlmedia' and \
recentPostsCache.get('index'): recentPostsCache.get('index'):
postId = \ postId = \
item['id'].replace('/activity', '').replace('/', '#') removeIdEnding(item['id']).replace('/', '#')
if postId in recentPostsCache['index']: if postId in recentPostsCache['index']:
if not item.get('muted'): if not item.get('muted'):
if recentPostsCache['html'].get(postId): if recentPostsCache['html'].get(postId):
@ -4942,6 +5160,28 @@ def htmlBookmarks(defaultTimeline: str,
minimal, YTReplacementDomain) minimal, YTReplacementDomain)
def htmlEvents(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, bookmarksJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
minimal: bool, YTReplacementDomain: str) -> str:
"""Show the events as html
"""
manuallyApproveFollowers = \
followerApprovalActive(baseDir, nickname, domain)
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, bookmarksJson,
'tlevents', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers,
minimal, YTReplacementDomain)
def htmlInboxDMs(defaultTimeline: str, def htmlInboxDMs(defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int, recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int, translate: {}, pageNumber: int, itemsPerPage: int,
@ -5105,7 +5345,7 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain, YTReplacementDomain,
False, authorized, False, False, False) False, authorized, False, False, False)
messageId = postJsonObject['id'].replace('/activity', '') messageId = removeIdEnding(postJsonObject['id'])
# show the previous posts # show the previous posts
if isinstance(postJsonObject['object'], dict): if isinstance(postJsonObject['object'], dict):