Skip to content

Commit

Permalink
partial episode patch support
Browse files Browse the repository at this point in the history
  • Loading branch information
trim21 committed Aug 22, 2024
1 parent 080ab20 commit 5ce8eba
Show file tree
Hide file tree
Showing 13 changed files with 565 additions and 18 deletions.
34 changes: 34 additions & 0 deletions episode.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
create table episode_patch
(
id uuid not null
primary key,
episode_id integer not null,
state integer default 0 not null,
from_user_id integer not null,
wiki_user_id integer default 0 not null,
reason text not null,

original_name text,
name text,

original_name_cn text,
name_cn text,

original_duration varchar(255),
duration varchar(255),

original_airdate varchar(64),
airdate varchar(64),

original_description text,
description text,

created_at timestamp with time zone default CURRENT_TIMESTAMP not null,
updated_at timestamp with time zone default CURRENT_TIMESTAMP not null,
deleted_at timestamp with time zone,

reject_reason varchar(255) default '' not null
);


create index on episode_patch (state);
15 changes: 14 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ redis = { extras = ["hiredis"], version = "^5.0.0" }
uvicorn = { version = "^0.30.6", extras = ['standard'] }
uuid6 = "^2024.7.10"
pydash = "^8.0.3"
dacite = "^1.8.1"

[tool.poetry.group.dev.dependencies]
mypy = "^1.11.1"
Expand Down
4 changes: 3 additions & 1 deletion server/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def allow_edit(self) -> bool:
return self.group_id in {2, 9, 11}


http_client = httpx.AsyncClient(follow_redirects=False)
http_client = httpx.AsyncClient(
follow_redirects=False, headers={"user-agent": "trim21/submit-patch"}
)
pg = asyncpg.create_pool(dsn=PG_DSN)


Expand Down
135 changes: 130 additions & 5 deletions server/contrib.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import Annotated, Any
Expand All @@ -15,7 +16,7 @@
from litestar.response import Redirect, Template
from uuid6 import uuid7

from config import TURNSTILE_SECRET_KEY, TURNSTILE_SITE_KEY, UTC
from config import TURNSTILE_SECRET_KEY, UTC
from server.auth import require_user_login
from server.base import AuthorizedRequest, BadRequestException, Request, http_client, pg
from server.model import Patch, Wiki
Expand All @@ -39,10 +40,7 @@ async def suggest_ui(request: Request, subject_id: int = 0) -> Response[Any]:
if res.status_code >= 300:
raise NotFoundException()
data = res.json()
return Template(
"suggest.html.jinja2",
context={"data": data, "subject_id": subject_id, "CAPTCHA_SITE_KEY": TURNSTILE_SITE_KEY},
)
return Template("suggest.html.jinja2", context={"data": data, "subject_id": subject_id})


@dataclass(frozen=True, slots=True, kw_only=True)
Expand Down Expand Up @@ -165,3 +163,130 @@ async def delete_patch(patch_id: str, request: AuthorizedRequest) -> Redirect:
)

return Redirect("/")


@router
@litestar.get("/suggest-episode")
async def episode_suggest_ui(request: Request, episode_id: int = 0) -> Response[Any]:
if episode_id == 0:
return Template("episode/select.html.jinja2")

if not request.auth:
request.set_session({"backTo": request.url.path + f"?episode_id={episode_id}"})
return Redirect("/login")

res = await http_client.get(f"https://api.bgm.tv/v0/episodes/{episode_id}")
if res.status_code == 404:
raise NotFoundException()

res.raise_for_status()

data = res.json()

return Template("episode/suggest.html.jinja2", context={"data": data, "subject_id": episode_id})


@dataclass(frozen=True, slots=True, kw_only=True)
class CreateEpisodePatch:
airdate: str
name: str
name_cn: str
duration: str
desc: str

cf_turnstile_response: str
reason: str


@router
@litestar.post("/suggest-episode", guards=[require_user_login])
async def creat_episode_patch(
request: Request,
episode_id: int,
data: Annotated[CreateEpisodePatch, Body(media_type=RequestEncodingType.URL_ENCODED)],
) -> Response[Any]:
if not data.reason:
raise ValidationException("missing suggestion description")

res = await http_client.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={
"secret": TURNSTILE_SECRET_KEY,
"response": data.cf_turnstile_response,
},
)
if res.status_code > 300:
raise BadRequestException("验证码无效")
captcha_data = res.json()
if captcha_data.get("success") is not True:
raise BadRequestException("验证码无效")

res = await http_client.get(f"https://api.bgm.tv/v0/episodes/{episode_id}")
if res.status_code == 404:
raise NotFoundException()

res.raise_for_status()

original_wiki = res.json()

keys = ["airdate", "name", "name_cn", "duration", "desc"]

changed = {}

for key in keys:
if original_wiki[key] != getattr(data, key):
changed[key] = getattr(data, key)

if not changed:
raise HTTPException("no changes found", status_code=400)

pk = uuid7()

await pg.execute(
"""
insert into episode_patch (id, episode_id, from_user_id, reason, original_name, name,
original_name_cn, name_cn, original_duration, duration,
original_airdate, airdate, original_description, description)
VALUES ($1, $2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
""",
pk,
episode_id,
request.auth.user_id,

Check failure on line 254 in server/contrib.py

View workflow job for this annotation

GitHub Actions / mypy

Item "None" of "User | None" has no attribute "user_id"
data.reason,
original_wiki["name"],
changed.get("name"),
original_wiki["name_cn"],
changed.get("name_cn"),
original_wiki["duration"],
changed.get("duration"),
original_wiki["airdate"],
changed.get("airdate"),
original_wiki["desc"],
changed.get("desc"),
)

return Redirect(f"/episode/{pk}")


@router
@litestar.post("/api/delete-episode/{patch_id:uuid}", guards=[require_user_login])
async def delete_episode_patch(patch_id: uuid.UUID, request: AuthorizedRequest) -> Redirect:
async with pg.acquire() as conn:
async with conn.transaction():
p = await conn.fetchrow(
"""select from_user_id from episode_patch where id = $1 and deleted_at is NULL""",
patch_id,
)
if not p:
raise NotFoundException()

if p["from_user_id"] != request.auth.user_id:
raise NotAuthorizedException

await conn.execute(
"update episode_patch set deleted_at = $1 where id = $2",
datetime.now(tz=UTC),
patch_id,
)

return Redirect("/")
24 changes: 24 additions & 0 deletions server/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ class Patch:
reject_reason: str


@dataclass(frozen=True, kw_only=True, slots=True)
class EpisodePatch:
id: str
episode_id: int
state: int
from_user_id: int
wiki_user_id: int
reason: str
original_name: str
name: str
original_name_cn: str
name_cn: str
original_duration: str
duration: str | None
original_airdate: str
airdate: str | None
original_description: str
description: str | None
created_at: str
updated_at: str
deleted_at: datetime | None
reject_reason: str


@dataclass(frozen=True, slots=True, kw_only=True)
class Wiki:
name: str
Expand Down
65 changes: 56 additions & 9 deletions server/patch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import difflib
import uuid

import litestar
import uuid6
from litestar.exceptions import InternalServerException, NotFoundException
from litestar.response import Template
from loguru import logger
Expand All @@ -15,14 +15,8 @@


@router
@litestar.get("/patch/{patch_id:str}")
async def get_patch(patch_id: str, request: Request) -> Template:
try:
uuid6.UUID(hex=patch_id)
except ValueError as e:
# not valid uuid string, just raise not-found
raise NotFoundException() from e

@litestar.get("/patch/{patch_id:uuid}")
async def get_patch(patch_id: uuid.UUID, request: Request) -> Template:
p = await pg.fetchrow(
"""select * from patch where id = $1 and deleted_at is NULL limit 1""", patch_id
)
Expand Down Expand Up @@ -87,3 +81,56 @@ async def get_patch(patch_id: str, request: Request) -> Template:
"submitter": submitter,
},
)


@router
@litestar.get("/episode/{patch_id:uuid}")
async def get_episode_patch(patch_id: uuid.UUID, request: Request) -> Template:
p = await pg.fetchrow(
"""select * from episode_patch where id = $1 and deleted_at is NULL limit 1""", patch_id
)
if not p:
raise NotFoundException()

diff = []

keys = ["name", "name_cn", "duration", "airdate", "description"]

for key in keys:
after = p[key]
if after is None:
continue

original = p["original_" + key]

if original != after:
diff.append(
"".join(
# need a tailing new line to generate correct diff
difflib.unified_diff(
(original + "\n").splitlines(True),
(after + "\n").splitlines(True),
key,
key,
)
)
)

reviewer = None
if p["state"] != PatchState.Pending:
reviewer = await pg.fetchrow(
"select * from patch_users where user_id=$1", p["wiki_user_id"]
)

submitter = await pg.fetchrow("select * from patch_users where user_id=$1", p["from_user_id"])

return Template(
"episode/patch.html.jinja2",
context={
"patch": p,
"auth": request.auth,
"diff": "".join(diff),
"reviewer": reviewer,
"submitter": submitter,
},
)
Loading

0 comments on commit 5ce8eba

Please sign in to comment.