Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
26fe032
スケジューラ機能を追加し、メッセージの送信と更新を実装
Aqua-218 Jan 27, 2026
a786c1e
スケジュールコマンドの実装とエラーハンドリングを追加
Aqua-218 Jan 27, 2026
f8b6fed
スケジュールコマンドのセット機能を追加し、繰り返し投稿のモーダルを実装
Aqua-218 Jan 27, 2026
7e04973
予約投稿の一覧表示機能を追加
Aqua-218 Jan 27, 2026
538d445
予約投稿削除コマンドを実装
Aqua-218 Jan 27, 2026
f3f61ca
モーダル送信処理を実装し、1回限りおよび繰り返しのスケジュール作成機能を追加
Aqua-218 Jan 27, 2026
eb92c29
スケジュールコマンドを追加し、モーダル送信処理をハンドルする機能を実装
Aqua-218 Jan 27, 2026
f6b36ed
スケジュールマネージャーを追加し、ボットの起動時に開始および停止処理を実装
Aqua-218 Jan 27, 2026
74f738f
cronパッケージを追加し、依存関係を更新
Aqua-218 Jan 27, 2026
3906490
go fmt
Aqua-218 Jan 27, 2026
6f10977
cron形式の変換機能を追加し、スケジュール設定のエラーメッセージを改善
Aqua-218 Jan 27, 2026
b57eb1b
pinコマンド追加
Aqua-218 Jan 28, 2026
9ffb0de
unpinコマンド追加
Aqua-218 Jan 28, 2026
44e4753
pin選択コマンド追加
Aqua-218 Jan 28, 2026
d850fbf
pinモーダル追加
Aqua-218 Jan 28, 2026
588ac84
pin設定削除追加
Aqua-218 Jan 28, 2026
4078a57
pinモーダル受付
Aqua-218 Jan 28, 2026
0b16c0a
pin再送処理
Aqua-218 Jan 28, 2026
b57ec03
pinコマンド登録
Aqua-218 Jan 28, 2026
5fe6c87
pinハンドラ登録
Aqua-218 Jan 28, 2026
e837c5f
pinコマンドからスケジュール機能を削除し、ピンエラー応答にリクエスト者情報を追加
Aqua-218 Jan 28, 2026
0d9281a
PinSettingの作成と更新時にギルドの存在を確認する処理を追加
Aqua-218 Jan 29, 2026
a22ebc3
[v9] ヘルプコマンドの実装 (#228)
ibuki-hum4 Jan 29, 2026
c710733
スケジュールコマンドを追加し、モーダル送信処理をハンドルする機能を実装
Aqua-218 Jan 27, 2026
a6526fb
schedule応答をEdit化
Aqua-218 Jan 31, 2026
3fd8623
スケジュールモーダルの処理を改善し、ボタンのハンドラを追加
Aqua-218 Feb 10, 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
5 changes: 5 additions & 0 deletions src/cmd/bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"unibot/internal/bot/command"
"unibot/internal/bot/handler"
"unibot/internal/db"
"unibot/internal/scheduler"
)

func main() {
Expand Down Expand Up @@ -57,6 +58,10 @@ func main() {
}
defer dg.Close()

schedulerManager := scheduler.NewManager(ctx, dg)
schedulerManager.Start()
defer schedulerManager.Stop()

log.Println("Bot is running...")

// Register commands
Expand Down
1 change: 1 addition & 0 deletions src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
Expand Down
4 changes: 4 additions & 0 deletions src/internal/bot/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import (
var Commands = []*discordgo.ApplicationCommand{
general.LoadPingCommandContext(),
general.LoadAboutCommandContext(),
general.LoadPinCommandContext(),
general.LoadUnpinCommandContext(),
general.LoadPinSelectCommandContext(),
general.LoadTtsCommandContext(),
general.LoadScheduleCommandContext(),
general.LoadHelpCommandContext(),
admin.LoadMaintenanceCommandContext(),
}
1 change: 0 additions & 1 deletion src/internal/bot/command/general/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ func Help(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.Interacti
},
Timestamp: time.Now().Format(time.RFC3339),
}

_, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{
Embeds: &[]*discordgo.MessageEmbed{responseEmbed},
})
Expand Down
91 changes: 91 additions & 0 deletions src/internal/bot/command/general/pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package general

import (
"time"
"unibot/internal"

"github.com/bwmarrin/discordgo"
)

func LoadPinCommandContext() *discordgo.ApplicationCommand {
perm := int64(discordgo.PermissionManageMessages)
dm := false
contexts := []discordgo.InteractionContextType{discordgo.InteractionContextGuild}
return &discordgo.ApplicationCommand{
Name: "pin",
Description: "メッセージをピン留めします。",
DefaultMemberPermissions: &perm,
DMPermission: &dm,
Contexts: &contexts,
}
}

func Pin(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) {
config := ctx.Config

if !hasPinPermission(s, i) {
replyPinError(s, i, config, "権限がありません", "この操作を実行する権限がありません。")
return
}

showPinModal(s, i)
}

func showPinModal(s *discordgo.Session, i *discordgo.InteractionCreate) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseModal,
Data: &discordgo.InteractionResponseData{
CustomID: "pin_message",
Title: "メッセージのピン留め",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{Components: []discordgo.MessageComponent{
discordgo.TextInput{
CustomID: "message",
Label: "投稿内容",
Style: discordgo.TextInputParagraph,
Placeholder: "投稿内容を入力してください。すでにPinされたメッセージがある場合は上書きされます。",
Required: true,
},
}},
},
},
})
}

func hasPinPermission(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if i.Member == nil || i.Member.User == nil {
return false
}
perms, err := s.UserChannelPermissions(i.Member.User.ID, i.ChannelID)
if err != nil {
return false
}
return perms&discordgo.PermissionManageMessages != 0
}

func replyPinError(s *discordgo.Session, i *discordgo.InteractionCreate, config *internal.Config, title, description string) {
footer := &discordgo.MessageEmbedFooter{Text: "Requested by Unknown"}
if i.Member != nil {
footer.Text = "Requested by " + i.Member.DisplayName()
footer.IconURL = i.Member.AvatarURL("")
} else if i.User != nil {
footer.Text = "Requested by " + i.User.Username
footer.IconURL = i.User.AvatarURL("")
}

_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: title,
Description: description,
Color: config.Colors.Error,
Footer: footer,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
122 changes: 122 additions & 0 deletions src/internal/bot/command/general/pin_modal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package general

import (
"unibot/internal"
"unibot/internal/model"
"unibot/internal/repository"

"github.com/bwmarrin/discordgo"
)

// HandlePinModalSubmit はピン留めモーダルの送信を処理する
func HandlePinModalSubmit(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) bool {
data := i.ModalSubmitData()
if data.CustomID != "pin_message" {
return false
}

config := ctx.Config
if !hasPinPermission(s, i) {
replyPinError(s, i, config, "権限がありません", "この操作を実行する権限がありません。")
return true
}

message := getPinModalValue(data, "message")
if message == "" {
replyPinError(s, i, config, "入力エラー", "投稿内容を入力してください。")
return true
}

channel, err := s.State.Channel(i.ChannelID)
if err != nil {
channel, _ = s.Channel(i.ChannelID)
}
if channel == nil || channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM {
replyPinError(s, i, config, "エラー", "このチャンネルではメッセージを送信できません。")
return true
}

embed := &discordgo.MessageEmbed{
Description: message,
Color: config.Colors.Success,
Footer: &discordgo.MessageEmbedFooter{
Text: "Pinned Message",
},
}

sentMessage, err := s.ChannelMessageSendEmbed(i.ChannelID, embed)
if err != nil {
replyPinError(s, i, config, "エラー", "メッセージの送信に失敗しました。")
return true
}

repo := repository.NewPinSettingRepository(ctx.DB)
setting := &model.PinSetting{
ID: i.ChannelID,
URL: sentMessage.ID,
Title: "Pinned Message",
Content: message,
GuildID: i.GuildID,
ChannelID: i.ChannelID,
}

existing, err := repo.GetByChannelID(i.ChannelID)
if err != nil {
replyPinError(s, i, config, "エラー", "ピン留めの保存に失敗しました。")
return true
}
if len(existing) == 0 {
if err := repo.Create(setting); err != nil {
replyPinError(s, i, config, "エラー", "ピン留めの保存に失敗しました。")
return true
}
} else {
if err := repo.Update(setting); err != nil {
replyPinError(s, i, config, "エラー", "ピン留めの保存に失敗しました。")
return true
}
}

_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "メッセージをピン留めしました: `" + message + "`",
Flags: discordgo.MessageFlagsEphemeral,
},
})

