Compare commits

..

7 commits

Author SHA1 Message Date
Izalia Mae 189ac887a9 minor tweak to data used in admin pages 2024-03-11 03:24:44 -04:00
Izalia Mae 21e0e0a3ec add markdown support for the note setting 2024-03-11 03:17:38 -04:00
Izalia Mae 5b1f244703 actually use signed headers 2024-03-11 02:36:34 -04:00
Izalia Mae b3ab6e6d40 add message on home page when relay is in manual approval mode 2024-03-11 01:53:12 -04:00
Izalia Mae d5069d98a6 fix TypeError when a person tries joining 2024-03-11 01:52:28 -04:00
Izalia Mae c852867636 add option to require approval for new instances 2024-03-11 01:21:46 -04:00
Izalia Mae 1672f7a7eb add missing file 2024-03-10 19:15:59 -04:00
27 changed files with 527 additions and 120 deletions

View file

@ -111,6 +111,11 @@ class Application(web.Application):
self['signer'] = Signer(value, self.config.keyid)
@property
def template(self) -> Template:
return self['template']
@property
def uptime(self) -> timedelta:
if not self['start_time']:

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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.'),

View file

@ -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()

View file

@ -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")

View file

@ -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()

View file

@ -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")

View file

@ -16,7 +16,7 @@
%input(type="submit" value="Ban Domain")
#data-table.section
.data-table.section
%table
%thead
%tr

View file

@ -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") << &check;
%td.deny
%a(href="/admin/instances/deny/{{request.domain}}" title="Deny Request") << &#10006;
.data-table.section
.title << Instances
%table
%thead
%tr

View file

@ -16,7 +16,7 @@
%input(type="submit" value="Ban Software")
#data-table.section
.data-table.section
%table
%thead
%tr

View file

@ -19,7 +19,7 @@
%input(type="submit" value="Add User")
#data-table.section
.data-table.section
%table
%thead
%tr

View file

@ -10,7 +10,7 @@
%input(type="submit" value="Add Domain")
#data-table.section
.data-table.section
%table
%thead
%tr

View file

@ -2,9 +2,7 @@
-set page = "Home"
-block content
.section
-for line in config.note.splitlines()
-if line
%p -> =line
-markdown -> =config.note
.section
%p
@ -14,12 +12,17 @@
You may subscribe to this relay with the address:
%a(href="https://{{domain}}/actor") << https://{{domain}}/actor</a>
-if config["whitelist-enabled"]
-if config["approval-required"]
%p.section.message
Note: The whitelist is enabled on this instance. Ask the admin to add your instance
before joining.
Follow requests require approval. You will need to wait for an admin to accept or deny
your request.
#data-table.section
-elif config["whitelist-enabled"]
%p.section.message
The whitelist is enabled on this instance. Ask the admin to add your instance before
joining.
.data-table.section
%table
%thead
%tr

View file

@ -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;
}

View file

@ -0,0 +1,16 @@
:root {
--text: {{theme["text"]}};
--background: {{theme["background"]}};
--primary: {{theme["primary"]}};
--primary-hover: {{theme["primary-hover"]}};
--section-background: {{theme["section-background"]}};
--table-background: {{theme["table-background"]}};
--border: {{theme["border"]}};
--message-text: {{theme["message-text"]}};
--message-background: {{theme["message-background"]}};
--message-border: {{theme["message-border"]}};
--error-text: {{theme["error-text"]}};
--error-background: {{theme["error-background"]}};
--error-border: {{theme["error-border"]}};
--spacing: 10px;
}

View file

@ -116,7 +116,7 @@ class HttpClient:
headers = {}
if sign_headers:
self.signer.sign_headers('GET', url, algorithm = 'original')
headers = self.signer.sign_headers('GET', url, algorithm = 'original')
try:
logging.debug('Fetching resource: %s', url)
@ -130,7 +130,7 @@ class HttpClient:
if resp.status != 200:
logging.verbose('Received error when requesting %s: %i', url, resp.status)
logging.debug(await resp.read())
logging.debug(data)
return None
message = loads(data)

View file

@ -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'

View file

@ -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',

View file

@ -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(
@ -92,23 +97,53 @@ async def handle_follow(view: ActorView, conn: Connection) -> None:
actor = view.actor.id,
followid = view.message.id,
accept = False
)
),
view.instance
)
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 +224,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
)

View file

@ -1,15 +1,22 @@
from __future__ import annotations
import textwrap
import typing
from hamlish_jinja.extension import HamlishExtension
from collections.abc import Callable
from hamlish_jinja import HamlishExtension
from jinja2 import Environment, FileSystemLoader
from jinja2.ext import Extension
from jinja2.nodes import CallBlock
from markdown import Markdown
from . import __version__
from .database.config import THEMES
from .misc import get_resource
if typing.TYPE_CHECKING:
from jinja2.nodes import Node
from jinja2.parser import Parser
from typing import Any
from .application import Application
from .views.base import View
@ -22,7 +29,8 @@ class Template(Environment):
trim_blocks = True,
lstrip_blocks = True,
extensions = [
HamlishExtension
HamlishExtension,
MarkdownExtension
],
loader = FileSystemLoader([
get_resource('frontend'),
@ -36,8 +44,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,
@ -49,3 +57,36 @@ class Template(Environment):
}
return self.get_template(path).render(new_context)
class MarkdownExtension(Extension):
tags = {'markdown'}
extensions = {
'attr_list',
'smarty',
'tables'
}
def __init__(self, environment: Environment):
Extension.__init__(self, environment)
self._markdown = Markdown(extensions = MarkdownExtension.extensions)
environment.extend(
render_markdown = self._render_markdown
)
def parse(self, parser: Parser) -> Node | list[Node]:
lineno = next(parser.stream).lineno
body = parser.parse_statements(
['name:endmarkdown'],
drop_needle = True
)
output = CallBlock(self.call_method('_render_markdown'), [], [], body)
return output.set_lineno(lineno)
def _render_markdown(self, caller: Callable[[], str] | str) -> str:
text = caller() if isinstance(caller, Callable) else caller
return self._markdown.convert(textwrap.dedent(text.strip('\n')))

View file

@ -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 = self.app.template.render_markdown(config['note']),
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):

View file

@ -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:

View file

@ -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,
@ -197,7 +249,7 @@ class AdminWhitelist(View):
with self.database.session() as conn:
context = {
'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all())
'whitelist': tuple(conn.execute('SELECT * FROM whitelist ORDER BY domain ASC'))
}
if error:
@ -247,7 +299,7 @@ class AdminDomainBans(View):
with self.database.session() as conn:
context = {
'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC').all())
'bans': tuple(conn.execute('SELECT * FROM domain_bans ORDER BY domain ASC'))
}
if error:
@ -305,7 +357,7 @@ class AdminSoftwareBans(View):
with self.database.session() as conn:
context = {
'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC').all())
'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC'))
}
if error:
@ -363,7 +415,7 @@ class AdminUsers(View):
with self.database.session() as conn:
context = {
'users': tuple(conn.execute('SELECT * FROM users').all())
'users': tuple(conn.execute('SELECT * FROM users ORDER BY username ASC'))
}
if error:
@ -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)

View file

@ -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':

View file

@ -6,6 +6,7 @@ barkshark-sql@https://git.barkshark.xyz/barkshark/bsql/archive/0.1.2.tar.gz
click>=8.1.2
hamlish-jinja@https://git.barkshark.xyz/barkshark/hamlish-jinja/archive/0.3.5.tar.gz
hiredis==2.3.2
markdown==3.5.2
platformdirs==4.2.0
pyyaml>=6.0
redis==5.0.1