wikidata-bot/src/main.ts
2023-05-28 19:45:45 +01:00

149 lines
4.4 KiB
TypeScript

import wikibaseEdit from "wikibase-edit"
import wikibaseSDK, { SparqlResults } from "wikibase-sdk"
import fetch from "node-fetch"
import { config as loadDotEnv } from "dotenv"
import { bool, cleanEnv, num, str, url } from "envalid"
/** == TYPES == */
type ID = string
type GUID = `${ID}$${string}`
// The fields here will depend on the SELECT statement in incorrectStatementsQuery
interface QueryResultItem {
item: ID
statement: GUID
}
/** == HELPER FUNCTIONS == */
/** Generates a wikidata.org URL that points to the provided item, property, or statement */
function wikidataURL(item: ID, property?: ID | null, statement?: GUID) {
const url = new URL("https://www.wikidata.org")
const path = ["wiki", item]
url.pathname = path.join("/")
if (property) url.hash = property
if (statement) url.hash = statement
return url
}
async function fetchSparqlResults<TResult>(query: string) {
const url = wbk.sparqlQuery(query)
const results = (await fetch(url).then((res) => res.json())) as SparqlResults
const simplifiedResults = wbk.simplify.sparqlResults(results)
return simplifiedResults as TResult[]
}
/** Prints a summary of the completed operations. Called at the end of the script. */
function printStatistics(status: string) {
const items = editedItems.size
console.log(`${status}: Made ${edits} edits to ${items} items.`)
}
/** == LOAD AND VALIDATE ENVIRONMENT VARIABLES == */
loadDotEnv()
const env = cleanEnv(process.env, {
WIKIDATA_USERNAME: str(),
WIKIDATA_PASSWORD: str(),
WIKIDATA_INSTANCE: url(),
SPARQL_ENDPOINT: url(),
WIKIDATA_BOT: bool({ default: false }),
WIKIDATA_EDIT_SUMMARY: str(),
WIKIDATA_MAX_EDITS: num(),
DRY_RUN: bool({ default: false }),
})
/** == THE IMPORTANT CONFIGURATION == */
// The property that we are working with
const targetProperty = "P1641" // P1641 "port"
// The qualifier properties that we are working with
const oldQualifier = "P642" // P642 "of"
const newQualifier = "P2700" // P2700 "protocol"
// The SPARQL query for the statements that we need to modify
const incorrectStatementsQuery = `
SELECT ?item ?statement WHERE {
?item p:${targetProperty} ?statement.
?statement pq:${oldQualifier} ?protocol.
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}
`
/** == WIKIDATA API INTERACTION == */
// Configure the wikibase-sdk API client
const wbk = wikibaseSDK({
instance: env.WIKIDATA_INSTANCE,
sparqlEndpoint: env.SPARQL_ENDPOINT,
})
// Configure the wikibase-edit API client
const wbEdit = wikibaseEdit({
instance: env.WIKIDATA_INSTANCE,
credentials: {
username: env.WIKIDATA_USERNAME,
password: env.WIKIDATA_PASSWORD,
},
bot: !!env.WIKIDATA_BOT,
summary: env.WIKIDATA_EDIT_SUMMARY,
})
// Get the list of statements that we'll edit
const statements = await fetchSparqlResults<QueryResultItem>(
incorrectStatementsQuery
)
const targetStatements = statements.slice(0, env.WIKIDATA_MAX_EDITS)
// Track some statistics to be printed at the end
const editedItems = new Set<ID>()
let edits = 0
for (const { item, statement } of targetStatements) {
try {
if (!env.DRY_RUN) {
// Swap the old qualifier for the new qualifier
await wbEdit.qualifier.move({
guid: statement,
oldProperty: oldQualifier,
newProperty: newQualifier,
})
}
} catch (error) {
if (
error instanceof Error &&
error.message === "no qualifiers found for this property"
) {
// This error occurs when there's an statement in targetStatements that doesn't have the old qualifier.
// In other words, the property has qualifier already been updated since the query was run.
// This will happen if there are multiple qualifier triples that use the same propriety,
// since they both get updated at once when the script finds the first qualifier, leaving it with
// nothing to change when it tries to update the second qualifier.
// Could theoretically also be caused by someone removing the old qualifier during the script's runtime.
continue
}
printStatistics("Exited due to an error")
console.error(`🔥 Failed to edit ${item}. See below for error.`)
debugger
// Stop executing the script if we encounter an error
throw error
}
editedItems.add(item)
edits++
const url = wikidataURL(item, null, statement)
console.log(`✅ Updated ${item} (${url})`)
// debugger
}
printStatistics("🎉 Finished successfully")