Merge branch 'main' of ssh://code.freedombone.net:2222/bashrc/epicyon into main

main
Bob Mottram 2021-02-13 16:01:07 +00:00
commit b2defda3c9
32 changed files with 350 additions and 62 deletions

View File

@ -788,6 +788,13 @@ def addHtmlTags(baseDir: str, httpPrefix: str,
prevWordStr = '' prevWordStr = ''
continue continue
elif firstChar == '#': elif firstChar == '#':
# remove any endings from the hashtag
hashTagEndings = ('.', ':', ';', '-', '\n')
for ending in hashTagEndings:
if wordStr.endswith(ending):
wordStr = wordStr[:len(wordStr) - 1]
break
if _addHashTags(wordStr, httpPrefix, originalDomain, if _addHashTags(wordStr, httpPrefix, originalDomain,
replaceHashTags, hashtags): replaceHashTags, hashtags):
prevWordStr = '' prevWordStr = ''

View File

@ -218,6 +218,7 @@ from utils import loadJson
from utils import saveJson from utils import saveJson
from utils import isSuspended from utils import isSuspended
from utils import dangerousMarkup from utils import dangerousMarkup
from utils import refreshNewswire
from manualapprove import manualDenyFollowRequest from manualapprove import manualDenyFollowRequest
from manualapprove import manualApproveFollowRequest from manualapprove import manualApproveFollowRequest
from announce import createAnnounce from announce import createAnnounce
@ -255,7 +256,6 @@ from newswire import rss2Footer
from newswire import loadHashtagCategories from newswire import loadHashtagCategories
from newsdaemon import runNewswireWatchdog from newsdaemon import runNewswireWatchdog
from newsdaemon import runNewswireDaemon from newsdaemon import runNewswireDaemon
from newsdaemon import refreshNewswire
from filters import isFiltered from filters import isFiltered
from filters import addGlobalFilter from filters import addGlobalFilter
from filters import removeGlobalFilter from filters import removeGlobalFilter
@ -1948,12 +1948,50 @@ class PubServer(BaseHTTPRequestHandler):
if postsToNews == 'on': if postsToNews == 'on':
if os.path.isfile(newswireBlockedFilename): if os.path.isfile(newswireBlockedFilename):
os.remove(newswireBlockedFilename) os.remove(newswireBlockedFilename)
refreshNewswire(self.server.baseDir)
else: else:
if os.path.isdir(accountDir): if os.path.isdir(accountDir):
noNewswireFile = open(newswireBlockedFilename, "w+") noNewswireFile = open(newswireBlockedFilename, "w+")
if noNewswireFile: if noNewswireFile:
noNewswireFile.write('\n') noNewswireFile.write('\n')
noNewswireFile.close() noNewswireFile.close()
refreshNewswire(self.server.baseDir)
usersPathStr = \
usersPath + '/' + self.server.defaultTimeline + \
'?page=' + str(pageNumber)
self._redirect_headers(usersPathStr, cookie,
callingDomain)
self.server.POSTbusy = False
return
# person options screen, permission to post to featured articles
# See htmlPersonOptions
if '&submitPostToFeatures=' in optionsConfirmParams:
adminNickname = getConfigParam(self.server.baseDir, 'admin')
if (chooserNickname != optionsNickname and
(chooserNickname == adminNickname or
(isModerator(self.server.baseDir, chooserNickname) and
not isModerator(self.server.baseDir, optionsNickname)))):
postsToFeatures = None
if 'postsToFeatures=' in optionsConfirmParams:
postsToFeatures = \
optionsConfirmParams.split('postsToFeatures=')[1]
if '&' in postsToFeatures:
postsToFeatures = postsToFeatures.split('&')[0]
accountDir = self.server.baseDir + '/accounts/' + \
optionsNickname + '@' + optionsDomain
featuresBlockedFilename = accountDir + '/.nofeatures'
if postsToFeatures == 'on':
if os.path.isfile(featuresBlockedFilename):
os.remove(featuresBlockedFilename)
refreshNewswire(self.server.baseDir)
else:
if os.path.isdir(accountDir):
noFeaturesFile = open(featuresBlockedFilename, "w+")
if noFeaturesFile:
noFeaturesFile.write('\n')
noFeaturesFile.close()
refreshNewswire(self.server.baseDir)
usersPathStr = \ usersPathStr = \
usersPath + '/' + self.server.defaultTimeline + \ usersPath + '/' + self.server.defaultTimeline + \
'?page=' + str(pageNumber) '?page=' + str(pageNumber)
@ -5469,7 +5507,9 @@ class PubServer(BaseHTTPRequestHandler):
self.server.dormantMonths, self.server.dormantMonths,
backToPath, backToPath,
lockedAccount, lockedAccount,
movedTo, alsoKnownAs).encode('utf-8') movedTo, alsoKnownAs,
self.server.textModeBanner,
self.server.newsInstance).encode('utf-8')
msglen = len(msg) msglen = len(msg)
self._set_headers('text/html', msglen, self._set_headers('text/html', msglen,
cookie, callingDomain) cookie, callingDomain)
@ -11085,7 +11125,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.translate, self.server.translate,
self.server.baseDir, self.path, self.server.baseDir, self.path,
self.server.httpPrefix, self.server.httpPrefix,
self.server.domainFull).encode('utf-8') self.server.domainFull,
self.server.textModeBanner).encode('utf-8')
msglen = len(msg) msglen = len(msg)
self._set_headers('text/html', msglen, cookie, callingDomain) self._set_headers('text/html', msglen, cookie, callingDomain)
self._write(msg) self._write(msg)

