mirror of https://gitlab.com/bashrc2/epicyon
				
				
				
			
		
			
				
	
	
		
			914 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			914 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
	
| __filename__ = "blog.py"
 | |
| __author__ = "Bob Mottram"
 | |
| __license__ = "AGPL3+"
 | |
| __version__ = "1.2.0"
 | |
| __maintainer__ = "Bob Mottram"
 | |
| __email__ = "bob@freedombone.net"
 | |
| __status__ = "Production"
 | |
| __module_group__ = "ActivityPub"
 | |
| 
 | |
| import os
 | |
| from datetime import datetime
 | |
| 
 | |
| from content import replaceEmojiFromTags
 | |
| from webapp_utils import htmlHeaderWithExternalStyle
 | |
| from webapp_utils import htmlHeaderWithBlogMarkup
 | |
| from webapp_utils import htmlFooter
 | |
| from webapp_utils import getPostAttachmentsAsHtml
 | |
| from webapp_utils import editTextArea
 | |
| from webapp_media import addEmbeddedElements
 | |
| from utils import getActorLanguagesList
 | |
| from utils import getBaseContentFromPost
 | |
| from utils import getContentFromPost
 | |
| from utils import isAccountDir
 | |
| from utils import removeHtml
 | |
| from utils import getConfigParam
 | |
| from utils import getFullDomain
 | |
| from utils import getMediaFormats
 | |
| from utils import getNicknameFromActor
 | |
| from utils import getDomainFromActor
 | |
| from utils import locatePost
 | |
| from utils import loadJson
 | |
| from utils import firstParagraphFromString
 | |
| from utils import getActorPropertyUrl
 | |
| from utils import acctDir
 | |
| from posts import createBlogsTimeline
 | |
| from newswire import rss2Header
 | |
| from newswire import rss2Footer
 | |
| from cache import getPersonFromCache
 | |
| 
 | |
| 
 | |
| def _noOfBlogReplies(baseDir: str, httpPrefix: str, translate: {},
 | |
|                      nickname: str, domain: str, domainFull: str,
 | |
|                      postId: str, depth=0) -> int:
 | |
|     """Returns the number of replies on the post
 | |
|     This is recursive, so can handle replies to replies
 | |
|     """
 | |
|     if depth > 4:
 | |
|         return 0
 | |
|     if not postId:
 | |
|         return 0
 | |
| 
 | |
|     tryPostBox = ('tlblogs', 'inbox', 'outbox')
 | |
|     boxFound = False
 | |
|     for postBox in tryPostBox:
 | |
|         postFilename = \
 | |
|             acctDir(baseDir, nickname, domain) + '/' + postBox + '/' + \
 | |
|             postId.replace('/', '#') + '.replies'
 | |
|         if os.path.isfile(postFilename):
 | |
|             boxFound = True
 | |
|             break
 | |
|     if not boxFound:
 | |
|         # post may exist but has no replies
 | |
|         for postBox in tryPostBox:
 | |
|             postFilename = \
 | |
|                 acctDir(baseDir, nickname, domain) + '/' + postBox + '/' + \
 | |
|                 postId.replace('/', '#')
 | |
|             if os.path.isfile(postFilename):
 | |
|                 return 1
 | |
|         return 0
 | |
| 
 | |
|     removals = []
 | |
|     replies = 0
 | |
|     lines = []
 | |
|     with open(postFilename, 'r') as f:
 | |
|         lines = f.readlines()
 | |
|         for replyPostId in lines:
 | |
|             replyPostId = replyPostId.replace('\n', '').replace('\r', '')
 | |
|             replyPostId = replyPostId.replace('.json', '')
 | |
|             if locatePost(baseDir, nickname, domain, replyPostId):
 | |
|                 replyPostId = replyPostId.replace('.replies', '')
 | |
|                 replies += \
 | |
|                     1 + _noOfBlogReplies(baseDir, httpPrefix, translate,
 | |
|                                          nickname, domain, domainFull,
 | |
|                                          replyPostId, depth+1)
 | |
|             else:
 | |
|                 # remove post which no longer exists
 | |
|                 removals.append(replyPostId)
 | |
| 
 | |
|     # remove posts from .replies file if they don't exist
 | |
|     if lines and removals:
 | |
|         print('Rewriting ' + postFilename + ' to remove ' +
 | |
|               str(len(removals)) + ' entries')
 | |
|         with open(postFilename, 'w+') as f:
 | |
|             for replyPostId in lines:
 | |
|                 replyPostId = replyPostId.replace('\n', '').replace('\r', '')
 | |
|                 if replyPostId not in removals:
 | |
|                     f.write(replyPostId + '\n')
 | |
| 
 | |
|     return replies
 | |
| 
 | |
| 
 | |
| def _getBlogReplies(baseDir: str, httpPrefix: str, translate: {},
 | |
|                     nickname: str, domain: str, domainFull: str,
 | |
|                     postId: str, depth=0) -> str:
 | |
|     """Returns a string containing html blog posts
 | |
|     """
 | |
|     if depth > 4:
 | |
|         return ''
 | |
|     if not postId:
 | |
|         return ''
 | |
| 
 | |
|     tryPostBox = ('tlblogs', 'inbox', 'outbox')
 | |
|     boxFound = False
 | |
|     for postBox in tryPostBox:
 | |
|         postFilename = \
 | |
|             acctDir(baseDir, nickname, domain) + '/' + postBox + '/' + \
 | |
|             postId.replace('/', '#') + '.replies'
 | |
