Compare commits

..

No commits in common. "a0ee22406bdfe9a604ac74d0f6732e03f30437e2" and "2a866eaaaa2413f6f93e2a6851d5f4e65f189b50" have entirely different histories.

27 changed files with 291 additions and 443 deletions

5
MANIFEST.in Normal file
View file

@ -0,0 +1,5 @@
include frontend/base.haml
include frontend/style.css
include data/statements.sql
include data/swagger.yaml
include frontend/page/home.haml

56
relay.spec Normal file
View file

@ -0,0 +1,56 @@
# -*- mode: python ; coding: utf-8 -*-
import importlib
from pathlib import Path
block_cipher = None
aiohttp_swagger_path = Path(importlib.import_module('aiohttp_swagger').__file__).parent
a = Analysis(
['relay/__main__.py'],
pathex=[],
binaries=[],
datas=[
('relay/data', 'relay/data'),
('relay/frontend', 'relay/frontend'),
(aiohttp_swagger_path, 'aiohttp_swagger')
],
hiddenimports=[
'pg8000',
'sqlite3'
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='activityrelay',
icon=None,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View file

@ -1,8 +1,5 @@
import multiprocessing
from relay.manage import main from relay.manage import main
if __name__ == '__main__': if __name__ == '__main__':
multiprocessing.freeze_support()
main() main()

View file

@ -26,10 +26,9 @@ from .views.api import handle_api_path
from .views.frontend import handle_frontend_path from .views.frontend import handle_frontend_path
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from collections.abc import Coroutine
from tinysql import Database, Row from tinysql import Database, Row
from .cache import Cache from .cache import Cache
from .misc import Message, Response from .misc import Message
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
@ -37,12 +36,11 @@ if typing.TYPE_CHECKING:
class Application(web.Application): class Application(web.Application):
DEFAULT: Application = None DEFAULT: Application = None
def __init__(self, cfgpath: str | None, dev: bool = False): def __init__(self, cfgpath: str | None):
web.Application.__init__(self, web.Application.__init__(self,
middlewares = [ middlewares = [
handle_api_path, handle_api_path,
handle_frontend_path, handle_frontend_path
handle_response_headers
] ]
) )
@ -52,7 +50,6 @@ class Application(web.Application):
self['signer'] = None self['signer'] = None
self['start_time'] = None self['start_time'] = None
self['cleanup_thread'] = None self['cleanup_thread'] = None
self['dev'] = dev
self['config'] = Config(cfgpath, load = True) self['config'] = Config(cfgpath, load = True)
self['database'] = get_database(self.config) self['database'] = get_database(self.config)
@ -258,18 +255,25 @@ class PushWorker(multiprocessing.Process):
await client.close() await client.close()
@web.middleware
async def handle_response_headers(request: web.Request, handler: Coroutine) -> Response:
resp = await handler(request)
resp.headers['Server'] = 'ActivityRelay'
# if not request.app['dev'] and request.path.endswith(('.css', '.js')): async def handle_access_log(request: web.Request, response: web.Response) -> None:
# resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable' address = request.headers.get(
'X-Forwarded-For',
request.headers.get(
'X-Real-Ip',
request.remote
)
)
# else: logging.info(
# resp.headers['Cache-Control'] = 'no-store' '%s "%s %s" %i %i "%s"',
address,
return resp request.method,
request.path,
response.status,
response.content_length or 0,
request.headers.get('User-Agent', 'n/a')
)
async def handle_cleanup(app: Application) -> None: async def handle_cleanup(app: Application) -> None:

View file

@ -52,11 +52,7 @@ if IS_DOCKER:
class Config: class Config:
def __init__(self, path: str, load: bool = False): def __init__(self, path: str, load: bool = False):
if path: self.path = Config.get_config_dir()
self.path = Path(path).expanduser().resolve()
else:
self.path = Config.get_config_dir()
self.listen = None self.listen = None
self.port = None self.port = None

View file