return true
}

func getPinModalValue(data discordgo.ModalSubmitInteractionData, customID string) string {
for _, comp := range data.Components {
switch row := comp.(type) {
case *discordgo.ActionsRow:
if value := getTextInputValue(row.Components, customID); value != "" {
return value
}
case discordgo.ActionsRow:
if value := getTextInputValue(row.Components, customID); value != "" {
return value
}
}
}

return ""
}

func getTextInputValue(components []discordgo.MessageComponent, customID string) string {
for _, component := range components {
switch input := component.(type) {
case *discordgo.TextInput:
if input.CustomID == customID {
return input.Value
}
case discordgo.TextInput:
if input.CustomID == customID {
return input.Value
}
}
}
return ""
}
109 changes: 109 additions & 0 deletions src/internal/bot/command/general/pin_select.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package general

import (
"time"
"unibot/internal"
"unibot/internal/model"
"unibot/internal/repository"

"github.com/bwmarrin/discordgo"
)

func LoadPinSelectCommandContext() *discordgo.ApplicationCommand {
contexts := []discordgo.InteractionContextType{discordgo.InteractionContextGuild}
return &discordgo.ApplicationCommand{
Name: "Pinするメッセージを選択",
Type: discordgo.MessageApplicationCommand,
Contexts: &contexts,
}
}

func PinSelect(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) {
config := ctx.Config

if !hasPinPermission(s, i) {
replyPinError(s, i, config, "権限がありません", "この操作を実行する権限がありません。")
return
}

data := i.ApplicationCommandData()
if data.Resolved == nil || data.Resolved.Messages == nil {
replyPinError(s, i, config, "エラー", "メッセージの取得に失敗しました。")
return
}

targetMsg, ok := data.Resolved.Messages[data.TargetID]
if !ok || targetMsg == nil {
replyPinError(s, i, config, "エラー", "メッセージの取得に失敗しました。")
return
}

if targetMsg.Author != nil && targetMsg.Author.Bot {
replyPinError(s, i, config, "エラー", "ボットのメッセージはピン留めできません。")
return
}

channel, err := s.State.Channel(i.ChannelID)
if err != nil {
channel, _ = s.Channel(i.ChannelID)
}
if channel == nil || channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM {
replyPinError(s, i, config, "エラー", "このチャンネルではメッセージをピン留めできません。")
return
}

repo := repository.NewPinSettingRepository(ctx.DB)
settings, err := repo.GetByChannelID(i.ChannelID)
if err != nil {
replyPinError(s, i, config, "エラー", "ピン留めの取得に失敗しました。")
return
}
if len(settings) > 0 {
replyPinError(s, i, config, "エラー", "このチャンネルには既にピン留めされたメッセージがあります。\n最初にそれを`/unpin`で解除してください。")
return
}

embed := &discordgo.MessageEmbed{
Description: targetMsg.Content,
Color: config.Colors.Success,
Footer: &discordgo.MessageEmbedFooter{
Text: "Pinned Message",
},
}

sentMessage, err := s.ChannelMessageSendEmbed(i.ChannelID, embed)
if err != nil {
replyPinError(s, i, config, "エラー", "メッセージの送信に失敗しました。")
return
}

setting := &model.PinSetting{
ID: i.ChannelID,
URL: sentMessage.ID,
Title: "Pinned Message",
Content: targetMsg.Content,
GuildID: i.GuildID,
ChannelID: i.ChannelID,
}

err = repo.Create(setting)
if err != nil {
replyPinError(s, i, config, "エラー", "ピン留めの保存に失敗しました。")
return
}

successEmbed := &discordgo.MessageEmbed{
Title: "メッセージをピン留めしました",
Description: "このメッセージは今後ピン留めされます。\nファイルは保存されないのでご注意ください。",
Color: config.Colors.Success,
Timestamp: time.Now().Format(time.RFC3339),
}

_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{successEmbed},
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
Loading