Skip to content

Commit 582b242

Browse files
authored
bug: an unlisted workshop map can be added to the map pool, read from its workshop page (#964)
1 parent 8f7a77e commit 582b242

2 files changed

Lines changed: 121 additions & 1 deletion

File tree

‎server/api/get-workshop-map.post.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,45 @@ export default defineEventHandler(async (event) => {
1919

2020
const map = data.response.publishedfiledetails.at(0);
2121
if (!map.title) {
22-
return null;
22+
return await unlistedWorkshopMap(workshopMapId);
2323
}
2424

2525
return map;
2626
} catch (error) {
2727
console.error("error verifying workshop map:", error);
2828
}
2929
});
30+
31+
// Steam's API answers an unlisted item with result 9, the same as a missing or
32+
// private one, but its workshop page is open to anyone with the link.
33+
async function unlistedWorkshopMap(workshopMapId: string) {
34+
if (!/^\d+$/.test(workshopMapId)) {
35+
return null;
36+
}
37+
38+
const response = await fetch(
39+
`https://steamcommunity.com/sharedfiles/filedetails/?id=${workshopMapId}`,
40+
{ signal: AbortSignal.timeout(10_000) },
41+
);
42+
const html = await response.text();
43+
const title = html.match(/class="workshopItemTitle">([^<]+)</)?.[1];
44+
45+
if (
46+
!title ||
47+
!new RegExp(`ShowAddToCollection\\(\\s*'${workshopMapId}',\\s*'730'`).test(
48+
html,
49+
)
50+
) {
51+
return null;
52+
}
53+
54+
return {
55+
publishedfileid: workshopMapId,
56+
title: title.replace(/&(#\d+|amp|lt|gt|quot);/g, (_, entity: string) =>
57+
entity.startsWith("#")
58+
? String.fromCharCode(Number(entity.slice(1)))
59+
: { amp: "&", lt: "<", gt: ">", quot: '"' }[entity]!,
60+
),
61+
preview_url: html.match(/<meta property="og:image" content="([^"?]+)/)?.[1],
62+
};
63+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// @vitest-environment node
2+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
// The parts of a real workshop page the lookup reads.
5+
const workshopPage = (id: string, title: string, app = "730") => `
6+
<meta property="og:image" content="https://images.steamusercontent.com/ugc/1/${id}/?imw=512&amp;&amp;ima=fit">
7+
<div class="workshopItemTitle">${title}</div>
8+
<span onclick="ShowAddToCollection( '${id}', '${app}' );" id="AddToCollectionBtn"></span>`;
9+
10+
let details: Record<string, unknown>;
11+
let page: string;
12+
let pageFetches: Array<string>;
13+
14+
beforeEach(() => {
15+
vi.resetModules();
16+
pageFetches = [];
17+
vi.stubGlobal("defineEventHandler", (handler: unknown) => handler);
18+
vi.stubGlobal("readBody", async (event: any) => event.body);
19+
vi.stubGlobal("fetch", async (url: string) => {
20+
if (url.startsWith("https://steamcommunity.com/")) {
21+
pageFetches.push(url);
22+
return new Response(page);
23+
}
24+
return new Response(
25+
JSON.stringify({ response: { publishedfiledetails: [details] } }),
26+
);
27+
});
28+
});
29+
30+
afterEach(() => {
31+
vi.unstubAllGlobals();
32+
});
33+
34+
async function lookup(workshopMapId: string) {
35+
const handler = (await import("~/server/api/get-workshop-map.post"))
36+
.default as any;
37+
return handler({ body: { workshop_map_id: workshopMapId } });
38+
}
39+
40+
describe("the workshop map lookup", () => {
41+
it("returns a public item from Steam's API", async () => {
42+
details = {
43+
publishedfileid: "3070290240",
44+
result: 1,
45+
title: "Brewery",
46+
preview_url: "https://images.steamusercontent.com/ugc/2/brewery/",
47+
};
48+
49+
expect(await lookup("3070290240")).toEqual(details);
50+
expect(pageFetches).toEqual([]);
51+
});
52+
53+
// Steam's API answers an unlisted item with result 9, as it does a missing one.
54+
it("reads an unlisted item from its workshop page", async () => {
55+
details = { publishedfileid: "3820018883", result: 9 };
56+
page = workshopPage("3820018883", "de_lbt_mill &amp; co");
57+
58+
expect(await lookup(" 3820018883 ")).toEqual({
59+
publishedfileid: "3820018883",
60+
title: "de_lbt_mill & co",
61+
preview_url: "https://images.steamusercontent.com/ugc/1/3820018883/",
62+
});
63+
});
64+
65+
it("finds nothing when the workshop page is Steam's error page", async () => {
66+
details = { publishedfileid: "1", result: 9 };
67+
page = "<title>Steam Community :: Error</title>";
68+
69+
expect(await lookup("1")).toBeNull();
70+
});
71+
72+
it("never puts anything but a numeric id into the page link", async () => {
73+
details = { result: 9 };
74+
page = workshopPage("1", "de_dust2");
75+
76+
expect(await lookup("1&id=1")).toBeNull();
77+
expect(pageFetches).toEqual([]);
78+
});
79+
80+
it("finds nothing when the page is another game's item", async () => {
81+
details = { publishedfileid: "42", result: 9 };
82+
page = workshopPage("42", "Some Mod", "4000");
83+
84+
expect(await lookup("42")).toBeNull();
85+
});
86+
});

0 commit comments

Comments
 (0)