-
Notifications
You must be signed in to change notification settings - Fork 0
[v9] Pinメッセージ機能の実装 #229
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
base: feat/go
Are you sure you want to change the base?
[v9] Pinメッセージ機能の実装 #229
Changes from all commits
68e6a11
20b306a
f08166b
7f22aa5
1a79682
38f9e5b
142ccea
fcf63e8
5def9fb
b7bb442
162fc71
ab2214a
b03263e
e27125e
e07045d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| 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.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ | ||
| Embeds: &[]*discordgo.MessageEmbed{ | ||
| { | ||
| Title: title, | ||
| Description: description, | ||
| Color: config.Colors.Error, | ||
| Footer: footer, | ||
| Timestamp: time.Now().Format(time.RFC3339), | ||
| }, | ||
| }, | ||
| Flags: discordgo.MessageFlagsEphemeral, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ | ||||||||||||||||||||||||||||||||||
| Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, | ||||||||||||||||||||||||||||||||||
| Data: &discordgo.InteractionResponseData{ | ||||||||||||||||||||||||||||||||||
| Flags: discordgo.MessageFlagsEphemeral, | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| repo := repository.NewPinSettingRepository(ctx.DB) | ||||||||||||||||||||||||||||||||||
| existing, err := repo.GetByChannelID(i.ChannelID) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| replyPinError(s, i, config, "エラー", "ピン留めの取得に失敗しました。") | ||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if len(existing) > 0 { | ||||||||||||||||||||||||||||||||||
| replyPinError(s, i, config, "エラー", "このチャンネルには既にピン留めされたメッセージがあります。\n最初にそれを`/unpin`で解除してください。") | ||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+69
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| setting := &model.PinSetting{ | ||||||||||||||||||||||||||||||||||
| ID: i.ChannelID, | ||||||||||||||||||||||||||||||||||
| URL: sentMessage.ID, | ||||||||||||||||||||||||||||||||||
| Title: "Pinned Message", | ||||||||||||||||||||||||||||||||||
| Content: message, | ||||||||||||||||||||||||||||||||||
| GuildID: i.GuildID, | ||||||||||||||||||||||||||||||||||
| ChannelID: i.ChannelID, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if err := repo.Create(setting); err != nil { | ||||||||||||||||||||||||||||||||||
| replyPinError(s, i, config, "エラー", "ピン留めの保存に失敗しました。") | ||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| content := "メッセージをピン留めしました: `" + message + "`" | ||||||||||||||||||||||||||||||||||
| _, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ | ||||||||||||||||||||||||||||||||||
| Content: &content, | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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 "" | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
109
to
124
|
||||||||||||||||||||||||||||||||||
| func replyPinSuccess(s *discordgo.Session, i *discordgo.InteractionCreate, config *internal.Config, title string) { | |
| _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ | |
| Type: discordgo.InteractionResponseChannelMessageWithSource, | |
| Data: &discordgo.InteractionResponseData{ | |
| Embeds: []*discordgo.MessageEmbed{ | |
| { | |
| Title: title, | |
| Color: config.Colors.Success, | |
| Timestamp: time.Now().Format(time.RFC3339), | |
| }, | |
| }, | |
| Flags: discordgo.MessageFlagsEphemeral, | |
| }, | |
| }) | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| 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 | ||
| } | ||
|
Comment on lines
+74
to
+93
|
||
|
|
||
| successEmbed := &discordgo.MessageEmbed{ | ||
| Title: "メッセージをピン留めしました", | ||
| Description: "このメッセージは今後ピン留めされます。\nファイルは保存されないのでご注意ください。", | ||
| Color: config.Colors.Success, | ||
| Timestamp: time.Now().Format(time.RFC3339), | ||
| } | ||
|
|
||
| _, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ | ||
| Embeds: &[]*discordgo.MessageEmbed{successEmbed}, | ||
| Flags: discordgo.MessageFlagsEphemeral, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package general | ||
|
|
||
| import ( | ||
| "time" | ||
| "unibot/internal" | ||
| "unibot/internal/repository" | ||
|
|
||
| "github.com/bwmarrin/discordgo" | ||
| ) | ||
|
|
||
| func LoadUnpinCommandContext() *discordgo.ApplicationCommand { | ||
| perm := int64(discordgo.PermissionManageMessages) | ||
| dm := false | ||
| contexts := []discordgo.InteractionContextType{discordgo.InteractionContextGuild} | ||
| return &discordgo.ApplicationCommand{ | ||
| Name: "unpin", | ||
| Description: "ピン留めを解除します。", | ||
| DefaultMemberPermissions: &perm, | ||
| DMPermission: &dm, | ||
| Contexts: &contexts, | ||
| } | ||
| } | ||
|
|
||
| func Unpin(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { | ||
| config := ctx.Config | ||
|
|
||
| if !hasPinPermission(s, i) { | ||
| 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 { | ||
| content := "このチャンネルにはピン留めされたメッセージがありません。" | ||
| _, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ | ||
| Content: &content, | ||
| Flags: discordgo.MessageFlagsEphemeral, | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| err = repo.DeleteByChannelID(i.ChannelID) | ||
| if err != nil { | ||
| replyPinError(s, i, config, "エラーが発生しました", "ピン留めの解除中にエラーが発生しました。") | ||
| return | ||
| } | ||
|
Comment on lines
+47
to
+51
|
||
|
|
||
| successEmbed := &discordgo.MessageEmbed{ | ||
| Title: "ピン留めを解除しました", | ||
| Color: config.Colors.Success, | ||
| Timestamp: time.Now().Format(time.RFC3339), | ||
| } | ||
|
|
||
| _, _ = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ | ||
| Embeds: &[]*discordgo.MessageEmbed{successEmbed}, | ||
| Flags: discordgo.MessageFlagsEphemeral, | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The modal placeholder text states "すでにPinされたメッセージがある場合は上書きされます" (existing pinned messages will be overwritten), but this contradicts the actual behavior in pin_select.go (lines 61-64) which prevents pinning when a message already exists. This discrepancy in the user-facing message creates confusion about the expected behavior. Ensure the placeholder text accurately reflects the actual implementation, or align the implementation with this stated behavior by allowing overwrites.