BIN
emoji/android.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -1,4 +1,5 @@
{ {
"android": "android",
"popcorn": "1F37F", "popcorn": "1F37F",
"1stplacemedal": "1F947", "1stplacemedal": "1F947",
"abbutton": "1F18E", "abbutton": "1F18E",

View File

@ -84,6 +84,14 @@ a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
.transparent {
color: transparent;
background: transparent;
font-size: 0px;
line-height: 0px;
height: 0px;
}
.calendar__day__header, .calendar__day__header,
.calendar__day__cell { .calendar__day__cell {
border: 2px solid var(--lines-color); border: 2px solid var(--lines-color);

View File

@ -98,6 +98,14 @@ a:focus {
border: 2px solid var(--focus-color); border: 2px solid var(--focus-color);
} }
.transparent {
color: transparent;
background: transparent;
font-size: 0px;
line-height: 0px;
height: 0px;
}
.follow { .follow {
height: 100%; height: 100%;
position: relative; position: relative;

View File

@ -750,15 +750,3 @@ def runNewswireWatchdog(projectVersion: str, httpd) -> None:
newswireOriginal.clone(runNewswireDaemon) newswireOriginal.clone(runNewswireDaemon)
httpd.thrNewswireDaemon.start() httpd.thrNewswireDaemon.start()
print('Restarting newswire daemon...') print('Restarting newswire daemon...')
def refreshNewswire(baseDir: str) -> None:
"""Causes the newswire to be updated.
This creates a file which is then detected by the daemon
"""
refreshFilename = baseDir + '/accounts/.refresh_newswire'
if os.path.isfile(refreshFilename):
return
refreshFile = open(refreshFilename, 'w+')
refreshFile.write('\n')
refreshFile.close()

View File

@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
import os import os
import json
import requests import requests
from socket import error as SocketError from socket import error as SocketError
import errno import errno
@ -332,12 +333,14 @@ def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str,
result, pubDateStr, result, pubDateStr,
title, link, title, link,
votesStatus, postFilename, votesStatus, postFilename,
description, moderated, mirrored) description, moderated,
mirrored)
postCtr += 1 postCtr += 1
if postCtr >= maxPostsPerSource: if postCtr >= maxPostsPerSource:
break break
if postCtr > 0: if postCtr > 0:
print('Added ' + str(postCtr) + ' rss 2.0 feed items to newswire') print('Added ' + str(postCtr) +
' rss 2.0 feed items to newswire')
return result return result
@ -416,12 +419,14 @@ def _xml1StrToDict(baseDir: str, domain: str, xmlStr: str,
result, pubDateStr, result, pubDateStr,
title, link, title, link,
votesStatus, postFilename, votesStatus, postFilename,
description, moderated, mirrored) description, moderated,
mirrored)
postCtr += 1 postCtr += 1
if postCtr >= maxPostsPerSource: if postCtr >= maxPostsPerSource:
break break
if postCtr > 0: if postCtr > 0:
print('Added ' + str(postCtr) + ' rss 1.0 feed items to newswire') print('Added ' + str(postCtr) +
' rss 1.0 feed items to newswire')
return result return result
@ -488,12 +493,124 @@ def _atomFeedToDict(baseDir: str, domain: str, xmlStr: str,
result, pubDateStr, result, pubDateStr,
title, link, title, link,
votesStatus, postFilename, votesStatus, postFilename,
description, moderated, mirrored) description, moderated,
mirrored)
postCtr += 1 postCtr += 1
if postCtr >= maxPostsPerSource: if postCtr >= maxPostsPerSource:
break break
if postCtr > 0: if postCtr > 0:
print('Added ' + str(postCtr) + ' atom feed items to newswire') print('Added ' + str(postCtr) +
' atom feed items to newswire')
return result
def _jsonFeedV1ToDict(baseDir: str, domain: str, xmlStr: str,
moderated: bool, mirrored: bool,
maxPostsPerSource: int,
maxFeedItemSizeKb: int) -> {}:
"""Converts a json feed string to a dictionary
See https://jsonfeed.org/version/1.1
"""
if '"items"' not in xmlStr:
return {}
try:
feedJson = json.loads(xmlStr)
except BaseException:
return {}
maxBytes = maxFeedItemSizeKb * 1024
if not feedJson.get('version'):
return {}
if not feedJson['version'].startswith('https://jsonfeed.org/version/1'):
return {}
if not feedJson.get('items'):
return {}
if not isinstance(feedJson['items'], list):
return {}
postCtr = 0
result = {}
for jsonFeedItem in feedJson['items']:
if not jsonFeedItem:
continue
if not isinstance(jsonFeedItem, dict):
continue
if not jsonFeedItem.get('url'):
continue
if not isinstance(jsonFeedItem['url'], str):
continue
if not jsonFeedItem.get('date_published'):
if not jsonFeedItem.get('date_modified'):
continue
if not jsonFeedItem.get('content_text'):
if not jsonFeedItem.get('content_html'):
continue
if jsonFeedItem.get('content_html'):
if not isinstance(jsonFeedItem['content_html'], str):
continue
title = removeHtml(jsonFeedItem['content_html'])
else:
if not isinstance(jsonFeedItem['content_text'], str):
continue
title = removeHtml(jsonFeedItem['content_text'])
if len(title) > maxBytes:
print('WARN: json feed title is too long')
continue
description = ''
if jsonFeedItem.get('description'):
if not isinstance(jsonFeedItem['description'], str):
continue
description = removeHtml(jsonFeedItem['description'])
if len(description) > maxBytes:
print('WARN: json feed description is too long')
continue
if jsonFeedItem.get('tags'):
if isinstance(jsonFeedItem['tags'], list):
for tagName in jsonFeedItem['tags']:
if not isinstance(tagName, str):
continue
if ' ' in tagName:
continue
if not tagName.startswith('#'):
tagName = '#' + tagName
if tagName not in description:
description += ' ' + tagName
link = jsonFeedItem['url']
if '://' not in link:
continue
if len(link) > maxBytes:
print('WARN: json feed link is too long')
continue
itemDomain = link.split('://')[1]
if '/' in itemDomain:
itemDomain = itemDomain.split('/')[0]
if isBlockedDomain(baseDir, itemDomain):
continue
if jsonFeedItem.get('date_published'):
if not isinstance(jsonFeedItem['date_published'], str):
continue
pubDate = jsonFeedItem['date_published']
else:
if not isinstance(jsonFeedItem['date_modified'], str):
continue
pubDate = jsonFeedItem['date_modified']
pubDateStr = parseFeedDate(pubDate)
if pubDateStr:
if _validFeedDate(pubDateStr):
postFilename = ''
votesStatus = []
_addNewswireDictEntry(baseDir, domain,
result, pubDateStr,
title, link,
votesStatus, postFilename,
description, moderated,
mirrored)
postCtr += 1
if postCtr >= maxPostsPerSource:
break
if postCtr > 0:
print('Added ' + str(postCtr) +
' json feed items to newswire')
return result return result
@ -593,6 +710,10 @@ def _xmlStrToDict(baseDir: str, domain: str, xmlStr: str,
return _atomFeedToDict(baseDir, domain, return _atomFeedToDict(baseDir, domain,
xmlStr, moderated, mirrored, xmlStr, moderated, mirrored,
maxPostsPerSource, maxFeedItemSizeKb) maxPostsPerSource, maxFeedItemSizeKb)
elif 'https://jsonfeed.org/version/1' in xmlStr:
return _jsonFeedV1ToDict(baseDir, domain,
xmlStr, moderated, mirrored,
maxPostsPerSource, maxFeedItemSizeKb)
return {} return {}
@ -794,7 +915,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str,
locatePost(baseDir, nickname, locatePost(baseDir, nickname,
domain, postUrl, False) domain, postUrl, False)
if not fullPostFilename: if not fullPostFilename:
print('Unable to locate post ' + postUrl) print('Unable to locate post for newswire ' + postUrl)
ctr += 1 ctr += 1
if ctr >= maxBlogsPerAccount: if ctr >= maxBlogsPerAccount:
break break
@ -840,7 +961,7 @@ def _addBlogsToNewswire(baseDir: str, domain: str, newswire: {},
for handle in dirs: for handle in dirs:
if '@' not in handle: if '@' not in handle:
continue continue
if 'inbox@' in handle: if 'inbox@' in handle or 'news@' in handle:
continue continue
nickname = handle.split('@')[0] nickname = handle.split('@')[0]

View File

@ -18,6 +18,7 @@ from utils import getFullDomain
from utils import removeIdEnding from utils import removeIdEnding
from utils import getDomainFromActor from utils import getDomainFromActor
from utils import dangerousMarkup from utils import dangerousMarkup
from utils import isFeaturedWriter
from blocking import isBlockedDomain from blocking import isBlockedDomain
from blocking import outboxBlock from blocking import outboxBlock
from blocking import outboxUndoBlock from blocking import outboxUndoBlock
@ -211,14 +212,16 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
# save all instance blogs to the news actor # save all instance blogs to the news actor
if postToNickname != 'news' and outboxName == 'tlblogs': if postToNickname != 'news' and outboxName == 'tlblogs':
if '/' in savedFilename: if '/' in savedFilename:
savedPostId = savedFilename.split('/')[-1] if isFeaturedWriter(baseDir, postToNickname, domain):
blogsDir = baseDir + '/accounts/news@' + domain + '/tlblogs' savedPostId = savedFilename.split('/')[-1]
if not os.path.isdir(blogsDir): blogsDir = \
os.mkdir(blogsDir) baseDir + '/accounts/news@' + domain + '/tlblogs'
copyfile(savedFilename, blogsDir + '/' + savedPostId) if not os.path.isdir(blogsDir):
inboxUpdateIndex('tlblogs', baseDir, os.mkdir(blogsDir)
'news@' + domain, copyfile(savedFilename, blogsDir + '/' + savedPostId)
savedFilename, debug) inboxUpdateIndex('tlblogs', baseDir,
'news@' + domain,
savedFilename, debug)
# clear the citations file if it exists # clear the citations file if it exists
citationsFilename = \ citationsFilename = \

View File

@ -40,6 +40,7 @@ from utils import loadJson
from utils import saveJson from utils import saveJson
from utils import setConfigParam from utils import setConfigParam
from utils import getConfigParam from utils import getConfigParam
from utils import refreshNewswire
def generateRSAKey() -> (str, str): def generateRSAKey() -> (str, str):
@ -915,6 +916,9 @@ def removeAccount(baseDir: str, nickname: str,
os.remove(baseDir + '/wfdeactivated/' + handle + '.json') os.remove(baseDir + '/wfdeactivated/' + handle + '.json')
if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname): if os.path.isdir(baseDir + '/sharefilesdeactivated/' + nickname):
shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname) shutil.rmtree(baseDir + '/sharefilesdeactivated/' + nickname)
refreshNewswire(baseDir)
return True return True
@ -944,6 +948,9 @@ def deactivateAccount(baseDir: str, nickname: str, domain: str) -> bool:
os.mkdir(deactivatedSharefilesDir) os.mkdir(deactivatedSharefilesDir)
shutil.move(baseDir + '/sharefiles/' + nickname, shutil.move(baseDir + '/sharefiles/' + nickname,
deactivatedSharefilesDir + '/' + nickname) deactivatedSharefilesDir + '/' + nickname)
refreshNewswire(baseDir)
return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain) return os.path.isdir(deactivatedDir + '/' + nickname + '@' + domain)
@ -970,6 +977,8 @@ def activateAccount(baseDir: str, nickname: str, domain: str) -> None:
shutil.move(deactivatedSharefilesDir + '/' + nickname, shutil.move(deactivatedSharefilesDir + '/' + nickname,
baseDir + '/sharefiles/' + nickname) baseDir + '/sharefiles/' + nickname)
refreshNewswire(baseDir)
def isPersonSnoozed(baseDir: str, nickname: str, domain: str, def isPersonSnoozed(baseDir: str, nickname: str, domain: str,
snoozeActor: str) -> bool: snoozeActor: str) -> bool:

View File

@ -367,5 +367,6 @@
"Skip to timeline": "تخطي إلى الجدول الزمني", "Skip to timeline": "تخطي إلى الجدول الزمني",
"Skip to Newswire": "انتقل إلى Newswire", "Skip to Newswire": "انتقل إلى Newswire",
"Skip to Links": "تخطي إلى روابط الويب", "Skip to Links": "تخطي إلى روابط الويب",
"Publish a blog article": "نشر مقال بلوق" "Publish a blog article": "نشر مقال بلوق",
"Featured writer": "كاتب متميز"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Ves a la cronologia", "Skip to timeline": "Ves a la cronologia",
"Skip to Newswire": "Vés a Newswire", "Skip to Newswire": "Vés a Newswire",
"Skip to Links": "Vés als enllaços web", "Skip to Links": "Vés als enllaços web",
"Publish a blog article": "Publicar un article del bloc" "Publish a blog article": "Publicar un article del bloc",
"Featured writer": "Escriptor destacat"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Neidio i'r llinell amser", "Skip to timeline": "Neidio i'r llinell amser",
"Skip to Newswire": "Neidio i Newswire", "Skip to Newswire": "Neidio i Newswire",
"Skip to Links": "Neidio i Dolenni Gwe", "Skip to Links": "Neidio i Dolenni Gwe",
"Publish a blog article": "Cyhoeddi erthygl blog" "Publish a blog article": "Cyhoeddi erthygl blog",
"Featured writer": "Awdur dan sylw"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Zur Zeitleiste springen", "Skip to timeline": "Zur Zeitleiste springen",
"Skip to Newswire": "Springe zu Newswire", "Skip to Newswire": "Springe zu Newswire",
"Skip to Links": "Springe zu Weblinks", "Skip to Links": "Springe zu Weblinks",
"Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel" "Publish a blog article": "Veröffentlichen Sie einen Blog-Artikel",
"Featured writer": "Ausgewählter Schriftsteller"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Skip to timeline", "Skip to timeline": "Skip to timeline",
"Skip to Newswire": "Skip to Newswire", "Skip to Newswire": "Skip to Newswire",
"Skip to Links": "Skip to Links", "Skip to Links": "Skip to Links",
"Publish a blog article": "Publish a blog article" "Publish a blog article": "Publish a blog article",
"Featured writer": "Featured writer"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Saltar a la línea de tiempo", "Skip to timeline": "Saltar a la línea de tiempo",
"Skip to Newswire": "Saltar a Newswire", "Skip to Newswire": "Saltar a Newswire",
"Skip to Links": "Saltar a enlaces web", "Skip to Links": "Saltar a enlaces web",
"Publish a blog article": "Publica un artículo de blog" "Publish a blog article": "Publica un artículo de blog",
"Featured writer": "Escritora destacada"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Passer à la chronologie", "Skip to timeline": "Passer à la chronologie",
"Skip to Newswire": "Passer à Newswire", "Skip to Newswire": "Passer à Newswire",
"Skip to Links": "Passer aux liens Web", "Skip to Links": "Passer aux liens Web",
"Publish a blog article": "Publier un article de blog" "Publish a blog article": "Publier un article de blog",
"Featured writer": "Écrivain en vedette"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Scipeáil chuig an amlíne", "Skip to timeline": "Scipeáil chuig an amlíne",
"Skip to Newswire": "Scipeáil chuig Newswire", "Skip to Newswire": "Scipeáil chuig Newswire",
"Skip to Links": "Scipeáil chuig Naisc Ghréasáin", "Skip to Links": "Scipeáil chuig Naisc Ghréasáin",
"Publish a blog article": "Foilsigh alt blagála" "Publish a blog article": "Foilsigh alt blagála",
"Featured writer": "Scríbhneoir mór le rá"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "टाइमलाइन पर जाएं", "Skip to timeline": "टाइमलाइन पर जाएं",
"Skip to Newswire": "Newswire पर जाएं", "Skip to Newswire": "Newswire पर जाएं",
"Skip to Links": "वेब लिंक पर जाएं", "Skip to Links": "वेब लिंक पर जाएं",
"Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें" "Publish a blog article": "एक ब्लॉग लेख प्रकाशित करें",
"Featured writer": "फीचर्ड लेखक"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Passa alla sequenza temporale", "Skip to timeline": "Passa alla sequenza temporale",
"Skip to Newswire": "Passa a Newswire", "Skip to Newswire": "Passa a Newswire",
"Skip to Links": "Passa a collegamenti Web", "Skip to Links": "Passa a collegamenti Web",
"Publish a blog article": "Pubblica un articolo sul blog" "Publish a blog article": "Pubblica un articolo sul blog",
"Featured writer": "Scrittore in primo piano"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "タイムラインにスキップ", "Skip to timeline": "タイムラインにスキップ",
"Skip to Newswire": "Newswireにスキップ", "Skip to Newswire": "Newswireにスキップ",
"Skip to Links": "Webリンクにスキップ", "Skip to Links": "Webリンクにスキップ",
"Publish a blog article": "ブログ記事を公開する" "Publish a blog article": "ブログ記事を公開する",
"Featured writer": "注目の作家"
} }

