Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main
2
auth.py
|
|
@ -130,7 +130,7 @@ def storeBasicCredentials(baseDir: str, nickname: str, password: str) -> bool:
|
|||
os.rename(passwordFile + '.new', passwordFile)
|
||||
else:
|
||||
# append to password file
|
||||
with open(passwordFile, "a") as passfile:
|
||||
with open(passwordFile, 'a+') as passfile:
|
||||
passfile.write(storeStr + '\n')
|
||||
else:
|
||||
with open(passwordFile, "w") as passfile:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
|
|||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
from utils import removeIdEnding
|
||||
from utils import isEvil
|
||||
from utils import locatePost
|
||||
from utils import evilIncarnate
|
||||
|
|
@ -214,7 +215,7 @@ def outboxBlock(baseDir: str, httpPrefix: str,
|
|||
if debug:
|
||||
print('DEBUG: c2s block request arrived in outbox')
|
||||
|
||||
messageId = messageJson['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
if '/statuses/' not in messageId:
|
||||
if debug:
|
||||
print('DEBUG: c2s block object is not a status')
|
||||
|
|
@ -293,7 +294,7 @@ def outboxUndoBlock(baseDir: str, httpPrefix: str,
|
|||
if debug:
|
||||
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 debug:
|
||||
print('DEBUG: c2s undo block object is not a status')
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ __status__ = "Production"
|
|||
|
||||
import os
|
||||
from pprint import pprint
|
||||
from utils import removeIdEnding
|
||||
from utils import removePostFromCache
|
||||
from utils import urlPermitted
|
||||
from utils import getNicknameFromActor
|
||||
|
|
@ -607,7 +608,7 @@ def outboxBookmark(recentPostsCache: {},
|
|||
if debug:
|
||||
print('DEBUG: c2s bookmark request arrived in outbox')
|
||||
|
||||
messageId = messageJson['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
if ':' in domain:
|
||||
domain = domain.split(':')[0]
|
||||
postFilename = locatePost(baseDir, nickname, domain, messageId)
|
||||
|
|
@ -667,7 +668,7 @@ def outboxUndoBookmark(recentPostsCache: {},
|
|||
if debug:
|
||||
print('DEBUG: c2s undo bookmark request arrived in outbox')
|
||||
|
||||
messageId = messageJson['object']['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object']['object'])
|
||||
if ':' in domain:
|
||||
domain = domain.split(':')[0]
|
||||
postFilename = locatePost(baseDir, nickname, domain, messageId)
|
||||
|
|
|
|||
261
daemon.py
|
|
@ -6,7 +6,7 @@ __maintainer__ = "Bob Mottram"
|
|||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
|
|
@ -69,6 +69,7 @@ from posts import createBlogPost
|
|||
from posts import createReportPost
|
||||
from posts import createUnlistedPost
|
||||
from posts import createFollowersOnlyPost
|
||||
from posts import createEventPost
|
||||
from posts import createDirectMessagePost
|
||||
from posts import populateRepliesJson
|
||||
from posts import addToField
|
||||
|
|
@ -126,6 +127,7 @@ from webinterface import htmlIndividualPost
|
|||
from webinterface import htmlProfile
|
||||
from webinterface import htmlInbox
|
||||
from webinterface import htmlBookmarks
|
||||
from webinterface import htmlEvents
|
||||
from webinterface import htmlShares
|
||||
from webinterface import htmlOutbox
|
||||
from webinterface import htmlModeration
|
||||
|
|
@ -153,6 +155,7 @@ from shares import getSharesFeedForPerson
|
|||
from shares import addShare
|
||||
from shares import removeShare
|
||||
from shares import expireShares
|
||||
from utils import removeIdEnding
|
||||
from utils import updateLikesCollection
|
||||
from utils import undoLikesCollectionEntry
|
||||
from utils import deletePost
|
||||
|
|
@ -315,12 +318,14 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
|
||||
print('Voting on message ' + messageId)
|
||||
print('Vote for: ' + answer)
|
||||
commentsEnabled = True
|
||||
messageJson = \
|
||||
createPublicPost(self.server.baseDir,
|
||||
nickname,
|
||||
self.server.domain, self.server.port,
|
||||
self.server.httpPrefix,
|
||||
answer, False, False, False,
|
||||
commentsEnabled,
|
||||
None, None, None, True,
|
||||
messageId, messageId, None,
|
||||
False, None, None, None)
|
||||
|
|
@ -2731,10 +2736,9 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
|
||||
self._benchmarkGETtimings(GETstartTime, GETtimings, 32)
|
||||
|
||||
# unrepeatPrivate = False
|
||||
if htmlGET and '?unrepeatprivate=' in self.path:
|
||||
self.path = self.path.replace('?unrepeatprivate=', '?unrepeat=')
|
||||
# unrepeatPrivate = True
|
||||
|
||||
# undo an announce/repeat from the web interface
|
||||
if htmlGET and '?unrepeat=' in self.path:
|
||||
pageNumber = 1
|
||||
|
|
@ -3595,6 +3599,40 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.GETbusy = False
|
||||
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
|
||||
if '/users/' in self.path and self.path.endswith('/editprofile'):
|
||||
msg = htmlEditProfile(self.server.translate,
|
||||
|
|
@ -3619,6 +3657,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.path.endswith('/newfollowers') or
|
||||
self.path.endswith('/newdm') or
|
||||
self.path.endswith('/newreminder') or
|
||||
self.path.endswith('/newevent') or
|
||||
self.path.endswith('/newreport') or
|
||||
self.path.endswith('/newquestion') or
|
||||
self.path.endswith('/newshare'))):
|
||||
|
|
@ -4796,7 +4835,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
else:
|
||||
# don't need authenticated fetch here because
|
||||
# there is already the authorization check
|
||||
msg = json.dumps(inboxFeed,
|
||||
msg = json.dumps(bookmarksFeed,
|
||||
ensure_ascii=False)
|
||||
msg = msg.encode('utf-8')
|
||||
self._set_headers('application/json',
|
||||
|
|
@ -4819,6 +4858,103 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.GETbusy = False
|
||||
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)
|
||||
|
||||
# get outbox feed for a person
|
||||
|
|
@ -5486,11 +5622,13 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
fields['subject'] = None
|
||||
if not fields.get('replyTo'):
|
||||
fields['replyTo'] = None
|
||||
|
||||
if not fields.get('schedulePost'):
|
||||
fields['schedulePost'] = False
|
||||
else:
|
||||
fields['schedulePost'] = True
|
||||
print('DEBUG: shedulePost ' + str(fields['schedulePost']))
|
||||
|
||||
if not fields.get('eventDate'):
|
||||
fields['eventDate'] = None
|
||||
if not fields.get('eventTime'):
|
||||
|
|
@ -5515,6 +5653,14 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mentionsStr = ''
|
||||
if fields.get('mentions'):
|
||||
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':
|
||||
messageJson = \
|
||||
createPublicPost(self.server.baseDir,
|
||||
|
|
@ -5523,7 +5669,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.port,
|
||||
self.server.httpPrefix,
|
||||
mentionsStr + fields['message'],
|
||||
False, False, False,
|
||||
False, False, False, commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5550,7 +5696,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.domain, self.server.port,
|
||||
self.server.httpPrefix,
|
||||
fields['message'],
|
||||
False, False, False,
|
||||
False, False, False, commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5653,7 +5799,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.domain, self.server.port,
|
||||
self.server.httpPrefix,
|
||||
mentionsStr + fields['message'],
|
||||
False, False, False,
|
||||
False, False, False, commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5686,6 +5832,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.httpPrefix,
|
||||
mentionsStr + fields['message'],
|
||||
True, False, False,
|
||||
commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5709,6 +5856,60 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
return 1
|
||||
else:
|
||||
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':
|
||||
messageJson = None
|
||||
print('A DM was posted')
|
||||
|
|
@ -5722,6 +5923,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
mentionsStr +
|
||||
fields['message'],
|
||||
True, False, False,
|
||||
commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5761,7 +5963,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.port,
|
||||
self.server.httpPrefix,
|
||||
mentionsStr + fields['message'],
|
||||
True, False, False,
|
||||
True, False, False, False,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5794,7 +5996,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.domain, self.server.port,
|
||||
self.server.httpPrefix,
|
||||
mentionsStr + fields['message'],
|
||||
True, False, False,
|
||||
True, False, False, True,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -5825,6 +6027,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
self.server.httpPrefix,
|
||||
fields['message'], qOptions,
|
||||
False, False, False,
|
||||
commentsEnabled,
|
||||
filename, attachmentMediaType,
|
||||
fields['imageDescription'],
|
||||
self.server.useBlurHash,
|
||||
|
|
@ -6181,6 +6384,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if not self.path.endswith('confirm'):
|
||||
self.path = self.path.replace('/outbox/', '/outbox')
|
||||
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('/shares/', '/shares')
|
||||
self.path = self.path.replace('/sharedInbox/', '/sharedInbox')
|
||||
|
|
@ -6912,6 +7116,20 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if not removeTwitterActive:
|
||||
if os.path.isfile(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
|
||||
if fields.get('isBot'):
|
||||
if fields['isBot'] == 'on':
|
||||
|
|
@ -8369,15 +8587,16 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
# receive different types of post created by htmlNewPost
|
||||
postTypes = ("newpost", "newblog", "newunlisted", "newfollowers",
|
||||
"newdm", "newreport", "newshare", "newquestion",
|
||||
"editblogpost", "newreminder")
|
||||
"editblogpost", "newreminder", "newevent")
|
||||
for currPostType in postTypes:
|
||||
if not authorized:
|
||||
break
|
||||
|
||||
if currPostType != 'newshare':
|
||||
postRedirect = self.server.defaultTimeline
|
||||
else:
|
||||
if currPostType == 'newshare':
|
||||
postRedirect = 'shares'
|
||||
elif currPostType == 'newevent':
|
||||
postRedirect = 'tlevents'
|
||||
|
||||
pageNumber = self._receiveNewPost(currPostType, self.path)
|
||||
if pageNumber:
|
||||
|
|
@ -8612,8 +8831,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
if self.outboxAuthenticated:
|
||||
if self._postToOutbox(messageJson, __version__):
|
||||
if messageJson.get('id'):
|
||||
locnStr = messageJson['id'].replace('/activity', '')
|
||||
locnStr = locnStr.replace('/undo', '')
|
||||
locnStr = removeIdEnding(messageJson['id'])
|
||||
self.headers['Location'] = locnStr
|
||||
self.send_response(201)
|
||||
self.end_headers()
|
||||
|
|
@ -8658,6 +8876,7 @@ class PubServer(BaseHTTPRequestHandler):
|
|||
|
||||
self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 22)
|
||||
|
||||
if not self.server.unitTest:
|
||||
if not inboxPermittedMessage(self.server.domain,
|
||||
messageJson,
|
||||
self.server.federationList):
|
||||
|
|
@ -8711,6 +8930,17 @@ class PubServerUnitTest(PubServer):
|
|||
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:
|
||||
"""Manages the threads used to send posts
|
||||
"""
|
||||
|
|
@ -8812,7 +9042,7 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
|
|||
pubHandler = partial(PubServer)
|
||||
|
||||
try:
|
||||
httpd = ThreadingHTTPServer(serverAddress, pubHandler)
|
||||
httpd = EpicyonServer(serverAddress, pubHandler)
|
||||
except Exception as e:
|
||||
if e.errno == 98:
|
||||
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))
|
||||
return False
|
||||
|
||||
httpd.unitTest = unitTest
|
||||
httpd.YTReplacementDomain = YTReplacementDomain
|
||||
|
||||
# This counter is used to update the list of blocked domains in memory.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram"
|
|||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
from utils import removeIdEnding
|
||||
from utils import getStatusNumber
|
||||
from utils import urlPermitted
|
||||
from utils import getNicknameFromActor
|
||||
|
|
@ -257,7 +258,7 @@ def outboxDelete(baseDir: str, httpPrefix: str,
|
|||
if debug:
|
||||
print('DEBUG: delete not permitted from other instances')
|
||||
return
|
||||
messageId = messageJson['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
if '/statuses/' not in messageId:
|
||||
if debug:
|
||||
print('DEBUG: c2s delete object is not a status')
|
||||
|
|
|
|||
|
|
@ -1269,6 +1269,26 @@ aside .toggle-inside li {
|
|||
padding: 10px;
|
||||
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) {
|
||||
|
|
@ -1689,4 +1709,24 @@ aside .toggle-inside li {
|
|||
padding: 20px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
epicyon.py
|
|
@ -108,7 +108,7 @@ parser.add_argument('-p', '--port', dest='port', type=int,
|
|||
default=None,
|
||||
help='Port number to run on')
|
||||
parser.add_argument('--postcache', dest='maxRecentPosts', type=int,
|
||||
default=100,
|
||||
default=512,
|
||||
help='The maximum number of recent posts to store in RAM')
|
||||
parser.add_argument('--proxy', dest='proxyPort', type=int, default=None,
|
||||
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')
|
||||
parser.add_argument('-f', '--federate', nargs='+', dest='federationList',
|
||||
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='?',
|
||||
const=True, default=False,
|
||||
help="Allow followers without approval")
|
||||
|
|
@ -829,7 +834,7 @@ if args.message:
|
|||
domain, port,
|
||||
toNickname, toDomain, toPort, ccUrl,
|
||||
httpPrefix, sendMessage, followersOnly,
|
||||
attach, mediaType,
|
||||
args.commentsEnabled, attach, mediaType,
|
||||
attachedImageDescription, useBlurhash,
|
||||
cachedWebfingers, personCache, isArticle,
|
||||
args.debug, replyTo, replyTo, subject)
|
||||
|
|
@ -1751,30 +1756,31 @@ if args.testdata:
|
|||
deleteAllPosts(baseDir, nickname, domain, 'outbox')
|
||||
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
|
||||
"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,
|
||||
"Zoiks!!!",
|
||||
False, True, False, None, None, useBlurhash)
|
||||
False, True, False, True, None, None, useBlurhash)
|
||||
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
|
||||
"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,
|
||||
"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,
|
||||
"And they would have gotten away with it too" +
|
||||
"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)
|
||||
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
|
||||
"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,
|
||||
"another mystery solved #test",
|
||||
False, True, False, None, None, useBlurhash)
|
||||
False, True, False, True, None, None, useBlurhash)
|
||||
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
|
||||
"let's go bowling",
|
||||
False, True, False, None, None, useBlurhash)
|
||||
False, True, False, True, None, None, useBlurhash)
|
||||
|
||||
domainFull = domain + ':' + str(port)
|
||||
clearFollows(baseDir, nickname, domain)
|
||||
|
|
|
|||
|
|
@ -202,14 +202,14 @@ def unfollowPerson(baseDir: str, nickname: str, domain: str,
|
|||
if debug:
|
||||
print('DEBUG: follow file ' + filename + ' was not found')
|
||||
return False
|
||||
if handleToUnfollow.lower() not in open(filename).read().lower():
|
||||
handleToUnfollowLower = handleToUnfollow.lower()
|
||||
if handleToUnfollowLower not in open(filename).read().lower():
|
||||
if debug:
|
||||
print('DEBUG: handle to unfollow ' + handleToUnfollow +
|
||||
' is not in ' + filename)
|
||||
return
|
||||
with open(filename, "r") as f:
|
||||
lines = f.readlines()
|
||||
handleToUnfollowLower = handleToUnfollow.lower()
|
||||
with open(filename, "w") as f:
|
||||
for line in lines:
|
||||
if line.strip("\n").strip("\r").lower() != handleToUnfollowLower:
|
||||
|
|
@ -520,7 +520,7 @@ def storeFollowRequest(baseDir: str,
|
|||
approveFollowsFilename = accountsDir + '/followrequests.txt'
|
||||
if os.path.isfile(approveFollowsFilename):
|
||||
if approveHandle not in open(approveFollowsFilename).read():
|
||||
with open(approveFollowsFilename, "a") as fp:
|
||||
with open(approveFollowsFilename, 'a+') as fp:
|
||||
fp.write(approveHandle + '\n')
|
||||
else:
|
||||
if debug:
|
||||
|
|
|
|||
|
|
@ -43,12 +43,14 @@ def removeEventFromTimeline(eventId: str, tlEventsFilename: str) -> None:
|
|||
pass
|
||||
|
||||
|
||||
def saveEvent(baseDir: str, handle: str, postId: str,
|
||||
def saveEventPost(baseDir: str, handle: str, postId: str,
|
||||
eventJson: {}) -> bool:
|
||||
"""Saves an event to the calendar and/or the events timeline
|
||||
If an event has extra fields, as per Mobilizon,
|
||||
Then it is saved as a separate entity and added to the
|
||||
events timeline
|
||||
See https://framagit.org/framasoft/mobilizon/-/blob/
|
||||
master/lib/federation/activity_stream/converter/event.ex
|
||||
"""
|
||||
calendarPath = baseDir + '/accounts/' + handle + '/calendar'
|
||||
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'):
|
||||
if not validUuid(eventJson['uuid']):
|
||||
return False
|
||||
print('Mobilizon type event')
|
||||
# if this is a full description of an event then save it
|
||||
# as a separate json file
|
||||
eventsPath = baseDir + '/accounts/' + handle + '/events'
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
221
inbox.py
|
|
@ -10,6 +10,8 @@ import json
|
|||
import os
|
||||
import datetime
|
||||
import time
|
||||
from utils import isEventPost
|
||||
from utils import removeIdEnding
|
||||
from utils import getProtocolPrefixes
|
||||
from utils import isBlogPost
|
||||
from utils import removeAvatarFromCache
|
||||
|
|
@ -49,9 +51,11 @@ from filters import isFiltered
|
|||
from announce import updateAnnounceCollection
|
||||
from announce import undoAnnounceCollectionEntry
|
||||
from httpsig import messageContentDigest
|
||||
from posts import validContentWarning
|
||||
from posts import downloadAnnounce
|
||||
from posts import isDM
|
||||
from posts import isReply
|
||||
from posts import isMuted
|
||||
from posts import isImageMedia
|
||||
from posts import sendSignedJson
|
||||
from posts import sendToFollowersThread
|
||||
|
|
@ -64,7 +68,7 @@ from git import isGitPatch
|
|||
from git import receiveGitPatch
|
||||
from followingCalendar import receivingCalendarEvents
|
||||
from content import dangerousMarkup
|
||||
from happening import saveEvent
|
||||
from happening import saveEventPost
|
||||
|
||||
|
||||
def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
||||
|
|
@ -93,7 +97,7 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None:
|
|||
continue
|
||||
tagName = tag['name'].replace('#', '').strip()
|
||||
tagsFilename = tagsDir + '/' + tagName + '.txt'
|
||||
postUrl = postJsonObject['id'].replace('/activity', '')
|
||||
postUrl = removeIdEnding(postJsonObject['id'])
|
||||
postUrl = postUrl.replace('/', '#')
|
||||
daysDiff = datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1)
|
||||
daysSinceEpoch = daysDiff.days
|
||||
|
|
@ -122,12 +126,13 @@ def inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int,
|
|||
session, cachedWebfingers: {}, personCache: {},
|
||||
nickname: str, domain: str, port: int,
|
||||
postJsonObject: {},
|
||||
allowDeletion: bool) -> None:
|
||||
allowDeletion: bool, boxname: str) -> None:
|
||||
"""Converts the json post into html and stores it in a cache
|
||||
This enables the post to be quickly displayed later
|
||||
"""
|
||||
pageNumber = -999
|
||||
avatarUrl = None
|
||||
if boxname != 'tlevents' and boxname != 'outbox':
|
||||
boxName = 'inbox'
|
||||
individualPostAsHtml(recentPostsCache, maxRecentPosts,
|
||||
getIconsDir(baseDir), translate, pageNumber,
|
||||
|
|
@ -230,9 +235,11 @@ def getPersonPubKey(baseDir: str, session, personUrl: str,
|
|||
def inboxMessageHasParams(messageJson: {}) -> bool:
|
||||
"""Checks whether an incoming message contains expected parameters
|
||||
"""
|
||||
expectedParams = ['type', 'actor', 'object']
|
||||
expectedParams = ['actor', 'type', 'object']
|
||||
for param in expectedParams:
|
||||
if not messageJson.get(param):
|
||||
# print('inboxMessageHasParams: ' +
|
||||
# param + ' ' + str(messageJson))
|
||||
return False
|
||||
if not messageJson.get('to'):
|
||||
allowedWithoutToParam = ['Like', 'Follow', 'Request',
|
||||
|
|
@ -248,6 +255,7 @@ def inboxPermittedMessage(domain: str, messageJson: {},
|
|||
"""
|
||||
if not messageJson.get('actor'):
|
||||
return False
|
||||
|
||||
actor = messageJson['actor']
|
||||
# always allow the local domain
|
||||
if domain in actor:
|
||||
|
|
@ -354,15 +362,13 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str,
|
|||
return None
|
||||
originalPostId = None
|
||||
if postJsonObject.get('id'):
|
||||
originalPostId = \
|
||||
postJsonObject['id'].replace('/activity', '').replace('/undo', '')
|
||||
originalPostId = removeIdEnding(postJsonObject['id'])
|
||||
|
||||
currTime = datetime.datetime.utcnow()
|
||||
|
||||
postId = None
|
||||
if postJsonObject.get('id'):
|
||||
postId = postJsonObject['id'].replace('/activity', '')
|
||||
postId = postId.replace('/undo', '')
|
||||
postId = removeIdEnding(postJsonObject['id'])
|
||||
published = currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
if not postId:
|
||||
statusNumber, published = getStatusNumber()
|
||||
|
|
@ -706,9 +712,8 @@ def receiveUndoFollow(session, baseDir: str, httpPrefix: str,
|
|||
nicknameFollowing, domainFollowingFull,
|
||||
nicknameFollower, domainFollowerFull,
|
||||
debug):
|
||||
if debug:
|
||||
print('DEBUG: Follower ' +
|
||||
nicknameFollower + '@' + domainFollowerFull +
|
||||
print(nicknameFollowing + '@' + domainFollowingFull + ': '
|
||||
'Follower ' + nicknameFollower + '@' + domainFollowerFull +
|
||||
' was removed')
|
||||
return True
|
||||
|
||||
|
|
@ -771,6 +776,28 @@ def receiveUndo(session, baseDir: str, httpPrefix: str,
|
|||
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,
|
||||
domain: str, port: int,
|
||||
updateNickname: str, updateDomain: str,
|
||||
|
|
@ -857,7 +884,7 @@ def receiveUpdateToQuestion(recentPostsCache: {}, messageJson: {},
|
|||
return
|
||||
if not messageJson.get('actor'):
|
||||
return
|
||||
messageId = messageJson['id'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['id'])
|
||||
if '#' in messageId:
|
||||
messageId = messageId.split('#', 1)[0]
|
||||
# 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):
|
||||
print('DEBUG: unknown recipient of like - ' + handle)
|
||||
# if this post in the outbox of the person?
|
||||
messageId = messageJson['object'].replace('/activity', '')
|
||||
messageId = messageId.replace('/undo', '')
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
removeModerationPostFromIndex(baseDir, messageId, debug)
|
||||
postFilename = locatePost(baseDir, handle.split('@')[0],
|
||||
handle.split('@')[1], messageId)
|
||||
|
|
@ -1532,6 +1558,28 @@ def receiveUndoAnnounce(recentPostsCache: {},
|
|||
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,
|
||||
messageJson: {}, maxReplies: int, debug: bool) -> bool:
|
||||
"""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:
|
||||
print('DEBUG: post may have expired - ' + replyTo)
|
||||
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
|
||||
postRepliesFilename = postFilename.replace('.json', '.replies')
|
||||
messageId = messageJson['id'].replace('/activity', '')
|
||||
messageId = messageId.replace('/undo', '')
|
||||
messageId = removeIdEnding(messageJson['id'])
|
||||
if os.path.isfile(postRepliesFilename):
|
||||
numLines = sum(1 for line in open(postRepliesFilename))
|
||||
if numLines > maxReplies:
|
||||
return False
|
||||
if messageId not in open(postRepliesFilename).read():
|
||||
repliesFile = open(postRepliesFilename, "a")
|
||||
repliesFile = open(postRepliesFilename, 'a+')
|
||||
repliesFile.write(messageId + '\n')
|
||||
repliesFile.close()
|
||||
else:
|
||||
|
|
@ -1624,6 +1675,15 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
|
|||
if 'Z' not in messageJson['object']['published']:
|
||||
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,
|
||||
messageJson['object']['type'],
|
||||
messageJson['object']['summary'],
|
||||
|
|
@ -1667,6 +1727,16 @@ def validPostContent(baseDir: str, nickname: str, domain: str,
|
|||
messageJson['object']['content']):
|
||||
print('REJECT: content filtered')
|
||||
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')
|
||||
return True
|
||||
|
||||
|
|
@ -1778,8 +1848,12 @@ def likeNotify(baseDir: str, domain: str, onionDomain: str,
|
|||
return
|
||||
|
||||
accountDir = baseDir + '/accounts/' + handle
|
||||
if not os.path.isdir(accountDir):
|
||||
|
||||
# are like notifications enabled?
|
||||
notifyLikesEnabledFilename = accountDir + '/.notifyLikes'
|
||||
if not os.path.isfile(notifyLikesEnabledFilename):
|
||||
return
|
||||
|
||||
likeFile = accountDir + '/.newLike'
|
||||
if os.path.isfile(likeFile):
|
||||
if '##sent##' not in open(likeFile).read():
|
||||
|
|
@ -1981,8 +2055,7 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None:
|
|||
actorNickname, actorDomain):
|
||||
return
|
||||
|
||||
postId = \
|
||||
postJsonObject['id'].replace('/activity', '').replace('/', '#')
|
||||
postId = removeIdEnding(postJsonObject['id']).replace('/', '#')
|
||||
|
||||
# look for events within the tags list
|
||||
for tagDict in postJsonObject['object']['tag']:
|
||||
|
|
@ -1992,7 +2065,7 @@ def inboxUpdateCalendar(baseDir: str, handle: str, postJsonObject: {}) -> None:
|
|||
continue
|
||||
if not tagDict.get('startTime'):
|
||||
continue
|
||||
saveEvent(baseDir, handle, postId, tagDict)
|
||||
saveEventPost(baseDir, handle, postId, tagDict)
|
||||
|
||||
|
||||
def inboxUpdateIndex(boxname: str, baseDir: str, handle: str,
|
||||
|
|
@ -2171,12 +2244,18 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if validPostContent(baseDir, nickname, domain,
|
||||
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
|
||||
if isinstance(postJsonObject['object'], dict):
|
||||
if postJsonObject['object'].get('content') and \
|
||||
postJsonObject['object'].get('summary') and \
|
||||
postJsonObject['object'].get('attributedTo'):
|
||||
attributedTo = postJsonObject['object']['attributedTo']
|
||||
if jsonObj:
|
||||
if jsonObj.get('content') and \
|
||||
jsonObj.get('summary') and \
|
||||
jsonObj.get('attributedTo'):
|
||||
attributedTo = jsonObj['attributedTo']
|
||||
if isinstance(attributedTo, str):
|
||||
fromNickname = getNicknameFromActor(attributedTo)
|
||||
fromDomain, fromPort = getDomainFromActor(attributedTo)
|
||||
|
|
@ -2184,17 +2263,17 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if fromPort != 80 and fromPort != 443:
|
||||
fromDomain += ':' + str(fromPort)
|
||||
if receiveGitPatch(baseDir, nickname, domain,
|
||||
postJsonObject['object']['type'],
|
||||
postJsonObject['object']['summary'],
|
||||
postJsonObject['object']['content'],
|
||||
jsonObj['type'],
|
||||
jsonObj['summary'],
|
||||
jsonObj['content'],
|
||||
fromNickname, fromDomain):
|
||||
gitPatchNotify(baseDir, handle,
|
||||
postJsonObject['object']['summary'],
|
||||
postJsonObject['object']['content'],
|
||||
jsonObj['summary'],
|
||||
jsonObj['content'],
|
||||
fromNickname, fromDomain)
|
||||
elif '[PATCH]' in postJsonObject['object']['content']:
|
||||
elif '[PATCH]' in jsonObj['content']:
|
||||
print('WARN: git patch not accepted - ' +
|
||||
postJsonObject['object']['summary'])
|
||||
jsonObj['summary'])
|
||||
return False
|
||||
|
||||
# replace YouTube links, so they get less tracking data
|
||||
|
|
@ -2224,6 +2303,8 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
postJsonObject, debug,
|
||||
__version__)
|
||||
|
||||
isReplyToMutedPost = False
|
||||
|
||||
if not isGroup:
|
||||
# create a DM notification file if needed
|
||||
postIsDM = isDM(postJsonObject)
|
||||
|
|
@ -2274,9 +2355,13 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if nickname != 'inbox':
|
||||
# replies index will be updated
|
||||
updateIndexList.append('tlreplies')
|
||||
if not isMuted(baseDir, nickname, domain,
|
||||
postJsonObject['object']['inReplyTo']):
|
||||
replyNotify(baseDir, handle,
|
||||
httpPrefix + '://' + domain +
|
||||
'/users/' + nickname + '/tlreplies')
|
||||
else:
|
||||
isReplyToMutedPost = True
|
||||
|
||||
if isImageMedia(session, baseDir, httpPrefix,
|
||||
nickname, domain, postJsonObject,
|
||||
|
|
@ -2286,6 +2371,9 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if isBlogPost(postJsonObject):
|
||||
# blogs index will be updated
|
||||
updateIndexList.append('tlblogs')
|
||||
elif isEventPost(postJsonObject):
|
||||
# events index will be updated
|
||||
updateIndexList.append('tlevents')
|
||||
|
||||
# get the avatar for a reply/announce
|
||||
obtainAvatarForReplyPost(session, baseDir,
|
||||
|
|
@ -2294,32 +2382,49 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
|
|||
|
||||
# save the post to file
|
||||
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
|
||||
for boxname in updateIndexList:
|
||||
if not inboxUpdateIndex(boxname, baseDir, handle,
|
||||
destinationFilename, debug):
|
||||
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)
|
||||
|
||||
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
|
||||
if isGroup:
|
||||
sendToGroupMembers(session, baseDir, handle, port,
|
||||
|
|
@ -2594,6 +2699,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if accountMaxPostsPerDay > 0 or domainMaxPostsPerDay > 0:
|
||||
pprint(quotasDaily)
|
||||
|
||||
if queueJson.get('actor'):
|
||||
print('Obtaining public key for actor ' + queueJson['actor'])
|
||||
|
||||
# Try a few times to obtain the public key
|
||||
|
|
@ -2716,6 +2822,23 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
|
|||
queue.pop(0)
|
||||
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,
|
||||
baseDir, httpPrefix,
|
||||
domain, port,
|
||||
|
|
|
|||
5
like.py
|
|
@ -6,6 +6,7 @@ __maintainer__ = "Bob Mottram"
|
|||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
from utils import removeIdEnding
|
||||
from utils import urlPermitted
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
|
|
@ -411,7 +412,7 @@ def outboxLike(recentPostsCache: {},
|
|||
if debug:
|
||||
print('DEBUG: c2s like request arrived in outbox')
|
||||
|
||||
messageId = messageJson['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object'])
|
||||
if ':' in domain:
|
||||
domain = domain.split(':')[0]
|
||||
postFilename = locatePost(baseDir, nickname, domain, messageId)
|
||||
|
|
@ -462,7 +463,7 @@ def outboxUndoLike(recentPostsCache: {},
|
|||
if debug:
|
||||
print('DEBUG: c2s undo like request arrived in outbox')
|
||||
|
||||
messageId = messageJson['object']['object'].replace('/activity', '')
|
||||
messageId = removeIdEnding(messageJson['object']['object'])
|
||||
if ':' in domain:
|
||||
domain = domain.split(':')[0]
|
||||
postFilename = locatePost(baseDir, nickname, domain, messageId)
|
||||
|
|
|
|||
21
outbox.py
|
|
@ -13,6 +13,7 @@ from posts import outboxMessageCreateWrap
|
|||
from posts import savePostToBox
|
||||
from posts import sendToFollowersThread
|
||||
from posts import sendToNamedAddresses
|
||||
from utils import removeIdEnding
|
||||
from utils import getDomainFromActor
|
||||
from blocking import isBlockedDomain
|
||||
from blocking import outboxBlock
|
||||
|
|
@ -152,15 +153,14 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
|
||||
permittedOutboxTypes = ('Create', 'Announce', 'Like', 'Follow', 'Undo',
|
||||
'Update', 'Add', 'Remove', 'Block', 'Delete',
|
||||
'Delegate', 'Skill', 'Bookmark')
|
||||
'Delegate', 'Skill', 'Bookmark', 'Event')
|
||||
if messageJson['type'] not in permittedOutboxTypes:
|
||||
if debug:
|
||||
print('DEBUG: POST to outbox - ' + messageJson['type'] +
|
||||
' is not a permitted activity type')
|
||||
return False
|
||||
if messageJson.get('id'):
|
||||
postId = \
|
||||
messageJson['id'].replace('/activity', '').replace('/undo', '')
|
||||
postId = removeIdEnding(messageJson['id'])
|
||||
if debug:
|
||||
print('DEBUG: id attribute exists within POST to outbox')
|
||||
else:
|
||||
|
|
@ -172,13 +172,15 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
if messageJson['type'] != 'Upgrade':
|
||||
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.get('object'):
|
||||
if isinstance(messageJson['object'], dict):
|
||||
if messageJson['object'].get('type'):
|
||||
if messageJson['object']['type'] == 'Article':
|
||||
outboxName = 'tlblogs'
|
||||
elif messageJson['object']['type'] == 'Event':
|
||||
outboxName = 'tlevents'
|
||||
|
||||
savedFilename = \
|
||||
savePostToBox(baseDir,
|
||||
|
|
@ -186,20 +188,25 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
|
|||
postId,
|
||||
postToNickname,
|
||||
domainFull, messageJson, outboxName)
|
||||
if not savedFilename:
|
||||
print('WARN: post not saved to outbox ' + outboxName)
|
||||
return False
|
||||
if messageJson['type'] == 'Create' or \
|
||||
messageJson['type'] == 'Question' or \
|
||||
messageJson['type'] == 'Note' or \
|
||||
messageJson['type'] == 'EncryptedMessage' or \
|
||||
messageJson['type'] == 'Article' or \
|
||||
messageJson['type'] == 'Event' or \
|
||||
messageJson['type'] == 'Patch' or \
|
||||
messageJson['type'] == 'Announce':
|
||||
indexes = [outboxName, "inbox"]
|
||||
selfActor = \
|
||||
httpPrefix + '://' + domainFull + '/users/' + postToNickname
|
||||
for boxNameIndex in indexes:
|
||||
if not boxNameIndex:
|
||||
continue
|
||||
if boxNameIndex == 'inbox' and outboxName == 'tlblogs':
|
||||
continue
|
||||
selfActor = \
|
||||
httpPrefix + '://' + domainFull + \
|
||||
'/users/' + postToNickname
|
||||
# avoid duplicates of the message if already going
|
||||
# back to the inbox of the same account
|
||||
if selfActor not in messageJson['to']:
|
||||
|
|
|
|||
22
person.py
|
|
@ -25,6 +25,7 @@ from posts import createRepliesTimeline
|
|||
from posts import createMediaTimeline
|
||||
from posts import createBlogsTimeline
|
||||
from posts import createBookmarksTimeline
|
||||
from posts import createEventsTimeline
|
||||
from posts import createInbox
|
||||
from posts import createOutbox
|
||||
from posts import createModeration
|
||||
|
|
@ -459,6 +460,12 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int,
|
|||
with open(followDMsFilename, "w") as fFile:
|
||||
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'):
|
||||
os.mkdir(baseDir + '/accounts')
|
||||
if not os.path.isdir(baseDir + '/accounts/' + nickname + '@' + domain):
|
||||
|
|
@ -598,7 +605,8 @@ def personBoxJson(recentPostsCache: {},
|
|||
boxname != 'tlreplies' and boxname != 'tlmedia' and \
|
||||
boxname != 'tlblogs' and \
|
||||
boxname != 'outbox' and boxname != 'moderation' and \
|
||||
boxname != 'tlbookmarks' and boxname != 'bookmarks':
|
||||
boxname != 'tlbookmarks' and boxname != 'bookmarks' and \
|
||||
boxname != 'tlevents':
|
||||
return None
|
||||
|
||||
if not '/' + boxname in path:
|
||||
|
|
@ -638,7 +646,8 @@ def personBoxJson(recentPostsCache: {},
|
|||
httpPrefix,
|
||||
noOfItems, headerOnly, ocapAlways, pageNumber)
|
||||
elif boxname == 'dm':
|
||||
return createDMTimeline(session, baseDir, nickname, domain, port,
|
||||
return createDMTimeline(recentPostsCache,
|
||||
session, baseDir, nickname, domain, port,
|
||||
httpPrefix,
|
||||
noOfItems, headerOnly, ocapAlways, pageNumber)
|
||||
elif boxname == 'tlbookmarks' or boxname == 'bookmarks':
|
||||
|
|
@ -646,8 +655,15 @@ def personBoxJson(recentPostsCache: {},
|
|||
port, httpPrefix,
|
||||
noOfItems, headerOnly, ocapAlways,
|
||||
pageNumber)
|
||||
elif boxname == 'tlevents':
|
||||
return createEventsTimeline(recentPostsCache,
|
||||
session, baseDir, nickname, domain,
|
||||
port, httpPrefix,
|
||||
noOfItems, headerOnly, ocapAlways,
|
||||
pageNumber)
|
||||
elif boxname == 'tlreplies':
|
||||
return createRepliesTimeline(session, baseDir, nickname, domain,
|
||||
return createRepliesTimeline(recentPostsCache,
|
||||
session, baseDir, nickname, domain,
|
||||
port, httpPrefix,
|
||||
noOfItems, headerOnly, ocapAlways,
|
||||
pageNumber)
|
||||
|
|
|
|||
299
posts.py
|
|
@ -13,6 +13,7 @@ import os
|
|||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from socket import error as SocketError
|
||||
from time import gmtime, strftime
|
||||
from collections import OrderedDict
|
||||
|
|
@ -28,6 +29,7 @@ from session import postJsonString
|
|||
from session import postImage
|
||||
from webfinger import webfingerHandle
|
||||
from httpsig import createSignedHeader
|
||||
from utils import removeIdEnding
|
||||
from utils import siteIsActive
|
||||
from utils import removePostFromCache
|
||||
from utils import getCachedPostFilename
|
||||
|
|
@ -45,6 +47,7 @@ from capabilities import getOcapFilename
|
|||
from capabilities import capabilitiesUpdate
|
||||
from media import attachMedia
|
||||
from media import replaceYouTube
|
||||
from content import removeHtml
|
||||
from content import removeLongWords
|
||||
from content import addHtmlTags
|
||||
from content import replaceEmojiFromTags
|
||||
|
|
@ -501,7 +504,8 @@ def deleteAllPosts(baseDir: str,
|
|||
nickname: str, domain: str, boxname: str) -> None:
|
||||
"""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
|
||||
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
|
||||
for deleteFilename in os.scandir(boxDir):
|
||||
|
|
@ -523,7 +527,8 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str,
|
|||
Returns the filename
|
||||
"""
|
||||
if boxname != 'inbox' and boxname != 'outbox' and \
|
||||
boxname != 'tlblogs' and boxname != 'scheduled':
|
||||
boxname != 'tlblogs' and boxname != 'tlevents' and \
|
||||
boxname != 'scheduled':
|
||||
return None
|
||||
originalDomain = domain
|
||||
if ':' in domain:
|
||||
|
|
@ -606,15 +611,78 @@ def addSchedulePost(baseDir: str, nickname: str, domain: str,
|
|||
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,
|
||||
toUrl: str, ccUrl: str, httpPrefix: str, content: str,
|
||||
followersOnly: bool, saveToFile: bool, clientToServer: bool,
|
||||
commentsEnabled: bool,
|
||||
attachImageFilename: str,
|
||||
mediaType: str, imageDescription: str,
|
||||
useBlurhash: bool, isModerationReport: bool,
|
||||
isArticle: bool, inReplyTo=None,
|
||||
isArticle: bool,
|
||||
inReplyTo=None,
|
||||
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
|
||||
"""
|
||||
mentionedRecipients = \
|
||||
|
|
@ -657,7 +725,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
sensitive = False
|
||||
summary = None
|
||||
if subject:
|
||||
summary = subject
|
||||
summary = validContentWarning(subject)
|
||||
sensitive = True
|
||||
|
||||
toRecipients = []
|
||||
|
|
@ -703,6 +771,24 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
sensitive = True
|
||||
if replyToJson['object'].get('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
|
||||
if eventDate:
|
||||
eventName = summary
|
||||
|
|
@ -717,15 +803,17 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
':00' + strftime("%z", gmtime())
|
||||
else:
|
||||
eventDateStr = eventDate + 'T12:00:00Z'
|
||||
if not schedulePost:
|
||||
if not endDateStr:
|
||||
endDateStr = eventDateStr
|
||||
if not schedulePost and not eventUUID:
|
||||
tags.append({
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Event",
|
||||
"name": eventName,
|
||||
"startTime": eventDateStr,
|
||||
"endTime": eventDateStr
|
||||
"endTime": endDateStr
|
||||
})
|
||||
if location:
|
||||
if location and not eventUUID:
|
||||
tags.append({
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Place",
|
||||
|
|
@ -755,6 +843,11 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
for ccRemoval in removeFromCC:
|
||||
toCC.remove(ccRemoval)
|
||||
|
||||
# the type of post to be made
|
||||
postObjectType = 'Note'
|
||||
if eventUUID:
|
||||
postObjectType = 'Event'
|
||||
|
||||
if not clientToServer:
|
||||
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
|
||||
|
||||
|
|
@ -783,7 +876,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
'cc': toCC,
|
||||
'object': {
|
||||
'id': newPostId,
|
||||
'type': 'Note',
|
||||
'type': postObjectType,
|
||||
'summary': summary,
|
||||
'inReplyTo': inReplyTo,
|
||||
'published': published,
|
||||
|
|
@ -794,6 +887,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
'sensitive': sensitive,
|
||||
'atomUri': newPostId,
|
||||
'inReplyToAtomUri': inReplyToAtomUri,
|
||||
'commentsEnabled': commentsEnabled,
|
||||
'mediaType': 'text/html',
|
||||
'content': content,
|
||||
'contentMap': {
|
||||
|
|
@ -817,6 +911,13 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
attachMedia(baseDir, httpPrefix, domain, port,
|
||||
newPost['object'], attachImageFilename,
|
||||
mediaType, imageDescription, useBlurhash)
|
||||
appendEventFields(newPost['object'], eventUUID, eventStatus,
|
||||
anonymousParticipationEnabled,
|
||||
repliesModerationOption,
|
||||
category, joinMode,
|
||||
eventDateStr, endDateStr,
|
||||
location, maximumAttendeeCapacity,
|
||||
ticketUrl, subject)
|
||||
else:
|
||||
idStr = \
|
||||
httpPrefix + '://' + domain + '/users/' + nickname + \
|
||||
|
|
@ -824,7 +925,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
newPost = {
|
||||
"@context": postContext,
|
||||
'id': newPostId,
|
||||
'type': 'Note',
|
||||
'type': postObjectType,
|
||||
'summary': summary,
|
||||
'inReplyTo': inReplyTo,
|
||||
'published': published,
|
||||
|
|
@ -835,6 +936,7 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
'sensitive': sensitive,
|
||||
'atomUri': newPostId,
|
||||
'inReplyToAtomUri': inReplyToAtomUri,
|
||||
'commentsEnabled': commentsEnabled,
|
||||
'mediaType': 'text/html',
|
||||
'content': content,
|
||||
'contentMap': {
|
||||
|
|
@ -857,6 +959,13 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
attachMedia(baseDir, httpPrefix, domain, port,
|
||||
newPost, attachImageFilename,
|
||||
mediaType, imageDescription, useBlurhash)
|
||||
appendEventFields(newPost, eventUUID, eventStatus,
|
||||
anonymousParticipationEnabled,
|
||||
repliesModerationOption,
|
||||
category, joinMode,
|
||||
eventDateStr, endDateStr,
|
||||
location, maximumAttendeeCapacity,
|
||||
ticketUrl, subject)
|
||||
if ccUrl:
|
||||
if len(ccUrl) > 0:
|
||||
newPost['cc'] = [ccUrl]
|
||||
|
|
@ -892,12 +1001,15 @@ def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
|
|||
'date and time values')
|
||||
return newPost
|
||||
elif saveToFile:
|
||||
if not isArticle:
|
||||
savePostToBox(baseDir, httpPrefix, newPostId,
|
||||
nickname, domain, newPost, 'outbox')
|
||||
else:
|
||||
if isArticle:
|
||||
savePostToBox(baseDir, httpPrefix, newPostId,
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -1006,7 +1118,7 @@ def postIsAddressedToPublic(baseDir: str, postJsonObject: {}) -> bool:
|
|||
def createPublicPost(baseDir: str,
|
||||
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||
content: str, followersOnly: bool, saveToFile: bool,
|
||||
clientToServer: bool,
|
||||
clientToServer: bool, commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
||||
|
|
@ -1024,11 +1136,13 @@ def createPublicPost(baseDir: str,
|
|||
httpPrefix + '://' + domainFull + '/users/' +
|
||||
nickname + '/followers',
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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,
|
||||
|
|
@ -1058,7 +1172,7 @@ def createQuestionPost(baseDir: str,
|
|||
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||
content: str, qOptions: [],
|
||||
followersOnly: bool, saveToFile: bool,
|
||||
clientToServer: bool,
|
||||
clientToServer: bool, commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
subject: str, durationDays: int) -> {}:
|
||||
|
|
@ -1075,11 +1189,13 @@ def createQuestionPost(baseDir: str,
|
|||
httpPrefix + '://' + domainFull + '/users/' +
|
||||
nickname + '/followers',
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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']['oneOf'] = []
|
||||
messageJson['object']['votersCount'] = 0
|
||||
|
|
@ -1104,7 +1220,7 @@ def createQuestionPost(baseDir: str,
|
|||
def createUnlistedPost(baseDir: str,
|
||||
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||
content: str, followersOnly: bool, saveToFile: bool,
|
||||
clientToServer: bool,
|
||||
clientToServer: bool, commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
inReplyTo=None, inReplyToAtomUri=None, subject=None,
|
||||
|
|
@ -1122,11 +1238,13 @@ def createUnlistedPost(baseDir: str,
|
|||
nickname + '/followers',
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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,
|
||||
|
|
@ -1134,7 +1252,7 @@ def createFollowersOnlyPost(baseDir: str,
|
|||
httpPrefix: str,
|
||||
content: str, followersOnly: bool,
|
||||
saveToFile: bool,
|
||||
clientToServer: bool,
|
||||
clientToServer: bool, commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
inReplyTo=None, inReplyToAtomUri=None,
|
||||
|
|
@ -1153,11 +1271,67 @@ def createFollowersOnlyPost(baseDir: str,
|
|||
nickname + '/followers',
|
||||
None,
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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,
|
||||
|
|
@ -1200,6 +1374,7 @@ def createDirectMessagePost(baseDir: str,
|
|||
httpPrefix: str,
|
||||
content: str, followersOnly: bool,
|
||||
saveToFile: bool, clientToServer: bool,
|
||||
commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
inReplyTo=None, inReplyToAtomUri=None,
|
||||
|
|
@ -1222,11 +1397,13 @@ def createDirectMessagePost(baseDir: str,
|
|||
createPostBase(baseDir, nickname, domain, port,
|
||||
postTo, postCc,
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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
|
||||
messageJson['to'] = messageJson['object']['cc']
|
||||
messageJson['object']['to'] = messageJson['to']
|
||||
|
|
@ -1241,7 +1418,7 @@ def createDirectMessagePost(baseDir: str,
|
|||
def createReportPost(baseDir: str,
|
||||
nickname: str, domain: str, port: int, httpPrefix: str,
|
||||
content: str, followersOnly: bool, saveToFile: bool,
|
||||
clientToServer: bool,
|
||||
clientToServer: bool, commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
debug: bool, subject=None) -> {}:
|
||||
|
|
@ -1314,17 +1491,20 @@ def createReportPost(baseDir: str,
|
|||
createPostBase(baseDir, nickname, domain, port,
|
||||
toUrl, postCc,
|
||||
httpPrefix, content, followersOnly, saveToFile,
|
||||
clientToServer,
|
||||
clientToServer, commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
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:
|
||||
continue
|
||||
|
||||
# update the inbox index with the report filename
|
||||
# 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():
|
||||
# try:
|
||||
# with open(indexFilename, 'a+') as fp:
|
||||
|
|
@ -1402,6 +1582,7 @@ def sendPost(projectVersion: str,
|
|||
toNickname: str, toDomain: str, toPort: int, cc: str,
|
||||
httpPrefix: str, content: str, followersOnly: bool,
|
||||
saveToFile: bool, clientToServer: bool,
|
||||
commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
federationList: [], sendThreads: [], postLog: [],
|
||||
|
|
@ -1470,11 +1651,14 @@ def sendPost(projectVersion: str,
|
|||
createPostBase(baseDir, nickname, domain, port,
|
||||
toPersonId, cc, httpPrefix, content,
|
||||
followersOnly, saveToFile, clientToServer,
|
||||
commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
False, isArticle, inReplyTo,
|
||||
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
|
||||
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private')
|
||||
|
|
@ -1528,6 +1712,7 @@ def sendPostViaServer(projectVersion: str,
|
|||
fromDomain: str, fromPort: int,
|
||||
toNickname: str, toDomain: str, toPort: int, cc: str,
|
||||
httpPrefix: str, content: str, followersOnly: bool,
|
||||
commentsEnabled: bool,
|
||||
attachImageFilename: str, mediaType: str,
|
||||
imageDescription: str, useBlurhash: bool,
|
||||
cachedWebfingers: {}, personCache: {},
|
||||
|
|
@ -1614,11 +1799,14 @@ def sendPostViaServer(projectVersion: str,
|
|||
fromNickname, fromDomain, fromPort,
|
||||
toPersonId, cc, httpPrefix, content,
|
||||
followersOnly, saveToFile, clientToServer,
|
||||
commentsEnabled,
|
||||
attachImageFilename, mediaType,
|
||||
imageDescription, useBlurhash,
|
||||
False, isArticle, inReplyTo,
|
||||
inReplyToAtomUri, subject,
|
||||
False, None, None, None)
|
||||
False, None, None, None, None, None,
|
||||
None, None, None,
|
||||
None, None, None, None, None)
|
||||
|
||||
authHeader = createBasicAuthHeader(fromNickname, password)
|
||||
|
||||
|
|
@ -2261,20 +2449,34 @@ def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
|
|||
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,
|
||||
headerOnly: bool, ocapAlways: bool,
|
||||
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,
|
||||
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,
|
||||
headerOnly: bool, ocapAlways: bool,
|
||||
pageNumber=None) -> {}:
|
||||
return createBoxIndexed({}, session, baseDir, 'tlreplies',
|
||||
return createBoxIndexed(recentPostsCache, session, baseDir, 'tlreplies',
|
||||
nickname, domain, port, httpPrefix,
|
||||
itemsPerPage, headerOnly, True,
|
||||
ocapAlways, pageNumber)
|
||||
|
|
@ -2441,6 +2643,7 @@ def isImageMedia(session, baseDir: str, httpPrefix: str,
|
|||
if postJsonObject['object'].get('moderationStatus'):
|
||||
return False
|
||||
if postJsonObject['object']['type'] != 'Note' and \
|
||||
postJsonObject['object']['type'] != 'Event' and \
|
||||
postJsonObject['object']['type'] != 'Article':
|
||||
return False
|
||||
if not postJsonObject['object'].get('attachment'):
|
||||
|
|
@ -2581,6 +2784,7 @@ def addPostStringToTimeline(postStr: str, boxname: str,
|
|||
# must be a recognized ActivityPub type
|
||||
if ('"Note"' in postStr or
|
||||
'"EncryptedMessage"' in postStr or
|
||||
'"Event"' in postStr or
|
||||
'"Article"' in postStr or
|
||||
'"Patch"' in postStr or
|
||||
'"Announce"' in postStr or
|
||||
|
|
@ -2632,10 +2836,12 @@ def createBoxIndexed(recentPostsCache: {},
|
|||
boxname != 'tlreplies' and boxname != 'tlmedia' and \
|
||||
boxname != 'tlblogs' and \
|
||||
boxname != 'outbox' and boxname != 'tlbookmarks' and \
|
||||
boxname != 'bookmarks':
|
||||
boxname != 'bookmarks' and \
|
||||
boxname != 'tlevents':
|
||||
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
|
||||
if boxname == "tlbookmarks":
|
||||
boxname = "bookmarks"
|
||||
|
|
@ -3303,6 +3509,17 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str,
|
|||
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,
|
||||
recentPostsCache: {}) -> None:
|
||||
""" 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 recentPostsCache.get('index'):
|
||||
postId = \
|
||||
postJsonObject['id'].replace('/activity', '').replace('/', '#')
|
||||
removeIdEnding(postJsonObject['id']).replace('/', '#')
|
||||
if postId in recentPostsCache['index']:
|
||||
print('MUTE: ' + postId + ' is in recent posts cache')
|
||||
if recentPostsCache['json'].get(postId):
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -20,6 +20,7 @@ from cache import getPersonFromCache
|
|||
from threads import threadWithTrace
|
||||
from daemon import runDaemon
|
||||
from session import createSession
|
||||
from posts import validContentWarning
|
||||
from posts import deleteAllPosts
|
||||
from posts import createPublicPost
|
||||
from posts import sendPost
|
||||
|
|
@ -31,6 +32,7 @@ from follow import clearFollows
|
|||
from follow import clearFollowers
|
||||
from follow import sendFollowRequestViaServer
|
||||
from follow import sendUnfollowRequestViaServer
|
||||
from utils import removeIdEnding
|
||||
from utils import siteIsActive
|
||||
from utils import updateRecentPostsCache
|
||||
from utils import followPerson
|
||||
|
|
@ -62,6 +64,7 @@ from announce import sendAnnounceViaServer
|
|||
from media import getMediaPath
|
||||
from media import getAttachmentMediaType
|
||||
from delete import sendDeleteViaServer
|
||||
from inbox import jsonPostAllowsComments
|
||||
from inbox import validInbox
|
||||
from inbox import validInboxFilenames
|
||||
from content import htmlReplaceQuoteMarks
|
||||
|
|
@ -270,14 +273,16 @@ def createServerAlice(path: str, domain: str, port: int,
|
|||
clientToServer = False
|
||||
createPublicPost(path, nickname, domain, port, httpPrefix,
|
||||
"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,
|
||||
"Curiouser and curiouser!", False, True,
|
||||
clientToServer, None, None, useBlurhash)
|
||||
clientToServer, True, None, None, useBlurhash)
|
||||
createPublicPost(path, nickname, domain, port, httpPrefix,
|
||||
"In the gardens of memory, in the palace " +
|
||||
"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
|
||||
testServerAliceRunning = True
|
||||
maxMentions = 10
|
||||
|
|
@ -335,14 +340,17 @@ def createServerBob(path: str, domain: str, port: int,
|
|||
if hasPosts:
|
||||
createPublicPost(path, nickname, domain, port, httpPrefix,
|
||||
"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,
|
||||
"One of the things I've realised is that " +
|
||||
"I am very simple",
|
||||
False, True, clientToServer, None, None, useBlurhash)
|
||||
False, True, clientToServer, True,
|
||||
None, None, useBlurhash)
|
||||
createPublicPost(path, nickname, domain, port, httpPrefix,
|
||||
"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
|
||||
testServerBobRunning = True
|
||||
maxMentions = 10
|
||||
|
|
@ -503,7 +511,8 @@ def testPostMessageBetweenServers():
|
|||
'Why is a mouse when it spins? ' +
|
||||
'यह एक परीक्षण है #sillyquestion',
|
||||
followersOnly,
|
||||
saveToFile, clientToServer, attachedImageFilename, mediaType,
|
||||
saveToFile, clientToServer, True,
|
||||
attachedImageFilename, mediaType,
|
||||
attachedImageDescription, useBlurhash, federationList,
|
||||
aliceSendThreads, alicePostLog, aliceCachedWebfingers,
|
||||
alicePersonCache, isArticle, inReplyTo,
|
||||
|
|
@ -788,7 +797,8 @@ def testFollowBetweenServers():
|
|||
sessionAlice, aliceDir, 'alice', aliceDomain, alicePort,
|
||||
'bob', bobDomain, bobPort, ccUrl,
|
||||
httpPrefix, 'Alice message', followersOnly, saveToFile,
|
||||
clientToServer, None, None, None, useBlurhash, federationList,
|
||||
clientToServer, True,
|
||||
None, None, None, useBlurhash, federationList,
|
||||
aliceSendThreads, alicePostLog, aliceCachedWebfingers,
|
||||
alicePersonCache, isArticle, inReplyTo,
|
||||
inReplyToAtomUri, subject)
|
||||
|
|
@ -1092,7 +1102,7 @@ def testCreatePerson():
|
|||
archivePostsForPerson(nickname, domain, baseDir, 'outbox', None, {}, 4)
|
||||
createPublicPost(baseDir, nickname, domain, port, httpPrefix,
|
||||
"G'day world!", False, True, clientToServer,
|
||||
None, None, useBlurhash, None, None,
|
||||
True, None, None, useBlurhash, None, None,
|
||||
'Not suitable for Vogons')
|
||||
|
||||
os.chdir(currDir)
|
||||
|
|
@ -1315,7 +1325,7 @@ def testClientToServer():
|
|||
aliceDomain, alicePort,
|
||||
'bob', bobDomain, bobPort, None,
|
||||
httpPrefix, 'Sent from my ActivityPub client',
|
||||
followersOnly,
|
||||
followersOnly, True,
|
||||
attachedImageFilename, mediaType,
|
||||
attachedImageDescription, useBlurhash,
|
||||
cachedWebfingers, personCache, isArticle,
|
||||
|
|
@ -1356,7 +1366,7 @@ def testClientToServer():
|
|||
outboxPostFilename = outboxPath + '/' + name
|
||||
postJsonObject = loadJson(outboxPostFilename, 0)
|
||||
if postJsonObject:
|
||||
outboxPostId = postJsonObject['id'].replace('/activity', '')
|
||||
outboxPostId = removeIdEnding(postJsonObject['id'])
|
||||
assert outboxPostId
|
||||
print('message id obtained: ' + outboxPostId)
|
||||
assert validInbox(bobDir, 'bob', bobDomain)
|
||||
|
|
@ -1974,8 +1984,106 @@ def runHtmlReplaceQuoteMarks():
|
|||
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():
|
||||
print('Running tests...')
|
||||
testTranslations()
|
||||
testValidContentWarning()
|
||||
testRemoveIdEnding()
|
||||
testJsonPostAllowsComments()
|
||||
runHtmlReplaceQuoteMarks()
|
||||
testDangerousMarkup()
|
||||
testRemoveHtml()
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "نال إعجاب",
|
||||
"Solidaric": "تضامن",
|
||||
"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": "يخطر عندما يتم اعجاب المشاركات"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "M'agrada",
|
||||
"Solidaric": "Solidaritat",
|
||||
"YouTube Replacement Domain": "Domini de substitució de YouTube",
|
||||
"Notes": "Notes"
|
||||
"Notes": "Notes",
|
||||
"Allow replies.": "Permetre respostes.",
|
||||
"Event": "Esdeveniment",
|
||||
"Event name": "Nom de l’esdeveniment",
|
||||
"Events": "Esdeveniments",
|
||||
"Create an event": "Crea un esdeveniment",
|
||||
"Describe the event": "Descriviu l’esdeveniment",
|
||||
"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 s’hi pot apuntar",
|
||||
"Apply to join": "Sol·liciteu participar",
|
||||
"Invitation only": "Només invitació",
|
||||
"Joining": "Unir-se",
|
||||
"Status of the event": "Estat de l’esdeveniment",
|
||||
"Tentative": "Temptatiu",
|
||||
"Confirmed": "Confirmat",
|
||||
"Cancelled": "Cancel·lat",
|
||||
"Event banner image description": "Descripció de la imatge del banner de l’esdeveniment",
|
||||
"Banner image": "Imatge de pancarta",
|
||||
"Maximum attendees": "Màxim d’assistents",
|
||||
"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 l’esdeveniment",
|
||||
"Notify when posts are liked": "Notifiqueu-ho quan us agradin les publicacions"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Hoffi",
|
||||
"Solidaric": "Undod",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Gefallen von",
|
||||
"Solidaric": "Solidarität",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Liked by",
|
||||
"Solidaric": "Solidaric",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Apreciado por",
|
||||
"Solidaric": "Solidaridad",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Aimé par",
|
||||
"Solidaric": "Solidarité",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Thaitin",
|
||||
"Solidaric": "Dlúthpháirtíocht",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "द्वारा पसंद किया गया",
|
||||
"Solidaric": "एकजुटता",
|
||||
"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": "पोस्ट पसंद आने पर सूचित करें"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Mi è piaciuto",
|
||||
"Solidaric": "Solidarietà",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "好き",
|
||||
"Solidaric": "連帯",
|
||||
"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": "投稿が高く評価されたときに通知する"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,5 +251,32 @@
|
|||
"Liked by": "Liked by",
|
||||
"Solidaric": "Solidaric",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Curtida por",
|
||||
"Solidaric": "Solidariedade",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "Понравилось",
|
||||
"Solidaric": "солидарность",
|
||||
"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": "Уведомлять, когда публикации нравятся"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -255,5 +255,32 @@
|
|||
"Liked by": "喜欢的人",
|
||||
"Solidaric": "团结互助",
|
||||
"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": "通知喜欢的帖子"
|
||||
}
|
||||
|
|
|
|||
78
utils.py
|
|
@ -19,6 +19,20 @@ from calendar import monthrange
|
|||
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() -> []:
|
||||
"""Returns a list of valid prefixes
|
||||
"""
|
||||
|
|
@ -384,13 +398,13 @@ def locatePost(baseDir: str, nickname: str, domain: str,
|
|||
extension = 'replies'
|
||||
|
||||
# if this post in the shared inbox?
|
||||
postUrl = postUrl.replace('/', '#').replace('/activity', '').strip()
|
||||
postUrl = removeIdEnding(postUrl.strip()).replace('/', '#')
|
||||
|
||||
# add the extension
|
||||
postUrl = postUrl + '.' + extension
|
||||
|
||||
# search boxes
|
||||
boxes = ('inbox', 'outbox', 'tlblogs')
|
||||
boxes = ('inbox', 'outbox', 'tlblogs', 'tlevents')
|
||||
accountDir = baseDir + '/accounts/' + nickname + '@' + domain + '/'
|
||||
for boxName in boxes:
|
||||
postFilename = accountDir + boxName + '/' + postUrl
|
||||
|
|
@ -402,7 +416,7 @@ def locatePost(baseDir: str, nickname: str, domain: str,
|
|||
if os.path.isfile(postFilename):
|
||||
return postFilename
|
||||
|
||||
print('WARN: unable to locate ' + nickname + ' ' + postUrl)
|
||||
# print('WARN: unable to locate ' + nickname + ' ' + postUrl)
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -435,7 +449,7 @@ def removeModerationPostFromIndex(baseDir: str, postUrl: str,
|
|||
moderationIndexFile = baseDir + '/accounts/moderation.txt'
|
||||
if not os.path.isfile(moderationIndexFile):
|
||||
return
|
||||
postId = postUrl.replace('/activity', '')
|
||||
postId = removeIdEnding(postUrl)
|
||||
if postId in open(moderationIndexFile).read():
|
||||
with open(moderationIndexFile, "r") as f:
|
||||
lines = f.readlines()
|
||||
|
|
@ -463,7 +477,7 @@ def isReplyToBlogPost(baseDir: str, nickname: str, domain: str,
|
|||
nickname + '@' + domain + '/tlblogs.index'
|
||||
if not os.path.isfile(blogsIndexFilename):
|
||||
return False
|
||||
postId = postJsonObject['object']['inReplyTo'].replace('/activity', '')
|
||||
postId = removeIdEnding(postJsonObject['object']['inReplyTo'])
|
||||
postId = postId.replace('/', '#')
|
||||
if postId in open(blogsIndexFilename).read():
|
||||
return True
|
||||
|
|
@ -494,7 +508,7 @@ def deletePost(baseDir: str, httpPrefix: str,
|
|||
# remove from recent posts cache in memory
|
||||
if recentPostsCache:
|
||||
postId = \
|
||||
postJsonObject['id'].replace('/activity', '').replace('/', '#')
|
||||
removeIdEnding(postJsonObject['id']).replace('/', '#')
|
||||
if recentPostsCache.get('index'):
|
||||
if postId in recentPostsCache['index']:
|
||||
recentPostsCache['index'].remove(postId)
|
||||
|
|
@ -526,7 +540,7 @@ def deletePost(baseDir: str, httpPrefix: str,
|
|||
if isinstance(postJsonObject['object'], dict):
|
||||
if postJsonObject['object'].get('moderationStatus'):
|
||||
if postJsonObject.get('id'):
|
||||
postId = postJsonObject['id'].replace('/activity', '')
|
||||
postId = removeIdEnding(postJsonObject['id'])
|
||||
removeModerationPostFromIndex(baseDir, postId, debug)
|
||||
|
||||
# remove any hashtags index entries
|
||||
|
|
@ -540,8 +554,7 @@ def deletePost(baseDir: str, httpPrefix: str,
|
|||
if postJsonObject['object'].get('id') and \
|
||||
postJsonObject['object'].get('tag'):
|
||||
# get the id of the post
|
||||
postId = \
|
||||
postJsonObject['object']['id'].replace('/activity', '')
|
||||
postId = removeIdEnding(postJsonObject['object']['id'])
|
||||
for tag in postJsonObject['object']['tag']:
|
||||
if tag['type'] != 'Hashtag':
|
||||
continue
|
||||
|
|
@ -600,6 +613,7 @@ def validNickname(domain: str, nickname: str) -> bool:
|
|||
'public', 'followers',
|
||||
'channel', 'capabilities', 'calendar',
|
||||
'tlreplies', 'tlmedia', 'tlblogs',
|
||||
'tlevents',
|
||||
'moderation', 'activity', 'undo',
|
||||
'reply', 'replies', 'question', 'like',
|
||||
'likes', 'users', 'statuses',
|
||||
|
|
@ -710,7 +724,7 @@ def getCachedPostFilename(baseDir: str, nickname: str, domain: str,
|
|||
return None
|
||||
cachedPostFilename = \
|
||||
cachedPostDir + \
|
||||
'/' + postJsonObject['id'].replace('/activity', '').replace('/', '#')
|
||||
'/' + removeIdEnding(postJsonObject['id']).replace('/', '#')
|
||||
cachedPostFilename = cachedPostFilename + '.html'
|
||||
return cachedPostFilename
|
||||
|
||||
|
|
@ -727,7 +741,7 @@ def removePostFromCache(postJsonObject: {}, recentPostsCache: {}):
|
|||
postId = postJsonObject['id']
|
||||
if '#' in postId:
|
||||
postId = postId.split('#', 1)[0]
|
||||
postId = postId.replace('/activity', '').replace('/', '#')
|
||||
postId = removeIdEnding(postId).replace('/', '#')
|
||||
if postId not in recentPostsCache['index']:
|
||||
return
|
||||
|
||||
|
|
@ -747,7 +761,7 @@ def updateRecentPostsCache(recentPostsCache: {}, maxRecentPosts: int,
|
|||
postId = postJsonObject['id']
|
||||
if '#' in postId:
|
||||
postId = postId.split('#', 1)[0]
|
||||
postId = postId.replace('/activity', '').replace('/', '#')
|
||||
postId = removeIdEnding(postId).replace('/', '#')
|
||||
if recentPostsCache.get('index'):
|
||||
if postId in recentPostsCache['index']:
|
||||
return
|
||||
|
|
@ -757,6 +771,7 @@ def updateRecentPostsCache(recentPostsCache: {}, maxRecentPosts: int,
|
|||
recentPostsCache['html'][postId] = htmlStr
|
||||
|
||||
while len(recentPostsCache['html'].items()) > maxRecentPosts:
|
||||
postId = recentPostsCache['index'][0]
|
||||
recentPostsCache['index'].pop(0)
|
||||
del recentPostsCache['json'][postId]
|
||||
del recentPostsCache['html'][postId]
|
||||
|
|
@ -792,6 +807,43 @@ def mergeDicts(dict1: {}, dict2: {}) -> {}:
|
|||
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:
|
||||
"""Is the given post a blog post?
|
||||
"""
|
||||
|
|
@ -1071,7 +1123,7 @@ def updateAnnounceCollection(recentPostsCache: {},
|
|||
return
|
||||
if not isinstance(postJsonObject['object'], dict):
|
||||
return
|
||||
postUrl = postJsonObject['id'].replace('/activity', '') + '/shares'
|
||||
postUrl = removeIdEnding(postJsonObject['id']) + '/shares'
|
||||
if not postJsonObject['object'].get('shares'):
|
||||
if debug:
|
||||
print('DEBUG: Adding initial shares (announcements) to ' +
|
||||
|
|
|
|||
308
webinterface.py
|
|
@ -25,9 +25,11 @@ from ssb import getSSBAddress
|
|||
from tox import getToxAddress
|
||||
from matrix import getMatrixAddress
|
||||
from donate import getDonationUrl
|
||||
from utils import removeIdEnding
|
||||
from utils import getProtocolPrefixes
|
||||
from utils import getFileCaseInsensitive
|
||||
from utils import searchBoxPosts
|
||||
from utils import isEventPost
|
||||
from utils import isBlogPost
|
||||
from utils import updateRecentPostsCache
|
||||
from utils import getNicknameFromActor
|
||||
|
|
@ -1081,6 +1083,7 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
isGroup = ''
|
||||
followDMs = ''
|
||||
removeTwitter = ''
|
||||
notifyLikes = ''
|
||||
mediaInstanceStr = ''
|
||||
displayNickname = nickname
|
||||
bioStr = ''
|
||||
|
|
@ -1128,6 +1131,9 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
if os.path.isfile(baseDir + '/accounts/' +
|
||||
nickname + '@' + domain + '/.removeTwitter'):
|
||||
removeTwitter = 'checked'
|
||||
if os.path.isfile(baseDir + '/accounts/' +
|
||||
nickname + '@' + domain + '/.notifyLikes'):
|
||||
notifyLikes = 'checked'
|
||||
|
||||
mediaInstance = getConfigParam(baseDir, "mediaInstance")
|
||||
if mediaInstance:
|
||||
|
|
@ -1463,6 +1469,10 @@ def htmlEditProfile(translate: {}, baseDir: str, path: str,
|
|||
' <input type="checkbox" class="profilecheckbox" ' + \
|
||||
'name="mediaInstance" ' + mediaInstanceStr + '> ' + \
|
||||
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 += \
|
||||
' <br><b><label class="labels">' + \
|
||||
|
|
@ -1837,6 +1847,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
replyStr = ''
|
||||
|
||||
showPublicOnDropdown = True
|
||||
messageBoxHeight = 400
|
||||
|
||||
if not path.endswith('/newshare'):
|
||||
if not path.endswith('/newreport'):
|
||||
|
|
@ -1920,15 +1931,31 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
pathBase = path.replace('/newreport', '').replace('/newpost', '')
|
||||
pathBase = pathBase.replace('/newblog', '').replace('/newshare', '')
|
||||
pathBase = pathBase.replace('/newunlisted', '')
|
||||
pathBase = pathBase.replace('/newevent', '')
|
||||
pathBase = pathBase.replace('/newreminder', '')
|
||||
pathBase = pathBase.replace('/newfollowers', '').replace('/newdm', '')
|
||||
|
||||
newPostImageSection = ' <div class="container">'
|
||||
if not path.endswith('/newevent'):
|
||||
newPostImageSection += \
|
||||
' <label class="labels">' + translate['Image description'] + \
|
||||
'</label>\n'
|
||||
' <label class="labels">' + \
|
||||
translate['Image description'] + '</label>\n'
|
||||
else:
|
||||
newPostImageSection += \
|
||||
' <label class="labels">' + \
|
||||
translate['Event banner image description'] + '</label>\n'
|
||||
newPostImageSection += \
|
||||
' <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 += \
|
||||
' <input type="file" id="attachpic" name="attachpic"'
|
||||
newPostImageSection += \
|
||||
|
|
@ -1969,6 +1996,12 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
scopeIcon = 'scope_reminder.png'
|
||||
scopeDescription = translate['Reminder']
|
||||
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'):
|
||||
scopeIcon = 'scope_report.png'
|
||||
scopeDescription = translate['Report']
|
||||
|
|
@ -2027,29 +2060,139 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
if endpoint != 'newshare' and \
|
||||
endpoint != 'newreport' and \
|
||||
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 += \
|
||||
'<p><input type="checkbox" class="profilecheckbox" ' + \
|
||||
'name="schedulePost"><label class="labels"> ' + \
|
||||
translate['This is a scheduled post.'] + '</label></p>\n'
|
||||
|
||||
if endpoint != 'newevent':
|
||||
dateAndLocation += \
|
||||
'<p><img loading="lazy" alt="" title="" ' + \
|
||||
'class="emojicalendar" src="/' + \
|
||||
iconsDir + '/calendar.png"/>\n'
|
||||
# select a date and time for this post
|
||||
dateAndLocation += '<label class="labels">' + \
|
||||
translate['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'
|
||||
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 class="container">\n'
|
||||
dateAndLocation += '<br><label class="labels">' + \
|
||||
translate['Location'] + ': </label>\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'
|
||||
|
||||
newPostForm = htmlHeader(cssFilename, newPostCSS)
|
||||
|
|
@ -2093,6 +2236,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
dropdownUnlistedSuffix = '/newunlisted'
|
||||
dropdownFollowersSuffix = '/newfollowers'
|
||||
dropdownDMSuffix = '/newdm'
|
||||
dropdownEventSuffix = '/newevent'
|
||||
dropdownReminderSuffix = '/newreminder'
|
||||
dropdownReportSuffix = '/newreport'
|
||||
if inReplyTo or mentions:
|
||||
|
|
@ -2101,6 +2245,7 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
dropdownUnlistedSuffix = ''
|
||||
dropdownFollowersSuffix = ''
|
||||
dropdownDMSuffix = ''
|
||||
dropdownEventSuffix = ''
|
||||
dropdownReminderSuffix = ''
|
||||
dropdownReportSuffix = ''
|
||||
if inReplyTo:
|
||||
|
|
@ -2176,6 +2321,12 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
iconsDir + '/scope_reminder.png"/><b>' + translate['Reminder'] + \
|
||||
'</b><br>' + translate['Scheduled note to yourself'] + \
|
||||
'</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 += " " \
|
||||
'<a href="' + pathBase + dropdownReportSuffix + \
|
||||
'"><li><img loading="lazy" alt="" title="" src="/' + iconsDir + \
|
||||
|
|
@ -2251,7 +2402,6 @@ def htmlNewPost(mediaInstance: bool, translate: {},
|
|||
|
||||
newPostForm += \
|
||||
' <br><label class="labels">' + placeholderMessage + '</label>'
|
||||
messageBoxHeight = 400
|
||||
if mediaInstance:
|
||||
messageBoxHeight = 200
|
||||
|
||||
|
|
@ -3186,7 +3336,7 @@ def insertQuestion(baseDir: str, translate: {},
|
|||
return content
|
||||
if len(postJsonObject['object']['oneOf']) == 0:
|
||||
return content
|
||||
messageId = postJsonObject['id'].replace('/activity', '')
|
||||
messageId = removeIdEnding(postJsonObject['id'])
|
||||
if '#' in messageId:
|
||||
messageId = messageId.split('#', 1)[0]
|
||||
pageNumberStr = ''
|
||||
|
|
@ -3654,7 +3804,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
avatarPosition = ''
|
||||
messageId = ''
|
||||
if postJsonObject.get('id'):
|
||||
messageId = postJsonObject['id'].replace('/activity', '')
|
||||
messageId = removeIdEnding(postJsonObject['id'])
|
||||
|
||||
messageIdStr = ''
|
||||
if messageId:
|
||||
|
|
@ -3743,7 +3893,7 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
if boxName == 'tlbookmarks' or boxName == 'bookmarks':
|
||||
return ''
|
||||
|
||||
timelinePostBookmark = postJsonObject['id'].replace('/activity', '')
|
||||
timelinePostBookmark = removeIdEnding(postJsonObject['id'])
|
||||
timelinePostBookmark = timelinePostBookmark.replace('://', '-')
|
||||
timelinePostBookmark = timelinePostBookmark.replace('/', '-')
|
||||
|
||||
|
|
@ -3828,7 +3978,13 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
iconsDir + '/dm.png" class="DMicon"/>\n'
|
||||
|
||||
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']
|
||||
if postJsonObject['object'].get('attributedTo'):
|
||||
if isinstance(postJsonObject['object']['attributedTo'], str):
|
||||
|
|
@ -3872,20 +4028,35 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
|
|||
translate['Reply to this post'] + \
|
||||
' |" src="/' + iconsDir + '/reply.png"/></a>\n'
|
||||
|
||||
isEvent = isEventPost(postJsonObject)
|
||||
|
||||
editStr = ''
|
||||
if fullDomain + '/users/' + nickname in postJsonObject['actor']:
|
||||
if isBlogPost(postJsonObject):
|
||||
if '/statuses/' in postJsonObject['object']['id']:
|
||||
if isBlogPost(postJsonObject):
|
||||
blogPostId = postJsonObject['object']['id']
|
||||
editStr += \
|
||||
'<a class="imageAnchor" href="/users/' + nickname + \
|
||||
'/tlblogs?editblogpost=' + \
|
||||
postJsonObject['object']['id'].split('/statuses/')[1] + \
|
||||
blogPostId.split('/statuses/')[1] + \
|
||||
'?actor=' + actorNickname + \
|
||||
'" title="' + translate['Edit blog post'] + '">' + \
|
||||
'<img loading="lazy" title="' + \
|
||||
translate['Edit blog post'] + '" alt="' + \
|
||||
translate['Edit blog post'] + \
|
||||
' |" 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 = ''
|
||||
if not isModerationPost and showRepeatIcon:
|
||||
|
|
@ -4448,7 +4619,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
if boxName == 'tlshares':
|
||||
os.remove(newShareFile)
|
||||
|
||||
# should the Moderation button be highlighted?
|
||||
# should the Moderation/reports button be highlighted?
|
||||
newReport = False
|
||||
newReportFile = accountDir + '/.newReport'
|
||||
if os.path.isfile(newReportFile):
|
||||
|
|
@ -4456,11 +4627,16 @@ def htmlTimeline(defaultTimeline: str,
|
|||
if boxName == 'moderation':
|
||||
os.remove(newReportFile)
|
||||
|
||||
# directory where icons are found
|
||||
# This changes depending upon theme
|
||||
iconsDir = getIconsDir(baseDir)
|
||||
|
||||
# the css filename
|
||||
cssFilename = baseDir + '/epicyon-profile.css'
|
||||
if os.path.isfile(baseDir + '/epicyon.css'):
|
||||
cssFilename = baseDir + '/epicyon.css'
|
||||
|
||||
# filename of the banner shown at the top
|
||||
bannerFile = 'banner.png'
|
||||
bannerFilename = baseDir + '/accounts/' + \
|
||||
nickname + '@' + domain + '/' + bannerFile
|
||||
|
|
@ -4476,16 +4652,20 @@ def htmlTimeline(defaultTimeline: str,
|
|||
bannerFile = 'banner.webp'
|
||||
|
||||
with open(cssFilename, 'r') as cssFile:
|
||||
# load css
|
||||
profileStyle = \
|
||||
cssFile.read().replace('banner.png',
|
||||
'/users/' + nickname + '/' + bannerFile)
|
||||
# replace any https within the css with whatever prefix is needed
|
||||
if httpPrefix != 'https':
|
||||
profileStyle = \
|
||||
profileStyle.replace('https://',
|
||||
httpPrefix + '://')
|
||||
|
||||
# is the user a moderator?
|
||||
moderator = isModerator(baseDir, nickname)
|
||||
|
||||
# the appearance of buttons - highlighted or not
|
||||
inboxButton = 'button'
|
||||
blogsButton = 'button'
|
||||
dmButton = 'button'
|
||||
|
|
@ -4496,6 +4676,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
repliesButton = 'buttonhighlighted'
|
||||
mediaButton = 'button'
|
||||
bookmarksButton = 'button'
|
||||
eventsButton = 'button'
|
||||
sentButton = 'button'
|
||||
sharesButton = 'button'
|
||||
if newShare:
|
||||
|
|
@ -4529,16 +4710,21 @@ def htmlTimeline(defaultTimeline: str,
|
|||
sharesButton = 'buttonselectedhighlighted'
|
||||
elif boxName == 'tlbookmarks' or boxName == 'bookmarks':
|
||||
bookmarksButton = 'buttonselected'
|
||||
elif boxName == 'tlevents':
|
||||
eventsButton = 'buttonselected'
|
||||
|
||||
# get the full domain, including any port number
|
||||
fullDomain = domain
|
||||
if port != 80 and port != 443:
|
||||
if ':' not in domain:
|
||||
fullDomain = domain + ':' + str(port)
|
||||
|
||||
usersPath = '/users/' + nickname
|
||||
actor = httpPrefix + '://' + fullDomain + usersPath
|
||||
|
||||
showIndividualPostIcons = True
|
||||
|
||||
# show an icon for new follow approvals
|
||||
followApprovals = ''
|
||||
followRequestsFilename = \
|
||||
baseDir + '/accounts/' + \
|
||||
|
|
@ -4558,6 +4744,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
'" src="/' + iconsDir + '/person.png"/></a>\n'
|
||||
break
|
||||
|
||||
# moderation / reports button
|
||||
moderationButtonStr = ''
|
||||
if moderator and not minimal:
|
||||
moderationButtonStr = \
|
||||
|
|
@ -4567,8 +4754,10 @@ def htmlTimeline(defaultTimeline: str,
|
|||
htmlHighlightLabel(translate['Mod'], newReport) + \
|
||||
' </span></button></a>\n'
|
||||
|
||||
# shares, bookmarks and events buttons
|
||||
sharesButtonStr = ''
|
||||
bookmarksButtonStr = ''
|
||||
eventsButtonStr = ''
|
||||
if not minimal:
|
||||
sharesButtonStr = \
|
||||
'<a href="' + usersPath + '/tlshares"><button class="' + \
|
||||
|
|
@ -4581,10 +4770,39 @@ def htmlTimeline(defaultTimeline: str,
|
|||
bookmarksButton + '"><span>' + translate['Bookmarks'] + \
|
||||
' </span></button></a>\n'
|
||||
|
||||
eventsButtonStr = \
|
||||
'<a href="' + usersPath + '/tlevents"><button class="' + \
|
||||
eventsButton + '"><span>' + translate['Events'] + \
|
||||
' </span></button></a>\n'
|
||||
|
||||
tlStr = htmlHeader(cssFilename, profileStyle)
|
||||
|
||||
if boxName != 'dm':
|
||||
if boxName != 'tlblogs':
|
||||
# what screen to go to when a new post is created
|
||||
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:
|
||||
newPostButtonStr = \
|
||||
'<a class="imageAnchor" href="' + usersPath + \
|
||||
|
|
@ -4601,22 +4819,6 @@ def htmlTimeline(defaultTimeline: str,
|
|||
translate['Create a new post'] + \
|
||||
'" alt="| ' + translate['Create a new post'] + \
|
||||
'" 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
|
||||
# in lynx, but should be invisible in a graphical web browser
|
||||
|
|
@ -4681,6 +4883,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
'</span></button></a>\n'
|
||||
|
||||
# typically the blogs button
|
||||
# but may change if this is a blogging oriented instance
|
||||
if defaultTimeline != 'tlblogs':
|
||||
if not minimal:
|
||||
tlStr += \
|
||||
|
|
@ -4696,14 +4899,19 @@ def htmlTimeline(defaultTimeline: str,
|
|||
inboxButton + '"><span>' + translate['Inbox'] + \
|
||||
'</span></button></a>\n'
|
||||
|
||||
# button for the outbox
|
||||
tlStr += \
|
||||
' <a href="' + usersPath + \
|
||||
'/outbox"><button class="' + \
|
||||
sentButton+'"><span>' + translate['Outbox'] + \
|
||||
'</span></button></a>\n'
|
||||
|
||||
# add other buttons
|
||||
tlStr += \
|
||||
sharesButtonStr + bookmarksButtonStr + \
|
||||
sharesButtonStr + bookmarksButtonStr + eventsButtonStr + \
|
||||
moderationButtonStr + newPostButtonStr
|
||||
|
||||
# the search button
|
||||
tlStr += \
|
||||
' <a class="imageAnchor" href="' + usersPath + \
|
||||
'/search"><img loading="lazy" src="/' + \
|
||||
|
|
@ -4711,6 +4919,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
translate['Search and follow'] + '" alt="| ' + \
|
||||
translate['Search and follow'] + '" class="timelineicon"/></a>\n'
|
||||
|
||||
# the calendar button
|
||||
calendarAltText = translate['Calendar']
|
||||
if newCalendarEvent:
|
||||
# indicate that the calendar icon is highlighted
|
||||
|
|
@ -4721,6 +4930,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
calendarImage + '" title="' + translate['Calendar'] + \
|
||||
'" alt="| ' + calendarAltText + '" class="timelineicon"/></a>\n'
|
||||
|
||||
# the show/hide button, for a simpler header appearance
|
||||
tlStr += \
|
||||
' <a class="imageAnchor" href="' + usersPath + '/minimal' + \
|
||||
'"><img loading="lazy" src="/' + iconsDir + \
|
||||
|
|
@ -4781,11 +4991,15 @@ def htmlTimeline(defaultTimeline: str,
|
|||
if boxName == 'inbox' and pageNumber == 1:
|
||||
if todaysEventsCheck(baseDir, nickname, domain):
|
||||
now = datetime.now()
|
||||
|
||||
# happening today button
|
||||
tlStr += \
|
||||
'<center>\n<a href="' + usersPath + '/calendar?year=' + \
|
||||
str(now.year) + '?month=' + str(now.month) + \
|
||||
'?day=' + str(now.day) + '"><button class="buttonevent">' + \
|
||||
translate['Happening Today'] + '</button></a>\n'
|
||||
|
||||
# happening this week button
|
||||
if thisWeeksEventsCheck(baseDir, nickname, domain):
|
||||
tlStr += \
|
||||
'<a href="' + usersPath + \
|
||||
|
|
@ -4793,6 +5007,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
translate['Happening This Week'] + '</button></a>\n'
|
||||
tlStr += '</center>\n'
|
||||
else:
|
||||
# happening this week button
|
||||
if thisWeeksEventsCheck(baseDir, nickname, domain):
|
||||
tlStr += \
|
||||
'<center>\n<a href="' + usersPath + \
|
||||
|
|
@ -4813,10 +5028,13 @@ def htmlTimeline(defaultTimeline: str,
|
|||
# show the posts
|
||||
itemCtr = 0
|
||||
if timelineJson:
|
||||
# if this is the media timeline then add an extra gallery container
|
||||
if boxName == 'tlmedia':
|
||||
if pageNumber > 1:
|
||||
tlStr += '<br>'
|
||||
tlStr += '<div class="galleryContainer">\n'
|
||||
|
||||
# show each post in the timeline
|
||||
for item in timelineJson['orderedItems']:
|
||||
if item['type'] == 'Create' or \
|
||||
item['type'] == 'Announce' or \
|
||||
|
|
@ -4830,7 +5048,7 @@ def htmlTimeline(defaultTimeline: str,
|
|||
if boxName != 'tlmedia' and \
|
||||
recentPostsCache.get('index'):
|
||||
postId = \
|
||||
item['id'].replace('/activity', '').replace('/', '#')
|
||||
removeIdEnding(item['id']).replace('/', '#')
|
||||
if postId in recentPostsCache['index']:
|
||||
if not item.get('muted'):
|
||||
if recentPostsCache['html'].get(postId):
|
||||
|
|
@ -4942,6 +5160,28 @@ def htmlBookmarks(defaultTimeline: str,
|
|||
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,
|
||||
recentPostsCache: {}, maxRecentPosts: int,
|
||||
translate: {}, pageNumber: int, itemsPerPage: int,
|
||||
|
|
@ -5105,7 +5345,7 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
|
|||
httpPrefix, projectVersion, 'inbox',
|
||||
YTReplacementDomain,
|
||||
False, authorized, False, False, False)
|
||||
messageId = postJsonObject['id'].replace('/activity', '')
|
||||
messageId = removeIdEnding(postJsonObject['id'])
|
||||
|
||||
# show the previous posts
|
||||
if isinstance(postJsonObject['object'], dict):
|
||||
|
|
|
|||