Skip to content
This repository has been archived by the owner on May 12, 2024. It is now read-only.

Commit

Permalink
Add epic games store free games notification
Browse files Browse the repository at this point in the history
  • Loading branch information
aloop committed Jan 6, 2024
1 parent d9dd58d commit aa097a5
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 2 deletions.
76 changes: 76 additions & 0 deletions api-client/epic-games-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const productBaseUrl = "https://www.epicgames.com/store/en-US/product/";
const freeGamesApiUrl =
"https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US&country=US&allowCountries=US";

const isCurrentlyFree = (game) => {
const currentDate = Date.now();
const giveawayStart =
game?.promotions?.promotionalOffers[0]?.promotionalOffers[0]?.startDate;
const giveawayEnd =
game?.promotions?.promotionalOffers[0]?.promotionalOffers[0]?.endDate;

if (giveawayStart !== null && giveawayEnd !== null) {
return (
new Date(giveawayStart) - currentDate < 0 &&
new Date(giveawayEnd) - currentDate > 0
);
}

return false;
};

const getFormattedEndDate = (game) => {
const giveawayEnd =
game?.promotions?.promotionalOffers[0]?.promotionalOffers[0]?.endDate;

if (giveawayEnd !== null) {
return new Date(giveawayEnd).toLocaleString("en-US", {
timeZone: "America/Los_Angeles",
dateStyle: "full",
timeStyle: "long",
});
}

return null;
};

export async function fetchFreeGames() {
const response = await fetch(freeGamesApiUrl);

if (!response.ok) {
throw new Error(
`Could not obtain Token Price, server responded with: ${response}`
);
}

const result = await response.json();

if (result?.data?.Catalog?.searchStore?.elements) {
const elements = result.data.Catalog.searchStore.elements;

return elements
.filter(
(game) =>
// Check for a discounted price of $0
game?.price?.totalPrice?.discountPrice === 0 &&
// Make sure it has "promotional offers"
game?.promotions?.promotionalOffers?.length > 0 &&
// Make sure it's actually free
isCurrentlyFree(game)
)
.map((game) => ({
title: game.title,
description: game.description,
id: game.id,
url: new URL(`${game?.productSlug}`, productBaseUrl).href,
thumbnailUrl:
game?.keyImages?.find(
(img) => img?.type?.toLowerCase() === "thumbnail"
)?.url ?? game?.keyImages[0]?.url,
freeUntil: getFormattedEndDate(game),
endDate:
game?.promotions?.promotionalOffers[0]?.promotionalOffers[0]
?.endDate,
}));
}
}
3 changes: 3 additions & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
"host": "https://example.com",
"listenHost": "127.0.0.1",
"listenPort": 5000
},
"channels": {
"deals": ""
}
}
73 changes: 73 additions & 0 deletions cron-tasks/fetch-free-epic-games.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { EmbedBuilder } from "discord.js";

import { fetchNewGames } from "../models/epic-games-store.js";
import loadConfig from "../utils/config.js";

const {
channels: { deals },
} = await loadConfig();

const postNewDeals = (client, games) => {
if (games.length > 0) {
try {
const channel = client.channels.cache.get(deals);

const embeds = games.map((game) =>
new EmbedBuilder()
.setTitle(game.title)
.setURL(game.url)
.addFields([
{
name: "Free at",
value: "Epic Games Store",
},
{
name: "Description",
value: game.description,
},
{
name: "Free until",
value: game.freeUntil,
},
])
.setImage(game.thumbnailUrl)
);

channel.send({
embeds: embeds,
});
} catch (err) {
console.error(err);
}
}
};

// 2 hours in milliseconds
const intervalLength = 2 * 60 * 60 * 1000;

let intervalId = null;

export function stop() {
if (intervalId !== null) {
clearInterval(intervalId);
}
}

export async function start(client) {
if (!intervalId) {
const freeGames = await fetchNewGames();
postNewDeals(client, freeGames);

intervalId = setInterval(async () => {
console.log(
"Cron Task: Fetching latest free games from Epic Games Store"
);

const freeGames = await fetchNewGames();
postNewDeals(client, freeGames);
}, intervalLength);
}
}

process.on("SIGINT", stop);
process.on("SIGTERM", stop);
2 changes: 1 addition & 1 deletion cron-tasks/update-wow-token-price.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function stop() {
}

// 15 minutes in milliseconds
const intervalLength = 15 * 1000 * 60;
const intervalLength = 15 * 60 * 1000;

export async function start() {
if (!intervalId) {
Expand Down
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@ const cronTasks = fs
for (const file of cronTasks) {
const filePath = path.join(cronTasksPath, file);
const cronTask = await import(filePath);
await cronTask?.start?.();
await cronTask?.start?.(client);
}
41 changes: 41 additions & 0 deletions models/epic-games-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Store from "../utils/json-store.js";
import { fetchFreeGames } from "../api-client/epic-games-store.js";

const pickGameId = (game) => game.id;

export async function fetchNewGames() {
let freeGames = null;

try {
freeGames = await fetchFreeGames();
} catch (err) {
console.error(
"Error fetching free games from the Epic Games Store:",
err
);
}

if (freeGames === null) {
return;
}

const currentDate = Date.now();
const store = new Store("../db/epic-games-store-free-games.json");
const currentGames = await store.read();
const currentGameIds = currentGames.map(pickGameId);

// Filter out any games that we have already announced
const newFreeGames = freeGames.filter(
(game) => !currentGameIds.includes(game.id)
);

// Remove expired games
const currentGamesWithExpiredRemoved = currentGames.filter(
(game) => new Date(game.endDate) - currentDate > 0
);

// Update file with all current games, removing expired games and adding new ones.
await store.write([...newFreeGames, ...currentGamesWithExpiredRemoved]);

return newFreeGames;
}
41 changes: 41 additions & 0 deletions utils/json-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

export default class JSONStore {
constructor(filePath) {
this._path = path.join(
path.dirname(fileURLToPath(import.meta.url)),
filePath
);
}

async read() {
try {
const contents = await fs.readFile(this._path, {
encoding: "utf8",
});

return JSON.parse(contents);
} catch (err) {
console.error(
`An error occurred while reading "${this._path}"`,
err
);
}

return [];
}

async write(data) {
try {
const contents = JSON.stringify(data);
return await fs.writeFile(this._path, contents);
} catch (err) {
console.error(
`An error occurred while writing "${this._path}"`,
err
);
}
}
}

0 comments on commit aa097a5

Please sign in to comment.