From c85286763679b3122cc96adc5447a33bea239488 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 11 Mar 2024 01:21:46 -0400 Subject: [PATCH] add option to require approval for new instances --- relay/data/statements.sql | 15 +++- relay/data/swagger.yaml | 47 +++++++++++ relay/database/__init__.py | 1 + relay/database/config.py | 3 +- relay/database/connection.py | 87 ++++++++++++-------- relay/database/schema.py | 7 ++ relay/frontend/functions.haml | 16 ++++ relay/frontend/page/admin-config.haml | 24 ++---- relay/frontend/page/admin-domain_bans.haml | 2 +- relay/frontend/page/admin-instances.haml | 33 +++++++- relay/frontend/page/admin-software_bans.haml | 2 +- relay/frontend/page/admin-users.haml | 2 +- relay/frontend/page/admin-whitelist.haml | 2 +- relay/frontend/page/home.haml | 2 +- relay/frontend/style.css | 29 +++++-- relay/manage.py | 78 +++++++++++++++++- relay/misc.py | 4 +- relay/processors.py | 62 ++++++++++---- relay/template.py | 7 +- relay/views/activitypub.py | 13 ++- relay/views/api.py | 46 ++++++++++- relay/views/frontend.py | 62 ++++++++++++-- relay/views/misc.py | 7 +- 23 files changed, 445 insertions(+), 106 deletions(-) create mode 100644 relay/frontend/functions.haml diff --git a/relay/data/statements.sql b/relay/data/statements.sql index bc06d25..f06d4b5 100644 --- a/relay/data/statements.sql +++ b/relay/data/statements.sql @@ -23,17 +23,26 @@ SELECT * FROM inboxes WHERE domain = :value or inbox = :value or actor = :value; -- name: put-inbox -INSERT INTO inboxes (domain, actor, inbox, followid, software, created) -VALUES (:domain, :actor, :inbox, :followid, :software, :created) -ON CONFLICT (domain) DO UPDATE SET followid = :followid +INSERT INTO inboxes (domain, actor, inbox, followid, software, accepted, created) +VALUES (:domain, :actor, :inbox, :followid, :software, :accepted, :created) +ON CONFLICT (domain) DO +UPDATE SET followid = :followid, inbox = :inbox, software = :software, created = :created RETURNING *; +-- name: put-inbox-accept +UPDATE inboxes SET accepted = :accepted WHERE domain = :domain RETURNING *; + + -- name: del-inbox DELETE FROM inboxes WHERE domain = :value or inbox = :value or actor = :value; +-- name: get-request +SELECT * FROM inboxes WHERE accepted = 0 and domain = :domain; + + -- name: get-user SELECT * FROM users WHERE username = :value or handle = :value; diff --git a/relay/data/swagger.yaml b/relay/data/swagger.yaml index 9c313ae..b4136ec 100644 --- a/relay/data/swagger.yaml +++ b/relay/data/swagger.yaml @@ -285,6 +285,50 @@ paths: schema: $ref: "#/definitions/Error" + /v1/request: + get: + tags: + - Follow Request + description: Get the list of follow requests + produces: + - application/json + responses: + "200": + description: List of instances + schema: + type: array + items: + $ref: "#/definitions/Instance" + + post: + tags: + - Follow Request + description: Approve or deny a follow request + parameters: + - in: formData + name: domain + required: true + type: string + - in: formData + name: accept + required: true + type: boolean + consumes: + - application/json + - multipart/form-data + - application/x-www-form-urlencoded + produces: + - application/json + responses: + "200": + description: Follow request successfully accepted or denied + schema: + $ref: "#/definitions/Message" + "500": + description: Follow request does not exist + schema: + $ref: "#/definitions/Error" + /v1/domain_ban: get: tags: @@ -672,6 +716,9 @@ definitions: software: description: Nodeinfo-formatted name of the instance's software type: string + accepted: + description: Whether or not the follow request has been accepted + type: boolean created: description: Date the instance joined or was added type: string diff --git a/relay/database/__init__.py b/relay/database/__init__.py index d248713..02b0bc9 100644 --- a/relay/database/__init__.py +++ b/relay/database/__init__.py @@ -53,6 +53,7 @@ def get_database(config: Config, migrate: bool = True) -> bsql.Database: if schema_ver < ver: func(conn) conn.put_config('schema-version', ver) + logging.info("Updated database to %i", ver) if (privkey := conn.get_config('private-key')): conn.app.signer = privkey diff --git a/relay/database/config.py b/relay/database/config.py index 82e2e69..7d2abd4 100644 --- a/relay/database/config.py +++ b/relay/database/config.py @@ -60,8 +60,9 @@ THEMES = { } CONFIG_DEFAULTS: dict[str, tuple[str, Any]] = { - 'schema-version': ('int', 20240206), + 'schema-version': ('int', 20240310), 'private-key': ('str', None), + 'approval-required': ('bool', False), 'log-level': ('loglevel', logging.LogLevel.INFO), 'name': ('str', 'ActivityRelay'), 'note': ('str', 'Make a note about your instance here.'), diff --git a/relay/database/connection.py b/relay/database/connection.py index 2792111..ca98e6a 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -52,7 +52,7 @@ class Connection(SqlConnection): urlparse(message.object_id).netloc } - for inbox in self.execute('SELECT * FROM inboxes'): + for inbox in self.get_inboxes(): if inbox['domain'] not in src_domains: yield inbox['inbox'] @@ -124,53 +124,45 @@ class Connection(SqlConnection): return cur.one() + def get_inboxes(self) -> tuple[Row]: + with self.execute("SELECT * FROM inboxes WHERE accepted = 1") as cur: + return tuple(cur.all()) + + def put_inbox(self, domain: str, - inbox: str, + inbox: str | None = None, actor: str | None = None, followid: str | None = None, - software: str | None = None) -> Row: + software: str | None = None, + accepted: bool = True) -> Row: params = { - 'domain': domain, 'inbox': inbox, 'actor': actor, 'followid': followid, 'software': software, - 'created': datetime.now(tz = timezone.utc) + 'accepted': accepted } - with self.run('put-inbox', params) as cur: + if not self.get_inbox(domain): + if not inbox: + raise ValueError("Missing inbox") + + params['domain'] = domain + params['created'] = datetime.now(tz = timezone.utc) + + with self.run('put-inbox', params) as cur: + return cur.one() + + for key, value in tuple(params.items()): + if value is None: + del params[key] + + with self.update('inboxes', params, domain = domain) as cur: return cur.one() - def update_inbox(self, - inbox: str, - actor: str | None = None, - followid: str | None = None, - software: str | None = None) -> Row: - - if not (actor or followid or software): - raise ValueError('Missing "actor", "followid", and/or "software"') - - data = {} - - if actor: - data['actor'] = actor - - if followid: - data['followid'] = followid - - if software: - data['software'] = software - - statement = Update('inboxes', data) - statement.set_where("inbox", inbox) - - with self.query(statement): - return self.get_inbox(inbox) - - def del_inbox(self, value: str) -> bool: with self.run('del-inbox', {'value': value}) as cur: if cur.row_count > 1: @@ -179,6 +171,35 @@ class Connection(SqlConnection): return cur.row_count == 1 + def get_request(self, domain: str) -> Row: + with self.run('get-request', {'domain': domain}) as cur: + if not (row := cur.one()): + raise KeyError(domain) + + return row + + + def get_requests(self) -> tuple[Row]: + with self.execute('SELECT * FROM inboxes WHERE accepted = 0') as cur: + return tuple(cur.all()) + + + def put_request_response(self, domain: str, accepted: bool) -> Row: + instance = self.get_request(domain) + + if not accepted: + self.del_inbox(domain) + return instance + + params = { + 'domain': domain, + 'accepted': accepted + } + + with self.run('put-inbox-accept', params) as cur: + return cur.one() + + def get_user(self, value: str) -> Row: with self.run('get-user', {'value': value}) as cur: return cur.one() diff --git a/relay/database/schema.py b/relay/database/schema.py index e3a0303..d965348 100644 --- a/relay/database/schema.py +++ b/relay/database/schema.py @@ -25,6 +25,7 @@ TABLES: Tables = Tables( Column('inbox', 'text', unique = True, nullable = False), Column('followid', 'text'), Column('software', 'text'), + Column('accepted', 'boolean'), Column('created', 'timestamp', nullable = False) ), Table( @@ -76,3 +77,9 @@ def migrate_0(conn: Connection) -> None: @migration def migrate_20240206(conn: Connection) -> None: conn.create_tables() + + +@migration +def migrate_20240310(conn: Connection) -> None: + conn.execute("ALTER TABLE inboxes ADD COLUMN accepted BOOLEAN") + conn.execute("UPDATE inboxes SET accepted = 1") diff --git a/relay/frontend/functions.haml b/relay/frontend/functions.haml new file mode 100644 index 0000000..68fbd2c --- /dev/null +++ b/relay/frontend/functions.haml @@ -0,0 +1,16 @@ +-macro new_checkbox(name, checked) + -if checked + %input(id="{{name}}" name="{{name}}" type="checkbox" checked) + + -else + %input(id="{{name}}" name="{{name}}" type="checkbox") + + +-macro new_select(name, selected, items) + %select(id="{{name}}" name="{{name}}") + -for item in items + -if item == selected + %option(value="{{item}}" selected) -> =item.title() + + -else + %option(value="{{item}}") -> =item.title() diff --git a/relay/frontend/page/admin-config.haml b/relay/frontend/page/admin-config.haml index 4028eb1..ef77d3f 100644 --- a/relay/frontend/page/admin-config.haml +++ b/relay/frontend/page/admin-config.haml @@ -1,5 +1,6 @@ -extends "base.haml" -set page="Config" +-import "functions.haml" as func -block content %form.section(action="/admin/config" method="POST") .grid-2col @@ -10,28 +11,15 @@ %textarea(id="description" name="note" value="{{config.note}}") << {{config.note}} %label(for="theme") << Color Theme - %select(id="theme" name="theme") - -for theme in themes - -if theme == config.theme - %option(value="{{theme}}" selected) -> =theme.title() - - -else - %option(value="{{theme}}") -> =theme.title() + =func.new_select("theme", config.theme, themes) %label(for="log-level") << Log Level - %select(id="log-level" name="log-level") - -for level in LogLevel - -if level == config["log-level"] - %option(value="{{level.name}}" selected) -> =level.name.title() - - -else - %option(value="{{level.name}}") -> =level.name.title() + =func.new_select("log-level", config["log-level"].name, levels) %label(for="whitelist-enabled") << Whitelist - -if config["whitelist-enabled"] - %input(id="whitelist-enabled" name="whitelist-enabled" type="checkbox" checked) + =func.new_checkbox("whitelist-enabled", config["whitelist-enabled"]) - -else - %input(id="whitelist-enabled" name="whitelist-enabled" type="checkbox") + %label(for="approval-required") << Approval Required + =func.new_checkbox("approval-required", config["approval-required"]) %input(type="submit" value="Save") diff --git a/relay/frontend/page/admin-domain_bans.haml b/relay/frontend/page/admin-domain_bans.haml index fbee683..f874499 100644 --- a/relay/frontend/page/admin-domain_bans.haml +++ b/relay/frontend/page/admin-domain_bans.haml @@ -16,7 +16,7 @@ %input(type="submit" value="Ban Domain") - #data-table.section + .data-table.section %table %thead %tr diff --git a/relay/frontend/page/admin-instances.haml b/relay/frontend/page/admin-instances.haml index 106e31d..770ccc1 100644 --- a/relay/frontend/page/admin-instances.haml +++ b/relay/frontend/page/admin-instances.haml @@ -19,7 +19,38 @@ %input(type="submit" value="Add Instance") - #data-table.section + -if requests + .data-table.section + .title << Requests + %table + %thead + %tr + %td.instance << Instance + %td.software << Software + %td.date << Joined + %td.approve + %td.deny + + %tbody + -for request in requests + %tr + %td.instance + %a(href="https://{{request.domain}}" target="_new") -> =request.domain + + %td.software + =request.software or "n/a" + + %td.date + =request.created.strftime("%Y-%m-%d") + + %td.approve + %a(href="/admin/instances/approve/{{request.domain}}" title="Approve Request") << ✓ + + %td.deny + %a(href="/admin/instances/deny/{{request.domain}}" title="Deny Request") << ✖ + + .data-table.section + .title << Instances %table %thead %tr diff --git a/relay/frontend/page/admin-software_bans.haml b/relay/frontend/page/admin-software_bans.haml index 9490405..7ac9d07 100644 --- a/relay/frontend/page/admin-software_bans.haml +++ b/relay/frontend/page/admin-software_bans.haml @@ -16,7 +16,7 @@ %input(type="submit" value="Ban Software") - #data-table.section + .data-table.section %table %thead %tr diff --git a/relay/frontend/page/admin-users.haml b/relay/frontend/page/admin-users.haml index 65c268e..a87c0db 100644 --- a/relay/frontend/page/admin-users.haml +++ b/relay/frontend/page/admin-users.haml @@ -19,7 +19,7 @@ %input(type="submit" value="Add User") - #data-table.section + .data-table.section %table %thead %tr diff --git a/relay/frontend/page/admin-whitelist.haml b/relay/frontend/page/admin-whitelist.haml index b294552..9126297 100644 --- a/relay/frontend/page/admin-whitelist.haml +++ b/relay/frontend/page/admin-whitelist.haml @@ -10,7 +10,7 @@ %input(type="submit" value="Add Domain") - #data-table.section + .data-table.section %table %thead %tr diff --git a/relay/frontend/page/home.haml b/relay/frontend/page/home.haml index 7f09644..527f33a 100644 --- a/relay/frontend/page/home.haml +++ b/relay/frontend/page/home.haml @@ -19,7 +19,7 @@ Note: The whitelist is enabled on this instance. Ask the admin to add your instance before joining. - #data-table.section + .data-table.section %table %thead %tr diff --git a/relay/frontend/style.css b/relay/frontend/style.css index f2a6fe1..4104519 100644 --- a/relay/frontend/style.css +++ b/relay/frontend/style.css @@ -91,6 +91,17 @@ textarea { margin: 0px auto; } +#content .title { + font-size: 24px; + text-align: center; + font-weight: bold; + margin-bottom: 10px; +} + +#content .title:not(:first-child) { + margin-top: 10px; +} + #header { display: grid; grid-template-columns: 50px auto 50px; @@ -193,15 +204,6 @@ textarea { align-items: center; } -#data-table td:first-child { - width: 100%; -} - -#data-table .date { - width: max-content; - text-align: right; -} - .button { background-color: var(--primary); border: 1px solid var(--primary); @@ -220,6 +222,15 @@ textarea { grid-template-columns: max-content auto; } +.data-table td:first-child { + width: 100%; +} + +.data-table .date { + width: max-content; + text-align: right; +} + .error, .message { text-align: center; } diff --git a/relay/manage.py b/relay/manage.py index 796ec0b..cb95748 100644 --- a/relay/manage.py +++ b/relay/manage.py @@ -342,7 +342,7 @@ def cli_config_list(ctx: click.Context) -> None: for key, value in conn.get_config_all().items(): if key not in CONFIG_IGNORE: key = f'{key}:'.ljust(20) - click.echo(f'- {key} {value}') + click.echo(f'- {key} {repr(value)}') @cli_config.command('set') @@ -477,7 +477,7 @@ def cli_inbox_list(ctx: click.Context) -> None: click.echo('Connected to the following instances or relays:') with ctx.obj.database.session() as conn: - for inbox in conn.execute('SELECT * FROM inboxes'): + for inbox in conn.get_inboxes(): click.echo(f'- {inbox["inbox"]}') @@ -618,6 +618,80 @@ def cli_inbox_remove(ctx: click.Context, inbox: str) -> None: click.echo(f'Removed inbox from the database: {inbox}') +@cli.group('request') +def cli_request() -> None: + 'Manage follow requests' + + +@cli_request.command('list') +@click.pass_context +def cli_request_list(ctx: click.Context) -> None: + 'List all current follow requests' + + click.echo('Follow requests:') + + with ctx.obj.database.session() as conn: + for instance in conn.get_requests(): + date = instance['created'].strftime('%Y-%m-%d') + click.echo(f'- [{date}] {instance["domain"]}') + + +@cli_request.command('accept') +@click.argument('domain') +@click.pass_context +def cli_request_accept(ctx: click.Context, domain: str) -> None: + 'Accept a follow request' + + try: + with ctx.obj.database.session() as conn: + instance = conn.put_request_response(domain, True) + + except KeyError: + click.echo('Request not found') + return + + message = Message.new_response( + host = ctx.obj.config.domain, + actor = instance['actor'], + followid = instance['followid'], + accept = True + ) + + asyncio.run(http.post(instance['inbox'], message, instance)) + + if instance['software'] != 'mastodon': + message = Message.new_follow( + host = ctx.obj.config.domain, + actor = instance['actor'] + ) + + asyncio.run(http.post(instance['inbox'], message, instance)) + + +@cli_request.command('deny') +@click.argument('domain') +@click.pass_context +def cli_request_deny(ctx: click.Context, domain: str) -> None: + 'Accept a follow request' + + try: + with ctx.obj.database.session() as conn: + instance = conn.put_request_response(domain, False) + + except KeyError: + click.echo('Request not found') + return + + response = Message.new_response( + host = ctx.obj.config.domain, + actor = instance['actor'], + followid = instance['followid'], + accept = False + ) + + asyncio.run(http.post(instance['inbox'], response, instance)) + + @cli.group('instance') def cli_instance() -> None: 'Manage instance bans' diff --git a/relay/misc.py b/relay/misc.py index 33e7a06..9fe4b83 100644 --- a/relay/misc.py +++ b/relay/misc.py @@ -117,7 +117,8 @@ class Message(aputils.Message): def new_actor(cls: type[Message], # pylint: disable=arguments-differ host: str, pubkey: str, - description: str | None = None) -> Message: + description: str | None = None, + approves: bool = False) -> Message: return cls({ '@context': 'https://www.w3.org/ns/activitystreams', @@ -126,6 +127,7 @@ class Message(aputils.Message): 'preferredUsername': 'relay', 'name': 'ActivityRelay', 'summary': description or 'ActivityRelay bot', + 'manuallyApprovesFollowers': approves, 'followers': f'https://{host}/followers', 'following': f'https://{host}/following', 'inbox': f'https://{host}/inbox', diff --git a/relay/processors.py b/relay/processors.py index 824a975..04aa4a5 100644 --- a/relay/processors.py +++ b/relay/processors.py @@ -62,9 +62,12 @@ async def handle_forward(view: ActorView, conn: Connection) -> None: async def handle_follow(view: ActorView, conn: Connection) -> None: nodeinfo = await view.client.fetch_nodeinfo(view.actor.domain) software = nodeinfo.sw_name if nodeinfo else None + config = conn.get_config_all() # reject if software used by actor is banned if conn.get_software_ban(software): + logging.verbose('Rejected banned actor: %s', view.actor.id) + view.app.push_message( view.actor.shared_inbox, Message.new_response( @@ -85,6 +88,8 @@ async def handle_follow(view: ActorView, conn: Connection) -> None: ## reject if the actor is not an instance actor if person_check(view.actor, software): + logging.verbose('Non-application actor tried to follow: %s', view.actor.id) + view.app.push_message( view.actor.shared_inbox, Message.new_response( @@ -95,20 +100,49 @@ async def handle_follow(view: ActorView, conn: Connection) -> None: ) ) - logging.verbose('Non-application actor tried to follow: %s', view.actor.id) return - with conn.transaction(): - if conn.get_inbox(view.actor.shared_inbox): - view.instance = conn.update_inbox(view.actor.shared_inbox, followid = view.message.id) + if not conn.get_domain_whitelist(view.actor.domain): + # add request if approval-required is enabled + if config['approval-required']: + logging.verbose('New follow request fromm actor: %s', view.actor.id) - else: - view.instance = conn.put_inbox( - view.actor.domain, + with conn.transaction(): + view.instance = conn.put_inbox( + domain = view.actor.domain, + inbox = view.actor.shared_inbox, + actor = view.actor.id, + followid = view.message.id, + software = software, + accepted = False + ) + + return + + # reject if the actor isn't whitelisted while the whiltelist is enabled + if config['whitelist-enabled']: + logging.verbose('Rejected actor for not being in the whitelist: %s', view.actor.id) + + view.app.push_message( view.actor.shared_inbox, - view.actor.id, - view.message.id, - software + Message.new_response( + host = view.config.domain, + actor = view.actor.id, + followid = view.message.id, + accept = False + ) + ) + + return + + with conn.transaction(): + view.instance = conn.put_inbox( + domain = view.actor.domain, + inbox = view.actor.shared_inbox, + actor = view.actor.id, + followid = view.message.id, + software = software, + accepted = True ) view.app.push_message( @@ -189,15 +223,15 @@ async def run_processor(view: ActorView) -> None: if not view.instance['software']: if (nodeinfo := await view.client.fetch_nodeinfo(view.instance['domain'])): with conn.transaction(): - view.instance = conn.update_inbox( - view.instance['inbox'], + view.instance = conn.put_inbox( + domain = view.instance['domain'], software = nodeinfo.sw_name ) if not view.instance['actor']: with conn.transaction(): - view.instance = conn.update_inbox( - view.instance['inbox'], + view.instance = conn.put_inbox( + domain = view.instance['domain'], actor = view.actor.id ) diff --git a/relay/template.py b/relay/template.py index 64738e0..4699d01 100644 --- a/relay/template.py +++ b/relay/template.py @@ -2,11 +2,10 @@ from __future__ import annotations import typing -from hamlish_jinja.extension import HamlishExtension +from hamlish_jinja import HamlishExtension from jinja2 import Environment, FileSystemLoader from . import __version__ -from .database.config import THEMES from .misc import get_resource if typing.TYPE_CHECKING: @@ -36,8 +35,8 @@ class Template(Environment): def render(self, path: str, view: View | None = None, **context: Any) -> str: - with self.app.database.session(False) as s: - config = s.get_config_all() + with self.app.database.session(False) as conn: + config = conn.get_config_all() new_context = { 'view': view, diff --git a/relay/views/activitypub.py b/relay/views/activitypub.py index 31266f6..df3085f 100644 --- a/relay/views/activitypub.py +++ b/relay/views/activitypub.py @@ -30,9 +30,14 @@ class ActorView(View): async def get(self, request: Request) -> Response: + with self.database.session(False) as conn: + config = conn.get_config_all() + data = Message.new_actor( host = self.config.domain, - pubkey = self.app.signer.pubkey + pubkey = self.app.signer.pubkey, + description = ''.join(f"

{line}

" for line in config['note'].splitlines()), + approves = config['approval-required'] ) return Response.new(data, ctype='activity') @@ -44,12 +49,6 @@ class ActorView(View): with self.database.session() as conn: self.instance = conn.get_inbox(self.actor.shared_inbox) - config = conn.get_config_all() - - ## reject if the actor isn't whitelisted while the whiltelist is enabled - if config['whitelist-enabled'] and not conn.get_domain_whitelist(self.actor.domain): - logging.verbose('Rejected actor for not being in the whitelist: %s', self.actor.id) - return Response.new_error(403, 'access denied', 'json') ## reject if actor is banned if conn.get_domain_ban(self.actor.domain): diff --git a/relay/views/api.py b/relay/views/api.py index 5a32cac..5a12e95 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -212,7 +212,7 @@ class Inbox(View): if not (instance := conn.get_inbox(data['domain'])): return Response.new_error(404, 'Instance with domain not found', 'json') - instance = conn.update_inbox(instance['inbox'], **data) + instance = conn.put_inbox(instance['domain'], **data) return Response.new(instance, ctype = 'json') @@ -232,6 +232,50 @@ class Inbox(View): return Response.new({'message': 'Deleted instance'}, ctype = 'json') +@register_route('/api/v1/request') +class RequestView(View): + async def get(self, request: Request) -> Response: + with self.database.session() as conn: + instances = conn.get_requests() + + return Response.new(instances, ctype = 'json') + + + async def post(self, request: Request) -> Response: + data = await self.get_api_data(['domain', 'accept'], []) + + if not isinstance(data['accept'], bool): + atype = type(data['accept']).__name__ + return Response.new_error(400, f'Invalid type for "accept": {atype}', 'json') + + try: + with self.database.session(True) as conn: + instance = conn.put_request_response(data['domain'], data['accept']) + + except KeyError: + return Response.new_error(404, 'Request not found', 'json') + + message = Message.new_response( + host = self.config.domain, + actor = instance['actor'], + followid = instance['followid'], + accept = data['accept'] + ) + + self.app.push_message(instance['inbox'], message, instance) + + if data['accept'] and instance['software'] != 'mastodon': + message = Message.new_follow( + host = self.config.domain, + actor = instance['actor'] + ) + + self.app.push_message(instance['inbox'], message, instance) + + resp_message = {'message': 'Request accepted' if data['accept'] else 'Request denied'} + return Response.new(resp_message, ctype = 'json') + + @register_route('/api/v1/domain_ban') class DomainBan(View): async def get(self, request: Request) -> Response: diff --git a/relay/views/frontend.py b/relay/views/frontend.py index bd63417..5367de5 100644 --- a/relay/views/frontend.py +++ b/relay/views/frontend.py @@ -56,7 +56,7 @@ class HomeView(View): async def get(self, request: Request) -> Response: with self.database.session() as conn: context = { - 'instances': tuple(conn.execute('SELECT * FROM inboxes').all()) + 'instances': tuple(conn.get_inboxes()) } data = self.template.render('page/home.haml', self, **context) @@ -137,7 +137,8 @@ class AdminInstances(View): with self.database.session() as conn: context = { - 'instances': tuple(conn.execute('SELECT * FROM inboxes').all()) + 'instances': tuple(conn.get_inboxes()), + 'requests': tuple(conn.get_requests()) } if error: @@ -179,15 +180,66 @@ class AdminInstances(View): @register_route('/admin/instances/delete/{domain}') class AdminInstancesDelete(View): async def get(self, request: Request, domain: str) -> Response: - with self.database.session() as conn: + with self.database.session(True) as conn: if not conn.get_inbox(domain): - return await AdminInstances(request).get(request, message = 'Instance not found') + return await AdminInstances(request).get(request, error = 'Instance not found') conn.del_inbox(domain) return await AdminInstances(request).get(request, message = 'Removed instance') +@register_route('/admin/instances/approve/{domain}') +class AdminInstancesApprove(View): + async def get(self, request: Request, domain: str) -> Response: + try: + with self.database.session(True) as conn: + instance = conn.put_request_response(domain, True) + + except KeyError: + return await AdminInstances(request).get(request, error = 'Instance not found') + + message = Message.new_response( + host = self.config.domain, + actor = instance['actor'], + followid = instance['followid'], + accept = True + ) + + self.app.push_message(instance['inbox'], message, instance) + + if instance['software'] != 'mastodon': + message = Message.new_follow( + host = self.config.domain, + actor = instance['actor'] + ) + + self.app.push_message(instance['inbox'], message, instance) + + return await AdminInstances(request).get(request, message = 'Request accepted') + + +@register_route('/admin/instances/deny/{domain}') +class AdminInstancesDeny(View): + async def get(self, request: Request, domain: str) -> Response: + try: + with self.database.session(True) as conn: + instance = conn.put_request_response(domain, False) + + except KeyError: + return await AdminInstances(request).get(request, error = 'Instance not found') + + message = Message.new_response( + host = self.config.domain, + actor = instance['actor'], + followid = instance['followid'], + accept = False + ) + + self.app.push_message(instance['inbox'], message, instance) + return await AdminInstances(request).get(request, message = 'Request denied') + + @register_route('/admin/whitelist') class AdminWhitelist(View): async def get(self, @@ -412,7 +464,7 @@ class AdminConfig(View): async def get(self, request: Request, message: str | None = None) -> Response: context = { 'themes': tuple(THEMES.keys()), - 'LogLevel': LogLevel, + 'levels': tuple(level.name for level in LogLevel), 'message': message } data = self.template.render('page/admin-config.haml', self, **context) diff --git a/relay/views/misc.py b/relay/views/misc.py index 65025e3..ff4a6a4 100644 --- a/relay/views/misc.py +++ b/relay/views/misc.py @@ -34,7 +34,7 @@ class NodeinfoView(View): # pylint: disable=no-self-use async def get(self, request: Request, niversion: str) -> Response: with self.database.session() as conn: - inboxes = conn.execute('SELECT * FROM inboxes').all() + inboxes = conn.get_inboxes() data = { 'name': 'activityrelay', @@ -42,7 +42,10 @@ class NodeinfoView(View): 'protocols': ['activitypub'], 'open_regs': not conn.get_config('whitelist-enabled'), 'users': 1, - 'metadata': {'peers': [inbox['domain'] for inbox in inboxes]} + 'metadata': { + 'approval_required': conn.get_config('approval-required'), + 'peers': [inbox['domain'] for inbox in inboxes] + } } if niversion == '2.1':