Skip to content

Commit e89f5bc

Browse files
committed
v1
1 parent d08cead commit e89f5bc

4 files changed

Lines changed: 275 additions & 0 deletions

File tree

config/config.example.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# steam:
2+
# username: "your_steam_username"
3+
# password: "your_steam_password"
4+
# github-token: "your_default_github_token"
5+
apps:
6+
#Team Fortress 2
7+
440:
8+
#branch:
9+
webhooks:
10+
- repo: "steamtracking/gametracking-tf2"
11+
#access_token:
12+
workflow_id: update.yml
13+
branch: master
14+
#Dota 2
15+
570:
16+
#branch:
17+
webhooks:
18+
- repo: "steamtracking/gametracking-dota2"
19+
#access_token:
20+
workflow_id: update.yml
21+
branch: master
22+
#CS2
23+
730:
24+
#branch:
25+
webhooks:
26+
- repo: "steamtracking/gametracking-cs2"
27+
#access_token:
28+
workflow_id: update.yml
29+
branch: master
30+
#SteamVR
31+
250820:
32+
branch: beta
33+
webhooks:
34+
- repo: "steamtracking/gametracking-steamvr"
35+
#access_token:
36+
workflow_id: update.yml
37+
branch: master
38+
1422450:
39+
#branch:
40+
webhooks:
41+
- repo: "steamtracking/gametracking-deadlock"
42+
#access_token:
43+
workflow_id: update.yml
44+
branch: master

src/bot.mjs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env node
2+
3+
import SteamUser from "steam-user";
4+
5+
import Cache from "./cache.mjs";
6+
import Config from "./config.mjs";
7+
8+
const updateRate = process.env.UPDATE_RATE ? parseInt(process.env.UPDATE_RATE, 10) : 10000;
9+
10+
const client = new SteamUser();
11+
const config = new Config();
12+
await config.init();
13+
const cache = new Cache();
14+
await cache.init();
15+
16+
const sendWebhook = async (webhook) => {
17+
try {
18+
const response = await fetch(`https://api.github.com/repos/${webhook.repo}/actions/workflows/${webhook.workflow_id}/dispatches`, {
19+
method: "POST",
20+
headers: {
21+
Accept: "application/vnd.github.everest-preview+json",
22+
"Content-Type": "application/json",
23+
Authorization: `Bearer ${webhook.access_token}`,
24+
},
25+
body: JSON.stringify({
26+
ref: webhook.branch || "main",
27+
}),
28+
});
29+
if (response && !response.ok) {
30+
console.error(`Failed to send webhook: ${response.status} ${response.statusText} ${response.url}`);
31+
} else {
32+
console.info(`Webhook sent successfully to ${webhook.repo} for workflow ${webhook.workflow_id}`);
33+
}
34+
} catch (err) {
35+
console.error(err);
36+
}
37+
};
38+
39+
client.setOptions({
40+
enablePicsCache: true,
41+
changelistUpdateInterval: updateRate,
42+
machineIdType: SteamUser.EMachineIDType.PersistentRandom,
43+
});
44+
45+
client.logOn(config.getSteamLogins());
46+
47+
const initAfterLogin = async () => {
48+
Object.keys(config.getApp()).forEach(async (appid) => {
49+
// for some reason getProductInfo only times out
50+
// hopefully waiting 30s after boot is enough to make picsCache never be empty
51+
// if (!client.picsCache.apps[appid]) {
52+
// console.debug("Product info fetching");
53+
// await client.getProductInfo([appid], []);
54+
// console.debug("Product info fetched");
55+
// }
56+
if (!client.picsCache.apps[appid]?.appinfo) {
57+
console.info(`App ${appid} is not available`);
58+
return;
59+
}
60+
const branch = config.getBranch(appid);
61+
const branchData = client.picsCache.apps[appid].appinfo.depots?.branches?.[branch];
62+
if (!branchData) {
63+
console.info(`Branch ${branch} is not available for app ${appid}`);
64+
return;
65+
}
66+
if (cache.is_buildid_updated(appid, branchData.buildid)) {
67+
config.getApp(appid)?.webhooks.forEach((webhook) => {
68+
sendWebhook(webhook, appid);
69+
});
70+
}
71+
});
72+
};
73+
74+
client.on("loggedOn", () => {
75+
console.info("Logged into Steam");
76+
setTimeout(initAfterLogin, 30000);
77+
});
78+
79+
client.on("error", (err) => {
80+
console.error(err);
81+
});
82+
83+
client.on("appUpdate", (appid, data) => {
84+
console.info(`App ${appid} has been updated`);
85+
if (!config.getApp(appid)) {
86+
console.info(`App ${appid} is not being monitored`);
87+
return;
88+
}
89+
90+
const branch = config.getBranch(appid);
91+
const newBranchData = data?.appinfo?.depots?.branches?.[branch];
92+
const oldBranchData = client.picsCache.apps[appid]?.appinfo?.depots?.branches?.[branch];
93+
94+
if (
95+
(newBranchData && cache.is_buildid_updated(appid, newBranchData.buildid)) ||
96+
(oldBranchData && newBranchData && oldBranchData.buildid !== newBranchData.buildid)
97+
) {
98+
config.getApp(appid)?.webhooks?.forEach((webhook) => {
99+
sendWebhook(webhook, appid);
100+
});
101+
}
102+
});

