octoplus-coffee-check/main.ts
2025-05-22 17:56:23 +01:00

147 lines
4.1 KiB
TypeScript

import { loadState, saveState } from "./state.ts";
import sendMessage from "./webhook.ts";
// grab this from https://octopus.energy/dashboard/new/accounts/personal-details/api-access
const OCTOPUS_API_KEY = Deno.env.get("OCTOPUS_API_KEY");
// grab this from octopus dashboard - looks something like A-XXXXXXXX
const OCTOPUS_ACCOUNT_NUMBER = Deno.env.get("OCTOPUS_ACCOUNT_NUMBER");
// webhook url
const WEBHOOK_URL = Deno.env.get("WEBHOOK_URL");
const OCTOPUS_API_ENDPOINT = "https://api.octopus.energy/v1/graphql/";
const OFFER_SLUG_MAP: { [slug: string]: string } = {
"greggs": "Greegs",
"caffe-nero": "Caffè Nero"
};
if (OCTOPUS_API_KEY === undefined || OCTOPUS_ACCOUNT_NUMBER === undefined) {
throw new Error("Octopus API key and account number are required!");
}
if (WEBHOOK_URL === undefined) {
throw new Error("Webhook URL is required!");
}
async function graphQlRequest(query: string, variables: { [key: string]: any }, auth?: string): Promise<any> {
const headers: Headers = new Headers({ "Content-Type": "application/json" });
if (auth) {
headers.set("Authorization", auth);
}
const res = await fetch(OCTOPUS_API_ENDPOINT, {
method: "POST",
body: JSON.stringify({
query,
variables
}),
headers
});
if (res.status !== 200) {
throw new Error(`GraphQL request failed with status ${res.status}`);
}
const jsonData = await res.json();
if (jsonData.errors) {
// deno-lint-ignore no-explicit-any
const errorMessages = jsonData.errors.map((m: any) => m.message);
throw new Error(`GraphQL request failed: ${errorMessages.join(". ")}.`);
}
return jsonData.data;
}
async function checkCoffee(): Promise<void> {
console.log("Fetching API token...");
const token = (await graphQlRequest(
/* GraphQL */ `
mutation ($apiKey: String!) {
obtainKrakenToken(input: {
APIKey: $apiKey
}) {
token
}
}`,
{ apiKey: OCTOPUS_API_KEY }
)).obtainKrakenToken.token;
const state = await loadState();
console.log("Fetching offers...");
const offers = await graphQlRequest(
/* GraphQL */ `
query ($accountNumber: String!, $first: Int!) {
octoplusOfferGroups(accountNumber: $accountNumber, first: $first) {
edges {
node {
octoplusOffers {
slug
claimAbility {
canClaimOffer
cannotClaimReason
}
claimBy
}
}
}
}
}`,
{
accountNumber: OCTOPUS_ACCOUNT_NUMBER,
first: 12,
},
token,
);
console.log(`Found ${offers.octoplusOfferGroups.edges.length} offers`);
const messages = [];
for (const { node } of offers.octoplusOfferGroups.edges) {
for (const offer of node.octoplusOffers) {
if (offer.slug !== "greggs" && offer.slug !== "caffe-nero") continue;
const friendlyName = `${OFFER_SLUG_MAP[offer.slug]} coffee offer`;
if (offer.slug in state) {
if (offer.claimAbility.canClaimOffer !== state[offer.slug].canClaimOffer) {
if (offer.claimAbility.canClaimOffer) {
messages.push(`${friendlyName} is now claimable! Claim by ${offer.claimBy}`);
} else {
messages.push(`${friendlyName} is no longer claimable (${offer.claimAbility.cannotClaimReason})`);
}
} else if (offer.claimBy !== state[offer.slug].claimBy) {
messages.push(`${friendlyName} must be claimed by ${offer.claimBy}.`);
}
} else if (offer.claimAbility.canClaimOffer) {
messages.push(`New ${friendlyName} found! Claim it by ${offer.claimBy}`);
}
state[offer.slug] = {
canClaimOffer: offer.claimAbility.canClaimOffer,
claimBy: offer.claimBy,
cannotClaimReason: offer.claimAbility.cannotClaimReason,
}
}
}
console.log(`Sending ${messages.length} notifications...`);
for (const msg of messages) {
await sendMessage(WEBHOOK_URL!, msg);
}
await saveState(state);
}
if (import.meta.main) {
try {
checkCoffee();
} catch (ex) {
await sendMessage(WEBHOOK_URL!, `Failed to check for coffee: ${ex}`);
throw ex;
}
}