328 lines
7.9 KiB
Python
328 lines
7.9 KiB
Python
import Crypto
|
|
import asyncio
|
|
import click
|
|
import json
|
|
import logging
|
|
import platform
|
|
|
|
from aiohttp.web import AppRunner, TCPSite
|
|
from cachetools import LRUCache
|
|
|
|
from . import misc, views
|
|
from .application import app
|
|
from .config import DotDict, RelayConfig
|
|
from .database import RelayDatabase
|
|
from .misc import follow_remote_actor, unfollow_remote_actor
|
|
|
|
|
|
@click.group('cli', context_settings={'show_default': True}, invoke_without_command=True)
|
|
@click.option('--config', '-c', default='relay.yaml', help='path to the relay\'s config')
|
|
@click.pass_context
|
|
def cli(ctx, config):
|
|
app['config'] = RelayConfig(config)
|
|
|
|
if not app['config'].load():
|
|
app['config'].save()
|
|
|
|
app['database'] = RelayDatabase(app['config'])
|
|
app['database'].load()
|
|
|
|
app['cache'] = DotDict()
|
|
app['semaphore'] = asyncio.Semaphore(app['config']['push_limit'])
|
|
|
|
for key in app['config'].cachekeys:
|
|
app['cache'][key] = LRUCache(app['config'][key])
|
|
|
|
if not ctx.invoked_subcommand:
|
|
relay_run.callback()
|
|
|
|
|
|
@cli.command('list')
|
|
@click.argument('type', required=False, default='inbox')
|
|
def relay_list(type):
|
|
'List all following instances'
|
|
|
|
assert type in [None, 'inbox', 'ban', 'whitelist']
|
|
|
|
config = app['config']
|
|
database = app['database']
|
|
|
|
if not type or type == 'inbox':
|
|
click.echo('Connected to the following instances or relays:')
|
|
|
|
for inbox in database.inboxes:
|
|
click.echo(f'- {inbox}')
|
|
|
|
elif type == 'ban':
|
|
click.echo('Banned instances:')
|
|
|
|
for instance in config.blocked_instances:
|
|
click.echo(f'- {instance}')
|
|
|
|
click.echo('\nBanned software:')
|
|
|
|
for software in config.blocked_software:
|
|
click.echo(f'- {software}')
|
|
|
|
elif type == 'whitelist':
|
|
click.echo('Whitelisted instances:')
|
|
|
|
for instance in config.whitelist:
|
|
click.echo(f'- {instance}')
|
|
|
|
|
|
@cli.command('follow')
|
|
@click.argument('actor')
|
|
def relay_follow(actor):
|
|
'Follow an actor (Relay must be running)'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
loop.run_until_complete(handle_follow_actor(actor))
|
|
|
|
|
|
@cli.command('unfollow')
|
|
@click.argument('actor')
|
|
def relay_follow(actor):
|
|
'Unfollow an actor (Relay must be running)'
|
|
|
|
loop = asyncio.new_event_loop()
|
|
loop.run_until_complete(handle_unfollow_actor(actor))
|
|
|
|
|
|
@cli.command('add')
|
|
@click.argument('inbox')
|
|
def relay_add(inbox):
|
|
'Add an inbox to the database'
|
|
|
|
database = app['database']
|
|
config = app['config']
|
|
|
|
if not inbox.startswith('http'):
|
|
inbox = f'https://{inbox}/inbox'
|
|
|
|
if database.get_inbox(inbox):
|
|
click.echo(f'Error: Inbox already in database: {inbox}')
|
|
return
|
|
|
|
if database.get_inbox(inbox):
|
|
click.echo(f'Error: Already added inbox: {inbox}')
|
|
return
|
|
|
|
if config.is_banned(inbox):
|
|
click.echo(f'Error: Refusing to add banned inbox: {inbox}')
|
|
return
|
|
|
|
database.add_inbox(inbox)
|
|
database.save()
|
|
click.echo(f'Added inbox to the database: {inbox}')
|
|
|
|
|
|
@cli.command('remove')
|
|
@click.argument('inbox')
|
|
def relay_remove(inbox):
|
|
'Remove an inbox from the database'
|
|
|
|
database = app['database']
|
|
dbinbox = database.get_inbox(inbox)
|
|
|
|
if not dbinbox:
|
|
click.echo(f'Error: Inbox does not exist: {inbox}')
|
|
return
|
|
|
|
database.del_inbox(dbinbox)
|
|
database.save()
|
|
click.echo(f'Removed inbox from the database: {inbox}')
|
|
|
|
|
|
# todo: add nested groups
|
|
@cli.command('ban')
|
|
@click.argument('type')
|
|
@click.argument('target')
|
|
def relay_ban(type, target):
|
|
'Ban an instance or software'
|
|
|
|
assert type in ['instance', 'software']
|
|
|
|
config = app['config']
|
|
database = app['database']
|
|
inbox = database.get_inbox(target)
|
|
|
|
bancmd = getattr(config, f'ban_{type}')
|
|
|
|
if bancmd(target):
|
|
config.save()
|
|
|
|
if inbox:
|
|
database.del_inbox(inbox)
|
|
database.save()
|
|
|
|
click.echo(f'Banned {type}: {target}')
|
|
return
|
|
|
|
click.echo(f'{type.title()} already banned: {target}')
|
|
|
|
|
|
@cli.command('unban')
|
|
@click.argument('type')
|
|
@click.argument('target')
|
|
def relay_unban(type, target):
|
|
'Unban an instance or software'
|
|
|
|
assert type in ['instance', 'software']
|
|
|
|
config = app['config']
|
|
database = app['database']
|
|
|
|
unbancmd = getattr(config, f'unban_{type}')
|
|
|
|
if unbancmd(target):
|
|
config.save()
|
|
|
|
return click.echo(f'Unbanned {type}: {target}')
|
|
|
|
return click.echo(f'{type.title()} is not banned: {target}')
|
|
|
|
|
|
@cli.command('allow')
|
|
@click.argument('instance')
|
|
def relay_allow(instance):
|
|
'Add an instance to the whitelist'
|
|
|
|
config = app['config']
|
|
|
|
if not config.add_whitelist(instance):
|
|
return click.echo(f'Instance already in the whitelist: {instance}')
|
|
|
|
config.save()
|
|
click.echo(f'Instance added to the whitelist: {instance}')
|
|
|
|
|
|
@cli.command('deny')
|
|
@click.argument('instance')
|
|
def relay_deny(instance):
|
|
'Remove an instance from the whitelist'
|
|
|
|
config = app['config']
|
|
database = app['database']
|
|
inbox = database.get_inbox(instance)
|
|
|
|
if not config.del_whitelist(instance):
|
|
return click.echo(f'Instance not in the whitelist: {instance}')
|
|
|
|
config.save()
|
|
|
|
if inbox and config.whitelist_enabled:
|
|
database.del_inbox(inbox)
|
|
database.save()
|
|
|
|
click.echo(f'Removed instance from the whitelist: {instance}')
|
|
|
|
|
|
@cli.command('setup')
|
|
def relay_setup():
|
|
'Generate a new config'
|
|
|
|
config = app['config']
|
|
|
|
while True:
|
|
config.host = click.prompt('What domain will the relay be hosted on?', default=config.host)
|
|
|
|
if not config.host.endswith('example.com'):
|
|
break
|
|
|
|
click.echo('The domain must not be example.com')
|
|
|
|
config.listen = click.prompt('Which address should the relay listen on?', default=config.listen)
|
|
|
|
while True:
|
|
config.port = click.prompt('What TCP port should the relay listen on?', default=config.port, type=int)
|
|
break
|
|
|
|
config.save()
|
|
|
|
if click.confirm('Relay all setup! Would you like to run it now?'):
|
|
relay_run.callback()
|
|
|
|
|
|
@cli.command('run')
|
|
def relay_run():
|
|
'Run the relay'
|
|
|
|
if app['config'].host.endswith('example.com'):
|
|
return click.echo('Relay is not set up. Please edit your relay config or run "activityrelay setup".')
|
|
|
|
vers_split = platform.python_version().split('.')
|
|
pip_command = 'pip3 uninstall pycrypto && pip3 install pycryptodome'
|
|
|
|
if Crypto.__version__ == '2.6.1':
|
|
if int(vers_split[1]) > 7:
|
|
click.echo('Error: PyCrypto is broken on Python 3.8+. Please replace it with pycryptodome before running again. Exiting...')
|
|
return click.echo(pip_command)
|
|
|
|
else:
|
|
click.echo('Warning: PyCrypto is old and should be replaced with pycryptodome')
|
|
return click.echo(pip_command)
|
|
|
|
# web pages
|
|
app.router.add_get('/', views.home)
|
|
|
|
# endpoints
|
|
app.router.add_post('/actor', views.inbox)
|
|
app.router.add_post('/inbox', views.inbox)
|
|
app.router.add_get('/actor', views.actor)
|
|
app.router.add_get('/nodeinfo/2.0.json', views.nodeinfo_2_0)
|
|
app.router.add_get('/.well-known/nodeinfo', views.nodeinfo_wellknown)
|
|
app.router.add_get('/.well-known/webfinger', views.webfinger)
|
|
|
|
if logging.DEBUG >= logging.root.level:
|
|
app.router.add_get('/stats', views.stats)
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
asyncio.ensure_future(handle_start_webserver(), loop=loop)
|
|
loop.run_forever()
|
|
|
|
|
|
async def handle_follow_actor(app, target):
|
|
config = app['config']
|
|
|
|
if not target.startswith('http'):
|
|
target = f'https://{target}/actor'
|
|
|
|
if config.is_banned(target):
|
|
return click.echo(f'Error: Refusing to follow banned actor: {target}')
|
|
|
|
await misc.follow_remote_actor(target)
|
|
click.echo(f'Sent follow message to: {target}')
|
|
|
|
|
|
async def handle_unfollow_actor(app, target):
|
|
database = app['database']
|
|
|
|
if not target.startswith('http'):
|
|
target = f'https://{target}/actor'
|
|
|
|
if not database.get_inbox(target):
|
|
return click.echo(f'Error: Not following actor: {target}')
|
|
|
|
await misc.unfollow_remote_actor(target)
|
|
click.echo(f'Sent unfollow message to: {target}')
|
|
|
|
|
|
async def handle_start_webserver():
|
|
config = app['config']
|
|
runner = AppRunner(app, access_log_format='%{X-Forwarded-For}i "%r" %s %b "%{Referer}i" "%{User-Agent}i"')
|
|
|
|
logging.info(f'Starting webserver at {config.host} ({config.listen}:{config.port})')
|
|
await runner.setup()
|
|
|
|
site = TCPSite(runner, config.listen, config.port)
|
|
await site.start()
|
|
|
|
|
|
def main():
|
|
cli(prog_name='relay')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
click.echo('Running relay.manage is depreciated. Run `activityrelay [command]` instead.')
|