Last active
August 26, 2023 18:25
-
-
Save ggorlen/1bbcc4841af230d9de9fb214e8ead36f to your computer and use it in GitHub Desktop.
codewars tagger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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