|         if os.path.isfile(postFilename):
 | |
|             boxFound = True
 | |
|             break
 | |
|     if not boxFound:
 | |
|         # post may exist but has no replies
 | |
|         for postBox in tryPostBox:
 | |
|             postFilename = \
 | |
|                 acctDir(baseDir, nickname, domain) + '/' + postBox + '/' + \
 | |
|                 postId.replace('/', '#') + '.json'
 | |
|             if os.path.isfile(postFilename):
 | |
|                 postFilename = acctDir(baseDir, nickname, domain) + \
 | |
|                     '/postcache/' + \
 | |
|                     postId.replace('/', '#') + '.html'
 | |
|                 if os.path.isfile(postFilename):
 | |
|                     with open(postFilename, 'r') as postFile:
 | |
|                         return postFile.read() + '\n'
 | |
|         return ''
 | |
| 
 | |
|     with open(postFilename, 'r') as f:
 | |
|         lines = f.readlines()
 | |
|         repliesStr = ''
 | |
|         for replyPostId in lines:
 | |
|             replyPostId = replyPostId.replace('\n', '').replace('\r', '')
 | |
|             replyPostId = replyPostId.replace('.json', '')
 | |
|             replyPostId = replyPostId.replace('.replies', '')
 | |
|             postFilename = acctDir(baseDir, nickname, domain) + \
 | |
|                 '/postcache/' + \
 | |
|                 replyPostId.replace('/', '#') + '.html'
 | |
|             if not os.path.isfile(postFilename):
 | |
|                 continue
 | |
|             with open(postFilename, 'r') as postFile:
 | |
|                 repliesStr += postFile.read() + '\n'
 | |
|             rply = _getBlogReplies(baseDir, httpPrefix, translate,
 | |
|                                    nickname, domain, domainFull,
 | |
|                                    replyPostId, depth+1)
 | |
|             if rply not in repliesStr:
 | |
|                 repliesStr += rply
 | |
| 
 | |
|         # indicate the reply indentation level
 | |
|         indentStr = '>'
 | |
|         for indentLevel in range(depth):
 | |
|             indentStr += ' >'
 | |
| 
 | |
|         repliesStr = repliesStr.replace(translate['SHOW MORE'], indentStr)
 | |
|         return repliesStr.replace('?tl=outbox', '?tl=tlblogs')
 | |
|     return ''
 | |
| 
 | |
| 
 | |
| def _htmlBlogPostContent(authorized: bool,
 | |
|                          baseDir: str, httpPrefix: str, translate: {},
 | |
|                          nickname: str, domain: str, domainFull: str,
 | |
|                          postJsonObject: {},
 | |
|                          handle: str, restrictToDomain: bool,
 | |
|                          peertubeInstances: [],
 | |
|                          systemLanguage: str,
 | |
|                          personCache: {},
 | |
|                          blogSeparator: str = '<hr>') -> str:
 | |
|     """Returns the content for a single blog post
 | |
|     """
 | |
|     linkedAuthor = False
 | |
|     actor = ''
 | |
|     blogStr = ''
 | |
|     messageLink = ''
 | |
|     if postJsonObject['object'].get('id'):
 | |
|         messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
 | |
|     titleStr = ''
 | |
|     articleAdded = False
 | |
|     if postJsonObject['object'].get('summary'):
 | |
|         titleStr = postJsonObject['object']['summary']
 | |
|         blogStr += '<article><h1><a href="' + messageLink + '">' + \
 | |
|             titleStr + '</a></h1>\n'
 | |
|         articleAdded = True
 | |
| 
 | |
|     # get the handle of the author
 | |
|     if postJsonObject['object'].get('attributedTo'):
 | |
|         authorNickname = None
 | |
|         if isinstance(postJsonObject['object']['attributedTo'], str):
 | |
|             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 += '<h3>' + \
 | |
|                 postJsonObject['object']['published'].split('T')[0]
 | |
|             if handle:
 | |
|                 if handle.startswith(nickname + '@' + domain):
 | |
|                     blogStr += ' <a href="' + httpPrefix + '://' + \
 | |
|                         domainFull + \
 | |
|                         '/users/' + nickname + '">' + handle + '</a>'
 | |
|                     linkedAuthor = True
 | |
|                 else:
 | |
|                     if actor:
 | |
|                         blogStr += ' <a href="' + actor + '">' + \
 | |
|                             handle + '</a>'
 | |
|                         linkedAuthor = True
 | |
|                     else:
 | |
|                         blogStr += ' ' + handle
 | |
|             blogStr += '</h3>\n'
 | |
| 
 | |
|     avatarLink = ''
 | |
|     replyStr = ''
 | |
|     announceStr = ''
 | |
|     likeStr = ''
 | |
|     bookmarkStr = ''
 | |
|     deleteStr = ''
 | |
|     muteStr = ''
 | |
|     isMuted = False
 | |
|     attachmentStr, galleryStr = getPostAttachmentsAsHtml(postJsonObject,
 | |
|                                                          'tlblogs', translate,
 | |
|                                                          isMuted, avatarLink,
 | |
|                                                          replyStr, announceStr,
 | |
|                                                          likeStr, bookmarkStr,
 | |
|                                                          deleteStr, muteStr)
 | |
|     if attachmentStr:
 | |
|         blogStr += '<br><center>' + attachmentStr + '</center>'
 | |
| 
 | |
|     personUrl = \
 | |
