diff --git a/bookmarks.py b/bookmarks.py
new file mode 100644
index 00000000..2462406b
--- /dev/null
+++ b/bookmarks.py
@@ -0,0 +1,667 @@
+__filename__ = "bookmarks.py"
+__author__ = "Bob Mottram"
+__license__ = "AGPL3+"
+__version__ = "1.0.0"
+__maintainer__ = "Bob Mottram"
+__email__ = "bob@freedombone.net"
+__status__ = "Production"
+
+import os
+import json
+import time
+import commentjson
+from pprint import pprint
+from utils import urlPermitted
+from utils import getNicknameFromActor
+from utils import getDomainFromActor
+from utils import locatePost
+from utils import getCachedPostFilename
+from utils import loadJson
+from utils import saveJson
+from posts import sendSignedJson
+from session import postJson
+from webfinger import webfingerHandle
+from auth import createBasicAuthHeader
+from posts import getPersonBox
+
+def undoBookmarksCollectionEntry(baseDir: str,postFilename: str,objectUrl: str, \
+ actor: str,domain: str,debug: bool) -> None:
+ """Undoes a bookmark for a particular actor
+ """
+ postJsonObject=loadJson(postFilename)
+ if postJsonObject:
+ # remove any cached version of this post so that the bookmark icon is changed
+ nickname=getNicknameFromActor(actor)
+ cachedPostFilename= \
+ getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
+ if os.path.isfile(cachedPostFilename):
+ os.remove(cachedPostFilename)
+
+ if not postJsonObject.get('type'):
+ return
+ if postJsonObject['type']!='Create':
+ return
+ if not postJsonObject.get('object'):
+ if debug:
+ pprint(postJsonObject)
+ print('DEBUG: post '+objectUrl+' has no object')
+ return
+ if not isinstance(postJsonObject['object'], dict):
+ return
+ if not postJsonObject['object'].get('bookmarks'):
+ return
+ if not isinstance(postJsonObject['object']['bookmarks'], dict):
+ return
+ if not postJsonObject['object']['bookmarks'].get('items'):
+ return
+ totalItems=0
+ if postJsonObject['object']['bookmarks'].get('totalItems'):
+ totalItems=postJsonObject['object']['bookmarks']['totalItems']
+ itemFound=False
+ for bookmarkItem in postJsonObject['object']['bookmarks']['items']:
+ if bookmarkItem.get('actor'):
+ if bookmarkItem['actor']==actor:
+ if debug:
+ print('DEBUG: bookmark was removed for '+actor)
+ postJsonObject['object']['bookmarks']['items'].remove(bookmarkItem)
+ itemFound=True
+ break
+ if itemFound:
+ if totalItems==1:
+ if debug:
+ print('DEBUG: bookmarks was removed from post')
+ del postJsonObject['object']['bookmarks']
+ else:
+ postJsonObject['object']['bookmarks']['totalItems']= \
+ len(postJsonObject['bookmarks']['items'])
+ saveJson(postJsonObject,postFilename)
+
+ # remove from the index
+ bookmarksIndexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/bookmarks.index'
+ if os.path.isfile(bookmarksIndexFilename):
+ bookmarkIndex=postFilename.split('/')[-1]+'\n'
+ if bookmarkIndex in open(bookmarksIndexFilename).read():
+ indexStr=''
+ indexStrChanged=False
+ with open(bookmarksIndexFilename, 'r') as indexFile:
+ indexStr=indexFile.read().replace(bookmarkIndex,'')
+ indexStrChanged=True
+ if indexStrChanged:
+ bookmarksIndexFile=open(bookmarksIndexFilename,'w')
+ if bookmarksIndexFile:
+ bookmarksIndexFile.write(indexStr)
+ bookmarksIndexFile.close()
+
+def bookmarkedByPerson(postJsonObject: {}, nickname: str,domain: str) -> bool:
+ """Returns True if the given post is bookmarked by the given person
+ """
+ if noOfBookmarks(postJsonObject)==0:
+ return False
+ actorMatch=domain+'/users/'+nickname
+ for item in postJsonObject['object']['bookmarks']['items']:
+ if item['actor'].endswith(actorMatch):
+ return True
+ return False
+
+def noOfBookmarks(postJsonObject: {}) -> int:
+ """Returns the number of bookmarks ona given post
+ """
+ if not postJsonObject.get('object'):
+ return 0
+ if not isinstance(postJsonObject['object'], dict):
+ return 0
+ if not postJsonObject['object'].get('bookmarks'):
+ return 0
+ if not isinstance(postJsonObject['object']['bookmarks'], dict):
+ return 0
+ if not postJsonObject['object']['bookmarks'].get('items'):
+ postJsonObject['object']['bookmarks']['items']=[]
+ postJsonObject['object']['bookmarks']['totalItems']=0
+ return len(postJsonObject['object']['bookmarks']['items'])
+
+def updateBookmarksCollection(baseDir: str,postFilename: str, \
+ objectUrl: str, \
+ actor: str,domain: str,debug: bool) -> None:
+ """Updates the bookmarks collection within a post
+ """
+ postJsonObject=loadJson(postFilename)
+ if postJsonObject:
+ # remove any cached version of this post so that the bookmark icon is changed
+ nickname=getNicknameFromActor(actor)
+ cachedPostFilename= \
+ getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
+ if os.path.isfile(cachedPostFilename):
+ os.remove(cachedPostFilename)
+
+ if not postJsonObject.get('object'):
+ if debug:
+ pprint(postJsonObject)
+ print('DEBUG: post '+objectUrl+' has no object')
+ return
+ if not objectUrl.endswith('/bookmarks'):
+ objectUrl=objectUrl+'/bookmarks'
+ if not postJsonObject['object'].get('bookmarks'):
+ if debug:
+ print('DEBUG: Adding initial bookmarks to '+objectUrl)
+ bookmarksJson = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'id': objectUrl,
+ 'type': 'Collection',
+ "totalItems": 1,
+ 'items': [{
+ 'type': 'Bookmark',
+ 'actor': actor
+ }]
+ }
+ postJsonObject['object']['bookmarks']=bookmarksJson
+ else:
+ if not postJsonObject['object']['bookmarks'].get('items'):
+ postJsonObject['object']['bookmarks']['items']=[]
+ for bookmarkItem in postJsonObject['object']['bookmarks']['items']:
+ if bookmarkItem.get('actor'):
+ if bookmarkItem['actor']==actor:
+ return
+ newBookmark={
+ 'type': 'Bookmark',
+ 'actor': actor
+ }
+ postJsonObject['object']['bookmarks']['items'].append(newBookmark)
+ postJsonObject['object']['bookmarks']['totalItems']= \
+ len(postJsonObject['object']['bookmarks']['items'])
+
+ if debug:
+ print('DEBUG: saving post with bookmarks added')
+ pprint(postJsonObject)
+
+ saveJson(postJsonObject,postFilename)
+
+ # prepend to the index
+ bookmarksIndexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/bookmarks.index'
+ bookmarkIndex=postFilename.split('/')[-1]
+ if os.path.isfile(bookmarksIndexFilename):
+ if bookmarkIndex not in open(bookmarksIndexFilename).read():
+ try:
+ with open(bookmarksIndexFilename, 'r+') as bookmarksIndexFile:
+ content = bookmarksIndexFile.read()
+ bookmarksIndexFile.seek(0, 0)
+ bookmarksIndexFile.write(bookmarkIndex+'\n'+content)
+ if debug:
+ print('DEBUG: bookmark added to index')
+ except Exception as e:
+ print('WARN: Failed to write entry to bookmarks index '+ \
+ bookmarksIndexFilename+' '+str(e))
+ else:
+ bookmarksIndexFile=open(bookmarksIndexFilename,'w')
+ if bookmarksIndexFile:
+ bookmarksIndexFile.write(bookmarkIndex+'\n')
+ bookmarksIndexFile.close()
+
+def bookmark(session,baseDir: str,federationList: [], \
+ nickname: str,domain: str,port: int, \
+ ccList: [],httpPrefix: str, \
+ objectUrl: str,actorBookmarked: str, \
+ clientToServer: bool, \
+ sendThreads: [],postLog: [], \
+ personCache: {},cachedWebfingers: {}, \
+ debug: bool,projectVersion: str) -> {}:
+ """Creates a bookmark
+ actor is the person doing the bookmarking
+ 'to' might be a specific person (actor) whose post was bookmarked
+ object is typically the url of the message which was bookmarked
+ """
+ if not urlPermitted(objectUrl,federationList,"inbox:write"):
+ return None
+
+ fullDomain=domain
+ if port:
+ if port!=80 and port!=443:
+ if ':' not in domain:
+ fullDomain=domain+':'+str(port)
+
+ bookmarkTo=[]
+ if '/statuses/' in objectUrl:
+ bookmarkTo=[objectUrl.split('/statuses/')[0]]
+
+ newBookmarkJson = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Bookmark',
+ 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+ 'object': objectUrl
+ }
+ if ccList:
+ if len(ccList)>0:
+ newBookmarkJson['cc']=ccList
+
+ # Extract the domain and nickname from a statuses link
+ bookmarkedPostNickname=None
+ bookmarkedPostDomain=None
+ bookmarkedPostPort=None
+ if actorBookmarked:
+ bookmarkedPostNickname=getNicknameFromActor(actorBookmarked)
+ bookmarkedPostDomain,bookmarkedPostPort=getDomainFromActor(actorBookmarked)
+ else:
+ if '/users/' in objectUrl or \
+ '/channel/' in objectUrl or \
+ '/profile/' in objectUrl:
+ bookmarkedPostNickname=getNicknameFromActor(objectUrl)
+ bookmarkedPostDomain,bookmarkedPostPort=getDomainFromActor(objectUrl)
+
+ if bookmarkedPostNickname:
+ postFilename=locatePost(baseDir,nickname,domain,objectUrl)
+ if not postFilename:
+ print('DEBUG: bookmark baseDir: '+baseDir)
+ print('DEBUG: bookmark nickname: '+nickname)
+ print('DEBUG: bookmark domain: '+domain)
+ print('DEBUG: bookmark objectUrl: '+objectUrl)
+ return None
+
+ updateBookmarksCollection(baseDir,postFilename,objectUrl, \
+ newBookmarkJson['actor'],domain,debug)
+
+ sendSignedJson(newBookmarkJson,session,baseDir, \
+ nickname,domain,port, \
+ bookmarkedPostNickname,bookmarkedPostDomain,bookmarkedPostPort, \
+ 'https://www.w3.org/ns/activitystreams#Public', \
+ httpPrefix,True,clientToServer,federationList, \
+ sendThreads,postLog,cachedWebfingers,personCache, \
+ debug,projectVersion)
+
+ return newBookmarkJson
+
+def bookmarkPost(session,baseDir: str,federationList: [], \
+ nickname: str,domain: str,port: int,httpPrefix: str, \
+ bookmarkNickname: str,bookmarkedomain: str,bookmarkPort: int, \
+ ccList: [], \
+ bookmarkStatusNumber: int,clientToServer: bool, \
+ sendThreads: [],postLog: [], \
+ personCache: {},cachedWebfingers: {}, \
+ debug: bool,projectVersion: str) -> {}:
+ """Bookmarks a given status post. This is only used by unit tests
+ """
+ bookmarkedomain=bookmarkedomain
+ if bookmarkPort:
+ if bookmarkPort!=80 and bookmarkPort!=443:
+ if ':' not in bookmarkedomain:
+ bookmarkedomain=bookmarkedomain+':'+str(bookmarkPort)
+
+ actorBookmarked= \
+ httpPrefix + '://'+bookmarkedomain+'/users/'+bookmarkNickname
+ objectUrl=actorBookmarked+'/statuses/'+str(bookmarkStatusNumber)
+
+ ccUrl=httpPrefix+'://'+bookmarkedomain+'/users/'+bookmarkNickname
+ if bookmarkPort:
+ if bookmarkPort!=80 and bookmarkPort!=443:
+ if ':' not in bookmarkedomain:
+ ccUrl= \
+ httpPrefix+'://'+bookmarkedomain+':'+ \
+ str(bookmarkPort)+'/users/'+bookmarkNickname
+
+ return bookmark(session,baseDir,federationList,nickname,domain,port, \
+ ccList,httpPrefix,objectUrl,actorBookmarked,clientToServer, \
+ sendThreads,postLog,personCache,cachedWebfingers, \
+ debug,projectVersion)
+
+def undoBookmark(session,baseDir: str,federationList: [], \
+ nickname: str,domain: str,port: int, \
+ ccList: [],httpPrefix: str, \
+ objectUrl: str,actorBookmarked: str, \
+ clientToServer: bool, \
+ sendThreads: [],postLog: [], \
+ personCache: {},cachedWebfingers: {}, \
+ debug: bool,projectVersion: str) -> {}:
+ """Removes a bookmark
+ actor is the person doing the bookmarking
+ 'to' might be a specific person (actor) whose post was bookmarked
+ object is typically the url of the message which was bookmarked
+ """
+ if not urlPermitted(objectUrl,federationList,"inbox:write"):
+ return None
+
+ fullDomain=domain
+ if port:
+ if port!=80 and port!=443:
+ if ':' not in domain:
+ fullDomain=domain+':'+str(port)
+
+ bookmarkTo=[]
+ if '/statuses/' in objectUrl:
+ bookmarkTo=[objectUrl.split('/statuses/')[0]]
+
+ newUndoBookmarkJson = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Undo',
+ 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+ 'object': {
+ 'type': 'Bookmark',
+ 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
+ 'object': objectUrl
+ }
+ }
+ if ccList:
+ if len(ccList)>0:
+ newUndoBookmarkJson['cc']=ccList
+ newUndoBookmarkJson['object']['cc']=ccList
+
+ # Extract the domain and nickname from a statuses link
+ bookmarkedPostNickname=None
+ bookmarkedPostDomain=None
+ bookmarkedPostPort=None
+ if actorBookmarked:
+ bookmarkedPostNickname=getNicknameFromActor(actorBookmarked)
+ bookmarkedPostDomain,bookmarkedPostPort=getDomainFromActor(actorBookmarked)
+ else:
+ if '/users/' in objectUrl or \
+ '/channel/' in objectUrl or \
+ '/profile/' in objectUrl:
+ bookmarkedPostNickname=getNicknameFromActor(objectUrl)
+ bookmarkedPostDomain,bookmarkedPostPort=getDomainFromActor(objectUrl)
+
+ if bookmarkedPostNickname:
+ postFilename=locatePost(baseDir,nickname,domain,objectUrl)
+ if not postFilename:
+ return None
+
+ undoBookmarksCollectionEntry(baseDir,postFilename,objectUrl, \
+ newBookmarkJson['actor'],domain,debug)
+
+ sendSignedJson(newUndoBookmarkJson,session,baseDir, \
+ nickname,domain,port, \
+ bookmarkedPostNickname,bookmarkedPostDomain,bookmarkedPostPort, \
+ 'https://www.w3.org/ns/activitystreams#Public', \
+ httpPrefix,True,clientToServer,federationList, \
+ sendThreads,postLog,cachedWebfingers,personCache, \
+ debug,projectVersion)
+ else:
+ return None
+
+ return newUndoBookmarkJson
+
+def undoBookmarkPost(session,baseDir: str,federationList: [], \
+ nickname: str,domain: str,port: int,httpPrefix: str, \
+ bookmarkNickname: str,bookmarkedomain: str,bookmarkPort: int, \
+ ccList: [], \
+ bookmarkStatusNumber: int,clientToServer: bool, \
+ sendThreads: [],postLog: [], \
+ personCache: {},cachedWebfingers: {}, \
+ debug: bool) -> {}:
+ """Removes a bookmarked post
+ """
+ bookmarkedomain=bookmarkedomain
+ if bookmarkPort:
+ if bookmarkPort!=80 and bookmarkPort!=443:
+ if ':' not in bookmarkedomain:
+ bookmarkedomain=bookmarkedomain+':'+str(bookmarkPort)
+
+ objectUrl = \
+ httpPrefix + '://'+bookmarkedomain+'/users/'+bookmarkNickname+ \
+ '/statuses/'+str(bookmarkStatusNumber)
+
+ ccUrl=httpPrefix+'://'+bookmarkedomain+'/users/'+bookmarkNickname
+ if bookmarkPort:
+ if bookmarkPort!=80 and bookmarkPort!=443:
+ if ':' not in bookmarkedomain:
+ ccUrl= \
+ httpPrefix+'://'+bookmarkedomain+':'+ \
+ str(bookmarkPort)+'/users/'+bookmarkNickname
+
+ return undoBookmark(session,baseDir,federationList,nickname,domain,port, \
+ ccList,httpPrefix,objectUrl,clientToServer, \
+ sendThreads,postLog,personCache,cachedWebfingers,debug)
+
+def sendBookmarkViaServer(baseDir: str,session, \
+ fromNickname: str,password: str,
+ fromDomain: str,fromPort: int, \
+ httpPrefix: str,bookmarkUrl: str, \
+ cachedWebfingers: {},personCache: {}, \
+ debug: bool,projectVersion: str) -> {}:
+ """Creates a bookmark via c2s
+ """
+ if not session:
+ print('WARN: No session for sendBookmarkViaServer')
+ return 6
+
+ fromDomainFull=fromDomain
+ if fromPort:
+ if fromPort!=80 and fromPort!=443:
+ if ':' not in fromDomain:
+ fromDomainFull=fromDomain+':'+str(fromPort)
+
+ toUrl=['https://www.w3.org/ns/activitystreams#Public']
+ ccUrl=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
+
+ if '/statuses/' in bookmarkUrl:
+ toUrl=[bookmarkUrl.split('/statuses/')[0]]
+
+ newBookmarkJson = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Bookmark',
+ 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
+ 'object': bookmarkUrl
+ }
+
+ handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
+
+ # lookup the inbox for the To handle
+ wfRequest=webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
+ fromDomain,projectVersion)
+ if not wfRequest:
+ if debug:
+ print('DEBUG: announce webfinger failed for '+handle)
+ return 1
+
+ postToBox='outbox'
+
+ # get the actor inbox for the To handle
+ inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
+ getPersonBox(baseDir,session,wfRequest,personCache, \
+ projectVersion,httpPrefix,fromNickname, \
+ fromDomain,postToBox)
+
+ if not inboxUrl:
+ if debug:
+ print('DEBUG: No '+postToBox+' was found for '+handle)
+ return 3
+ if not fromPersonId:
+ if debug:
+ print('DEBUG: No actor was found for '+handle)
+ return 4
+
+ authHeader=createBasicAuthHeader(fromNickname,password)
+
+ headers = {'host': fromDomain, \
+ 'Content-type': 'application/json', \
+ 'Authorization': authHeader}
+ postResult = \
+ postJson(session,newBookmarkJson,[],inboxUrl,headers,"inbox:write")
+ #if not postResult:
+ # if debug:
+ # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
+ # return 5
+
+ if debug:
+ print('DEBUG: c2s POST bookmark success')
+
+ return newBookmarkJson
+
+def sendUndoBookmarkViaServer(baseDir: str,session, \
+ fromNickname: str,password: str, \
+ fromDomain: str,fromPort: int, \
+ httpPrefix: str,bookmarkUrl: str, \
+ cachedWebfingers: {},personCache: {}, \
+ debug: bool,projectVersion: str) -> {}:
+ """Undo a bookmark via c2s
+ """
+ if not session:
+ print('WARN: No session for sendUndoBookmarkViaServer')
+ return 6
+
+ fromDomainFull=fromDomain
+ if fromPort:
+ if fromPort!=80 and fromPort!=443:
+ if ':' not in fromDomain:
+ fromDomainFull=fromDomain+':'+str(fromPort)
+
+ toUrl=['https://www.w3.org/ns/activitystreams#Public']
+ ccUrl=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
+
+ if '/statuses/' in bookmarkUrl:
+ toUrl=[bookmarkUrl.split('/statuses/')[0]]
+
+ newUndoBookmarkJson = {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Undo',
+ 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
+ 'object': {
+ 'type': 'Bookmark',
+ 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
+ 'object': bookmarkUrl
+ }
+ }
+
+ handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
+
+ # lookup the inbox for the To handle
+ wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
+ fromDomain,projectVersion)
+ if not wfRequest:
+ if debug:
+ print('DEBUG: announce webfinger failed for '+handle)
+ return 1
+
+ postToBox='outbox'
+
+ # get the actor inbox for the To handle
+ inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
+ getPersonBox(baseDir,session,wfRequest,personCache, \
+ projectVersion,httpPrefix,fromNickname, \
+ fromDomain,postToBox)
+
+ if not inboxUrl:
+ if debug:
+ print('DEBUG: No '+postToBox+' was found for '+handle)
+ return 3
+ if not fromPersonId:
+ if debug:
+ print('DEBUG: No actor was found for '+handle)
+ return 4
+
+ authHeader=createBasicAuthHeader(fromNickname,password)
+
+ headers = {'host': fromDomain, \
+ 'Content-type': 'application/json', \
+ 'Authorization': authHeader}
+ postResult = \
+ postJson(session,newUndoBookmarkJson,[],inboxUrl,headers,"inbox:write")
+ #if not postResult:
+ # if debug:
+ # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
+ # return 5
+
+ if debug:
+ print('DEBUG: c2s POST undo bookmark success')
+
+ return newUndoBookmarkJson
+
+def outboxBookmark(baseDir: str,httpPrefix: str, \
+ nickname: str,domain: str,port: int, \
+ messageJson: {},debug: bool) -> None:
+ """ When a bookmark request is received by the outbox from c2s
+ """
+ if not messageJson.get('type'):
+ if debug:
+ print('DEBUG: bookmark - no type')
+ return
+ if not messageJson['type']=='Bookmark':
+ if debug:
+ print('DEBUG: not a bookmark')
+ return
+ if not messageJson.get('object'):
+ if debug:
+ print('DEBUG: no object in bookmark')
+ return
+ if not isinstance(messageJson['object'], str):
+ if debug:
+ print('DEBUG: bookmark object is not string')
+ return
+ if messageJson['object'].get('to'):
+ if not isinstance(messageJson['object']['to'], list):
+ return
+ if len(messageJson['object']['to'])!=1:
+ print('WARN: Bookmark should only be sent to one recipient')
+ return
+ if messageJson['object']['to'][0]!=messageJson['actor']:
+ print('WARN: Bookmark should be addressed to the same actor')
+ return
+ if debug:
+ print('DEBUG: c2s bookmark request arrived in outbox')
+
+ messageId=messageJson['object'].replace('/activity','')
+ if ':' in domain:
+ domain=domain.split(':')[0]
+ postFilename=locatePost(baseDir,nickname,domain,messageId)
+ if not postFilename:
+ if debug:
+ print('DEBUG: c2s bookmark post not found in inbox or outbox')
+ print(messageId)
+ return True
+ updateBookmarksCollection(baseDir,postFilename,messageId, \
+ messageJson['actor'],domain,debug)
+ if debug:
+ print('DEBUG: post bookmarked via c2s - '+postFilename)
+
+def outboxUndoBookmark(baseDir: str,httpPrefix: str, \
+ nickname: str,domain: str,port: int, \
+ messageJson: {},debug: bool) -> None:
+ """ When an undo bookmark request is received by the outbox from c2s
+ """
+ if not messageJson.get('type'):
+ return
+ if not messageJson['type']=='Undo':
+ return
+ if not messageJson.get('object'):
+ return
+ if not isinstance(messageJson['object'], dict):
+ if debug:
+ print('DEBUG: undo bookmark object is not dict')
+ return
+ if not messageJson['object'].get('type'):
+ if debug:
+ print('DEBUG: undo bookmark - no type')
+ return
+ if not messageJson['object']['type']=='Bookmark':
+ if debug:
+ print('DEBUG: not a undo bookmark')
+ return
+ if not messageJson['object'].get('object'):
+ if debug:
+ print('DEBUG: no object in undo bookmark')
+ return
+ if not isinstance(messageJson['object']['object'], str):
+ if debug:
+ print('DEBUG: undo bookmark object is not string')
+ return
+ if messageJson['object'].get('to'):
+ if not isinstance(messageJson['object']['to'], list):
+ return
+ if len(messageJson['object']['to'])!=1:
+ print('WARN: Bookmark should only be sent to one recipient')
+ return
+ if messageJson['object']['to'][0]!=messageJson['actor']:
+ print('WARN: Bookmark should be addressed to the same actor')
+ return
+ if debug:
+ print('DEBUG: c2s undo bookmark request arrived in outbox')
+
+ messageId=messageJson['object']['object'].replace('/activity','')
+ if ':' in domain:
+ domain=domain.split(':')[0]
+ postFilename=locatePost(baseDir,nickname,domain,messageId)
+ if not postFilename:
+ if debug:
+ print('DEBUG: c2s undo bookmark post not found in inbox or outbox')
+ print(messageId)
+ return True
+ undoBookmarksCollectionEntry(baseDir,postFilename,messageId, \
+ messageJson['actor'],domain,debug)
+ if debug:
+ print('DEBUG: post undo bookmarked via c2s - '+postFilename)
diff --git a/daemon.py b/daemon.py
index 66b28ac8..1ba12cde 100644
--- a/daemon.py
+++ b/daemon.py
@@ -75,6 +75,8 @@ from media import createMediaDirs
from delete import outboxDelete
from like import outboxLike
from like import outboxUndoLike
+from bookmarks import outboxBookmark
+from bookmarks import outboxUndoBookmark
from blocking import outboxBlock
from blocking import outboxUndoBlock
from blocking import addBlock
@@ -101,6 +103,7 @@ from webinterface import htmlPersonOptions
from webinterface import htmlIndividualPost
from webinterface import htmlProfile
from webinterface import htmlInbox
+from webinterface import htmlBookmarks
from webinterface import htmlShares
from webinterface import htmlOutbox
from webinterface import htmlModeration
@@ -566,7 +569,7 @@ class PubServer(BaseHTTPRequestHandler):
permittedOutboxTypes=[
'Create','Announce','Like','Follow','Undo', \
'Update','Add','Remove','Block','Delete', \
- 'Delegate','Skill'
+ 'Delegate','Skill','Bookmark'
]
if messageJson['type'] not in permittedOutboxTypes:
if self.server.debug:
@@ -643,6 +646,7 @@ class PubServer(BaseHTTPRequestHandler):
if self.server.debug:
print('DEBUG: handle availability changes requests')
outboxAvailability(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
+
if self.server.debug:
print('DEBUG: handle any like requests')
outboxLike(self.server.baseDir,self.server.httpPrefix, \
@@ -653,6 +657,18 @@ class PubServer(BaseHTTPRequestHandler):
outboxUndoLike(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain,self.server.port, \
messageJson,self.server.debug)
+
+ if self.server.debug:
+ print('DEBUG: handle any bookmark requests')
+ outboxBookmark(self.server.baseDir,self.server.httpPrefix, \
+ self.postToNickname,self.server.domain,self.server.port, \
+ messageJson,self.server.debug)
+ if self.server.debug:
+ print('DEBUG: handle any undo bookmark requests')
+ outboxUndoBookmark(self.server.baseDir,self.server.httpPrefix, \
+ self.postToNickname,self.server.domain,self.server.port, \
+ messageJson,self.server.debug)
+
if self.server.debug:
print('DEBUG: handle delete requests')
outboxDelete(self.server.baseDir,self.server.httpPrefix, \
@@ -1752,6 +1768,102 @@ class PubServer(BaseHTTPRequestHandler):
'?page='+str(pageNumber),cookie)
return
+ self._benchmarkGETtimings(GETstartTime,GETtimings,36)
+
+ # bookmark from the web interface icon
+ if htmlGET and '?bookmark=' in self.path:
+ pageNumber=1
+ bookmarkUrl=self.path.split('?bookmark=')[1]
+ if '?' in bookmarkUrl:
+ bookmarkUrl=bookmarkUrl.split('?')[0]
+ actor=self.path.split('?bookmark=')[0]
+ if '?page=' in self.path:
+ pageNumberStr=self.path.split('?page=')[1]
+ if '?' in pageNumberStr:
+ pageNumberStr=pageNumberStr.split('?')[0]
+ if pageNumberStr.isdigit():
+ pageNumber=int(pageNumberStr)
+ timelineStr='inbox'
+ if '?tl=' in self.path:
+ timelineStr=self.path.split('?tl=')[1]
+ if '?' in timelineStr:
+ timelineStr=timelineStr.split('?')[0]
+
+ self.postToNickname=getNicknameFromActor(actor)
+ if not self.postToNickname:
+ print('WARN: unable to find nickname in '+actor)
+ self.server.GETbusy=False
+ self._redirect_headers(actor+'/'+timelineStr+ \
+ '?page='+str(pageNumber),cookie)
+ return
+ if not self.server.session:
+ self.server.session= \
+ createSession(self.server.useTor)
+ bookmarkActor= \
+ self.server.httpPrefix+'://'+ \
+ self.server.domainFull+'/users/'+self.postToNickname
+ bookmarkJson= {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Bookmark',
+ 'actor': bookmarkActor,
+ 'to': [bookmarkActor],
+ 'object': bookmarkUrl
+ }
+ self._postToOutbox(bookmarkJson,self.server.projectVersion)
+ self.server.GETbusy=False
+ self._redirect_headers(actor+'/'+timelineStr+ \
+ '?page='+str(pageNumber),cookie)
+ return
+
+ # undo a bookmark from the web interface icon
+ if htmlGET and '?unbookmark=' in self.path:
+ pageNumber=1
+ bookmarkUrl=self.path.split('?unbookmark=')[1]
+ if '?' in bookmarkUrl:
+ bookmarkUrl=bookmarkUrl.split('?')[0]
+ if '?page=' in self.path:
+ pageNumberStr=self.path.split('?page=')[1]
+ if '?' in pageNumberStr:
+ pageNumberStr=pageNumberStr.split('?')[0]
+ if pageNumberStr.isdigit():
+ pageNumber=int(pageNumberStr)
+ timelineStr='inbox'
+ if '?tl=' in self.path:
+ timelineStr=self.path.split('?tl=')[1]
+ if '?' in timelineStr:
+ timelineStr=timelineStr.split('?')[0]
+ actor=self.path.split('?unbookmark=')[0]
+ self.postToNickname=getNicknameFromActor(actor)
+ if not self.postToNickname:
+ print('WARN: unable to find nickname in '+actor)
+ self.server.GETbusy=False
+ self._redirect_headers(actor+'/'+timelineStr+ \
+ '?page='+str(pageNumber),cookie)
+ return
+ if not self.server.session:
+ self.server.session= \
+ createSession(self.server.useTor)
+ undoActor= \
+ self.server.httpPrefix+'://'+ \
+ self.server.domainFull+'/users/'+self.postToNickname
+ undoBookmarkJson= {
+ "@context": "https://www.w3.org/ns/activitystreams",
+ 'type': 'Undo',
+ 'actor': undoActor,
+ 'to': [undoActor],
+ 'object': {
+ 'type': 'Bookmark',
+ 'actor': undoActor,
+ 'to': [undoActor],
+ 'object': bookmarkUrl
+ }
+ }
+ self._postToOutbox(undoBookmarkJson,self.server.projectVersion)
+ self.server.GETbusy=False
+ self._redirect_headers(actor+'/'+timelineStr+ \
+ '?page='+str(pageNumber),cookie)
+ return
+
self._benchmarkGETtimings(GETstartTime,GETtimings,37)
# delete a post from the web interface icon
@@ -2559,7 +2671,77 @@ class PubServer(BaseHTTPRequestHandler):
self.end_headers()
self.server.GETbusy=False
return
-
+
+ # get the bookmarks for a given person
+ if self.path.endswith('/tlbookmarks') or '/tlbookmarks?page=' in self.path:
+ if '/users/' in self.path:
+ if authorized:
+ bookmarksFeed= \
+ personBoxJson(self.server.session, \
+ self.server.baseDir, \
+ self.server.domain, \
+ self.server.port, \
+ self.path, \
+ self.server.httpPrefix, \
+ maxPostsInFeed, 'tlbookmarks', \
+ True,self.server.ocapAlways)
+ if bookmarksFeed:
+ if self._requestHTTP():
+ nickname=self.path.replace('/users/','').replace('/inbox','')
+ 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
+ bookmarksFeed= \
+ personBoxJson(self.server.session, \
+ self.server.baseDir, \
+ self.server.domain, \
+ self.server.port, \
+ self.path+'?page=1', \
+ self.server.httpPrefix, \
+ maxPostsInFeed, 'tlbookmarks', \
+ True,self.server.ocapAlways)
+ msg=htmlBookmarks(self.server.translate, \
+ pageNumber,maxPostsInFeed, \
+ self.server.session, \
+ self.server.baseDir, \
+ self.server.cachedWebfingers, \
+ self.server.personCache, \
+ nickname, \
+ self.server.domain, \
+ self.server.port, \
+ bookmarksFeed, \
+ self.server.allowDeletion, \
+ self.server.httpPrefix, \
+ self.server.projectVersion).encode('utf-8')
+ self._set_headers('text/html',len(msg),cookie)
+ self._write(msg)
+ else:
+ # don't need authenticated fetch here because there is
+ # already the authorization check
+ msg=json.dumps(inboxFeed,ensure_ascii=False).encode('utf-8')
+ self._set_headers('application/json',len(msg),None)
+ self._write(msg)
+ self.server.GETbusy=False
+ return
+ else:
+ if self.server.debug:
+ nickname=self.path.replace('/users/','').replace('/inbox','')
+ print('DEBUG: '+nickname+ \
+ ' was not authorized to access '+self.path)
+ if self.server.debug:
+ print('DEBUG: GET access to bookmarks 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
diff --git a/inbox.py b/inbox.py
index 1d4df69e..6b1d265e 100644
--- a/inbox.py
+++ b/inbox.py
@@ -40,6 +40,8 @@ from capabilities import CapablePost
from capabilities import capabilitiesReceiveUpdate
from like import updateLikesCollection
from like import undoLikesCollectionEntry
+from bookmarks import updateBookmarkssCollection
+from bookmarks import undoBookmarksCollectionEntry
from blocking import isBlocked
from blocking import isBlockedDomain
from filters import isFiltered
@@ -900,6 +902,129 @@ def receiveUndoLike(session,handle: str,isGroup: bool,baseDir: str, \
undoLikesCollectionEntry(baseDir,postFilename,messageJson['object'],messageJson['actor'],domain,debug)
return True
+def receiveBookmark(session,handle: str,isGroup: bool,baseDir: str, \
+ httpPrefix: str,domain :str,port: int, \
+ sendThreads: [],postLog: [],cachedWebfingers: {}, \
+ personCache: {},messageJson: {},federationList: [], \
+ debug : bool) -> bool:
+ """Receives a bookmark activity within the POST section of HTTPServer
+ """
+ if messageJson['type']!='Bookmark':
+ return False
+ if not messageJson.get('actor'):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' has no actor')
+ return False
+ if not messageJson.get('object'):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' has no object')
+ return False
+ if not isinstance(messageJson['object'], str):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' object is not a string')
+ return False
+ if not messageJson.get('to'):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' has no "to" list')
+ return False
+ if '/users/' not in messageJson['actor']:
+ if debug:
+ print('DEBUG: "users" missing from actor in '+messageJson['type'])
+ return False
+ if '/statuses/' not in messageJson['object']:
+ if debug:
+ print('DEBUG: "statuses" missing from object in '+messageJson['type'])
+ return False
+ if domain not in handle.split('@')[1]:
+ if debug:
+ print('DEBUG: unrecognized domain '+handle)
+ return False
+ domainFull=domain
+ if port:
+ if port!=80 and port!=443:
+ domainFull=domain+':'+str(port)
+ nickname=handle.split('@')[0]
+ if not messageJson['actor'].endswith(domainFull+'/users/'+nickname):
+ if debug:
+ print('DEBUG: bookmark actor should be the same as the handle sent to '+handle+' != '+messageJson['actor'])
+ return False
+ if not os.path.isdir(baseDir+'/accounts/'+handle):
+ print('DEBUG: unknown recipient of bookmark - '+handle)
+ # if this post in the outbox of the person?
+ postFilename=locatePost(baseDir,nickname,domain,messageJson['object'])
+ if not postFilename:
+ if debug:
+ print('DEBUG: post not found in inbox or outbox')
+ print(messageJson['object'])
+ return True
+ if debug:
+ print('DEBUG: bookmarked post was found')
+
+ updateBookmarksCollection(baseDir,postFilename,messageJson['object'],messageJson['actor'],domain,debug)
+ return True
+
+def receiveUndoBookmark(session,handle: str,isGroup: bool,baseDir: str, \
+ httpPrefix: str,domain :str,port: int, \
+ sendThreads: [],postLog: [],cachedWebfingers: {}, \
+ personCache: {},messageJson: {},federationList: [], \
+ debug : bool) -> bool:
+ """Receives an undo bookmark activity within the POST section of HTTPServer
+ """
+ if messageJson['type']!='Undo':
+ 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']!='Bookmark':
+ return False
+ if not messageJson['object'].get('object'):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' like has no object')
+ return False
+ if not isinstance(messageJson['object']['object'], str):
+ if debug:
+ print('DEBUG: '+messageJson['type']+' like object is not a string')
+ return False
+ if '/users/' not in messageJson['actor']:
+ if debug:
+ print('DEBUG: "users" missing from actor in '+messageJson['type']+' like')
+ return False
+ if '/statuses/' not in messageJson['object']['object']:
+ if debug:
+ print('DEBUG: "statuses" missing from like object in '+messageJson['type'])
+ return False
+ domainFull=domain
+ if port:
+ if port!=80 and port!=443:
+ domainFull=domain+':'+str(port)
+ nickname=handle.split('@')[0]
+ if domain not in handle.split('@')[1]:
+ if debug:
+ print('DEBUG: unrecognized bookmark domain '+handle)
+ return False
+ if not messageJson['actor'].endswith(domainFull+'/users/'+nickname):
+ if debug:
+ print('DEBUG: bookmark actor should be the same as the handle sent to '+handle+' != '+messageJson['actor'])
+ return False
+ if not os.path.isdir(baseDir+'/accounts/'+handle):
+ print('DEBUG: unknown recipient of bookmark undo - '+handle)
+ # if this post in the outbox of the person?
+ postFilename=locatePost(baseDir,nickname,domain,messageJson['object']['object'])
+ if not postFilename:
+ if debug:
+ print('DEBUG: unbookmarked post not found in inbox or outbox')
+ print(messageJson['object']['object'])
+ return True
+ if debug:
+ print('DEBUG: bookmarked post found. Now undoing.')
+ undoBookmarksCollectionEntry(baseDir,postFilename,messageJson['object'],messageJson['actor'],domain,debug)
+ return True
+
def receiveDelete(session,handle: str,isGroup: bool,baseDir: str, \
httpPrefix: str,domain :str,port: int, \
sendThreads: [],postLog: [],cachedWebfingers: {}, \
@@ -1517,6 +1642,32 @@ def inboxAfterCapabilities(session,keyId: str,handle: str,messageJson: {}, \
print('DEBUG: Undo like accepted from '+actor)
return False
+ if receiveBookmark(session,handle,isGroup, \
+ baseDir,httpPrefix, \
+ domain,port, \
+ sendThreads,postLog, \
+ cachedWebfingers, \
+ personCache, \
+ messageJson, \
+ federationList, \
+ debug):
+ if debug:
+ print('DEBUG: Bookmark accepted from '+actor)
+ return False
+
+ if receiveUndoBookmark(session,handle,isGroup, \
+ baseDir,httpPrefix, \
+ domain,port, \
+ sendThreads,postLog, \
+ cachedWebfingers, \
+ personCache, \
+ messageJson, \
+ federationList, \
+ debug):
+ if debug:
+ print('DEBUG: Undo bookmark accepted from '+actor)
+ return False
+
if receiveAnnounce(session,handle,isGroup, \
baseDir,httpPrefix, \
domain,port, \
diff --git a/person.py b/person.py
index 242dc500..47206ef5 100644
--- a/person.py
+++ b/person.py
@@ -21,6 +21,7 @@ from webfinger import storeWebfingerEndpoint
from posts import createDMTimeline
from posts import createRepliesTimeline
from posts import createMediaTimeline
+from posts import createBookmarksTimeline
from posts import createInbox
from posts import createOutbox
from posts import createModeration
@@ -411,7 +412,8 @@ def personBoxJson(session,baseDir: str,domain: str,port: int,path: str, \
"""
if boxname!='inbox' and boxname!='dm' and \
boxname!='tlreplies' and boxname!='tlmedia' and \
- boxname!='outbox' and boxname!='moderation':
+ boxname!='outbox' and boxname!='moderation' and \
+ boxname!='tlbookmarks':
return None
if not '/'+boxname in path:
@@ -448,9 +450,12 @@ def personBoxJson(session,baseDir: str,domain: str,port: int,path: str, \
if boxname=='inbox':
return createInbox(session,baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
- if boxname=='dm':
+ elif boxname=='dm':
return createDMTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
+ elif boxname=='tlbookmarks':
+ return createBookmarksTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
+ noOfItems,headerOnly,ocapAlways,pageNumber)
elif boxname=='tlreplies':
return createRepliesTimeline(session,baseDir,nickname,domain,port,httpPrefix, \
noOfItems,headerOnly,ocapAlways,pageNumber)
diff --git a/posts.py b/posts.py
index f5e4858d..ad0f558d 100644
--- a/posts.py
+++ b/posts.py
@@ -1788,6 +1788,11 @@ def createInbox(session,baseDir: str,nickname: str,domain: str,port: int,httpPre
return createBoxBase(session,baseDir,'inbox',nickname,domain,port,httpPrefix, \
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
+def createBookmarksTimeline(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
+ itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
+ return createBoxBase(session,baseDir,'tlbookmarks',nickname,domain,port,httpPrefix, \
+ itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
+
def createDMTimeline(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
return createBoxBase(session,baseDir,'dm',nickname,domain,port,httpPrefix, \
@@ -2058,17 +2063,25 @@ def createBoxBase(session,baseDir: str,boxname: str, \
pageNumber=1
if boxname!='inbox' and boxname!='dm' and \
- boxname!='tlreplies' and boxname!='tlmedia' and boxname!='outbox':
+ boxname!='tlreplies' and boxname!='tlmedia' and \
+ boxname!='outbox' and boxname!='tlbookmarks':
return None
- if boxname!='dm' and boxname!='tlreplies' and boxname!='tlmedia':
+ if boxname!='dm' and boxname!='tlreplies' and \
+ boxname!='tlmedia' and boxname!='tlbookmarks':
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
else:
# extract DMs or replies or media from the inbox
boxDir = createPersonDir(nickname,domain,baseDir,'inbox')
sharedBoxDir=None
- if boxname=='inbox' or boxname=='tlreplies' or boxname=='tlmedia':
+ if boxname=='inbox' or boxname=='tlreplies' or \
+ boxname=='tlmedia' or boxname=='tlbookmarks':
sharedBoxDir = createPersonDir('inbox',domain,baseDir,boxname)
+ # bookmarks timeline is like the inbox but has its own separate index
+ indexBoxName=boxname
+ if boxname=='tlbookmarks':
+ indexBoxName='bookmarks'
+
if port:
if port!=80 and port!=443:
if ':' not in domain:
@@ -2102,7 +2115,7 @@ def createBoxBase(session,baseDir: str,boxname: str, \
postsInBoxDict={}
postsInBox={}
- indexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+boxname+'.index'
+ indexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+indexBoxName+'.index'
lookedUpFromIndex=False
if os.path.isfile(indexFilename):
print('DEBUG: using index file to construct timeline')
@@ -2125,7 +2138,7 @@ def createBoxBase(session,baseDir: str,boxname: str, \
postsCtr=createBoxIndex(boxDir,postsInBoxDict)
# combine the inbox for the account with the shared inbox
- if sharedBoxDir:
+ if sharedBoxDir and boxname!='tlbookmarks':
postsCtr= \
createSharedInboxIndex(baseDir,sharedBoxDir, \
postsInBoxDict,postsCtr, \
diff --git a/translations/ar.json b/translations/ar.json
index 611078c4..59b7140b 100644
--- a/translations/ar.json
+++ b/translations/ar.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/ca.json b/translations/ca.json
index fe4743b4..7e650277 100644
--- a/translations/ca.json
+++ b/translations/ca.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/cy.json b/translations/cy.json
index 7ba59182..24a970f4 100644
--- a/translations/cy.json
+++ b/translations/cy.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/de.json b/translations/de.json
index a9127589..a91916ae 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/en.json b/translations/en.json
index 5b76afc7..b215262b 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/es.json b/translations/es.json
index 437eff74..f21f6f0c 100644
--- a/translations/es.json
+++ b/translations/es.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/fr.json b/translations/fr.json
index df6a7e85..1914f9c8 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/ga.json b/translations/ga.json
index 137dc141..300f97bc 100644
--- a/translations/ga.json
+++ b/translations/ga.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/hi.json b/translations/hi.json
index 5e484816..0e24d5d2 100644
--- a/translations/hi.json
+++ b/translations/hi.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/it.json b/translations/it.json
index a75d0ea3..b8ab78b5 100644
--- a/translations/it.json
+++ b/translations/it.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/ja.json b/translations/ja.json
index 990ff9bc..06d0ddbc 100644
--- a/translations/ja.json
+++ b/translations/ja.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/oc.json b/translations/oc.json
index e040d8cb..19406e3b 100644
--- a/translations/oc.json
+++ b/translations/oc.json
@@ -173,5 +173,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/pt.json b/translations/pt.json
index 98f2e12c..a738b055 100644
--- a/translations/pt.json
+++ b/translations/pt.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/ru.json b/translations/ru.json
index 5fff6893..778e0310 100644
--- a/translations/ru.json
+++ b/translations/ru.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/translations/zh.json b/translations/zh.json
index 736db55e..47d3c0c2 100644
--- a/translations/zh.json
+++ b/translations/zh.json
@@ -177,5 +177,8 @@
"Instance Title": "Instance Title",
"Instance Short Description": "Instance Short Description",
"Instance Description": "Instance Description",
- "Instance Logo": "Instance Logo"
+ "Instance Logo": "Instance Logo",
+ "Bookmark this post": "Bookmark this post",
+ "Undo the bookmark": "Undo the bookmark",
+ "Bookmarks": "Bookmarks"
}
diff --git a/webinterface.py b/webinterface.py
index e3d4babb..762052ab 100644
--- a/webinterface.py
+++ b/webinterface.py
@@ -44,6 +44,7 @@ from session import getJson
from auth import createPassword
from like import likedByPerson
from like import noOfLikes
+from bookmarks import bookmarkedByPerson
from announce import announcedByPerson
from blocking import isBlocked
from content import getMentionsFromHtml
@@ -2213,6 +2214,22 @@ def individualPostAsHtml(iconsDir: str,translate: {}, \
'?tl='+boxName+'" title="'+likeTitle+'">'
likeStr+='
'
+ bookmarkStr=''
+ if not isModerationPost:
+ bookmarkIcon='bookmark_inactive.png'
+ bookmarkLink='bookmark'
+ bookmarkTitle=translate['Bookmark this post']
+ if bookmarkedByPerson(postJsonObject,nickname,fullDomain):
+ bookmarkIcon='bookmark.png'
+ bookmarkLink='unbookmark'
+ bookmarkTitle=translate['Undo the bookmark']
+ bookmarkStr= \
+ ''
+ bookmarkStr+='
'
+
deleteStr=''
if allowDeletion or \
('/'+fullDomain+'/' in postActor and \
@@ -2258,7 +2275,7 @@ def individualPostAsHtml(iconsDir: str,translate: {}, \
'?actor='+postJsonObject['actor']+ \
'" title="'+translate['Reply to this post']+'">'
footerStr+='
'
- footerStr+=announceStr+likeStr+deleteStr
+ footerStr+=announceStr+likeStr+bookmarkStr+deleteStr
footerStr+=''+publishedStr+''
footerStr+=''
@@ -2405,6 +2422,7 @@ def htmlTimeline(translate: {},pageNumber: int, \
if newReply:
repliesButton='buttonhighlighted'
mediaButton='button'
+ bookmarksButton='button'
sentButton='button'
sharesButton='button'
if newShare:
@@ -2434,6 +2452,8 @@ def htmlTimeline(translate: {},pageNumber: int, \
sharesButton='buttonselected'
if newShare:
sharesButton='buttonselectedhighlighted'
+ elif boxName=='tlbookmarks':
+ bookmarksButton='buttonselected'
fullDomain=domain
if port!=80 and port!=443:
@@ -2462,6 +2482,8 @@ def htmlTimeline(translate: {},pageNumber: int, \
sharesButtonStr=''
+ bookmarksButtonStr=''
+
tlStr=htmlHeader(cssFilename,profileStyle)
#if (boxName=='inbox' or boxName=='dm') and pageNumber==1:
# refresh if on the first page of the inbox and dm timeline
@@ -2485,7 +2507,7 @@ def htmlTimeline(translate: {},pageNumber: int, \
tlStr+=' '
tlStr+=' '
tlStr+=' '
- tlStr+=sharesButtonStr+moderationButtonStr+newPostButtonStr
+ tlStr+=sharesButtonStr+bookmarksButtonStr+moderationButtonStr+newPostButtonStr
tlStr+='
'
tlStr+='
'
tlStr+='
'
@@ -2582,6 +2604,21 @@ def htmlInbox(translate: {},pageNumber: int,itemsPerPage: int, \
nickname,domain,port,inboxJson,'inbox',allowDeletion, \
httpPrefix,projectVersion,manuallyApproveFollowers)
+def htmlBookmarks(translate: {},pageNumber: int,itemsPerPage: int, \
+ session,baseDir: str,wfRequest: {},personCache: {}, \
+ nickname: str,domain: str,port: int,inboxJson: {}, \
+ allowDeletion: bool, \
+ httpPrefix: str,projectVersion: str) -> str:
+ """Show the bookmarks as html
+ """
+ manuallyApproveFollowers= \
+ followerApprovalActive(baseDir,nickname,domain)
+
+ return htmlTimeline(translate,pageNumber, \
+ itemsPerPage,session,baseDir,wfRequest,personCache, \
+ nickname,domain,port,inboxJson,'tlbookmarks',allowDeletion, \
+ httpPrefix,projectVersion,manuallyApproveFollowers)
+
def htmlInboxDMs(translate: {},pageNumber: int,itemsPerPage: int, \
session,baseDir: str,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int,inboxJson: {}, \