Compare commits

...

13 commits

Author SHA1 Message Date
Izalia Mae a0ee22406b don't use spec file when building bin 2024-03-04 05:39:21 -05:00
Izalia Mae 0af6a33b69 use darker text color for active menu item 2024-03-04 02:34:28 -05:00
Izalia Mae 01a491f272 replace target with action in forms 2024-03-04 02:32:57 -05:00
Izalia Mae 5f156401c7 fix linter warnings 2024-03-04 02:30:54 -05:00
Izalia Mae 4cbf83a7b7 simplify css 2024-03-04 02:19:26 -05:00
Izalia Mae 33102f9e4e disable caching for now 2024-03-04 01:42:27 -05:00
Izalia Mae b73fdece95 create admin users page 2024-03-04 01:40:51 -05:00
Izalia Mae 2223695d15 remove commas in element attributes 2024-03-04 01:14:17 -05:00
Izalia Mae d44498b966 fix dev mode 2024-03-04 01:09:40 -05:00
Izalia Mae 5dcf375247 create admin whitelist page 2024-03-04 01:00:57 -05:00
Izalia Mae a163f2baab create admin software bans page 2024-03-04 00:43:12 -05:00
Izalia Mae d7cfa12145 add Cache-Control header 2024-03-04 00:32:13 -05:00
Izalia Mae 4639d8a78d minor frontend tweaks 2024-03-03 18:28:49 -05:00
27 changed files with 443 additions and 291 deletions

View file

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

View file

@ -1,56 +0,0 @@
# -*- 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,5 +1,8 @@
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,9 +26,10 @@ 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 from .misc import Message, Response
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
@ -36,11 +37,12 @@ if typing.TYPE_CHECKING:
class Application(web.Application): class Application(web.Application):
DEFAULT: Application = None DEFAULT: Application = None
def __init__(self, cfgpath: str | None): def __init__(self, cfgpath: str | None, dev: bool = False):
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
] ]
) )
@ -50,6 +52,7 @@ 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)
@ -255,25 +258,18 @@ 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'
async def handle_access_log(request: web.Request, response: web.Response) -> None: # if not request.app['dev'] and request.path.endswith(('.css', '.js')):
address = request.headers.get( # resp.headers['Cache-Control'] = 'public,max-age=2628000,immutable'
'X-Forwarded-For',
request.headers.get(
'X-Real-Ip',
request.remote
)
)
logging.info( # else:
'%s "%s %s" %i %i "%s"', # resp.headers['Cache-Control'] = 'no-store'
address,
request.method, return resp
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,7 +52,11 @@ if IS_DOCKER:
class Config: class Config:
def __init__(self, path: str, load: bool = False): def __init__(self, path: str, load: bool = False):
self.path = Config.get_config_dir() if path:
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,10 +1,14 @@
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
@ -46,25 +50,33 @@ 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():
cmd = [sys.executable, '-m', 'PyInstaller', 'relay.spec'] with TemporaryDirectory() as tmp:
subprocess.run(cmd, check = False) arch = 'amd64' if sys.maxsize >= 2**32 else 'i386'
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')
@ -94,7 +106,7 @@ def cli_run():
class WatchHandler(PatternMatchingEventHandler): class WatchHandler(PatternMatchingEventHandler):
patterns = ['*.py'] patterns = ['*.py']
cmd = [sys.executable, '-m', 'relay', 'run'] cmd = [sys.executable, '-m', 'relay', 'run', '-d']
def __init__(self): def __init__(self):
@ -145,7 +157,6 @@ 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?page={{page}}") %link(rel="stylesheet" type="text/css" href="/style.css")
-block head -block head
%body %body
@ -27,6 +27,7 @@
{{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")}}
@ -35,8 +36,13 @@
#container #container
#header.section #header.section
%span#menu-open.button << &#8286; %span#menu-open << &#8286;
%a(href="https://{{domain}}/") -> =config.name %span.title-container
%a.title(href="/") -> =config.name
-if view.request.path not in ["/", "/login"]
.page -> =page
.empty .empty
-if error -if error
@ -45,7 +51,7 @@
-if message -if message
.message.section -> =message .message.section -> =message
#content #content(class="page-{{page.lower().replace(' ', '_')}}")
-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")
#config-options .grid-2col
%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-domain #add-item
%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")
#domains.section #data-table.section
%table %table
%thead %thead
%tr %tr
%td.domain << Instance %td.domain << Instance
%td.date << Joined %td << Date
%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")
.items .grid-2col
.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,20 +3,23 @@
-block content -block content
%details.section %details.section
%summary << Add Instance %summary << Add Instance
%form(target="/admin/instances", method="POST") %form(action="/admin/instances" method="POST")
#add-instance #add-item
%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")
#instances.section #data-table.section
%table %table
%thead %thead
%tr %tr

View file

@ -1,5 +1,48 @@
-extends "base.haml" -extends "base.haml"
-set page="Software Bans" -set page="Software Bans"
-block content -block content
.section %details.section
UvU %summary << Ban Software
%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

@ -0,0 +1,44 @@
-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,5 +1,31 @@
-extends "base.haml" -extends "base.haml"
-set page="Whitelist" -set page="Whitelist"
-block content -block content
.section %details.section
UvU %summary << Add Domain
%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.
#instances.section #data-table.section
%table %table
%thead %thead
%tr %tr
@ -29,5 +29,8 @@
%tbody %tbody
-for instance in instances -for instance in instances
%tr %tr
%td.instance -> %a(href="https://{{instance.domain}}/" target="_new") -> =instance.domain %td.instance -> %a(href="https://{{instance.domain}}/" target="_new")
%td.date -> =instance.created.strftime("%Y-%m-%d") =instance.domain
%td.date
=instance.created.strftime("%Y-%m-%d")

View file

@ -1,9 +1,12 @@
-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")
%label(for="username") << Username .grid-2col
%input(id="username" name="username" placeholder="Username" value="{{username or ''}}") %label(for="username") << Username
%label(for="password") << Password %input(id="username" name="username" placeholder="Username" value="{{username or ''}}")
%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,13 +15,6 @@
--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;
@ -32,10 +25,26 @@ 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;
@ -53,6 +62,7 @@ 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 {
@ -81,13 +91,18 @@ table td {
table thead td { table thead td {
background-color: var(--primary); background-color: var(--primary);
color: var(--table-background) color: var(--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;
@ -105,8 +120,17 @@ table tbody td {
font-size: 2em; font-size: 2em;
} }
#header > *:nth-child(2) { #header .title-container {
text-align: center;
}
#header .title {
font-weight: bold; font-weight: bold;
line-height: 0.75em;
}
#header .page {
font-size: 0.5em;
} }
#menu { #menu {
@ -135,7 +159,6 @@ table tbody td {
#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;
} }
@ -153,11 +176,19 @@ table tbody td {
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-close, #menu-open { #menu-open, #menu-close {
min-width: 35px; min-width: 35px;
text-align: center; text-align: center;
} }
@ -171,6 +202,23 @@ table tbody td {
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);
@ -200,6 +248,15 @@ table tbody td {
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;
@ -222,9 +279,10 @@ table tbody td {
} }
{% if page %} /* config */
{% include "style/" + page.lower().replace(" ", "_") + ".css" %} #content.page-config input[type="checkbox"] {
{% endif %} justify-self: left;
}
@media (max-width: 1026px) { @media (max-width: 1026px) {
@ -254,7 +312,8 @@ table tbody td {
} }
.section { .section {
border-width: 0px; border-left-width: 0px;
border-right-width: 0px;
border-radius: 0px; border-radius: 0px;
} }
} }

