diff --git a/blog.py b/blog.py new file mode 100644 index 00000000..8e8694c5 --- /dev/null +++ b/blog.py @@ -0,0 +1,260 @@ +__filename__ = "blog.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.1.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import json +import time +import os +from collections import OrderedDict +from datetime import datetime +from datetime import date +from dateutil.parser import parse +from shutil import copyfile +from shutil import copyfileobj +from pprint import pprint + +from content import replaceEmojiFromTags +from webinterface import htmlHeader +from webinterface import htmlFooter +from webinterface import addEmbeddedElements +from utils import getNicknameFromActor +from utils import getDomainFromActor +from posts import createBlogsTimeline + + +def htmlBlogPostContent(baseDir: str,httpPrefix: str,translate: {}, \ + nickname: str,domain: str,domainFull: str, \ + postJsonObject: {}, \ + handle: str,restrictToDomain: bool) -> str: + """Returns the content for a single blog post + """ + linkedAuthor=False + actor='' + blogStr='' + if postJsonObject['object'].get('summary'): + blogStr+='

'+postJsonObject['object']['summary']+'

\n' + + # get the handle of the author + if postJsonObject['object'].get('attributedTo'): + actor=postJsonObject['object']['attributedTo'] + authorNickname=getNicknameFromActor(actor) + if authorNickname: + authorDomain,authorPort=getDomainFromActor(actor) + if authorDomain: + # author must be from the given domain + if restrictToDomain and authorDomain != domain: + return '' + handle=authorNickname+'@'+authorDomain + else: + # posts from the domain are expected to have an attributedTo field + if restrictToDomain: + return '' + + if postJsonObject['object'].get('published'): + if 'T' in postJsonObject['object']['published']: + blogStr+='

'+postJsonObject['object']['published'].split('T')[0] + if handle: + if handle.startswith(nickname+'@'+domain): + blogStr+= \ + ' '+handle+'' + linkedAuthor=True + else: + if author: + blogStr+= \ + ' '+handle+'' + linkedAuthor=True + else: + blogStr+=' '+handle + blogStr+='

\n' + + if postJsonObject['object'].get('content'): + contentStr=addEmbeddedElements(translate,postJsonObject['object']['content']) + if postJsonObject['object'].get('tag'): + contentStr= \ + replaceEmojiFromTags(contentStr, \ + postJsonObject['object']['tag'],'content') + blogStr+='
'+contentStr+'\n' + + blogStr+='

\n' + if not linkedAuthor: + blogStr+= \ + '

'+translate['About the author']+'

\n' + return blogStr + + +def htmlBlogPost(baseDir: str,httpPrefix: str,translate: {}, \ + nickname: str,domainFull: str,postJsonObject: {}) -> str: + """Returns a html blog post + """ + blogStr='' + + cssFilename=baseDir+'/epicyon-blog.css' + if os.path.isfile(baseDir+'/blog.css'): + cssFilename=baseDir+'/blog.css' + with open(cssFilename, 'r') as cssFile: + blogCSS=cssFile.read() + blogStr=htmlHeader(cssFilename,blogCSS) + + blogStr+= \ + htmlBlogPostContent(baseDir,httpPrefix,translate, \ + nickname,domainFull,postJsonObject, \ + None,False) + + return blogStr+htmlFooter() + return None + + +def htmlBlogPage(session, \ + baseDir: str,httpPrefix: str,translate: {}, \ + nickname: str,domain: str,port: int, \ + noOfItems: int,pageNumber: int) -> str: + """Returns a html blog page containing posts + """ + if ' ' in nickname or '@' in nickname or '\n' in nickname: + return None + blogStr='' + + cssFilename=baseDir+'/epicyon-blog.css' + if os.path.isfile(baseDir+'/blog.css'): + cssFilename=baseDir+'/blog.css' + with open(cssFilename, 'r') as cssFile: + blogCSS=cssFile.read() + blogStr=htmlHeader(cssFilename,blogCSS) + + blogsIndex= \ + baseDir+'/accounts/'+nickname+'@'+domain+'/tlblogs.index' + if not os.path.isfile(blogsIndex): + return blogStr+htmlFooter() + + if pageNumber: + if pageNumber>1: + # show previous button + print('TODO previous') + # show next button + print('TODO next') + + timelineJson= \ + createBlogsTimeline(session,baseDir, \ + nickname,domain,port,httpPrefix, \ + noOfItems,False,False,pageNumber) + + if not timelineJson: + return blogStr+htmlFooter() + + domainFull=domain + if port: + if port!=80 and port!=443: + domainFull=domain+':'+str(port) + + for item in timelineJson['orderedItems']: + if item['type']!='Create': + continue + + blogStr+= \ + htmlBlogPostContent(baseDir,httpPrefix,translate, \ + nickname,domainFull,item, \ + None,True) + + return blogStr+htmlFooter() + return None + + +def getBlogIndexesForAccounts(baseDir: str) -> {}: + """ Get the index files for blogs for each account + and add them to a dict + """ + blogIndexes={} + for subdir, dirs, files in os.walk(baseDir+'/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct: + continue + accountDir=os.path.join(baseDir+'/accounts', acct) + blogsIndex=accountDir+'/tlblogs.index' + if os.path.isfile(blogsIndex): + blogIndexes[acct]=blogsIndex + return blogIndexes + +def noOfBlogAccounts(baseDir: str) -> int: + """Returns the number of blog accounts + """ + ctr=0 + for subdir, dirs, files in os.walk(baseDir+'/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' not in acct: + continue + accountDir=os.path.join(baseDir+'/accounts', acct) + blogsIndex=accountDir+'/tlblogs.index' + if os.path.isfile(blogsIndex): + ctr+=1 + return ctr + +def singleBlogAccountNickname(baseDir: str) -> str: + """Returns the nickname of a single blog account + """ + for subdir, dirs, files in os.walk(baseDir+'/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct: + continue + accountDir=os.path.join(baseDir+'/accounts', acct) + blogsIndex=accountDir+'/tlblogs.index' + if os.path.isfile(blogsIndex): + return acct.split('@')[0] + return None + +def htmlBlogView(session,baseDir: str,httpPrefix: str, \ + translate: {},domain: str,port: int, \ + noOfItems: int) -> str: + """Show the blog main page + """ + blogStr='' + + cssFilename=baseDir+'/epicyon-blog.css' + if os.path.isfile(baseDir+'/blog.css'): + cssFilename=baseDir+'/blog.css' + with open(cssFilename, 'r') as cssFile: + blogCSS=cssFile.read() + blogStr=htmlHeader(cssFilename,blogCSS) + + if noOfBlogAccounts(baseDir) <= 1: + nickname=singleBlogAccountNickname(baseDir) + if nickname: + return htmlBlogPage(session, \ + baseDir,httpPrefix,translate, \ + nickname,domain,port, \ + noOfItems,1) + + domainFull=domain + if port: + if port!=80 and port!=443: + domainFull=domain+':'+str(port) + + for subdir, dirs, files in os.walk(baseDir+'/accounts'): + for acct in dirs: + if '@' not in acct: + continue + if 'inbox@' in acct: + continue + accountDir=os.path.join(baseDir+'/accounts', acct) + blogsIndex=accountDir+'/tlblogs.index' + if os.path.isfile(blogsIndex): + blogStr+='

' + blogStr+= \ + ''+acct+'' + blogStr+='

' + + return blogStr+htmlFooter() + return None diff --git a/daemon.py b/daemon.py index 3bd4a1bc..ed752373 100644 --- a/daemon.py +++ b/daemon.py @@ -108,7 +108,9 @@ from roles import setRole from roles import clearModeratorStatus from skills import outboxSkills from availability import outboxAvailability -from webinterface import htmlBlogPost +from blog import htmlBlogView +from blog import htmlBlogPage +from blog import htmlBlogPost from webinterface import htmlCalendarDeleteConfirm from webinterface import htmlDeletePost from webinterface import htmlAbout @@ -995,7 +997,61 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime,GETtimings,8) - if htmlGET and '/users/' in self.path: + # show the main blog page + if htmlGET and (self.path=='/blog' or self.path=='/blogs'): + if not self.server.session: + self.server.session= \ + createSession(self.server.useTor) + msg=htmlBlogView(self.server.session, \ + self.server.baseDir, \ + self.server.httpPrefix, \ + self.server.translate, \ + self.server.domain,self.server.port, \ + maxPostsInBlogsFeed) + if msg!=None: + msg=msg.encode() + self._set_headers('text/html',len(msg),cookie) + self._write(msg) + return + self._404() + return + + if htmlGET and self.path.startswith('/blog/'): + pageNumber=1 + nickname=self.path.split('/blog/')[1] + if '/' in nickname: + nickname=nickname.split('/')[0] + if '?' in nickname: + nickname=nickname.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) + if pageNumber<1: + pageNumber=1 + elif pageNumber>10: + pageNumber=10 + if not self.server.session: + self.server.session= \ + createSession(self.server.useTor) + msg=htmlBlogPage(self.server.session, \ + self.server.baseDir, \ + self.server.httpPrefix, \ + self.server.translate, \ + nickname, \ + self.server.domain,self.server.port, \ + maxPostsInBlogsFeed,pageNumber) + if msg!=None: + msg=msg.encode() + self._set_headers('text/html',len(msg),cookie) + self._write(msg) + return + self._404() + return + + if htmlGET and '/users/' in self.path: # show the person options screen with view/follow/block/report if '?options=' in self.path: optionsStr=self.path.split('?options=')[1] @@ -1040,28 +1096,28 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(originPathStrAbsolute,cookie) return - # show blog post - blogFilename,nickname= \ - self._pathContainsBlogLink(self.server.baseDir, \ - self.server.httpPrefix, \ - self.server.domain, \ - self.server.domainFull, \ - self.path) - if blogFilename and nickname: - postJsonObject=loadJson(blogFilename) - if isBlogPost(postJsonObject): - msg=htmlBlogPost(self.server.baseDir, \ - self.server.httpPrefix, \ - self.server.translate, \ - nickname,self.server.domain, \ - postJsonObject) - if msg!=None: - msg=msg.encode() - self._set_headers('text/html',len(msg),cookie) - self._write(msg) - return - self._404() - return + # show blog post + blogFilename,nickname= \ + self._pathContainsBlogLink(self.server.baseDir, \ + self.server.httpPrefix, \ + self.server.domain, \ + self.server.domainFull, \ + self.path) + if blogFilename and nickname: + postJsonObject=loadJson(blogFilename) + if isBlogPost(postJsonObject): + msg=htmlBlogPost(self.server.baseDir, \ + self.server.httpPrefix, \ + self.server.translate, \ + nickname,self.server.domain, \ + postJsonObject) + if msg!=None: + msg=msg.encode() + self._set_headers('text/html',len(msg),cookie) + self._write(msg) + return + self._404() + return self._benchmarkGETtimings(GETstartTime,GETtimings,9) diff --git a/person.py b/person.py index fc8ca963..40dc7cd9 100644 --- a/person.py +++ b/person.py @@ -466,7 +466,7 @@ def personLookup(domain: str,path: str,baseDir: str) -> {}: def personBoxJson(recentPostsCache: {}, \ session,baseDir: str,domain: str,port: int,path: str, \ httpPrefix: str,noOfItems: int,boxname: str, \ - authorized: bool,ocapAlways: bool) -> []: + authorized: bool,ocapAlways: bool) -> {}: """Obtain the inbox/outbox/moderation feed for the given person """ if boxname!='inbox' and boxname!='dm' and \ diff --git a/webinterface.py b/webinterface.py index 82ec8503..db3ff7c5 100644 --- a/webinterface.py +++ b/webinterface.py @@ -5011,38 +5011,3 @@ def htmlProfileAfterSearch(recentPostsCache: {},maxRecentPosts: int, \ return htmlHeader(cssFilename,profileStyle)+profileStr+htmlFooter() -def htmlBlogPost(baseDir: str,httpPrefix: str,translate: {}, \ - nickname: str,domain: str,postJsonObject: {}) -> str: - """Returns a html blog post - """ - blogStr='' - - cssFilename=baseDir+'/epicyon-blog.css' - if os.path.isfile(baseDir+'/blog.css'): - cssFilename=baseDir+'/blog.css' - with open(cssFilename, 'r') as cssFile: - blogCSS=cssFile.read() - blogStr=htmlHeader(cssFilename,blogCSS) - - if postJsonObject['object'].get('summary'): - blogStr+='

'+postJsonObject['object']['summary']+'

\n' - - if postJsonObject['object'].get('published'): - if 'T' in postJsonObject['object']['published']: - blogStr+='

'+postJsonObject['object']['published'].split('T')[0]+'

\n' - - if postJsonObject['object'].get('content'): - contentStr=addEmbeddedElements(translate,postJsonObject['object']['content']) - if postJsonObject['object'].get('tag'): - contentStr= \ - replaceEmojiFromTags(contentStr, \ - postJsonObject['object']['tag'],'content') - blogStr+='
'+contentStr+'\n' - - blogStr+='

\n' - blogStr+= \ - '

'+translate['About the author']+'

\n' - - return blogStr+htmlFooter() - return None