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 += \
- '