|         httpPrefix + '://' + domainFull + '/users/' + nickname
 | |
|     actorJson = \
 | |
|         getPersonFromCache(baseDir, personUrl, personCache, False)
 | |
|     languagesUnderstood = []
 | |
|     if actorJson:
 | |
|         languagesUnderstood = getActorLanguagesList(actorJson)
 | |
|     jsonContent = getContentFromPost(postJsonObject, systemLanguage,
 | |
|                                      languagesUnderstood)
 | |
|     if jsonContent:
 | |
|         contentStr = addEmbeddedElements(translate, jsonContent,
 | |
|                                          peertubeInstances)
 | |
|         if postJsonObject['object'].get('tag'):
 | |
|             contentStr = replaceEmojiFromTags(contentStr,
 | |
|                                               postJsonObject['object']['tag'],
 | |
|                                               'content')
 | |
|         if articleAdded:
 | |
|             blogStr += '<br>' + contentStr + '</article>\n'
 | |
|         else:
 | |
|             blogStr += '<br><article>' + contentStr + '</article>\n'
 | |
| 
 | |
|     citationsStr = ''
 | |
|     if postJsonObject['object'].get('tag'):
 | |
|         for tagJson in postJsonObject['object']['tag']:
 | |
|             if not isinstance(tagJson, dict):
 | |
|                 continue
 | |
|             if not tagJson.get('type'):
 | |
|                 continue
 | |
|             if tagJson['type'] != 'Article':
 | |
|                 continue
 | |
|             if not tagJson.get('name'):
 | |
|                 continue
 | |
|             if not tagJson.get('url'):
 | |
|                 continue
 | |
|             citationsStr += \
 | |
|                 '<li><a href="' + tagJson['url'] + '">' + \
 | |
|                 '<cite>' + tagJson['name'] + '</cite></a></li>\n'
 | |
|         if citationsStr:
 | |
|             citationsStr = '<p><b>' + translate['Citations'] + \
 | |
|                 ':</b></p>' + \
 | |
|                 '<ul>\n' + citationsStr + '</ul>\n'
 | |
| 
 | |
|     blogStr += '<br>\n' + citationsStr
 | |
| 
 | |
|     if not linkedAuthor:
 | |
|         blogStr += '<p class="about"><a class="about" href="' + \
 | |
|             httpPrefix + '://' + domainFull + \
 | |
|             '/users/' + nickname + '">' + translate['About the author'] + \
 | |
|             '</a></p>\n'
 | |
| 
 | |
|     replies = _noOfBlogReplies(baseDir, httpPrefix, translate,
 | |
|                                nickname, domain, domainFull,
 | |
|                                postJsonObject['object']['id'])
 | |
| 
 | |
|     # separator between blogs should be centered
 | |
|     if '<center>' not in blogSeparator:
 | |
|         blogSeparator = '<center>' + blogSeparator + '</center>'
 | |
| 
 | |
|     if replies == 0:
 | |
|         blogStr += blogSeparator + '\n'
 | |
|         return blogStr
 | |
| 
 | |
|     if not authorized:
 | |
|         blogStr += '<p class="blogreplies">' + \
 | |
|             translate['Replies'].lower() + ': ' + str(replies) + '</p>'
 | |
|         blogStr += '<br><br><br>' + blogSeparator + '\n'
 | |
|     else:
 | |
|         blogStr += blogSeparator + '<h1>' + translate['Replies'] + '</h1>\n'
 | |
|         if not titleStr:
 | |
|             blogStr += _getBlogReplies(baseDir, httpPrefix, translate,
 | |
|                                        nickname, domain, domainFull,
 | |
|                                        postJsonObject['object']['id'])
 | |
|         else:
 | |
|             blogRepliesStr = _getBlogReplies(baseDir, httpPrefix, translate,
 | |
|                                              nickname, domain, domainFull,
 | |
|                                              postJsonObject['object']['id'])
 | |
|             blogStr += blogRepliesStr.replace('>' + titleStr + '<', '')
 | |
| 
 | |
|     return blogStr
 | |
| 
 | |
| 
 | |
| def _htmlBlogPostRSS2(authorized: bool,
 | |
|                       baseDir: str, httpPrefix: str, translate: {},
 | |
|                       nickname: str, domain: str, domainFull: str,
 | |
|                       postJsonObject: {},
 | |
|                       handle: str, restrictToDomain: bool,
 | |
|                       systemLanguage: str) -> str:
 | |
|     """Returns the RSS version 2 feed for a single blog post
 | |
|     """
 | |
|     rssStr = ''
 | |
|     messageLink = ''
 | |
|     if postJsonObject['object'].get('id'):
 | |
|         messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
 | |
|         if not restrictToDomain or \
 | |
|            (restrictToDomain and '/' + domain in messageLink):
 | |
|             if postJsonObject['object'].get('summary') and \
 | |
|                postJsonObject['object'].get('published'):
 | |
|                 published = postJsonObject['object']['published']
 | |
|                 pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
 | |
|                 titleStr = postJsonObject['object']['summary']
 | |
|                 rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")
 | |
|                 content = \
 | |
|                     getBaseContentFromPost(postJsonObject, systemLanguage)
 | |
|                 description = firstParagraphFromString(content)
 | |
|                 rssStr = '     <item>'
 | |
|                 rssStr += '         <title>' + titleStr + '</title>'
 | |
|                 rssStr += '         <link>' + messageLink + '</link>'
 | |
|                 rssStr += \
 | |
|                     '         <description>' + description + '</description>'
 | |
|                 rssStr += '         <pubDate>' + rssDateStr + '</pubDate>'
 | |
|                 rssStr += '     </item>'
 | |
|     return rssStr
 | |
| 
 | |
| 
 | |
| def _htmlBlogPostRSS3(authorized: bool,
 | |
|                       baseDir: str, httpPrefix: str, translate: {},
 | |
|                       nickname: str, domain: str, domainFull: str,
 | |
|                       postJsonObject: {},
 | |
|                       handle: str, restrictToDomain: bool,
 | |
|                       systemLanguage: str) -> str:
 | |
|     """Returns the RSS version 3 feed for a single blog post
 | |
|     """
 | |
|     rssStr = ''
 | |
|     messageLink = ''
 | |
|     if postJsonObject['object'].get('id'):
 | |
|         messageLink = postJsonObject['object']['id'].replace('/statuses/', '/')
 | |
|         if not restrictToDomain or \
 | |
|            (restrictToDomain and '/' + domain in messageLink):
 | |
|             if postJsonObject['object'].get('summary') and \
 | |
|                postJsonObject['object'].get('published'):
 | |
|                 published = postJsonObject['object']['published']
 | |
|                 pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ")
 | |
|                 titleStr = postJsonObject['object']['summary']
 | |
|                 rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")
 | |
|                 content = \
 | |
|                     getBaseContentFromPost(postJsonObject, systemLanguage)
 | |
|                 description = firstParagraphFromString(content)
 | |
|                 rssStr = 'title: ' + titleStr + '\n'
 | |
|                 rssStr += 'link: ' + messageLink + '\n'
 | |
|                 rssStr += 'description: ' + description + '\n'
 | |
|                 rssStr += 'created: ' + rssDateStr + '\n\n'
 | |
|     return rssStr
 | |
| 
 | |
| 
 | |
| def _htmlBlogRemoveCwButton(blogStr: str, translate: {}) -> str:
 | |
|     """Removes the CW button from blog posts, where the
 | |
|     summary field is instead used as the blog title
 | |
|     """
 | |
|     blogStr = blogStr.replace('<details>', '<b>')
 | |
|     blogStr = blogStr.replace('</details>', '</b>')
 | |
|     blogStr = blogStr.replace('<summary>', '')
 | |
|     blogStr = blogStr.replace('</summary>', '')
 | |
|     blogStr = blogStr.replace(translate['SHOW MORE'], '')
 | |
|     return blogStr
 | |
| 
 | |
| 
 | |
| def _getSnippetFromBlogContent(postJsonObject: {}, systemLanguage: str) -> str:
 | |
|     """Returns a snippet of text from the blog post as a preview
 | |
|     """
 | |
|     content = getBaseContentFromPost(postJsonObject, systemLanguage)
 | |
|     if '<p>' in content:
 | |
|         content = content.split('<p>', 1)[1]
 | |
|         if '</p>' in content:
 | |
|             content = content.split('</p>', 1)[0]
 | |
|     content = removeHtml(content)
 | |
|     if '\n' in content:
 | |
|         content = content.split('\n')[0]
 | |
|     if len(content) >= 256:
 | |
|         content = content[:252] + '...'
 | |
|     return content
 | |
| 
 | |
| 
 | |
| def htmlBlogPost(authorized: bool,
 | |
|                  baseDir: str, httpPrefix: str, translate: {},
 | |
|                  nickname: str, domain: str, domainFull: str,
 | |
|                  postJsonObject: {},
 | |
|                  peertubeInstances: [],
 | |
|                  systemLanguage: str, personCache: {}) -> str:
 | |
|     """Returns a html blog post
 | |
|     """
 | |
|     blogStr = ''
 | |
| 
 | |
|     cssFilename = baseDir + '/epicyon-blog.css'
 | |
|     if os.path.isfile(baseDir + '/blog.css'):
 | |
|         cssFilename = baseDir + '/blog.css'
 | |
|     instanceTitle = \
 | |
|         getConfigParam(baseDir, 'instanceTitle')
 | |
|     published = postJsonObject['object']['published']
 | |
|     title = postJsonObject['object']['summary']
 | |
|     snippet = _getSnippetFromBlogContent(postJsonObject, systemLanguage)
 | |
|     blogStr = htmlHeaderWithBlogMarkup(cssFilename, instanceTitle,
 | |
|                                        httpPrefix, domainFull, nickname,
 | |
|                                        systemLanguage, published,
 | |
|                                        title, snippet)
 | |
|     _htmlBlogRemoveCwButton(blogStr, translate)
 | |
| 
 | |
|     blogStr += _htmlBlogPostContent(authorized, baseDir,
 | |
|                                     httpPrefix, translate,
 | |
|                                     nickname, domain,
 | |
|                                     domainFull, postJsonObject,
 | |
|                                     None, False,
 | |
|                                     peertubeInstances, systemLanguage,
 | |
|                                     personCache)
 | |
| 
 | |
|     # show rss links
 | |
|     blogStr += '<p class="rssfeed">'
 | |
| 
 | |
|     blogStr += '<a href="' + httpPrefix + '://' + \
 | |
|         domainFull + '/blog/' + nickname + '/rss.xml">'
 | |
|     blogStr += '<img style="width:3%;min-width:50px" ' + \
 | |
|         'loading="lazy" alt="RSS 2.0" ' + \
 | |
|         'title="RSS 2.0" src="/' + \
 | |
|         'icons/logorss.png" /></a>'
 | |
