fix(backend,frontend): expired story keeps its kind and archive heals stories that lost it, phone inputs no longer zoom the page
This commit is contained in:
1 parent
d9378056f4
commit
acfa726e61
7 files changed
+69
-9
No files matched your search
@@ -5,7 +5,7 @@ from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
|||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
from utils.files import content_disposition, story_file_name
|
from utils.files import content_disposition, sniff_media_kind, story_file_name
|
||||||
from utils.read import peers
|
from utils.read import peers
|
||||||
from utils.read.models import DEFAULT_LIMIT, Page, StoryView
|
from utils.read.models import DEFAULT_LIMIT, Page, StoryView
|
||||||
from utils.storage import ContentAddressedStorage
|
from utils.storage import ContentAddressedStorage
|
||||||
@@ -17,17 +17,34 @@ AccountId = Annotated[int, Query()]
|
|||||||
_STORY_MIME = {"photo": "image/jpeg", "video": "video/mp4"}
|
_STORY_MIME = {"photo": "image/jpeg", "video": "video/mp4"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _with_kind(
|
||||||
|
pool: asyncpg.Pool,
|
||||||
|
storage: ContentAddressedStorage,
|
||||||
|
account_id: int,
|
||||||
|
story: StoryView,
|
||||||
|
) -> StoryView:
|
||||||
|
if story.media_kind or not story.storage_key:
|
||||||
|
return story
|
||||||
|
if not storage.exists(story.storage_key):
|
||||||
|
return story
|
||||||
|
kind = sniff_media_kind(storage.head(story.storage_key))
|
||||||
|
await peers.set_story_kind(pool, account_id, story.peer_id, story.story_id, kind)
|
||||||
|
return story.model_copy(update={"media_kind": kind})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stories")
|
@router.get("/stories")
|
||||||
async def list_stories(
|
async def list_stories(
|
||||||
pool: FromDishka[asyncpg.Pool],
|
pool: FromDishka[asyncpg.Pool],
|
||||||
|
storage: FromDishka[ContentAddressedStorage],
|
||||||
account_id: AccountId,
|
account_id: AccountId,
|
||||||
peer_id: Annotated[int | None, Query()] = None,
|
peer_id: Annotated[int | None, Query()] = None,
|
||||||
limit: Annotated[int, Query()] = DEFAULT_LIMIT,
|
limit: Annotated[int, Query()] = DEFAULT_LIMIT,
|
||||||
offset: Annotated[int, Query()] = 0,
|
offset: Annotated[int, Query()] = 0,
|
||||||
) -> list[StoryView]:
|
) -> list[StoryView]:
|
||||||
return await peers.get_stories(
|
stories = await peers.get_stories(
|
||||||
pool, account_id, Page(limit=limit, offset=offset), peer_id=peer_id
|
pool, account_id, Page(limit=limit, offset=offset), peer_id=peer_id
|
||||||
)
|
)
|
||||||
|
return [await _with_kind(pool, storage, account_id, story) for story in stories]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stories/{peer_id}/{story_id}/media")
|
@router.get("/stories/{peer_id}/{story_id}/media")
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ _UPSERT_STORY = """
|
|||||||
INSERT INTO stories
|
INSERT INTO stories
|
||||||
(account_id, peer_id, story_id, date, expire_date, caption, media_kind,
|
(account_id, peer_id, story_id, date, expire_date, caption, media_kind,
|
||||||
storage_key, file_size, downloaded, views, pinned, deleted, raw, observed_at)
|
storage_key, file_size, downloaded, views, pinned, deleted, raw, observed_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb, now())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, false, $13::jsonb, now())
|
||||||
ON CONFLICT (account_id, peer_id, story_id) DO UPDATE SET
|
ON CONFLICT (account_id, peer_id, story_id) DO UPDATE SET
|
||||||
date = EXCLUDED.date,
|
date = EXCLUDED.date,
|
||||||
expire_date = EXCLUDED.expire_date,
|
expire_date = EXCLUDED.expire_date,
|
||||||
@@ -17,10 +17,17 @@ ON CONFLICT (account_id, peer_id, story_id) DO UPDATE SET
|
|||||||
downloaded = stories.downloaded OR EXCLUDED.downloaded,
|
downloaded = stories.downloaded OR EXCLUDED.downloaded,
|
||||||
views = EXCLUDED.views,
|
views = EXCLUDED.views,
|
||||||
pinned = EXCLUDED.pinned,
|
pinned = EXCLUDED.pinned,
|
||||||
deleted = EXCLUDED.deleted,
|
deleted = false,
|
||||||
raw = EXCLUDED.raw
|
raw = EXCLUDED.raw
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_MARK_DELETED = """
|
||||||
|
INSERT INTO stories
|
||||||
|
(account_id, peer_id, story_id, downloaded, pinned, deleted, raw, observed_at)
|
||||||
|
VALUES ($1, $2, $3, false, false, true, '{}'::jsonb, now())
|
||||||
|
ON CONFLICT (account_id, peer_id, story_id) DO UPDATE SET deleted = true
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def is_downloaded(
|
async def is_downloaded(
|
||||||
pool: asyncpg.Pool, account_id: int, peer_id: int, story_id: int
|
pool: asyncpg.Pool, account_id: int, peer_id: int, story_id: int
|
||||||
@@ -51,7 +58,6 @@ async def upsert_story( # noqa: PLR0913
|
|||||||
raw: str,
|
raw: str,
|
||||||
*,
|
*,
|
||||||
pinned: bool,
|
pinned: bool,
|
||||||
deleted: bool,
|
|
||||||
downloaded: bool,
|
downloaded: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
await pool.execute(
|
await pool.execute(
|
||||||
@@ -68,6 +74,11 @@ async def upsert_story( # noqa: PLR0913
|
|||||||
downloaded,
|
downloaded,
|
||||||
views,
|
views,
|
||||||
pinned,
|
pinned,
|
||||||
deleted,
|
|
||||||
raw,
|
raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_deleted(
|
||||||
|
pool: asyncpg.Pool, account_id: int, peer_id: int, story_id: int
|
||||||
|
) -> None:
|
||||||
|
await pool.execute(_MARK_DELETED, account_id, peer_id, story_id)
|
||||||
@@ -17,13 +17,18 @@ def story_peer_id(story: Story) -> int:
|
|||||||
|
|
||||||
async def save_story(client: Client, capture: CaptureContext, story: Story) -> None:
|
async def save_story(client: Client, capture: CaptureContext, story: Story) -> None:
|
||||||
peer_id = story_peer_id(story)
|
peer_id = story_peer_id(story)
|
||||||
|
if story.deleted:
|
||||||
|
await repository.mark_deleted(
|
||||||
|
capture.pool, capture.account_id, peer_id, story.id
|
||||||
|
)
|
||||||
|
return
|
||||||
storage_key: str | None = None
|
storage_key: str | None = None
|
||||||
file_size: int | None = None
|
file_size: int | None = None
|
||||||
downloaded = False
|
downloaded = False
|
||||||
stored = await repository.is_downloaded(
|
stored = await repository.is_downloaded(
|
||||||
capture.pool, capture.account_id, peer_id, story.id
|
capture.pool, capture.account_id, peer_id, story.id
|
||||||
)
|
)
|
||||||
if not (stored or story.deleted or story.media is None):
|
if not (stored or story.media is None):
|
||||||
data = await download_bytes(client, story)
|
data = await download_bytes(client, story)
|
||||||
if data is not None:
|
if data is not None:
|
||||||
storage_key = capture.storage.put(data)
|
storage_key = capture.storage.put(data)
|
||||||
@@ -43,6 +48,5 @@ async def save_story(client: Client, capture: CaptureContext, story: Story) -> N
|
|||||||
story.views,
|
story.views,
|
||||||
raw_json(story.raw),
|
raw_json(story.raw),
|
||||||
pinned=bool(story.pinned),
|
pinned=bool(story.pinned),
|
||||||
deleted=bool(story.deleted),
|
|
||||||
downloaded=downloaded,
|
downloaded=downloaded,
|
||||||
)
|
)
|
||||||
@@ -191,6 +191,11 @@ def story_file_name(peer_id: int, story_id: int, media_kind: str | None) -> str:
|
|||||||
return f"story_{peer_id}_{story_id}{extension_for(media_kind or 'photo', None)}"
|
return f"story_{peer_id}_{story_id}{extension_for(media_kind or 'photo', None)}"
|
||||||
|
|
||||||
|
|
||||||
|
def sniff_media_kind(head: bytes) -> str:
|
||||||
|
is_video = head[4:8] == b"ftyp" or head.startswith(b"\x1aE\xdf\xa3")
|
||||||
|
return "video" if is_video else "photo"
|
||||||
|
|
||||||
|
|
||||||
def resolve_mime(
|
def resolve_mime(
|
||||||
kind: str | None, mime: str | None, file_name: str | None = None
|
kind: str | None, mime: str | None, file_name: str | None = None
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ async def get_stories(
|
|||||||
"SELECT peer_id, story_id, date, expire_date, caption, media_kind, " # noqa: S608
|
"SELECT peer_id, story_id, date, expire_date, caption, media_kind, " # noqa: S608
|
||||||
"storage_key, downloaded, views, pinned, deleted FROM stories "
|
"storage_key, downloaded, views, pinned, deleted FROM stories "
|
||||||
f"WHERE {where} "
|
f"WHERE {where} "
|
||||||
f"ORDER BY date DESC NULLS LAST LIMIT ${len(params) - 1} "
|
f"ORDER BY date DESC NULLS LAST, story_id DESC LIMIT ${len(params) - 1} "
|
||||||
f"OFFSET ${len(params)}",
|
f"OFFSET ${len(params)}",
|
||||||
*params,
|
*params,
|
||||||
)
|
)
|
||||||
@@ -95,3 +95,16 @@ async def get_story(
|
|||||||
story_id,
|
story_id,
|
||||||
)
|
)
|
||||||
return StoryView(**dict(row)) if row else None
|
return StoryView(**dict(row)) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
async def set_story_kind(
|
||||||
|
pool: asyncpg.Pool, account_id: int, peer_id: int, story_id: int, kind: str
|
||||||
|
) -> None:
|
||||||
|
await pool.execute(
|
||||||
|
"UPDATE stories SET media_kind = $4 "
|
||||||
|
"WHERE account_id = $1 AND peer_id = $2 AND story_id = $3",
|
||||||
|
account_id,
|
||||||
|
peer_id,
|
||||||
|
story_id,
|
||||||
|
kind,
|
||||||
|
)
|
||||||
@@ -24,6 +24,10 @@ class ContentAddressedStorage:
|
|||||||
def get(self, key: str) -> bytes:
|
def get(self, key: str) -> bytes:
|
||||||
return self._path(key).read_bytes()
|
return self._path(key).read_bytes()
|
||||||
|
|
||||||
|
def head(self, key: str, size: int = 16) -> bytes:
|
||||||
|
with self._path(key).open("rb") as file:
|
||||||
|
return file.read(size)
|
||||||
|
|
||||||
def exists(self, key: str) -> bool:
|
def exists(self, key: str) -> bool:
|
||||||
return self._path(key).exists()
|
return self._path(key).exists()
|
||||||
|
|
||||||
|
|||||||
@@ -159,6 +159,12 @@ html.theme-transition * {
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-size: max(1em, 1rem) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-menu-content {
|
.bg-menu-content {
|
||||||
|
|||||||
Reference in new issue
Block a user