@ -1,14 +1,10 @@
import click import click
import platform
import subprocess import subprocess
import sys import sys
import time import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory
from . import __version__
try: try:
from watchdog.observers import Observer from watchdog.observers import Observer
@ -50,33 +46,25 @@ def cli_lint(path):
subprocess.run([sys.executable, '-m', 'pylint', path], check = False) subprocess.run([sys.executable, '-m', 'pylint', path], check = False)
@cli.command('manifest-gen')
def cli_manifest_install():
paths = []
for path in SCRIPT.rglob('*'):
if path.suffix.lower() in IGNORE_EXT or not path.is_file():
continue
paths.append(path)
with REPO.joinpath('MANIFEST.in').open('w', encoding = 'utf-8') as fd:
for path in paths:
fd.write(f'include {str(path.relative_to(SCRIPT))}\n')
@cli.command('build') @cli.command('build')
def cli_build(): def cli_build():
with TemporaryDirectory() as tmp: cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec']
arch = 'amd64' if sys.maxsize >= 2**32 else 'i386' subprocess.run(cmd, check = False)
cmd = [
sys.executable, '-m', 'PyInstaller',
'--collect-data', 'relay',
'--collect-data', 'aiohttp_swagger',
'--hidden-import', 'pg8000',
'--hidden-import', 'sqlite3',
'--name', f'activityrelay-{__version__}-{platform.system().lower()}-{arch}',
'--workpath', tmp,
'--onefile', 'relay/__main__.py',
]
if platform.system() == 'Windows':
cmd.append('--console')
# putting the spec path on a different drive than the source dir breaks
if str(SCRIPT)[0] == tmp[0]:
cmd.extend(['--specpath', tmp])
else:
cmd.append('--strip')
cmd.extend(['--specpath', tmp])
subprocess.run(cmd, check = False)
@cli.command('run') @cli.command('run')
@ -106,7 +94,7 @@ def cli_run():
class WatchHandler(PatternMatchingEventHandler): class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py'] patterns = ['*.py']
cmd = [sys.executable, '-m', 'relay', 'run', '-d'] cmd = [sys.executable, '-m', 'relay', 'run']
def __init__(self): def __init__(self):
@ -157,6 +145,7 @@ class WatchHandler(PatternMatchingEventHandler):
if event.event_type not in ['modified', 'created', 'deleted']: if event.event_type not in ['modified', 'created', 'deleted']:
return return
print(event.src_path)
self.run_proc(restart = True) self.run_proc(restart = True)

View file

@ -11,7 +11,7 @@
%title << {{config.name}}: {{page}} %title << {{config.name}}: {{page}}
%meta(charset="UTF-8") %meta(charset="UTF-8")
%meta(name="viewport" content="width=device-width, initial-scale=1") %meta(name="viewport" content="width=device-width, initial-scale=1")
%link(rel="stylesheet" type="text/css" href="/style.css") %link(rel="stylesheet" type="text/css" href="/style.css?page={{page}}")
-block head -block head
%body %body
@ -27,7 +27,6 @@
{{menu_item("Whitelist", "/admin/whitelist")}} {{menu_item("Whitelist", "/admin/whitelist")}}
{{menu_item("Domain Bans", "/admin/domain_bans")}} {{menu_item("Domain Bans", "/admin/domain_bans")}}
{{menu_item("Software Bans", "/admin/software_bans")}} {{menu_item("Software Bans", "/admin/software_bans")}}
{{menu_item("Users", "/admin/users")}}
{{menu_item("Config", "/admin/config")}} {{menu_item("Config", "/admin/config")}}
{{menu_item("Logout", "/logout")}} {{menu_item("Logout", "/logout")}}
@ -36,13 +35,8 @@
#container #container
#header.section #header.section
%span#menu-open << &#8286; %span#menu-open.button << &#8286;
%span.title-container %a(href="https://{{domain}}/") -> =config.name
%a.title(href="/") -> =config.name
-if view.request.path not in ["/", "/login"]
.page -> =page
.empty .empty
-if error -if error
@ -51,7 +45,7 @@
-if message -if message
.message.section -> =message .message.section -> =message
#content(class="page-{{page.lower().replace(' ', '_')}}") #content
-block content -block content
#footer.section #footer.section

View file

@ -2,7 +2,7 @@
-set page="Config" -set page="Config"
-block content -block content
%form.section(action="/admin/config" method="POST") %form.section(action="/admin/config" method="POST")
.grid-2col #config-options
%label(for="name") << Name %label(for="name") << Name
%input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}") %input(id = "name" name="name" placeholder="Relay Name" value="{{config.name or ''}}")

View file

