Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save itsmeow/ad3571ba50cde6a94b5ea36d0cdb263d to your computer and use it in GitHub Desktop.
Save itsmeow/ad3571ba50cde6a94b5ea36d0cdb263d to your computer and use it in GitHub Desktop.
A tampermonkey script to update a google sheet with data from the CurseForge rewards transactions page.
// ==UserScript==
// @name CurseForge Rewards Transactions Page to Google Sheet
// @namespace github.com/itsmeow/ad3571ba50cde6a94b5ea36d0cdb263d
// @updateURL https://gist.githubusercontent.com/itsmeow/ad3571ba50cde6a94b5ea36d0cdb263d/raw/tampermonkey-sheets-update-curseforge-rewards.user.js
// @version 1.0.1
// @description Gathers data from your CurseForge rewards transactions and enters it into Google Sheets.
// @author itsmeowdev
// @match https://authors.curseforge.com/store/transactions
// @icon https://media.forgecdn.net/avatars/130/458/636460205549127215.png
// @grant none
// @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js
// @require https://apis.google.com/js/api.js
// ==/UserScript==
/*
SETUP GUIDE:
CLIENT ID / API KEY:
You need to obtain and enter a Google Sheets API authorized OAuth 2.0 Client ID and an API Key.
Create a Google Cloud project: https://developers.google.com/workspace/guides/create-project
Enable the Google Sheets API
Create an OAuth consent screen (use external, it won't be verified by Google unless you set it to public. You won't)
If you can select Internal, do that instead. It's much easier.
https://developers.google.com/workspace/guides/create-credentials
Set the ToS/Privacy Policy to any URL
Add authorized domains (curseforge.com, the url you put ToS/privacy policy at)
Click credentials on the left
Create an OAuth client ID
URIs: https://authors.curseforge.com
Redirect URLs: https://authors.curseforge.com/store/transactions
Put resulting client ID in const below.
Create an API key
Edit it to add restrictions
Application: HTTP referrers
Website Restrictions: authors.curseforge.com/*
API restrictions: select Sheets API.
Paste API key into const below.
USAGE GUIDE:
Open the transactions page (https://authors.curseforge.com/store/transactions)
Press Authorize next to "Transfer points"
Go through Google authentication prompt, select account to use
Find the sheet you want to update, open it in your browser.
In the URL of the sheet, there is a large ID that looks like this: 1zzedHcmsgYRsYDseotAD2BjWrEeWBFvQQZ4COOF55Wg
Copy it, paste it into the text box that appears after sign-in.
Type a slash at the end (/)
Put the name of the sheet in your spreadsheet to enter into (they are listed on the bottom of the screen, the default is Sheet1)
Press "Update Data"! All your data will now be entered, corrected, and updated accordingly.
No need to spam Show More, it gets all of your award data.
*/
(function() {
'use strict';
const CLIENT_ID = "";
const API_KEY = "";
const epochDay = 24 * 60 * 60 * 1000;
const BUTTON_STYLE = "float: right; padding: 8px; display: inline-block; background-color: #005380 !important; margin: 8px; border: 1px solid #404040; color: white !important; border-radius: 12px;";
const AUTH_BUTTON = document.createElement("button");
AUTH_BUTTON.id = "auth-sheets-button";
AUTH_BUTTON.innerHTML = "Authorize";
AUTH_BUTTON.style = BUTTON_STYLE;
const ID_SUBMIT = document.createElement("button");
ID_SUBMIT.id = "id-box-submit";
ID_SUBMIT.innerHTML = "Update Data";
ID_SUBMIT.style = BUTTON_STYLE;
ID_SUBMIT.style.display = "none";
const ID_TEXTBOX = document.createElement("input");
ID_TEXTBOX.id = "id-box";
ID_TEXTBOX.type = "text";
ID_TEXTBOX.style = "float: right; padding: 8px; margin: 8px; width: 200px;";
ID_TEXTBOX.style.display = "none";
ID_TEXTBOX.value = $.cookie("sheets_id_save") || "";
const signInClick = () => {
gapi.auth2.getAuthInstance().signIn();
};
const signOutClick = () => {
gapi.auth2.getAuthInstance().signOut();
};
const updateSigninStatus = isIn => {
if(isIn) {
AUTH_BUTTON.removeEventListener("click", signInClick);
AUTH_BUTTON.innerHTML = "Sign Out";
AUTH_BUTTON.addEventListener("click", signOutClick);
ID_TEXTBOX.style.display = "inline-block";
ID_SUBMIT.style.display = "inline-block";
} else {
AUTH_BUTTON.removeEventListener("click", signOutClick);
AUTH_BUTTON.innerHTML = "Authorize";
AUTH_BUTTON.addEventListener("click", signInClick);
ID_TEXTBOX.style.display = "none";
ID_SUBMIT.style.display = "none";
}
};
let lastDate;
let oldData = new Map();
let sheetId = "";
let sheetName = "";
const onSheetSet = async () => {
if($.cookie("sheets_id_save") !== ID_TEXTBOX.value) {
$.cookie("sheets_id_save", ID_TEXTBOX.value, {expires: 99999});
}
try {
let dataReses = [];
let data = [];
let totalRetrieved = 0;
let i = 0;
do {
let res = await $.ajax({
url: Elerium.Routes.StoreTransactionListingAjax(1000 * i, 1000, 7)
});
res = res.trim();
data = document.createElement("template");
data.innerHTML = res;
data = data.content.children;
dataReses.push(data);
i++;
totalRetrieved += data.length;
} while(data.length > 0);
try {
let updates = [];
let inserts = [];
const epochToStr = epoch => new Date(epoch).toISOString().split("T")[0];
let a = [];
let allDates = new Set([... oldData.keys()]);
for(let dataRes of dataReses) {
for(let t of dataRes) {
let epochTime = (parseInt($(t).find("abbr.standard-date")[0].attributes["data-epoch"].value)) * 1000;
let awards = $(t).find(".award a span strong");
let points = undefined;
if(awards.length > 0) {
points = parseFloat(awards[0].innerHTML);
}
let date = epochToStr(epochTime);
allDates.delete(epochTime);
let old = oldData.get(epochTime);
if(epochTime > lastDate) {
a.push("INSERT: " + date + " as " + points);
inserts.push({epoch: epochTime, date: date, points: points});
} else if(old.points !== points) {
a.push("UPDATE: " + date + " from " + old.points + " to " + points);
updates.push({epoch: epochTime, date: date, points: points, row: old.row});
}
}
}
let temp = [];
for(let d of allDates) {
let date = epochToStr(d);
let old = oldData.get(d);
if(old.points !== undefined) {
a.push("UPDATE NULL: " + date + " from " + old.points + " to undefined");
temp.push({epoch: d, date: date, points: undefined, row: old.row});
}
}
temp = temp.reverse();
updates = updates.concat(temp);
console.log(a);
inserts = inserts.reverse();
updates = updates.reverse();
const toRanges = (u) => {
if(u.length === 0) {
return u;
}
let result = [];
let data = [];
let rangeMin = u[0].row;
let rangeMax = rangeMin;
for(let i = 0; i < u.length; i++) {
let next = u[i + 1];
let hasNext = next !== undefined;
let p = u[i].points;
data.push([p === undefined ? "" : p]);
if(hasNext && next.row === u[i].row + 1) {
rangeMax++;
} else {
result.push({min: rangeMin, max: rangeMax, pointsData: data});
data = [];
if(hasNext) {
rangeMin = next.row;
rangeMax = rangeMin;
}
}
}
return result;
};
let updatesLen = updates.length;
updates = toRanges(updates);
let insertRows = [];
let lastEpoch = lastDate;
for(let insert of inserts) {
if(lastEpoch && lastEpoch !== insert.epoch + epochDay) {
let tempEpoch = lastEpoch + epochDay;
let diff = ((insert.epoch - lastEpoch) / epochDay) - 1;
for(let i = 0; i < diff; i++) {
insertRows.push([epochToStr(tempEpoch), ""]);
tempEpoch += epochDay;
}
}
insertRows.push([insert.date, insert.points === undefined ? "" : insert.points]);
lastEpoch = insert.epoch;
}
const wrapRange = (i) => {
let res = [];
if(i.length > 0) {
let min = lastDate === 0 ? 2 : (oldData.get(lastDate).row + 1);
res.push({min: min, max: min + (i.length - 1), pointsData: i});
}
return res;
};
const sheetRange = (o, d) => sheetName + "!" + (d ? "A" : "B") + o.min + ":B" + o.max;
const printRes = res => console.log(JSON.parse(res.body));
if(inserts.length > 0) {
let insert = wrapRange(insertRows)[0];
let range = sheetRange(insert, true);
console.log(insert);
gapi.client.sheets.spreadsheets.values.update({
spreadsheetId: sheetId,
range: range,
valueInputOption: "USER_ENTERED"
}, {range: range, values: insert.pointsData}).then(printRes, printRes);
}
if(updates.length > 0) {
let d = updates.map(v => {
return {
range: sheetRange(v, false),
values: v.pointsData
}
});
console.log(d);
gapi.client.sheets.spreadsheets.values.batchUpdate({spreadsheetId: sheetId}, {
valueInputOption: "USER_ENTERED",
data: d
}).then(printRes, printRes);
}
const sRow = (i, act) => (i + " row" + (i !== 1 ? "s" + (act ? " have" : "") : (act ? " has" : "")));
alert("Successfully retrieved " + sRow(totalRetrieved, false) + ". " + sRow(updatesLen, true) + " been corrected. " + sRow(insertRows.length, true) + " been inserted.");
} catch(err) {
console.log(err);
alert("Error parsing response data! Logged to console.");
}
} catch (err) {
console.log(err);
alert("Error retrieving transaction data! Logged to console.");
}
};
gapi.load('client:auth2', () => {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: ["https://sheets.googleapis.com/$discovery/rest?version=v4"],
scope: "https://www.googleapis.com/auth/spreadsheets"
}).then(() => {
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
ID_SUBMIT.addEventListener("click", async () => {
oldData = new Map();
try {
let temp = ID_TEXTBOX.value.split("/");
sheetId = temp[0];
sheetName = temp[1];
let res = await gapi.client.sheets.spreadsheets.values.get({
spreadsheetId: sheetId,
range: sheetName + "!A2:B",
valueRenderOption: "UNFORMATTED_VALUE",
dateTimeRenderOption: "SERIAL_NUMBER"
});
if(res.result.values) {
for(let i = 0; i < res.result.values.length; i++) {
let v = res.result.values[i];
let e = Date.UTC(0, 0, v[0] - 1);
let p = v[1];
oldData.set(e, {points: p, row: i + 2});
if(i === res.result.values.length - 1) {
lastDate = e;
}
}
} else {
lastDate = 0;
}
onSheetSet();
} catch (err) {
console.log(err);
alert("Invalid sheet/range. Please enter the sheet's ID (from URL), a slash, and then the name of the specific sheet inside the spreadsheet to use. Make sure your data is formatted correctly, so that A2 is the first date and B2 is the first point value.");
}
});
}, error => {
console.log(JSON.stringify(error, null, 2));
});
});
if($("#auth-sheets-button").length === 0) {
$(".e-header-nav .e-wrapper .e-menu")[0].appendChild(AUTH_BUTTON);
}
if($("#id-box-submit").length === 0) {
$(".e-header-nav .e-wrapper .e-menu")[0].appendChild(ID_SUBMIT);
}
if($("#id-box").length === 0) {
$(".e-header-nav .e-wrapper .e-menu")[0].appendChild(ID_TEXTBOX);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment