fix(onebot): keep a known media URL when a re-index carries none
Quoted images reach the media indexer with only their hash, so the stored record was overwritten with an empty url and get_image handed callers an empty address plus a synthetic <md5>.png name that no download can satisfy. Reuse the URL already stored for the same object when the incoming record has none, and let get_image sign a record whose address lives on url instead of imageUrl.
This commit is contained in:
@@ -235,7 +235,7 @@ export class MediaStore {
|
||||
throw new Error(`cannot store ${type} metadata without an identifier`);
|
||||
}
|
||||
|
||||
const data = serializeMediaInfo(info);
|
||||
const data = serializeMediaInfo(await this.mergeStoredUrls(type, primary, info));
|
||||
await this.db.exec('BEGIN IMMEDIATE');
|
||||
|
||||
try {
|
||||
@@ -323,6 +323,64 @@ export class MediaStore {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Re-indexing an object without a download address — a quoted or forwarded
|
||||
* image often carries only its hash — must not erase an address already
|
||||
* proven for the same object. `get_image` hands `url` straight to its
|
||||
* caller, so a dropped address turns a known picture into an unusable
|
||||
* record that the caller retries until it gives up.
|
||||
*/
|
||||
private async mergeStoredUrls(type: string, primary: string, info: object): Promise<object> {
|
||||
const next: Record<string, unknown> = { ...(info as Record<string, unknown>) };
|
||||
const missing = (['url', 'imageUrl'] as const).filter(
|
||||
(field) => field in next && (typeof next[field] !== 'string' || !next[field]),
|
||||
);
|
||||
|
||||
if (missing.length === 0) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const stored = await this.readStoredUrls(type, primary);
|
||||
|
||||
for (const field of missing) {
|
||||
if (stored[field]) {
|
||||
next[field] = stored[field];
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Direct row read: `remember` already runs inside the serial queue. */
|
||||
private async readStoredUrls(
|
||||
type: string,
|
||||
primary: string,
|
||||
): Promise<{ url: string; imageUrl: string }> {
|
||||
const empty = { url: '', imageUrl: '' };
|
||||
const row = (await this.findEntry.get(type, type, primary, primary)) as
|
||||
| { data: string }
|
||||
| undefined;
|
||||
|
||||
if (!row) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(row.data);
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return empty;
|
||||
}
|
||||
|
||||
const stored = parsed as Record<string, unknown>;
|
||||
return {
|
||||
url: typeof stored.url === 'string' ? stored.url : '',
|
||||
imageUrl: typeof stored.imageUrl === 'string' ? stored.imageUrl : '',
|
||||
};
|
||||
} catch {
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeMediaInfo(info: object): string {
|
||||
|
||||
@@ -148,14 +148,17 @@ async function resolveImageUrl(
|
||||
cached: CachedImage | null,
|
||||
resolver?: ImageUrlResolver | null,
|
||||
): Promise<string | null> {
|
||||
if (!cached?.imageUrl || !resolver) {
|
||||
// get_image returns `url` ahead of `imageUrl`, so a record indexed from
|
||||
// segment data can carry its address only on `url`. That address still
|
||||
// needs signing when its rkey has expired; ignoring it here handed the
|
||||
// caller back an unrefreshed URL.
|
||||
const source = (cached?.imageUrl || cached?.url || '').trim();
|
||||
|
||||
if (!cached || !source || !resolver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolver(
|
||||
{ type: 'image', imageUrl: cached.imageUrl, subType: cached.subType },
|
||||
cached.isGroup,
|
||||
);
|
||||
return resolver({ type: 'image', imageUrl: source, subType: cached.subType }, cached.isGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,6 +119,41 @@ describe('MediaStore basic semantics', () => {
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('keeps a stored download URL when a later index carries none', async () => {
|
||||
const store = open('img-url-retain');
|
||||
|
||||
await store.rememberImage({
|
||||
file: 'abc.png',
|
||||
url: 'https://example.com/abc.png?rkey=live',
|
||||
fileSize: 4242,
|
||||
fileName: 'abc.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: 'https://example.com/abc.png?rkey=live',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
|
||||
// A quoted copy reaches the store with only the hash and no address.
|
||||
await store.rememberImage({
|
||||
file: 'abc.png',
|
||||
url: '',
|
||||
fileSize: 4242,
|
||||
fileName: 'abc.png',
|
||||
subType: 0,
|
||||
summary: '',
|
||||
imageUrl: '',
|
||||
isGroup: true,
|
||||
sessionId: GROUP_ID,
|
||||
});
|
||||
|
||||
const cached = await store.findImage('abc.png');
|
||||
|
||||
expect(cached?.url).toBe('https://example.com/abc.png?rkey=live');
|
||||
expect(cached?.imageUrl).toBe('https://example.com/abc.png?rkey=live');
|
||||
await store.close();
|
||||
});
|
||||
|
||||
it('indexes records by file, fileName, fileId, and url', async () => {
|
||||
const store = open('rec-keys');
|
||||
await store.rememberRecord({
|
||||
|
||||
Reference in New Issue
Block a user