Compare commits

...

7 commits

Author SHA1 Message Date
Izalia Mae beaddca708 Merge branch 'dev' into 'main'
version 0.3.1

See merge request pleroma/relay!58
2024-04-02 18:30:00 +00:00
Izalia Mae b1a2989ba7 update installation docs to replace git+http url with pypi package 2024-04-02 14:29:49 -04:00
Izalia Mae fd0b9fb723 add shebang to dev script 2024-04-02 13:56:11 -04:00
Izalia Mae ffed955ab3 add note for changing log-level via cli 2024-04-02 13:38:23 -04:00
Izalia Mae 71de40dfca various cli changes
* ensure database file is correctly named in docker container
* add `--skip-questions` option to setup command
* ask to proceed with setup if private key already exists
2024-04-02 13:36:08 -04:00
Izalia Mae 7064adb000 move dev script out of module 2024-04-02 12:36:06 -04:00
Izalia Mae 437075e512 Fix running via docker 2024-04-02 12:32:49 -04:00
8 changed files with 181 additions and 165 deletions

View file

@ -5,7 +5,7 @@ ENV DOCKER_RUNNING=true
# setup various container properties # setup various container properties
VOLUME ["/data"] VOLUME ["/data"]
CMD ["python3", "-m", "relay"] CMD ["python3", "-m", "relay", "run"]
EXPOSE 8080/tcp EXPOSE 8080/tcp
WORKDIR /opt/activityrelay WORKDIR /opt/activityrelay