View File

@ -363,5 +363,6 @@
"Skip to timeline": "Skip to timeline", "Skip to timeline": "Skip to timeline",
"Skip to Newswire": "Skip to Newswire", "Skip to Newswire": "Skip to Newswire",
"Skip to Links": "Skip to Links", "Skip to Links": "Skip to Links",
"Publish a blog article": "Publish a blog article" "Publish a blog article": "Publish a blog article",
"Featured writer": "Featured writer"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Pular para a linha do tempo", "Skip to timeline": "Pular para a linha do tempo",
"Skip to Newswire": "Pular para Newswire", "Skip to Newswire": "Pular para Newswire",
"Skip to Links": "Pular para links da web", "Skip to Links": "Pular para links da web",
"Publish a blog article": "Publique um artigo de blog" "Publish a blog article": "Publique um artigo de blog",
"Featured writer": "Escritor em destaque"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "Перейти к временной шкале", "Skip to timeline": "Перейти к временной шкале",
"Skip to Newswire": "Перейти к ленте новостей", "Skip to Newswire": "Перейти к ленте новостей",
"Skip to Links": "Перейти к веб-ссылкам", "Skip to Links": "Перейти к веб-ссылкам",
"Publish a blog article": "Опубликовать статью в блоге" "Publish a blog article": "Опубликовать статью в блоге",
"Featured writer": "Избранный писатель"
} }