@ -3,8 +3,8 @@
-block content -block content
%details.section %details.section
%summary << Ban Domain %summary << Ban Domain
%form(action="/admin/domain_bans" method="POST") %form(action="/admin/domain_bans", method="POST")
#add-item #add-domain
%label(for="domain") << Domain %label(for="domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="domain" id="domain" name="domain" placeholder="Domain")
@ -16,12 +16,12 @@
%input(type="submit" value="Ban Domain") %input(type="submit" value="Ban Domain")
#data-table.section #domains.section
%table %table
%thead %thead
%tr %tr
%td.domain << Instance %td.domain << Instance
%td << Date %td.date << Joined
%td.remove %td.remove
%tbody %tbody
@ -31,14 +31,14 @@
%details %details
%summary -> =ban.domain %summary -> =ban.domain
%form(action="/admin/domain_bans" method="POST") %form(action="/admin/domain_bans" method="POST")
.grid-2col .items
.reason << Reason .reason << Reason
%textarea.reason(id="reason" name="reason") << {{ban.reason or ""}} %textarea.reason(id="reason" name="reason") << {{ban.reason or ""}}
.note << Note .note << Note
%textarea.note(id="note" name="note") << {{ban.note or ""}} %textarea.note(id="note" name="note") << {{ban.note or ""}}
%input(type="hidden" name="domain" value="{{ban.domain}}") %input(type="hidden" name="domain", value="{{ban.domain}}")
%input(type="submit" value="Update") %input(type="submit" value="Update")
%td.date %td.date

View file

@ -3,23 +3,20 @@
-block content -block content
%details.section %details.section
%summary << Add Instance %summary << Add Instance
%form(action="/admin/instances" method="POST") %form(target="/admin/instances", method="POST")
#add-item #add-instance
%label(for="domain") << Domain %label(for="domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain") %input(type="domain", id="domain" name="domain", placeholder="Domain")
%label(for="actor") << Actor URL %label(for="actor") << Actor URL
%input(type="url" id="actor" name="actor" placeholder="Actor URL") %input(type="url", id="actor" name="actor", placeholder="Actor URL")
%label(for="inbox") << Inbox URL %label(for="inbox") << Inbox URL
%input(type="url" id="inbox" name="inbox" placeholder="Inbox URL") %input(type="url", id="inbox" name="inbox", placeholder="Inbox URL")
%label(for="software") << Software %label(for="software") << Software
%input(name="software" id="software" placeholder="software") %input(name="software", id="software" placeholder="software")
%input(type="submit" value="Add Instance") %input(type="submit" value="Add Instance")
#data-table.section #instances.section
%table %table
%thead %thead
%tr %tr

View file

@ -1,48 +1,5 @@
-extends "base.haml" -extends "base.haml"
-set page="Software Bans" -set page="Software Bans"
-block content -block content
%details.section .section
%summary << Ban Software UvU
%form(action="/admin/software_bans" method="POST")
#add-item
%label(for="name") << Name
%input(id="name" name="name" placeholder="Name")
%label(for="reason") << Ban Reason
%textarea(id="reason" name="reason") << {{""}}
%label(for="note") << Admin Note
%textarea(id="note" name="note") << {{""}}
%input(type="submit" value="Ban Software")
#data-table.section
%table
%thead
%tr
%td.name << Instance
%td << Date
%td.remove
%tbody
-for ban in bans
%tr
%td.name
%details
%summary -> =ban.name
%form(action="/admin/software_bans" method="POST")
.grid-2col
.reason << Reason
%textarea.reason(id="reason" name="reason") << {{ban.reason or ""}}
.note << Note
%textarea.note(id="note" name="note") << {{ban.note or ""}}
%input(type="hidden" name="name" value="{{ban.name}}")
%input(type="submit" value="Update")
%td.date
=ban.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/software_bans/delete/{{ban.name}}" title="Unban software") << &#10006;

View file

@ -1,44 +0,0 @@
-extends "base.haml"
-set page="Users"
-block content
%details.section
%summary << Add User
%form(action="/admin/users", method="POST")
#add-item
%label(for="username") << Username
%input(id="username" name="username" placeholder="Username")
%label(for="password") << Password
%input(type="password" id="password" name="password" placeholder="Password")
%label(for="password2") << Password Again
%input(type="password" id="password2" name="password2" placeholder="Password Again")
%label(for="handle") << Handle
%input(type="email" name="handle" id="handle" placeholder="handle")
%input(type="submit" value="Add User")
#data-table.section
%table
%thead
%tr
%td.username << Username
%td.handle << Handle
%td.date << Joined
%td.remove
%tbody
-for user in users
%tr
%td.username
=user.username
%td.handle
=user.handle or "n/a"
%td.date
=user.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/users/delete/{{user.username}}" title="Remove User") << &#10006;

View file

@ -1,31 +1,5 @@
-extends "base.haml" -extends "base.haml"
-set page="Whitelist" -set page="Whitelist"
-block content -block content
%details.section .section
%summary << Add Domain UvU
%form(action="/admin/whitelist" method="POST")
#add-item
%label(for="domain") << Domain
%input(type="domain" id="domain" name="domain" placeholder="Domain")
%input(type="submit" value="Add Domain")
#data-table.section
%table
%thead
%tr
%td.domain << Domain
%td.date << Added
%td.remove
%tbody
-for item in whitelist
%tr
%td.domain
=item.domain
%td.date
=item.created.strftime("%Y-%m-%d")
%td.remove
%a(href="/admin/whitelist/delete/{{item.domain}}" title="Remove whitlisted domain") << &#10006;

View file

@ -19,7 +19,7 @@
Note: The whitelist is enabled on this instance. Ask the admin to add your instance Note: The whitelist is enabled on this instance. Ask the admin to add your instance
before joining. before joining.
#data-table.section #instances.section
%table %table
%thead %thead
%tr %tr
@ -29,8 +29,5 @@
%tbody %tbody
-for instance in instances -for instance in instances
%tr %tr
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new") %td.instance -> %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain
=instance.domain %td.date -> =instance.created.strftime("%Y-%m-%d")
%td.date
=instance.created.strftime("%Y-%m-%d")

View file

@ -1,12 +1,9 @@
-extends "base.haml" -extends "base.haml"
-set page="Login" -set page="Login"
-block content -block content
%form.section(action="/login" method="POST") %form.section(action="/login" method="post")
.grid-2col %label(for="username") << Username
%label(for="username") << Username %input(id="username" name="username" placeholder="Username" value="{{username or ''}}")
%input(id="username" name="username" placeholder="Username" value="{{username or ''}}") %label(for="password") << Password
%input(id="password" name="password" placeholder="Password" type="password")
%label(for="password") << Password
%input(id="password" name="password" placeholder="Password" type="password")
%input(type="submit" value="Login") %input(type="submit" value="Login")

View file

@ -15,6 +15,13 @@
--spacing: 10px; --spacing: 10px;
} }
body {
color: var(--text);
background-color: #222;
margin: var(--spacing);
font-family: sans serif;
}
a { a {
color: var(--primary); color: var(--primary);
text-decoration: none; text-decoration: none;
@ -25,26 +32,10 @@ a:hover {
text-decoration: underline; text-decoration: underline;
} }
body {
color: var(--text);
background-color: #222;
margin: var(--spacing);
font-family: sans serif;
}
details *:nth-child(2) {
margin-top: 5px;
}
details summary { details summary {
cursor: pointer; cursor: pointer;
} }
form input[type="submit"] {
display: block;
margin: 0 auto;
}
p { p {
line-height: 1em; line-height: 1em;
margin: 0px; margin: 0px;
@ -62,7 +53,6 @@ table {
border: 1px solid var(--primary); border: 1px solid var(--primary);
border-radius: 5px; border-radius: 5px;
border-spacing: 0px; border-spacing: 0px;
width: 100%;
} }
table tbody tr:nth-child(even) td { table tbody tr:nth-child(even) td {
@ -91,18 +81,13 @@ table td {
table thead td { table thead td {
background-color: var(--primary); background-color: var(--primary);
color: var(--background); color: var(--table-background)
text-align: center;
} }
table tbody td { table tbody td {
background-color: var(--table-background); background-color: var(--table-background);
} }
textarea {
height: calc(5em);
}
#container { #container {
width: 1024px; width: 1024px;
margin: 0px auto; margin: 0px auto;
@ -120,17 +105,8 @@ textarea {
font-size: 2em; font-size: 2em;
} }
#header .title-container { #header > *:nth-child(2) {
text-align: center;
}
#header .title {
font-weight: bold; font-weight: bold;
line-height: 0.75em;
}
#header .page {
font-size: 0.5em;
} }
#menu { #menu {
@ -159,6 +135,7 @@ textarea {
#menu > a[active="true"]:not(:hover) { #menu > a[active="true"]:not(:hover) {
background-color: var(--primary-hover); background-color: var(--primary-hover);
color: var(--primary);
border-color: transparent; border-color: transparent;
} }
@ -176,19 +153,11 @@ textarea {
color: var(--primary); color: var(--primary);
} }
#menu-open {
color: var(--primary);
}
#menu-open:hover {
color: var(--primary-hover);
}
#menu-open, #menu-close { #menu-open, #menu-close {
cursor: pointer; cursor: pointer;
} }
#menu-open, #menu-close { #menu-close, #menu-open {
min-width: 35px; min-width: 35px;
text-align: center; text-align: center;
} }
@ -202,23 +171,6 @@ textarea {
text-align: right text-align: right
} }
#add-item {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
#data-table td:first-child {
width: 100%;
}
#data-table .date {
width: max-content;
text-align: right;
}
.button { .button {
background-color: var(--primary); background-color: var(--primary);
@ -248,15 +200,6 @@ textarea {
border: 1px solid var(--error-border) !important; border: 1px solid var(--error-border) !important;
} }
.grid-2col {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
.message { .message {
color: var(--message-text) !important; color: var(--message-text) !important;
background-color: var(--message-background) !important; background-color: var(--message-background) !important;
@ -279,10 +222,9 @@ textarea {
} }
/* config */ {% if page %}
#content.page-config input[type="checkbox"] { {% include "style/" + page.lower().replace(" ", "_") + ".css" %}
justify-self: left; {% endif %}
}
@media (max-width: 1026px) { @media (max-width: 1026px) {
@ -312,8 +254,7 @@ textarea {
} }
.section { .section {
border-left-width: 0px; border-width: 0px;
border-right-width: 0px;
border-radius: 0px; border-radius: 0px;
} }
} }

View file

@ -0,0 +1,20 @@
#config-options {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-bottom: var(--spacing);
align-items: center;
}
form input[type="submit"] {
display: block;
margin: 0 auto;
}
form input[type="checkbox"] {
justify-self: left;
}
textarea {
height: 4em;
}