17
relay/dev.py → dev.py Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import click import click
import platform import platform
import shutil import shutil
@ -7,12 +8,10 @@ import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from relay import __version__, logger as logging
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Sequence from typing import Sequence
from . import __version__
from . import logger as logging
try: try:
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler from watchdog.events import PatternMatchingEventHandler
@ -22,8 +21,7 @@ except ImportError:
pass pass
SCRIPT = Path(__file__).parent REPO = Path(__file__).parent
REPO = SCRIPT.parent
IGNORE_EXT = { IGNORE_EXT = {
'.py', '.py',
'.pyc' '.pyc'
@ -58,8 +56,8 @@ def cli_lint(path: Path, strict: bool, watch: bool) -> None:
handle_run_watcher([sys.executable, "-m", "relay.dev", "lint", str(path)], wait = True) handle_run_watcher([sys.executable, "-m", "relay.dev", "lint", str(path)], wait = True)
return return
flake8 = [sys.executable, '-m', 'flake8', str(path)] flake8 = [sys.executable, '-m', 'flake8', "dev.py", str(path)]
mypy = [sys.executable, '-m', 'mypy', str(path)] mypy = [sys.executable, '-m', 'mypy', "dev.py", str(path)]
if strict: if strict:
mypy.append('--strict') mypy.append('--strict')
@ -108,7 +106,7 @@ def cli_build():
cmd.append('--console') cmd.append('--console')
# putting the spec path on a different drive than the source dir breaks # putting the spec path on a different drive than the source dir breaks
if str(SCRIPT)[0] == tmp[0]: if str(REPO)[0] == tmp[0]:
cmd.extend(['--specpath', tmp]) cmd.extend(['--specpath', tmp])
else: else:
@ -136,7 +134,7 @@ def handle_run_watcher(*commands: Sequence[str], wait: bool = False):
handler.run_procs() handler.run_procs()
watcher = Observer() watcher = Observer()
watcher.schedule(handler, str(SCRIPT), recursive=True) watcher.schedule(handler, str(REPO), recursive=True)
watcher.start() watcher.start()
try: try:
@ -151,7 +149,6 @@ def handle_run_watcher(*commands: Sequence[str], wait: bool = False):
watcher.join() watcher.join()
class WatchHandler(PatternMatchingEventHandler): class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py'] patterns = ['*.py']

View file

@ -2,9 +2,21 @@
case $1 in case $1 in
install) install)
if [[ -z ${2#$} ]]; then
host=127.0.0.1
else
host=$2
fi
if [[ -z ${3#$} ]]; then
port=8080
else
port=$3
fi
docker build -f Dockerfile -t activityrelay . && \ docker build -f Dockerfile -t activityrelay . && \
docker volume create activityrelay-data && \ docker volume create activityrelay-data && \
docker run -it -p 8080:8080 -v activityrelay-data:/data --name activityrelay activityrelay docker run -it -p target=8080,published=${host}:${port} -v activityrelay-data:/data --name activityrelay activityrelay
;; ;;
uninstall) uninstall)
@ -22,6 +34,10 @@ case $1 in
docker stop activityrelay docker stop activityrelay
;; ;;
restart)
docker restart activityrelay
;;
manage) manage)
shift shift
docker exec -it activityrelay python3 -m relay "$@" docker exec -it activityrelay python3 -m relay "$@"
@ -61,10 +77,7 @@ case $1 in
printf "$COLS" "- edit" "Edit the relay's config in \$EDITOR" printf "$COLS" "- edit" "Edit the relay's config in \$EDITOR"
printf "$COLS" "- shell" "Drop into a bash shell on the running container" printf "$COLS" "- shell" "Drop into a bash shell on the running container"
printf "$COLS" "- rescue" "Drop into a bash shell on a temp container with the data volume mounted" printf "$COLS" "- rescue" "Drop into a bash shell on a temp container with the data volume mounted"
printf "$COLS" "- install" "Build the image, create a new container and volume, and run relay setup" printf "$COLS" "- install [address] [port]" "Build the image, create a new container and volume, and run relay setup"
printf "$COLS" "- uninstall" "Delete the relay image, container, and volume" printf "$COLS" "- uninstall" "Delete the relay image, container, and volume"
echo ""
echo "Note: This script may not work. It is recommended to manually install and manage the container if you know what you're doing."
;; ;;
esac esac

View file

@ -154,6 +154,8 @@ When enabled, instances that try to follow the relay will have to be manually ap
Maximum level of messages to log. Maximum level of messages to log.
Note: Changing this setting via CLI does not actually take effect until restart.
Valid values: `DEBUG`, `VERBOSE`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` Valid values: `DEBUG`, `VERBOSE`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
log-level: INFO log-level: INFO

View file

@ -13,9 +13,9 @@ the [official pipx docs](https://pypa.github.io/pipx/installation/) for more in-
python3 -m pip install pipx python3 -m pip install pipx
Now simply install ActivityRelay directly from git Now simply install ActivityRelay from pypi
pipx install git+https://git.pleroma.social/pleroma/relay@0.3.0 pipx install activityrelay
Or from a cloned git repo. Or from a cloned git repo.
@ -36,10 +36,9 @@ be installed via [pyenv](https://github.com/pyenv/pyenv).
## Pip ## Pip
The instructions for installation via pip are very similar to pipx. Installation can be done from The instructions for installation via pip are very similar to pipx
git
python3 -m pip install git+https://git.pleroma.social/pleroma/relay@0.3.0 python3 -m pip install activityrelay
or a cloned git repo. or a cloned git repo.
@ -58,10 +57,11 @@ And start the relay when finished
Installation and management via Docker can be handled with the `docker.sh` script. To install Installation and management via Docker can be handled with the `docker.sh` script. To install
ActivityRelay, run the install command. Once the image is built and the container is created, ActivityRelay, run the install command. Once the image is built and the container is created,
your will be asked to fill out some config options for your relay. you will be asked to fill out some config options for your relay. An address and port can be
specified to change what the relay listens on.
./docker.sh install ./docker.sh install 0.0.0.0 6942
Finally start it up. It will be listening on TCP port 8080. Finally start it up. It will be listening on TCP localhost:8080 by default.
./docker.sh start ./docker.sh start

View file

@ -55,7 +55,7 @@ class Application(web.Application):
DEFAULT: Application | None = None DEFAULT: Application | None = None
def __init__(self, cfgpath: str | None, dev: bool = False): def __init__(self, cfgpath: Path | None, dev: bool = False):
web.Application.__init__(self, web.Application.__init__(self,
middlewares = [ middlewares = [
handle_api_path, handle_api_path,

View file

@ -33,7 +33,7 @@ else:
DOCKER_VALUES = { DOCKER_VALUES = {
'listen': '0.0.0.0', 'listen': '0.0.0.0',
'port': 8080, 'port': 8080,
'sq_path': '/data/relay.jsonld' 'sq_path': '/data/relay.sqlite3'
} }
@ -65,7 +65,7 @@ class Config:
rd_prefix: str = 'activityrelay' rd_prefix: str = 'activityrelay'
def __init__(self, path: str | None = None, load: bool = False): def __init__(self, path: Path | None = None, load: bool = False):
self.path = Config.get_config_dir(path) self.path = Config.get_config_dir(path)
self.reset() self.reset()
@ -92,9 +92,12 @@ class Config:
@staticmethod @staticmethod
def get_config_dir(path: str | None = None) -> Path: def get_config_dir(path: Path | str | None = None) -> Path:
if path: if isinstance(path, str):
return Path(path).expanduser().resolve() path = Path(path)
if path is not None:
return path.expanduser().resolve()
paths = ( paths = (
Path("relay.yaml").resolve(), Path("relay.yaml").resolve(),

View file

@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
import Crypto
import aputils import aputils
import asyncio import asyncio
import click import click
import json
import os import os
import platform
import typing import typing
from pathlib import Path from pathlib import Path
@ -32,139 +31,152 @@ def check_alphanumeric(text: str) -> str:
return text return text
@click.group('cli', context_settings={'show_default': True}, invoke_without_command=True) @click.group('cli', context_settings = {'show_default': True})
@click.option('--config', '-c', help='path to the relay\'s config') @click.option('--config', '-c', type = Path, help = 'path to the relay\'s config')
@click.version_option(version=__version__, prog_name='ActivityRelay') @click.version_option(version = __version__, prog_name = 'ActivityRelay')
@click.pass_context @click.pass_context
def cli(ctx: click.Context, config: str | None) -> None: def cli(ctx: click.Context, config: Path | None) -> None:
if IS_DOCKER:
config = Path("/data/relay.yaml")
# The database was named "relay.jsonld" even though it's an sqlite file. Fix it.
db = Path('/data/relay.sqlite3')
wrongdb = Path('/data/relay.jsonld')
if wrongdb.exists() and not db.exists():
try:
with wrongdb.open('rb') as fd:
json.load(fd)
except json.JSONDecodeError:
wrongdb.rename(db)
ctx.obj = Application(config) ctx.obj = Application(config)
if not ctx.invoked_subcommand:
if ctx.obj.config.domain.endswith('example.com'):
cli_setup.callback() # type: ignore
else:
click.echo(
'[DEPRECATED] Running the relay without the "run" command will be removed in the ' +
'future.'
)
cli_run.callback() # type: ignore
@cli.command('setup') @cli.command('setup')
@click.option('--skip-questions', '-s', is_flag = True, help = 'Just setup the database')
@click.pass_context @click.pass_context
def cli_setup(ctx: click.Context) -> None: def cli_setup(ctx: click.Context, skip_questions: bool) -> None:
'Generate a new config and create the database' 'Generate a new config and create the database'
while True: if ctx.obj.signer is not None:
ctx.obj.config.domain = click.prompt( if not click.prompt('The database is already setup. Are you sure you want to continue?'):
'What domain will the relay be hosted on?', return
default = ctx.obj.config.domain
if skip_questions and ctx.obj.config.domain.endswith('example.com'):
click.echo('You cannot skip the questions if the relay is not configured yet')
return
if not skip_questions:
while True:
ctx.obj.config.domain = click.prompt(
'What domain will the relay be hosted on?',
default = ctx.obj.config.domain
)
if not ctx.obj.config.domain.endswith('example.com'):
break
click.echo('The domain must not end with "example.com"')
if not IS_DOCKER:
ctx.obj.config.listen = click.prompt(
'Which address should the relay listen on?',
default = ctx.obj.config.listen
)
ctx.obj.config.port = click.prompt(
'What TCP port should the relay listen on?',
default = ctx.obj.config.port,
type = int
)
ctx.obj.config.db_type = click.prompt(
'Which database backend will be used?',
default = ctx.obj.config.db_type,
type = click.Choice(['postgres', 'sqlite'], case_sensitive = False)
) )
if not ctx.obj.config.domain.endswith('example.com'): if ctx.obj.config.db_type == 'sqlite' and not IS_DOCKER:
break ctx.obj.config.sq_path = click.prompt(
'Where should the database be stored?',
default = ctx.obj.config.sq_path
)
click.echo('The domain must not end with "example.com"') elif ctx.obj.config.db_type == 'postgres':
ctx.obj.config.pg_name = click.prompt(
'What is the name of the database?',
default = ctx.obj.config.pg_name
)
if not IS_DOCKER: ctx.obj.config.pg_host = click.prompt(
ctx.obj.config.listen = click.prompt( 'What IP address, hostname, or unix socket does the server listen on?',
'Which address should the relay listen on?', default = ctx.obj.config.pg_host,
default = ctx.obj.config.listen type = int
)
ctx.obj.config.pg_port = click.prompt(
'What port does the server listen on?',
default = ctx.obj.config.pg_port,
type = int
)
ctx.obj.config.pg_user = click.prompt(
'Which user will authenticate with the server?',
default = ctx.obj.config.pg_user
)
ctx.obj.config.pg_pass = click.prompt(
'User password',
hide_input = True,
show_default = False,
default = ctx.obj.config.pg_pass or ""
) or None
ctx.obj.config.ca_type = click.prompt(
'Which caching backend?',
default = ctx.obj.config.ca_type,
type = click.Choice(['database', 'redis'], case_sensitive = False)
) )
ctx.obj.config.port = click.prompt( if ctx.obj.config.ca_type == 'redis':
'What TCP port should the relay listen on?', ctx.obj.config.rd_host = click.prompt(
default = ctx.obj.config.port, 'What IP address, hostname, or unix socket does the server listen on?',
type = int default = ctx.obj.config.rd_host
) )
ctx.obj.config.db_type = click.prompt( ctx.obj.config.rd_port = click.prompt(
'Which database backend will be used?', 'What port does the server listen on?',
default = ctx.obj.config.db_type, default = ctx.obj.config.rd_port,
type = click.Choice(['postgres', 'sqlite'], case_sensitive = False) type = int
) )
if ctx.obj.config.db_type == 'sqlite': ctx.obj.config.rd_user = click.prompt(
ctx.obj.config.sq_path = click.prompt( 'Which user will authenticate with the server',
'Where should the database be stored?', default = ctx.obj.config.rd_user
default = ctx.obj.config.sq_path )
)
elif ctx.obj.config.db_type == 'postgres': ctx.obj.config.rd_pass = click.prompt(
ctx.obj.config.pg_name = click.prompt( 'User password',
'What is the name of the database?', hide_input = True,
default = ctx.obj.config.pg_name show_default = False,
) default = ctx.obj.config.rd_pass or ""
) or None
ctx.obj.config.pg_host = click.prompt( ctx.obj.config.rd_database = click.prompt(
'What IP address, hostname, or unix socket does the server listen on?', 'Which database number to use?',
default = ctx.obj.config.pg_host, default = ctx.obj.config.rd_database,
type = int type = int
) )
ctx.obj.config.pg_port = click.prompt( ctx.obj.config.rd_prefix = click.prompt(
'What port does the server listen on?', 'What text should each cache key be prefixed with?',
default = ctx.obj.config.pg_port, default = ctx.obj.config.rd_database,
type = int type = check_alphanumeric
) )
ctx.obj.config.pg_user = click.prompt( ctx.obj.config.save()
'Which user will authenticate with the server?',
default = ctx.obj.config.pg_user
)
ctx.obj.config.pg_pass = click.prompt(
'User password',
hide_input = True,
show_default = False,
default = ctx.obj.config.pg_pass or ""
) or None
ctx.obj.config.ca_type = click.prompt(
'Which caching backend?',
default = ctx.obj.config.ca_type,
type = click.Choice(['database', 'redis'], case_sensitive = False)
)
if ctx.obj.config.ca_type == 'redis':
ctx.obj.config.rd_host = click.prompt(
'What IP address, hostname, or unix socket does the server listen on?',
default = ctx.obj.config.rd_host
)
ctx.obj.config.rd_port = click.prompt(
'What port does the server listen on?',
default = ctx.obj.config.rd_port,
type = int
)
ctx.obj.config.rd_user = click.prompt(
'Which user will authenticate with the server',
default = ctx.obj.config.rd_user
)
ctx.obj.config.rd_pass = click.prompt(
'User password',
hide_input = True,
show_default = False,
default = ctx.obj.config.rd_pass or ""
) or None
ctx.obj.config.rd_database = click.prompt(
'Which database number to use?',
default = ctx.obj.config.rd_database,
type = int
)
ctx.obj.config.rd_prefix = click.prompt(
'What text should each cache key be prefixed with?',
default = ctx.obj.config.rd_database,
type = check_alphanumeric
)
ctx.obj.config.save()
config = { config = {
'private-key': aputils.Signer.new('n/a').export() 'private-key': aputils.Signer.new('n/a').export()
@ -174,7 +186,11 @@ def cli_setup(ctx: click.Context) -> None:
for key, value in config.items(): for key, value in config.items():
conn.put_config(key, value) conn.put_config(key, value)
if not IS_DOCKER and click.confirm('Relay all setup! Would you like to run it now?'): if IS_DOCKER:
click.echo("Relay all setup! Start the container to run the relay.")
return
if click.confirm('Relay all setup! Would you like to run it now?'):
cli_run.callback() # type: ignore cli_run.callback() # type: ignore
@ -184,28 +200,13 @@ def cli_setup(ctx: click.Context) -> None:
def cli_run(ctx: click.Context, dev: bool = False) -> None: def cli_run(ctx: click.Context, dev: bool = False) -> None:
'Run the relay' 'Run the relay'
if ctx.obj.config.domain.endswith('example.com') or not ctx.obj.signer: if ctx.obj.config.domain.endswith('example.com') or ctx.obj.signer is None:
click.echo( if not IS_DOCKER:
'Relay is not set up. Please edit your relay config or run "activityrelay setup".' click.echo('Relay is not set up. Please run "activityrelay setup".')
)
return
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...'
)
click.echo(pip_command)
return return
click.echo('Warning: PyCrypto is old and should be replaced with pycryptodome') cli_setup.callback() # type: ignore
click.echo(pip_command)
return return
ctx.obj['dev'] = dev ctx.obj['dev'] = dev