Initial commit
This commit is contained in:
commit
64eb4da894
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
161
edb-id-bot.mjs
Executable file
161
edb-id-bot.mjs
Executable file
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import {execSync} from 'node:child_process';
|
||||||
|
import makeFetchCookie from 'fetch-cookie';
|
||||||
|
|
||||||
|
const fetchWithCookies = makeFetchCookie(fetch);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://stackoverflow.com/a/6969486
|
||||||
|
* @param {string}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const regExpEscape = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a regular expression that matches a link to the named item and
|
||||||
|
* captures its EDB ID from the matched link's `href` attribute.
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {RegExp}
|
||||||
|
*/
|
||||||
|
const itemLinkRegExp = name => new RegExp(`<a href="/lodestone/playguide/db/item/(?<id>[a-z0-9]+)[^"]+"[^>]*>(?<name>${regExpEscape(name)})</a>`, 'i');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ID of the named item in Eorzea Database.
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {Promise<string | undefined>}
|
||||||
|
*/
|
||||||
|
async function findItemID (name) {
|
||||||
|
// execute a search for the item's name
|
||||||
|
const searchURL = `https://na.finalfantasyxiv.com/lodestone/playguide/db/item/?q=${encodeURIComponent(name)}`;
|
||||||
|
const response = await fetchWithCookies(searchURL);
|
||||||
|
const body = await response.text();
|
||||||
|
// find an `<a>` in the HTML response whose text exactly matches the name
|
||||||
|
const match = body.match(itemLinkRegExp(name));
|
||||||
|
// return the ID parsed from the URL in the `href` attribute
|
||||||
|
return match?.groups.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current contents of the named item's wiki page and returns the
|
||||||
|
* contents with the infobox updated to use the given EDB item ID.
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function getWikiPageContents (name) {
|
||||||
|
const response = await fetchWithCookies(`https://ffxiv.consolegameswiki.com/mediawiki/index.php?action=raw&title=${encodeURIComponent(name)}`);
|
||||||
|
const rawContents = await response.text();
|
||||||
|
return rawContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches an empty `id-edb` infobox parameter which can just have a value
|
||||||
|
* inserted after it.
|
||||||
|
*/
|
||||||
|
const existingEmptyEDBIDParameter = /^(\s*\|\s*id-edb\s*=)\s*$/m;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches an `id-gt` infobox parameter above which we can insert an `id-edb`
|
||||||
|
* parameter above. Whitespace in capture groups so the inserted parameter can
|
||||||
|
* use the exact same formatting.
|
||||||
|
*/
|
||||||
|
const existingGTIDParameter = /^(\s*)\|(\s*)id-gt(\s*)=(\s*).*$/m;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts the `id-edb` item infobox parameter into the given page contents.
|
||||||
|
* @param {string} pageContent
|
||||||
|
* @param {string} edbID
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function insertInfoboxEDBID (pageContent, edbID) {
|
||||||
|
if (pageContent.includes(`edb-id = ${edbID}`)) {
|
||||||
|
console.log('what????', pageContent);
|
||||||
|
throw new Error('Page seems to already contain its own EDB ID??');
|
||||||
|
}
|
||||||
|
if (pageContent.match(existingEmptyEDBIDParameter)) {
|
||||||
|
console.log('Page has existing empty `id-edb` parameter');
|
||||||
|
return pageContent.replace(existingEmptyEDBIDParameter, `$1 ${edbID}`);
|
||||||
|
}
|
||||||
|
if (pageContent.match(existingGTIDParameter)) {
|
||||||
|
console.log('Page has `id-gt` parameter, inserting `id-edb` above that');
|
||||||
|
return pageContent.replace(existingGTIDParameter, `$1|$2id-edb$3=$4${edbID}\n$&`);
|
||||||
|
}
|
||||||
|
console.log('bad', pageContent);
|
||||||
|
throw new Error('Dunno how to insert the parameter into this page');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wikiAPI = 'https://ffxiv.consolegameswiki.com/mediawiki/api.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of wiki pages from "Category:Missing EDB ID".
|
||||||
|
* @returns {Promise<{pageid: number; title: string}[]>}
|
||||||
|
*/
|
||||||
|
async function getItemPagesWithNoEDBID () {
|
||||||
|
const response = await fetchWithCookies(`https://ffxiv.consolegameswiki.com/mediawiki/api.php?${new URLSearchParams({
|
||||||
|
action: 'query',
|
||||||
|
list: 'categorymembers',
|
||||||
|
cmlimit: 500,
|
||||||
|
cmtitle: 'Category:Missing EDB ID',
|
||||||
|
format: 'json',
|
||||||
|
})}`);
|
||||||
|
const body = await response.json();
|
||||||
|
if (body.error) {
|
||||||
|
throw new Error(`[${body.error.code}] ${body.error.info}`);
|
||||||
|
}
|
||||||
|
return body.query.categorymembers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** terrible terrible terrible string diff helper for debugging */
|
||||||
|
function diff (a, b) {
|
||||||
|
// base64 input strings before passing to shell to avoid escaping issues
|
||||||
|
// https://stackoverflow.com/a/60221847
|
||||||
|
// also use `|| true` to not throw an error when `diff` returns non-zero
|
||||||
|
execSync(`bash -c '
|
||||||
|
diff --color -u <(echo ${btoa(a)} | base64 -d) <(echo ${btoa(b)} | base64 -d)
|
||||||
|
' || true`, {
|
||||||
|
// display result directly in terminal
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an item name, looks up its EDB ID and edits its wiki page to include
|
||||||
|
* that ID in the item infobox if it doesn't already.
|
||||||
|
*/
|
||||||
|
async function processItem (name) {
|
||||||
|
console.log('Page:', name);
|
||||||
|
const edbID = await findItemID(name);
|
||||||
|
if (!edbID) {
|
||||||
|
console.log('No EDB ID found for this item, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('EDB ID:', edbID, `(https://na.finalfantasyxiv.com/lodestone/playguide/db/item/${encodeURIComponent(edbID)})`);
|
||||||
|
|
||||||
|
let updatedText;
|
||||||
|
try {
|
||||||
|
const originalText = await getWikiPageContents(name);
|
||||||
|
updatedText = insertInfoboxEDBID(originalText, edbID);
|
||||||
|
diff(originalText, updatedText);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
console.log('not doing anything with this item');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: actually submit wiki edit
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemPagesWithoutEDBIDs = await getItemPagesWithNoEDBID();
|
||||||
|
console.log('Looking up EDB IDs of', itemPagesWithoutEDBIDs.length, 'items\n');
|
||||||
|
|
||||||
|
for (const {title} of itemPagesWithoutEDBIDs) {
|
||||||
|
await processItem(title);
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// this runs serially with an artificial delay between requests to decrease
|
||||||
|
// the chance of sqenix sending ninjas to my house
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('done!');
|
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"name": "ffxiv-wiki-edb-id-script",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ffxiv-wiki-edb-id-script",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-cookie": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fetch-cookie": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-s/XhhreJpqH0ftkGVcQt8JE9bqk+zRn4jF5mPJXWZeQMCI5odV9K+wEWYbnzFPHgQZlvPSMjS4n4yawWE8RINw==",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"set-cookie-parser": "^2.4.8",
|
||||||
|
"tough-cookie": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.85",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz",
|
||||||
|
"integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.85"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.85",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz",
|
||||||
|
"integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
package.json
Normal file
7
package.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "ffxiv-wiki-edb-id-script",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-cookie": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue