This repository has been archived by the owner on May 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add epic games store free games notification
- Loading branch information
Showing
7 changed files
with
236 additions
and
2 deletions.
There are no files selected for viewing
This file contains 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
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, | ||
})); | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,5 +14,8 @@ | |
"host": "https://example.com", | ||
"listenHost": "127.0.0.1", | ||
"listenPort": 5000 | ||
}, | ||
"channels": { | ||
"deals": "" | ||
} | ||
} |
This file contains 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
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); |
This file contains 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 file contains 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 file contains 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
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; | ||
} |
This file contains 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
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 | ||
); | ||
} | ||
} | ||
} |