Compare commits
10 Commits
de5ed6b67d
...
97395ab7f9
Author | SHA1 | Date |
---|---|---|
Maxim Lebedev | 97395ab7f9 | |
Maxim Lebedev | 42ef399a5c | |
Maxim Lebedev | 1831742dcc | |
Maxim Lebedev | 7a97fa48ba | |
Maxim Lebedev | 4844174303 | |
Maxim Lebedev | 2684561233 | |
Maxim Lebedev | 3679416087 | |
Maxim Lebedev | fbc38ad34f | |
Maxim Lebedev | 500ee05d2a | |
Maxim Lebedev | 9c1ff27d1a |
|
@ -0,0 +1,111 @@
|
|||
// TODO(toby3d): save lines in a public channel for references?
|
||||
package discord
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
const InteractionID string = "line"
|
||||
|
||||
func init() {
|
||||
for _, entry := range [...]domain.CatalogEntry{
|
||||
{
|
||||
Tag: language.English, Key: "You must describe the subject of the line.",
|
||||
Message: "You must describe the subject of the line.",
|
||||
},
|
||||
{
|
||||
Tag: language.Russian, Key: "You must describe the subject of the line.",
|
||||
Message: "Вы должны описать предмет линии.",
|
||||
},
|
||||
{Tag: language.English, Key: "(Line: %s)", Message: "(Line: %s)"},
|
||||
{Tag: language.Russian, Key: "(Line: %s)", Message: "(Линия: %s)"},
|
||||
} {
|
||||
switch msg := entry.Message.(type) {
|
||||
case string:
|
||||
message.SetString(entry.Tag, entry.Key, msg)
|
||||
case catalog.Message:
|
||||
message.Set(entry.Tag, entry.Key, msg)
|
||||
case []catalog.Message:
|
||||
message.Set(entry.Tag, entry.Key, msg...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(logger *log.Logger) *Handler {
|
||||
return &Handler{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeReady(s *discordgo.Session, r *discordgo.Ready) {
|
||||
for i := range r.Guilds {
|
||||
if _, err := s.ApplicationCommandCreate(s.State.User.ID, r.Guilds[i].ID, &discordgo.ApplicationCommand{
|
||||
Name: InteractionID,
|
||||
NameLocalizations: &map[discordgo.Locale]string{
|
||||
discordgo.Russian: "линия",
|
||||
},
|
||||
Description: "request a complete content ban",
|
||||
DescriptionLocalizations: &map[discordgo.Locale]string{
|
||||
discordgo.Russian: "запросить полный запрет контента",
|
||||
},
|
||||
Options: []*discordgo.ApplicationCommandOption{{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "description",
|
||||
NameLocalizations: map[discordgo.Locale]string{
|
||||
discordgo.Russian: "описание",
|
||||
},
|
||||
Description: "description of the content to be banned",
|
||||
DescriptionLocalizations: map[discordgo.Locale]string{
|
||||
discordgo.Russian: "описание контента который требуется запретить",
|
||||
},
|
||||
Required: true,
|
||||
}},
|
||||
}); err != nil {
|
||||
h.logger.Println("cannot register command:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.Type != discordgo.InteractionApplicationCommand {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
if data.Name != InteractionID {
|
||||
return
|
||||
}
|
||||
|
||||
printer := message.NewPrinter(language.Make(string(i.Locale)))
|
||||
|
||||
if len(data.Options) == 0 {
|
||||
go s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: printer.Sprintf("You must describe the subject of the line."),
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: printer.Sprintf("(Line: %s)", data.Options[0].StringValue()),
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"log"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/message/catalog"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
"source.toby3d.me/toby3d/alice/internal/search"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
logger *log.Logger
|
||||
searches search.UseCase
|
||||
cards fs.FS
|
||||
cache map[domain.Search]*discordgo.MessageEmbedImage
|
||||
}
|
||||
|
||||
const InteractionID string = "search"
|
||||
|
||||
func init() {
|
||||
for _, entry := range [...]domain.CatalogEntry{
|
||||
{Tag: language.English, Key: "You go on a search for something that can help you...", Message: "You go on a search for something that can help you..."},
|
||||
{Tag: language.Russian, Key: "You go on a search for something that can help you...", Message: "Вы отправляетесь на поиски чего-нибудь, что может вам помочь..."},
|
||||
{Tag: language.English, Key: "Unfortunately, you've already found all you can find.", Message: "Unfortunately, you've already found all you can find."},
|
||||
{Tag: language.Russian, Key: "Unfortunately, you've already found all you can find.", Message: "К сожалению, вы уже нашли всё, что могли найти."},
|
||||
} {
|
||||
switch msg := entry.Message.(type) {
|
||||
case string:
|
||||
message.SetString(entry.Tag, entry.Key, msg)
|
||||
case catalog.Message:
|
||||
message.Set(entry.Tag, entry.Key, msg)
|
||||
case []catalog.Message:
|
||||
message.Set(entry.Tag, entry.Key, msg...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(searches search.UseCase, cards fs.FS, logger *log.Logger) *Handler {
|
||||
return &Handler{
|
||||
cards: cards,
|
||||
logger: logger,
|
||||
searches: searches,
|
||||
cache: make(map[domain.Search]*discordgo.MessageEmbedImage),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeReady(s *discordgo.Session, r *discordgo.Ready) {
|
||||
for i := range r.Guilds {
|
||||
if _, err := s.ApplicationCommandCreate(s.State.User.ID, r.Guilds[i].ID, &discordgo.ApplicationCommand{
|
||||
Name: InteractionID,
|
||||
NameLocalizations: &map[discordgo.Locale]string{
|
||||
discordgo.Russian: "поиск",
|
||||
},
|
||||
Description: "do a search",
|
||||
DescriptionLocalizations: &map[discordgo.Locale]string{
|
||||
discordgo.Russian: "производит поиск",
|
||||
},
|
||||
}); err != nil {
|
||||
h.logger.Println("cannot register command:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.Type != discordgo.InteractionApplicationCommand {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
|
||||
if data.Name != InteractionID {
|
||||
return
|
||||
}
|
||||
|
||||
printer := message.NewPrinter(language.Make(string(i.Locale)))
|
||||
respond := &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: printer.Sprintf("You go on a search for something that can help you..."),
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
}
|
||||
|
||||
// NOTE(toby3d): see if there's anything else we can find.
|
||||
found, err := h.searches.Search(context.Background())
|
||||
if err != nil {
|
||||
respond.Data.Content = printer.Sprintf("Unfortunately, you've already found all you can find.")
|
||||
|
||||
go s.InteractionRespond(i.Interaction, respond)
|
||||
h.logger.Println("cannot do search interaction:", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go s.InteractionRespond(i.Interaction, respond)
|
||||
|
||||
// NOTE(toby3d): send search card into personal character channel
|
||||
roles, err := s.GuildRoles(i.GuildID)
|
||||
if err != nil {
|
||||
h.logger.Printf("cannot fetch guild roles: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE(toby3d): get current user character via roles.
|
||||
var character domain.Character
|
||||
|
||||
for _, role := range i.Member.Roles {
|
||||
for j := range roles {
|
||||
if role != roles[j].ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if character, err = domain.ParseCharacter(strings.ToLower(roles[j].Name)); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if character == domain.CharacterUnd {
|
||||
h.logger.Println("cannot check user character role")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE(toby3d): find personal character text channel for sending card.
|
||||
channels, err := s.GuildChannels(i.GuildID)
|
||||
if err != nil {
|
||||
h.logger.Printf("cannot fetch guild channels: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var characterChannel *discordgo.Channel
|
||||
|
||||
for _, channel := range channels {
|
||||
if !strings.EqualFold(channel.Name, character.String()+"-clues") {
|
||||
continue
|
||||
}
|
||||
|
||||
characterChannel = channel
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if characterChannel == nil {
|
||||
h.logger.Printf("cannot find %s text channel", character)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE(toby3d): send card from cache, if exists.
|
||||
message := &discordgo.MessageSend{
|
||||
Content: "",
|
||||
File: nil,
|
||||
Embed: &discordgo.MessageEmbed{
|
||||
Type: discordgo.EmbedTypeImage,
|
||||
Color: 15844367, // NOTE(toby3d): gold
|
||||
Image: h.cache[found],
|
||||
},
|
||||
}
|
||||
|
||||
if h.cache[found] == nil {
|
||||
var f fs.File
|
||||
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png"} {
|
||||
f, err = h.cards.Open(filepath.Join("assets", "cards", "search", found.String()+ext))
|
||||
if err != nil {
|
||||
h.logger.Printf("cannot find %s card with extention '%s': %s", found, ext, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
h.logger.Printf("cannot find any cards for %s, check your embeding directory contents", found)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
info, _ := f.Stat()
|
||||
message.File = &discordgo.File{
|
||||
Name: info.Name(),
|
||||
ContentType: mime.TypeByExtension(filepath.Ext(info.Name())),
|
||||
Reader: f,
|
||||
}
|
||||
message.Embed.Image = &discordgo.MessageEmbedImage{
|
||||
URL: "attachment://" + info.Name(),
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := s.ChannelMessageSendComplex(characterChannel.ID, message)
|
||||
if err != nil {
|
||||
h.logger.Println("cannot send search card:", err)
|
||||
}
|
||||
|
||||
// NOTE(toby3d): if card is not cached, then store response embed to reuse later.
|
||||
if h.cache[found] != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.cache[found] = resp.Embeds[0].Image
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
// Load check what provided search is available, returns true if it is.
|
||||
Load(ctx context.Context, search domain.Search) (bool, error)
|
||||
|
||||
// Remember add provided search into the memory, marked it as
|
||||
// unavailable for searching again.
|
||||
Remember(ctx context.Context, search domain.Search) (bool, error)
|
||||
|
||||
// Forget removed provided search from the memory, making it available
|
||||
// for searching again.
|
||||
Forget(ctx context.Context, search domain.Search) (bool, error)
|
||||
|
||||
// Fetch returns all remembered searches.
|
||||
Fetch(ctx context.Context) ([]domain.Search, error)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
"source.toby3d.me/toby3d/alice/internal/search"
|
||||
)
|
||||
|
||||
type memorySearchRepository struct {
|
||||
mutex *sync.RWMutex
|
||||
searches map[domain.Search]struct{}
|
||||
}
|
||||
|
||||
// Forget implements search.Repository.
|
||||
func (repo *memorySearchRepository) Forget(ctx context.Context, s domain.Search) (bool, error) {
|
||||
ok, err := repo.Load(ctx, s)
|
||||
if err != nil {
|
||||
return ok, fmt.Errorf("cannot load search from memory before forget: %w", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
repo.mutex.Lock()
|
||||
defer repo.mutex.Unlock()
|
||||
|
||||
delete(repo.searches, s)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Remember implements search.Repository.
|
||||
func (repo *memorySearchRepository) Remember(ctx context.Context, s domain.Search) (bool, error) {
|
||||
ok, err := repo.Load(ctx, s)
|
||||
if err != nil {
|
||||
return ok, fmt.Errorf("cannot load search from memory before remember: %w", err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
repo.mutex.Lock()
|
||||
defer repo.mutex.Unlock()
|
||||
|
||||
repo.searches[s] = struct{}{}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Load implements search.Repository.
|
||||
func (repo *memorySearchRepository) Load(ctx context.Context, s domain.Search) (bool, error) {
|
||||
repo.mutex.RLock()
|
||||
defer repo.mutex.RUnlock()
|
||||
|
||||
_, ok := repo.searches[s]
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (repo *memorySearchRepository) Fetch(ctx context.Context) ([]domain.Search, error) {
|
||||
repo.mutex.RLock()
|
||||
defer repo.mutex.RUnlock()
|
||||
|
||||
out := make([]domain.Search, 0, len(repo.searches))
|
||||
|
||||
for s := range repo.searches {
|
||||
out = append(out, s)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func NewMemorySearchRepository() search.Repository {
|
||||
return &memorySearchRepository{
|
||||
mutex: new(sync.RWMutex),
|
||||
searches: make(map[domain.Search]struct{}),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
)
|
||||
|
||||
type UseCase interface {
|
||||
// Search make a random search.
|
||||
Search(ctx context.Context) (domain.Search, error)
|
||||
|
||||
// Reset wipes searching history.
|
||||
Reset(ctx context.Context) (bool, error)
|
||||
}
|
||||
|
||||
var ErrNoMoreSearching = errors.New("there's no more searching")
|
|
@ -0,0 +1,68 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
"source.toby3d.me/toby3d/alice/internal/search"
|
||||
)
|
||||
|
||||
type searchUseCase struct {
|
||||
rand *rand.Rand
|
||||
searches search.Repository
|
||||
}
|
||||
|
||||
// Reset implements search.UseCase.
|
||||
func (ucase *searchUseCase) Reset(ctx context.Context) (bool, error) {
|
||||
searched, err := ucase.searches.Fetch(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot fetch searched memory: %w", err)
|
||||
}
|
||||
|
||||
for i := range searched {
|
||||
if _, err = ucase.searches.Forget(ctx, searched[i]); err != nil {
|
||||
return false, fmt.Errorf("cannot reset searched memory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return len(searched) > 0, nil
|
||||
}
|
||||
|
||||
// Search implements search.UseCase.
|
||||
func (ucase *searchUseCase) Search(ctx context.Context) (domain.Search, error) {
|
||||
searched, err := ucase.searches.Fetch(ctx)
|
||||
if err != nil {
|
||||
return domain.SearchUnd, fmt.Errorf("cannot check searched memory: %w", err)
|
||||
}
|
||||
|
||||
if len(searched) >= len(domain.SearchesBase) {
|
||||
return domain.SearchUnd, fmt.Errorf("cannot do searching: %w", search.ErrNoMoreSearching)
|
||||
}
|
||||
|
||||
variants := make([]domain.Search, 0, len(domain.SearchesBase))
|
||||
for i := range domain.SearchesBase {
|
||||
if slices.Contains(searched, domain.SearchesBase[i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
variants = append(variants, domain.SearchesBase[i])
|
||||
}
|
||||
|
||||
pick := variants[ucase.rand.Intn(len(variants))]
|
||||
|
||||
if _, err = ucase.searches.Remember(ctx, pick); err != nil {
|
||||
return domain.SearchUnd, fmt.Errorf("cannot remember selected searching: %w", err)
|
||||
}
|
||||
|
||||
return pick, nil
|
||||
}
|
||||
|
||||
func NewSearchUseCase(searches search.Repository, seed rand.Source) search.UseCase {
|
||||
return &searchUseCase{
|
||||
rand: rand.New(seed),
|
||||
searches: searches,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package usecase_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
searchrepo "source.toby3d.me/toby3d/alice/internal/search/repository/memory"
|
||||
"source.toby3d.me/toby3d/alice/internal/search/usecase"
|
||||
)
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
seed := rand.NewSource(1)
|
||||
repo := searchrepo.NewMemorySearchRepository()
|
||||
ucase := usecase.NewSearchUseCase(repo, seed)
|
||||
checks := make(map[domain.Search]struct{})
|
||||
|
||||
// NOTE(toby3d): make a one more step for error checking
|
||||
for i := 0; i < len(domain.SearchesBase); i++ {
|
||||
out, err := ucase.Search(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := checks[out]; !ok {
|
||||
checks[out] = struct{}{}
|
||||
} else {
|
||||
t.Errorf("%s already searched earler", out)
|
||||
}
|
||||
}
|
||||
|
||||
if len(checks) != len(domain.SearchesBase) {
|
||||
t.Errorf("expect %d unique searches, got %d", len(domain.SearchesBase), len(checks))
|
||||
}
|
||||
}
|
22
main.go
22
main.go
|
@ -3,14 +3,20 @@ package main
|
|||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/caarlos0/env/v9"
|
||||
|
||||
characterdiscorddelivery "source.toby3d.me/toby3d/alice/internal/character/delivery/discord"
|
||||
"source.toby3d.me/toby3d/alice/internal/domain"
|
||||
linediscorddelivery "source.toby3d.me/toby3d/alice/internal/line/delivery/discord"
|
||||
searchdiscorddelivery "source.toby3d.me/toby3d/alice/internal/search/delivery/discord"
|
||||
searchfsrepo "source.toby3d.me/toby3d/alice/internal/search/repository/memory"
|
||||
searchusecase "source.toby3d.me/toby3d/alice/internal/search/usecase"
|
||||
voicemaildiscorddelivery "source.toby3d.me/toby3d/alice/internal/voicemail/delivery/discord"
|
||||
voicemailfsrepo "source.toby3d.me/toby3d/alice/internal/voicemail/repository/fs"
|
||||
voicemailusecase "source.toby3d.me/toby3d/alice/internal/voicemail/usecase"
|
||||
|
@ -46,15 +52,23 @@ func main() {
|
|||
voicemailDiscordHandler := voicemaildiscorddelivery.NewHandler(voicemailsService, assets, logger, *config)
|
||||
xDiscordHandler := xdiscorddelivery.NewHandler(assets, logger)
|
||||
characterDiscordDelivery := characterdiscorddelivery.NewHandler(logger)
|
||||
lineDiscordHandler := linediscorddelivery.NewHandler(logger)
|
||||
searches := searchfsrepo.NewMemorySearchRepository()
|
||||
searchService := searchusecase.NewSearchUseCase(searches, rand.NewSource(time.Now().UnixNano()))
|
||||
searchDiscordDelivery := searchdiscorddelivery.NewHandler(searchService, assets, logger)
|
||||
|
||||
for _, h := range []any{
|
||||
xDiscordHandler.ServeReady,
|
||||
voicemailDiscordHandler.ServeReady,
|
||||
characterDiscordDelivery.ServeReady,
|
||||
lineDiscordHandler.ServeReady,
|
||||
searchDiscordDelivery.ServeReady,
|
||||
voicemailDiscordHandler.ServeReady,
|
||||
xDiscordHandler.ServeReady,
|
||||
voicemailDiscordHandler.ServeMessage,
|
||||
xDiscordHandler.ServeInteraction,
|
||||
voicemailDiscordHandler.ServeInteraction,
|
||||
characterDiscordDelivery.ServeInteraction,
|
||||
lineDiscordHandler.ServeInteraction,
|
||||
searchDiscordDelivery.ServeInteraction,
|
||||
voicemailDiscordHandler.ServeInteraction,
|
||||
xDiscordHandler.ServeInteraction,
|
||||
} {
|
||||
session.AddHandler(h)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue