Compare commits

...
Sign in to create a new pull request.

10 commits
beep ... main

Author SHA1 Message Date
53d48912d0
add stupid shit i just ran 2025-09-24 06:41:38 -04:00
ecedf23c76
this bug was my girlfriend's fault 2025-09-04 23:57:33 -04:00
80bf319378
add script for fixing flavor text formatting from robogurgum
see
2025-09-04 23:55:43 -04:00
a8ad47f830
whoops i didnt want to commit that yet lol 2025-09-04 23:08:53 -04:00
6804184cf1
update items without GT ID category name 2025-09-04 23:08:02 -04:00
ef69ee4c9f
use new fancy logging in edb and gt scripts 2025-09-04 23:07:48 -04:00
ded35a743d
fix potential base64 injection
"EOF" is made up of entirely valid base64 characters, it would be bad if the program crashed because we tried to diff a file whose base64 representation contained the string EOF. underscores don't appear in base64 strings
2025-09-04 23:07:04 -04:00
4f17754f4c
allow custom diff header labels 2025-09-04 23:05:54 -04:00
436acf7299
improve killpage logic
relying on an interval causes the process to never exit, because the interval is still running in the background. this is bad for oneshot scripts like ours. instead the check works by comparing the current date to the last known date any time a post request is sent
2025-09-04 23:04:54 -04:00
d0e5bf33ff
uhh i have no idea how this happened 2025-09-04 22:41:22 -04:00
6 changed files with 178 additions and 30 deletions

View file

@ -47,7 +47,8 @@ const pages = (await Promise.all(Object.entries(categoryTypes).map(async ([categ
console.log('Processing', pages.length, 'items\n');
for (const {title, type} of pages) {
for (const page of pages) {
const {title, type} = page;
// 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, 1000));
@ -73,7 +74,7 @@ for (const {title, type} of pages) {
let updatedText;
try {
updatedText = insertInfoboxEDBID(originalText, edbID);
diff(originalText, updatedText);
diff(originalText, updatedText, page);
} catch (error) {
console.log('Error inserting parameter:', error);
console.group('Bad page content:');

View file

@ -1,8 +1,5 @@
#!/usr/bin/env -S node --env-file-if-exists=.env
console.log(process.env);
process.exit(1);
import {findItemGTID} from '../lib/api/xivapi.js';
import {getMediawikiClient} from '../lib/config.js';
import {diff} from '../lib/util/diff.js';
@ -38,10 +35,11 @@ const mw = await getMediawikiClient();
// TODO: update this to work with the new maintenance category hierarchy
// Get pages in the "Missing internal ID" category from the main article namespace
const itemPagesWithoutGTIDs = await mw.listCategoryPages('Category:Missing internal ID', [0], +process.env.LIMIT || 500);
console.log('Processing', itemPagesWithoutGTIDs.length, 'item pages from [[Category:Missing internal ID]]\n');
const itemPagesWithoutGTIDs = await mw.listCategoryPages('Category:Items with no internal ID specified', [0], +process.env.LIMIT || 500);
console.log('Processing', itemPagesWithoutGTIDs.length, 'item pages from [[Category:Items with no internal ID specified]]\n');
for (const {title} of itemPagesWithoutGTIDs) {
for (const page of itemPagesWithoutGTIDs) {
const {title} = page;
console.log('Page:', title);
// look up on XIVAPI
let gtID;
@ -68,7 +66,7 @@ for (const {title} of itemPagesWithoutGTIDs) {
let updatedText;
try {
updatedText = insertInfoboxGTBID(originalText, gtID);
diff(originalText, updatedText);
diff(originalText, updatedText, page);
} catch (error) {
console.log('Error inserting parameter:', error);
console.group('Bad page content:');

View file

@ -0,0 +1,60 @@
#!/usr/bin/env -S node --env-file-if-exists=.env
// Adds `quest-sync` parameters to quest infoboxes that don't have them
import { findEDBQuestSync } from '../../lib/api/lodestone.js';
import {getMediawikiClient} from '../../lib/config.js';
import {diff} from '../../lib/util/diff.js';
import { addParameterBesideExistingParameter } from '../../lib/util/template-parameters.js';
const mw = await getMediawikiClient();
const allquestcats = `
Category:Main Scenario quests
Category:Side Story quests
Category:Guildleves
Category:Sidequests
Category:other quests
Category:Daily quests
Category:Feature quests
Category:Repeatable Feature quests
Category:Quasi-quests
Category:Feature quasi-quests
Category:Quests with no type specified
`.split('\n').filter(s => s);
console.log(allquestcats)
const pages = (await Promise.all(allquestcats.map(category => mw.listCategoryPages(category, [0], 'max')))).flat();
console.log(pages.length);
function insertQuestSync (pageContent) {
if (pageContent.includes('quest-sync')) {
throw new Error('Page already contains a `quest-sync` parameter');
}
let result = addParameterBesideExistingParameter(pageContent, 'quest-sync', 'true', 'after', 'level');
if (result == null) throw new Error('Failed to add parameter');
return result;
}
const start = parseInt(process.env.START ?? '0', 10);
if (start) {
console.log('Starting from item index', start);
pages.splice(0, start);
}
for (const [i, {title}] of Object.entries(pages)) {
console.log(start+parseInt(i), title);
const content = await mw.readPage(title);
const hasQuestSync = await findEDBQuestSync(title.replace(' (Quest)', ''));
if (!hasQuestSync) continue;
let newContent;
try {
newContent = insertQuestSync(content);
} catch (error) {
console.error(error);
continue;
}
diff(content, newContent);
await mw.editPage(title, newContent, 'Add quest-sync parameter', true);
}

View file

@ -0,0 +1,52 @@
#!/usr/bin/env -S node --env-file-if-exists=.env
// Fixes flavor text formatting on files robogurgum created for patches 7.3/7.31
// see https://discord.com/channels/312060255469305856/1412850908668100810
import {getMediawikiClient} from '../../lib/config.js';
import {diff} from '../../lib/util/diff.js';
const mw = await getMediawikiClient();
function fixPageContent (pageContent) {
const flavorTextRegexp = /\| flavor-text = ([\s\S]*?)\n\|/;
let match = pageContent.match(flavorTextRegexp);
if (!match) return pageContent;
let flavorText = match[1];
if (!flavorText.trim()) return pageContent;
let newFlavorText = flavorText
// linebreaks with
// - no <br> or <br /> etc before or after
// - no {{action fact}} after (that renders a line break at the start)
.replace(/(?<!<br\s*\/?>)\n(?!<br\s*\/?>)(?!{{action fact|\s*$)/gi, '<br>\n')
// spans that set color via inline style instead of using {{colorize}}
.replace(/<span style="color: #00cc22;">([^<]+)<\/span>/g, '`$1`')
.replace(/<span style="color: #ffff66;">([^<]+)<\/span>/g, '``$1``')
.replace(/<span style="color: #ff7b1a;">([^<]+)<\/span>/g, '```$1```')
// convert lunar/phaenna credit bonuses to use {{action fact}}, but only
// when we can be sure it won't add extra line breaks
.replace(
/<br\s*\/?>(\n?)`([\w\d _-]+)(?<=Credit Bonus)(?::`|`:) +([^\n<]+)/g,
'$1{{action fact|$2|$3}}',
)
// flavor-text is automatically colorized, remove redundant invocations
.replace(/{{colorize\|([^|{}]+)}}/g, '$1');
return pageContent.replace(flavorTextRegexp, `| flavor-text = ${newFlavorText}\n|`);
}
const pages = await mw.listUserContribs('RoboGurgum', {namespaces: [0], show: 'new', limit: 1000});
for (const page of pages) {
const {title} = page;
const pageContent = await mw.readPage(title);
let fixedContent = fixPageContent(pageContent);
if (fixedContent === pageContent) {
console.log('#', title, ' No change\n');
continue;
}
diff(pageContent, fixedContent, page);
console.log();
await mw.editPage(title, fixedContent, 'Fix flavor text formatting', true);
}

View file

@ -27,9 +27,26 @@ export class MediaWikiClient {
constructor (wikiURL, {killPage}) {
this.wikiURL = wikiURL;
this.killPage = killPage;
this.lastKillPageCheck = 0; // first check is triggered after login
this.fetch = makeFetchCookie(fetch);
}
/** Checks the kill page if needed, killing the process if not empty. */
async tryKillPageCheck () {
if (!this.killPage) return;
// wait at least 10 seconds since the last check
if (Date.now() > this.lastKillPageCheck + 10 * 1000) return;
this.lastKillPageCheck = Date.now();
const response = await this.fetch(`${this.wikiURL}/index.php?action=raw&title=${encodeURIComponent(this.killPage)}`);
const content = await response.text();
if (content.trim()) {
console.error('*** Kill page is not empty; stopping ***\n');
console.error(content);
process.exit(1);
}
};
/**
* Makes a GET request against `index.php`.
* @param {Record<string, string>} params Query string parameters
@ -71,6 +88,7 @@ export class MediaWikiClient {
* @returns {Promise<any>}
*/
async fetchApiPost (params, options = {}) {
await this.tryKillPageCheck();
const response = await this.fetch(`${this.wikiURL}/api.php`, {
...options,
method: 'POST',
@ -128,20 +146,6 @@ export class MediaWikiClient {
if (body.login.result === 'Failed') {
throw new Error(body.login.reason);
}
if (this.killPage) {
// start the kill page check loop as soon as we're logged in
const checkKillPage = async () => {
let content = await this.readPage(this.killPage);
if (content.trim()) {
console.error('*** Kill page is not empty; stopping ***\n');
console.error(content);
process.exit(1);
}
setTimeout(checkKillPage, 30 * 1000); // every 30 seconds
};
checkKillPage();
}
}
/**
@ -239,4 +243,28 @@ export class MediaWikiClient {
});
return body.query.categorymembers;
}
/**
* Gets the list of a user's contributions.
* @param {string} username Name of the user whose contribs should be fetched
* @param {object} options
* @param {number | number[] | '*'} [options.namespaces] List of namespaces from which to return results
* @param {number} [options.limit] Maximum number of items to return
* @param {string} [options.show] See the documentation of `ucshow` at https://www.mediawiki.org/wiki/API:Usercontribs
* @returns {Promise<{pageid: string; revid: string; timestamp: string; title: string}[]>}
*/
async listUserContribs (username, {namespaces = '*', limit = 50, show}) {
if (Array.isArray(namespaces)) {
namespaces = namespaces.join('|');
}
const body = await this.fetchApiGet({
action: 'query',
list: 'usercontribs',
ucuser: username,
uclimit: limit,
ucnamespace: namespaces,
ucshow: show,
});
return body.query.usercontribs;
}
}

View file

@ -13,17 +13,26 @@ const toBase64 = str => Buffer.from(str, 'utf-8').toString('base64');
* terrible terrible terrible string diff helper for debugging
* @param {string} a
* @param {string} b
* @param {object} page Page object from the API, used to generate diff labels
*/
export function diff (a, b) {
export function diff (a, b, page) {
let labels = null;
if (page) { // generate diff labels based on
labels = [
`${page.title} (${page.revid ? `revision ${page.revid}` : 'current revision'}${page.timestamp ? ` from ${page.timestamp}` : ''})`,
`${page.title} (pending edit)`,
];
}
// base64 input strings before passing to shell to avoid escaping issues
// https://stackoverflow.com/a/60221847
// use tail to cut off useless file info lines
execSync(String.raw`bash <<- EOF
// use tail to cut off file info lines and re-add with fake filenames/dates
// this function is so extra now holy shit
execSync(String.raw`bash <<- '_EOF_'
diff --color=always -u \
<(echo "${toBase64(a)}" | base64 -d) \
<(echo "${toBase64(b)}" | base64 -d) \
| tail -n +3
EOF`, {
${labels ? `--label="$(echo '${toBase64(labels[0])}' | base64 -d)" ` : ''}<(echo '${toBase64(a)}' | base64 -d) \
${labels ? `--label="$(echo '${toBase64(labels[1])}' | base64 -d)" ` : ''}<(echo '${toBase64(b)}' | base64 -d) \
${labels ? `|| true` : `| tail -n +3` /* cut off header if no labels */}
_EOF_`, {
// display result directly in terminal
stdio: 'inherit',
});