| 
 | |
|     # blogStr += '<a href="' + httpPrefix + '://' + \
 | |
|     #     domainFull + '/blog/' + nickname + '/rss.txt">'
 | |
|     # blogStr += '<img style="width:3%;min-width:50px" ' + \
 | |
|     #     'loading="lazy" alt="RSS 3.0" ' + \
 | |
|     #     'title="RSS 3.0" src="/' + \
 | |
|     #     'icons/rss3.png" /></a>'
 | |
| 
 | |
|     blogStr += '</p>'
 | |
| 
 | |
|     return blogStr + htmlFooter()
 | |
| 
 | |
| 
 | |
| def htmlBlogPage(authorized: bool, session,
 | |
|                  baseDir: str, httpPrefix: str, translate: {},
 | |
|                  nickname: str, domain: str, port: int,
 | |
|                  noOfItems: int, pageNumber: int,
 | |
|                  peertubeInstances: [], systemLanguage: str,
 | |
|                  personCache: {}) -> str:
 | |
|     """Returns a html blog page containing posts
 | |
|     """
 | |
|     if ' ' in nickname or '@' in nickname or \
 | |
|        '\n' in nickname or '\r' in nickname:
 | |
|         return None
 | |
|     blogStr = ''
 | |
| 
 | |
|     cssFilename = baseDir + '/epicyon-profile.css'
 | |
|     if os.path.isfile(baseDir + '/epicyon.css'):
 | |
|         cssFilename = baseDir + '/epicyon.css'
 | |
|     instanceTitle = \
 | |
|         getConfigParam(baseDir, 'instanceTitle')
 | |
|     blogStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
 | |
|     _htmlBlogRemoveCwButton(blogStr, translate)
 | |
| 
 | |
|     blogsIndex = acctDir(baseDir, nickname, domain) + '/tlblogs.index'
 | |
|     if not os.path.isfile(blogsIndex):
 | |
|         return blogStr + htmlFooter()
 | |
| 
 | |
|     timelineJson = createBlogsTimeline(session, baseDir,
 | |
|                                        nickname, domain, port,
 | |
|                                        httpPrefix,
 | |
|                                        noOfItems, False,
 | |
|                                        pageNumber)
 | |
| 
 | |
|     if not timelineJson:
 | |
|         return blogStr + htmlFooter()
 | |
| 
 | |
|     domainFull = getFullDomain(domain, port)
 | |
| 
 | |
|     # show previous and next buttons
 | |
|     if pageNumber is not None:
 | |
|         navigateStr = '<p>'
 | |
|         if pageNumber > 1:
 | |
|             # show previous button
 | |
|             navigateStr += '<a href="' + httpPrefix + '://' + \
 | |
|                 domainFull + '/blog/' + \
 | |
|                 nickname + '?page=' + str(pageNumber-1) + '">' + \
 | |
|                 '<img loading="lazy" alt="<" title="<" ' + \
 | |
|                 'src="/icons' + \
 | |
|                 '/prev.png" class="buttonprev"/></a>\n'
 | |
|         if len(timelineJson['orderedItems']) >= noOfItems:
 | |
|             # show next button
 | |
|             navigateStr += '<a href="' + httpPrefix + '://' + \
 | |
|                 domainFull + '/blog/' + nickname + \
 | |
|                 '?page=' + str(pageNumber + 1) + '">' + \
 | |
|                 '<img loading="lazy" alt=">" title=">" ' + \
 | |
|                 'src="/icons' + \
 | |
|                 '/prev.png" class="buttonnext"/></a>\n'
 | |
|         navigateStr += '</p>'
 | |
|         blogStr += navigateStr
 | |
| 
 | |
|     for item in timelineJson['orderedItems']:
 | |
|         if item['type'] != 'Create':
 | |
|             continue
 | |
| 
 | |
|         blogStr += _htmlBlogPostContent(authorized, baseDir,
 | |
|                                         httpPrefix, translate,
 | |
|                                         nickname, domain,
 | |
|                                         domainFull, item,
 | |
|                                         None, True,
 | |
|                                         peertubeInstances,
 | |
|                                         systemLanguage,
 | |
|                                         personCache)
 | |
| 
 | |
|     if len(timelineJson['orderedItems']) >= noOfItems:
 | |
|         blogStr += navigateStr
 | |
| 
 | |
|     # show rss link
 | |
|     blogStr += '<p class="rssfeed">'
 | |
| 
 | |
|     blogStr += '<a href="' + httpPrefix + '://' + \
 | |
|         domainFull + '/blog/' + nickname + '/rss.xml">'
 | |
|     blogStr += '<img loading="lazy" alt="RSS 2.0" ' + \
 | |
|         'title="RSS 2.0" src="/' + \
 | |
|         'icons/logorss.png" /></a>'
 | |
| 
 | |
|     # blogStr += '<a href="' + httpPrefix + '://' + \
 | |
|     #     domainFull + '/blog/' + nickname + '/rss.txt">'
 | |
|     # blogStr += '<img loading="lazy" alt="RSS 3.0" ' + \
 | |
|     #     'title="RSS 3.0" src="/' + \
 | |
|     #     'icons/rss3.png" /></a>'
 | |
| 
 | |
|     blogStr += '</p>'
 | |
|     return blogStr + htmlFooter()
 | |
| 
 | |
| 
 | |
