Skip to content

Instantly share code, notes, and snippets.

@ggorlen
Last active August 26, 2023 18:25
Show Gist options
  • Select an option

  • Save ggorlen/1bbcc4841af230d9de9fb214e8ead36f to your computer and use it in GitHub Desktop.

Select an option

Save ggorlen/1bbcc4841af230d9de9fb214e8ead36f to your computer and use it in GitHub Desktop.
codewars tagger
// This script programmatically adjusts tags on Codewars kata
// list of untagged kata: https://www.codewars.com/kata/search?untagged
const puppeteer = require("puppeteer"); // ^20.2.0
let browser;
(async () => {
////////////////////////////////////////////////////////////////////
// SETUP STEP 1: Set headless: false temporarily and log in manually.
// Credentials will be stored in the browserdata dir.
// After you're logged in, go back to headless: true (or "new").
browser = await puppeteer.launch({
headless: true,
userDataDir: "browserdata",
});
// SETUP STEP 2: Manually create an empty collection
// and fill in the information below.
// This doesn't work in parallel; I would have to
// create a new unique list per operation to do so,
// which is possible, but fussy and unreliable.
const collectionId = "62bf1595222562003e960b5b";
const collectionName = "adjust-tags";
// SETUP STEP 3: configure the kata and the tag operation.
// (these should eventually be moved to params, args or file input)
const kataId = "6459aa9aedc6d8004b348a16";
const postAction = {
op: "add", // "add" or "remove" tag(s)
// tags from https://www.codewars.com/tags lowercased:
tags: ["algorithms", "strings"],
};
////////////////////////////////////////////////////////////////////
const baseUrl = "https://www.codewars.com/";
// navigate to the kata page and add it to the dummy collection
const [page] = await browser.pages();
await page.goto(baseUrl + "kata/" + kataId, {
waitUntil: "domcontentloaded",
});
const addToColl = await page.waitForSelector(
".js-add-to-collection"
);
await addToColl.click();
// poll until the modal opens and find the
// row that matches our dummy collection
const el = await page.waitForFunction(
collectionId =>
[...document.querySelectorAll(".collections li")].find(
el =>
el
.querySelector("a")
.href.includes(collectionId)
),
{timeout: 10_000},
collectionId
);
// click the button to add the item to the collection
await el.evaluate(el => {
if (
el
.querySelector(".btn")
?.textContent.toLowerCase()
.trim() === "add"
) {
el.querySelector(".btn").click();
}
});
// wait for the 'add' button to change to 'remove'
await page.waitForFunction(
collectionId =>
[...document.querySelectorAll(".collections li")]
.find(el =>
el
.querySelector(".info-row a")
.href.includes(collectionId)
)
?.textContent.toLowerCase()
.includes("remove"),
{timeout: 10_001},
collectionId
);
// navigate to the collection and check to see if
// the removal button for the item we added is ready
await page.goto(baseUrl + "collections/" + collectionName, {
waitUntil: "domcontentloaded",
});
let removeBtn;
try {
removeBtn = await page.waitForSelector(".remove.js-remove", {
timeout: 5_000,
});
} catch (err) {
// if navigation beat the element, try refreshing
await page.goto(baseUrl + "collections/" + collectionName, {
waitUntil: "domcontentloaded",
});
removeBtn = await page.waitForSelector(".remove.js-remove", {
timeout: 5_000,
});
}
// ensure there is exactly 1 item in the collection
const hasMultipleButtons = await page.evaluate(
() =>
document.querySelectorAll(".remove.js-remove").length !== 1
);
if (hasMultipleButtons) {
throw Error("Found multiple challenges in the collection");
}
// click the add tags button and inject our POST action
// rather than messing with tagging in the DOM
let resolve = null;
const reqP = new Promise(res => {
resolve = res;
});
const handleRequest = async request => {
if (/\/item-tags$/.test(request.url())) {
const data = {
method: "POST",
headers: {...request.headers()},
postData: JSON.stringify({
...JSON.parse(request.postData()),
...postAction,
}),
};
request.continue(data);
resolve();
} else {
request.continue();
}
};
await page.setRequestInterception(true);
page.on("request", handleRequest);
const addBtn = await page.waitForSelector(
'[data-action="click->collection#addTags"]'
);
await addBtn.click();
await reqP;
page.off("request", handleRequest);
await page.setRequestInterception(false);
// remove the new kata; it's temporary
await removeBtn.evaluate(el => el.click());
await page.evaluate(() =>
document.querySelector(".remove.js-remove")?.click()
);
const confirmBtn = await page.waitForFunction(
() =>
[...document.querySelectorAll(".modal-footer .btn")].find(
el => el.textContent.toLowerCase().includes("remove")
),
{timeout: 10_001}
);
await confirmBtn.evaluate(el => el.click());
// final check to make sure the collection is empty
await page.goto(baseUrl + "collections/" + collectionName, {
waitUntil: "domcontentloaded",
});
try {
await page.waitForFunction(
`!document.querySelector(".remove.js-remove")`,
{timeout: 10_002}
);
} catch (err) {
// refresh to see if the item has been removed
await page.goto(baseUrl + "collections/" + collectionName, {
waitUntil: "domcontentloaded",
});
await page.waitForFunction(
`!document.querySelector(".remove.js-remove")`,
{timeout: 10_003}
);
}
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment