Skip to content

Commit 096544e

Browse files
flloschyChickenStrangeGirlMurphMurphyWilsontheWolf
authored
feat: game activity tracker (#36)
* tracking options in config * creating db entry * added indent for updatePresent * Tracking done - 7 commands & 2 autocompletes to go * prevent "Must be 25 or fewer in length" error * activated logging again * map length protection + blacklistRemove completer * blacklist + look commands * All comands Complete * added interface * All autocompletes done * Prettier * fix: eslint-prettier conflict preventing merge * fix: merge mistakes * Did Anttis things, and ig it works now? its late I need sleep * Prettier * ESLint error fix (maybe) * workflow please work * changed to final channel + trimed time formating * format * fix eslint warnings * moved functions to Utils * buttons misc + use it * made admin and statistic commands ephemeral * removed useless imports * small tweaks * prettify * No logging in to chat * prevent duplicates in autocomplete * better * autocomplete for admin -> blacklistgame * lowercase blacklist stuff * new Descriptions+list cmd+blacklist add a-complete * listing logic * removed async * list command stuff * prettier * Wow, I figured out there is an prettify command :O * finished Tracker * stop bot activity to get logged * page fix * fixed wrong user amount * button features, layout stuff and some other things * Wilson did a thing Co-authored-by: Wilson <[email protected]> * Wilson did a thing 2 Co-authored-by: Wilson <[email protected]> * yes Co-authored-by: Wilson <[email protected]> * thx Co-authored-by: Wilson <[email protected]> * configurable Admin command permission * wrong game/user count fix * unused import * prettify * register can be boolean now * GuildPresences intent only when needed * prettify.......... * Uniform file names * fix: git hook... * prettify * revised code and styled a bit * dynamic jumps Co-authored-by: Antti <[email protected]> Co-authored-by: Murphy <[email protected]> Co-authored-by: Murphy <[email protected]> Co-authored-by: Wilson <[email protected]>
1 parent ba87517 commit 096544e

27 files changed

+1569
-254
lines changed

.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
},
1212
"plugins": ["@typescript-eslint"],
1313
"rules": {
14+
"@typescript-eslint/no-non-null-assertion": "off",
1415
"sort-imports": [
1516
"error",
1617
{

package-lock.json

+287-212
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build": "tsc -p tsconfig.json",
1111
"run": "node --experimental-specifier-resolution=node .",
1212
"start": "npm run build && npm run run",
13-
"prepare:": "husky install"
13+
"prepare": "husky install"
1414
},
1515
"repository": {
1616
"type": "git",

src/codeSamples/sampleAutocompleter.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,27 @@ class CountryAutocompleter extends Autocompleter {
99
async execute(interaction: AutocompleteInteraction): Promise<void> {
1010
// your options
1111
const country = ["Finland", "Sweden", "Norway"];
12-
// for example return all options which start with the user input
13-
await interaction.respond(
14-
country
15-
.filter((c) => c.startsWith(interaction.options.getFocused() as string))
16-
.map((c) => ({ name: c, value: c }))
12+
13+
// for example filter for options which start with the user input
14+
let filterdoptions = country.filter((c) =>
15+
c
16+
// to make sure capitalisaion doesnt matter, make every option to lower case
17+
.toLowerCase()
18+
.startsWith(
19+
// the same with the user input
20+
interaction.options.getFocused().toLowerCase() as string
21+
)
1722
);
23+
24+
// if the filterd options are more than 25 remove everything after the 25th option
25+
// because discord only allows 25 autocomplete results
26+
if (filterdoptions.length > 25) filterdoptions = filterdoptions.slice(0, 25);
27+
28+
// map filtered options
29+
const map = filterdoptions.map((c) => ({ name: c, value: c }));
30+
31+
// send map back to the user
32+
await interaction.respond(map);
1833
}
1934
}
2035

src/commands/TrackerCommand.ts

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import {
2+
ChatInputCommandInteraction,
3+
SlashCommandBuilder,
4+
SlashCommandSubcommandsOnlyBuilder,
5+
} from "discord.js";
6+
import { Command } from "../interactions/interactionClasses";
7+
import { config } from "../config";
8+
import { blacklistAdd, blacklistRemove, blacklistShow } from "../util/game-activity-tracker/blacklist";
9+
import {
10+
statisticsAllStats,
11+
statisticsGameStats,
12+
statisticsMyStats,
13+
} from "../util/game-activity-tracker/statistics";
14+
import {
15+
adminBlacklistGame,
16+
adminLook,
17+
adminReset,
18+
adminShow,
19+
adminWhitelistGame,
20+
} from "../util/game-activity-tracker/admin";
21+
import { list } from "../util/game-activity-tracker/list";
22+
23+
class TrackerCommand extends Command {
24+
constructor() {
25+
super("game-activity-tracker");
26+
}
27+
28+
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
29+
const group: string | null = interaction.options.getSubcommandGroup();
30+
const sub: string | null = interaction.options.getSubcommand();
31+
32+
if (group === "blacklist") {
33+
if (sub === "add") {
34+
await blacklistAdd(interaction);
35+
} else if (sub === "remove") {
36+
await blacklistRemove(interaction);
37+
} else if (sub === "show") {
38+
await blacklistShow(interaction);
39+
}
40+
} else if (group === "statistics") {
41+
if (sub === "my-stats") {
42+
await statisticsMyStats(interaction);
43+
} else if (sub === "game-stats") {
44+
await statisticsGameStats(interaction);
45+
} else if (sub === "all-stats") {
46+
await statisticsAllStats(interaction);
47+
}
48+
} else if (group === "admin") {
49+
if (interaction.memberPermissions?.bitfield == config.activityTrackerAdminCommandPermission) {
50+
return;
51+
} else if (sub === "reset") {
52+
await adminReset(interaction);
53+
} else if (sub === "blacklist") {
54+
await adminBlacklistGame(interaction);
55+
} else if (sub === "whitelist") {
56+
await adminWhitelistGame(interaction);
57+
} else if (sub === "look") {
58+
await adminLook(interaction);
59+
} else if (sub == "show") {
60+
await adminShow(interaction);
61+
}
62+
} else if (sub == "list") {
63+
await list(interaction);
64+
}
65+
}
66+
67+
register():
68+
| SlashCommandBuilder
69+
| SlashCommandSubcommandsOnlyBuilder
70+
| Omit<SlashCommandBuilder, "addSubcommandGroup" | "addSubcommand">
71+
| boolean {
72+
if (!config.logActivity) {
73+
return false;
74+
}
75+
return new SlashCommandBuilder()
76+
.setName("game-activity-tracker")
77+
.setDescription("All commands associated with the Game Activity Tracker!")
78+
.addSubcommand((sub) =>
79+
sub
80+
.setName("list")
81+
.setDescription("Returns a list of the played games based on a selected sorting method.")
82+
.addStringOption((opt) =>
83+
opt
84+
.setName("sort")
85+
.setDescription("Sort by the ...")
86+
.addChoices(
87+
{ name: "logs chronolocially and per person", value: "log-history" },
88+
{ name: "playtime per game", value: "playtime-per-game" },
89+
{ name: "number of logs per game", value: "logs-per-game" },
90+
{ name: "last log date per game", value: "log-date-per-game" }
91+
)
92+
)
93+
.addStringOption((opt) =>
94+
opt
95+
.setName("order")
96+
.setDescription("Order the list ...")
97+
.addChoices(
98+
{ name: "decreasing", value: "decreasing" },
99+
{ name: "increasing", value: "increasing" }
100+
)
101+
)
102+
)
103+
.addSubcommandGroup((group) =>
104+
group
105+
.setName("statistics")
106+
.setDescription("Gives you statistics based on the game activity.")
107+
.addSubcommand((sub) =>
108+
sub
109+
.setName("my-stats")
110+
.setDescription("Show statistics about your own logs.")
111+
.addStringOption((opt) =>
112+
opt.setName("game").setDescription("Filter stats for the given name.").setAutocomplete(true)
113+
)
114+
)
115+
.addSubcommand((sub) =>
116+
sub
117+
.setName("game-stats")
118+
.setDescription("Show statistics about a given game across all logs.")
119+
.addStringOption((opt) =>
120+
opt
121+
.setName("game")
122+
.setDescription("Filter stats for the given game.")
123+
.setAutocomplete(true)
124+
.setRequired(true)
125+
)
126+
)
127+
.addSubcommand((sub) => sub.setName("all-stats").setDescription("Show statistics across all logs."))
128+
)
129+
.addSubcommandGroup((group) =>
130+
group
131+
.setName("blacklist")
132+
.setDescription("Manage your blacklist - if a game is on blacklist no activity will be tracked.")
133+
.addSubcommand((sub) =>
134+
sub
135+
.setName("add")
136+
.setDescription("Add a game to your blacklist.")
137+
.addStringOption((opt) =>
138+
opt
139+
.setName("game")
140+
.setDescription("The game to blacklist - (capitalization doesnt matter)")
141+
.setAutocomplete(true)
142+
.setRequired(true)
143+
)
144+
)
145+
.addSubcommand((sub) =>
146+
sub
147+
.setName("remove")
148+
.setDescription("Remove a game from your blacklist.")
149+
.addStringOption((opt) =>
150+
opt
151+
.setName("game")
152+
.setDescription("The game to whitelist - (capitalization doesnt matter)")
153+
.setAutocomplete(true)
154+
.setRequired(true)
155+
)
156+
)
157+
.addSubcommand((sub) => sub.setName("show").setDescription("See what is on your blacklist."))
158+
)
159+
.addSubcommandGroup((group) =>
160+
group
161+
.setName("admin")
162+
.setDescription("Commands which only users with higher permissions can use.")
163+
.addSubcommand((sub) =>
164+
sub
165+
.setName("reset")
166+
.setDescription("Reset every log and blacklist entry.")
167+
.addBooleanOption((opt) =>
168+
opt.setName("sure").setDescription("Are you really sure?").setRequired(true)
169+
)
170+
.addStringOption((opt) =>
171+
opt
172+
.setName("really")
173+
.setDescription("Are you really sure you want to delete every entry?")
174+
.addChoices(
175+
{ name: "No. I dont want to delete every log and blacklist entry!", value: "no" },
176+
{ name: "Yes I am sure. I want to delete every log and blacklist entry!", value: "yes" }
177+
)
178+
.setRequired(true)
179+
)
180+
)
181+
.addSubcommand((sub) =>
182+
sub
183+
.setName("blacklist")
184+
.setDescription("Add a game on the global blacklist - don't log anything for this game.")
185+
.addStringOption((opt) =>
186+
opt
187+
.setName("game")
188+
.setDescription("Enter game which should get blacklisted. (Capitalization doesnt matter)")
189+
.setRequired(true)
190+
.setAutocomplete(true)
191+
)
192+
)
193+
.addSubcommand((sub) =>
194+
sub
195+
.setName("whitelist")
196+
.setDescription("Remove a game from the global blacklist - log this game again.")
197+
.addStringOption((opt) =>
198+
opt
199+
.setName("game")
200+
.setDescription(
201+
"Enter game which should get removed from the blacklisted . (Capitalization doesnt matter)"
202+
)
203+
.setRequired(true)
204+
.setAutocomplete(true)
205+
)
206+
)
207+
.addSubcommand((sub) => sub.setName("show").setDescription("Show the global blacklist."))
208+
.addSubcommand((sub) =>
209+
sub
210+
.setName("look")
211+
.setDescription("Look into a users blacklist.")
212+
.addUserOption((opt) =>
213+
opt.setName("user").setDescription("Take a look into this users blacklist.").setRequired(true)
214+
)
215+
)
216+
);
217+
}
218+
}
219+
220+
export default new TrackerCommand();

src/config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ColorResolvable, ComponentEmojiResolvable } from "discord.js";
1+
import { ColorResolvable, ComponentEmojiResolvable, PermissionFlagsBits } from "discord.js";
22

33
// Configure your bot here.
44
export const config: BotConfig = {
@@ -60,6 +60,8 @@ export const config: BotConfig = {
6060
serverDescription:
6161
"We're a group of young and mostly queer people having a game jam/hackathon server together. We're a very friendly and welcoming community and are happy to have you join us! \nCheck out <#1022874504525008997> for more information!",
6262
logLevel: "debug",
63+
logActivity: true,
64+
activityTrackerAdminCommandPermission: PermissionFlagsBits.Administrator,
6365
};
6466

6567
interface BotConfig {
@@ -81,4 +83,8 @@ interface BotConfig {
8183
serverDescription: string;
8284
// Also available: error, warn, info, http, verbose, debug, silly.
8385
logLevel: "debug" | "info" | "warn" | "error" | "verbose";
86+
// If User Game Activity should get logged
87+
logActivity: boolean;
88+
// Which permissions are needed to user game-activity-tracker commands
89+
activityTrackerAdminCommandPermission: bigint;
8490
}

src/db.ts

+9
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,12 @@ interface Reminder {
1010
export const reminderTimeoutCache = new Map<number, NodeJS.Timeout>();
1111
export const reminderDb = new Enmap<number, Reminder>("reminder");
1212
export const latexDb = new Enmap<string, string>("latex");
13+
14+
interface ActivityLogEntry {
15+
t: number;
16+
w: number;
17+
}
18+
export const activityTrackerLogDb = new Enmap<string, ActivityLogEntry[]>("TrackerLog");
19+
export const activityTrackerBlacklistDb = new Enmap<string, string[]>("TrackerBlacklist");
20+
activityTrackerBlacklistDb.ensure("general-user", []);
21+
activityTrackerBlacklistDb.ensure("general-game", []);

src/events/presenceUpdate.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Presence } from "discord.js";
2+
import { config } from "../config";
3+
import { blacklistCheck, getStopedActivities, logTime } from "../util/game-activity-tracker/presence";
4+
5+
export default async function presenceUpdate(oldPresence: Presence | null, newPresence: Presence) {
6+
if (!config.logActivity) return;
7+
8+
const stopedActivities = await getStopedActivities(oldPresence, newPresence);
9+
if (stopedActivities.length == 0) return;
10+
11+
const userid = newPresence.userId;
12+
13+
stopedActivities.forEach(async (element) => {
14+
const start = element.createdTimestamp;
15+
const timePlayed = Date.now() - start;
16+
if (timePlayed < 20000) return;
17+
18+
if (await blacklistCheck(userid, element.name.toLowerCase())) return;
19+
if (oldPresence?.user?.bot) return;
20+
await logTime(userid, element.name.toLowerCase(), timePlayed);
21+
});
22+
}

src/events/ready.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ async function updateRegisteredCommands(client: Client) {
3434
ctx.commands.map(async (cmd) => {
3535
const cmdBuilder = cmd.register();
3636
const existingCmd = registeredCommands.find((c) => c.name === cmd.name);
37-
if (!existingCmd) {
38-
logger.debug(`Creating new command: ${cmdBuilder.name}`);
39-
return client.application?.commands.create(cmdBuilder.toJSON(), ctx.defaultGuild);
40-
} else if (!commandsEqual(existingCmd, cmdBuilder)) {
41-
logger.debug(`Updating command: ${cmdBuilder.name}`);
42-
return client.application?.commands.edit(existingCmd.id, cmdBuilder.toJSON(), ctx.defaultGuild);
37+
if (typeof cmdBuilder != "boolean") {
38+
if (!existingCmd) {
39+
logger.debug(`Creating new command: ${cmdBuilder.name}`);
40+
return client.application?.commands.create(cmdBuilder.toJSON(), ctx.defaultGuild);
41+
} else if (!commandsEqual(existingCmd, cmdBuilder)) {
42+
logger.debug(`Updating command: ${cmdBuilder.name}`);
43+
return client.application?.commands.edit(existingCmd.id, cmdBuilder.toJSON(), ctx.defaultGuild);
44+
}
4345
}
4446
})
4547
);

src/interactions/autocompleters/ReminderAutocomplete.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Autocompleter } from "../interactionClasses";
22
import { AutocompleteInteraction } from "discord.js";
33
import { reminderDb } from "../../db";
4-
import { timestampToReadable } from "../../util/reminderCommand/reminderUtil";
4+
import { msToReadable } from "../../util/misc/time";
55

66
class ReminderAutocompleter extends Autocompleter {
77
constructor() {
@@ -16,7 +16,7 @@ class ReminderAutocompleter extends Autocompleter {
1616
const id = value.pings[1].replace(/\D/g, "");
1717
const role = await interaction.guild?.roles.fetch(id);
1818
const member = await interaction.guild?.members.fetch(id).catch(() => null);
19-
let name = `ID: ${key} - ${timestampToReadable(value.timestamp, true)} ${
19+
let name = `ID: ${key} - ${msToReadable(value.timestamp, true)} ${
2020
role || member ? `- @${role ? role.name : member?.displayName} -` : "-"
2121
} ${value.message == "" ? "No message." : `${value.message}`}`;
2222

0 commit comments

Comments
 (0)