| def htmlBlogPageRSS2(authorized: bool, session,
 | |
|                      baseDir: str, httpPrefix: str, translate: {},
 | |
|                      nickname: str, domain: str, port: int,
 | |
|                      noOfItems: int, pageNumber: int,
 | |
|                      includeHeader: bool, systemLanguage: str) -> str:
 | |
|     """Returns an RSS version 2 feed containing posts
 | |
|     """
 | |
|     if ' ' in nickname or '@' in nickname or \
 | |
|        '\n' in nickname or '\r' in nickname:
 | |
|         return None
 | |
| 
 | |
|     domainFull = getFullDomain(domain, port)
 | |
| 
 | |
|     blogRSS2 = ''
 | |
|     if includeHeader:
 | |
|         blogRSS2 = rss2Header(httpPrefix, nickname, domainFull,
 | |
|                               'Blog', translate)
 | |
| 
 | |
|     blogsIndex = acctDir(baseDir, nickname, domain) + '/tlblogs.index'
 | |
|     if not os.path.isfile(blogsIndex):
 | |
|         if includeHeader:
 | |
|             return blogRSS2 + rss2Footer()
 | |
|         else:
 | |
|             return blogRSS2
 | |
| 
 | |
|     timelineJson = createBlogsTimeline(session, baseDir,
 | |
|                                        nickname, domain, port,
 | |
|                                        httpPrefix,
 | |
|                                        noOfItems, False,
 | |
|                                        pageNumber)
 | |
| 
 | |
|     if not timelineJson:
 | |
|         if includeHeader:
 | |
|             return blogRSS2 + rss2Footer()
 | |
|         else:
 | |
|             return blogRSS2
 | |
| 
 | |
|     if pageNumber is not None:
 | |
|         for item in timelineJson['orderedItems']:
 | |
|             if item['type'] != 'Create':
 | |
|                 continue
 | |
| 
 | |
|             blogRSS2 += \
 | |
|                 _htmlBlogPostRSS2(authorized, baseDir,
 | |
|                                   httpPrefix, translate,
 | |
|                                   nickname, domain,
 | |
|                                   domainFull, item,
 | |
|                                   None, True, systemLanguage)
 | |
| 
 | |
|     if includeHeader:
 | |
|         return blogRSS2 + rss2Footer()
 | |
|     else:
 | |
|         return blogRSS2
 | |
| 
 | |
| 
 | |
| def htmlBlogPageRSS3(authorized: bool, session,
 | |
|                      baseDir: str, httpPrefix: str, translate: {},
 | |
|                      nickname: str, domain: str, port: int,
 | |
|                      noOfItems: int, pageNumber: int,
 | |
|                      systemLanguage: str) -> str:
 | |
|     """Returns an RSS version 3 feed containing posts
 | |
|     """
 | |
|     if ' ' in nickname or '@' in nickname or \
 | |
|        '\n' in nickname or '\r' in nickname:
 | |
|         return None
 | |
| 
 | |
|     domainFull = getFullDomain(domain, port)
 | |
| 
 | |
|     blogRSS3 = ''
 | |
| 
 | |
|     blogsIndex = acctDir(baseDir, nickname, domain) + '/tlblogs.index'
 | |
|     if not os.path.isfile(blogsIndex):
 | |
|         return blogRSS3
 | |
| 
 | |
|     timelineJson = createBlogsTimeline(session, baseDir,
 | |
|                                        nickname, domain, port,
 | |
|                                        httpPrefix,
 | |
|                                        noOfItems, False,
 | |
|                                        pageNumber)
 | |
| 
 | |
|     if not timelineJson:
 | |
|         return blogRSS3
 | |
| 
 | |
|     if pageNumber is not None:
 | |
|         for item in timelineJson['orderedItems']:
 | |
|             if item['type'] != 'Create':
 | |
|                 continue
 | |
| 
 | |
|             blogRSS3 += \
 | |
|                 _htmlBlogPostRSS3(authorized, baseDir,
 | |
|                                   httpPrefix, translate,
 | |
|                                   nickname, domain,
 | |
|                                   domainFull, item,
 | |
|                                   None, True,
 | |
|                                   systemLanguage)
 | |
| 
 | |
|     return blogRSS3
 | |
| 
 | |
| 
 | |
| 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 isAccountDir(acct):
 | |
|                 continue
 | |
|             accountDir = os.path.join(baseDir + '/accounts', acct)
 | |
|             blogsIndex = accountDir + '/tlblogs.index'
 | |
|             if os.path.isfile(blogsIndex):
 | |
|                 ctr += 1
 | |
|         break
 | |
|     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 isAccountDir(acct):
 | |
|                 continue
 | |
|             accountDir = os.path.join(baseDir + '/accounts', acct)
 | |
|             blogsIndex = accountDir + '/tlblogs.index'
 | |
|             if os.path.isfile(blogsIndex):
 | |
|                 return acct.split('@')[0]
 | |
|         break
 | |
|     return None
 | |
| 
 | |
| 
 | |
| def htmlBlogView(authorized: bool,
 | |
|                  session, baseDir: str, httpPrefix: str,
 | |
|                  translate: {}, domain: str, port: int,
 | |
|                  noOfItems: int,
 | |
|                  peertubeInstances: [], systemLanguage: str,
 | |
|                  personCache: {}) -> str:
 | |
|     """Show the blog main page
 | |
|     """
 | |
|     blogStr = ''
 | |
| 
 | |
|     cssFilename = baseDir + '/epicyon-profile.css'
 | |
|     if os.path.isfile(baseDir + '/epicyon.css'):
 | |