View file

@ -1,20 +0,0 @@
#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

@ -1,40 +0,0 @@
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

@ -1,16 +0,0 @@
#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

@ -1,33 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -188,8 +188,9 @@ 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) -> 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 not ctx.obj.signer:
@ -216,6 +217,7 @@ def cli_run(ctx: click.Context) -> 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,9 +11,10 @@ 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 Self from typing import Any, 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,6 +4,7 @@ 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
@ -13,8 +14,11 @@ 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'
@ -147,22 +151,22 @@ class AdminInstances(View):
async def post(self, request: Request) -> Response: async def post(self, request: Request) -> Response:
data = {key: value for key, value in (await request.post()).items()} data = await request.post()
if not data['actor'] and not data['domain']: if not data.get('actor') and not data.get('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['domain']: if not data.get('domain'):
data['domain'] = urlparse(data['actor']).netloc data['domain'] = urlparse(data['actor']).netloc
if not data['software']: if not data.get('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['actor'] and data['software'] in ACTOR_FORMATS: if not data.get('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['inbox'] and data['actor']: if not data.get('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
@ -176,7 +180,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)
@ -193,7 +197,7 @@ class AdminWhitelist(View):
with self.database.session() as conn: with self.database.session() as conn:
context = { context = {
'domains': tuple(conn.execute('SELECT * FROM whitelist').all()) 'whitelist': tuple(conn.execute('SELECT * FROM whitelist').all())
} }
if error: if error:
@ -206,6 +210,34 @@ 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,
@ -230,13 +262,12 @@ 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 (ban := conn.get_domain_ban(data['domain'])): if conn.get_domain_ban(data['domain']):
conn.update_domain_ban( conn.update_domain_ban(
data['domain'], data['domain'],
data.get('reason'), data.get('reason'),
@ -257,7 +288,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)
@ -267,11 +298,115 @@ class AdminDomainBansDelete(View):
@register_route('/admin/software_bans') @register_route('/admin/software_bans')
class AdminSoftwareBans(View): class AdminSoftwareBans(View):
async def get(self, request: Request) -> Response: async def get(self,
data = self.template.render('page/admin-software_bans.haml', self) request: Request,
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:
@ -308,5 +443,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, page = request.query.getone('page', "")) data = self.template.render('style.css', self)
return Response.new(data, ctype = 'css') return Response.new(data, ctype = 'css')

View file

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