Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4a1abbf
refactor: create services folder and move reactionRole
sora81dev Jun 12, 2026
1f767bc
refactor: create handlerEvents and move onMessageReactionRemove
sora81dev Jun 12, 2026
c59b0e5
chore: update package-lock.json
sora81dev Jun 12, 2026
a13a42f
refactor: move messageReactionAdd events to handlers/events
sora81dev Jun 12, 2026
6bb0ffc
chore: remove unused imports in index.ts
sora81dev Jun 12, 2026
9b3403d
refactor: add env file and apply to index.ts
sora81dev Jun 12, 2026
8c2e4ab
refactor: move env.ts to configs/env.ts
sora81dev Jun 12, 2026
a04631c
fix: logTitle in onMessageReactionRemove event handler
sora81dev Jun 12, 2026
d732ca2
fix: log message in event handler
sora81dev Jun 17, 2026
5904f22
refactor: add runtimeConfig for reactionRoleMessageId
sora81dev Jun 17, 2026
9303e4e
refactor: apply runtimeConfig for index.ts
sora81dev Jun 17, 2026
eee0d04
refactor: add cemicolon to env.ts
sora81dev Jun 17, 2026
4683c14
refactor: migrate VC_ROLE_ID and NOTIFIER_ROLE_ID to configFile
sora81dev Jul 5, 2026
af5e7bd
Merge branch 'sf-kosen:main' into refactor/conciseindexFile
sora81dev Jul 15, 2026
9873435
refactor: migrate BOT_ID and REACTIONROLE_CHANNEL_ID to env.ts
sora81dev Jul 15, 2026
f5a897f
refactor: move runSafely and addRoleSafely to utils/safe.ts
sora81dev Jul 24, 2026
1862bd3
refactor: remove unused import
sora81dev Jul 24, 2026
8376d55
chore: remove reactionRoleMessage in index.ts
sora81dev Jul 24, 2026
1a8cb58
refactor: separate guildMemberAdd event
sora81dev Jul 24, 2026
965c1a9
refactor: export client for future refactor
sora81dev Jul 24, 2026
f555f67
refactor: separate updateMemberCount from index.ts
sora81dev Jul 24, 2026
f3895f7
refactor: auto loading events
sora81dev Jul 24, 2026
30e7015
fix: reactionRole channelid in checkReactionRoleMessage jobs
sora81dev Jul 24, 2026
6b416a2
refactor: separate threadCrate from index.ts
sora81dev Jul 24, 2026
451bae5
refactor: separate guildMemberUpdate from index.ts
sora81dev Jul 24, 2026
98a5d73
refactor: separate logAndSendError from index.ts
sora81dev Jul 24, 2026
0bb1166
refactor: separete interactionCreate from index.ts
sora81dev Jul 24, 2026
c7c47eb
refactor: separate commandRegister from index.ts
sora81dev Jul 24, 2026
8e9e446
refactor: more simple checkReactionRoleMessage
sora81dev Jul 24, 2026
49ba0de
chore: remove client args
sora81dev Jul 24, 2026
4a02dcf
chore: move register to jobs from utils folder
sora81dev Jul 24, 2026
6fddf90
chore: remove package-lock for conflicts
sora81dev Jul 24, 2026
a31abb9
fix: repl role_id to roleID in addReactionRole
sora81dev Jul 24, 2026
7822495
fix: remove client args
sora81dev Jul 24, 2026
19b88a5
refactor: remove unused var
sora81dev Jul 24, 2026
2bfc777
refactor: remove magic number
sora81dev Jul 24, 2026
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
876 changes: 0 additions & 876 deletions package-lock.json

This file was deleted.

25 changes: 25 additions & 0 deletions src/configs/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import dotenv from "dotenv";

dotenv.config();

export const env = {
info: {
botID: process.env.BOT_ID,
guildID: "1454446371221536788",
},
tokens: {
discordToken: process.env.DISCORD_TOKEN,
},
channelID: {
reactionRole: process.env.REACTIONROLE_CHANNEL_ID,
recruitThread: "1454446371221536788",
recruitNotice: "1461005041409327463",
memberCount: "1454473598973509697",
},
roleID: {
notifier: process.env.NOTIFIER_ROLE_ID,
vc: process.env.VC_ROLE_ID,
bot: "1454099602641780737",
student: "1454446371221536788",
},
};
3 changes: 3 additions & 0 deletions src/configs/runtimeConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const runtimeConfig: {
reactionRoleMessageId?: string;
} = {};
245 changes: 129 additions & 116 deletions src/handlers/button/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,140 +2,153 @@
* Ticket button interaction handler
*/
import {
ButtonInteraction,
MessageFlags,
EmbedBuilder,
Colors,
ChannelType,
PermissionFlagsBits,
type OverwriteResolvable,
ButtonInteraction,
MessageFlags,
EmbedBuilder,
Colors,
ChannelType,
PermissionFlagsBits,
type OverwriteResolvable,
} from "discord.js";
import { Action } from "../../types/action";

function createErrorEmbed(description: string) {
return new EmbedBuilder()
.setTitle("エラー")
.setDescription(description)
.setColor(Colors.Red);
return new EmbedBuilder()
.setTitle("エラー")
.setDescription(description)
.setColor(Colors.Red);
}

export default {
data: {
action: "ticket-open",
flags: MessageFlags.Ephemeral,
defer: true,
},
data: {
action: "ticket-open",
flags: MessageFlags.Ephemeral,
defer: true,
},

async execute(interaction: ButtonInteraction) {
const guild = interaction.guild;
const targetUser = interaction.user;
async execute(interaction: ButtonInteraction) {
const guild = interaction.guild;
const targetUser = interaction.user;

if (!guild) {
await interaction.followUp({
embeds: [createErrorEmbed("サーバー情報を取得できませんでした。")],
ephemeral: true,
});
return;
}
if (!guild) {
await interaction.followUp({
embeds: [createErrorEmbed("サーバー情報を取得できませんでした。")],
ephemeral: true,
});
return;
}

let categoryId: string | undefined;
try {
const customId = JSON.parse(interaction.customId);
const rawCategory = customId?.value?.category;
categoryId =
typeof rawCategory === "string"
? rawCategory
: typeof rawCategory?.category === "string"
? rawCategory.category
: undefined;
} catch {
await interaction.followUp({
embeds: [createErrorEmbed("ボタンの情報を正しく読み取れませんでした。")],
ephemeral: true,
});
return;
}
let categoryId: string | undefined;
try {
const customId = JSON.parse(interaction.customId);
const rawCategory = customId?.value?.category;
categoryId =
typeof rawCategory === "string"
? rawCategory
: typeof rawCategory?.category === "string"
? rawCategory.category
: undefined;
} catch {
await interaction.followUp({
embeds: [
createErrorEmbed("ボタンの情報を正しく読み取れませんでした。"),
],
ephemeral: true,
});
return;
}

if (typeof categoryId !== "string" || categoryId.length === 0) {
await interaction.followUp({
embeds: [createErrorEmbed("カテゴリー情報が不正です。")],
ephemeral: true,
});
return;
}
if (typeof categoryId !== "string" || categoryId.length === 0) {
await interaction.followUp({
embeds: [createErrorEmbed("カテゴリー情報が不正です。")],
ephemeral: true,
});
return;
}

const category = guild.channels.cache.get(categoryId);
if (!category || category.type !== ChannelType.GuildCategory) {
await interaction.followUp({
embeds: [createErrorEmbed("指定されたカテゴリーが見つからないか、カテゴリーではありません。")],
ephemeral: true,
});
return;
}
const category = guild.channels.cache.get(categoryId);
if (!category || category.type !== ChannelType.GuildCategory) {
await interaction.followUp({
embeds: [
createErrorEmbed(
"指定されたカテゴリーが見つからないか、カテゴリーではありません。",
),
],
ephemeral: true,
});
return;
}

const ticketChannelName = `ticket-${targetUser.id}`;
const existingTicket = guild.channels.cache.find(
(channel) =>
channel.parentId === category.id &&
channel.name === ticketChannelName,
);
const ticketChannelName = `ticket-${targetUser.id}`;
const existingTicket = guild.channels.cache.find(
(channel) =>
channel.parentId === category.id && channel.name === ticketChannelName,
);

if (existingTicket) {
const embed = new EmbedBuilder()
.setTitle("既存のチケットがあります")
.setDescription(`すでにチケットチャンネル ${existingTicket} があります。`)
.setColor(Colors.Yellow);
if (existingTicket) {
const embed = new EmbedBuilder()
.setTitle("既存のチケットがあります")
.setDescription(
`すでにチケットチャンネル ${existingTicket} があります。`,
)
.setColor(Colors.Yellow);

await interaction.followUp({ embeds: [embed], ephemeral: true });
return;
}
await interaction.followUp({ embeds: [embed], ephemeral: true });
return;
}

try {
const permissionOverwrites: OverwriteResolvable[] = [
{
id: guild.roles.everyone.id,
deny: [PermissionFlagsBits.ViewChannel],
},
{
id: targetUser.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory,
],
},
];
try {
const permissionOverwrites: OverwriteResolvable[] = [
{
id: guild.roles.everyone.id,
deny: [PermissionFlagsBits.ViewChannel],
},
{
id: targetUser.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory,
],
},
];

if (guild.members.me) {
permissionOverwrites.push({
id: guild.members.me.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory,
PermissionFlagsBits.ManageChannels,
],
});
}
if (guild.members.me) {
permissionOverwrites.push({
id: guild.members.me.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.ReadMessageHistory,
PermissionFlagsBits.ManageChannels,
],
});
}

const ticketChannel = await guild.channels.create({
name: ticketChannelName,
type: ChannelType.GuildText,
parent: category.id,
permissionOverwrites,
});
const ticketChannel = await guild.channels.create({
name: ticketChannelName,
type: ChannelType.GuildText,
parent: category.id,
permissionOverwrites,
});

const embed = new EmbedBuilder()
.setTitle("チケットが作成されました")
.setDescription(`チケットチャンネル ${ticketChannel} が作成されました。`)
.setColor(Colors.Green);
await interaction.followUp({ embeds: [embed], ephemeral: true });
} catch (error) {
console.error(error);
await interaction.followUp({
embeds: [createErrorEmbed("チケットチャンネルの作成中にエラーが発生しました。")],
ephemeral: true,
});
}
const embed = new EmbedBuilder()
.setTitle("チケットが作成されました")
.setDescription(
`チケットチャンネル ${ticketChannel} が作成されました。`,
)
.setColor(Colors.Green);
await interaction.followUp({ embeds: [embed], ephemeral: true });
} catch (error) {
console.error(error);
await interaction.followUp({
embeds: [
createErrorEmbed(
"チケットチャンネルの作成中にエラーが発生しました。",
),
],
ephemeral: true,
});
}
},
} as Action<ButtonInteraction>;
23 changes: 23 additions & 0 deletions src/handlers/events/guildMemberAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { GuildMember } from "discord.js";
import { addRoleSafely } from "../../utils/safe";
import { client } from "../..";
import { env } from "../../configs/env";