|         cssFilename = baseDir + '/epicyon.css'
 | |
|     instanceTitle = \
 | |
|         getConfigParam(baseDir, 'instanceTitle')
 | |
|     blogStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
 | |
| 
 | |
|     if _noOfBlogAccounts(baseDir) <= 1:
 | |
|         nickname = _singleBlogAccountNickname(baseDir)
 | |
|         if nickname:
 | |
|             return htmlBlogPage(authorized, session,
 | |
|                                 baseDir, httpPrefix, translate,
 | |
|                                 nickname, domain, port,
 | |
|                                 noOfItems, 1, peertubeInstances,
 | |
|                                 systemLanguage, personCache)
 | |
| 
 | |
|     domainFull = getFullDomain(domain, port)
 | |
| 
 | |
|     for subdir, dirs, files in os.walk(baseDir + '/accounts'):
 | |
|         for acct in dirs:
 | |
|             if not isAccountDir(acct):
 | |
|                 continue
 | |
|             accountDir = os.path.join(baseDir + '/accounts', acct)
 | |
|             blogsIndex = accountDir + '/tlblogs.index'
 | |
|             if os.path.isfile(blogsIndex):
 | |
|                 blogStr += '<p class="blogaccount">'
 | |
|                 blogStr += '<a href="' + \
 | |
|                     httpPrefix + '://' + domainFull + '/blog/' + \
 | |
|                     acct.split('@')[0] + '">' + acct + '</a>'
 | |
|                 blogStr += '</p>'
 | |
|         break
 | |
| 
 | |
|     return blogStr + htmlFooter()
 | |
| 
 | |
| 
 | |
| def htmlEditBlog(mediaInstance: bool, translate: {},
 | |
|                  baseDir: str, httpPrefix: str,
 | |
|                  path: str,
 | |
|                  pageNumber: int,
 | |
|                  nickname: str, domain: str,
 | |
|                  postUrl: str, systemLanguage: str) -> str:
 | |
|     """Edit a blog post after it was created
 | |
|     """
 | |
|     postFilename = locatePost(baseDir, nickname, domain, postUrl)
 | |
|     if not postFilename:
 | |
|         print('Edit blog: Filename not found for ' + postUrl)
 | |
|         return None
 | |
| 
 | |
|     postJsonObject = loadJson(postFilename)
 | |
|     if not postJsonObject:
 | |
|         print('Edit blog: json not loaded for ' + postFilename)
 | |
|         return None
 | |
| 
 | |
|     editBlogText = '<h1">' + translate['Write your post text below.'] + '</h1>'
 | |
| 
 | |
|     if os.path.isfile(baseDir + '/accounts/newpost.txt'):
 | |
|         with open(baseDir + '/accounts/newpost.txt', 'r') as file:
 | |
|             editBlogText = '<p>' + file.read() + '</p>'
 | |
| 
 | |
|     cssFilename = baseDir + '/epicyon-profile.css'
 | |
|     if os.path.isfile(baseDir + '/epicyon.css'):
 | |
|         cssFilename = baseDir + '/epicyon.css'
 | |
| 
 | |
|     if '?' in path:
 | |
|         path = path.split('?')[0]
 | |
|     pathBase = path
 | |
| 
 | |
|     editBlogImageSection = '    <div class="container">'
 | |
|     editBlogImageSection += '      <label class="labels">' + \
 | |
|         translate['Image description'] + '</label>'
 | |
|     editBlogImageSection += '      <input type="text" name="imageDescription">'
 | |
|     editBlogImageSection += \
 | |
|         '      <input type="file" id="attachpic" name="attachpic"'
 | |
|     editBlogImageSection += \
 | |
|         '            accept="' + getMediaFormats() + '">'
 | |
|     editBlogImageSection += '    </div>'
 | |
| 
 | |
|     placeholderMessage = translate['Write something'] + '...'
 | |
|     endpoint = 'editblogpost'
 | |
|     placeholderSubject = translate['Title']
 | |
|     scopeIcon = 'scope_blog.png'
 | |
|     scopeDescription = translate['Blog']
 | |
| 
 | |
|     dateAndLocation = ''
 | |
|     dateAndLocation = '<div class="container">'
 | |
| 
 | |
|     dateAndLocation += \
 | |
|         '<p><input type="checkbox" class="profilecheckbox" ' + \
 | |
|         'name="schedulePost"><label class="labels">' + \
 | |
|         translate['This is a scheduled post.'] + '</label></p>'
 | |
| 
 | |
|     dateAndLocation += \
 | |
|         '<p><img loading="lazy" alt="" title="" ' + \
 | |
|         'class="emojicalendar" src="/icons/calendar.png"/>'
 | |
|     dateAndLocation += \
 | |
|         '<label class="labels">' + translate['Date'] + ': </label>'
 | |
|     dateAndLocation += '<input type="date" name="eventDate">'
 | |
|     dateAndLocation += '<label class="labelsright">' + translate['Time'] + ':'
 | |
|     dateAndLocation += '<input type="time" name="eventTime"></label></p>'
 | |
|     dateAndLocation += '</div>'
 | |
|     dateAndLocation += '<div class="container">'
 | |
|     dateAndLocation += \
 | |
|         '<br><label class="labels">' + translate['Location'] + ': </label>'
 | |
|     dateAndLocation += '<input type="text" name="location">'
 | |
|     dateAndLocation += '</div>'
 | |
| 
 | |
|     instanceTitle = \
 | |