View File

@ -367,5 +367,6 @@
"Skip to timeline": "跳到时间线", "Skip to timeline": "跳到时间线",
"Skip to Newswire": "跳到新闻专线", "Skip to Newswire": "跳到新闻专线",
"Skip to Links": "跳到网页链接", "Skip to Links": "跳到网页链接",
"Publish a blog article": "发布博客文章" "Publish a blog article": "发布博客文章",
"Featured writer": "特色作家"
} }

View File

@ -26,6 +26,27 @@ invalidCharacters = (
) )
def isFeaturedWriter(baseDir: str, nickname: str, domain: str) -> bool:
"""Is the given account a featured writer, appearing in the features
timeline on news instances?
"""
featuresBlockedFilename = \
baseDir + '/accounts/' + \
nickname + '@' + domain + '/.nofeatures'
return not os.path.isfile(featuresBlockedFilename)
def refreshNewswire(baseDir: str):
"""Causes the newswire to be updates after a change to user accounts
"""
refreshNewswireFilename = baseDir + '/accounts/.refresh_newswire'
if os.path.isfile(refreshNewswireFilename):
return
refreshFile = open(refreshNewswireFilename, 'w+')
refreshFile.write('\n')
refreshFile.close()
def getSHA256(msg: str): def getSHA256(msg: str):
"""Returns a SHA256 hash of the given string """Returns a SHA256 hash of the given string
""" """