client.on("guildMemberAdd", handler);

async function handler(member: GuildMember) {
const time = Date.now();
const date = new Date(time);

if (member.user.bot) {
// BOTロールを付与
await addRoleSafely(member, env.roleID.bot, "bot");
}

// 年に応じたロールを付与
if (date.getFullYear() == 2025) {
await addRoleSafely(member, "1454661774576980090", "2025 student");
} else if (date.getFullYear() == 2026) {
await addRoleSafely(member, "1455864840630308925", "2026 student");
}
}
8 changes: 8 additions & 0 deletions src/handlers/events/guildMemberRemove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { client } from "./../../index";
import { updateMemberCount } from "../../jobs/updateMemberCount";

client.on("guildMemberRemove", handler);

async function handler() {
await updateMemberCount();
}
32 changes: 32 additions & 0 deletions src/handlers/events/guildMemberUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { GuildMember, PartialGuildMember } from "discord.js";

import { client } from "../..";
import { updateMemberCount } from "../../jobs/updateMemberCount";
import { env } from "../../configs/env";

client.on("guildMemberUpdate", handler);

async function handler(
oldMember: GuildMember | PartialGuildMember,
newMember: GuildMember | PartialGuildMember,
) {
console.log("[INFO] Detect guildMemberUpdate");
console.log("-> NEW MEMBER");
console.log(
` -> hasStudentRole: ${newMember.roles.cache.has(env.roleID.student)}`,
);
console.log("-> OLD MEMBER");
console.log(
` -> hasStudentRole: ${oldMember.roles.cache.has(env.roleID.student)}`,
);

// 学生ロールの付与、剥奪を検知して学生数カウントを更新
if (
(!oldMember.roles.cache.has(env.roleID.student) &&
newMember.roles.cache.has(env.roleID.student)) ||
(oldMember.roles.cache.has(env.roleID.student) &&
!newMember.roles.cache.has(env.roleID.student))
) {
await updateMemberCount();
}
}
Loading