View file

@ -0,0 +1,40 @@
form input[type="submit"] {
display: block;
margin: 0 auto;
}
textarea {
height: calc(5em);
}
table .items {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
}
#domains table {
width: 100%;
}
#domains .domain {
width: 100%;
}
#domains .date {
width: max-content;
text-align: right;
}
#domains thead td {
text-align: center !important;
}
#add-domain {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
margin-bottom: var(--spacing);
}

View file

@ -0,0 +1,16 @@
#instances table {
width: 100%;
}
#instances .instance {
width: 100%;
}
#instances .date {
width: max-content;
text-align: right;
}
#instances thead td {
text-align: center !important;
}

View file

@ -0,0 +1,33 @@
form input[type="submit"] {
display: block;
margin: 0 auto;
}
#instances table {
width: 100%;
}
#instances .instance {
width: 100%;
}
#instances .software {
text-align: center;
}
#instances .date {
width: max-content;
text-align: right;
}
#instances thead td {
text-align: center !important;
}
#add-instance {
display: grid;
grid-template-columns: max-content auto;
grid-gap: var(--spacing);
margin-top: var(--spacing);
margin-bottom: var(--spacing);
}

View file

@ -0,0 +1,18 @@
label, input {
margin: 0 auto;
display: block;
}
label, input:not([type="submit"]) {
width: 50%;
}
input:not([type="submit"]) {
margin-bottom: var(--spacing);
}
@media (max-width: 1026px) {
label, input:not([type="submit"]) {
width: 75%;
}
}

