Skip to content

feat: game activity tracker #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 62 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9a6a422
tracking options in config
flloschy Oct 15, 2022
48faaeb
creating db entry
flloschy Oct 15, 2022
87ca118
added indent for updatePresent
flloschy Oct 15, 2022
7d27126
Tracking done - 7 commands & 2 autocompletes to go
flloschy Oct 15, 2022
c36bb0c
prevent "Must be 25 or fewer in length" error
flloschy Oct 16, 2022
d2b24b1
activated logging again
flloschy Oct 16, 2022
98ea5af
map length protection + blacklistRemove completer
flloschy Oct 16, 2022
ed2ba37
blacklist + look commands
flloschy Oct 16, 2022
95e98f6
All comands Complete
flloschy Oct 16, 2022
58fde26
added interface
flloschy Oct 16, 2022
42ac022
All autocompletes done
flloschy Oct 16, 2022
8f1889b
Prettier
flloschy Oct 16, 2022
c8449cf
Merge branch 'main' into gameActivityTracker
Chicken Oct 16, 2022
71e2143
fix: eslint-prettier conflict preventing merge
Chicken Oct 16, 2022
fdb4154
fix: merge mistakes
Chicken Oct 16, 2022
8a8bbb0
Did Anttis things, and ig it works now? its late I need sleep
flloschy Oct 17, 2022
0dfa1e8
Prettier
flloschy Oct 17, 2022
e6942ba
ESLint error fix (maybe)
flloschy Oct 19, 2022
d5807b4
workflow please work
flloschy Oct 19, 2022
c09c9a5
changed to final channel + trimed time formating
flloschy Oct 19, 2022
1de62a2
format
Chicken Oct 19, 2022
115a04f
merge main
Chicken Oct 19, 2022
a336740
fix eslint warnings
Chicken Oct 19, 2022
3f55418
moved functions to Utils
flloschy Oct 22, 2022
9d8b317
buttons misc + use it
flloschy Oct 22, 2022
3bc4f25
made admin and statistic commands ephemeral
flloschy Oct 22, 2022
91374f1
removed useless imports
flloschy Oct 22, 2022
0775ab7
small tweaks
StrangeGirlMurph Oct 23, 2022
d17fd15
prettify
StrangeGirlMurph Oct 23, 2022
618fc5a
No logging in to chat
flloschy Oct 24, 2022
e14a981
prevent duplicates in autocomplete
flloschy Oct 24, 2022
8808bd1
better
flloschy Oct 24, 2022
be945b3
autocomplete for admin -> blacklistgame
flloschy Oct 24, 2022
0c6439c
lowercase blacklist stuff
flloschy Oct 24, 2022
93647ce
new Descriptions+list cmd+blacklist add a-complete
flloschy Oct 24, 2022
3e7d5ea
listing logic
flloschy Oct 24, 2022
37da19f
removed async
flloschy Oct 24, 2022
f97a712
list command stuff
flloschy Oct 24, 2022
a2f8455
prettier
flloschy Oct 24, 2022
61fae7b
Wow, I figured out there is an prettify command :O
flloschy Oct 25, 2022
704484e
finished Tracker
flloschy Oct 26, 2022
76b7c05
stop bot activity to get logged
flloschy Oct 27, 2022
86fcb13
page fix
flloschy Oct 27, 2022
5d1ce3c
fixed wrong user amount
flloschy Oct 27, 2022
35a1f04
button features, layout stuff and some other things
Oct 28, 2022
b0b9688
Wilson did a thing
flloschy Oct 28, 2022
4277adb
Wilson did a thing 2
flloschy Oct 28, 2022
0087776
yes
flloschy Oct 28, 2022
22cfb86
thx
flloschy Oct 28, 2022
ffab736
configurable Admin command permission
flloschy Oct 28, 2022
863e7bf
wrong game/user count fix
flloschy Oct 29, 2022
9b4eb4a
unused import
flloschy Oct 29, 2022
c1bd5a8
prettify
flloschy Oct 29, 2022
6410a1b
register can be boolean now
flloschy Oct 30, 2022
7ddaebe
GuildPresences intent only when needed
flloschy Oct 30, 2022
1c2c58a
prettify..........
flloschy Oct 30, 2022
21dcd75
Uniform file names
flloschy Oct 30, 2022
179e9a4
Merge branch 'main' into gameActivityTracker
Chicken Nov 1, 2022
bb8a292
fix: git hook...
Chicken Nov 1, 2022
12df18c
prettify
Chicken Nov 1, 2022
28f6973
revised code and styled a bit
Nov 2, 2022
dae9ec8
dynamic jumps
Nov 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"sort-imports": [
"error",
{
Expand Down
499 changes: 287 additions & 212 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"build": "tsc -p tsconfig.json",
"run": "node --experimental-specifier-resolution=node .",
"start": "npm run build && npm run run",
"prepare:": "husky install"
"prepare": "husky install"
},
"repository": {
"type": "git",
Expand Down
25 changes: 20 additions & 5 deletions src/codeSamples/sampleAutocompleter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,27 @@ class CountryAutocompleter extends Autocompleter {
async execute(interaction: AutocompleteInteraction): Promise<void> {
// your options
const country = ["Finland", "Sweden", "Norway"];
// for example return all options which start with the user input
await interaction.respond(
country
.filter((c) => c.startsWith(interaction.options.getFocused() as string))
.map((c) => ({ name: c, value: c }))

// for example filter for options which start with the user input
let filterdoptions = country.filter((c) =>
c
// to make sure capitalisaion doesnt matter, make every option to lower case
.toLowerCase()
.startsWith(
// the same with the user input
interaction.options.getFocused().toLowerCase() as string
)
);

// if the filterd options are more than 25 remove everything after the 25th option
// because discord only allows 25 autocomplete results
if (filterdoptions.length > 25) filterdoptions = filterdoptions.slice(0, 25);

// map filtered options
const map = filterdoptions.map((c) => ({ name: c, value: c }));

// send map back to the user
await interaction.respond(map);
}
}

Expand Down
220 changes: 220 additions & 0 deletions src/commands/TrackerCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
ChatInputCommandInteraction,
SlashCommandBuilder,
SlashCommandSubcommandsOnlyBuilder,
} from "discord.js";
import { Command } from "../interactions/interactionClasses";
import { config } from "../config";
import { blacklistAdd, blacklistRemove, blacklistShow } from "../util/game-activity-tracker/blacklist";
import {
statisticsAllStats,
statisticsGameStats,
statisticsMyStats,
} from "../util/game-activity-tracker/statistics";
import {
adminBlacklistGame,
adminLook,
adminReset,
adminShow,
adminWhitelistGame,
} from "../util/game-activity-tracker/admin";
import { list } from "../util/game-activity-tracker/list";

class TrackerCommand extends Command {
constructor() {
super("game-activity-tracker");
}

async execute(interaction: ChatInputCommandInteraction): Promise<void> {
const group: string | null = interaction.options.getSubcommandGroup();
const sub: string | null = interaction.options.getSubcommand();

if (group === "blacklist") {
if (sub === "add") {
await blacklistAdd(interaction);
} else if (sub === "remove") {
await blacklistRemove(interaction);
} else if (sub === "show") {
await blacklistShow(interaction);
}
} else if (group === "statistics") {
if (sub === "my-stats") {
await statisticsMyStats(interaction);
} else if (sub === "game-stats") {
await statisticsGameStats(interaction);
} else if (sub === "all-stats") {
await statisticsAllStats(interaction);
}
} else if (group === "admin") {
if (interaction.memberPermissions?.bitfield == config.activityTrackerAdminCommandPermission) {
return;
} else if (sub === "reset") {
await adminReset(interaction);
} else if (sub === "blacklist") {
await adminBlacklistGame(interaction);
} else if (sub === "whitelist") {
await adminWhitelistGame(interaction);
} else if (sub === "look") {
await adminLook(interaction);
} else if (sub == "show") {
await adminShow(interaction);
}
} else if (sub == "list") {
await list(interaction);
}
}

register():
| SlashCommandBuilder
| SlashCommandSubcommandsOnlyBuilder
| Omit<SlashCommandBuilder, "addSubcommandGroup" | "addSubcommand">
| boolean {
if (!config.logActivity) {
return false;
}
return new SlashCommandBuilder()
.setName("game-activity-tracker")
.setDescription("All commands associated with the Game Activity Tracker!")
.addSubcommand((sub) =>
sub
.setName("list")
.setDescription("Returns a list of the played games based on a selected sorting method.")
.addStringOption((opt) =>
opt
.setName("sort")
.setDescription("Sort by the ...")
.addChoices(
{ name: "logs chronolocially and per person", value: "log-history" },
{ name: "playtime per game", value: "playtime-per-game" },
{ name: "number of logs per game", value: "logs-per-game" },
{ name: "last log date per game", value: "log-date-per-game" }
)
)
.addStringOption((opt) =>
opt
.setName("order")
.setDescription("Order the list ...")
.addChoices(
{ name: "decreasing", value: "decreasing" },
{ name: "increasing", value: "increasing" }
)
)
)
.addSubcommandGroup((group) =>
group
.setName("statistics")
.setDescription("Gives you statistics based on the game activity.")
.addSubcommand((sub) =>
sub
.setName("my-stats")
.setDescription("Show statistics about your own logs.")
.addStringOption((opt) =>
opt.setName("game").setDescription("Filter stats for the given name.").setAutocomplete(true)
)
)
.addSubcommand((sub) =>
sub
.setName("game-stats")
.setDescription("Show statistics about a given game across all logs.")
.addStringOption((opt) =>
opt
.setName("game")
.setDescription("Filter stats for the given game.")
.setAutocomplete(true)
.setRequired(true)
)
)
.addSubcommand((sub) => sub.setName("all-stats").setDescription("Show statistics across all logs."))
)
.addSubcommandGroup((group) =>
group
.setName("blacklist")
.setDescription("Manage your blacklist - if a game is on blacklist no activity will be tracked.")
.addSubcommand((sub) =>
sub
.setName("add")
.setDescription("Add a game to your blacklist.")
.addStringOption((opt) =>
opt
.setName("game")
.setDescription("The game to blacklist - (capitalization doesnt matter)")
.setAutocomplete(true)
.setRequired(true)
)
)
.addSubcommand((sub) =>
sub
.setName("remove")
.setDescription("Remove a game from your blacklist.")
.addStringOption((opt) =>
opt
.setName("game")
.setDescription("The game to whitelist - (capitalization doesnt matter)")
.setAutocomplete(true)
.setRequired(true)
)
)
.addSubcommand((sub) => sub.setName("show").setDescription("See what is on your blacklist."))
)
.addSubcommandGroup((group) =>
group
.setName("admin")
.setDescription("Commands which only users with higher permissions can use.")
.addSubcommand((sub) =>
sub
.setName("reset")
.setDescription("Reset every log and blacklist entry.")
.addBooleanOption((opt) =>
opt.setName("sure").setDescription("Are you really sure?").setRequired(true)
)
.addStringOption((opt) =>
opt
.setName("really")
.setDescription("Are you really sure you want to delete every entry?")
.addChoices(
{ name: "No. I dont want to delete every log and blacklist entry!", value: "no" },
{ name: "Yes I am sure. I want to delete every log and blacklist entry!", value: "yes" }
)
.setRequired(true)
)
)
.addSubcommand((sub) =>
sub
.setName("blacklist")
.setDescription("Add a game on the global blacklist - don't log anything for this game.")
.addStringOption((opt) =>
opt
.setName("game")
.setDescription("Enter game which should get blacklisted. (Capitalization doesnt matter)")
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand((sub) =>
sub
.setName("whitelist")
.setDescription("Remove a game from the global blacklist - log this game again.")
.addStringOption((opt) =>
opt
.setName("game")
.setDescription(
"Enter game which should get removed from the blacklisted . (Capitalization doesnt matter)"
)
.setRequired(true)
.setAutocomplete(true)
)
)
.addSubcommand((sub) => sub.setName("show").setDescription("Show the global blacklist."))
.addSubcommand((sub) =>
sub
.setName("look")
.setDescription("Look into a users blacklist.")
.addUserOption((opt) =>
opt.setName("user").setDescription("Take a look into this users blacklist.").setRequired(true)
)
)
);
}
}

export default new TrackerCommand();
8 changes: 7 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ColorResolvable, ComponentEmojiResolvable } from "discord.js";
import { ColorResolvable, ComponentEmojiResolvable, PermissionFlagsBits } from "discord.js";

// Configure your bot here.
export const config: BotConfig = {
Expand Down Expand Up @@ -60,6 +60,8 @@ export const config: BotConfig = {
serverDescription:
"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!",
logLevel: "debug",
logActivity: true,
activityTrackerAdminCommandPermission: PermissionFlagsBits.Administrator,
};

interface BotConfig {
Expand All @@ -81,4 +83,8 @@ interface BotConfig {
serverDescription: string;
// Also available: error, warn, info, http, verbose, debug, silly.
logLevel: "debug" | "info" | "warn" | "error" | "verbose";
// If User Game Activity should get logged
logActivity: boolean;
// Which permissions are needed to user game-activity-tracker commands
activityTrackerAdminCommandPermission: bigint;
}
9 changes: 9 additions & 0 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ interface Reminder {
export const reminderTimeoutCache = new Map<number, NodeJS.Timeout>();
export const reminderDb = new Enmap<number, Reminder>("reminder");
export const latexDb = new Enmap<string, string>("latex");

interface ActivityLogEntry {
t: number;
w: number;
}
export const activityTrackerLogDb = new Enmap<string, ActivityLogEntry[]>("TrackerLog");
export const activityTrackerBlacklistDb = new Enmap<string, string[]>("TrackerBlacklist");
activityTrackerBlacklistDb.ensure("general-user", []);
activityTrackerBlacklistDb.ensure("general-game", []);
22 changes: 22 additions & 0 deletions src/events/presenceUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Presence } from "discord.js";
import { config } from "../config";
import { blacklistCheck, getStopedActivities, logTime } from "../util/game-activity-tracker/presence";

export default async function presenceUpdate(oldPresence: Presence | null, newPresence: Presence) {
if (!config.logActivity) return;

const stopedActivities = await getStopedActivities(oldPresence, newPresence);
if (stopedActivities.length == 0) return;

const userid = newPresence.userId;

stopedActivities.forEach(async (element) => {
const start = element.createdTimestamp;
const timePlayed = Date.now() - start;
if (timePlayed < 20000) return;

if (await blacklistCheck(userid, element.name.toLowerCase())) return;
if (oldPresence?.user?.bot) return;
await logTime(userid, element.name.toLowerCase(), timePlayed);
});
}
14 changes: 8 additions & 6 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ async function updateRegisteredCommands(client: Client) {
ctx.commands.map(async (cmd) => {
const cmdBuilder = cmd.register();
const existingCmd = registeredCommands.find((c) => c.name === cmd.name);
if (!existingCmd) {
logger.debug(`Creating new command: ${cmdBuilder.name}`);
return client.application?.commands.create(cmdBuilder.toJSON(), ctx.defaultGuild);
} else if (!commandsEqual(existingCmd, cmdBuilder)) {
logger.debug(`Updating command: ${cmdBuilder.name}`);
return client.application?.commands.edit(existingCmd.id, cmdBuilder.toJSON(), ctx.defaultGuild);
if (typeof cmdBuilder != "boolean") {
if (!existingCmd) {
logger.debug(`Creating new command: ${cmdBuilder.name}`);
return client.application?.commands.create(cmdBuilder.toJSON(), ctx.defaultGuild);
} else if (!commandsEqual(existingCmd, cmdBuilder)) {
logger.debug(`Updating command: ${cmdBuilder.name}`);
return client.application?.commands.edit(existingCmd.id, cmdBuilder.toJSON(), ctx.defaultGuild);
}
}
})
);
Expand Down
4 changes: 2 additions & 2 deletions src/interactions/autocompleters/ReminderAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Autocompleter } from "../interactionClasses";
import { AutocompleteInteraction } from "discord.js";
import { reminderDb } from "../../db";
import { timestampToReadable } from "../../util/reminderCommand/reminderUtil";
import { msToReadable } from "../../util/misc/time";

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

Expand Down
Loading