diff --git a/acceptreject.py b/acceptreject.py index 712d9ef7a..d3f4c50cd 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -77,7 +77,8 @@ def _acceptFollow(baseDir: str, domain: str, messageJson: {}, if not messageJson['object'].get('type'): return if not messageJson['object']['type'] == 'Follow': - return + if not messageJson['object']['type'] == 'Join': + return if debug: print('DEBUG: receiving Follow activity') if not messageJson['object'].get('actor'): diff --git a/categories.py b/categories.py index fc13a7c76..a99241b73 100644 --- a/categories.py +++ b/categories.py @@ -114,7 +114,7 @@ def _validHashtagCategory(category: str) -> bool: if not category: return False - invalidChars = (',', ' ', '<', ';', '\\') + invalidChars = (',', ' ', '<', ';', '\\', '"', '&', '#') for ch in invalidChars: if ch in category: return False diff --git a/content.py b/content.py index ff5c4aa97..1d8334412 100644 --- a/content.py +++ b/content.py @@ -10,6 +10,7 @@ import os import email.parser import urllib.parse from shutil import copyfile +from utils import isValidLanguage from utils import getImageExtensions from utils import loadJson from utils import fileLastModified @@ -377,12 +378,19 @@ def validHashTag(hashtag: str) -> bool: # long hashtags are not valid if len(hashtag) >= 32: return False - # TODO: this may need to be an international character set validChars = set('0123456789' + 'abcdefghijklmnopqrstuvwxyz' + - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + '¡¿ÄäÀàÁáÂâÃãÅåǍǎĄąĂăÆæĀā' + + 'ÇçĆćĈĉČčĎđĐďðÈèÉéÊêËëĚěĘęĖėĒē' + + 'ĜĝĢģĞğĤĥÌìÍíÎîÏïıĪīĮįĴĵĶķ' + + 'ĹĺĻļŁłĽľĿŀÑñŃńŇňŅņÖöÒòÓóÔôÕõŐőØøŒœ' + + 'ŔŕŘřẞߌśŜŝŞşŠšȘșŤťŢţÞþȚțÜüÙùÚúÛûŰűŨũŲųŮůŪū' + + 'ŴŵÝýŸÿŶŷŹźŽžŻż') if set(hashtag).issubset(validChars): return True + if isValidLanguage(hashtag): + return True return False diff --git a/context.py b/context.py index 1a0a3ad17..11d9b727d 100644 --- a/context.py +++ b/context.py @@ -13,7 +13,8 @@ validContexts = ( "https://w3id.org/security/v1", "*/apschema/v1.9", "*/apschema/v1.21", - "*/litepub-0.1.jsonld" + "*/litepub-0.1.jsonld", + "https://litepub.social/litepub/context.jsonld" ) @@ -129,6 +130,33 @@ def getApschemaV1_21() -> {}: } +def getLitepubSocial() -> {}: + # https://litepub.social/litepub/context.jsonld + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { + 'Emoji': 'toot:Emoji', + 'Hashtag': 'as:Hashtag', + 'PropertyValue': 'schema:PropertyValue', + 'atomUri': 'ostatus:atomUri', + 'conversation': { + '@id': 'ostatus:conversation', + '@type': '@id' + }, + 'manuallyApprovesFollowers': 'as:manuallyApprovesFollowers', + 'ostatus': 'http://ostatus.org#', + 'schema': 'http://schema.org', + 'sensitive': 'as:sensitive', + 'toot': 'http://joinmastodon.org/ns#', + 'totalItems': 'as:totalItems', + 'value': 'schema:value' + } + ] + } + + def getLitepubV0_1() -> {}: # https://domain/schemas/litepub-0.1.jsonld return { diff --git a/daemon.py b/daemon.py index 675a205de..7afb725b9 100644 --- a/daemon.py +++ b/daemon.py @@ -2344,15 +2344,15 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('You cannot follow the news actor') else: - if debug: - print('Sending follow request from ' + - followerNickname + ' to ' + followingActor) + print('Sending follow request from ' + + followerNickname + ' to ' + followingActor) sendFollowRequest(self.server.session, baseDir, followerNickname, domain, port, httpPrefix, followingNickname, followingDomain, + followingActor, followingPort, httpPrefix, False, self.server.federationList, self.server.sendThreads, @@ -13537,8 +13537,10 @@ class PubServer(BaseHTTPRequestHandler): return # refuse to receive non-json content - if self.headers['Content-type'] != 'application/json' and \ - self.headers['Content-type'] != 'application/activity+json': + contentTypeStr = self.headers['Content-type'] + if not contentTypeStr.startswith('application/json') and \ + not contentTypeStr.startswith('application/activity+json') and \ + not contentTypeStr.startswith('application/ld+json'): print("POST is not json: " + self.headers['Content-type']) if self.server.debug: print(str(self.headers)) diff --git a/emoji/copyleft.png b/emoji/copyleft.png new file mode 100644 index 000000000..e13f7a266 Binary files /dev/null and b/emoji/copyleft.png differ diff --git a/emoji/default_emoji.json b/emoji/default_emoji.json index 20780feac..9f86c0384 100644 --- a/emoji/default_emoji.json +++ b/emoji/default_emoji.json @@ -98,6 +98,7 @@ "confused": "1F615", "confusedface": "1F615", "copyleftsymbol": "1F12F", + "copyleft": "1F12F", "copyright": "00A9", "couchandlamp": "1F6CB", "couplewithheart": "1F491", diff --git a/epicyon.py b/epicyon.py index b776e0bbd..1a741302c 100644 --- a/epicyon.py +++ b/epicyon.py @@ -1379,6 +1379,10 @@ if args.actor: nickname = args.actor.split('/accounts/')[1] nickname = nickname.replace('\n', '').replace('\r', '') domain = args.actor.split('/accounts/')[0] + elif '/u/' in args.actor: + nickname = args.actor.split('/u/')[1] + nickname = nickname.replace('\n', '').replace('\r', '') + domain = args.actor.split('/u/')[0] else: # format: @nick@domain if '@' not in args.actor: @@ -1445,6 +1449,7 @@ if args.actor: personUrl = personUrl.replace('/accounts/', '/actor/') personUrl = personUrl.replace('/channel/', '/actor/') personUrl = personUrl.replace('/profile/', '/actor/') + personUrl = personUrl.replace('/u/', '/actor/') if not personUrl: # try single user instance personUrl = httpPrefix + '://' + domain @@ -1508,6 +1513,10 @@ if args.followers: nickname = args.followers.split('/accounts/')[1] nickname = nickname.replace('\n', '').replace('\r', '') domain = args.followers.split('/accounts/')[0] + elif '/u/' in args.followers: + nickname = args.followers.split('/u/')[1] + nickname = nickname.replace('\n', '').replace('\r', '') + domain = args.followers.split('/u/')[0] else: # format: @nick@domain if '@' not in args.followers: @@ -1572,6 +1581,7 @@ if args.followers: personUrl = personUrl.replace('/accounts/', '/actor/') personUrl = personUrl.replace('/channel/', '/actor/') personUrl = personUrl.replace('/profile/', '/actor/') + personUrl = personUrl.replace('/u/', '/actor/') if not personUrl: # try single user instance personUrl = httpPrefix + '://' + domain diff --git a/follow.py b/follow.py index 085c7aecb..cce18f1b2 100644 --- a/follow.py +++ b/follow.py @@ -204,6 +204,9 @@ def isFollowerOfPerson(baseDir: str, nickname: str, domain: str, elif '://' + followerDomain + \ '/accounts/' + followerNickname in followersStr: alreadyFollowing = True + elif '://' + followerDomain + \ + '/u/' + followerNickname in followersStr: + alreadyFollowing = True return alreadyFollowing @@ -542,6 +545,8 @@ def _storeFollowRequest(baseDir: str, alreadyFollowing = True elif '://' + domainFull + '/accounts/' + nickname in followersStr: alreadyFollowing = True + elif '://' + domainFull + '/u/' + nickname in followersStr: + alreadyFollowing = True if alreadyFollowing: if debug: @@ -598,7 +603,8 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, """Receives a follow request within the POST section of HTTPServer """ if not messageJson['type'].startswith('Follow'): - return False + if not messageJson['type'].startswith('Join'): + return False print('Receiving follow request') if not messageJson.get('actor'): if debug: @@ -866,6 +872,7 @@ def followedAccountRejects(session, baseDir: str, httpPrefix: str, def sendFollowRequest(session, baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, followNickname: str, followDomain: str, + followedActor: str, followPort: int, followHttpPrefix: str, clientToServer: bool, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {}, @@ -874,6 +881,7 @@ def sendFollowRequest(session, baseDir: str, """Gets the json object for sending a follow request """ if not domainPermitted(followDomain, federationList): + print('You are not permitted to follow the domain ' + followDomain) return None fullDomain = getFullDomain(domain, port) @@ -884,8 +892,7 @@ def sendFollowRequest(session, baseDir: str, statusNumber, published = getStatusNumber() if followNickname: - followedId = followHttpPrefix + '://' + \ - requestDomain + '/users/' + followNickname + followedId = followedActor followHandle = followNickname + '@' + requestDomain else: if debug: @@ -1162,7 +1169,8 @@ def outboxUndoFollow(baseDir: str, messageJson: {}, debug: bool) -> None: if not messageJson['object'].get('type'): return if not messageJson['object']['type'] == 'Follow': - return + if not messageJson['object']['type'] == 'Join': + return if not messageJson['object'].get('object'): return if not messageJson['object'].get('actor'): diff --git a/img/screenshot_lynx.jpg b/img/screenshot_lynx.jpg index 47ef21ce4..2790a4593 100644 Binary files a/img/screenshot_lynx.jpg and b/img/screenshot_lynx.jpg differ diff --git a/inbox.py b/inbox.py index c213e0d22..26d9e5268 100644 --- a/inbox.py +++ b/inbox.py @@ -105,6 +105,8 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: for tag in postJsonObject['object']['tag']: if not tag.get('type'): continue + if not isinstance(tag['type'], str): + continue if tag['type'] != 'Hashtag': continue if not tag.get('name'): @@ -274,8 +276,30 @@ def inboxMessageHasParams(messageJson: {}) -> bool: # print('inboxMessageHasParams: ' + # param + ' ' + str(messageJson)) return False + + # actor should be a string + if not isinstance(messageJson['actor'], str): + print('WARN: actor should be a string, but is actually: ' + + str(messageJson['actor'])) + return False + + # type should be a string + if not isinstance(messageJson['type'], str): + print('WARN: type from ' + str(messageJson['actor']) + + ' should be a string, but is actually: ' + + str(messageJson['type'])) + return False + + # object should be a dict or a string + if not isinstance(messageJson['object'], dict): + if not isinstance(messageJson['object'], str): + print('WARN: object from ' + str(messageJson['actor']) + + ' should be a dict or string, but is actually: ' + + str(messageJson['object'])) + return False + if not messageJson.get('to'): - allowedWithoutToParam = ['Like', 'Follow', 'Request', + allowedWithoutToParam = ['Like', 'Follow', 'Join', 'Request', 'Accept', 'Capability', 'Undo'] if messageJson['type'] not in allowedWithoutToParam: return False @@ -297,7 +321,7 @@ def inboxPermittedMessage(domain: str, messageJson: {}, if not urlPermitted(actor, federationList): return False - alwaysAllowedTypes = ('Follow', 'Like', 'Delete', 'Announce') + alwaysAllowedTypes = ('Follow', 'Join', 'Like', 'Delete', 'Announce') if messageJson['type'] not in alwaysAllowedTypes: if not messageJson.get('object'): return True @@ -693,7 +717,8 @@ def _receiveUndo(session, baseDir: str, httpPrefix: str, print('DEBUG: ' + messageJson['type'] + ' object within object is not a string') return False - if messageJson['object']['type'] == 'Follow': + if messageJson['object']['type'] == 'Follow' or \ + messageJson['object']['type'] == 'Join': return _receiveUndoFollow(session, baseDir, httpPrefix, port, messageJson, federationList, debug) @@ -731,7 +756,7 @@ def _personReceiveUpdate(baseDir: str, ' ' + str(personJson)) domainFull = getFullDomain(domain, port) updateDomainFull = getFullDomain(updateDomain, updatePort) - usersPaths = ('users', 'profile', 'channel', 'accounts') + usersPaths = ('users', 'profile', 'channel', 'accounts', 'u') usersStrFound = False for usersStr in usersPaths: actor = updateDomainFull + '/' + usersStr + '/' + updateNickname diff --git a/manualapprove.py b/manualapprove.py index 86315a797..9d2944d8d 100644 --- a/manualapprove.py +++ b/manualapprove.py @@ -120,6 +120,9 @@ def manualApproveFollowRequest(session, baseDir: str, elif reqPrefix + '/accounts/' + reqNick in approveFollowsStr: exists = True approveHandleFull = reqPrefix + '/accounts/' + reqNick + elif reqPrefix + '/u/' + reqNick in approveFollowsStr: + exists = True + approveHandleFull = reqPrefix + '/u/' + reqNick if not exists: print('Manual follow accept: ' + approveHandleFull + ' not in requests file "' + diff --git a/posts.py b/posts.py index a0920284c..d44257bd2 100644 --- a/posts.py +++ b/posts.py @@ -2414,7 +2414,8 @@ def sendToNamedAddresses(session, baseDir: str, print('DEBUG: ' + 'no "to" field when sending to named addresses') if postJsonObject['object'].get('type'): - if postJsonObject['object']['type'] == 'Follow': + if postJsonObject['object']['type'] == 'Follow' or \ + postJsonObject['object']['type'] == 'Join': if isinstance(postJsonObject['object']['object'], str): if debug: print('DEBUG: "to" field assigned to Follow') diff --git a/pyjsonld.py b/pyjsonld.py index 0ccffe504..36b44d0bb 100644 --- a/pyjsonld.py +++ b/pyjsonld.py @@ -40,6 +40,7 @@ from numbers import Integral, Real from context import getApschemaV1_9 from context import getApschemaV1_21 from context import getLitepubV0_1 +from context import getLitepubSocial from context import getV1Schema from context import getV1SecuritySchema from context import getActivitystreamsSchema @@ -420,6 +421,13 @@ def load_document(url): 'document': getLitepubV0_1() } return doc + elif url == 'https://litepub.social/litepub/context.jsonld': + doc = { + 'contextUrl': None, + 'documentUrl': url, + 'document': getLitepubSocial() + } + return doc return None except JsonLdError as e: raise e diff --git a/setup.cfg b/setup.cfg index 6b3bd5005..2a7d568f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = epicyon -version = 1.2.0 +version = 1.3.0 author = Bob Mottram author_email = bob@freedombone.net maintainer = Bob Mottram diff --git a/tests.py b/tests.py index 83ecabca7..308e1fb0c 100644 --- a/tests.py +++ b/tests.py @@ -76,6 +76,7 @@ from inbox import jsonPostAllowsComments from inbox import validInbox from inbox import validInboxFilenames from categories import guessHashtagCategory +from content import validHashTag from content import htmlReplaceEmailQuote from content import htmlReplaceQuoteMarks from content import dangerousCSS @@ -848,10 +849,12 @@ def testFollowBetweenServers(): alicePersonCache = {} aliceCachedWebfingers = {} alicePostLog = [] + bobActor = httpPrefix + '://' + bobAddress + '/users/bob' sendResult = \ sendFollowRequest(sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, httpPrefix, - 'bob', bobDomain, bobPort, httpPrefix, + 'bob', bobDomain, bobActor, + bobPort, httpPrefix, clientToServer, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers, alicePersonCache, @@ -3088,9 +3091,25 @@ def testPrepareHtmlPostNickname(): assert result == expectedHtml +def testValidHashTag(): + print('testValidHashTag') + assert validHashTag('ThisIsValid') + assert validHashTag('ThisIsValid12345') + assert validHashTag('ThisIsVälid') + assert validHashTag('यहमान्यहै') + assert not validHashTag('ThisIsNotValid!') + assert not validHashTag('#ThisIsAlsoNotValid') + assert not validHashTag('#यहमान्यहै') + assert not validHashTag('ThisIsAlso&NotValid') + assert not validHashTag('ThisIsAlsoNotValid"') + assert not validHashTag('This Is Also Not Valid"') + assert not validHashTag('This=IsAlsoNotValid"') + + def runAllTests(): print('Running tests...') testFunctions() + testValidHashTag() testPrepareHtmlPostNickname() testDomainHandling() testMastoApi() diff --git a/utils.py b/utils.py index f024a2b98..0f3d811cf 100644 --- a/utils.py +++ b/utils.py @@ -67,7 +67,7 @@ def getLockedAccount(actorJson: {}) -> bool: def hasUsersPath(pathStr: str) -> bool: """Whether there is a /users/ path (or equivalent) in the given string """ - usersList = ('users', 'accounts', 'channel', 'profile') + usersList = ('users', 'accounts', 'channel', 'profile', 'u') for usersStr in usersList: if '/' + usersStr + '/' in pathStr: return True @@ -656,6 +656,12 @@ def getNicknameFromActor(actor: str) -> str: return nickStr else: return nickStr.split('/')[0] + elif '/u/' in actor: + nickStr = actor.split('/u/')[1].replace('@', '') + if '/' not in nickStr: + return nickStr + else: + return nickStr.split('/')[0] elif '/@' in actor: # https://domain/@nick nickStr = actor.split('/@')[1] @@ -696,6 +702,10 @@ def getDomainFromActor(actor: str) -> (str, int): domain = actor.split('/users/')[0] for prefix in prefixes: domain = domain.replace(prefix, '') + elif '/u/' in actor: + domain = actor.split('/u/')[0] + for prefix in prefixes: + domain = domain.replace(prefix, '') elif '/@' in actor: domain = actor.split('/@')[0] for prefix in prefixes: @@ -1163,14 +1173,58 @@ def deletePost(baseDir: str, httpPrefix: str, os.remove(postFilename) -def validNickname(domain: str, nickname: str) -> bool: - forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@', '#') - for c in forbiddenChars: - if c in nickname: - return False - # this should only apply for the shared inbox - if nickname == domain: - return False +def isValidLanguage(text: str) -> bool: + """Returns true if the given text contains a valid + natural language string + """ + naturalLanguages = { + "Latin": [65, 866], + "Cyrillic": [1024, 1274], + "Greek": [880, 1280], + "isArmenian": [1328, 1424], + "isHebrew": [1424, 1536], + "Arabic": [1536, 1792], + "Syriac": [1792, 1872], + "Thaan": [1920, 1984], + "Devanagari": [2304, 2432], + "Bengali": [2432, 2560], + "Gurmukhi": [2560, 2688], + "Gujarati": [2688, 2816], + "Oriya": [2816, 2944], + "Tamil": [2944, 3072], + "Telugu": [3072, 3200], + "Kannada": [3200, 3328], + "Malayalam": [3328, 3456], + "Sinhala": [3456, 3584], + "Thai": [3584, 3712], + "Lao": [3712, 3840], + "Tibetan": [3840, 4096], + "Myanmar": [4096, 4256], + "Georgian": [4256, 4352], + "HangulJamo": [4352, 4608], + "Cherokee": [5024, 5120], + "UCAS": [5120, 5760], + "Ogham": [5760, 5792], + "Runic": [5792, 5888], + "Khmer": [6016, 6144], + "Mongolian": [6144, 6320] + } + for langName, langRange in naturalLanguages.items(): + okLang = True + for ch in text: + if ch.isdigit(): + continue + if ord(ch) not in range(langRange[0], langRange[1]): + okLang = False + break + if okLang: + return True + return False + + +def _isReservedName(nickname: str) -> bool: + """Is the given nickname reserved for some special function? + """ reservedNames = ('inbox', 'dm', 'outbox', 'following', 'public', 'followers', 'category', 'channel', 'calendar', @@ -1180,10 +1234,27 @@ def validNickname(domain: str, nickname: str) -> bool: 'activity', 'undo', 'pinned', 'reply', 'replies', 'question', 'like', 'likes', 'users', 'statuses', 'tags', - 'accounts', 'channels', 'profile', + 'accounts', 'channels', 'profile', 'u', 'updates', 'repeat', 'announce', 'shares', 'fonts', 'icons', 'avatars') if nickname in reservedNames: + return True + return False + + +def validNickname(domain: str, nickname: str) -> bool: + """Is the given nickname valid? + """ + if not isValidLanguage(nickname): + return False + forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@', '#') + for c in forbiddenChars: + if c in nickname: + return False + # this should only apply for the shared inbox + if nickname == domain: + return False + if _isReservedName(nickname): return False return True diff --git a/webapp_create_post.py b/webapp_create_post.py index 6387f9c5a..0a682af4d 100644 --- a/webapp_create_post.py +++ b/webapp_create_post.py @@ -136,12 +136,12 @@ def _htmlNewPostDropDown(scopeIcon: str, scopeDescription: str, 'icons/scope_reminder.png"/>' + \ translate['Reminder'] + '
' + \ translate['Scheduled note to yourself'] + '\n' - dropDownContent += \ - '
  • ' + \ - translate['Event'] + '
    ' + \ - translate['Create an event'] + '
  • \n' + # dropDownContent += \ + # '
  • ' + \ + # translate['Event'] + '
    ' + \ + # translate['Create an event'] + '
  • \n' dropDownContent += \ '
  • ' + \ translate['Write your reply to'] + \ - ' ' + \ + ' ' + \ translate['this post'] + '

    \n' replyStr = '\n' diff --git a/webapp_hashtagswarm.py b/webapp_hashtagswarm.py index ebb9aa2cc..19f08661f 100644 --- a/webapp_hashtagswarm.py +++ b/webapp_hashtagswarm.py @@ -154,6 +154,11 @@ def htmlHashTagSwarm(baseDir: str, actor: str, translate: {}) -> str: if len(hashTagName) > maxTagLength: # NoIncrediblyLongAndBoringHashtagsShownHere continue + if '#' in hashTagName or \ + '&' in hashTagName or \ + '"' in hashTagName or \ + "'" in hashTagName: + continue if '#' + hashTagName + '\n' in blockedStr: continue with open(tagsFilename, 'r') as fp: diff --git a/webapp_timeline.py b/webapp_timeline.py index 07c1843fa..f3707b826 100644 --- a/webapp_timeline.py +++ b/webapp_timeline.py @@ -158,7 +158,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, repliesButton = 'buttonhighlighted' mediaButton = 'button' bookmarksButton = 'button' - eventsButton = 'button' +# eventsButton = 'button' sentButton = 'button' sharesButton = 'button' if newShare: @@ -196,8 +196,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, sharesButton = 'buttonselectedhighlighted' elif boxName == 'tlbookmarks' or boxName == 'bookmarks': bookmarksButton = 'buttonselected' - elif boxName == 'tlevents': - eventsButton = 'buttonselected' +# elif boxName == 'tlevents': +# eventsButton = 'buttonselected' # get the full domain, including any port number fullDomain = getFullDomain(domain, port) @@ -254,11 +254,11 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, '' - - eventsButtonStr = \ - '' +# +# eventsButtonStr = \ +# '' instanceTitle = \ getConfigParam(baseDir, 'instanceTitle') @@ -400,8 +400,8 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, translate['Bookmarks'] menuShares = \ htmlHideFromScreenReader('🤝') + ' ' + sharesStr - menuEvents = \ - htmlHideFromScreenReader('🎫') + ' ' + translate['Events'] +# menuEvents = \ +# htmlHideFromScreenReader('🎫') + ' ' + translate['Events'] menuBlogs = \ htmlHideFromScreenReader('📝') + ' ' + translate['Blogs'] menuNewswire = \ @@ -426,7 +426,7 @@ def htmlTimeline(cssCache: {}, defaultTimeline: str, menuBookmarks: usersPath + '/tlbookmarks#timeline', menuShares: usersPath + '/tlshares#timeline', menuBlogs: usersPath + '/tlblogs#timeline', - menuEvents: usersPath + '/tlevents#timeline', + # menuEvents: usersPath + '/tlevents#timeline', menuNewswire: '#newswire', menuLinks: '#links' }