This commit is contained in:
Izalia Mae 2024-04-16 22:24:06 -04:00
parent abe8ed2e6e
commit 848350684f
11 changed files with 112 additions and 51 deletions

View file

@ -10,40 +10,35 @@ from .server import Application
SRC = Path(__file__).resolve().parent
APP = Application(Path("~/.config/barkshark/social/server.yaml").expanduser())
@click.group("cli")
@click.option("--config", "-c", type = Path)
def cli(config: Path) -> None:
@click.pass_context
def cli(ctx: click.Context, config: Path) -> None:
ctx.obj = Application(config)
if platform.system() == "Windows":
TRANS.print("general", "windows")
if config:
APP.state.config.path = config.expanduser().resolve()
APP.state.config.load()
@cli.command("setup")
def cli_setup() -> None:
config = APP.state.config
config.save()
if config.sqlite_path.exists():
@click.pass_context
def cli_setup(ctx: click.Context) -> None:
if ctx.obj.config.sqlite_path.exists():
click.confirm(TRANS.fetch("setup", "prompt-database"), abort = True)
config.sqlite_path.unlink()
ctx.obj.config.sqlite_path.unlink()
current = aputils.HttpDate.new_utc()
with APP.state.database.session(True) as s:
with ctx.obj.database.session(True) as s:
s.create_tables()
s.insert("instance", {
"id": -99,
"domain": config.host,
"web_domain": config.web_host,
"shared_inbox": f"https://{config.web_host}/inbox",
"domain": ctx.obj.config.host,
"web_domain": ctx.obj.config.web_host,
"shared_inbox": f"https://{ctx.obj.config.web_host}/inbox",
"name": "Barkshark Social",
"description": "Lightweight ActivityPub server",
"software": "barksharksocial",
@ -51,20 +46,20 @@ def cli_setup() -> None:
"updated": current
})
click.echo(TRANS.fetch("setup", "create-key", username = config.host))
instance_signer = aputils.Signer.new(f"https://{config.web_host}/actor")
click.echo(TRANS.fetch("setup", "create-key", username = ctx.obj.config.host))
instance_signer = aputils.Signer.new(f"https://{ctx.obj.config.web_host}/actor")
click.echo(TRANS.fetch("setup", "create-key", username = "relay"))
relay_signer = aputils.Signer.new(f"https://{config.web_host}/relay")
relay_signer = aputils.Signer.new(f"https://{ctx.obj.config.web_host}/relay")
s.insert("user", {
"id": -99,
"username": config.host,
"domain": config.host,
"display_name": config.host,
"actor": f"https://{config.web_host}/actor",
"inbox": f"https://{config.web_host}/inbox",
"page_url": f"https://{config.web_host}/about",
"username": ctx.obj.config.host,
"domain": ctx.obj.config.host,
"display_name": ctx.obj.config.host,
"actor": f"https://{ctx.obj.config.web_host}/actor",
"inbox": f"https://{ctx.obj.config.web_host}/inbox",
"page_url": f"https://{ctx.obj.config.web_host}/about",
"permission": 0,
"locked": False,
"private_key": instance_signer.export(),
@ -76,11 +71,11 @@ def cli_setup() -> None:
s.insert("user", {
"id": -98,
"username": "relay",
"domain": config.host,
"domain": ctx.obj.config.host,
"display_name": "Relay",
"actor": f"https://{config.web_host}/relay",
"inbox": f"https://{config.web_host}/relay",
"page_url": f"https://{config.web_host}/about#relay",
"actor": f"https://{ctx.obj.config.web_host}/relay",
"inbox": f"https://{ctx.obj.config.web_host}/relay",
"page_url": f"https://{ctx.obj.config.web_host}/about#relay",
"permission": 0,
"locked": False,
"private_key": relay_signer.export(),
@ -95,10 +90,11 @@ def cli_setup() -> None:
@cli.command("run")
@click.option("--language", "-l", default = "en")
@click.option("--dev", "-d", is_flag = True)
def cli_run(language: str, dev: bool = False) -> None:
APP.state.dev = dev
APP.state.trans.default = language
APP.run()
@click.pass_context
def cli_run(ctx: click.Context, language: str, dev: bool = False) -> None:
ctx.obj.dev = dev
ctx.obj.trans.default = language
ctx.obj.run()
@cli.group("user")
@ -111,16 +107,16 @@ def cli_user() -> None:
@click.argument("email")
@click.option("--permissions", "-p", type = PermissionLevel.parse, default = PermissionLevel.USER)
@click.option("--activate", "-a", is_flag = True)
@click.pass_context
def cli_user_create(
ctx: click.Context,
username: str,
email: str,
permissions: PermissionLevel,
activate: bool) -> None:
config = APP.state.config
with APP.state.database.session(True) as s:
if (user := s.get_user(username, config.host)) is not None:
with ctx.obj.database.session(True) as s:
if (user := s.get_user(username, ctx.obj.config.host)) is not None:
TRANS.print("error", "user-exists", handle = user.handle)
return
@ -148,8 +144,9 @@ def cli_user_create(
@cli_user.command("reset-password")
@click.argument("username")
def cli_user_reset_pass(username: str) -> None:
with APP.state.database.session(True) as s:
@click.pass_context
def cli_user_reset_pass(ctx: click.Context, username: str) -> None:
with ctx.obj.database.session(True) as s:
if (user := s.get_user(username, None)) is None:
click.echo("User does not exist")
return

View file

@ -83,9 +83,12 @@ class Config(dict[str, dict[str, Any]]):
dev: Property[bool] = Property("Advanced", "dev", False)
def __init__(self, path: Path | str) -> None:
def __init__(self, path: Path | str | None = None) -> None:
dict.__init__(self)
if path is None:
path = Path("~/.config/barkshark/social/server.yaml")
if isinstance(path, str):
path = Path(path)

View file

@ -96,10 +96,12 @@ class Connection(bsql.Connection):
return Instance.from_possible_row(cur.one())
def get_user(self, username: str, domain: str | int | None) -> User | None:
def get_user(self, username: str, domain: str | None = None) -> User | None:
if not domain:
domain = self.config.host
print(username, domain)
with self.select("user", username = username, domain = domain) as cur:
return User.from_possible_row(cur.one())
@ -362,6 +364,11 @@ class Connection(bsql.Connection):
return Cookie.from_row(row)
def del_cookie(self, token: str) -> None:
with self.delete("cookie", code = token):
pass
def get_count(row: bsql.Row | None) -> int:
if not row:
return 0

View file

@ -103,7 +103,6 @@ SCHEMA = Tables(
Column("id", "serial"),
Column("code", "text", nullable = False, unique = True),
Column("userid", "integer", nullable = False, foreign_key = ("user", "id")),
Column("authorization_code", "text", unique = True),
Column("user_agent", "text"),
Column("last_access", "timestamp"),
Column("created", "timestamp")

View file

@ -1,5 +1,5 @@
-extends "base.haml"
-block content
#error.section
<h1>HTTP Error {{error.status_code}}</h1>
{{error.detail}}
<h1>HTTP Error {{error.status.value}}</h1>
{{error.message}}

View file

@ -1,5 +1,6 @@
from importlib.resources import files as PackageFiles
from pathlib import Path
from typing import Any
def get_resource(path: str) -> Path:
@ -9,3 +10,31 @@ def get_resource(path: str) -> Path:
def read_resource_text(path: str) -> str:
with get_resource(path).open("r", encoding = "utf-8") as fd:
return fd.read()
class DictProperty:
"Provide attribute access to a dict key."
def __init__(self, key: str):
self.key = key
def __get__(self, obj: dict[str, Any] | None, objtype: type[Any] | None = None) -> Any:
if obj is None:
return self
try:
return obj[self.key]
except KeyError:
objname = object.__class__.__name__
raise AttributeError(f"'{objname}' has no attribute '{self.key}'") from None
def __set__(self, obj: dict[str, Any], value: Any) -> None:
obj[self.key] = value
def __delete__(self, obj: dict[str, Any]) -> None:
del obj[self.key]

View file

@ -47,7 +47,7 @@ async def handle_login_post(request: Request) -> Response:
raise HttpError(400, "Invalid field type")
with state.database.session(False) as s:
if (user := s.get_user(username, None)) is None:
if (user := s.get_user(username.lower())) is None:
raise HttpError(400, "User does not exist")
try:
@ -74,6 +74,25 @@ async def handle_login_post(request: Request) -> Response:
return response
@router.get("BarksharkSocial", "/logout")
async def handle_logout_get(request: Request) -> Response:
cname = request.app.state.config.cookie_name
response = Response.new_redirect("/")
try:
cookie = request.cookies[cname]
with request.app.state.database.session(True) as s:
s.del_cookie(cookie.value)
response.delete_cookie(cookie)
except KeyError:
pass
return response
@router.get("BarksharkSocial", "/register")
async def handle_register_get(request: Request) -> Response:
return TemplateResponse("page/login.haml")

View file

@ -63,7 +63,7 @@ async def handle_wk_nodeinfo(request: Request) -> Response:
return Response.new_json(200, data)
@router.get("BarksharkSocial", "/nodeinfo/{version:float}.json")
@router.get("BarksharkSocial", "/nodeinfo/{version:float}.json", "/nodeinfo/{version:float}")
async def handle_nodeinfo(request: Request) -> Response:
ni_version = request.params["version"]
config = request.app.state.config

View file

@ -28,7 +28,7 @@ from basgi import (
class Application(App[Request, RequestState, AppState]):
def __init__(self, cfg_path: Path | str, language: str = "en") -> None:
def __init__(self, cfg_path: Path | str | None = None, language: str = "en") -> None:
App.__init__(
self,
"BarksharkSocial",
@ -114,7 +114,7 @@ class Application(App[Request, RequestState, AppState]):
if "json" in request.headers.get("Accept", ""):
return Response.new_json(exc.status, {"error": str(exc)})
return TemplateResponse("error.haml", {"error": str(exc)}, exc.status)
return TemplateResponse("error.haml", {"error": exc}, exc.status)
def handle_context(self, env: Template, context: dict[str, Any]) -> dict[str, Any]:

View file

@ -13,9 +13,11 @@ from .translations import Translations
class AppState(StateProxy):
def setup(self, cfg_path: Path | str, language: str = "en") -> None:
def setup(self, cfg_path: Path | str | None = None, language: str = "en") -> None:
self["trans"] = Translations(get_resource("frontend/translations"), language)
self["config"] = Config(cfg_path)
self["config"].load()
self["hasher"] = PasswordHasher(
time_cost = 10,
memory_cost = 1024 * 100,

7
dev.py
View file

@ -130,8 +130,13 @@ def cli_update_files() -> None:
@cli.command("exec", context_settings = {"allow_extra_args": True})
@option("--watch", "-w", is_flag = True)
@pass_context
def cli_run(ctx: Context) -> None:
def cli_run(ctx: Context, watch: bool = False) -> None:
if watch:
handle_run_watcher("-m", "barkshark_social", *ctx.args)
return
run_python("-m", "barkshark_social", *ctx.args)