View File

@ -21,6 +21,8 @@ from happening import getCalendarEvents
from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter from webapp_utils import htmlFooter
from webapp_utils import getAltPath from webapp_utils import getAltPath
from webapp_utils import htmlHideFromScreenReader
from webapp_utils import htmlKeyboardNavigation
def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str, def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str,
@ -200,7 +202,8 @@ def _htmlCalendarDay(cssCache: {}, translate: {},
def htmlCalendar(cssCache: {}, translate: {}, def htmlCalendar(cssCache: {}, translate: {},
baseDir: str, path: str, baseDir: str, path: str,
httpPrefix: str, domainFull: str) -> str: httpPrefix: str, domainFull: str,
textModeBanner: str) -> str:
"""Show the calendar for a person """Show the calendar for a person
""" """
domain = domainFull domain = domainFull
@ -297,8 +300,10 @@ def htmlCalendar(cssCache: {}, translate: {},
instanceTitle = \ instanceTitle = \
getConfigParam(baseDir, 'instanceTitle') getConfigParam(baseDir, 'instanceTitle')
calendarStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) headerStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
calendarStr += '<main><table class="calendar">\n'
# the main graphical calendar as a table
calendarStr = '<main><table class="calendar">\n'
calendarStr += '<caption class="calendar__banner--month">\n' calendarStr += '<caption class="calendar__banner--month">\n'
calendarStr += \ calendarStr += \
' <a href="' + calActor + '/calendar?year=' + str(prevYear) + \ ' <a href="' + calActor + '/calendar?year=' + str(prevYear) + \
@ -320,24 +325,30 @@ def htmlCalendar(cssCache: {}, translate: {},
calendarStr += '</caption>\n' calendarStr += '</caption>\n'
calendarStr += '<thead>\n' calendarStr += '<thead>\n'
calendarStr += '<tr>\n' calendarStr += '<tr>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Sun'] + '</th>\n' translate['Sun'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Mon'] + '</th>\n' translate['Mon'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Tue'] + '</th>\n' translate['Tue'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Wed'] + '</th>\n' translate['Wed'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Thu'] + '</th>\n' translate['Thu'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Fri'] + '</th>\n' translate['Fri'] + '</th>\n'
calendarStr += ' <th class="calendar__day__header">' + \ calendarStr += ' <th scope="col" class="calendar__day__header">' + \
translate['Sat'] + '</th>\n' translate['Sat'] + '</th>\n'
calendarStr += '</tr>\n' calendarStr += '</tr>\n'
calendarStr += '</thead>\n' calendarStr += '</thead>\n'
calendarStr += '<tbody>\n' calendarStr += '<tbody>\n'
# beginning of the links used for accessibility
navLinks = {}
timelineLinkStr = htmlHideFromScreenReader('🏠') + ' ' + \
translate['Switch to timeline view']
navLinks[timelineLinkStr] = calActor + '/inbox'
dayOfMonth = 0 dayOfMonth = 0
dow = weekDayOfMonthStart(monthNumber, year) dow = weekDayOfMonthStart(monthNumber, year)
for weekOfMonth in range(1, 7): for weekOfMonth in range(1, 7):
@ -358,8 +369,15 @@ def htmlCalendar(cssCache: {}, translate: {},
url = calActor + '/calendar?year=' + \ url = calActor + '/calendar?year=' + \
str(year) + '?month=' + \ str(year) + '?month=' + \
str(monthNumber) + '?day=' + str(dayOfMonth) str(monthNumber) + '?day=' + str(dayOfMonth)
dayLink = '<a href="' + url + '">' + \ dayDescription = monthName + ' ' + str(dayOfMonth)
dayLink = '<a href="' + url + '" ' + \
'title="' + dayDescription + '">' + \
str(dayOfMonth) + '</a>' str(dayOfMonth) + '</a>'
# accessibility menu links
menuOptionStr = \
htmlHideFromScreenReader('📅') + ' ' + \
dayDescription
navLinks[menuOptionStr] = url
# there are events for this day # there are events for this day
if not isToday: if not isToday:
calendarStr += \ calendarStr += \
@ -387,5 +405,17 @@ def htmlCalendar(cssCache: {}, translate: {},
calendarStr += '</tbody>\n' calendarStr += '</tbody>\n'
calendarStr += '</table></main>\n' calendarStr += '</table></main>\n'
calendarStr += htmlFooter()
return calendarStr # end of the links used for accessibility
nextMonthStr = \
htmlHideFromScreenReader('') + ' ' + translate['Next month']
navLinks[nextMonthStr] = calActor + '/calendar?year=' + str(nextYear) + \
'?month=' + str(nextMonthNumber)
prevMonthStr = \
htmlHideFromScreenReader('') + ' ' + translate['Previous month']
navLinks[prevMonthStr] = calActor + '/calendar?year=' + str(prevYear) + \
'?month=' + str(prevMonthNumber)
screenReaderCal = \
htmlKeyboardNavigation(textModeBanner, navLinks, monthName)
return headerStr + screenReaderCal + calendarStr + htmlFooter()

View File

@ -17,6 +17,7 @@ from utils import isDormant
from utils import removeHtml from utils import removeHtml
from utils import getDomainFromActor from utils import getDomainFromActor
from utils import getNicknameFromActor from utils import getNicknameFromActor
from utils import isFeaturedWriter
from blocking import isBlocked from blocking import isBlocked
from follow import isFollowerOfPerson from follow import isFollowerOfPerson
from follow import isFollowingActor from follow import isFollowingActor
@ -24,6 +25,7 @@ from followingCalendar import receivingCalendarEvents
from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlFooter from webapp_utils import htmlFooter
from webapp_utils import getBrokenLinkSubstitute from webapp_utils import getBrokenLinkSubstitute
from webapp_utils import htmlKeyboardNavigation
def htmlPersonOptions(defaultTimeline: str, def htmlPersonOptions(defaultTimeline: str,
@ -49,7 +51,9 @@ def htmlPersonOptions(defaultTimeline: str,
backToPath: str, backToPath: str,
lockedAccount: bool, lockedAccount: bool,
movedTo: str, movedTo: str,
alsoKnownAs: []) -> str: alsoKnownAs: [],
textModeBanner: str,
newsInstance: bool) -> str:
"""Show options for a person: view/follow/block/report """Show options for a person: view/follow/block/report
""" """
optionsDomain, optionsPort = getDomainFromActor(optionsActor) optionsDomain, optionsPort = getDomainFromActor(optionsActor)
@ -108,12 +112,13 @@ def htmlPersonOptions(defaultTimeline: str,
if donateUrl: if donateUrl:
donateStr = \ donateStr = \
' <a href="' + donateUrl + \ ' <a href="' + donateUrl + \
'"><button class="button" name="submitDonate">' + \ ' tabindex="-1""><button class="button" name="submitDonate">' + \
translate['Donate'] + '</button></a>\n' translate['Donate'] + '</button></a>\n'
instanceTitle = \ instanceTitle = \
getConfigParam(baseDir, 'instanceTitle') getConfigParam(baseDir, 'instanceTitle')
optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) optionsStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
optionsStr += htmlKeyboardNavigation(textModeBanner, {})
optionsStr += '<br><br>\n' optionsStr += '<br><br>\n'
optionsStr += '<div class="options">\n' optionsStr += '<div class="options">\n'
optionsStr += ' <div class="optionsAvatar">\n' optionsStr += ' <div class="optionsAvatar">\n'
@ -284,6 +289,24 @@ def htmlPersonOptions(defaultTimeline: str,
checkboxStr = checkboxStr.replace(' checked>', '>') checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr optionsStr += checkboxStr
# checkbox for permission to post to featured articles
if newsInstance and optionsDomainFull == domainFull:
adminNickname = getConfigParam(baseDir, 'admin')
if (nickname == adminNickname or
(isModerator(baseDir, nickname) and
not isModerator(baseDir, optionsNickname))):
checkboxStr = \
' <input type="checkbox" ' + \
'class="profilecheckbox" name="postsToFeatures" checked> ' + \
translate['Featured writer'] + \
'\n <button type="submit" class="buttonsmall" ' + \
'name="submitPostToFeatures">' + \
translate['Submit'] + '</button><br>\n'
if not isFeaturedWriter(baseDir, optionsNickname,
optionsDomain):
checkboxStr = checkboxStr.replace(' checked>', '>')
optionsStr += checkboxStr
optionsStr += optionsLinkStr optionsStr += optionsLinkStr
backPath = '/' backPath = '/'
if nickname: if nickname:

View File

@ -327,6 +327,10 @@ def _getEditIconHtml(baseDir: str, nickname: str, domainFull: str,
""" """
editStr = '' editStr = ''
actor = postJsonObject['actor'] actor = postJsonObject['actor']
# This should either be a post which you created,
# or it could be generated from the newswire (see
# _addBlogsToNewswire) in which case anyone with
# editor status should be able to alter it
if (actor.endswith('/' + domainFull + '/users/' + nickname) or if (actor.endswith('/' + domainFull + '/users/' + nickname) or
(isEditor(baseDir, nickname) and (isEditor(baseDir, nickname) and
actor.endswith('/' + domainFull + '/users/news'))): actor.endswith('/' + domainFull + '/users/news'))):

View File

@ -1745,6 +1745,10 @@ def _individualFollowAsHtml(translate: {},
if avatarUrl2: if avatarUrl2:
avatarUrl = avatarUrl2 avatarUrl = avatarUrl2
if displayName: if displayName:
displayName = \
addEmojiToDisplayName(baseDir, httpPrefix,
actorNickname, domain,
displayName, False)
titleStr = displayName titleStr = displayName
if dormant: if dormant:

View File

@ -432,7 +432,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str,
} }
if moderator: if moderator:
navLinks[menuModeration] = usersPath + '/moderation#modtimeline' navLinks[menuModeration] = usersPath + '/moderation#modtimeline'
tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, tlStr += htmlKeyboardNavigation(textModeBanner, navLinks, None,
usersPath, translate, followApprovals) usersPath, translate, followApprovals)
# banner and row of buttons # banner and row of buttons

View File

@ -887,6 +887,7 @@ def htmlHideFromScreenReader(htmlStr: str) -> str:
def htmlKeyboardNavigation(banner: str, links: {}, def htmlKeyboardNavigation(banner: str, links: {},
subHeading=None,
usersPath=None, translate=None, usersPath=None, translate=None,
followApprovals=False) -> str: followApprovals=False) -> str:
"""Given a set of links return the html for keyboard navigation """Given a set of links return the html for keyboard navigation
@ -896,6 +897,10 @@ def htmlKeyboardNavigation(banner: str, links: {},
if banner: if banner:
htmlStr += '<pre aria-label="">' + banner + '<br><br></pre>' htmlStr += '<pre aria-label="">' + banner + '<br><br></pre>'
if subHeading:
htmlStr += '<strong><label class="transparent">' + \
subHeading + '</label></strong><br>'
# show new follower approvals # show new follower approvals
if usersPath and translate and followApprovals: if usersPath and translate and followApprovals:
htmlStr += '<strong><label class="transparent">' + \ htmlStr += '<strong><label class="transparent">' + \