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

merge-requests/30/head
Bob Mottram 2021-02-09 20:07:46 +00:00
commit e98a5d0d50
20 changed files with 241 additions and 49 deletions

View File

@ -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'):

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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))

BIN
emoji/copyleft.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -98,6 +98,7 @@
"confused": "1F615",
"confusedface": "1F615",
"copyleftsymbol": "1F12F",
"copyleft": "1F12F",
"copyright": "00A9",
"couchandlamp": "1F6CB",
"couplewithheart": "1F491",

View File

@ -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

View File

@ -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'):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -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

View File

@ -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 "' +

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -136,12 +136,12 @@ def _htmlNewPostDropDown(scopeIcon: str, scopeDescription: str,
'icons/scope_reminder.png"/><b>' + \
translate['Reminder'] + '</b><br>' + \
translate['Scheduled note to yourself'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownEventSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
'icons/scope_event.png"/><b>' + \
translate['Event'] + '</b><br>' + \
translate['Create an event'] + '</a></li>\n'
# dropDownContent += \
# '<li><a href="' + pathBase + dropdownEventSuffix + \
# '"><img loading="lazy" alt="" title="" src="/' + \
# 'icons/scope_event.png"/><b>' + \
# translate['Event'] + '</b><br>' + \
# translate['Create an event'] + '</a></li>\n'
dropDownContent += \
'<li><a href="' + pathBase + dropdownReportSuffix + \
'"><img loading="lazy" alt="" title="" src="/' + \
@ -198,7 +198,9 @@ def htmlNewPost(cssCache: {}, mediaInstance: bool, translate: {},
newPostText = \
'<p class="new-post-text">' + \
translate['Write your reply to'] + \
' <a href="' + inReplyTo + '">' + \
' <a href="' + inReplyTo + \
'" rel="nofollow noopener noreferrer" ' + \
'target="_blank">' + \
translate['this post'] + '</a></p>\n'
replyStr = '<input type="hidden" ' + \
'name="replyTo" value="' + inReplyTo + '">\n'

View File

@ -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:

View File

@ -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,
'<a href="' + usersPath + '/tlbookmarks"><button class="' + \
bookmarksButton + '"><span>' + translate['Bookmarks'] + \
'</span></button></a>'
eventsButtonStr = \
'<a href="' + usersPath + '/tlevents"><button class="' + \
eventsButton + '"><span>' + translate['Events'] + \
'</span></button></a>'
#
# eventsButtonStr = \
# '<a href="' + usersPath + '/tlevents"><button class="' + \
# eventsButton + '"><span>' + translate['Events'] + \
# '</span></button></a>'
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'
}