View file

View file

View file

@ -188,9 +188,8 @@ def cli_setup(ctx: click.Context) -> None:
@cli.command('run') @cli.command('run')
@click.option('--dev', '-d', is_flag=True, help='Enable developer mode')
@click.pass_context @click.pass_context
def cli_run(ctx: click.Context, dev: bool = False) -> None: def cli_run(ctx: click.Context) -> 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 not ctx.obj.signer:
@ -217,7 +216,6 @@ def cli_run(ctx: click.Context, dev: bool = False) -> None:
click.echo(pip_command) click.echo(pip_command)
return return
ctx.obj['dev'] = dev
ctx.obj.run() ctx.obj.run()
# todo: figure out why the relay doesn't quit properly without this # todo: figure out why the relay doesn't quit properly without this

View file

@ -11,10 +11,9 @@ from json.decoder import JSONDecodeError
from ..misc import Response from ..misc import Response
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from aiohttp.web import Request
from collections.abc import Callable, Coroutine, Generator from collections.abc import Callable, Coroutine, Generator
from bsql import Database from bsql import Database
from typing import Any, Self from typing import Self
from ..application import Application from ..application import Application
from ..cache import Cache from ..cache import Cache
from ..config import Config from ..config import Config

View file

@ -4,7 +4,6 @@ import typing
from aiohttp import web from aiohttp import web
from argon2.exceptions import VerifyMismatchError from argon2.exceptions import VerifyMismatchError
from urllib.parse import urlparse
from .base import View, register_route from .base import View, register_route
@ -14,11 +13,8 @@ from ..misc import ACTOR_FORMATS, Message, Response
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from aiohttp.web import Request from aiohttp.web import Request
from collections.abc import Coroutine
# pylint: disable=no-self-use
UNAUTH_ROUTES = { UNAUTH_ROUTES = {
'/', '/',
'/login' '/login'
@ -151,22 +147,22 @@ class AdminInstances(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await request.post() data = {key: value for key, value in (await request.post()).items()}
if not data.get('actor') and not data.get('domain'): if not data['actor'] and not data['domain']:
return await self.get(request, error = 'Missing actor and/or domain') return await self.get(request, error = 'Missing actor and/or domain')
if not data.get('domain'): if not data['domain']:
data['domain'] = urlparse(data['actor']).netloc data['domain'] = urlparse(data['actor']).netloc
if not data.get('software'): if not data['software']:
nodeinfo = await self.client.fetch_nodeinfo(data['domain']) nodeinfo = await self.client.fetch_nodeinfo(data['domain'])
data['software'] = nodeinfo.sw_name data['software'] = nodeinfo.sw_name
if not data.get('actor') and data['software'] in ACTOR_FORMATS: if not data['actor'] and data['software'] in ACTOR_FORMATS:
data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain']) data['actor'] = ACTOR_FORMATS[data['software']].format(domain = data['domain'])
if not data.get('inbox') and data['actor']: if not data['inbox'] and data['actor']:
actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse) actor = await self.client.get(data['actor'], sign_headers = True, loads = Message.parse)
data['inbox'] = actor.shared_inbox data['inbox'] = actor.shared_inbox
@ -180,7 +176,7 @@ class AdminInstances(View):
class AdminInstancesDelete(View): class AdminInstancesDelete(View):
async def get(self, request: Request, domain: str) -> Response: async def get(self, request: Request, domain: str) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
if not conn.get_inbox(domain): if not (conn.get_inbox(domain)):
return await AdminInstances(request).get(request, message = 'Instance not found') return await AdminInstances(request).get(request, message = 'Instance not found')
conn.del_inbox(domain) conn.del_inbox(domain)
@ -197,7 +193,7 @@ class AdminWhitelist(View):
with self.database.session() as conn: with self.database.session() as conn:
context = { context = {
'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all()) 'domains': tuple(conn.execute('SELECT * FROM whitelist').all())
} }
if error: if error:
@ -210,34 +206,6 @@ class AdminWhitelist(View):
return Response.new(data, ctype = 'html') return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
if not data['domain']:
return await self.get(request, error = 'Missing domain')
with self.database.session(True) as conn:
if conn.get_domain_whitelist(data['domain']):
return await self.get(request, message = "Domain already in whitelist")
conn.put_domain_whitelist(data['domain'])
return await self.get(request, message = "Added/updated domain ban")
@register_route('/admin/whitelist/delete/{domain}')
class AdminWhitlistDelete(View):
async def get(self, request: Request, domain: str) -> Response:
with self.database.session() as conn:
if not conn.get_domain_whitelist(domain):
msg = 'Whitelisted domain not found'
return await AdminWhitelist.run("GET", request, message = msg)
conn.del_domain_whitelist(domain)
return await AdminWhitelist.run("GET", request, message = 'Removed domain from whitelist')
@register_route('/admin/domain_bans') @register_route('/admin/domain_bans')
class AdminDomainBans(View): class AdminDomainBans(View):
async def get(self, async def get(self,
@ -262,12 +230,13 @@ class AdminDomainBans(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = await request.post() data = await request.post()
print(data)
if not data['domain']: if not data['domain']:
return await self.get(request, error = 'Missing domain') return await self.get(request, error = 'Missing domain')
with self.database.session(True) as conn: with self.database.session(True) as conn:
if conn.get_domain_ban(data['domain']): if (ban := conn.get_domain_ban(data['domain'])):
conn.update_domain_ban( conn.update_domain_ban(
data['domain'], data['domain'],
data.get('reason'), data.get('reason'),
@ -288,7 +257,7 @@ class AdminDomainBans(View):
class AdminDomainBansDelete(View): class AdminDomainBansDelete(View):
async def get(self, request: Request, domain: str) -> Response: async def get(self, request: Request, domain: str) -> Response:
with self.database.session() as conn: with self.database.session() as conn:
if not conn.get_domain_ban(domain): if not (conn.get_domain_ban(domain)):
return await AdminDomainBans.run("GET", request, message = 'Domain ban not found') return await AdminDomainBans.run("GET", request, message = 'Domain ban not found')
conn.del_domain_ban(domain) conn.del_domain_ban(domain)
@ -298,115 +267,11 @@ class AdminDomainBansDelete(View):
@register_route('/admin/software_bans') @register_route('/admin/software_bans')
class AdminSoftwareBans(View): class AdminSoftwareBans(View):
async def get(self, async def get(self, request: Request) -> Response:
request: Request, data = self.template.render('page/admin-software_bans.haml', self)
error: str | None = None,
message: str | None = None) -> Response:
with self.database.session() as conn:
context = {
'bans': tuple(conn.execute('SELECT * FROM software_bans ORDER BY name ASC').all())
}
if error:
context['error'] = error
if message:
context['message'] = message
data = self.template.render('page/admin-software_bans.haml', self, **context)
return Response.new(data, ctype = 'html') return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
if not data['name']:
return await self.get(request, error = 'Missing name')
with self.database.session(True) as conn:
if conn.get_software_ban(data['name']):
conn.update_software_ban(
data['name'],
data.get('reason'),
data.get('note')
)
else:
conn.put_software_ban(
data['name'],
data.get('reason'),
data.get('note')
)
return await self.get(request, message = "Added/updated software ban")
@register_route('/admin/software_bans/delete/{name}')
class AdminSoftwareBansDelete(View):
async def get(self, request: Request, name: str) -> Response:
with self.database.session() as conn:
if not conn.get_software_ban(name):
return await AdminSoftwareBans.run("GET", request, message = 'Software ban not found')
conn.del_software_ban(name)
return await AdminSoftwareBans.run("GET", request, message = 'Unbanned software')
@register_route('/admin/users')
class AdminUsers(View):
async def get(self,
request: Request,
error: str | None = None,
message: str | None = None) -> Response:
with self.database.session() as conn:
context = {
'users': tuple(conn.execute('SELECT * FROM users').all())
}
if error:
context['error'] = error
if message:
context['message'] = message
data = self.template.render('page/admin-users.haml', self, **context)
return Response.new(data, ctype = 'html')
async def post(self, request: Request) -> Response:
data = await request.post()
required_fields = {'username', 'password', 'password2'}
if not all(data.get(field) for field in required_fields):
return await self.get(request, error = 'Missing username and/or password')
if data['password'] != data['password2']:
return await self.get(request, error = 'Passwords do not match')
with self.database.session(True) as conn:
if conn.get_user(data['username']):
return await self.get(request, message = "User already exists")
conn.put_user(data['username'], data['password'], data['handle'])
return await self.get(request, message = "Added user")
@register_route('/admin/users/delete/{name}')
class AdminUsersDelete(View):
async def get(self, request: Request, name: str) -> Response:
with self.database.session() as conn:
if not conn.get_user(name):
return await AdminUsers.run("GET", request, message = 'User not found')
conn.del_user(name)
return await AdminUsers.run("GET", request, message = 'User deleted')
@register_route('/admin/config') @register_route('/admin/config')
class AdminConfig(View): class AdminConfig(View):
async def get(self, request: Request, message: str | None = None) -> Response: async def get(self, request: Request, message: str | None = None) -> Response:
@ -443,5 +308,5 @@ class AdminConfig(View):
@register_route('/style.css') @register_route('/style.css')
class StyleCss(View): class StyleCss(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:
data = self.template.render('style.css', self) data = self.template.render('style.css', self, page = request.query.getone('page', ""))
return Response.new(data, ctype = 'css') return Response.new(data, ctype = 'css')

View file

@ -34,9 +34,8 @@ dev = file: dev-requirements.txt
[options.package_data] [options.package_data]
relay = relay =
data/* data/statements.sql
frontend/* data/swagger.yaml
frontend/page/*
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =