Clarify group-chat scraping and refresh puppet URLs via the core
This commit is contained in:
@@ -40,6 +40,11 @@ mobile draft at 390px.
|
|||||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||||
|
- [x] C32 Group-chat clarity and puppet refresh. Group DM scrapes already worked (live-verified
|
||||||
|
against the running daemon: `zıbb & fred` and a 1:1 DM, backend `puppet`, done, previews
|
||||||
|
cached); the Group chat tab had shown an empty list while the core was offline. It now says so
|
||||||
|
explicitly and offers Retry. Expired attachment URLs in DM/group-DM scrapes refresh through
|
||||||
|
the core bridge (`message.fetch` with `before: id+1`, which returns exactly the target).
|
||||||
- [x] C31 Batched previews. The preview pass now runs in batches (default 25) with a cooldown
|
- [x] C31 Batched previews. The preview pass now runs in batches (default 25) with a cooldown
|
||||||
(default 60 s), configurable via `VESKSLOP_MEDIA_PREVIEW_BATCH` and
|
(default 60 s), configurable via `VESKSLOP_MEDIA_PREVIEW_BATCH` and
|
||||||
`VESKSLOP_MEDIA_PREVIEW_BATCH_PAUSE_MS`; batching counts rows, not successes. Live log:
|
`VESKSLOP_MEDIA_PREVIEW_BATCH_PAUSE_MS`; batching counts rows, not successes. Live log:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface ScrapeBackend {
|
|||||||
| Backend | Implementation |
|
| Backend | Implementation |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `slopbot` | discord.js REST (`channels.messages.fetch`), bucket-aware client |
|
| `slopbot` | discord.js REST (`channels.messages.fetch`), bucket-aware client |
|
||||||
| `puppet` | The core action `message.fetch` over the bridge |
|
| `puppet` | The core action `message.fetch` over the bridge. Expired URLs refresh with `before: id+1`, which returns exactly the target message |
|
||||||
|
|
||||||
The daemon picks `slopbot` when the channel is a guild channel and slopbot is configured and in the
|
The daemon picks `slopbot` when the channel is a guild channel and slopbot is configured and in the
|
||||||
guild; otherwise `puppet`. A scrape records the backend it used. The walk can resume on the same
|
guild; otherwise `puppet`. A scrape records the backend it used. The walk can resume on the same
|
||||||
|
|||||||
+19
-5
@@ -118,8 +118,10 @@
|
|||||||
document.querySelector("#scope-bot")?.classList.remove("active");
|
document.querySelector("#scope-bot")?.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChannels() {
|
async function loadChannels(force = false) {
|
||||||
if (state.loadedChannels) return;
|
if (state.loadedChannels && !force) return;
|
||||||
|
state.dmsError = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await window.VK.api.get("/api/state");
|
const data = await window.VK.api.get("/api/state");
|
||||||
state.guilds = data?.guilds ?? [];
|
state.guilds = data?.guilds ?? [];
|
||||||
@@ -150,14 +152,16 @@
|
|||||||
label: `${dm.name || names || "DM"} (${dm.type === 3 ? "group DM" : "DM"})`
|
label: `${dm.name || names || "DM"} (${dm.type === 3 ? "group DM" : "DM"})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
state.dms = [];
|
state.dms = [];
|
||||||
|
state.dmsError = error.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.loadedChannels = true;
|
state.loadedChannels = true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
state.guilds = [];
|
state.guilds = [];
|
||||||
state.channels = [];
|
state.channels = [];
|
||||||
|
state.dmsError = error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,6 +848,16 @@
|
|||||||
tree.replaceChildren();
|
tree.replaceChildren();
|
||||||
|
|
||||||
if (scrapeTab === "group") {
|
if (scrapeTab === "group") {
|
||||||
|
if (state.dmsError) {
|
||||||
|
hint.textContent = `Group chats need the core client: ${state.dmsError}`;
|
||||||
|
tree.append(
|
||||||
|
button("Retry", () => {
|
||||||
|
void loadChannels(true).then(renderScrapeTree);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const groups = state.dms.filter(dm => dm.type === 3);
|
const groups = state.dms.filter(dm => dm.type === 3);
|
||||||
hint.textContent = groups.length ? "Group chats use the puppet account." : "No cached group chats yet.";
|
hint.textContent = groups.length ? "Group chats use the puppet account." : "No cached group chats yet.";
|
||||||
for (const dm of groups) {
|
for (const dm of groups) {
|
||||||
@@ -881,7 +895,7 @@
|
|||||||
function openScrapeModal() {
|
function openScrapeModal() {
|
||||||
renderScrapeTree();
|
renderScrapeTree();
|
||||||
scrapeModal.hidden = false;
|
scrapeModal.hidden = false;
|
||||||
void loadChannels().then(renderScrapeTree);
|
void loadChannels(true).then(renderScrapeTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [tab, id] of [
|
for (const [tab, id] of [
|
||||||
|
|||||||
+69
-35
@@ -425,63 +425,97 @@ export class MediaService {
|
|||||||
const meta = this.engine.get(scrapeId);
|
const meta = this.engine.get(scrapeId);
|
||||||
const item = this.getMediaItem(scrapeId, itemId);
|
const item = this.getMediaItem(scrapeId, itemId);
|
||||||
if (!meta || !item) return null;
|
if (!meta || !item) return null;
|
||||||
if (meta.backend !== "slopbot" || !meta.guildId || !this.bot) return null;
|
|
||||||
|
|
||||||
return this.refreshUrls(meta.id, meta.channelId, item);
|
return this.refreshUrls(meta, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Re-fetch the source message and update expired URLs. Returns the fresh playable URL. */
|
/**
|
||||||
private async refreshUrls(
|
* Re-fetch the source message and update expired URLs. Returns the fresh playable URL.
|
||||||
scrapeId: string,
|
* Slopbot scrapes use the bot REST; DM and group-DM scrapes go through the core bridge.
|
||||||
channelId: string,
|
*/
|
||||||
item: MediaRow
|
private async refreshUrls(meta: ScrapeMeta, item: MediaRow): Promise<string | null> {
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
try {
|
||||||
const store = new ScrapeStore(scrapeDir(this.rootDir, scrapeId));
|
const store = new ScrapeStore(scrapeDir(this.rootDir, meta.id));
|
||||||
let messageId: string | null = null;
|
let messageId: string | null = null;
|
||||||
try {
|
try {
|
||||||
messageId = store.sourceMessageId(item.id);
|
messageId = store.sourceMessageId(item.id);
|
||||||
} finally {
|
} finally {
|
||||||
store.close();
|
store.close();
|
||||||
}
|
}
|
||||||
if (!messageId || !this.bot) return null;
|
if (!messageId) return null;
|
||||||
|
|
||||||
const channel = await this.bot.channels.fetch(channelId);
|
|
||||||
if (!channel?.isTextBased()) return null;
|
|
||||||
const message = await channel.messages.fetch(messageId);
|
|
||||||
|
|
||||||
const path = (url: string) => url.split("?")[0];
|
const path = (url: string) => url.split("?")[0];
|
||||||
const attachment = [...message.attachments.values()].find(
|
const saveAndReturn = (update: { url?: string; playUrl?: string | null }, fresh: string) => {
|
||||||
candidate => path(candidate.url) === path(item.url)
|
const writer = new ScrapeStore(scrapeDir(this.rootDir, meta.id));
|
||||||
);
|
|
||||||
if (attachment) {
|
|
||||||
const update = new ScrapeStore(scrapeDir(this.rootDir, scrapeId));
|
|
||||||
try {
|
try {
|
||||||
update.updateUrls(item.id, { url: attachment.url });
|
writer.updateUrls(item.id, update);
|
||||||
} finally {
|
} finally {
|
||||||
update.close();
|
writer.close();
|
||||||
|
}
|
||||||
|
return fresh;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.bot && meta.guildId && meta.backend === "slopbot") {
|
||||||
|
const channel = await this.bot.channels.fetch(meta.channelId);
|
||||||
|
if (channel?.isTextBased()) {
|
||||||
|
const message = await channel.messages.fetch(messageId);
|
||||||
|
|
||||||
|
const attachment = [...message.attachments.values()].find(
|
||||||
|
candidate => path(candidate.url) === path(item.url)
|
||||||
|
);
|
||||||
|
if (attachment) return saveAndReturn({ url: attachment.url }, attachment.url);
|
||||||
|
|
||||||
|
const embed = message.embeds.find(
|
||||||
|
candidate =>
|
||||||
|
(item.play_url &&
|
||||||
|
candidate.video?.url &&
|
||||||
|
path(candidate.video.url) === path(item.play_url)) ||
|
||||||
|
(candidate.video?.url && path(candidate.url ?? "") === path(item.url))
|
||||||
|
);
|
||||||
|
if (embed?.video?.url) {
|
||||||
|
return saveAndReturn(
|
||||||
|
{ playUrl: embed.video.url, url: embed.url ?? item.url },
|
||||||
|
embed.video.url
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return attachment.url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const embed = message.embeds.find(
|
// The puppet path: the newest message below id+1 is exactly the target message.
|
||||||
candidate =>
|
const core = this.deps.getCore();
|
||||||
(item.play_url && candidate.video?.url && path(candidate.video.url) === path(item.play_url)) ||
|
if (core && this.deps.isCoreConnected()) {
|
||||||
(candidate.video?.url && path(candidate.url ?? "") === path(item.url))
|
const result = (await core.fetchMessages(meta.channelId, 1, {
|
||||||
);
|
before: (BigInt(messageId) + 1n).toString()
|
||||||
if (embed?.video?.url) {
|
})) as { messages?: Array<Record<string, any>> };
|
||||||
const update = new ScrapeStore(scrapeDir(this.rootDir, scrapeId));
|
const message =
|
||||||
try {
|
result?.messages?.find(candidate => candidate?.id === messageId) ??
|
||||||
update.updateUrls(item.id, { playUrl: embed.video.url, url: embed.url ?? item.url });
|
result?.messages?.[0];
|
||||||
} finally {
|
|
||||||
update.close();
|
if (message) {
|
||||||
|
const attachment = (message.attachments ?? []).find(
|
||||||
|
(candidate: any) => candidate?.url && path(candidate.url) === path(item.url)
|
||||||
|
);
|
||||||
|
if (attachment?.url) return saveAndReturn({ url: attachment.url }, attachment.url);
|
||||||
|
|
||||||
|
const embed = (message.embeds ?? []).find(
|
||||||
|
(candidate: any) =>
|
||||||
|
(item.play_url &&
|
||||||
|
candidate?.videoUrl &&
|
||||||
|
path(candidate.videoUrl) === path(item.play_url)) ||
|
||||||
|
(candidate?.videoUrl && path(candidate.url ?? "") === path(item.url))
|
||||||
|
);
|
||||||
|
if (embed?.videoUrl) {
|
||||||
|
return saveAndReturn(
|
||||||
|
{ playUrl: embed.videoUrl, url: embed.url ?? item.url },
|
||||||
|
embed.videoUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return embed.video.url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.deps.log?.(`refresh failed for ${scrapeId}:${item.id}: ${String(error)}`);
|
this.deps.log?.(`refresh failed for ${meta.id}:${item.id}: ${String(error)}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user