src/cache.mjs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { existsSync } from "node:fs";
2+
import { readFile, writeFile } from "node:fs/promises";
3+
4+
const isDocker = existsSync("/.dockerenv");
5+
const CACHE_FILE = isDocker ? "/var/cache/gametracking-monitor" : "config/cache.json";
6+
7+
export default class Cache {
8+
constructor() {
9+
this.data = { apps: {} };
10+
}
11+
12+
async init() {
13+
try {
14+
this.data = await readFile(CACHE_FILE, "utf8").then((data) => JSON.parse(data));
15+
} catch (err) {
16+
if (err.code === "ENOENT") {
17+
console.info("Cache file not found, creating one");
18+
this.#write();
19+
} else if (err.name === "SyntaxError") {
20+
console.warn("Cache file is corrupted, resetting it");
21+
this.#write();
22+
console.info("Cache file has been reset");
23+
} else {
24+
console.error(err);
25+
process.exit(1);
26+
}
27+
}
28+
}
29+
30+
async #write() {
31+
try {
32+
await writeFile(CACHE_FILE, JSON.stringify(this.data));
33+
} catch (err) {
34+
console.error(err);
35+
}
36+
}
37+
38+
is_buildid_updated(appid, buildid) {
39+
this.data.apps[appid] ??= {};
40+
41+
if (this.data.apps[appid].buildid !== buildid) {
42+
this.data.apps[appid].buildid = buildid;
43+
this.#write();
44+
return true;
45+
}
46+
return false;
47+
}
48+
}

src/config.mjs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { readFile, watch } from "node:fs/promises";
2+
import yaml from "js-yaml";
3+
4+
const CONFIG_FILE = "config/config.yaml";
5+
6+
export default class Config {
7+
#data;
8+
9+
constructor() {
10+
this.#data = {};
11+
}
12+
13+
async init() {
14+
try {
15+
this.#data = await readFile(CONFIG_FILE, "utf8").then((data) => yaml.load(data));
16+
this.verify();
17+
} catch (err) {
18+
console.error(err);
19+
if (err.code === "ENOENT") {
20+
console.error(`Config file not found, it should be under ${CONFIG_FILE}`);
21+
process.exit(1);
22+
}
23+
}
24+
}
25+
26+
verify(config = this.#data) {
27+
try {
28+
const envLogins = process.env.STEAM_USERNAME && process.env.STEAM_PASSWORD;
29+
const configLogins = config.steam?.username && config.steam?.password;
30+
if (!envLogins && !configLogins) {
31+
console.error("Steam logins missing");
32+
process.exit(1);
33+
}
34+
35+
if (!config.apps || typeof config.apps !== "object" || Array.isArray(config.apps)) {
36+
console.error("Config is missing the 'apps' object");
37+
process.exit(1);
38+
}
39+
40+
for (const [appid, app] of Object.entries(config.apps)) {
41+
if (!app.webhooks || !Array.isArray(app.webhooks) || app.webhooks.length === 0) {
42+
console.error(`App ${appid} is missing a valid 'webhooks' array`);
43+
process.exit(1);
44+
}
45+
for (const [index, webhook] of app.webhooks.entries()) {
46+
if (!webhook.repo || !webhook.workflow_id) {
47+
console.error(`Webhook configuration for app ${appid} at index ${index} is missing 'repo' or 'workflow_id'`);
48+
process.exit(1);
49+
}
50+
if (!webhook.access_token || !process.env.GITHUB_ACCESS_TOKEN || !config.github-token) {
51+
console.error(`Webhook configuration for app ${appid} at index ${index} is missing an access token and there's no default token set`);
52+
process.exit(1);
53+
}
54+
}
55+
}
56+
57+
return true;
58+
} catch (err) {
59+
console.error(`Is the config file valid? ${err}\n`);
60+
return false;
61+
}
62+
}
63+
64+
getBranch(appid) {
65+
return this.#data.apps[appid]?.branch || "public";
66+
}
67+
68+
getApp(appid) {
69+
if (appid !== undefined) {
70+
return this.#data.apps?.[appid] || null;
71+
}
72+
return this.#data.apps || {};
73+
}
74+
75+
getSteamLogins() {
76+
return {
77+
accountName: process.env.STEAM_USERNAME || this.#data?.steam?.username,
78+
password: process.env.STEAM_PASSWORD || this.#data?.steam?.password,
79+
};
80+
}
81+
}

0 commit comments

Comments
 (0)