Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6df852e34 | ||
|
|
ffc93abf3b | ||
|
|
fc2732a5ed | ||
|
|
a6dc579aca | ||
|
|
8d78bb6f6b |
No files matched your search
@@ -1,7 +1,5 @@
|
||||
all:
|
||||
debug:
|
||||
clean:
|
||||
rm -f *.*~ *~
|
||||
rm -f orgs/*~
|
||||
rm -f scripts/*~
|
||||
rm -f *.txt~ *.md~ *.py~ *.json~ *.css~ Makefile~
|
||||
rm -rf __pycache__
|
||||
@@ -6,11 +6,9 @@ Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant s
|
||||
|
||||
[Project Goals](README_goals.md) - [Commandline interface](README_commandline.md) - [Customizations](README_customizations.md) - [Object Capabilities](ocaps.md) - [Code of Conduct](code-of-conduct.md)
|
||||
|
||||
Matrix room: **#epicyon:matrix.freedombone.net**
|
||||
Includes emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0)
|
||||
|
||||
Includes emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0). Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
<img src="https://code.freedombone.net/bashrc/epicyon/raw/main/img/screenshot_light.jpg" width="80%"/>
|
||||
<img src="https://code.freedombone.net/bashrc/epicyon/raw/master/img/screenshots.jpg?raw=true" width="80%"/>
|
||||
|
||||
## Package Dependencies
|
||||
|
||||
@@ -28,7 +26,7 @@ Or on Debian:
|
||||
``` bash
|
||||
sudo apt-get -y install tor python3-pip python3-socks imagemagick \
|
||||
python3-numpy python3-setuptools python3-crypto \
|
||||
python3-dateutil python3-pil.imagetk certbot nginx
|
||||
python3-dateutil python3-pil.imagetk certbot
|
||||
sudo pip3 install commentjson beautifulsoup4 pycryptodome
|
||||
```
|
||||
|
||||
@@ -36,8 +34,6 @@ sudo pip3 install commentjson beautifulsoup4 pycryptodome
|
||||
|
||||
In the most common case you'll be using systemd to set up a daemon to run the server.
|
||||
|
||||
The following instructions install Epicyon to the **/etc** directory. It's not essential that it be installed there, and it could be in any other preferred directory.
|
||||
|
||||
Add a dedicated user so that we don't have to run as root.
|
||||
|
||||
``` bash
|
||||
@@ -57,7 +53,7 @@ Type=simple
|
||||
User=epicyon
|
||||
Group=epicyon
|
||||
WorkingDirectory=/etc/epicyon
|
||||
ExecStart=/usr/bin/python3 /etc/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open
|
||||
ExecStart=/usr/bin/python3 /etc/epicyon/epicyon.py --port 443 --proxy 7156 --domain YOUR_DOMAIN --registration open --debug
|
||||
Environment=USER=epicyon
|
||||
Restart=always
|
||||
StandardError=syslog
|
||||
@@ -95,11 +91,15 @@ server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name YOUR_DOMAIN;
|
||||
root /var/www/YOUR_DOMAIN/htdocs;
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
client_max_body_size 31m;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
limit_conn conn_limit_per_ip 10;
|
||||
limit_req zone=req_limit_per_ip burst=10 nodelay;
|
||||
|
||||
index index.html;
|
||||
rewrite ^ https://$server_name$request_uri? permanent;
|
||||
}
|
||||
@@ -132,6 +132,7 @@ server {
|
||||
access_log /dev/null;
|
||||
error_log /dev/null;
|
||||
|
||||
root /var/www/YOUR_DOMAIN/htdocs;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
|
||||
@@ -34,24 +34,6 @@ To set the background image for an account:
|
||||
python3 epicyon.py --nickname [nick] --domain [name] --background [image filename]
|
||||
```
|
||||
|
||||
## Groups
|
||||
|
||||
Groups are a special type of account which relays any posts sent to it to its members (followers).
|
||||
|
||||
To create a group:
|
||||
|
||||
``` bash
|
||||
python3 epicyon.py --addgroup nickname@domain --password [yourpassword]
|
||||
```
|
||||
|
||||
To remove an account (be careful!):
|
||||
|
||||
``` bash
|
||||
python3 epicyon.py --rmgroup nickname@domain
|
||||
```
|
||||
|
||||
Setting avatar or changing background is the same as for any other account on the system. You can also moderate a group, applying filters, blocks or a perimeter, in the same way as for other acounts.
|
||||
|
||||
## Defining a perimeter
|
||||
|
||||
By default the server will federate with any others, but there may be cases where you want to limit this down to a defined set of servers within an organization.
|
||||
|
||||
@@ -26,34 +26,3 @@ When a moderator report is created the message at the top of the screen can be c
|
||||
|
||||
Extra emoji can be added to the *emoji* directory and you should then update the **emoji/emoji.json** file, which maps the name to the filename (without the .png extension).
|
||||
|
||||
## Style / Colors
|
||||
|
||||
There are a few predefined themes which you can change to. Move to the directory where Epicyon is installed, then you can use:
|
||||
|
||||
``` bash
|
||||
theme default
|
||||
theme light
|
||||
theme hacker
|
||||
theme purple
|
||||
```
|
||||
|
||||
Alternatively if you want to create a much more customized theme then you can manually edit css files.
|
||||
|
||||
Create copies of the default files:
|
||||
|
||||
``` bash
|
||||
cp epicyon-profile.css epicyon.css
|
||||
cp epicyon-follow.css follow.css
|
||||
cp epicyon-login.css login.css
|
||||
cp epicyon-suspended.css suspended.css
|
||||
```
|
||||
|
||||
You can then edit *epicyon.css*, *follow.css*, *login.css* and *follow.css* as needed and those files won't be overwritten if you upgrade.
|
||||
|
||||
*epicyon.css* is the main style for displaying profiles and timelines.
|
||||
|
||||
*follow.css* is used for displaying options when you select an avatar.
|
||||
|
||||
*login.css* defines the style of the login screen.
|
||||
|
||||
*suspended.css* is the style for the screen which shows that an account has been suspended.
|
||||
@@ -63,7 +63,8 @@ def createAccept(baseDir: str,federationList: [], \
|
||||
# create capabilities accept
|
||||
ocapNew= \
|
||||
capabilitiesAccept(baseDir,httpPrefix, \
|
||||
nickname,domain,port,toUrl,True,acceptedCaps)
|
||||
nickname,domain,port, \
|
||||
toUrl,True,acceptedCaps)
|
||||
return createAcceptReject(baseDir,federationList, \
|
||||
nickname,domain,port, \
|
||||
toUrl,ccUrl,httpPrefix, \
|
||||
@@ -97,6 +98,17 @@ def acceptFollow(baseDir: str,domain : str,messageJson: {}, \
|
||||
if debug:
|
||||
print('DEBUG: No "to" parameter in follow Accept')
|
||||
return
|
||||
#if len(messageJson['object']['to'])!=1:
|
||||
# if debug:
|
||||
# print('DEBUG: "to" does not contain a single recipient')
|
||||
# print(str(messageJson['object']['to']))
|
||||
# if messageJson['object'].get('object'):
|
||||
# if not isinstance(messageJson['object']['object'], str):
|
||||
# messageJson['object']['to']=messageJson['object']['object']
|
||||
# else:
|
||||
# return
|
||||
# else:
|
||||
# return
|
||||
if debug:
|
||||
print('DEBUG: follow Accept received')
|
||||
thisActor=messageJson['object']['actor']
|
||||
@@ -109,6 +121,10 @@ def acceptFollow(baseDir: str,domain : str,messageJson: {}, \
|
||||
if debug:
|
||||
print('DEBUG: domain not found in '+thisActor)
|
||||
return
|
||||
#if acceptedDomain != domain:
|
||||
# if debug:
|
||||
# print('DEBUG: domain mismatch '+acceptedDomain+' != '+domain)
|
||||
# return
|
||||
if not nickname:
|
||||
if debug:
|
||||
print('DEBUG: nickname not found in '+thisActor)
|
||||
@@ -117,13 +133,12 @@ def acceptFollow(baseDir: str,domain : str,messageJson: {}, \
|
||||
if '/'+acceptedDomain+':'+str(acceptedPort)+'/users/'+nickname not in thisActor:
|
||||
if debug:
|
||||
print('Port: '+str(acceptedPort))
|
||||
print('Expected: /'+acceptedDomain+':'+str(acceptedPort)+ \
|
||||
'/users/'+nickname)
|
||||
print('Expected: /'+acceptedDomain+':'+str(acceptedPort)+'/users/'+nickname)
|
||||
print('Actual: '+thisActor)
|
||||
print('DEBUG: unrecognized actor '+thisActor)
|
||||
return
|
||||
else:
|
||||
if not '/'+acceptedDomain+'/users/'+nickname in thisActor:
|
||||
if '/'+acceptedDomain+'/users/'+nickname not in thisActor:
|
||||
if debug:
|
||||
print('Expected: /'+acceptedDomain+'/users/'+nickname)
|
||||
print('Actual: '+thisActor)
|
||||
@@ -156,13 +171,10 @@ def acceptFollow(baseDir: str,domain : str,messageJson: {}, \
|
||||
followedNickname,followedDomainFull, \
|
||||
federationList,debug):
|
||||
if debug:
|
||||
print('DEBUG: '+nickname+'@'+acceptedDomainFull+ \
|
||||
' followed '+followedNickname+'@'+followedDomainFull)
|
||||
print('DEBUG: '+nickname+'@'+acceptedDomainFull+' followed '+followedNickname+'@'+followedDomainFull)
|
||||
else:
|
||||
if debug:
|
||||
print('DEBUG: Unable to create follow - '+ \
|
||||
nickname+'@'+acceptedDomain+' -> '+ \
|
||||
followedNickname+'@'+followedDomain)
|
||||
print('DEBUG: Unable to create follow - '+nickname+'@'+acceptedDomain+' -> '+followedNickname+'@'+followedDomain)
|
||||
|
||||
def receiveAcceptReject(session,baseDir: str, \
|
||||
httpPrefix: str,domain :str,port: int, \
|
||||
@@ -177,25 +189,20 @@ def receiveAcceptReject(session,baseDir: str, \
|
||||
if debug:
|
||||
print('DEBUG: '+messageJson['type']+' has no actor')
|
||||
return False
|
||||
if '/users/' not in messageJson['actor'] and \
|
||||
'/channel/' not in messageJson['actor'] and \
|
||||
'/profile/' not in messageJson['actor']:
|
||||
if '/users/' not in messageJson['actor'] and '/profile/' not in messageJson['actor']:
|
||||
if debug:
|
||||
print('DEBUG: "users" or "profile" missing from actor in '+ \
|
||||
messageJson['type']+'. Assuming single user instance.')
|
||||
print('DEBUG: "users" or "profile" missing from actor in '+messageJson['type'])
|
||||
return False
|
||||
domain,tempPort=getDomainFromActor(messageJson['actor'])
|
||||
if not domainPermitted(domain,federationList):
|
||||
if debug:
|
||||
print('DEBUG: '+messageJson['type']+ \
|
||||
' from domain not permitted - '+domain)
|
||||
print('DEBUG: '+messageJson['type']+' from domain not permitted - '+domain)
|
||||
return False
|
||||
nickname=getNicknameFromActor(messageJson['actor'])
|
||||
if not nickname:
|
||||
# single user instance
|
||||
nickname='dev'
|
||||
if debug:
|
||||
print('DEBUG: '+messageJson['type']+ \
|
||||
' does not contain a nickname. Assuming single user instance.')
|
||||
print('DEBUG: '+messageJson['type']+' does not contain a nickname')
|
||||
return False
|
||||
handle=nickname.lower()+'@'+domain.lower()
|
||||
# receive follow accept
|
||||
acceptFollow(baseDir,domain,messageJson,federationList,debug)
|
||||
|
||||
@@ -6,8 +6,6 @@ __maintainer__ = "Bob Mottram"
|
||||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import commentjson
|
||||
from pprint import pprint
|
||||
@@ -17,9 +15,6 @@ from utils import urlPermitted
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
from utils import locatePost
|
||||
from utils import getCachedPostFilename
|
||||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
from posts import sendSignedJson
|
||||
from posts import getPersonBox
|
||||
from session import postJson
|
||||
@@ -46,8 +41,7 @@ def outboxAnnounce(baseDir: str,messageJson: {},debug: bool) -> bool:
|
||||
domain,port=getDomainFromActor(messageJson['actor'])
|
||||
postFilename=locatePost(baseDir,nickname,domain,messageJson['object'])
|
||||
if postFilename:
|
||||
updateAnnounceCollection(baseDir,postFilename, \
|
||||
messageJson['actor'],domain,debug)
|
||||
updateAnnounceCollection(postFilename,messageJson['actor'],debug)
|
||||
return True
|
||||
if messageJson['type']=='Undo':
|
||||
if not isinstance(messageJson['object'], dict):
|
||||
@@ -62,32 +56,20 @@ def outboxAnnounce(baseDir: str,messageJson: {},debug: bool) -> bool:
|
||||
print('WARN: no nickname found in '+messageJson['actor'])
|
||||
return False
|
||||
domain,port=getDomainFromActor(messageJson['actor'])
|
||||
postFilename= \
|
||||
locatePost(baseDir,nickname,domain, \
|
||||
messageJson['object']['object'])
|
||||
postFilename=locatePost(baseDir,nickname,domain,messageJson['object']['object'])
|
||||
if postFilename:
|
||||
undoAnnounceCollectionEntry(baseDir,postFilename, \
|
||||
messageJson['actor'], \
|
||||
domain,debug)
|
||||
undoAnnounceCollectionEntry(postFilename,messageJson['actor'],debug)
|
||||
return True
|
||||
return False
|
||||
|
||||
def undoAnnounceCollectionEntry(baseDir: str,postFilename: str, \
|
||||
actor: str,domain: str,debug: bool) -> None:
|
||||
"""Undoes an announce for a particular actor by removing it from
|
||||
the "shares" collection within a post. Note that the "shares"
|
||||
collection has no relation to shared items in shares.py. It's
|
||||
shares of posts, not shares of physical objects.
|
||||
def undoAnnounceCollectionEntry(postFilename: str,actor: str, \
|
||||
debug: bool) -> None:
|
||||
"""Undoes an announce for a particular actor by removing it from the "shares"
|
||||
collection within a post. Note that the "shares" collection has no relation
|
||||
to shared items in shares.py. It's shares of posts, not shares of physical objects.
|
||||
"""
|
||||
postJsonObject=loadJson(postFilename)
|
||||
if postJsonObject:
|
||||
# remove any cached version of this announce so that the like icon is changed
|
||||
nickname=getNicknameFromActor(actor)
|
||||
cachedPostFilename= \
|
||||
getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
|
||||
if os.path.isfile(cachedPostFilename):
|
||||
os.remove(cachedPostFilename)
|
||||
|
||||
with open(postFilename, 'r') as fp:
|
||||
postJsonObject=commentjson.load(fp)
|
||||
if not postJsonObject.get('type'):
|
||||
return
|
||||
if postJsonObject['type']!='Create':
|
||||
@@ -123,25 +105,16 @@ def undoAnnounceCollectionEntry(baseDir: str,postFilename: str, \
|
||||
else:
|
||||
postJsonObject['object']['shares']['totalItems']= \
|
||||
len(postJsonObject['object']['shares']['items'])
|
||||
saveJson(postJsonObject,postFilename)
|
||||
with open(postFilename, 'w') as fp:
|
||||
commentjson.dump(postJsonObject, fp, indent=4, sort_keys=True)
|
||||
|
||||
def updateAnnounceCollection(baseDir: str,postFilename: str, \
|
||||
actor: str,domain: str,debug: bool) -> None:
|
||||
def updateAnnounceCollection(postFilename: str,actor: str,debug: bool) -> None:
|
||||
"""Updates the announcements collection within a post
|
||||
Confusingly this is known as "shares", but isn't the
|
||||
same as shared items within shares.py
|
||||
It's shares of posts, not shares of physical objects.
|
||||
Confusingly this is known as "shares", but isn't the same as shared
|
||||
items within shares.py. It's shares of posts, not shares of physical objects.
|
||||
"""
|
||||
postJsonObject=loadJson(postFilename)
|
||||
if postJsonObject:
|
||||
# remove any cached version of this announce so that the like
|
||||
# icon is changed
|
||||
nickname=getNicknameFromActor(actor)
|
||||
cachedPostFilename= \
|
||||
getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
|
||||
if os.path.isfile(cachedPostFilename):
|
||||
os.remove(cachedPostFilename)
|
||||
|
||||
with open(postFilename, 'r') as fp:
|
||||
postJsonObject=commentjson.load(fp)
|
||||
if not postJsonObject.get('object'):
|
||||
if debug:
|
||||
pprint(postJsonObject)
|
||||
@@ -152,8 +125,7 @@ def updateAnnounceCollection(baseDir: str,postFilename: str, \
|
||||
postUrl=postJsonObject['id'].replace('/activity','')+'/shares'
|
||||
if not postJsonObject['object'].get('shares'):
|
||||
if debug:
|
||||
print('DEBUG: Adding initial shares (announcements) to '+ \
|
||||
postUrl)
|
||||
print('DEBUG: Adding initial shares (announcements) to '+postUrl)
|
||||
announcementsJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'id': postUrl,
|
||||
@@ -185,7 +157,8 @@ def updateAnnounceCollection(baseDir: str,postFilename: str, \
|
||||
if debug:
|
||||
print('DEBUG: saving post with shares (announcements) added')
|
||||
pprint(postJsonObject)
|
||||
saveJson(postJsonObject,postFilename)
|
||||
with open(postFilename, 'w') as fp:
|
||||
commentjson.dump(postJsonObject, fp, indent=4, sort_keys=True)
|
||||
|
||||
def announcedByPerson(postJsonObject: {}, nickname: str,domain: str) -> bool:
|
||||
"""Returns True if the given post is announced by the given person
|
||||
@@ -248,14 +221,13 @@ def createAnnounce(session,baseDir: str,federationList: [], \
|
||||
if saveToFile:
|
||||
outboxDir = createOutboxDir(nickname,domain,baseDir)
|
||||
filename=outboxDir+'/'+newAnnounceId.replace('/','#')+'.json'
|
||||
saveJson(newAnnounce,filename)
|
||||
with open(filename, 'w') as fp:
|
||||
commentjson.dump(newAnnounce, fp, indent=4, sort_keys=False)
|
||||
|
||||
announceNickname=None
|
||||
announceDomain=None
|
||||
announcePort=None
|
||||
if '/users/' in objectUrl or \
|
||||
'/channel/' in objectUrl or \
|
||||
'/profile/' in objectUrl:
|
||||
if '/users/' in objectUrl or '/profile/' in objectUrl:
|
||||
announceNickname=getNicknameFromActor(objectUrl)
|
||||
announceDomain,announcePort=getDomainFromActor(objectUrl)
|
||||
|
||||
@@ -366,9 +338,7 @@ def undoAnnounce(session,baseDir: str,federationList: [], \
|
||||
announceNickname=None
|
||||
announceDomain=None
|
||||
announcePort=None
|
||||
if '/users/' in objectUrl or \
|
||||
'/channel/' in objectUrl or \
|
||||
'/profile/' in objectUrl:
|
||||
if '/users/' in objectUrl or '/profile/' in objectUrl:
|
||||
announceNickname=getNicknameFromActor(objectUrl)
|
||||
announceDomain,announcePort=getDomainFromActor(objectUrl)
|
||||
|
||||
@@ -457,8 +427,7 @@ def sendAnnounceViaServer(baseDir: str,session, \
|
||||
|
||||
statusNumber,published = getStatusNumber()
|
||||
newAnnounceId= \
|
||||
httpPrefix+'://'+fromDomainFull+'/users/'+ \
|
||||
fromNickname+'/statuses/'+statusNumber
|
||||
httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/statuses/'+statusNumber
|
||||
newAnnounceJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
|
||||
@@ -487,7 +456,7 @@ def sendAnnounceViaServer(baseDir: str,session, \
|
||||
# get the actor inbox for the To handle
|
||||
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
|
||||
getPersonBox(baseDir,session,wfRequest,personCache, \
|
||||
projectVersion,httpPrefix,fromNickname,fromDomain,postToBox)
|
||||
projectVersion,httpPrefix,fromDomain,postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
@@ -505,6 +474,10 @@ def sendAnnounceViaServer(baseDir: str,session, \
|
||||
'Authorization': authHeader}
|
||||
postResult = \
|
||||
postJson(session,newAnnounceJson,[],inboxUrl,headers,"inbox:write")
|
||||
#if not postResult:
|
||||
# if debug:
|
||||
# print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
||||
# return 5
|
||||
|
||||
if debug:
|
||||
print('DEBUG: c2s POST announce success')
|
||||
|
||||
@@ -17,9 +17,7 @@ def hashPassword(password: str) -> str:
|
||||
"""Hash a password for storing
|
||||
"""
|
||||
salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha512', \
|
||||
password.encode('utf-8'), \
|
||||
salt, 100000)
|
||||
pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000)
|
||||
pwdhash = binascii.hexlify(pwdhash)
|
||||
return (salt + pwdhash).decode('ascii')
|
||||
|
||||
@@ -48,9 +46,7 @@ def authorizeBasic(baseDir: str,path: str,authHeader: str,debug: bool) -> bool:
|
||||
if debug:
|
||||
print('DEBUG: Authorixation header does not contain a space character')
|
||||
return False
|
||||
if '/users/' not in path and \
|
||||
'/channel/' not in path and \
|
||||
'/profile/' not in path:
|
||||
if '/users/' not in path and '/profile/' not in path:
|
||||
if debug:
|
||||
print('DEBUG: Path for Authorization does not contain a user')
|
||||
return False
|
||||
|
||||
@@ -7,7 +7,6 @@ __email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import json
|
||||
import time
|
||||
import commentjson
|
||||
import os
|
||||
from webfinger import webfingerHandle
|
||||
@@ -16,8 +15,6 @@ from posts import getPersonBox
|
||||
from session import postJson
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
|
||||
def setAvailability(baseDir: str,nickname: str,domain: str, \
|
||||
status: str) -> bool:
|
||||
@@ -29,10 +26,11 @@ def setAvailability(baseDir: str,nickname: str,domain: str, \
|
||||
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
|
||||
if not os.path.isfile(actorFilename):
|
||||
return False
|
||||
actorJson=loadJson(actorFilename)
|
||||
if actorJson:
|
||||
with open(actorFilename, 'r') as fp:
|
||||
actorJson=commentjson.load(fp)
|
||||
actorJson['availability']=status
|
||||
saveJson(actorJson,actorFilename)
|
||||
with open(actorFilename, 'w') as fp:
|
||||
commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
|
||||
return True
|
||||
|
||||
def getAvailability(baseDir: str,nickname: str,domain: str) -> str:
|
||||
@@ -41,8 +39,8 @@ def getAvailability(baseDir: str,nickname: str,domain: str) -> str:
|
||||
actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
|
||||
if not os.path.isfile(actorFilename):
|
||||
return False
|
||||
actorJson=loadJson(actorFilename)
|
||||
if actorJson:
|
||||
with open(actorFilename, 'r') as fp:
|
||||
actorJson=commentjson.load(fp)
|
||||
if not actorJson.get('availability'):
|
||||
return None
|
||||
return actorJson['availability']
|
||||
@@ -116,7 +114,7 @@ def sendAvailabilityViaServer(baseDir: str,session, \
|
||||
# get the actor inbox for the To handle
|
||||
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
|
||||
getPersonBox(baseDir,session,wfRequest,personCache, \
|
||||
projectVersion,httpPrefix,nickname,domain,postToBox)
|
||||
projectVersion,httpPrefix,domain,postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
@@ -134,6 +132,10 @@ def sendAvailabilityViaServer(baseDir: str,session, \
|
||||
'Authorization': authHeader}
|
||||
postResult = \
|
||||
postJson(session,newAvailabilityJson,[],inboxUrl,headers,"inbox:write")
|
||||
#if not postResult:
|
||||
# if debug:
|
||||
# print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
||||
# return 5
|
||||
|
||||
if debug:
|
||||
print('DEBUG: c2s POST availability success')
|
||||
|
||||
@@ -137,13 +137,11 @@ def isBlocked(baseDir: str,nickname: str,domain: str, \
|
||||
blockHandle=blockNickname+'@'+blockDomain
|
||||
if blockHandle in open(globalBlockingFilename).read():
|
||||
return True
|
||||
allowFilename= \
|
||||
baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
|
||||
allowFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
|
||||
if os.path.isfile(allowFilename):
|
||||
if blockDomain not in open(allowFilename).read():
|
||||
return True
|
||||
blockingFilename= \
|
||||
baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
|
||||
blockingFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
|
||||
if os.path.isfile(blockingFilename):
|
||||
if '*@'+blockDomain in open(blockingFilename).read():
|
||||
return True
|
||||
@@ -170,9 +168,8 @@ def sendBlockViaServer(baseDir: str,session, \
|
||||
if ':' not in fromDomain:
|
||||
fromDomainFull=fromDomain+':'+str(fromPort)
|
||||
|
||||
toUrl= 'https://www.w3.org/ns/activitystreams#Public'
|
||||
ccUrl= \
|
||||
httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
|
||||
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
|
||||
ccUrl = httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
|
||||
|
||||
blockActor=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname
|
||||
newBlockJson = {
|
||||
@@ -199,8 +196,7 @@ def sendBlockViaServer(baseDir: str,session, \
|
||||
# get the actor inbox for the To handle
|
||||
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
|
||||
getPersonBox(baseDir,session,wfRequest,personCache, \
|
||||
projectVersion,httpPrefix,fromNickname, \
|
||||
fromDomain,postToBox)
|
||||
projectVersion,httpPrefix,fromDomain,postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
@@ -218,6 +214,10 @@ def sendBlockViaServer(baseDir: str,session, \
|
||||
'Authorization': authHeader}
|
||||
postResult = \
|
||||
postJson(session,newBlockJson,[],inboxUrl,headers,"inbox:write")
|
||||
#if not postResult:
|
||||
# if debug:
|
||||
# print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
||||
# return 5
|
||||
|
||||
if debug:
|
||||
print('DEBUG: c2s POST block success')
|
||||
@@ -242,9 +242,8 @@ def sendUndoBlockViaServer(baseDir: str,session, \
|
||||
if ':' not in fromDomain:
|
||||
fromDomainFull=fromDomain+':'+str(fromPort)
|
||||
|
||||
toUrl= 'https://www.w3.org/ns/activitystreams#Public'
|
||||
ccUrl= \
|
||||
httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
|
||||
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
|
||||
ccUrl = httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
|
||||
|
||||
blockActor=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname
|
||||
newBlockJson = {
|
||||
@@ -263,9 +262,8 @@ def sendUndoBlockViaServer(baseDir: str,session, \
|
||||
handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
|
||||
|
||||
# lookup the inbox for the To handle
|
||||
wfRequest= \
|
||||
webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
|
||||
fromDomain,projectVersion)
|
||||
wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
|
||||
fromDomain,projectVersion)
|
||||
if not wfRequest:
|
||||
if debug:
|
||||
print('DEBUG: announce webfinger failed for '+handle)
|
||||
@@ -276,8 +274,7 @@ def sendUndoBlockViaServer(baseDir: str,session, \
|
||||
# get the actor inbox for the To handle
|
||||
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
|
||||
getPersonBox(baseDir,session,wfRequest,personCache, \
|
||||
projectVersion,httpPrefix,fromNickname, \
|
||||
fromDomain,postToBox)
|
||||
projectVersion,httpPrefix,fromDomain,postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
@@ -295,6 +292,10 @@ def sendUndoBlockViaServer(baseDir: str,session, \
|
||||
'Authorization': authHeader}
|
||||
postResult = \
|
||||
postJson(session,newBlockJson,[],inboxUrl,headers,"inbox:write")
|
||||
#if not postResult:
|
||||
# if debug:
|
||||
# print('DEBUG: POST announce failed for c2s to '+inboxUrl)
|
||||
# return 5
|
||||
|
||||
if debug:
|
||||
print('DEBUG: c2s POST block success')
|
||||
@@ -330,9 +331,7 @@ def outboxBlock(baseDir: str,httpPrefix: str, \
|
||||
if debug:
|
||||
print('DEBUG: c2s block object is not a status')
|
||||
return
|
||||
if '/users/' not in messageId and \
|
||||
'/channel/' not in messageId and \
|
||||
'/profile/' not in messageId:
|
||||
if '/users/' not in messageId and '/profile/' not in messageId:
|
||||
if debug:
|
||||
print('DEBUG: c2s block object has no nickname')
|
||||
return
|
||||
@@ -407,9 +406,7 @@ def outboxUndoBlock(baseDir: str,httpPrefix: str, \
|
||||
if debug:
|
||||
print('DEBUG: c2s undo block object is not a status')
|
||||
return
|
||||
if '/users/' not in messageId and \
|
||||
'/channel/' not in messageId and \
|
||||
'/profile/' not in messageId:
|
||||
if '/users/' not in messageId and '/profile/' not in messageId:
|
||||
if debug:
|
||||
print('DEBUG: c2s undo block object has no nickname')
|
||||
return
|
||||
|
||||
@@ -10,10 +10,9 @@ import os
|
||||
import time
|
||||
import datetime
|
||||
import commentjson
|
||||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
|
||||
def storePersonInCache(baseDir: str,personUrl: str,personJson: {},personCache: {}) -> None:
|
||||
|
||||
def storePersonInCache(baseDir: str,personUrl: str, \
|
||||
personJson: {},personCache: {}) -> None:
|
||||
"""Store an actor in the cache
|
||||
"""
|
||||
currTime=datetime.datetime.utcnow()
|
||||
@@ -28,7 +27,8 @@ def storePersonInCache(baseDir: str,personUrl: str,personJson: {},personCache: {
|
||||
if os.path.isdir(baseDir+'/cache/actors'):
|
||||
cacheFilename=baseDir+'/cache/actors/'+personUrl.replace('/','#')+'.json'
|
||||
if not os.path.isfile(cacheFilename):
|
||||
saveJson(personJson,cacheFilename)
|
||||
with open(cacheFilename, 'w') as fp:
|
||||
commentjson.dump(personJson, fp, indent=4, sort_keys=False)
|
||||
|
||||
def getPersonFromCache(baseDir: str,personUrl: str,personCache: {}) -> {}:
|
||||
"""Get an actor from the cache
|
||||
@@ -36,9 +36,16 @@ def getPersonFromCache(baseDir: str,personUrl: str,personCache: {}) -> {}:
|
||||
# if the actor is not in memory then try to load it from file
|
||||
loadedFromFile=False
|
||||
if not personCache.get(personUrl):
|
||||
cacheFilename=baseDir+'/cache/actors/'+personUrl.replace('/','#')+'.json'
|
||||
cacheFilename= \
|
||||
baseDir+'/cache/actors/'+personUrl.replace('/','#')+'.json'
|
||||
if os.path.isfile(cacheFilename):
|
||||
personJson=loadJson(cacheFilename)
|
||||
personJson=None
|
||||
try:
|
||||
with open(cacheFilename, 'r') as fp:
|
||||
personJson=commentjson.load(fp)
|
||||
except Exception as e:
|
||||
print('ERROR: unable to load actor from cache '+cacheFilename)
|
||||
print(e)
|
||||
if personJson:
|
||||
storePersonInCache(baseDir,personUrl,personJson,personCache)
|
||||
loadedFromFile=True
|
||||
@@ -47,11 +54,12 @@ def getPersonFromCache(baseDir: str,personUrl: str,personCache: {}) -> {}:
|
||||
if not loadedFromFile:
|
||||
# update the timestamp for the last time the actor was retrieved
|
||||
currTime=datetime.datetime.utcnow()
|
||||
personCache[personUrl]['timestamp']=currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
personCache[personUrl]['timestamp']= \
|
||||
currTime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
return personCache[personUrl]['actor']
|
||||
return None
|
||||
|
||||
def expirePersonCache(personCache: {}):
|
||||
def expirePersonCache(personCache: {}) -> None:
|
||||
"""Expires old entries from the cache in memory
|
||||
"""
|
||||
currTime=datetime.datetime.utcnow()
|
||||
|
||||
@@ -14,10 +14,9 @@ import commentjson
|
||||
from auth import createPassword
|
||||
from utils import getNicknameFromActor
|
||||
from utils import getDomainFromActor
|
||||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
|
||||
def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: str) -> str:
|
||||
def getOcapFilename(baseDir :str,nickname: str,domain: str, \
|
||||
actor :str,subdir: str) -> str:
|
||||
"""Returns the filename for a particular capability accepted or granted
|
||||
Also creates directories as needed
|
||||
"""
|
||||
@@ -42,7 +41,8 @@ def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: st
|
||||
if not os.path.isdir(ocDir):
|
||||
os.mkdir(ocDir)
|
||||
|
||||
return baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+subdir+'/'+actor.replace('/','#')+'.json'
|
||||
return baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+ \
|
||||
subdir+'/'+actor.replace('/','#')+'.json'
|
||||
|
||||
def CapablePost(postJson: {}, capabilityList: [], debug :bool) -> bool:
|
||||
"""Determines whether a post arriving in the inbox
|
||||
@@ -122,14 +122,16 @@ def capabilitiesAccept(baseDir: str,httpPrefix: str, \
|
||||
fullDomain=domain+':'+str(port)
|
||||
|
||||
# make directories to store capabilities
|
||||
ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,acceptedActor,'accept')
|
||||
ocapFilename= \
|
||||
getOcapFilename(baseDir,nickname,fullDomain,acceptedActor,'accept')
|
||||
if not ocapFilename:
|
||||
return None
|
||||
ocapAccept=None
|
||||
|
||||
# if the capability already exists then load it from file
|
||||
if os.path.isfile(ocapFilename):
|
||||
ocapAccept=loadJson(ocapFilename)
|
||||
with open(ocapFilename, 'r') as fp:
|
||||
ocapAccept=commentjson.load(fp)
|
||||
# otherwise create a new capability
|
||||
if not ocapAccept:
|
||||
acceptedActorNickname=getNicknameFromActor(acceptedActor)
|
||||
@@ -138,9 +140,11 @@ def capabilitiesAccept(baseDir: str,httpPrefix: str, \
|
||||
return None
|
||||
acceptedActorDomain,acceptedActorPort=getDomainFromActor(acceptedActor)
|
||||
if acceptedActorPort:
|
||||
ocapId=acceptedActorNickname+'@'+acceptedActorDomain+':'+str(acceptedActorPort)+'#'+createPassword(32)
|
||||
ocapId=acceptedActorNickname+'@'+acceptedActorDomain+':'+ \
|
||||
str(acceptedActorPort)+'#'+createPassword(32)
|
||||
else:
|
||||
ocapId=acceptedActorNickname+'@'+acceptedActorDomain+'#'+createPassword(32)
|
||||
ocapId=acceptedActorNickname+'@'+acceptedActorDomain+'#'+ \
|
||||
createPassword(32)
|
||||
ocapAccept = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": httpPrefix+"://"+fullDomain+"/caps/"+ocapId,
|
||||
@@ -153,19 +157,23 @@ def capabilitiesAccept(baseDir: str,httpPrefix: str, \
|
||||
ocapAccept['actor']=httpPrefix+"://"+fullDomain+'/users/'+nickname
|
||||
|
||||
if saveToFile:
|
||||
saveJson(ocapAccept,ocapFilename)
|
||||
with open(ocapFilename, 'w') as fp:
|
||||
commentjson.dump(ocapAccept, fp, indent=4, sort_keys=False)
|
||||
return ocapAccept
|
||||
|
||||
def capabilitiesGrantedSave(baseDir :str,nickname :str,domain :str,ocap: {}) -> bool:
|
||||
def capabilitiesGrantedSave(baseDir :str, \
|
||||
nickname :str,domain :str,ocap: {}) -> bool:
|
||||
"""A capabilities accept is received, so stor it for
|
||||
reference when sending to the actor
|
||||
"""
|
||||
if not ocap.get('actor'):
|
||||
return False
|
||||
ocapFilename=getOcapFilename(baseDir,nickname,domain,ocap['actor'],'granted')
|
||||
ocapFilename= \
|
||||
getOcapFilename(baseDir,nickname,domain,ocap['actor'],'granted')
|
||||
if not ocapFilename:
|
||||
return False
|
||||
saveJson(ocap,ocapFilename)
|
||||
with open(ocapFilename, 'w') as fp:
|
||||
commentjson.dump(ocap, fp, indent=4, sort_keys=False)
|
||||
return True
|
||||
|
||||
def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
|
||||
@@ -188,7 +196,8 @@ def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
|
||||
fullDomain=domain+':'+str(port)
|
||||
|
||||
# Get the filename of the capability
|
||||
ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept')
|
||||
ocapFilename= \
|
||||
getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept')
|
||||
if not ocapFilename:
|
||||
return None
|
||||
|
||||
@@ -207,7 +216,8 @@ def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
|
||||
}
|
||||
|
||||
# read the existing capability
|
||||
ocapJson=loadJson(ocapFilename)
|
||||
with open(ocapFilename, 'r') as fp:
|
||||
ocapJson=commentjson.load(fp)
|
||||
|
||||
# set the new capabilities list. eg. ["inbox:write","objects:read"]
|
||||
ocapJson['capability']=updateCaps
|
||||
@@ -219,15 +229,17 @@ def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
|
||||
return None
|
||||
updateActorDomain,updateActorPort=getDomainFromActor(updateActor)
|
||||
if updateActorPort:
|
||||
ocapId=updateActorNickname+'@'+updateActorDomain+':'+str(updateActorPort)+'#'+createPassword(32)
|
||||
ocapId=updateActorNickname+'@'+updateActorDomain+':'+ \
|
||||
str(updateActorPort)+'#'+createPassword(32)
|
||||
else:
|
||||
ocapId=updateActorNickname+'@'+updateActorDomain+'#'+createPassword(32)
|
||||
ocapJson['id']=httpPrefix+"://"+fullDomain+"/caps/"+ocapId
|
||||
ocapUpdate['object']=ocapJson
|
||||
|
||||
# save it again
|
||||
saveJson(ocapJson,ocapFilename)
|
||||
|
||||
with open(ocapFilename, 'w') as fp:
|
||||
commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
|
||||
|
||||
return ocapUpdate
|
||||
|
||||
def capabilitiesReceiveUpdate(baseDir :str, \
|
||||
@@ -248,11 +260,12 @@ def capabilitiesReceiveUpdate(baseDir :str, \
|
||||
print(ocapFilename)
|
||||
return False
|
||||
|
||||
ocapJson=loadJson(ocapFilename)
|
||||
|
||||
if ocapJson:
|
||||
with open(ocapFilename, 'r') as fp:
|
||||
ocapJson=commentjson.load(fp)
|
||||
ocapJson['id']=newCapabilitiesId
|
||||
ocapJson['capability']=capabilityList
|
||||
|
||||
return saveJson(ocapJson,ocapFilename)
|
||||
|
||||
with open(ocapFilename, 'w') as fp:
|
||||
commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
|
||||
return True
|
||||
return False
|
||||
@@ -38,7 +38,7 @@ In cases where criticism of ideology or culture remains on-topic, respectfully d
|
||||
|
||||
## No proprietary software
|
||||
|
||||
This is a Free Software project and there is no intention to support or endorse any software which is proprietary, including that which may be running on client devices or corporate silo systems. For example, supporting iThings or Windows users is out of scope of the project. So too would be integration with Twitter or Google's sevices.
|
||||
This is a Free Software project and there is no intention to support or endorse any software which is proprietary, including that which may be running on client devices. For example, supporting iThings or Windows users is out of scope of the project. It's possible that Epicyon may work with those systems, but if there are problems it's not within the scope of this project to resolve compatibility issues with proprietary browsers or platforms.
|
||||
|
||||
## Raising concerns
|
||||
|
||||
@@ -46,7 +46,9 @@ If you're raising concerns about something or someone, there must be demonstrabl
|
||||
|
||||
This is not a big project and so there is no division of labor or special enforcement committee or bureaucratic process.
|
||||
|
||||
Complaints should be either reported in the Matrix chat room **#epicyon:matrix.freedombone.net** or sent to bob@freedombone.net, preferably via XMPP/Conversations with OMEMO enabled but you can also use the same address for email correspondence.
|
||||
If your concern is something which is suitable for public discussion then raise it in one of the chat rooms mentioned above. Be mindful to do so in a polite and considerate manner. Hostile communication towards anyone raising concerns publicly will be considered a code of conduct violation.
|
||||
|
||||
Private complaints should be sent to bob@freedombone.net, preferably via XMPP/Conversations with OMEMO enabled but you can also use the same address for email correspondence.
|
||||
|
||||
## In case of violations
|
||||
|
||||
|
||||
@@ -7,11 +7,8 @@ __email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import commentjson
|
||||
from utils import loadJson
|
||||
from utils import saveJson
|
||||
|
||||
def createConfig(baseDir: str) -> None:
|
||||
"""Creates a configuration file
|
||||
@@ -21,26 +18,27 @@ def createConfig(baseDir: str) -> None:
|
||||
return
|
||||
configJson = {
|
||||
}
|
||||
saveJson(configJson,configFilename)
|
||||
with open(configFilename, 'w') as fp:
|
||||
commentjson.dump(configJson, fp, indent=4, sort_keys=True)
|
||||
|
||||
def setConfigParam(baseDir: str, variableName: str, variableValue) -> None:
|
||||
"""Sets a configuration value
|
||||
"""
|
||||
createConfig(baseDir)
|
||||
configFilename=baseDir+'/config.json'
|
||||
configJson={}
|
||||
if os.path.isfile(configFilename):
|
||||
configJson=loadJson(configFilename)
|
||||
with open(configFilename, 'r') as fp:
|
||||
configJson=commentjson.load(fp)
|
||||
configJson[variableName]=variableValue
|
||||
saveJson(configJson,configFilename)
|
||||
with open(configFilename, 'w') as fp:
|
||||
commentjson.dump(configJson, fp, indent=4, sort_keys=True)
|
||||
|
||||
def getConfigParam(baseDir: str, variableName: str):
|
||||
def getConfigParam(baseDir: str, variableName: str) -> None:
|
||||
"""Gets a configuration value
|
||||
"""
|
||||
createConfig(baseDir)
|
||||
configFilename=baseDir+'/config.json'
|
||||
configJson=loadJson(configFilename)
|
||||
if configJson:
|
||||
if configJson.get(variableName):
|
||||
return configJson[variableName]
|
||||
with open(configFilename, 'r') as fp:
|
||||
configJson=commentjson.load(fp)
|
||||
if configJson.get(variableName):
|
||||
return configJson[variableName]
|
||||
return None
|
||||
@@ -7,35 +7,9 @@ __email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import os
|
||||
import time
|
||||
import commentjson
|
||||
from shutil import copyfile
|
||||
|
||||
def replaceEmojiFromTags(content: str,tag: [],messageType: str) -> str:
|
||||
"""Uses the tags to replace :emoji: with html image markup
|
||||
"""
|
||||
for tagItem in tag:
|
||||
if not tagItem.get('type'):
|
||||
continue
|
||||
if tagItem['type']!='Emoji':
|
||||
continue
|
||||
if not tagItem.get('name'):
|
||||
continue
|
||||
if not tagItem.get('icon'):
|
||||
continue
|
||||
if not tagItem['icon'].get('url'):
|
||||
continue
|
||||
if tagItem['name'] not in content:
|
||||
continue
|
||||
htmlClass='emoji'
|
||||
if messageType=='post header':
|
||||
htmlClass='emojiheader'
|
||||
if messageType=='profile':
|
||||
htmlClass='emojiprofile'
|
||||
emojiHtml="<img src=\""+tagItem['icon']['url']+"\" alt=\""+tagItem['name'].replace(':','')+"\" align=\"middle\" class=\""+htmlClass+"\"/>"
|
||||
content=content.replace(tagItem['name'],emojiHtml)
|
||||
return content
|
||||
|
||||
def addMusicTag(content: str,tag: str) -> str:
|
||||
"""If a music link is found then ensure that the post is tagged appropriately
|
||||
"""
|
||||
@@ -51,16 +25,17 @@ def addMusicTag(content: str,tag: str) -> str:
|
||||
break
|
||||
if not musicSiteFound:
|
||||
return content
|
||||
return ':music: '+content+' '+tag+' '
|
||||
return content+' '+tag+' '
|
||||
|
||||
def addWebLinks(content: str) -> str:
|
||||
"""Adds markup for web links
|
||||
"""
|
||||
if not ('https://' in content or 'http://' in content):
|
||||
if not ('https://' in content or \
|
||||
'http://' in content or \
|
||||
'dat://' in content):
|
||||
return content
|
||||
|
||||
maxLinkLength=40
|
||||
words=content.replace('\n',' --linebreak-- ').split(' ')
|
||||
words=content.replace('\n',' --linebreak--').split(' ')
|
||||
replaceDict={}
|
||||
for w in words:
|
||||
if w.startswith('https://') or \
|
||||
@@ -75,31 +50,34 @@ def addWebLinks(content: str) -> str:
|
||||
markup+='<span class="invisible">http://</span>'
|
||||
elif w.startswith('dat://'):
|
||||
markup+='<span class="invisible">dat://</span>'
|
||||
linkText=w.replace('https://','').replace('http://','').replace('dat://','')
|
||||
# prevent links from becoming too long
|
||||
if len(linkText)>maxLinkLength:
|
||||
markup+='<span class="ellipsis">'+linkText[:maxLinkLength]+'</span>'
|
||||
markup+='<span class="invisible">'+linkText[maxLinkLength:]+'</span></a>'
|
||||
else:
|
||||
markup+='<span class="ellipsis">'+linkText+'</span></a>'
|
||||
markup+='<span class="ellipsis">'+ \
|
||||
w.replace('https://','').replace('http://','').replace('dat://','')+ \
|
||||
'</span></a>'
|
||||
replaceDict[w]=markup
|
||||
for url,markup in replaceDict.items():
|
||||
content=content.replace(url,markup)
|
||||
content=content.replace(' --linebreak-- ','<br>')
|
||||
content=content.replace(' --linebreak--','<br>')
|
||||
return content
|
||||
|
||||
def validHashTag(hashtag: str) -> bool:
|
||||
"""Returns true if the give hashtag contains valid characters
|
||||
"""
|
||||
validChars = set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
validChars = \
|
||||
set('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
if set(hashtag).issubset(validChars):
|
||||
return True
|
||||
return False
|
||||
|
||||
def addHashTags(wordStr: str,httpPrefix: str,domain: str,replaceHashTags: {},postHashtags: {}) -> bool:
|
||||
def addHashTags(wordStr: str, \
|
||||
httpPrefix: str,domain: str, \
|
||||
replaceHashTags: {},postHashtags: {}) -> bool:
|
||||
"""Detects hashtags and adds them to the replacements dict
|
||||
Also updates the hashtags list to be added to the post
|
||||
"""
|
||||
if not wordStr.startswith('#'):
|
||||
return False
|
||||
if len(wordStr)<2:
|
||||
return False
|
||||
if replaceHashTags.get(wordStr):
|
||||
return True
|
||||
hashtag=wordStr[1:]
|
||||
@@ -112,7 +90,9 @@ def addHashTags(wordStr: str,httpPrefix: str,domain: str,replaceHashTags: {},pos
|
||||
'type': 'Hashtag'
|
||||
}
|
||||
replaceHashTags[wordStr]= \
|
||||
"<a href=\""+hashtagUrl+"\" class=\"mention hashtag\" rel=\"tag\">#<span>"+hashtag+"</span></a>"
|
||||
"<a href=\""+hashtagUrl+ \
|
||||
"\" class=\"mention hashtag\" rel=\"tag\">#<span>"+ \
|
||||
hashtag+"</span></a>"
|
||||
return True
|
||||
|
||||
def loadEmojiDict(emojiDataFilename: str,emojiDict: {}) -> None:
|
||||
@@ -135,12 +115,15 @@ def loadEmojiDict(emojiDataFilename: str,emojiDict: {}) -> None:
|
||||
continue
|
||||
if '..' in emojiUnicode:
|
||||
emojiUnicode=emojiUnicode.split('..')[0]
|
||||
emojiName=line.split(')',1)[1].strip().replace('\n','').replace(' ','').replace('-','')
|
||||
emojiName= \
|
||||
line.split(')',1)[1].strip().replace('\n','').replace(' ','').replace('-','')
|
||||
if '..' in emojiName:
|
||||
emojiName=emojiName.split('..')[0]
|
||||
emojiDict[emojiName.lower()]=emojiUnicode
|
||||
|
||||
def addEmoji(baseDir: str,wordStr: str,httpPrefix: str,domain: str,replaceEmoji: {},postTags: {},emojiDict: {}) -> bool:
|
||||
def addEmoji(baseDir: str,wordStr: str, \
|
||||
httpPrefix: str,domain: str, \
|
||||
replaceEmoji: {},postTags: {},emojiDict: {}) -> bool:
|
||||
"""Detects Emoji and adds them to the replacements dict
|
||||
Also updates the tags list to be added to the post
|
||||
"""
|
||||
@@ -152,10 +135,8 @@ def addEmoji(baseDir: str,wordStr: str,httpPrefix: str,domain: str,replaceEmoji:
|
||||
return False
|
||||
if replaceEmoji.get(wordStr):
|
||||
return True
|
||||
# remove leading and trailing : characters
|
||||
emoji=wordStr[1:]
|
||||
emoji=emoji[:-1]
|
||||
# is the text of the emoji valid?
|
||||
if not validHashTag(emoji):
|
||||
return False
|
||||
if not emojiDict.get(emoji):
|
||||
@@ -175,9 +156,15 @@ def addEmoji(baseDir: str,wordStr: str,httpPrefix: str,domain: str,replaceEmoji:
|
||||
}
|
||||
return True
|
||||
|
||||
def addMention(wordStr: str,httpPrefix: str,following: str,replaceMentions: {},recipients: [],tags: {}) -> bool:
|
||||
def addMention(wordStr: str, \
|
||||
httpPrefix: str,following: str, \
|
||||
replaceMentions: {},recipients: [],tags: {}) -> bool:
|
||||
"""Detects mentions and adds them to the replacements dict and recipients list
|
||||
"""
|
||||
if not wordStr.startswith('@'):
|
||||
return False
|
||||
if len(wordStr)<2:
|
||||
return False
|
||||
possibleHandle=wordStr[1:]
|
||||
# @nick
|
||||
if following and '@' not in possibleHandle:
|
||||
@@ -187,7 +174,8 @@ def addMention(wordStr: str,httpPrefix: str,following: str,replaceMentions: {},r
|
||||
for follow in following:
|
||||
if follow.startswith(possibleNickname+'@'):
|
||||
replaceDomain=follow.replace('\n','').split('@')[1]
|
||||
recipientActor=httpPrefix+"://"+replaceDomain+"/users/"+possibleNickname
|
||||
recipientActor= \
|
||||
httpPrefix+"://"+replaceDomain+"/users/"+possibleNickname
|
||||
if recipientActor not in recipients:
|
||||
recipients.append(recipientActor)
|
||||
tags[wordStr]={
|
||||
@@ -195,24 +183,21 @@ def addMention(wordStr: str,httpPrefix: str,following: str,replaceMentions: {},r
|
||||
'name': wordStr,
|
||||
'type': 'Mention'
|
||||
}
|
||||
replaceMentions[wordStr]="<span class=\"h-card\"><a href=\""+httpPrefix+"://"+replaceDomain+"/@"+possibleNickname+"\" class=\"u-url mention\">@<span>"+possibleNickname+"</span></a></span>"
|
||||
replaceMentions[wordStr]= \
|
||||
"<span class=\"h-card\"><a href=\""+httpPrefix+ \
|
||||
"://"+replaceDomain+"/@"+possibleNickname+ \
|
||||
"\" class=\"u-url mention\">@<span>"+ \
|
||||
possibleNickname+"</span></a></span>"
|
||||
return True
|
||||
return False
|
||||
possibleNickname=None
|
||||
possibleDomain=None
|
||||
if '@' not in possibleHandle:
|
||||
return False
|
||||
possibleNickname=possibleHandle.split('@')[0]
|
||||
if not possibleNickname:
|
||||
return False
|
||||
possibleDomain=possibleHandle.split('@')[1].strip('\n')
|
||||
if not possibleDomain:
|
||||
return False
|
||||
if following:
|
||||
for follow in following:
|
||||
if follow.replace('\n','')!=possibleHandle:
|
||||
continue
|
||||
recipientActor=httpPrefix+"://"+possibleDomain+"/users/"+possibleNickname
|
||||
recipientActor= \
|
||||
httpPrefix+"://"+possibleDomain+"/users/"+possibleNickname
|
||||
if recipientActor not in recipients:
|
||||
recipients.append(recipientActor)
|
||||
tags[wordStr]={
|
||||
@@ -220,70 +205,41 @@ def addMention(wordStr: str,httpPrefix: str,following: str,replaceMentions: {},r
|
||||
'name': wordStr,
|
||||
'type': 'Mention'
|
||||
}
|
||||
replaceMentions[wordStr]="<span class=\"h-card\"><a href=\""+httpPrefix+"://"+possibleDomain+"/@"+possibleNickname+"\" class=\"u-url mention\">@<span>"+possibleNickname+"</span></a></span>"
|
||||
replaceMentions[wordStr]= \
|
||||
"<span class=\"h-card\"><a href=\""+httpPrefix+ \
|
||||
"://"+possibleDomain+"/@"+possibleNickname+ \
|
||||
"\" class=\"u-url mention\">@<span>"+possibleNickname+ \
|
||||
"</span></a></span>"
|
||||
return True
|
||||
# @nick@domain
|
||||
if not (possibleDomain=='localhost' or '.' in possibleDomain):
|
||||
return False
|
||||
recipientActor=httpPrefix+"://"+possibleDomain+"/users/"+possibleNickname
|
||||
if recipientActor not in recipients:
|
||||
recipients.append(recipientActor)
|
||||
tags[wordStr]={
|
||||
'href': recipientActor,
|
||||
'name': wordStr,
|
||||
'type': 'Mention'
|
||||
}
|
||||
replaceMentions[wordStr]="<span class=\"h-card\"><a href=\""+httpPrefix+"://"+possibleDomain+"/@"+possibleNickname+"\" class=\"u-url mention\">@<span>"+possibleNickname+"</span></a></span>"
|
||||
return True
|
||||
|
||||
def removeLongWords(content: str,maxWordLength: int,longWordsList: []) -> str:
|
||||
"""Breaks up long words so that on mobile screens this doesn't disrupt the layout
|
||||
"""
|
||||
words=content.split(' ')
|
||||
if not longWordsList:
|
||||
longWordsList=[]
|
||||
for wordStr in words:
|
||||
if len(wordStr)>maxWordLength:
|
||||
if wordStr not in longWordsList:
|
||||
longWordsList.append(wordStr)
|
||||
for wordStr in longWordsList:
|
||||
if wordStr.startswith('<'):
|
||||
continue
|
||||
if '=\"' in wordStr:
|
||||
continue
|
||||
if '@' in wordStr:
|
||||
if '@@' not in wordStr:
|
||||
continue
|
||||
if 'https:' in wordStr:
|
||||
continue
|
||||
elif 'http:' in wordStr:
|
||||
continue
|
||||
elif 'dat:' in wordStr:
|
||||
continue
|
||||
if '<' in wordStr:
|
||||
wordStr=wordStr.split('<',1)[0]
|
||||
if '/' in wordStr:
|
||||
continue
|
||||
if len(wordStr[maxWordLength:])<maxWordLength:
|
||||
content= \
|
||||
content.replace(wordStr, \
|
||||
wordStr[:maxWordLength]+'\n'+ \
|
||||
wordStr[maxWordLength:])
|
||||
else:
|
||||
content= \
|
||||
content.replace(wordStr, \
|
||||
wordStr[:maxWordLength])
|
||||
return content
|
||||
if '@' in possibleHandle:
|
||||
if not (possibleDomain=='localhost' or '.' in possibleDomain):
|
||||
return False
|
||||
recipientActor= \
|
||||
httpPrefix+"://"+possibleDomain+"/users/"+possibleNickname
|
||||
if recipientActor not in recipients:
|
||||
recipients.append(recipientActor)
|
||||
tags[wordStr]={
|
||||
'href': recipientActor,
|
||||
'name': wordStr,
|
||||
'type': 'Mention'
|
||||
}
|
||||
replaceMentions[wordStr]= \
|
||||
"<span class=\"h-card\"><a href=\""+httpPrefix+ \
|
||||
"://"+possibleDomain+"/@"+possibleNickname+ \
|
||||
"\" class=\"u-url mention\">@<span>"+possibleNickname+ \
|
||||
"</span></a></span>"
|
||||
return True
|
||||
return False
|
||||
|
||||
def addHtmlTags(baseDir: str,httpPrefix: str, \
|
||||
nickname: str,domain: str,content: str, \
|
||||
recipients: [],hashtags: {},isJsonContent=False) -> str:
|
||||
recipients: [],hashtags: {}) -> str:
|
||||
""" Replaces plaintext mentions such as @nick@domain into html
|
||||
by matching against known following accounts
|
||||
"""
|
||||
if content.startswith('<p>'):
|
||||
return content
|
||||
maxWordLength=40
|
||||
content=content.replace('\n',' --linebreak-- ')
|
||||
content=addMusicTag(content,'nowplaying')
|
||||
words=content.replace(',',' ').replace(';',' ').split(' ')
|
||||
@@ -313,68 +269,48 @@ def addHtmlTags(baseDir: str,httpPrefix: str, \
|
||||
# read the following list so that we can detect just @nick
|
||||
# in addition to @nick@domain
|
||||
following=None
|
||||
if '@' in words:
|
||||
if os.path.isfile(followingFilename):
|
||||
with open(followingFilename, "r") as f:
|
||||
following = f.readlines()
|
||||
if os.path.isfile(followingFilename):
|
||||
with open(followingFilename, "r") as f:
|
||||
following = f.readlines()
|
||||
|
||||
# extract mentions and tags from words
|
||||
longWordsList=[]
|
||||
for wordStr in words:
|
||||
wordLen=len(wordStr)
|
||||
if wordLen>2:
|
||||
if wordLen>maxWordLength:
|
||||
longWordsList.append(wordStr)
|
||||
firstChar=wordStr[0]
|
||||
if firstChar=='@':
|
||||
if addMention(wordStr,httpPrefix,following,replaceMentions,recipients,hashtags):
|
||||
continue
|
||||
elif firstChar=='#':
|
||||
if addHashTags(wordStr,httpPrefix,originalDomain,replaceHashTags,hashtags):
|
||||
continue
|
||||
elif ':' in wordStr:
|
||||
#print('TAG: emoji located - '+wordStr)
|
||||
wordStr2=wordStr.split(':')[1]
|
||||
if not emojiDict:
|
||||
# emoji.json is generated so that it can be customized and the changes
|
||||
# will be retained even if default_emoji.json is subsequently updated
|
||||
if not os.path.isfile(baseDir+'/emoji/emoji.json'):
|
||||
copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
|
||||
emojiDictCtr=0
|
||||
while not emojiDict and emojiDictCtr<5:
|
||||
if emojiDictCtr>0:
|
||||
print('Retry emoji load '+baseDir+'/emoji/emoji.json')
|
||||
try:
|
||||
with open(baseDir+'/emoji/emoji.json', 'r') as fp:
|
||||
emojiDict=commentjson.load(fp)
|
||||
if emojiDictCtr>0:
|
||||
print('emojiDict loaded on try '+str(emojiDictCtr))
|
||||
break
|
||||
except:
|
||||
print('WARN: commentjson exception addHtmlTags')
|
||||
print('Failed to load emoji (try '+str(emojiDictCtr)+'): '+baseDir+'/emoji/emoji.json')
|
||||
time.sleep(1)
|
||||
emojiDictCtr+=1
|
||||
if addMention(wordStr,httpPrefix,following, \
|
||||
replaceMentions,recipients,hashtags):
|
||||
continue
|
||||
if addHashTags(wordStr,httpPrefix,originalDomain, \
|
||||
replaceHashTags,hashtags):
|
||||
continue
|
||||
if len(wordStr)>2 and wordStr.startswith(':') and \
|
||||
wordStr.endswith(':') and not emojiDict:
|
||||
print('Loading emoji lookup')
|
||||
|
||||
#print('TAG: looking up emoji for :'+wordStr2+':')
|
||||
addEmoji(baseDir,':'+wordStr2+':',httpPrefix,originalDomain,replaceEmoji,hashtags,emojiDict)
|
||||
# emoji.json is generated so that it can be customized and the changes
|
||||
# will be retained even if default_emoji.json is subsequently updated
|
||||
if not os.path.isfile(baseDir+'/emoji/emoji.json'):
|
||||
copyfile(baseDir+'/emoji/default_emoji.json', \
|
||||
baseDir+'/emoji/emoji.json')
|
||||
|
||||
with open(baseDir+'/emoji/emoji.json', 'r') as fp:
|
||||
emojiDict=commentjson.load(fp)
|
||||
|
||||
addEmoji(baseDir,wordStr,httpPrefix,originalDomain, \
|
||||
replaceEmoji,hashtags,emojiDict)
|
||||
|
||||
# replace words with their html versions
|
||||
for wordStr,replaceStr in replaceMentions.items():
|
||||
content=content.replace(wordStr,replaceStr)
|
||||
for wordStr,replaceStr in replaceHashTags.items():
|
||||
content=content.replace(wordStr,replaceStr)
|
||||
if not isJsonContent:
|
||||
for wordStr,replaceStr in replaceEmoji.items():
|
||||
content=content.replace(wordStr,replaceStr)
|
||||
|
||||
for wordStr,replaceStr in replaceEmoji.items():
|
||||
content=content.replace(wordStr,replaceStr)
|
||||
|
||||
content=addWebLinks(content)
|
||||
if longWordsList:
|
||||
content=removeLongWords(content,maxWordLength,longWordsList)
|
||||
content=content.replace(' --linebreak-- ','</p><p>')
|
||||
return '<p>'+content+'</p>'
|
||||
|
||||
def getMentionsFromHtml(htmlText: str,matchStr="<span class=\"h-card\"><a href=\"") -> []:
|
||||
def getMentionsFromHtml(htmlText: str, \
|
||||
matchStr="<span class=\"h-card\"><a href=\"") -> []:
|
||||
"""Extracts mentioned actors from the given html content string
|
||||
"""
|
||||
mentions=[]
|
||||
@@ -387,6 +323,5 @@ def getMentionsFromHtml(htmlText: str,matchStr="<span class=\"h-card\"><a href=\
|
||||
actorStr=mentionStr.split('"')[0]
|
||||
if actorStr.startswith('http') or \
|
||||
actorStr.startswith('dat:'):
|
||||
if actorStr not in mentions:
|
||||
mentions.append(actorStr)
|
||||
mentions.append(actorStr)
|
||||
return mentions
|
||||
@@ -20,22 +20,20 @@
|
||||
|
||||
<p>Content found to be non-compliant with this policy will be removed and any accounts on this instance producing, repeating or linking to such content will be deleted typically without prior notification.</p>
|
||||
|
||||
<h3>Federation Policy</h3>
|
||||
|
||||
<p>In a proactive effort to avoid the classic fate of <i>"embrace, extend, extinguish"</i> this system will block any instance launched, acquired or funded by Alphabet, Facebook, Twitter, Microsoft, Apple, Amazon, Elsevier or other monopolistic Silicon Valley companies.</p>
|
||||
|
||||
<p>This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible.</p>
|
||||
|
||||
<h3>Commercial Use</h3>
|
||||
|
||||
<p>Commercial use of original content on this instance is strictly forbidden without the prior written permission of individual account holders. The instance administrator does not hold copyright on any original content posted by account holders. Publication or federation of content does not imply permission for commercial use.</p>
|
||||
|
||||
<p>Commercial use includes the harvesting of data to create products which are then sold, such as statistics, business reports or machine learning models.</p>
|
||||
|
||||
<h3>Federation Policy</h3>
|
||||
|
||||
<p>In a proactive effort to avoid the classic fate of <i>"embrace, extend, extinguish"</i> this system will block any instance launched, acquired or funded by Alphabet, Facebook, Twitter, Microsoft, Apple, Amazon, Elsevier or other monopolistic Silicon Valley companies.</p>
|
||||
|
||||
<p>This system will not federate with instances whose moderation policy is incompatible with the content policy described above. If an instance lacks a moderation policy, or refuses to enforce one, it will be assumed to be incompatible.</p>
|
||||
|
||||
<h3>Copyrights</h3>
|
||||
|
||||
<p>Epicyon is licensed under <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU AGPL version 3</a>
|
||||
|
||||
<p>Emojis designed by <a href="https://openmoji.org">OpenMoji</a> – the open-source emoji and icon project. License: <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a></p>
|
||||
|
||||
<p>Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under <a href="https://www.apache.org/licenses/LICENSE-2.0">Apache 2.0</a>.</p>
|
||||
@@ -68,9 +68,7 @@ def createDelete(session,baseDir: str,federationList: [], \
|
||||
deleteNickname=None
|
||||
deleteDomain=None
|
||||
deletePort=None
|
||||
if '/users/' in objectUrl or \
|
||||
'/channel/' in objectUrl or \
|
||||
'/profile/' in objectUrl:
|
||||
if '/users/' in objectUrl or '/profile/' in objectUrl:
|
||||
deleteNickname=getNicknameFromActor(objectUrl)
|
||||
deleteDomain,deletePort=getDomainFromActor(objectUrl)
|
||||
|
||||
@@ -129,8 +127,7 @@ def sendDeleteViaServer(baseDir: str,session, \
|
||||
# get the actor inbox for the To handle
|
||||
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
|
||||
getPersonBox(baseDir,session,wfRequest,personCache, \
|
||||
projectVersion,httpPrefix,fromNickname, \
|
||||
fromDomain,postToBox)
|
||||
projectVersion,httpPrefix,fromDomain,postToBox)
|
||||
|
||||
if not inboxUrl:
|
||||
if debug:
|
||||
@@ -244,9 +241,7 @@ def outboxDelete(baseDir: str,httpPrefix: str, \
|
||||
if debug:
|
||||
print('DEBUG: c2s delete object is not a status')
|
||||
return
|
||||
if '/users/' not in messageId and \
|
||||
'/channel/' not in messageId and \
|
||||
'/profile/' not in messageId:
|
||||
if '/users/' not in messageId and '/profile/' not in messageId:
|
||||
if debug:
|
||||
print('DEBUG: c2s delete object has no nickname')
|
||||
return
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
__filename__ = "donate.py"
|
||||
__author__ = "Bob Mottram"
|
||||
__license__ = "AGPL3+"
|
||||
__version__ = "1.0.0"
|
||||
__maintainer__ = "Bob Mottram"
|
||||
__email__ = "bob@freedombone.net"
|
||||
__status__ = "Production"
|
||||
|
||||
import json
|
||||
|
||||
def getDonationTypes() -> str:
|
||||
return ('patreon','paypal','gofundme','liberapay', \
|
||||
'kickstarter','indiegogo','crowdsupply', \
|
||||
'subscribestar')
|
||||
|
||||
def getDonationUrl(actorJson: {}) -> str:
|
||||
"""Returns a link used for donations
|
||||
"""
|
||||
if not actorJson.get('attachment'):
|
||||
return ''
|
||||
donationType=getDonationTypes()
|
||||
for propertyValue in actorJson['attachment']:
|
||||
if not propertyValue.get('name'):
|
||||
continue
|
||||
if propertyValue['name'].lower() not in donationType:
|
||||
continue
|
||||
if not propertyValue.get('type'):
|
||||
continue
|
||||
if not propertyValue.get('value'):
|
||||
continue
|
||||
if propertyValue['type']!='PropertyValue':
|
||||
continue
|
||||
if '<a href="' not in propertyValue['value']:
|
||||
continue
|
||||
donateUrl=propertyValue['value'].split('<a href="')[1]
|
||||
if '"' in donateUrl:
|
||||
return donateUrl.split('"')[0]
|
||||
return ''
|
||||
|
||||
def setDonationUrl(actorJson: {},donateUrl: str) -> None:
|
||||
"""Sets a link used for donations
|
||||
"""
|
||||
if not actorJson.get('attachment'):
|
||||
actorJson['attachment']=[]
|
||||
|
||||
donationType=getDonationTypes()
|
||||
donateName=None
|
||||
for paymentService in donationType:
|
||||
if paymentService in donateUrl:
|
||||
donateName=paymentService
|
||||
if not donateName:
|
||||
return
|
||||
|
||||
donateValue='<a href="'+donateUrl+'" rel="me nofollow noopener noreferrer" target="_blank">'+donateUrl+'</a>'
|
||||
|
||||
for propertyValue in actorJson['attachment']:
|
||||
if not propertyValue.get('name'):
|
||||
continue
|
||||
if not propertyValue.get('type'):
|
||||
continue
|
||||
if propertyValue['name'].lower()!=donateName:
|
||||
continue
|
||||
if propertyValue['type']!='PropertyValue':
|
||||
continue
|
||||
propertyValue['value']=donateValue
|
||||
return
|
||||
|
||||
newDonate={
|
||||
"name": donateName,
|
||||
"type": "PropertyValue",
|
||||
"value": donateValue
|
||||
}
|
||||
actorJson['attachment'].append(newDonate)
|
||||
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -437,6 +437,7 @@
|
||||
"tent": "26FA",
|
||||
"thermometer": "1F321",
|
||||
"thinking": "1F914",
|
||||
"thinkingstallman": "7B2F1",
|
||||
"tick": "2714",
|
||||
"tiger": "1F405",
|
||||
"tomato": "1F345",
|
||||
@@ -529,11 +530,8 @@
|
||||
"manjaro": "manjaro",
|
||||
"mint": "mint",
|
||||
"ubuntu": "ubuntu",
|
||||
"ubuntumate": "ubuntumate",
|
||||
"kubuntu": "kubuntu",
|
||||
"opensuse": "opensuse",
|
||||
"arch": "arch",
|
||||
"archlinux": "arch",
|
||||
"debian": "debian",
|
||||
"zorin": "zorin",
|
||||
"solus": "solus",
|
||||
@@ -545,190 +543,5 @@
|
||||
"intersex": "intersex",
|
||||
"lesbianflag": "lesbianflag",
|
||||
"transflag": "transflag",
|
||||
"transfem": "transfem",
|
||||
"trebuchet": "trebuchet",
|
||||
"molotov": "molotov",
|
||||
"walkdog": "E182",
|
||||
"signpost": "E094",
|
||||
"parents": "E081",
|
||||
"redcross": "E090",
|
||||
"firehazard": "E089",
|
||||
"firstaid": "E140",
|
||||
"download": "E252",
|
||||
"upload": "E251",
|
||||
"wifi": "E254",
|
||||
"lighter": "E143",
|
||||
"power": "E097",
|
||||
"sleep": "1F4A4",
|
||||
"goose": "1F9A2",
|
||||
"shower": "1F6BF",
|
||||
"shopping": "1F6D2",
|
||||
"maple": "1F341",
|
||||
"shamrock2": "1F340",
|
||||
"shootingstar": "1F320",
|
||||
"rain": "1F327",
|
||||
"snow": "1F328",
|
||||
"lightning2": "1F329",
|
||||
"sunny": "1F324",
|
||||
"free": "1F193",
|
||||
"headphones": "1F3A7",
|
||||
"wildcat": "wildcat",
|
||||
"footprints": "1F463",
|
||||
"pear": "1F350",
|
||||
"snorkel": "1F93F",
|
||||
"pineapple": "1F34D",
|
||||
"statueliberty": "1F5FD",
|
||||
"grapeheart": "grapeheart",
|
||||
"firefox": "firefox",
|
||||
"dove": "dove",
|
||||
"unspecifiedgoose": "unspecifiedgoose",
|
||||
"lesbian_heart": "lesbian_heart",
|
||||
"heart_trans": "heart_trans",
|
||||
"greensun": "greensun",
|
||||
"calendar": "1F5D3",
|
||||
"thinkpadthinking": "thinkpadthinking",
|
||||
"meowflushed": "meowflushed",
|
||||
"meowpeek": "meowpeek",
|
||||
"meowamused": "meowamused",
|
||||
"meowaww": "meowaww",
|
||||
"meowkissblush": "meowkissblush",
|
||||
"meowthumbsup": "meowthumbsup",
|
||||
"meowthinking": "meowthinking",
|
||||
"meowrollingeyes": "meowrollingeyes",
|
||||
"meownervous": "meownervous",
|
||||
"meowshh": "meowshh",
|
||||
"meowsmilehappyeyes": "meowsmilehappyeyes",
|
||||
"meowfacepalm": "meowfacepalm",
|
||||
"meowmelt": "meowmelt",
|
||||
"meowheart": "meowheart",
|
||||
"meowsmirk": "meowsmirk",
|
||||
"meowhyperthink": "meowhyperthink",
|
||||
"meowuwu": "meowuwu",
|
||||
"meowowo": "meowowo",
|
||||
"meowthinkingcool": "meowthinkingcool",
|
||||
"meowdrool": "meowdrool",
|
||||
"meownauseated": "meownauseated",
|
||||
"meowpopcorn": "meowpopcorn",
|
||||
"meowwaitwhat": "meowwaitwhat",
|
||||
"meowupset": "meowupset",
|
||||
"meowtilt": "meowtilt",
|
||||
"meowsleepless": "meowsleepless",
|
||||
"meowtired": "meowtired",
|
||||
"meowpats": "meowpats",
|
||||
"meowglare": "meowglare",
|
||||
"meowowoevil": "meowowoevil",
|
||||
"meowpurrfecto": "meowpurrfecto",
|
||||
"meowblush": "meowblush",
|
||||
"meowthinkingsmirk": "meowthinkingsmirk",
|
||||
"meowstop": "meowstop",
|
||||
"meowsad": "meowsad",
|
||||
"meowcowboy": "meowcowboy",
|
||||
"meowwave": "meowwave",
|
||||
"meowpolice": "meowpolice",
|
||||
"meowdevil": "meowdevil",
|
||||
"meowkiss": "meowkiss",
|
||||
"meowpout": "meowpout",
|
||||
"meowcoy": "meowcoy",
|
||||
"meowcheer": "meowcheer",
|
||||
"meowmeltmeltmeltmelt": "meowmeltmeltmeltmelt",
|
||||
"meowsweats": "meowsweats",
|
||||
"meowokhand": "meowokhand",
|
||||
"meowreachrev": "meowreachrev",
|
||||
"meowreach": "meowreach",
|
||||
"meowopenmouth": "meowopenmouth",
|
||||
"meowweary": "meowweary",
|
||||
"meownomcookie": "meownomcookie",
|
||||
"meowwink": "meowwink",
|
||||
"meowthumbsdown": "meowthumbsdown",
|
||||
"meowthinkingglare": "meowthinkingglare",
|
||||
"meowscream": "meowscream",
|
||||
"meowgift": "meowgift",
|
||||
"meowhearteyes": "meowhearteyes",
|
||||
"meowdizzy": "meowdizzy",
|
||||
"meowupsidedown": "meowupsidedown",
|
||||
"meownom": "meownom",
|
||||
"meowneutral": "meowneutral",
|
||||
"meowwaverev": "meowwaverev",
|
||||
"meowsmile": "meowsmile",
|
||||
"meowsleeping": "meowsleeping",
|
||||
"meowhuh": "meowhuh",
|
||||
"meowmelttears": "meowmelttears",
|
||||
"meowsadpats": "meowsadpats",
|
||||
"meowsnuggle": "meowsnuggle",
|
||||
"meowcamera": "meowcamera",
|
||||
"meowtongue": "meowtongue",
|
||||
"meowangel": "meowangel",
|
||||
"meowmeltmelt": "meowmeltmelt",
|
||||
"meowmeltmeltmelt": "meowmeltmeltmelt",
|
||||
"meowsob": "meowsob",
|
||||
"meowdab": "meowdab",
|
||||
"meowwhistle": "meowwhistle",
|
||||
"meowsmileopenmouth": "meowsmileopenmouth",
|
||||
"meowterrified": "meowterrified",
|
||||
"meowtonguewink": "meowtonguewink",
|
||||
"meowcry": "meowcry",
|
||||
"meowthonkang": "meowthonkang",
|
||||
"meowhearthug": "meowhearthug",
|
||||
"meowcoffee": "meowcoffee",
|
||||
"meowmorning": "meowmorning",
|
||||
"meowevening": "meowevening",
|
||||
"meowdetective": "meowdetective",
|
||||
"meowxd": "meowxd",
|
||||
"meowheadphones": "meowheadphones",
|
||||
"meowreachsad": "meowreachsad",
|
||||
"meowreachsadrev": "meowreachsadrev",
|
||||
"meowthinksmart": "meowthinksmart",
|
||||
"meowshrug": "meowshrug",
|
||||
"meowwavepeek": "meowwavepeek",
|
||||
"meowthinkingportal": "meowthinkingportal",
|
||||
"meowsmilesweat": "meowsmilesweat",
|
||||
"meowghostwave": "meowghostwave",
|
||||
"meowmeltthumbsup": "meowmeltthumbsup",
|
||||
"meowtihihi": "meowtihihi",
|
||||
"meowinlove": "meowinlove",
|
||||
"meowbox": "meowbox",
|
||||
"meownwn": "meownwn",
|
||||
"meowderpy": "meowderpy",
|
||||
"blobcat": "blobcat",
|
||||
"blobcathappy": "blobcathappy",
|
||||
"blobcoolcat": "blobcoolcat",
|
||||
"blobcatwink": "blobcatwink",
|
||||
"blobcatwinktongue": "blobcatwinktongue",
|
||||
"blobcatcoffee": "blobcatcoffee",
|
||||
"blobcatsad": "blobcatsad",
|
||||
"blobcatblep": "blobcatblep",
|
||||
"blobcatevil": "blobcatevil",
|
||||
"blobcataww": "blobcataww",
|
||||
"blobcatpats": "blobcatpats",
|
||||
"blobcatangry": "blobcatangry",
|
||||
"blobcatmlem": "blobcatmlem",
|
||||
"blobcatthumbsup": "blobcatthumbsup",
|
||||
"blobcatthumbsdown": "blobcatthumbsdown",
|
||||
"blobcatsleep": "blobcatsleep",
|
||||
"blobcatblush": "blobcatblush",
|
||||
"blobcatsneezing": "blobcatsneezing",
|
||||
"blobcatsnuggle": "blobcatsnuggle",
|
||||
"blobcathighfive": "blobcathighfive",
|
||||
"blobcatheart": "blobcatheart",
|
||||
"blobcatfearful": "blobcatfearful",
|
||||
"blobcattilt": "blobcattilt",
|
||||
"blobcatfingerguns": "blobcatfingerguns",
|
||||
"blobcattea": "blobcattea",
|
||||
"blobcatkissheart": "blobcatkissheart",
|
||||
"blobcatshy": "blobcatshy",
|
||||
"blobcathissing": "blobcathissing",
|
||||
"blobcattableflip": "blobcattableflip",
|
||||
"blobcathug": "blobcathug",
|
||||
"blobcatlewd": "blobcatlewd",
|
||||
"blobcatdab": "blobcatdab",
|
||||
"blobcatcouple": "blobcatcouple",
|
||||
"blobcatjava": "blobcatjava",
|
||||
"blobcatunamused": "blobcatunamused",
|
||||
"blobcatscared": "blobcatscared",
|
||||
"blobcatmorningcoffee": "blobcatmorningcoffee",
|
||||
"blobcat3c": "blobcat3c",
|
||||
"blobcatbusiness": "blobcatbusiness",
|
||||
"blobcatdeficit": "blobcatdeficit",
|
||||
"blobcatscience": "blobcatscience",
|
||||
"blobcatstop": "blobcatstop"
|
||||
"transfem": "transfem"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |