Adds some more polish
This commit is contained in:
parent
b3d0b3a90c
commit
11bd836ec3
18 changed files with 272 additions and 29 deletions
|
|
@ -6,6 +6,10 @@ on:
|
|||
- release-*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BUILD_TARGET: DOCKER
|
||||
BUILD_LABEL: release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: node-20
|
||||
|
|
@ -31,5 +35,5 @@ jobs:
|
|||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: neintonine/pnp-scheduler:release
|
||||
tags: neintonine/pnp-scheduler:${{ env.BUILD_LABEL }}
|
||||
context: .
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
if (!process.env.BUILD_TARGET) {
|
||||
process.env.BUILD_TARGET = "LOCAL";
|
||||
}
|
||||
|
||||
if (!process.env.BUILD_LABEL) {
|
||||
process.env.BUILD_LABEL = "development";
|
||||
}
|
||||
|
||||
import "./create-build-file.mjs";
|
||||
import context from './context.mjs';
|
||||
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
await context.dispose();
|
||||
|
||||
|
|
|
|||
12
build/create-build-file.mjs
Normal file
12
build/create-build-file.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import * as os from "node:os";
|
||||
import * as child_process from "node:child_process";
|
||||
import {json} from "node:stream/consumers";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const buildContext = {
|
||||
target: process.env.BUILD_TARGET ?? 'LOCAL',
|
||||
commitHash: child_process.execSync("git rev-parse HEAD").toString(),
|
||||
label: process.env.BUILD_LABEL ?? 'development',
|
||||
}
|
||||
|
||||
fs.writeFileSync("./dist/deploy.json", JSON.stringify(buildContext))
|
||||
|
|
@ -15,7 +15,7 @@ export type RuntimeGroupConfiguration = {
|
|||
};
|
||||
|
||||
export type ChannelRuntimeGroupConfiguration = {
|
||||
newPlaydates: ChannelId,
|
||||
notifications: ChannelId,
|
||||
playdateReminders: ChannelId
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ export class GroupConfigurationProvider implements ConfigurationProvider<
|
|||
return new ConfigurationTransformer(
|
||||
[
|
||||
{
|
||||
path: ['channels', 'newPlaydates'],
|
||||
path: ['channels', 'notifications'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {Repository} from "./Repository";
|
|||
import {GroupModel} from "../Models/GroupModel";
|
||||
import Groups, {DBGroup} from "../tables/Groups";
|
||||
import {DatabaseConnection} from "../DatabaseConnection";
|
||||
import {GuildMember, UserFlagsBitField} from "discord.js";
|
||||
import {GuildMember} from "discord.js";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
import {PlaydateRepository} from "./PlaydateRepository";
|
||||
import {Container} from "../../Container/Container";
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ export class Repository<ModelType extends Model, IntermediateModelType = unknown
|
|||
return result.changes > 0;
|
||||
}
|
||||
|
||||
public getById(id: number): Nullable<ModelType> {
|
||||
public getById(id: number|bigint): Nullable<ModelType> {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE id = ? LIMIT 1`;
|
||||
return this.convertToModelType(this.database.fetch<number, IntermediateModelType>(sql, id));
|
||||
}
|
||||
|
||||
public delete(id: number) {
|
||||
public delete(id: number|bigint) {
|
||||
const sql = `DELETE FROM ${this.schema.name} WHERE id = ?`;
|
||||
return this.database.execute(sql, id);
|
||||
}
|
||||
|
|
|
|||
60
source/Discord/Commands/Bot.ts
Normal file
60
source/Discord/Commands/Bot.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import {CacheType, ChatInputCommandInteraction, hyperlink, PermissionFlagsBits, SlashCommandBuilder} from "discord.js";
|
||||
import {ChatInteractionCommand, Command} from "./Command";
|
||||
import * as fs from "node:fs";
|
||||
import {UserError} from "../UserError";
|
||||
import {ifError} from "node:assert";
|
||||
import {BuildContextGetter} from "../../Utilities/BuildContext";
|
||||
import {EmbedLibrary} from "../EmbedLibrary";
|
||||
|
||||
export class BotCommand implements Command, ChatInteractionCommand {
|
||||
definition(): SlashCommandBuilder {
|
||||
return new SlashCommandBuilder()
|
||||
.setName("bot")
|
||||
.setDescription("Offers some information about the bot")
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addSubcommand(command => command
|
||||
.setName("build")
|
||||
.setDescription("Displays some information about the build the bot is running on.")
|
||||
)
|
||||
}
|
||||
|
||||
execute(interaction: ChatInputCommandInteraction<CacheType>): Promise<void> {
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "build":
|
||||
this.displayBuildInfos(interaction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private displayBuildInfos(interaction: ChatInputCommandInteraction<CacheType>) {
|
||||
const buildContext = new BuildContextGetter().getContext();
|
||||
if (!buildContext) {
|
||||
throw new UserError("Can't find required deploy information", "Using a valid docker image or (when running on a dev build) running `npm run build` once.");
|
||||
}
|
||||
|
||||
const embed = EmbedLibrary.base("Current Build")
|
||||
.setFields(
|
||||
[
|
||||
{
|
||||
name: "Build Target",
|
||||
value: buildContext.target,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Build Label",
|
||||
value: buildContext.label,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Latest Commit",
|
||||
value: hyperlink(buildContext.commitHash, buildContext.commitLink)
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
interaction.reply({
|
||||
embeds: [embed]
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import {PlaydatesCommand} from "./Playdates";
|
|||
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
import {ServerCommand} from "./Server";
|
||||
import {BotCommand} from "./Bot";
|
||||
|
||||
const commands: Set<Command> = new Set<Command>([
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand(),
|
||||
new ServerCommand()
|
||||
new ServerCommand(),
|
||||
new BotCommand()
|
||||
]);
|
||||
|
||||
export default class Commands {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ import {ServerConfigurationProvider} from "../../Configuration/Server/ServerConf
|
|||
import {ServerConfigurationRepository} from "../../Database/Repositories/ServerConfigurationRepository";
|
||||
import {PermissionError} from "../PermissionError";
|
||||
import {EmbedLibrary, EmbedType} from "../EmbedLibrary";
|
||||
import {EventHandler} from "../../Events/EventHandler";
|
||||
import {ElementChangedEvent} from "../../Events/EventClasses/ElementChangedEvent";
|
||||
import GroupConfiguration from "../../Database/tables/GroupConfiguration";
|
||||
import Groups from "../../Database/tables/Groups";
|
||||
|
||||
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
|
||||
private static GOODBYE_MESSAGES: string[] = [
|
||||
|
|
@ -50,7 +54,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
definition(): SlashCommandBuilder {
|
||||
// @ts-expect-error Slash command expects more than needed.
|
||||
return new SlashCommandBuilder()
|
||||
.setName('groups')
|
||||
.setName('group')
|
||||
.setDescription(`Manages groups`)
|
||||
.addSubcommand(create =>
|
||||
create.setName("create")
|
||||
|
|
@ -289,9 +293,9 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
type: MenuItemType.Collection,
|
||||
children: [
|
||||
{
|
||||
traversalKey: "newPlaydates",
|
||||
label: "New Playdates",
|
||||
description: "Sets the channel, where the group gets notified, when new Playdates are set.",
|
||||
traversalKey: "notifications",
|
||||
label: "Notifications",
|
||||
description: "Sets the channel, where the group gets notified, when things are happening, such as a new playdate is created.",
|
||||
},
|
||||
{
|
||||
traversalKey: "playdateReminders",
|
||||
|
|
@ -369,6 +373,17 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
group.leader.memberid = newLeader.id
|
||||
repo.update(group);
|
||||
|
||||
Container.get<EventHandler>(EventHandler.name)
|
||||
.dispatch(new ElementChangedEvent<GroupModel>(
|
||||
Groups.name,
|
||||
{
|
||||
id: group.id,
|
||||
leader: {
|
||||
memberid: newLeader.id
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
EmbedLibrary.base(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
definition(): SlashCommandBuilder {
|
||||
// @ts-expect-error Command builder is improperly marked as incomplete.
|
||||
return new SlashCommandBuilder()
|
||||
.setName("playdates")
|
||||
.setName("playdate")
|
||||
.setDescription("Manage your playdates")
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("create")
|
||||
|
|
@ -153,7 +153,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = EmbedLibrary.playdate(
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
group,
|
||||
"Created a play-date.",
|
||||
":white_check_mark: Your playdate has been created! You and your group get notified, when its time.",
|
||||
|
|
@ -198,7 +198,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
private async list(interaction: ChatInputCommandInteraction, group: GroupModel) {
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
|
||||
const embed = EmbedLibrary.playdate(
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
group,
|
||||
"Created a play-date.",
|
||||
null,
|
||||
|
|
@ -242,7 +242,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
|
||||
repo.delete(playdateId);
|
||||
|
||||
const embed = EmbedLibrary.playdate(
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
group,
|
||||
"Playdate deleted",
|
||||
`:x: Deleted ${time(selected.from_time, 'F')} - ${time(selected.to_time, 'F')}`,
|
||||
|
|
@ -296,7 +296,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
|
|||
});
|
||||
}
|
||||
|
||||
const embed = EmbedLibrary.playdate(
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
group,
|
||||
"Imported play-dates",
|
||||
`:white_check_mark: Your ${playdates.length} playdates has been created! You and your group get notified, when its time.`,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export class EmbedLibrary {
|
|||
return embed
|
||||
}
|
||||
|
||||
public static playdate(
|
||||
public static withGroup(
|
||||
group: GroupModel,
|
||||
title: string,
|
||||
description: string|null = null,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export class UserError extends Error {
|
|||
public readonly tryInstead: string | null = null
|
||||
) {
|
||||
super(message);
|
||||
|
||||
}
|
||||
|
||||
public getEmbed(e: UserError): EmbedBuilder {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import {PlaydateModel} from "../Database/Models/PlaydateModel";
|
|||
import {TimedEvent} from "./EventHandler.types";
|
||||
import {CleanupEvent} from "./Handlers/CleanupEvent";
|
||||
import {Logger} from "log4js";
|
||||
import {ElementChangedEvent} from "./EventClasses/ElementChangedEvent";
|
||||
import {GroupModel} from "../Database/Models/GroupModel";
|
||||
import {sendLeaderChangeNotificationEventHandler} from "./Handlers/LeaderChanged";
|
||||
|
||||
export class DefaultEvents {
|
||||
public static setupTimed() {
|
||||
|
|
@ -29,5 +32,9 @@ export class DefaultEvents {
|
|||
method: sendCreatedNotificationEventHandler,
|
||||
persistent: true
|
||||
});
|
||||
eventHandler.addHandler<ElementChangedEvent<GroupModel>>(ElementChangedEvent.name, {
|
||||
method: sendLeaderChangeNotificationEventHandler,
|
||||
persistent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
13
source/Events/EventClasses/ElementChangedEvent.ts
Normal file
13
source/Events/EventClasses/ElementChangedEvent.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import {Model} from "../../Database/Models/Model";
|
||||
import {EventType, NormalEvent} from "../EventHandler.types";
|
||||
import {DeepPartial} from "../../types/Partial";
|
||||
|
||||
export class ElementChangedEvent<T extends Model = Model> implements NormalEvent {
|
||||
constructor(
|
||||
public readonly tableName: string,
|
||||
public readonly changes: DeepPartial<T> & Model,
|
||||
) {
|
||||
}
|
||||
|
||||
type: EventType.Normal = EventType.Normal;
|
||||
}
|
||||
81
source/Events/Handlers/LeaderChanged.ts
Normal file
81
source/Events/Handlers/LeaderChanged.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import {ElementChangedEvent} from "../EventClasses/ElementChangedEvent";
|
||||
import {GroupModel} from "../../Database/Models/GroupModel";
|
||||
import Groups from "../../Database/tables/Groups";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupRepository} from "../../Database/Repositories/GroupRepository";
|
||||
import {ConfigurationHandler} from "../../Configuration/ConfigurationHandler";
|
||||
import {GroupConfigurationModel} from "../../Database/Models/GroupConfigurationModel";
|
||||
import {
|
||||
GroupConfigurationProvider,
|
||||
RuntimeGroupConfiguration
|
||||
} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||
import {GroupConfigurationRepository} from "../../Database/Repositories/GroupConfigurationRepository";
|
||||
import {DiscordClient} from "../../Discord/DiscordClient";
|
||||
import {EmbedLibrary} from "../../Discord/EmbedLibrary";
|
||||
import {roleMention, userMention} from "discord.js";
|
||||
import * as util from "node:util";
|
||||
import {ArrayUtils} from "../../Utilities/ArrayUtils";
|
||||
|
||||
const CHANGED_LINES = [
|
||||
"Look who now manages your group, its %s. He will do a fantastic job!",
|
||||
"Oh the 14th god changed... again, now its %s",
|
||||
"This group was given to %s",
|
||||
"This world was given over to %s, lets hope you survive the next adventure :smiling_imp:"
|
||||
]
|
||||
|
||||
export async function sendLeaderChangeNotificationEventHandler(event: ElementChangedEvent<GroupModel>) {
|
||||
if (event.tableName !== Groups.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.changes.leader?.memberid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = Container.get<GroupRepository>(GroupRepository.name).getById(event.changes.id);
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupConfig = new ConfigurationHandler<GroupConfigurationModel, RuntimeGroupConfiguration>(
|
||||
new GroupConfigurationProvider(
|
||||
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
||||
group
|
||||
)
|
||||
);
|
||||
|
||||
const targetChannel = groupConfig.getConfigurationByPath('channels.notifications').value;
|
||||
if (!targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const channel = await Container.get<DiscordClient>(DiscordClient.name).Client.channels.fetch(<string>targetChannel)
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channel.isSendable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
group,
|
||||
"You have a new leader",
|
||||
util.format(ArrayUtils.chooseRandom(CHANGED_LINES), userMention(group.leader.memberid))
|
||||
)
|
||||
|
||||
channel.send({
|
||||
content: roleMention(group.role.roleid),
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
allowedMentions: {
|
||||
roles: [group.role.roleid]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {ElementCreatedEvent} from "../EventClasses/ElementCreatedEvent";
|
||||
import {PlaydateModel} from "../../Database/Models/PlaydateModel";
|
||||
import PlaydateTableConfiguration from "../../Database/tables/Playdate";
|
||||
import {EmbedBuilder, roleMention, time} from "discord.js";
|
||||
import {EmbedBuilder, roleMention, time, userMention} from "discord.js";
|
||||
import {ArrayUtils} from "../../Utilities/ArrayUtils";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupConfigurationRepository} from "../../Database/Repositories/GroupConfigurationRepository";
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
GroupConfigurationProvider,
|
||||
RuntimeGroupConfiguration
|
||||
} from "../../Configuration/Groups/GroupConfigurationProvider";
|
||||
import {EmbedLibrary} from "../../Discord/EmbedLibrary";
|
||||
|
||||
const NEW_PLAYDATE_MESSAGES = [
|
||||
'A new playdate was added. Lets hope, your GM has not planned to kill you. >:]',
|
||||
|
|
@ -37,7 +38,7 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
)
|
||||
);
|
||||
|
||||
const targetChannel = groupConfig.getConfigurationByPath('channels.newPlaydates').value;
|
||||
const targetChannel = groupConfig.getConfigurationByPath('channels.notifications').value;
|
||||
if (!targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -55,17 +56,14 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("New Playdate added")
|
||||
.setDescription(
|
||||
ArrayUtils.chooseRandom(NEW_PLAYDATE_MESSAGES)
|
||||
)
|
||||
const embed = EmbedLibrary.withGroup(
|
||||
playdate.group,
|
||||
"New Playdate added",
|
||||
ArrayUtils.chooseRandom(NEW_PLAYDATE_MESSAGES)
|
||||
)
|
||||
.addFields({
|
||||
name: "Playdate:",
|
||||
value: `${time(playdate.from_time, "F")} - ${time(playdate.to_time, 'F')}`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${playdate.group.name}`
|
||||
});
|
||||
|
||||
channel.send({
|
||||
|
|
|
|||
39
source/Utilities/BuildContext.ts
Normal file
39
source/Utilities/BuildContext.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import * as fs from "node:fs";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {DataManager} from "discord.js";
|
||||
import * as util from "node:util";
|
||||
|
||||
export type BuildContext = {
|
||||
target: string,
|
||||
commitHash: string,
|
||||
commitLink: string,
|
||||
label: string,
|
||||
}
|
||||
|
||||
export class BuildContextGetter {
|
||||
private static EXPECTED_PATHS = [
|
||||
"./deploy.json",
|
||||
"./dist/deploy.json"
|
||||
];
|
||||
private static GIT_PATH = "https://git.iedsoftworks.com/neintonine/pnp-scheduler/commit/%s";
|
||||
|
||||
getContext(): Nullable<BuildContext> {
|
||||
const path = this.findValidPath();
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const jsonData = fs.readFileSync(path).toString();
|
||||
const data = JSON.parse(jsonData);
|
||||
return {
|
||||
...data,
|
||||
commitLink: util.format(BuildContextGetter.GIT_PATH, data.commitHash)
|
||||
}
|
||||
}
|
||||
|
||||
private findValidPath(): Nullable<string> {
|
||||
return BuildContextGetter.EXPECTED_PATHS.find((path) => {
|
||||
return fs.existsSync(path);
|
||||
});
|
||||
}
|
||||
}
|
||||
3
source/types/Partial.ts
Normal file
3
source/types/Partial.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export type DeepPartial<T> = T extends object ? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
} : T;
|
||||
Loading…
Add table
Add a link
Reference in a new issue