From 52e1d4402198a3515d1701c24783b7ce769bb6e3 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 17 Nov 2019 14:01:49 +0000 Subject: [PATCH] Adding bookmarks --- bookmarks.py | 667 +++++++++++++++++++++++++++++++++++++++++++ daemon.py | 186 +++++++++++- inbox.py | 151 ++++++++++ person.py | 9 +- posts.py | 23 +- translations/ar.json | 5 +- translations/ca.json | 5 +- translations/cy.json | 5 +- translations/de.json | 5 +- translations/en.json | 5 +- translations/es.json | 5 +- translations/fr.json | 5 +- translations/ga.json | 5 +- translations/hi.json | 5 +- translations/it.json | 5 +- translations/ja.json | 5 +- translations/oc.json | 5 +- translations/pt.json | 5 +- translations/ru.json | 5 +- translations/zh.json | 5 +- webinterface.py | 41 ++- 21 files changed, 1126 insertions(+), 26 deletions(-) create mode 100644 bookmarks.py 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+=' '+translate['Search and follow']+'' tlStr+=' '+translate['Calendar']+'' tlStr+=' '+translate['Refresh']+'' @@ -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: {}, \