epicyon/epicyon.py

431 lines
17 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "epicyon.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "0.0.1"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
from person import createPerson
2019-07-05 11:27:18 +00:00
from person import createSharedInbox
from person import createCapabilitiesInbox
2019-07-03 09:40:27 +00:00
from person import setPreferredNickname
2019-06-28 20:00:25 +00:00
from person import setBio
from person import validNickname
2019-06-28 18:55:29 +00:00
from webfinger import webfingerHandle
2019-07-02 09:25:29 +00:00
from posts import getPosts
2019-06-29 10:08:59 +00:00
from posts import createPublicPost
2019-06-29 11:47:33 +00:00
from posts import deleteAllPosts
2019-06-29 13:17:02 +00:00
from posts import createOutbox
2019-06-29 13:44:21 +00:00
from posts import archivePosts
2019-06-30 10:14:02 +00:00
from posts import sendPost
2019-07-03 10:31:02 +00:00
from posts import getPublicPostsOfPerson
2019-07-05 15:53:26 +00:00
from posts import getUserUrl
2019-06-28 18:55:29 +00:00
from session import createSession
2019-06-29 16:47:37 +00:00
from session import getJson
2019-06-28 18:55:29 +00:00
import json
2019-06-30 22:56:37 +00:00
import os
2019-07-04 22:51:40 +00:00
import shutil
2019-06-28 18:55:29 +00:00
import sys
import requests
from pprint import pprint
2019-06-30 20:14:03 +00:00
from tests import testHttpsig
2019-06-28 18:55:29 +00:00
from daemon import runDaemon
2019-06-28 19:52:35 +00:00
import socket
2019-06-29 18:23:13 +00:00
from follow import clearFollows
2019-06-29 20:21:37 +00:00
from follow import clearFollowers
2019-07-06 19:24:52 +00:00
from utils import followPerson
2019-06-29 18:23:13 +00:00
from follow import followerOfPerson
from follow import unfollowPerson
from follow import unfollowerOfPerson
from follow import getFollowersOfPerson
2019-06-30 21:20:02 +00:00
from tests import testPostMessageBetweenServers
2019-07-06 13:49:25 +00:00
from tests import testFollowBetweenServers
2019-06-30 21:20:02 +00:00
from tests import runAllTests
2019-07-05 09:20:54 +00:00
from config import setConfigParam
from config import getConfigParam
2019-07-05 09:44:15 +00:00
from auth import storeBasicCredentials
2019-07-05 09:49:57 +00:00
from auth import removePassword
2019-07-05 11:27:18 +00:00
from auth import createPassword
2019-07-09 15:51:31 +00:00
from utils import getDomainFromActor
from utils import getNicknameFromActor
2019-07-03 09:24:55 +00:00
import argparse
2019-06-30 21:20:02 +00:00
2019-07-03 09:24:55 +00:00
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
2019-07-05 09:20:54 +00:00
2019-07-03 09:24:55 +00:00
parser = argparse.ArgumentParser(description='ActivityPub Server')
2019-07-09 15:51:31 +00:00
parser.add_argument('-n','--nickname', dest='nickname', type=str,default=None, \
help='Nickname of the account to use')
parser.add_argument('--fol','--follow', dest='follow', type=str,default=None, \
help='Handle of account to follow. eg. nickname@domain')
2019-07-06 20:19:49 +00:00
parser.add_argument('-d','--domain', dest='domain', type=str,default=None, \
2019-07-03 09:24:55 +00:00
help='Domain name of the server')
2019-07-06 20:19:49 +00:00
parser.add_argument('-p','--port', dest='port', type=int,default=None, \
2019-07-03 09:24:55 +00:00
help='Port number to run on')
2019-07-06 20:19:49 +00:00
parser.add_argument('--path', dest='baseDir', \
type=str,default=os.getcwd(), \
2019-07-03 09:24:55 +00:00
help='Directory in which to store posts')
2019-07-06 20:19:49 +00:00
parser.add_argument('-a','--addaccount', dest='addaccount', \
type=str,default=None, \
2019-07-04 22:44:32 +00:00
help='Adds a new account')
2019-07-06 20:19:49 +00:00
parser.add_argument('-r','--rmaccount', dest='rmaccount', \
type=str,default=None, \
2019-07-04 22:50:40 +00:00
help='Remove an account')
2019-07-06 20:19:49 +00:00
parser.add_argument('--pass','--password', dest='password', \
type=str,default=None, \
2019-07-04 22:44:32 +00:00
help='Set a password for an account')
2019-07-06 20:19:49 +00:00
parser.add_argument('--chpass','--changepassword', \
nargs='+',dest='changepassword', \
2019-07-05 09:44:15 +00:00
help='Change the password for an account')
2019-07-06 20:19:49 +00:00
parser.add_argument('--actor', dest='actor', type=str,default=None, \
2019-07-05 15:53:26 +00:00
help='Show the json actor the given handle')
2019-07-06 20:19:49 +00:00
parser.add_argument('--posts', dest='posts', type=str,default=None, \
2019-07-03 10:31:02 +00:00
help='Show posts for the given handle')
2019-07-06 20:19:49 +00:00
parser.add_argument('--postsraw', dest='postsraw', type=str,default=None, \
2019-07-03 11:24:38 +00:00
help='Show raw json of posts for the given handle')
2019-07-12 10:53:49 +00:00
parser.add_argument('--json', dest='json', type=str,default=None, \
help='Show the json for a given activitypub url')
2019-07-06 20:19:49 +00:00
parser.add_argument('-f','--federate', nargs='+',dest='federationList', \
2019-07-03 09:24:55 +00:00
help='Specify federation list separated by spaces')
2019-07-06 20:19:49 +00:00
parser.add_argument("--debug", type=str2bool, nargs='?', \
const=True, default=False, \
help="Show debug messages")
parser.add_argument("--http", type=str2bool, nargs='?', \
const=True, default=False, \
help="Use http only")
parser.add_argument("--dat", type=str2bool, nargs='?', \
const=True, default=False, \
help="Use dat protocol only")
parser.add_argument("--tor", type=str2bool, nargs='?', \
const=True, default=False, \
help="Route via Tor")
parser.add_argument("--tests", type=str2bool, nargs='?', \
const=True, default=False, \
help="Run unit tests")
parser.add_argument("--testsnetwork", type=str2bool, nargs='?', \
const=True, default=False, \
help="Run network unit tests")
parser.add_argument("--testdata", type=str2bool, nargs='?', \
const=True, default=False, \
help="Generate some data for testing purposes")
parser.add_argument("--ocap", type=str2bool, nargs='?', \
const=True, default=False, \
help="Always strictly enforce object capabilities")
2019-07-09 17:54:08 +00:00
parser.add_argument("--noreply", type=str2bool, nargs='?', \
const=True, default=False, \
help="Default capabilities don't allow replies on posts")
parser.add_argument("--nolike", type=str2bool, nargs='?', \
const=True, default=False, \
help="Default capabilities don't allow likes/favourites on posts")
2019-07-09 18:11:23 +00:00
parser.add_argument("--nopics", type=str2bool, nargs='?', \
const=True, default=False, \
help="Default capabilities don't allow attached pictures")
parser.add_argument("--noannounce","--norepeat", type=str2bool, nargs='?', \
const=True, default=False, \
help="Default capabilities don't allow announce/repeat")
parser.add_argument("--cw", type=str2bool, nargs='?', \
const=True, default=False, \
help="Default capabilities don't allow posts without content warnings")
2019-07-03 09:24:55 +00:00
args = parser.parse_args()
2019-07-03 10:31:02 +00:00
2019-07-03 16:14:45 +00:00
debug=False
if args.debug:
debug=True
2019-07-03 09:24:55 +00:00
if args.tests:
runAllTests()
sys.exit()
2019-07-03 10:31:02 +00:00
if args.testsnetwork:
print('Network Tests')
2019-07-06 19:36:27 +00:00
testPostMessageBetweenServers()
2019-07-06 13:49:25 +00:00
testFollowBetweenServers()
2019-07-03 10:31:02 +00:00
sys.exit()
2019-07-05 15:53:26 +00:00
2019-07-03 10:33:55 +00:00
if args.posts:
2019-07-05 15:53:26 +00:00
if '@' not in args.posts:
print('Syntax: --posts nickname@domain')
sys.exit()
2019-07-03 10:33:55 +00:00
nickname=args.posts.split('@')[0]
domain=args.posts.split('@')[1]
2019-07-03 11:24:38 +00:00
getPublicPostsOfPerson(nickname,domain,False,True)
sys.exit()
if args.postsraw:
2019-07-06 21:58:56 +00:00
if '@' not in args.postsraw:
2019-07-05 15:53:26 +00:00
print('Syntax: --postsraw nickname@domain')
sys.exit()
2019-07-03 11:24:38 +00:00
nickname=args.postsraw.split('@')[0]
domain=args.postsraw.split('@')[1]
getPublicPostsOfPerson(nickname,domain,True,False)
2019-07-03 10:33:55 +00:00
sys.exit()
2019-07-04 22:44:32 +00:00
baseDir=args.baseDir
if baseDir.endswith('/'):
print("--path option should not end with '/'")
2019-07-03 10:31:02 +00:00
sys.exit()
2019-07-05 09:20:54 +00:00
# get domain name from configuration
configDomain=getConfigParam(baseDir,'domain')
if configDomain:
domain=configDomain
else:
domain='localhost'
# get port number from configuration
configPort=getConfigParam(baseDir,'port')
if configPort:
port=configPort
else:
port=8085
2019-07-09 15:51:31 +00:00
nickname=None
if args.nickname:
nickname=nickname
httpPrefix='https'
if args.http:
httpPrefix='http'
federationList=[]
if args.federationList:
if len(args.federationList)==1:
if not (args.federationList[0].lower()=='any' or \
args.federationList[0].lower()=='all' or \
args.federationList[0].lower()=='*'):
for federationDomain in args.federationList:
if '@' in federationDomain:
print(federationDomain+': Federate with domains, not individual accounts')
sys.exit()
federationList=args.federationList.copy()
setConfigParam(baseDir,'federationList',federationList)
else:
configFederationList=getConfigParam(baseDir,'federationList')
if configFederationList:
federationList=configFederationList
2019-07-09 15:58:51 +00:00
useTor=args.tor
if domain.endswith('.onion'):
useTor=True
2019-07-09 15:51:31 +00:00
if args.follow and nickname:
if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
print(nickname+' is not an account on the system. use --addaccount if necessary.')
sys.exit()
if '.' not in args.follow:
print("This doesn't look like a fediverse handle")
sys.exit()
followNickname=getNicknameFromActor(args.follow)
followDomain,followPort=getDomainFromActor(args.follow)
if os.path.isfile(baseDir+'/accounts/'+nickname+'@'+domain+'/following.txt'):
if followNickname+'@'+followDomain in open(baseDir+'/accounts/'+nickname+'@'+domain+'/following.txt').read():
print(nickname+'@'+domain+' is already following '+followNickname+'@'+followDomain)
sys.exit()
2019-07-09 15:58:51 +00:00
session = createSession(domain,port,useTor)
2019-07-09 15:51:31 +00:00
personCache={}
cachedWebfingers={}
sendThreads=[]
sendThreads=[]
postLog=[]
followHttpPrefix=httpPrefix
if args.follow.startswith('https'):
followHttpPrefix='https'
sendFollowRequest(session,baseDir, \
nickname,domain,port,httpPrefix, \
followNickname,followDomain,followPort, \
followHttpPrefix, \
False,federationList, \
sendThreads,postLog, \
cachedWebfingers,personCache,debug)
for t in range(30):
time.sleep(1)
if os.path.isfile(baseDir+'/accounts/'+nickname+'@'+domain+'/following.txt'):
if followNickname+'@'+followDomain in open(baseDir+'/accounts/'+nickname+'@'+domain+'/following.txt').read():
print('Ok')
sys.exit()
print('Follow attempt failed')
sys.exit()
2019-07-03 09:40:27 +00:00
nickname='admin'
2019-07-05 09:20:54 +00:00
if args.domain:
domain=args.domain
setConfigParam(baseDir,'domain',domain)
if args.port:
port=args.port
setConfigParam(baseDir,'port',port)
ocapAlways=False
if args.ocap:
ocapAlways=args.ocap
2019-07-03 19:00:03 +00:00
if args.dat:
httpPrefix='dat'
2019-07-04 22:44:32 +00:00
2019-07-05 15:53:26 +00:00
if args.actor:
if '@' not in args.actor:
print('Syntax: --actor nickname@domain')
sys.exit()
nickname=args.actor.split('@')[0]
domain=args.actor.split('@')[1].replace('\n','')
wfCache={}
if domain.endswith('.onion'):
httpPrefix='http'
port=80
else:
httpPrefix='https'
port=443
session = createSession(domain,port,useTor)
wfRequest = webfingerHandle(session,nickname+'@'+domain,httpPrefix,wfCache)
if not wfRequest:
print('Unable to webfinger '+nickname+'@'+domain)
sys.exit()
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
personUrl = getUserUrl(wfRequest)
personJson = getJson(session,personUrl,asHeader,None)
if personJson:
pprint(personJson)
else:
print('Failed to get '+personUrl)
sys.exit()
2019-07-12 10:53:49 +00:00
if args.json:
session = createSession(domain,port,useTor)
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
testJson = getJson(session,args.json,asHeader,None)
pprint(testJson)
sys.exit()
2019-07-04 22:44:32 +00:00
if args.addaccount:
if '@' in args.addaccount:
2019-07-04 22:50:40 +00:00
nickname=args.addaccount.split('@')[0]
domain=args.addaccount.split('@')[1]
2019-07-04 22:44:32 +00:00
else:
2019-07-04 22:50:40 +00:00
nickname=args.addaccount
2019-07-05 09:20:54 +00:00
if not args.domain or not getConfigParam(baseDir,'domain'):
2019-07-04 22:44:32 +00:00
print('Use the --domain option to set the domain name')
sys.exit()
if not validNickname(nickname):
print(nickname+' is a reserved name. Use something different.')
sys.exit()
2019-07-05 09:20:54 +00:00
if not args.password:
print('Use the --password option to set the password for '+nickname)
sys.exit()
if len(args.password.strip())<8:
print('Password should be at least 8 characters')
sys.exit()
2019-07-04 22:50:40 +00:00
if os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
print('Account already exists')
sys.exit()
2019-07-05 09:20:54 +00:00
createPerson(baseDir,nickname,domain,port,httpPrefix,True,args.password.strip())
2019-07-04 22:44:32 +00:00
if os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
print('Account created for '+nickname+'@'+domain)
2019-07-05 09:20:54 +00:00
else:
print('Account creation failed')
2019-07-04 22:44:32 +00:00
sys.exit()
2019-07-04 22:50:40 +00:00
if args.rmaccount:
if '@' in args.rmaccount:
nickname=args.rmaccount.split('@')[0]
domain=args.rmaccount.split('@')[1]
else:
nickname=args.rmaccount
2019-07-05 09:20:54 +00:00
if not args.domain or not getConfigParam(baseDir,'domain'):
2019-07-04 22:50:40 +00:00
print('Use the --domain option to set the domain name')
sys.exit()
2019-07-05 09:20:54 +00:00
handle=nickname+'@'+domain
accountRemoved=False
2019-07-05 09:49:57 +00:00
removePassword(baseDir,nickname)
2019-07-05 09:20:54 +00:00
if os.path.isdir(baseDir+'/accounts/'+handle):
shutil.rmtree(baseDir+'/accounts/'+handle)
accountRemoved=True
if os.path.isfile(baseDir+'/accounts/'+handle+'.json'):
os.remove(baseDir+'/accounts/'+handle+'.json')
accountRemoved=True
if os.path.isfile(baseDir+'/wfendpoints/'+handle+'.json'):
os.remove(baseDir+'/wfendpoints/'+handle+'.json')
accountRemoved=True
if os.path.isfile(baseDir+'/keys/private/'+handle+'.key'):
os.remove(baseDir+'/keys/private/'+handle+'.key')
accountRemoved=True
2019-07-05 09:22:06 +00:00
if os.path.isfile(baseDir+'/keys/public/'+handle+'.pem'):
2019-07-05 09:20:54 +00:00
os.remove(baseDir+'/keys/public/'+handle+'.pem')
accountRemoved=True
if accountRemoved:
print('Account for '+handle+' was removed')
2019-07-04 22:50:40 +00:00
sys.exit()
2019-07-05 09:44:15 +00:00
if args.changepassword:
if len(args.changepassword)!=2:
print('--changepassword [nickname] [new password]')
sys.exit()
if '@' in args.changepassword[0]:
nickname=args.changepassword[0].split('@')[0]
domain=args.changepassword[0].split('@')[1]
else:
nickname=args.changepassword[0]
if not args.domain or not getConfigParam(baseDir,'domain'):
print('Use the --domain option to set the domain name')
sys.exit()
newPassword=args.changepassword[1]
if len(newPassword)<8:
print('Password should be at least 8 characters')
sys.exit()
2019-07-05 09:58:58 +00:00
if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
print('Account '+nickname+'@'+domain+' not found')
sys.exit()
2019-07-05 09:44:15 +00:00
passwordFile=baseDir+'/accounts/passwords'
if os.path.isfile(passwordFile):
if nickname+':' in open(passwordFile).read():
storeBasicCredentials(baseDir,nickname,newPassword)
print('Password for '+nickname+' was changed')
else:
print(nickname+' is not in the passwords file')
else:
print('Passwords file not found')
sys.exit()
2019-07-05 10:03:25 +00:00
if not args.domain and not domain:
2019-07-04 22:44:32 +00:00
print('Specify a domain with --domain [name]')
2019-07-03 09:24:55 +00:00
sys.exit()
2019-07-05 09:44:15 +00:00
2019-07-05 09:20:54 +00:00
if federationList:
2019-07-03 16:16:36 +00:00
print('Federating with: '+str(federationList))
2019-06-28 18:55:29 +00:00
2019-07-03 12:24:54 +00:00
if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
2019-07-03 12:25:42 +00:00
print('Creating default admin account '+nickname+'@'+domain)
2019-07-05 11:27:18 +00:00
print('See config.json for the password. You can remove the password from config.json after moving it elsewhere.')
adminPassword=createPassword(10)
setConfigParam(baseDir,'adminPassword',adminPassword)
createPerson(baseDir,nickname,domain,port,httpPrefix,True,adminPassword)
2019-07-03 12:24:54 +00:00
2019-07-06 20:19:49 +00:00
if args.testdata:
nickname='testuser567'
2019-07-06 20:19:49 +00:00
print('Generating some test data for user: '+nickname)
createPerson(baseDir,nickname,domain,port,httpPrefix,True,'likewhateveryouwantscoob')
deleteAllPosts(baseDir,nickname,domain,'inbox')
deleteAllPosts(baseDir,nickname,domain,'outbox')
followPerson(baseDir,nickname,domain,'admin',domain,federationList,True)
followerOfPerson(baseDir,nickname,domain,'admin',domain,federationList,True)
2019-07-09 14:20:23 +00:00
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"like, this is totally just a test, man",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Zoiks!!!",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Hey scoob we need like a hundred more milkshakes",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"Getting kinda spooky around here",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"And they would have gotten away with it too if it wasn't for those pesky hackers",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"man, these centralized sites are, like, the worst!",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"another mystery solved hey",False,True,False)
createPublicPost(baseDir,nickname,domain,port,httpPrefix,"let's go bowling",False,True,False)
2019-07-12 10:53:49 +00:00
2019-07-09 18:11:23 +00:00
runDaemon(baseDir,domain,port,httpPrefix,federationList, \
args.noreply,args.nolike,args.nopics, \
args.noannounce,args.cw,ocapAlways,useTor,debug)