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 { 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 { 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; } }