From 643ca568bcd6d61d966481195b22b5b5a289230b Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Tue, 23 Jul 2019 13:33:09 +0100 Subject: [PATCH] Functions for shared items --- daemon.py | 74 ++++++++++++++-- person.py | 6 +- posts.py | 8 +- shares.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++++ webinterface.py | 15 ++++ 5 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 shares.py diff --git a/daemon.py b/daemon.py index f5c12a0cc..e3868a0de 100644 --- a/daemon.py +++ b/daemon.py @@ -49,6 +49,7 @@ from webinterface import htmlProfile from webinterface import htmlInbox from webinterface import htmlOutbox from webinterface import htmlPostReplies +from shares import getSharesFeedForPerson import os import sys @@ -58,6 +59,9 @@ maxPostsInFeed=20 # number of follows/followers per page followsPerPage=12 +# number of item shares per page +sharesPerPage=12 + def readFollowList(filename: str): """Returns a list of ActivityPub addresses to follow """ @@ -354,6 +358,28 @@ class PubServer(BaseHTTPRequestHandler): return self._404() return + # show shared item images + # Note that this comes before the busy flag to avoid conflicts + if '/sharefiles/' in self.path: + if self.path.endswith('.png') or \ + self.path.endswith('.jpg') or \ + self.path.endswith('.gif'): + mediaStr=self.path.split('/sharefiles/')[1] + mediaFilename= \ + self.server.baseDir+'/sharefiles/'+mediaStr + if os.path.isfile(mediaFilename): + if mediaFilename.endswith('.png'): + self._set_headers('image/png') + elif mediaFilename.endswith('.jpg'): + self._set_headers('image/jpeg') + else: + self._set_headers('image/gif') + with open(mediaFilename, 'rb') as avFile: + mediaBinary = avFile.read() + self.wfile.write(mediaBinary) + return + self._404() + return # show avatar or background image # Note that this comes before the busy flag to avoid conflicts if '/users/' in self.path: @@ -699,7 +725,46 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy=False return authorized=self._isAuthorized() - + + shares=getSharesFeedForPerson(self.server.baseDir,self.server.domain, \ + self.server.port,self.path, \ + self.server.httpPrefix, \ + sharesPerPage) + if shares: + if 'text/html' in self.headers['Accept']: + if 'page=' not in self.path: + # get a page of shares, not the summary + shares=getSharesFeedForPerson(self.server.baseDir,self.server.domain, \ + self.server.port,self.path+'?page=true', \ + self.server.httpPrefix, \ + sharesPerPage) + getPerson = personLookup(self.server.domain,self.path.replace('/shares',''), \ + self.server.baseDir) + if getPerson: + if not self.server.session: + if self.server.debug: + print('DEBUG: creating new session') + self.server.session= \ + createSession(self.server.domain,self.server.port,self.server.useTor) + + self._set_headers('text/html') + self.wfile.write(htmlProfile(self.server.baseDir, \ + self.server.httpPrefix, \ + authorized, \ + self.server.ocapAlways, \ + getPerson,'shares', \ + self.server.session, \ + self.server.cachedWebfingers, \ + self.server.personCache, \ + shares).encode('utf-8')) + self.server.GETbusy=False + return + else: + self._set_headers('application/json') + self.wfile.write(json.dumps(shares).encode('utf-8')) + self.server.GETbusy=False + return + following=getFollowingFeed(self.server.baseDir,self.server.domain, \ self.server.port,self.path, \ self.server.httpPrefix, \ @@ -715,17 +780,12 @@ class PubServer(BaseHTTPRequestHandler): getPerson = personLookup(self.server.domain,self.path.replace('/following',''), \ self.server.baseDir) if getPerson: - if not self.server.session: - if self.server.debug: - print('DEBUG: creating new session for c2s') - self.server.session= \ - createSession(self.server.domain,self.server.port,self.server.useTor) - if not self.server.session: if self.server.debug: print('DEBUG: creating new session') self.server.session= \ createSession(self.server.domain,self.server.port,self.server.useTor) + self._set_headers('text/html') self.wfile.write(htmlProfile(self.server.baseDir, \ self.server.httpPrefix, \ diff --git a/person.py b/person.py index a394530ae..f33c20f98 100644 --- a/person.py +++ b/person.py @@ -137,6 +137,7 @@ def createPersonBase(baseDir: str,nickname: str,domain: str,port: int, \ 'endpoints': { 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/endpoints', 'sharedInbox': httpPrefix+'://'+domain+'/inbox', + 'shares': httpPrefix+'://'+domain+'/shares' }, 'capabilityAcquisitionEndpoint': httpPrefix+'://'+domain+'/caps/new', 'followers': httpPrefix+'://'+domain+'/users/'+nickname+'/followers', @@ -144,7 +145,6 @@ def createPersonBase(baseDir: str,nickname: str,domain: str,port: int, \ 'orgSchema': None, 'skills': {}, 'roles': {}, - 'shares': {}, 'availability': None, 'icon': {'mediaType': 'image/png', 'type': 'Image', @@ -233,6 +233,10 @@ def createPerson(baseDir: str,nickname: str,domain: str,port: int, \ setRole(baseDir,nickname,domain,'instance','admin') setRole(baseDir,nickname,domain,'instance','moderator') setRole(baseDir,nickname,domain,'instance','delegator') + + if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain): + os.mkdir(baseDir+'/accounts/'+nickname+'@'+domain) + if os.path.isfile(baseDir+'/img/default-avatar.png'): copyfile(baseDir+'/img/default-avatar.png',baseDir+'/accounts/'+nickname+'@'+domain+'/avatar.png') if os.path.isfile(baseDir+'/img/image.png'): diff --git a/posts.py b/posts.py index 993288b5c..ed1f7019f 100644 --- a/posts.py +++ b/posts.py @@ -114,16 +114,16 @@ def parseUserFeed(session,feedUrl: str,asHeader: {}) -> None: yield item def getPersonBox(session,wfRequest: {},personCache: {}, \ - boxName='inbox') -> (str,str,str,str,str,str): + boxName='inbox') -> (str,str,str,str,str,str,str,str): asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'} personUrl = getUserUrl(wfRequest) if not personUrl: - return None,None,None,None,None,None,None + return None,None,None,None,None,None,None,None personJson = getPersonFromCache(personUrl,personCache) if not personJson: personJson = getJson(session,personUrl,asHeader,None) if not personJson: - return None,None,None,None,None,None,None + return None,None,None,None,None,None,None,None boxJson=None if not personJson.get(boxName): if personJson.get('endpoints'): @@ -133,7 +133,7 @@ def getPersonBox(session,wfRequest: {},personCache: {}, \ boxJson=personJson[boxName] if not boxJson: - return None,None,None,None,None,None,None + return None,None,None,None,None,None,None,None personId=None if personJson.get('id'): diff --git a/shares.py b/shares.py new file mode 100644 index 000000000..20b30cdc3 --- /dev/null +++ b/shares.py @@ -0,0 +1,228 @@ +__filename__ = "shares.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "0.0.1" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import json +import commentjson +import os +import time +from shutil import copyfile +from person import validNickname +from webfinger import webfingerHandle +from auth import createBasicAuthHeader +from posts import getPersonBox +from session import postJson +from utils import getNicknameFromActor +from utils import getDomainFromActor + +def removeShare(baseDir: str,nickname: str,domain: str, \ + displayName: str) -> None: + """Removes a share for a person + """ + sharesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json' + if os.path.isfile(sharesFilename): + with open(sharesFilename, 'r') as fp: + sharesJson=commentjson.load(fp) + + itemID=displayName.replace(' ','') + if sharesJson.get(itemID): + # remove any image for the item + published=sharesJson[itemID]['published'] + itemIDfile=baseDir+'/sharefiles/'+str(published)+itemID + if sharesJson[itemID]['imageUrl']: + if sharesJson[itemID]['imageUrl'].endswith('.png'): + os.remove(itemIDfile+'.png') + if sharesJson[itemID]['imageUrl'].endswith('.jpg'): + os.remove(itemIDfile+'.jpg') + if sharesJson[itemID]['imageUrl'].endswith('.gif'): + os.remove(itemIDfile+'.gif') + # remove the item itself + del sharesJson[itemID] + with open(sharesFilename, 'w') as fp: + commentjson.dump(sharesJson, fp, indent=4, sort_keys=True) + +def addShare(baseDir: str,nickname: str,domain: str, \ + displayName: str, \ + summary: str, \ + imageFilename: str, \ + itemType: str, \ + itemCategory: str, \ + location: str, \ + duration: str, + debug: bool) -> None: + """Updates the likes collection within a post + """ + sharesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json' + sharesJson={} + if os.path.isfile(sharesFilename): + with open(sharesFilename, 'r') as fp: + sharesJson=commentjson.load(fp) + + duration=duration.lower() + durationSec=0 + published=int(time.time()) + if ' ' in duration: + durationList=duration.split(' ') + if durationList[0].isdigit(): + if 'hour' in durationList[1]: + durationSec=published+(int(durationList[0])*60*60) + if 'day' in durationList[1]: + durationSec=published+(int(durationList[0])*60*60*24) + if 'week' in durationList[1]: + durationSec=published+(int(durationList[0])*60*60*24*7) + if 'month' in durationList[1]: + durationSec=published+(int(durationList[0])*60*60*24*30) + if 'year' in durationList[1]: + durationSec=published+(int(durationList[0])*60*60*24*365) + + itemID=displayName.replace(' ','') + + imageUrl=None + if imageFilename: + if os.path.isfile(imageFilename): + if not os.path.isdir(baseDir+'/sharefiles'): + os.mkdir(baseDir+'/sharefiles') + itemIDfile=baseDir+'/sharefiles/'+str(published)+itemID + if imageFilename.endswidth('.png'): + copyfile(imageFilename,itemIDfile+'.png') + imageUrl='/sharefiles/'+str(published)+itemID+'.png' + if imageFilename.endswidth('.jpg'): + copyfile(imageFilename,itemIDfile+'.jpg') + imageUrl='/sharefiles/'+str(published)+itemID+'.jpg' + if imageFilename.endswidth('.gif'): + copyfile(imageFilename,itemIDfile+'.gif') + imageUrl='/sharefiles/'+str(published)+itemID+'.gif' + + sharesJson[itemID] = { + "displayName": displayName, + "summary": summary, + "imageUrl": imageUrl, + "type": itemType, + "category": category, + "location": location, + "published": published, + "expire": durationSec + } + + with open(sharesFilename, 'w') as fp: + commentjson.dump(sharesJson, fp, indent=4, sort_keys=True) + +def expireShares(baseDir: str,nickname: str,domain: str) -> None: + """Removes expired items from shares + """ + handleDomain=domain + if ':' in handleDomain: + handleDomain=domain.split(':')[0] + handle=nickname+'@'+handleDomain + sharesFilename=baseDir+'/accounts/'+handle+'/shares.json' + if os.path.isfile(sharesFilename): + with open(sharesFilename, 'r') as fp: + sharesJson=commentjson.load(fp) + currTime=int(time.time()) + deleteItemID=[] + for itemID,item in sharesJson.items(): + if currTime>item['expire']: + deleteItemID.append(itemID) + if deleteItemID: + for itemID in deleteItemID: + del sharesJson[itemID] + with open(sharesFilename, 'w') as fp: + commentjson.dump(sharesJson, fp, indent=4, sort_keys=True) + +def getSharesFeedForPerson(baseDir: str, \ + nickname: str,domain: str,port: int, \ + path: str,httpPrefix: str, \ + sharesPerPage=12) -> {}: + """Returns the shares for an account from GET requests + """ + if '/shares' not in path: + return None + # handle page numbers + headerOnly=True + pageNumber=None + if '?page=' in path: + pageNumber=path.split('?page=')[1] + if pageNumber=='true': + pageNumber=1 + else: + try: + pageNumber=int(pageNumber) + except: + pass + path=path.split('?page=')[0] + headerOnly=False + + if not path.endswith('/shares'): + return None + nickname=None + if path.startswith('/users/'): + nickname=path.replace('/users/','',1).replace('/shares','') + if path.startswith('/@'): + nickname=path.replace('/@','',1).replace('/shares','') + if not nickname: + return None + if not validNickname(nickname): + return None + + if port!=80 and port!=443: + domain=domain+':'+str(port) + + handleDomain=domain + if ':' in handleDomain: + handleDomain=domain.split(':')[0] + handle=nickname+'@'+handleDomain + sharesFilename=baseDir+'/accounts/'+handle+'/shares.json' + + if headerOnly: + noOfShares=0 + if os.path.isfile(sharesFilename): + with open(sharesFilename, 'r') as fp: + sharesJson=commentjson.load(fp) + noOfShares=len(sharesJson.items()) + shares = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page=1', + 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/shares', + 'totalItems': str(noOfShares), + 'type': 'OrderedCollection'} + return shares + + if not pageNumber: + pageNumber=1 + + nextPageNumber=int(pageNumber+1) + shares = { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page='+str(pageNumber), + 'orderedItems': [], + 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/shares', + 'totalItems': 0, + 'type': 'OrderedCollectionPage'} + + if not os.path.isfile(sharesFilename): + return shares + currPage=1 + pageCtr=0 + totalCtr=0 + + with open(sharesFilename, 'r') as fp: + sharesJson=commentjson.load(fp) + for itemID,item in sharesJson.items(): + pageCtr += 1 + totalCtr += 1 + if currPage==pageNumber: + shares['orderedItems'].append(item) + if pageCtr>=sharesPerPage: + pageCtr=0 + currPage += 1 + shares['totalItems']=totalCtr + lastPage=int(totalCtr/sharesPerPage) + if lastPage<1: + lastPage=1 + if nextPageNumber>lastPage: + shares['next']=httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page='+str(lastPage) + return shares diff --git a/webinterface.py b/webinterface.py index a5c9a5b03..c706f9560 100644 --- a/webinterface.py +++ b/webinterface.py @@ -98,6 +98,18 @@ def htmlProfileSkills(nickname: str,domain: str,skillsJson: {}) -> str: profileStr='
'+profileStr+'
' return profileStr +def htmlProfileShares(nickname: str,domain: str,sharesJson: {}) -> str: + """Shows shares on the profile screen + """ + profileStr='' + for item in sharesJson['orderedItems']: + profileStr+='
TODO

' + if len(profileStr)==0: + profileStr+='

@'+nickname+'@'+domain+' is not sharing any items

' + else: + profileStr='
' + return profileStr + def htmlProfile(baseDir: str,httpPrefix: str,authorized: bool, \ ocapAlways: bool,profileJson: {},selected: str, \ session,wfRequest: {},personCache: {}, \ @@ -175,6 +187,9 @@ def htmlProfile(baseDir: str,httpPrefix: str,authorized: bool, \ if selected=='skills': profileStr+= \ htmlProfileSkills(nickname,domainFull,extraJson) + if selected=='shares': + profileStr+= \ + htmlProfileShares(nickname,domainFull,extraJson) profileStr=htmlHeader(profileStyle)+profileStr+htmlFooter() return profileStr