|         getConfigParam(baseDir, 'instanceTitle')
 | |
|     editBlogForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
 | |
| 
 | |
|     editBlogForm += \
 | |
|         '<form enctype="multipart/form-data" method="POST" ' + \
 | |
|         'accept-charset="UTF-8" action="' + \
 | |
|         pathBase + '?' + endpoint + '?page=' + str(pageNumber) + '">'
 | |
|     editBlogForm += \
 | |
|         '  <input type="hidden" name="postUrl" value="' + postUrl + '">'
 | |
|     editBlogForm += \
 | |
|         '  <input type="hidden" name="pageNumber" value="' + \
 | |
|         str(pageNumber) + '">'
 | |
|     editBlogForm += '  <div class="vertical-center">'
 | |
|     editBlogForm += \
 | |
|         '    <label for="nickname"><b>' + editBlogText + '</b></label>'
 | |
|     editBlogForm += '    <div class="container">'
 | |
| 
 | |
|     editBlogForm += '      <div class="dropbtn">'
 | |
|     editBlogForm += \
 | |
|         '        <img loading="lazy" alt="" title="" src="/icons' + \
 | |
|         '/' + scopeIcon + '"/><b class="scope-desc">' + \
 | |
|         scopeDescription + '</b>'
 | |
|     editBlogForm += '      </div>'
 | |
| 
 | |
|     editBlogForm += '      <a href="' + pathBase + \
 | |
|         '/searchemoji"><img loading="lazy" ' + \
 | |
|         'class="emojisearch" src="/emoji/1F601.png" title="' + \
 | |
|         translate['Search for emoji'] + '" alt="' + \
 | |
|         translate['Search for emoji'] + '"/></a>'
 | |
|     editBlogForm += '    </div>'
 | |
|     editBlogForm += '    <div class="container"><center>'
 | |
|     editBlogForm += '      <a href="' + pathBase + \
 | |
|         '/inbox"><button class="cancelbtn">' + \
 | |
|         translate['Cancel'] + '</button></a>'
 | |
|     editBlogForm += '      <input type="submit" name="submitPost" value="' + \
 | |
|         translate['Submit'] + '">'
 | |
|     editBlogForm += '    </center></div>'
 | |
|     if mediaInstance:
 | |
|         editBlogForm += editBlogImageSection
 | |
|     editBlogForm += \
 | |
|         '    <label class="labels">' + placeholderSubject + '</label><br>'
 | |
|     titleStr = ''
 | |
|     if postJsonObject['object'].get('summary'):
 | |
|         titleStr = postJsonObject['object']['summary']
 | |
|     editBlogForm += \
 | |
|         '    <input type="text" name="subject" value="' + titleStr + '">'
 | |
|     editBlogForm += ''
 | |
|     editBlogForm += '    <br>'
 | |
|     messageBoxHeight = 800
 | |
| 
 | |
|     contentStr = getBaseContentFromPost(postJsonObject, systemLanguage)
 | |
|     contentStr = contentStr.replace('<p>', '').replace('</p>', '\n')
 | |
| 
 | |
|     editBlogForm += \
 | |
|         editTextArea(placeholderMessage, 'message', contentStr,
 | |
|                      messageBoxHeight, '', True)
 | |
|     editBlogForm += dateAndLocation
 | |
|     if not mediaInstance:
 | |
|         editBlogForm += editBlogImageSection
 | |
|     editBlogForm += '  </div>'
 | |
|     editBlogForm += '</form>'
 | |
| 
 | |
|     editBlogForm = editBlogForm.replace('<body>',
 | |
|                                         '<body onload="focusOnMessage()">')
 | |
| 
 | |
|     editBlogForm += htmlFooter()
 | |
|     return editBlogForm
 | |
| 
 | |
| 
 | |
| def pathContainsBlogLink(baseDir: str,
 | |
|                          httpPrefix: str, domain: str,
 | |
|                          domainFull: str, path: str) -> (str, str):
 | |
|     """If the path contains a blog entry then return its filename
 | |
|     """
 | |
|     if '/users/' not in path:
 | |
|         return None, None
 | |
|     userEnding = path.split('/users/', 1)[1]
 | |
|     if '/' not in userEnding:
 | |
|         return None, None
 | |
|     userEnding2 = userEnding.split('/')
 | |
|     nickname = userEnding2[0]
 | |
|     if len(userEnding2) != 2:
 | |
|         return None, None
 | |
|     if len(userEnding2[1]) < 14:
 | |
|         return None, None
 | |
|     userEnding2[1] = userEnding2[1].strip()
 | |
|     if not userEnding2[1].isdigit():
 | |
|         return None, None
 | |
|     # check for blog posts
 | |
|     blogIndexFilename = acctDir(baseDir, nickname, domain) + '/tlblogs.index'
 | |
|     if not os.path.isfile(blogIndexFilename):
 | |
|         return None, None
 | |
|     if '#' + userEnding2[1] + '.' not in open(blogIndexFilename).read():
 | |
|         return None, None
 | |
|     messageId = httpPrefix + '://' + domainFull + \
 | |
|         '/users/' + nickname + '/statuses/' + userEnding2[1]
 | |
|     return locatePost(baseDir, nickname, domain, messageId), nickname
 | |
| 
 | |
| 
 | |
| def getBlogAddress(actorJson: {}) -> str:
 | |
|     """Returns blog address for the given actor
 | |
|     """
 | |
|     return getActorPropertyUrl(actorJson, 'Blog')
 |