From 47cae1e99ddc25ba1b53d564caf183318c5b49c8 Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 11:07:36 +0500 Subject: [PATCH 01/20] - start implementing onion arch; --- adapters/adapters.go | 18 + client/discord/discord.go | 27 +- client/discord/handler/common.go | 22 + client/discord/handler/discord_handler.go | 541 ++++++++++++++++++ .../handle_external_task.go | 33 +- .../{handler => router}/handle_folder.go | 2 +- .../discord/{handler => router}/handle_git.go | 2 +- .../{handler => router}/handle_ping.go | 2 +- .../{handler => router}/handle_ticket.go | 2 +- client/discord/{handler => router}/handler.go | 28 +- client/telegram/handler/handler.go | 9 +- internal/controller/controller.go | 9 +- internal/domain/models.go | 5 + internal/services/cloud.go | 13 +- internal/services/dummy_telegram.go | 28 + 15 files changed, 665 insertions(+), 76 deletions(-) create mode 100644 adapters/adapters.go create mode 100644 client/discord/handler/common.go create mode 100644 client/discord/handler/discord_handler.go rename client/discord/{handler => router}/handle_external_task.go (91%) rename client/discord/{handler => router}/handle_folder.go (99%) rename client/discord/{handler => router}/handle_git.go (99%) rename client/discord/{handler => router}/handle_ping.go (97%) rename client/discord/{handler => router}/handle_ticket.go (99%) rename client/discord/{handler => router}/handler.go (77%) create mode 100644 internal/services/dummy_telegram.go diff --git a/adapters/adapters.go b/adapters/adapters.go new file mode 100644 index 0000000..ee05218 --- /dev/null +++ b/adapters/adapters.go @@ -0,0 +1,18 @@ +package adapters + +import "ticket-pimp/internal/domain" + +type IDummyTelegram interface { + DummyNotification(id string, text string) +} + +type ICloud interface { + CreateFolder(name string) domain.Response +} + +type ICoda interface { + ListDocs() + CreateApp(task domain.CodaApplication) + CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) + GetRowLink(id string) (string, error) +} diff --git a/client/discord/discord.go b/client/discord/discord.go index b540b34..4db4320 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -6,7 +6,7 @@ import ( "log" "os" "os/signal" - "ticket-pimp/client/discord/handler" + "ticket-pimp/client/discord/router" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" @@ -51,7 +51,7 @@ func Run(conf domain.Config, opts DiscordOptions) error { s := initBotWith(token) - router := handler.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) + router := router.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} for _, handler := range router.Commands { @@ -89,18 +89,35 @@ func Run(conf domain.Config, opts DiscordOptions) error { // UPDATE FORUM IF NEEDED: - forum, err := s.Channel(conf.Discord.IsProjectChannel) + forum, err := s.Channel(conf.Discord.IsTaskForum) if err != nil { log.Print(err) } - _, err = s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ - AvailableTags: &router.Tags, + tagsAvailable := []discordgo.ForumTag{ + { + Name: "В работе", + Moderated: true, + EmojiName: "👩‍🍳", + }, + { + Name: "Готово", + Moderated: true, + EmojiName: "✅", + }, + } + dchan, err := s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ + AvailableTags: &tagsAvailable, }) if err != nil { log.Fatal(err) } + log.Printf("Channel %s with ID %s propagated by tags:", dchan.Name, dchan.ID) + for _, t := range dchan.AvailableTags { + log.Printf("N: %s, ID: %s", t.Name, t.ID) + } + log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand var logString []string diff --git a/client/discord/handler/common.go b/client/discord/handler/common.go new file mode 100644 index 0000000..2dfe4d2 --- /dev/null +++ b/client/discord/handler/common.go @@ -0,0 +1,22 @@ +package handler + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Sending result: + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: answer, + }) + + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: fmt.Sprintf("Something went wrong: %v", err), + }) + return + } +} diff --git a/client/discord/handler/discord_handler.go b/client/discord/handler/discord_handler.go new file mode 100644 index 0000000..6b87a0d --- /dev/null +++ b/client/discord/handler/discord_handler.go @@ -0,0 +1,541 @@ +package handler + +import ( + "context" + "fmt" + "log" + "ticket-pimp/adapters" + "ticket-pimp/internal/controller" + "ticket-pimp/internal/domain" + + "github.com/bwmarrin/discordgo" +) + +type Handler struct { + controller *controller.WorkflowController + conf *domain.DiscordConfig + tg adapters.IDummyTelegram +} + +func NewHandler( + controller *controller.WorkflowController, + conf *domain.DiscordConfig, + tg adapters.IDummyTelegram, +) *Handler { + return &Handler{ + controller: controller, + conf: conf, + tg: tg, + } +} + +func (h *Handler) ping(s *discordgo.Session, i *discordgo.InteractionCreate) { + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Pong to: " + i.Member.User.Mention(), + }, + }) + if err != nil { + log.Println(err) + } +} + +// setFlag +// sets tag with In progress and Done text to discords channel; +func (h *Handler) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { + + th, err := s.Channel(i.ChannelID) + if err != nil { + return err + } + + forum, err := s.Channel(th.ParentID) + if err != nil { + return err + } + + // Проверка на существование тега в списке тегов: + if len(forum.AvailableTags) != 0 { + for _, some := range forum.AvailableTags { + if some.Name == tag.Name { + _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ + AppliedTags: &[]string{some.ID}, + }) + if err != nil { + return err + } + } + } + } + + return nil +} + +// ListenPosts +// ..listens to new posts in specific channel +// to act them like a task +func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) { + + // Check if thread starter is not a bot, and thread started at the tasks channel; + if th.ParentID != h.conf.IsTaskForum || th.OwnerID == s.State.User.ID { + return + } + + msgs, _ := s.ChannelMessages(th.ID, 1, "", "", "") + + msg, _ := s.ChannelMessage(th.ID, msgs[0].ID) + + if msg.Author.ID == s.State.User.ID { + return + } + + content := th.Name + content += "\n" + msg.Content + + user, _ := s.GuildMember(th.GuildID, msg.Author.ID) + + t, err := h.controller.WriteTaskToDB(&domain.Task{ + Description: content, + Creator: user.User.Mention(), + }) + if err != nil { + s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to write task to db, %v", err)) + return + } + + // [x] -- Отредактировать Thread name как для задачи + _, err = s.ChannelEditComplex(th.ID, &discordgo.ChannelEdit{ + Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), + }) + if err != nil { + log.Printf("th edition is not complete: %v", err) + } + + // Fix the original task message: + taskMessage, err := s.ChannelMessageSendComplex(th.ID, &discordgo.MessageSend{ + Content: content, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Start", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "task_start", + }, + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: true, + CustomID: "task_close", + }, + }, + }, + }, + }) + if err != nil { + log.Printf("th start message edition is not complete: %v", err) + } + + err = h.controller.UpdateTasksMessageID(context.TODO(), taskMessage.ID, t.ID) + if err != nil { + s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to update task at the db, %v", err)) + return + } +} + +// handleTaskBurrons +// .. handler function to work with the Action Buttons over a task +func (h *Handler) handleTaskButton(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Send an empty interaction response; ---------------------------------------------------------------- + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{}, + }) + + // Get assignee value; --------------------------------------------------------------------------------- + user := i.Member.User.Mention() + + var ( + opt int = -1 + doneButtonIsDisabled bool = false + state domain.TaskState = domain.NewTaskState() + message string + ) + + // Check what flow was touched: ------------------------------------------------------------------------- + switch i.Interaction.MessageComponentData().CustomID { + case "task_start": + opt = 0 + doneButtonIsDisabled = false + state = domain.InrpogressTaskState() + message = "взята в работу" + case "task_close": + opt = 1 + doneButtonIsDisabled = true + state = domain.DoneTaskState() + message = "выполнена" + } + + // Send the task update to db -------------------------------------------------------------------------- + convertable, err := h.controller.UpdateTask(i.Message.ID, opt, user) + if err != nil { + s.ChannelMessageSend(i.ChannelID, fmt.Sprintf("Unable to update task at the db w/ error: %v", err)) + return + } + + // Map DB's response to domain.Task: ------------------------------------------------------------------- + task := convertable. + ExtractDomain() + + newContent := task.DiscordMessage(state) + + // Send message to the creator in Telegram: ------------------------------------------------------------- + + if task.CreatorLink != "" { + h.tg.DummyNotification( + task.CreatorLink, + fmt.Sprintf("Task ID: %d %s", task.ID, message)) + } + + // Send a message to the thread about the task was started: --------------------------------------------- + _, err = s.ChannelMessageSendComplex(i.ChannelID, &discordgo.MessageSend{ + Content: newContent, + }) + if err != nil { + log.Printf("error while sending start task message: %v", err) + } + + // Fix the original task message: ---------------------------------------------------------------------- + _, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: i.ChannelID, + ID: i.Message.ID, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: doneButtonIsDisabled, + CustomID: "task_close", + }, + }, + }, + }, + }) + if err != nil { + log.Printf("th start message edition is not complete: %v", err) + } + + // [ ] Устанавливаем тэги статуса на тред --------------------------------------------------------------------- + // err = h.setFlag(s, i, &h.Tags[opt]) + // if err != nil { + // log.Printf("error while `start` tag setting: %v", err) + // } +} + +func (h *Handler) createFolderHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + const ( + nameOption string = "folder_name" + ) + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + // Определение переменной для ответа + var result string = "unexpected result" + + // Определение выбранных вариантов ответа + options := i.ApplicationCommandData().Options + + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + // Creating request: + var req controller.FolderRequest + name, insertedValueNotNil := optionMap[nameOption] + dchan, err := s.Channel(i.ChannelID) + + if err != nil { + log.Printf("error while identifying channel: %v", err) + } else { + + if dchan.ParentID == h.conf.IsProjectChannel { + req.ChannelID = dchan.ID + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + + } else { + req.ChannelID = "" + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + } + } + + // Making request: + resp := h.controller.CreateFolder(context.TODO(), req) + if resp.Project == nil { + result = "Надо написать имя для папки, или создать папку из проекта!" + } else { + result = resp.Project.DiscordString() + if resp.Message != nil { + result += "Errors: " + resp.Message.Error() + } + } + + h.defaultFollowUp(result, s, i) +} + +func (h *Handler) createRepoHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + const ( + repoType = "repo_type" + projectRepo = "project_repo" + buildRepo = "build_repo" + nameOption = "repo_name" + ) + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + // Определение переменной для ответа + var result string = "unexpected result" + + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + // Creating request: + var req controller.GitRequest + name, insertedValueNotNil := optionMap[nameOption] + isBuild := optionMap[repoType] + switch isBuild.StringValue() { + case buildRepo: + req.IsBuildGit = true + case projectRepo: + req.IsBuildGit = false + } + dchan, err := s.Channel(i.ChannelID) + + if err != nil { + log.Printf("error while identifying channel: %v", err) + } else { + + if dchan.ParentID == h.conf.IsProjectChannel { + req.ChannelID = dchan.ID + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + } else { + req.ChannelID = "" + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + } + } + + // Making request: + resp := h.controller.CreateGit(context.TODO(), req) + if resp.Project == nil { + if resp.Message != nil { + result = resp.Message.Error() + } + } else { + + result = resp.Project.DiscordString() + if resp.Message != nil { + result += "Errors: " + resp.Message.Error() + } + + } + h.defaultFollowUp(result, s, i) +} + +// PROJECT +func (h *Handler) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + var result string + + // Get channel from the request + dchan, err := s.Channel(i.ChannelID) + if err != nil { + result = "unable to get channel from the message" + } else { + project, err := h.controller.GetProjectByChannelID(context.TODO(), dchan.ID) + if err != nil { + result = err.Error() + } else { + if project != nil { + result = project.DiscordString() + if err != nil { + result += "Errors: " + err.Error() + } + } else { + result = "Something wrong with retrieving project from db" + } + + } + } + + h.defaultFollowUp(result, s, i) +} + +func (h *Handler) initProjectFromChannel(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + var result string + + // Get channel from the request + dchan, err := s.Channel(i.ChannelID) + if err != nil { + result = "unable to get channel from the message" + } else { + if dchan.ParentID != h.conf.IsProjectChannel { + // Sending result: + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: "This channel is not at the project's group", + }) + + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: fmt.Sprintf("Something went wrong: %v", err), + }) + return + } + return + } + + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + if option, ok := optionMap["key"]; ok { + var errMsg error = nil + + project, err := h.controller.InitProjectInChannel(context.TODO(), i.ChannelID, option.StringValue()) + if err != nil { + result = fmt.Sprintf("unable to init project: %v", err) + } else { + result = project.DiscordString() + if errMsg != nil { + result += "Errors: " + errMsg.Error() + } + } + } + } + + h.defaultFollowUp(result, s, i) +} + +func (h *Handler) createTicketHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + var result string + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + if option, ok := optionMap["project_name"]; ok { + dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText) + if err != nil { + result = fmt.Sprintf("chan creation problem: %v\n", err) + } else { + p, err := h.controller.ProjectCreate(context.TODO(), domain.Project{ + ChannelID: dchan.ID, + }) + if err != nil { + result = fmt.Sprintf("unable to create project: %v\n", err) + _, err := s.ChannelDelete(dchan.ID) + if err != nil { + result += fmt.Sprintf("\nunable to clean channel: %v\n", err) + } + } else { + edit := discordgo.ChannelEdit{ + Name: p.ShortName, + ParentID: "1150719794853716028", + } + + dchan, err = s.ChannelEdit(dchan.ID, &edit) + if err != nil { + result = fmt.Sprintf("channel %s created, but unable to edit follow up message: %v\n", p.ShortName, err) + + } else { + _, err = s.ChannelMessageSend(dchan.ID, "Hello!") + if err != nil { + log.Printf("message send problem: %v\n", err) + } + result = "Project " + p.ShortName + "Was created" + } + } + } + } + + h.defaultFollowUp(result, s, i) +} diff --git a/client/discord/handler/handle_external_task.go b/client/discord/router/handle_external_task.go similarity index 91% rename from client/discord/handler/handle_external_task.go rename to client/discord/router/handle_external_task.go index b41dd5c..18274c7 100644 --- a/client/discord/handler/handle_external_task.go +++ b/client/discord/router/handle_external_task.go @@ -1,4 +1,4 @@ -package handler +package router import ( "context" @@ -198,10 +198,10 @@ func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.Interaction } // Устанавливаем тэги статуса на тред --------------------------------------------------------------------- - err = c.setFlag(s, i, &c.Tags[opt]) - if err != nil { - log.Printf("error while `start` tag setting: %v", err) - } + // err = c.setFlag(s, i, &c.Tags[opt]) + // if err != nil { + // log.Printf("error while `start` tag setting: %v", err) + // } } type TelegramMessage struct { @@ -212,6 +212,7 @@ type TelegramMessage struct { DisablePreview bool `json:"disable_web_page_preview"` } +// [ ] As a separate service? func (c *client) sendTelegramMessageToCreator(tgChatID string, text string) { http := req.C() @@ -224,26 +225,4 @@ func (c *client) sendTelegramMessageToCreator(tgChatID string, text string) { DisablePreview: true, }). Post("https://api.telegram.org/bot" + c.tgConf.Token + "/sendMessage") - // [HTTP Kit Marlerino]::POST( - // "https://api.telegram.org/bot" + Config.botToken + - // "/sendMessage", - // "", - // Object( - // "chat_id", - // thisRow.[Creator ID].ToText(), - // "text", - // Format( - // "{2} взята в работу", - // thisRow.ObjectLink().ToText(), - // "Задача" - // ), - // "disable_notification", - // true, - // "parse_mode", - // "HTML", - // "disable_web_page_preview", - // true - // ) - // ) - } diff --git a/client/discord/handler/handle_folder.go b/client/discord/router/handle_folder.go similarity index 99% rename from client/discord/handler/handle_folder.go rename to client/discord/router/handle_folder.go index ab7a6e7..2cac468 100644 --- a/client/discord/handler/handle_folder.go +++ b/client/discord/router/handle_folder.go @@ -1,4 +1,4 @@ -package handler +package router import ( "context" diff --git a/client/discord/handler/handle_git.go b/client/discord/router/handle_git.go similarity index 99% rename from client/discord/handler/handle_git.go rename to client/discord/router/handle_git.go index f41ff60..9f1e2a6 100644 --- a/client/discord/handler/handle_git.go +++ b/client/discord/router/handle_git.go @@ -1,4 +1,4 @@ -package handler +package router import ( "context" diff --git a/client/discord/handler/handle_ping.go b/client/discord/router/handle_ping.go similarity index 97% rename from client/discord/handler/handle_ping.go rename to client/discord/router/handle_ping.go index 4f91795..9257715 100644 --- a/client/discord/handler/handle_ping.go +++ b/client/discord/router/handle_ping.go @@ -1,4 +1,4 @@ -package handler +package router import ( "log" diff --git a/client/discord/handler/handle_ticket.go b/client/discord/router/handle_ticket.go similarity index 99% rename from client/discord/handler/handle_ticket.go rename to client/discord/router/handle_ticket.go index f904e5d..9624d7c 100644 --- a/client/discord/handler/handle_ticket.go +++ b/client/discord/router/handle_ticket.go @@ -1,4 +1,4 @@ -package handler +package router import ( "context" diff --git a/client/discord/handler/handler.go b/client/discord/router/handler.go similarity index 77% rename from client/discord/handler/handler.go rename to client/discord/router/handler.go index cb64fc3..5deda56 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/router/handler.go @@ -1,4 +1,4 @@ -package handler +package router import ( "fmt" @@ -12,7 +12,7 @@ type client struct { Commands []Command Components []Component - Tags []discordgo.ForumTag + // Tags []discordgo.ForumTag controller controller.WorkflowController conf *domain.DiscordConfig @@ -27,30 +27,16 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tg r.conf = conf r.Commands = append(r.Commands, - r.CreateRepoHandler(3), - r.CreateFolderHandler(3), + // r.CreateRepoHandler(3), + // r.CreateFolderHandler(3), r.Ping(), - r.CreateTicketHandler(3), - r.InitProjectFromChannel(3), - r.GetInfo(), + // r.CreateTicketHandler(3), + // r.InitProjectFromChannel(3), + // r.GetInfo(), ) r.Components = append(r.Components, r.HandleTaskButtons(), ) - - r.Tags = append( - r.Tags, - discordgo.ForumTag{ - Name: "В работе", - Moderated: true, - EmojiName: "👩‍🍳", - }, - - discordgo.ForumTag{ - Name: "Готово", - Moderated: true, - EmojiName: "✅", - }) r.tgConf = tgConf return &r diff --git a/client/telegram/handler/handler.go b/client/telegram/handler/handler.go index da3a326..c2c21ac 100644 --- a/client/telegram/handler/handler.go +++ b/client/telegram/handler/handler.go @@ -1,14 +1,15 @@ package handler import ( + "ticket-pimp/adapters" "ticket-pimp/internal/controller" "ticket-pimp/internal/services" ) type Handler struct { git services.IGit - cloud services.ICloud - coda services.ICoda + cloud adapters.ICloud + coda adapters.ICoda key string id string controller *controller.WorkflowController @@ -16,8 +17,8 @@ type Handler struct { func NewHandler( git services.IGit, - cloud services.ICloud, - coda services.ICoda, + cloud adapters.ICloud, + coda adapters.ICoda, controller *controller.WorkflowController, ) *Handler { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index ecea95d..5b836fd 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "ticket-pimp/adapters" "ticket-pimp/internal/domain" "ticket-pimp/internal/services" "ticket-pimp/internal/storage/db" @@ -11,8 +12,8 @@ import ( type WorkflowController struct { IGit services.IGit - ICloud services.ICloud - ICoda services.ICoda + ICloud adapters.ICloud + ICoda adapters.ICoda pool *pgxpool.Pool q *db.Queries ATags []discordgo.ForumTag @@ -20,8 +21,8 @@ type WorkflowController struct { func NewWorkflowController( git services.IGit, - cloud services.ICloud, - coda services.ICoda, + cloud adapters.ICloud, + coda adapters.ICoda, pool *pgxpool.Pool, ) *WorkflowController { return &WorkflowController{ diff --git a/internal/domain/models.go b/internal/domain/models.go index 48ec870..5f320b9 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -11,6 +11,11 @@ type Folder struct { PrivateURL string // http://domain/apps/files/?dir=/temp/k OR http://domain/f/3333 } +type Response struct { + Folder *Folder + ErrMessage error +} + type CodaApplication struct { ID string `json:"id"` Summary string `json:"summary"` diff --git a/internal/services/cloud.go b/internal/services/cloud.go index b5a748e..b4f1692 100644 --- a/internal/services/cloud.go +++ b/internal/services/cloud.go @@ -16,10 +16,6 @@ type Cloud struct { Config domain.CloudConfig } -type ICloud interface { - CreateFolder(name string) Response -} - func NewCloud(conf domain.CloudConfig) *Cloud { client := NewClient(). @@ -35,13 +31,8 @@ func NewCloud(conf domain.CloudConfig) *Cloud { } } -type Response struct { - Folder *domain.Folder - ErrMessage error -} - -func (c *Cloud) CreateFolder(name string) Response { - var R Response +func (c *Cloud) CreateFolder(name string) domain.Response { + var R domain.Response rootDir := c.Config.RootDir user := c.Config.User diff --git a/internal/services/dummy_telegram.go b/internal/services/dummy_telegram.go new file mode 100644 index 0000000..dca909f --- /dev/null +++ b/internal/services/dummy_telegram.go @@ -0,0 +1,28 @@ +package services + +import "ticket-pimp/internal/domain" + +type DummyTelegram struct { + *CommonClient + config domain.TelegramConfig +} + +type TelegramMessage struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + DisableNotification bool `json:"disable_notification"` + ParseMode string `json:"parse_mode"` + DisablePreview bool `json:"disable_web_page_preview"` +} + +func (tg *DummyTelegram) DummyNotification(id string, text string) { + tg.R(). + SetBody(&TelegramMessage{ + ChatID: id, + Text: text, + DisableNotification: true, + ParseMode: "HTML", + DisablePreview: true, + }). + Post("https://api.telegram.org/bot" + tg.config.Token + "/sendMessage") +} From 98f401c6f465ba7b09399abacf6d619d898052a6 Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 16:05:16 +0500 Subject: [PATCH 02/20] move I to adapters package --- adapters/adapters.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/adapters/adapters.go b/adapters/adapters.go index ee05218..84d0181 100644 --- a/adapters/adapters.go +++ b/adapters/adapters.go @@ -6,6 +6,10 @@ type IDummyTelegram interface { DummyNotification(id string, text string) } +type IGit interface { + CreateRepo(name string) (*domain.Git, error) +} + type ICloud interface { CreateFolder(name string) domain.Response } From e668e0b50d1bc05ad9afd241a4cbe9d70ad6deae Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 16:07:11 +0500 Subject: [PATCH 03/20] - move initial response to middleware; - projects parent channel as conf var; - finish moving Interfaces to the middleware; --- client/discord/discord.go | 11 +++++ client/discord/router/handle_external_task.go | 6 --- client/discord/router/handle_folder.go | 11 ----- client/discord/router/handle_git.go | 10 ----- client/discord/router/handle_ticket.go | 40 ++----------------- client/discord/router/handler.go | 10 ++--- client/telegram/handler/handler.go | 5 +-- internal/controller/controller.go | 5 +-- internal/services/coda.go | 7 ---- internal/services/git.go | 4 -- 10 files changed, 24 insertions(+), 85 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index 4db4320..48db455 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -28,6 +28,17 @@ type DiscordOptions struct { } func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) error { + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + dchan, err := s.Channel(i.ChannelID) if err != nil { return err diff --git a/client/discord/router/handle_external_task.go b/client/discord/router/handle_external_task.go index 18274c7..25e2a1f 100644 --- a/client/discord/router/handle_external_task.go +++ b/client/discord/router/handle_external_task.go @@ -117,12 +117,6 @@ func (c *client) HandleTaskButtons() Component { func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Send an empty interaction response; ---------------------------------------------------------------- - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{}, - }) - // Get assignee value; --------------------------------------------------------------------------------- user := i.Member.User.Mention() diff --git a/client/discord/router/handle_folder.go b/client/discord/router/handle_folder.go index 2cac468..0efed58 100644 --- a/client/discord/router/handle_folder.go +++ b/client/discord/router/handle_folder.go @@ -37,17 +37,6 @@ func (c *client) createFolderHandler(s *discordgo.Session, i *discordgo.Interact nameOption string = "folder_name" ) - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - // Определение переменной для ответа var result string = "unexpected result" diff --git a/client/discord/router/handle_git.go b/client/discord/router/handle_git.go index 9f1e2a6..0d71953 100644 --- a/client/discord/router/handle_git.go +++ b/client/discord/router/handle_git.go @@ -57,16 +57,6 @@ func (c *client) createRepoHandler(s *discordgo.Session, i *discordgo.Interactio buildRepo = "build_repo" nameOption = "repo_name" ) - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) // Определение переменной для ответа var result string = "unexpected result" diff --git a/client/discord/router/handle_ticket.go b/client/discord/router/handle_ticket.go index 9624d7c..85eef5c 100644 --- a/client/discord/router/handle_ticket.go +++ b/client/discord/router/handle_ticket.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "ticket-pimp/internal/domain" + "ticket-pimp/internal/helpers" "github.com/bwmarrin/discordgo" ) @@ -21,17 +22,6 @@ func (c *client) GetInfo() Command { func (c *client) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - var result string // Get channel from the request @@ -79,17 +69,6 @@ func (c *client) InitProjectFromChannel(minLength int) Command { func (c *client) initProjectFromChannel(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - var result string // Get channel from the request @@ -160,17 +139,6 @@ func (c *client) CreateTicketHandler(repoNameMinLength int) Command { func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - var result string // Access options in the order provided by the user. options := i.ApplicationCommandData().Options @@ -197,8 +165,8 @@ func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.Interact } } else { edit := discordgo.ChannelEdit{ - Name: p.ShortName, - ParentID: "1150719794853716028", + Name: p.ShortName + "-" + helpers.Cut(option.StringValue()), + ParentID: c.conf.IsProjectChannel, } dchan, err = s.ChannelEdit(dchan.ID, &edit) @@ -210,7 +178,7 @@ func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.Interact if err != nil { log.Printf("message send problem: %v\n", err) } - result = "Project " + p.ShortName + "Was created" + result = "Project " + p.ShortName + " was created" } } } diff --git a/client/discord/router/handler.go b/client/discord/router/handler.go index 5deda56..27f3097 100644 --- a/client/discord/router/handler.go +++ b/client/discord/router/handler.go @@ -27,12 +27,12 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tg r.conf = conf r.Commands = append(r.Commands, - // r.CreateRepoHandler(3), - // r.CreateFolderHandler(3), + r.CreateRepoHandler(3), + r.CreateFolderHandler(3), r.Ping(), - // r.CreateTicketHandler(3), - // r.InitProjectFromChannel(3), - // r.GetInfo(), + r.CreateTicketHandler(3), + r.InitProjectFromChannel(3), + r.GetInfo(), ) r.Components = append(r.Components, r.HandleTaskButtons(), diff --git a/client/telegram/handler/handler.go b/client/telegram/handler/handler.go index c2c21ac..283cb8d 100644 --- a/client/telegram/handler/handler.go +++ b/client/telegram/handler/handler.go @@ -3,11 +3,10 @@ package handler import ( "ticket-pimp/adapters" "ticket-pimp/internal/controller" - "ticket-pimp/internal/services" ) type Handler struct { - git services.IGit + git adapters.IGit cloud adapters.ICloud coda adapters.ICoda key string @@ -16,7 +15,7 @@ type Handler struct { } func NewHandler( - git services.IGit, + git adapters.IGit, cloud adapters.ICloud, coda adapters.ICoda, controller *controller.WorkflowController, diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 5b836fd..c3e4840 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -3,7 +3,6 @@ package controller import ( "ticket-pimp/adapters" "ticket-pimp/internal/domain" - "ticket-pimp/internal/services" "ticket-pimp/internal/storage/db" "github.com/bwmarrin/discordgo" @@ -11,7 +10,7 @@ import ( ) type WorkflowController struct { - IGit services.IGit + IGit adapters.IGit ICloud adapters.ICloud ICoda adapters.ICoda pool *pgxpool.Pool @@ -20,7 +19,7 @@ type WorkflowController struct { } func NewWorkflowController( - git services.IGit, + git adapters.IGit, cloud adapters.ICloud, coda adapters.ICoda, pool *pgxpool.Pool, diff --git a/internal/services/coda.go b/internal/services/coda.go index 9c10b61..3cf1c5a 100644 --- a/internal/services/coda.go +++ b/internal/services/coda.go @@ -14,13 +14,6 @@ type Coda struct { Config domain.CodaConfig } -type ICoda interface { - ListDocs() - CreateApp(task domain.CodaApplication) - CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) - GetRowLink(id string) (string, error) -} - func NewCodaClient(conf domain.CodaConfig) *Coda { client := NewClient(). diff --git a/internal/services/git.go b/internal/services/git.go index 1778cce..1d9800f 100644 --- a/internal/services/git.go +++ b/internal/services/git.go @@ -13,10 +13,6 @@ type Git struct { conf *domain.GitConfig } -type IGit interface { - CreateRepo(name string) (*domain.Git, error) -} - func NewGit(conf domain.GitConfig) *Git { headers := map[string]string{ "Accept": "application/vnd.github+json", From 5c54d105561e8ebabf529ff7542c72e7abe0e566 Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 16:07:27 +0500 Subject: [PATCH 04/20] handle bots as background process; --- cmd/main.go | 61 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 6d672ab..94c641c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,7 +17,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" + "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" + "golang.org/x/sync/errgroup" ) const ( @@ -34,6 +36,19 @@ func main() { run(config) } +func Go(ctx context.Context, fns ...func(context.Context) error) error { + group, ctx := errgroup.WithContext(ctx) + + for _, fn := range fns { + fn := fn + group.Go(func() error { + return fn(ctx) + }) + } + + return group.Wait() +} + func run(conf domain.Config) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) defer cancel() @@ -87,26 +102,30 @@ func run(conf domain.Config) { conn, ) - go func() { - opts := discord.DiscordOptions{ - Controller: controller, - Config: &conf, - } - if err := discord.Run(conf, opts); err != nil { - log.Fatalf("discord bot cannot be runned: %v", err) - } - }() + Go(ctx, + func(ctx context.Context) error { + opts := discord.DiscordOptions{ + Controller: controller, + Config: &conf, + } + if err := discord.Run(conf, opts); err != nil { + return errors.Errorf("discord bot cannot be runned: %v", err) + } + return nil + }, + func(ctx context.Context) error { + opts := telegram.TelegramOptions{ + GitService: gitService, + CloudService: cloudService, + Coda: codaService, + AppConfig: &conf, + Controller: controller, + } - opts := telegram.TelegramOptions{ - GitService: gitService, - CloudService: cloudService, - Coda: codaService, - AppConfig: &conf, - Controller: controller, - } - - if err := telegram.Run(ctx, opts); err != nil { - log.Fatalf("telegram bot cannot be runned: %v", err) - defer os.Exit(1) - } + if err := telegram.Run(ctx, opts); err != nil { + return errors.Errorf("telegram bot cannot be runned: %v", err) + } + return nil + }, + ) } From 68d88f7036b6115b311fb8860c79130e83b532bf Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 16:07:36 +0500 Subject: [PATCH 05/20] rename handlers --- client/discord/handler/discord_handler.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/discord/handler/discord_handler.go b/client/discord/handler/discord_handler.go index 6b87a0d..5556b17 100644 --- a/client/discord/handler/discord_handler.go +++ b/client/discord/handler/discord_handler.go @@ -29,7 +29,7 @@ func NewHandler( } } -func (h *Handler) ping(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) { err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -148,7 +148,7 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) // handleTaskBurrons // .. handler function to work with the Action Buttons over a task -func (h *Handler) handleTaskButton(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { // Send an empty interaction response; ---------------------------------------------------------------- s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ @@ -237,7 +237,7 @@ func (h *Handler) handleTaskButton(s *discordgo.Session, i *discordgo.Interactio // } } -func (h *Handler) createFolderHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCreate) { const ( nameOption string = "folder_name" ) @@ -301,7 +301,7 @@ func (h *Handler) createFolderHandler(s *discordgo.Session, i *discordgo.Interac h.defaultFollowUp(result, s, i) } -func (h *Handler) createRepoHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) CreateGit(s *discordgo.Session, i *discordgo.InteractionCreate) { const ( repoType = "repo_type" projectRepo = "project_repo" @@ -378,7 +378,7 @@ func (h *Handler) createRepoHandler(s *discordgo.Session, i *discordgo.Interacti } // PROJECT -func (h *Handler) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) ProjectInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { // Моментальный ответ для избежания столкновения с протуханием токена initialResponse := discordgo.InteractionResponse{ @@ -417,7 +417,7 @@ func (h *Handler) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) h.defaultFollowUp(result, s, i) } -func (h *Handler) initProjectFromChannel(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) InitChannelAsProject(s *discordgo.Session, i *discordgo.InteractionCreate) { // Моментальный ответ для избежания столкновения с протуханием токена initialResponse := discordgo.InteractionResponse{ @@ -479,7 +479,7 @@ func (h *Handler) initProjectFromChannel(s *discordgo.Session, i *discordgo.Inte h.defaultFollowUp(result, s, i) } -func (h *Handler) createTicketHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { +func (h *Handler) CreateTicket(s *discordgo.Session, i *discordgo.InteractionCreate) { // Моментальный ответ для избежания столкновения с протуханием токена initialResponse := discordgo.InteractionResponse{ From 6cecec06b631c7a60f4e6dcd9e86d9750a808099 Mon Sep 17 00:00:00 2001 From: naudachu Date: Wed, 22 Nov 2023 16:07:52 +0500 Subject: [PATCH 06/20] go mod tidy --- go.mod | 7 +++---- go.sum | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index c4183a4..cf36b2c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,9 @@ require ( github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 github.com/mr-linch/go-tg v0.9.1 + github.com/pkg/errors v0.9.1 + github.com/rubenv/sql-migrate v1.5.2 + golang.org/x/sync v0.2.0 ) require ( @@ -20,22 +23,18 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx v3.6.2+incompatible // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/onsi/ginkgo/v2 v2.10.0 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/quic-go/quic-go v0.35.1 // indirect - github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/stretchr/testify v1.8.4 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect golang.org/x/crypto v0.10.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.11.0 // indirect - golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.10.0 // indirect golang.org/x/tools v0.9.3 // indirect diff --git a/go.sum b/go.sum index 683817c..83a0ff1 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -29,14 +33,20 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/mr-linch/go-tg v0.9.1 h1:4KNe7zwFG6svgM9w6pIcH3R7QWa6hIK8tCisQiFRCpU= github.com/mr-linch/go-tg v0.9.1/go.mod h1:276w69YW4pEo3ZYta+LQe4v/ut2w2h1ksP4ziBWkK98= github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= @@ -46,6 +56,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U= @@ -54,11 +65,12 @@ github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8G github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo= github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -95,6 +107,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= @@ -112,6 +125,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 86b00e03b67e55209df5185c7d62f30c2c5f8122 Mon Sep 17 00:00:00 2001 From: naudachu Date: Thu, 23 Nov 2023 12:08:42 +0500 Subject: [PATCH 07/20] simplify discord architecture; --- client/discord/discord.go | 204 +++++++++++----- client/discord/discord_handler/common.go | 82 +++++++ .../discord_handler.go | 71 ++---- client/discord/handler/common.go | 22 -- client/discord/router/handle_external_task.go | 222 ------------------ client/discord/router/handle_folder.go | 86 ------- client/discord/router/handle_git.go | 117 --------- client/discord/router/handle_ping.go | 30 --- client/discord/router/handle_ticket.go | 188 --------------- client/discord/router/handler.go | 71 ------ client/telegram/telegram.go | 4 +- .../handle_application.go | 2 +- .../handle_farmtask.go | 2 +- .../handle_folder.go | 2 +- .../handle_git.go | 2 +- .../handle_init.go | 2 +- .../{handler => telegram_handler}/handler.go | 2 +- internal/services/dummy_telegram.go | 18 +- internal/services/git.go | 1 - 19 files changed, 269 insertions(+), 859 deletions(-) create mode 100644 client/discord/discord_handler/common.go rename client/discord/{handler => discord_handler}/discord_handler.go (92%) delete mode 100644 client/discord/handler/common.go delete mode 100644 client/discord/router/handle_external_task.go delete mode 100644 client/discord/router/handle_folder.go delete mode 100644 client/discord/router/handle_git.go delete mode 100644 client/discord/router/handle_ping.go delete mode 100644 client/discord/router/handle_ticket.go delete mode 100644 client/discord/router/handler.go rename client/telegram/{handler => telegram_handler}/handle_application.go (97%) rename client/telegram/{handler => telegram_handler}/handle_farmtask.go (98%) rename client/telegram/{handler => telegram_handler}/handle_folder.go (97%) rename client/telegram/{handler => telegram_handler}/handle_git.go (97%) rename client/telegram/{handler => telegram_handler}/handle_init.go (98%) rename client/telegram/{handler => telegram_handler}/handler.go (95%) diff --git a/client/discord/discord.go b/client/discord/discord.go index 48db455..66964b9 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -1,18 +1,119 @@ package discord import ( - "errors" "fmt" "log" "os" "os/signal" - "ticket-pimp/client/discord/router" + "ticket-pimp/client/discord/discord_handler" + "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" + "ticket-pimp/internal/services" "github.com/bwmarrin/discordgo" ) +var ( + minLength int = 3 + repoType string = "repo_type" + projectRepo string = "project_repo" + buildRepo string = "build_repo" + nameOption string = "repo_name" +) + +var tags = []discordgo.ForumTag{ + { + Name: "В работе", + Moderated: true, + EmojiName: "👩‍🍳", + }, + { + Name: "Готово", + Moderated: true, + EmojiName: "✅", + }, +} + +var commands = []discordgo.ApplicationCommand{ + { + Name: "ping", + Description: "pongs in a reply", + }, + { + Name: "init_project", + Description: "Connect project with Coda ID", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "key", + Description: "Project's key from Coda.io", + Required: true, + MinLength: &minLength, + }, + }, + }, + { + Name: "project", + Description: "Create new development ticket", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "project_name", + Description: "Temporary project name", + Required: true, + MinLength: &minLength, + }, + }, + }, + { + Name: "info", + Description: "Get project's info", + }, + { + Name: "repo", + Description: "Creates repository of selected type. Name used for projects channels only", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: repoType, + Description: "The type of repo", + Required: true, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Unity project repo", + Value: projectRepo, + }, + { + Name: "XCode build repo", + Value: buildRepo, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the repository's name", + Required: false, + MinLength: &minLength, + }, + }, + }, + { + Name: "folder", + Description: "Command for cloud folder creation", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the folder's name", + Required: false, + MinLength: &minLength, + }, + }, + }, +} + func initBotWith(token string) *discordgo.Session { discord, err := discordgo.New("Bot " + token) if err != nil { @@ -27,60 +128,48 @@ type DiscordOptions struct { Controller *controller.WorkflowController } -func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) error { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - - dchan, err := s.Channel(i.ChannelID) - if err != nil { - return err - } - - if dchan.Type == discordgo.ChannelTypeDM { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Yo, fella! I'm not working in private!", - }, - }) - return errors.New("no private messages! lol") - } - - return nil -} - func Run(conf domain.Config, opts DiscordOptions) error { - token := conf.Discord.Token - s := initBotWith(token) + s := initBotWith(conf.Discord.Token) - router := router.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) + h := discord_handler.New( + opts.Controller, + &conf.Discord, + services.NewDummyClient(conf.Telegram), + tags, + ) commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} - for _, handler := range router.Commands { - commandHandlers[handler.Command.Name] = handler.Handler + + for _, cmd := range commands { + var f func(s *discordgo.Session, i *discordgo.InteractionCreate) + + switch cmd.Name { + case "ping": + f = h.Ping + case "project": + f = h.CreateTicket + case "info": + f = h.ProjectInfo + case "repo": + f = h.CreateGit + case "folder": + f = h.CreateFolder + case "init_project": + f = h.InitChannelAsProject + } + commandHandlers[cmd.Name] = f } componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ - "task_start": router.Components[0].Handler, - "task_close": router.Components[0].Handler, + "task_start": h.HandleTaskButtons, + "task_close": h.HandleTaskButtons, } - s.AddHandler(router.ListenPosts) + s.AddHandler(h.ListenPosts) s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - err := checkPrivateMessaging(s, i) - if err != nil { - return - } + h.AllInteractions(s, i) switch i.Type { case discordgo.InteractionApplicationCommand: @@ -105,20 +194,8 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Print(err) } - tagsAvailable := []discordgo.ForumTag{ - { - Name: "В работе", - Moderated: true, - EmojiName: "👩‍🍳", - }, - { - Name: "Готово", - Moderated: true, - EmojiName: "✅", - }, - } dchan, err := s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ - AvailableTags: &tagsAvailable, + AvailableTags: &tags, }) if err != nil { log.Fatal(err) @@ -131,19 +208,16 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand - var logString []string - for _, h := range router.Commands { - cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", &h.Command) + + for _, cmd := range commands { + cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", &cmd) if err != nil { - log.Panicf("Cannot create '%v' command: %v", h.Command.Name, err) + log.Panicf("Cannot create '%v' command: %v", cmd.Name, err) } cmds = append(cmds, cmd) - logString = append(logString, cmd.Name) + log.Println(cmd.Name + " command added") } - log.Println("Following commands added:") - log.Println(logString) - defer s.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) diff --git a/client/discord/discord_handler/common.go b/client/discord/discord_handler/common.go new file mode 100644 index 0000000..913d954 --- /dev/null +++ b/client/discord/discord_handler/common.go @@ -0,0 +1,82 @@ +package discord_handler + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Sending result: + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: answer, + }) + + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: fmt.Sprintf("Something went wrong: %v", err), + }) + return + } +} + +func (h *Handler) AllInteractions(s *discordgo.Session, i *discordgo.InteractionCreate) { + + dchan, err := s.Channel(i.ChannelID) + if err != nil { + return + } + + if dchan.Type == discordgo.ChannelTypeDM { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Yo, fella! I'm not working in private!", + }, + }) + return + } + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) +} + +// setFlag +// sets tag with In progress and Done text to discords channel; +func (h *Handler) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { + + th, err := s.Channel(i.ChannelID) + if err != nil { + return err + } + + forum, err := s.Channel(th.ParentID) + if err != nil { + return err + } + + // Проверка на существование тега в списке тегов: + if len(forum.AvailableTags) != 0 { + for _, some := range forum.AvailableTags { + if some.Name == tag.Name { + _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ + AppliedTags: &[]string{some.ID}, + }) + if err != nil { + return err + } + } + } + } + + return nil +} diff --git a/client/discord/handler/discord_handler.go b/client/discord/discord_handler/discord_handler.go similarity index 92% rename from client/discord/handler/discord_handler.go rename to client/discord/discord_handler/discord_handler.go index 5556b17..dd9e8fe 100644 --- a/client/discord/handler/discord_handler.go +++ b/client/discord/discord_handler/discord_handler.go @@ -1,4 +1,4 @@ -package handler +package discord_handler import ( "context" @@ -12,20 +12,23 @@ import ( ) type Handler struct { - controller *controller.WorkflowController - conf *domain.DiscordConfig - tg adapters.IDummyTelegram + controller *controller.WorkflowController + conf *domain.DiscordConfig + telegramDummyClient adapters.IDummyTelegram + tags []discordgo.ForumTag } -func NewHandler( +func New( controller *controller.WorkflowController, conf *domain.DiscordConfig, tg adapters.IDummyTelegram, + tags []discordgo.ForumTag, ) *Handler { return &Handler{ - controller: controller, - conf: conf, - tg: tg, + controller: controller, + conf: conf, + telegramDummyClient: tg, + tags: tags, } } @@ -42,37 +45,6 @@ func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) { } } -// setFlag -// sets tag with In progress and Done text to discords channel; -func (h *Handler) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { - - th, err := s.Channel(i.ChannelID) - if err != nil { - return err - } - - forum, err := s.Channel(th.ParentID) - if err != nil { - return err - } - - // Проверка на существование тега в списке тегов: - if len(forum.AvailableTags) != 0 { - for _, some := range forum.AvailableTags { - if some.Name == tag.Name { - _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ - AppliedTags: &[]string{some.ID}, - }) - if err != nil { - return err - } - } - } - } - - return nil -} - // ListenPosts // ..listens to new posts in specific channel // to act them like a task @@ -151,10 +123,10 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { // Send an empty interaction response; ---------------------------------------------------------------- - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseUpdateMessage, - Data: &discordgo.InteractionResponseData{}, - }) + // s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + // Type: discordgo.InteractionResponseUpdateMessage, + // Data: &discordgo.InteractionResponseData{}, + // }) // Get assignee value; --------------------------------------------------------------------------------- user := i.Member.User.Mention() @@ -164,6 +136,7 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti doneButtonIsDisabled bool = false state domain.TaskState = domain.NewTaskState() message string + tag discordgo.ForumTag ) // Check what flow was touched: ------------------------------------------------------------------------- @@ -173,11 +146,13 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti doneButtonIsDisabled = false state = domain.InrpogressTaskState() message = "взята в работу" + tag = h.tags[0] case "task_close": opt = 1 doneButtonIsDisabled = true state = domain.DoneTaskState() message = "выполнена" + tag = h.tags[1] } // Send the task update to db -------------------------------------------------------------------------- @@ -196,7 +171,7 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti // Send message to the creator in Telegram: ------------------------------------------------------------- if task.CreatorLink != "" { - h.tg.DummyNotification( + h.telegramDummyClient.DummyNotification( task.CreatorLink, fmt.Sprintf("Task ID: %d %s", task.ID, message)) } @@ -231,10 +206,10 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti } // [ ] Устанавливаем тэги статуса на тред --------------------------------------------------------------------- - // err = h.setFlag(s, i, &h.Tags[opt]) - // if err != nil { - // log.Printf("error while `start` tag setting: %v", err) - // } + err = h.setFlag(s, i, &tag) + if err != nil { + log.Printf("error while `start` tag setting: %v", err) + } } func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCreate) { diff --git a/client/discord/handler/common.go b/client/discord/handler/common.go deleted file mode 100644 index 2dfe4d2..0000000 --- a/client/discord/handler/common.go +++ /dev/null @@ -1,22 +0,0 @@ -package handler - -import ( - "fmt" - - "github.com/bwmarrin/discordgo" -) - -func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { - - // Sending result: - _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: answer, - }) - - if err != nil { - s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: fmt.Sprintf("Something went wrong: %v", err), - }) - return - } -} diff --git a/client/discord/router/handle_external_task.go b/client/discord/router/handle_external_task.go deleted file mode 100644 index 25e2a1f..0000000 --- a/client/discord/router/handle_external_task.go +++ /dev/null @@ -1,222 +0,0 @@ -package router - -import ( - "context" - "fmt" - "log" - "ticket-pimp/internal/domain" - - "github.com/bwmarrin/discordgo" - "github.com/imroc/req/v3" -) - -func (c *client) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { - - th, err := s.Channel(i.ChannelID) - if err != nil { - return err - } - - forum, err := s.Channel(th.ParentID) - if err != nil { - return err - } - - // Проверка на существование тега в списке тегов: - if len(forum.AvailableTags) != 0 { - for _, some := range forum.AvailableTags { - if some.Name == tag.Name { - _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ - AppliedTags: &[]string{some.ID}, - }) - if err != nil { - return err - } - } - } - } - - return nil -} - -func (c *client) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) { - - // Check if thread starter is not a bot, and thread started at the tasks channel; - if th.ParentID != c.conf.IsTaskForum || th.OwnerID == s.State.User.ID { - return - } - - msgs, _ := s.ChannelMessages(th.ID, 1, "", "", "") - - msg, _ := s.ChannelMessage(th.ID, msgs[0].ID) - - if msg.Author.ID == s.State.User.ID { - return - } - - content := th.Name - content += "\n" + msg.Content - - user, _ := s.GuildMember(th.GuildID, msg.Author.ID) - - t, err := c.controller.WriteTaskToDB(&domain.Task{ - Description: content, - Creator: user.User.Mention(), - }) - if err != nil { - s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to write task to db, %v", err)) - return - } - - // [x] -- Отредактировать Thread name как для задачи - _, err = s.ChannelEditComplex(th.ID, &discordgo.ChannelEdit{ - Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), - }) - if err != nil { - log.Printf("th edition is not complete: %v", err) - } - - // Fix the original task message: - taskMessage, err := s.ChannelMessageSendComplex(th.ID, &discordgo.MessageSend{ - Content: content, - Components: []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "Start", - Style: discordgo.SuccessButton, - Disabled: false, - CustomID: "task_start", - }, - discordgo.Button{ - Label: "Close", - Style: discordgo.DangerButton, - Disabled: true, - CustomID: "task_close", - }, - }, - }, - }, - }) - if err != nil { - log.Printf("th start message edition is not complete: %v", err) - } - - err = c.controller.UpdateTasksMessageID(context.TODO(), taskMessage.ID, t.ID) - if err != nil { - s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to update task at the db, %v", err)) - return - } -} - -func (c *client) HandleTaskButtons() Component { - return Component{ - Handler: c.handleTaskButton, - } -} - -func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.InteractionCreate) { - - // Get assignee value; --------------------------------------------------------------------------------- - user := i.Member.User.Mention() - - var ( - opt int = -1 - doneButtonIsDisabled bool = false - state domain.TaskState = domain.NewTaskState() - message string - ) - - // Check what flow was touched: ------------------------------------------------------------------------- - switch i.Interaction.MessageComponentData().CustomID { - case "task_start": - opt = 0 - doneButtonIsDisabled = false - state = domain.InrpogressTaskState() - message = "взята в работу" - case "task_close": - opt = 1 - doneButtonIsDisabled = true - state = domain.DoneTaskState() - message = "выполнена" - } - - // Send the task update to db -------------------------------------------------------------------------- - convertable, err := c.controller.UpdateTask(i.Message.ID, opt, user) - if err != nil { - s.ChannelMessageSend(i.ChannelID, fmt.Sprintf("Unable to update task at the db w/ error: %v", err)) - return - } - - // Map DB's response to domain.Task: ------------------------------------------------------------------- - task := convertable. - ExtractDomain() - - newContent := task.DiscordMessage(state) - - // Send message to the creator in Telegram: ------------------------------------------------------------- - - if task.CreatorLink != "" { - c.sendTelegramMessageToCreator( - task.CreatorLink, - fmt.Sprintf("Task ID: %d %s", task.ID, message)) - } - - // Send a message to the thread about the task was started: --------------------------------------------- - _, err = s.ChannelMessageSendComplex(i.ChannelID, &discordgo.MessageSend{ - Content: newContent, - }) - if err != nil { - log.Printf("error while sending start task message: %v", err) - } - - // Fix the original task message: ---------------------------------------------------------------------- - _, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{ - Channel: i.ChannelID, - ID: i.Message.ID, - Components: []discordgo.MessageComponent{ - discordgo.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "Close", - Style: discordgo.DangerButton, - Disabled: doneButtonIsDisabled, - CustomID: "task_close", - }, - }, - }, - }, - }) - if err != nil { - log.Printf("th start message edition is not complete: %v", err) - } - - // Устанавливаем тэги статуса на тред --------------------------------------------------------------------- - // err = c.setFlag(s, i, &c.Tags[opt]) - // if err != nil { - // log.Printf("error while `start` tag setting: %v", err) - // } -} - -type TelegramMessage struct { - ChatID string `json:"chat_id"` - Text string `json:"text"` - DisableNotification bool `json:"disable_notification"` - ParseMode string `json:"parse_mode"` - DisablePreview bool `json:"disable_web_page_preview"` -} - -// [ ] As a separate service? -func (c *client) sendTelegramMessageToCreator(tgChatID string, text string) { - - http := req.C() - http.R(). - SetBody(&TelegramMessage{ - ChatID: tgChatID, - Text: text, - DisableNotification: true, - ParseMode: "HTML", - DisablePreview: true, - }). - Post("https://api.telegram.org/bot" + c.tgConf.Token + "/sendMessage") -} diff --git a/client/discord/router/handle_folder.go b/client/discord/router/handle_folder.go deleted file mode 100644 index 0efed58..0000000 --- a/client/discord/router/handle_folder.go +++ /dev/null @@ -1,86 +0,0 @@ -package router - -import ( - "context" - "log" - "ticket-pimp/internal/controller" - - "github.com/bwmarrin/discordgo" -) - -func (c *client) CreateFolderHandler(nameMinLenght int) Command { - const ( - nameOption string = "folder_name" - ) - return Command{ - - Command: discordgo.ApplicationCommand{ - Name: "folder", - Description: "Command for cloud folder creation", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: nameOption, - Description: "Type the folder's name", - Required: false, - MinLength: &nameMinLenght, - }, - }, - }, - - Handler: c.createFolderHandler, - } -} - -func (c *client) createFolderHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - const ( - nameOption string = "folder_name" - ) - - // Определение переменной для ответа - var result string = "unexpected result" - - // Определение выбранных вариантов ответа - options := i.ApplicationCommandData().Options - - optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) - for _, opt := range options { - optionMap[opt.Name] = opt - } - - // Creating request: - var req controller.FolderRequest - name, insertedValueNotNil := optionMap[nameOption] - dchan, err := s.Channel(i.ChannelID) - - if err != nil { - log.Printf("error while identifying channel: %v", err) - } else { - - if dchan.ParentID == c.conf.IsProjectChannel { - req.ChannelID = dchan.ID - if insertedValueNotNil { - req.InsertedName = name.StringValue() - } - - } else { - req.ChannelID = "" - if insertedValueNotNil { - req.InsertedName = name.StringValue() - } - } - } - - // Making request: - resp := c.controller.CreateFolder(context.TODO(), req) - if resp.Project == nil { - result = "Надо написать имя для папки, или создать папку из проекта!" - } else { - result = resp.Project.DiscordString() - if resp.Message != nil { - result += "Errors: " + resp.Message.Error() - } - } - - c.defaultFollowUp(result, s, i) -} diff --git a/client/discord/router/handle_git.go b/client/discord/router/handle_git.go deleted file mode 100644 index 0d71953..0000000 --- a/client/discord/router/handle_git.go +++ /dev/null @@ -1,117 +0,0 @@ -package router - -import ( - "context" - "log" - "ticket-pimp/internal/controller" - - "github.com/bwmarrin/discordgo" -) - -func (c *client) CreateRepoHandler(repoNameMinLength int) Command { - const ( - repoType = "repo_type" - projectRepo = "project_repo" - buildRepo = "build_repo" - nameOption = "repo_name" - ) - - return Command{ - Command: discordgo.ApplicationCommand{ - Name: "repo", - Description: "Creates repository of selected type. Name used for projects channels only", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: repoType, - Description: "The type of repo", - Required: true, - Choices: []*discordgo.ApplicationCommandOptionChoice{ - { - Name: "Unity project repo", - Value: projectRepo, - }, - { - Name: "XCode build repo", - Value: buildRepo, - }, - }, - }, - { - Type: discordgo.ApplicationCommandOptionString, - Name: nameOption, - Description: "Type the repository's name", - Required: false, - MinLength: &repoNameMinLength, - }, - }, - }, - Handler: c.createRepoHandler, - } -} - -func (c *client) createRepoHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - const ( - repoType = "repo_type" - projectRepo = "project_repo" - buildRepo = "build_repo" - nameOption = "repo_name" - ) - - // Определение переменной для ответа - var result string = "unexpected result" - - // Access options in the order provided by the user. - options := i.ApplicationCommandData().Options - - // Or convert the slice into a map - optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) - for _, opt := range options { - optionMap[opt.Name] = opt - } - - // Creating request: - var req controller.GitRequest - name, insertedValueNotNil := optionMap[nameOption] - isBuild := optionMap[repoType] - switch isBuild.StringValue() { - case buildRepo: - req.IsBuildGit = true - case projectRepo: - req.IsBuildGit = false - } - dchan, err := s.Channel(i.ChannelID) - - if err != nil { - log.Printf("error while identifying channel: %v", err) - } else { - - if dchan.ParentID == c.conf.IsProjectChannel { - req.ChannelID = dchan.ID - if insertedValueNotNil { - req.InsertedName = name.StringValue() - } - } else { - req.ChannelID = "" - if insertedValueNotNil { - req.InsertedName = name.StringValue() - } - } - } - - // Making request: - resp := c.controller.CreateGit(context.TODO(), req) - if resp.Project == nil { - if resp.Message != nil { - result = resp.Message.Error() - } - } else { - - result = resp.Project.DiscordString() - if resp.Message != nil { - result += "Errors: " + resp.Message.Error() - } - - } - c.defaultFollowUp(result, s, i) -} diff --git a/client/discord/router/handle_ping.go b/client/discord/router/handle_ping.go deleted file mode 100644 index 9257715..0000000 --- a/client/discord/router/handle_ping.go +++ /dev/null @@ -1,30 +0,0 @@ -package router - -import ( - "log" - - "github.com/bwmarrin/discordgo" -) - -func (c *client) Ping() Command { - return Command{ - Command: discordgo.ApplicationCommand{ - Name: "ping", - Description: "pongs in a reply", - }, - Handler: c.ping, - } -} - -func (c *client) ping(s *discordgo.Session, i *discordgo.InteractionCreate) { - - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Pong to: " + i.Member.User.Mention(), - }, - }) - if err != nil { - log.Println(err) - } -} diff --git a/client/discord/router/handle_ticket.go b/client/discord/router/handle_ticket.go deleted file mode 100644 index 85eef5c..0000000 --- a/client/discord/router/handle_ticket.go +++ /dev/null @@ -1,188 +0,0 @@ -package router - -import ( - "context" - "fmt" - "log" - "ticket-pimp/internal/domain" - "ticket-pimp/internal/helpers" - - "github.com/bwmarrin/discordgo" -) - -func (c *client) GetInfo() Command { - return Command{ - Command: discordgo.ApplicationCommand{ - Name: "info", - Description: "Get project's info", - }, - Handler: c.getInfo, - } -} - -func (c *client) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { - - var result string - - // Get channel from the request - dchan, err := s.Channel(i.ChannelID) - if err != nil { - result = "unable to get channel from the message" - } else { - project, err := c.controller.GetProjectByChannelID(context.TODO(), dchan.ID) - if err != nil { - result = err.Error() - } else { - if project != nil { - result = project.DiscordString() - if err != nil { - result += "Errors: " + err.Error() - } - } else { - result = "Something wrong with retrieving project from db" - } - - } - } - - c.defaultFollowUp(result, s, i) -} - -func (c *client) InitProjectFromChannel(minLength int) Command { - return Command{ - Command: discordgo.ApplicationCommand{ - Name: "init_project", - Description: "Connect project with Coda ID", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "key", - Description: "Project's key from Coda.io", - Required: true, - MinLength: &minLength, - }, - }, - }, - Handler: c.initProjectFromChannel, - } -} - -func (c *client) initProjectFromChannel(s *discordgo.Session, i *discordgo.InteractionCreate) { - - var result string - - // Get channel from the request - dchan, err := s.Channel(i.ChannelID) - if err != nil { - result = "unable to get channel from the message" - } else { - if dchan.ParentID != c.conf.IsProjectChannel { - // Sending result: - _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: "This channel is not at the project's group", - }) - - if err != nil { - s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: fmt.Sprintf("Something went wrong: %v", err), - }) - return - } - return - } - - // Access options in the order provided by the user. - options := i.ApplicationCommandData().Options - - // Or convert the slice into a map - optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) - for _, opt := range options { - optionMap[opt.Name] = opt - } - - if option, ok := optionMap["key"]; ok { - var errMsg error = nil - - project, err := c.controller.InitProjectInChannel(context.TODO(), i.ChannelID, option.StringValue()) - if err != nil { - result = fmt.Sprintf("unable to init project: %v", err) - } else { - result = project.DiscordString() - if errMsg != nil { - result += "Errors: " + errMsg.Error() - } - } - } - } - - c.defaultFollowUp(result, s, i) -} - -func (c *client) CreateTicketHandler(repoNameMinLength int) Command { - return Command{ - Command: discordgo.ApplicationCommand{ - Name: "project", - Description: "Create new development ticket", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "project_name", - Description: "Temporary project name", - Required: true, - MinLength: &repoNameMinLength, - }, - }, - }, - Handler: c.createTicketHandler, - } -} - -func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - - var result string - // Access options in the order provided by the user. - options := i.ApplicationCommandData().Options - - // Or convert the slice into a map - optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) - for _, opt := range options { - optionMap[opt.Name] = opt - } - - if option, ok := optionMap["project_name"]; ok { - dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText) - if err != nil { - result = fmt.Sprintf("chan creation problem: %v\n", err) - } else { - p, err := c.controller.ProjectCreate(context.TODO(), domain.Project{ - ChannelID: dchan.ID, - }) - if err != nil { - result = fmt.Sprintf("unable to create project: %v\n", err) - _, err := s.ChannelDelete(dchan.ID) - if err != nil { - result += fmt.Sprintf("\nunable to clean channel: %v\n", err) - } - } else { - edit := discordgo.ChannelEdit{ - Name: p.ShortName + "-" + helpers.Cut(option.StringValue()), - ParentID: c.conf.IsProjectChannel, - } - - dchan, err = s.ChannelEdit(dchan.ID, &edit) - if err != nil { - result = fmt.Sprintf("channel %s created, but unable to edit follow up message: %v\n", p.ShortName, err) - - } else { - _, err = s.ChannelMessageSend(dchan.ID, "Hello!") - if err != nil { - log.Printf("message send problem: %v\n", err) - } - result = "Project " + p.ShortName + " was created" - } - } - } - } - - c.defaultFollowUp(result, s, i) -} diff --git a/client/discord/router/handler.go b/client/discord/router/handler.go deleted file mode 100644 index 27f3097..0000000 --- a/client/discord/router/handler.go +++ /dev/null @@ -1,71 +0,0 @@ -package router - -import ( - "fmt" - "ticket-pimp/internal/controller" - "ticket-pimp/internal/domain" - - "github.com/bwmarrin/discordgo" -) - -type client struct { - Commands []Command - Components []Component - - // Tags []discordgo.ForumTag - - controller controller.WorkflowController - conf *domain.DiscordConfig - tgConf *domain.TelegramConfig -} - -// Подключение роутов к Discord боту -func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tgConf *domain.TelegramConfig) *client { - - var r client - r.controller = wc - r.conf = conf - - r.Commands = append(r.Commands, - r.CreateRepoHandler(3), - r.CreateFolderHandler(3), - r.Ping(), - r.CreateTicketHandler(3), - r.InitProjectFromChannel(3), - r.GetInfo(), - ) - r.Components = append(r.Components, - r.HandleTaskButtons(), - ) - r.tgConf = tgConf - - return &r -} - -// -// Подключение роутов к Discord боту - -type Command struct { - Command discordgo.ApplicationCommand - Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) -} - -type Component struct { - Component discordgo.MessageComponent - Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) -} - -func (h *client) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { - - // Sending result: - _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: answer, - }) - - if err != nil { - s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: fmt.Sprintf("Something went wrong: %v", err), - }) - return - } -} diff --git a/client/telegram/telegram.go b/client/telegram/telegram.go index 63fb7d8..6f81531 100644 --- a/client/telegram/telegram.go +++ b/client/telegram/telegram.go @@ -3,7 +3,7 @@ package telegram import ( "context" "log" - "ticket-pimp/client/telegram/handler" + "ticket-pimp/client/telegram/telegram_handler" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" "ticket-pimp/internal/services" @@ -30,7 +30,7 @@ func Run(ctx context.Context, opts TelegramOptions) error { log.Print("Start telegram bot init..") client := tg.New(opts.AppConfig.Telegram.Token) - h := handler.NewHandler( + h := telegram_handler.NewHandler( opts.GitService, opts.CloudService, opts.Coda, diff --git a/client/telegram/handler/handle_application.go b/client/telegram/telegram_handler/handle_application.go similarity index 97% rename from client/telegram/handler/handle_application.go rename to client/telegram/telegram_handler/handle_application.go index a24f673..38cba20 100644 --- a/client/telegram/handler/handle_application.go +++ b/client/telegram/telegram_handler/handle_application.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler // func (h *Handler) DevelopmentTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/telegram_handler/handle_farmtask.go similarity index 98% rename from client/telegram/handler/handle_farmtask.go rename to client/telegram/telegram_handler/handle_farmtask.go index 128c120..6442dc6 100644 --- a/client/telegram/handler/handle_farmtask.go +++ b/client/telegram/telegram_handler/handle_farmtask.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler import ( "context" diff --git a/client/telegram/handler/handle_folder.go b/client/telegram/telegram_handler/handle_folder.go similarity index 97% rename from client/telegram/handler/handle_folder.go rename to client/telegram/telegram_handler/handle_folder.go index fa88377..0a903f9 100644 --- a/client/telegram/handler/handle_folder.go +++ b/client/telegram/telegram_handler/handle_folder.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler import ( "context" diff --git a/client/telegram/handler/handle_git.go b/client/telegram/telegram_handler/handle_git.go similarity index 97% rename from client/telegram/handler/handle_git.go rename to client/telegram/telegram_handler/handle_git.go index 52e0a76..d53240d 100644 --- a/client/telegram/handler/handle_git.go +++ b/client/telegram/telegram_handler/handle_git.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler import ( "context" diff --git a/client/telegram/handler/handle_init.go b/client/telegram/telegram_handler/handle_init.go similarity index 98% rename from client/telegram/handler/handle_init.go rename to client/telegram/telegram_handler/handle_init.go index c01ad4c..266e7d2 100644 --- a/client/telegram/handler/handle_init.go +++ b/client/telegram/telegram_handler/handle_init.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler import ( "context" diff --git a/client/telegram/handler/handler.go b/client/telegram/telegram_handler/handler.go similarity index 95% rename from client/telegram/handler/handler.go rename to client/telegram/telegram_handler/handler.go index 283cb8d..1b91fa4 100644 --- a/client/telegram/handler/handler.go +++ b/client/telegram/telegram_handler/handler.go @@ -1,4 +1,4 @@ -package handler +package telegram_handler import ( "ticket-pimp/adapters" diff --git a/internal/services/dummy_telegram.go b/internal/services/dummy_telegram.go index dca909f..79ace7e 100644 --- a/internal/services/dummy_telegram.go +++ b/internal/services/dummy_telegram.go @@ -1,12 +1,28 @@ package services -import "ticket-pimp/internal/domain" +import ( + "ticket-pimp/internal/domain" + "time" +) type DummyTelegram struct { *CommonClient config domain.TelegramConfig } +func NewDummyClient(conf domain.TelegramConfig) *DummyTelegram { + + client := NewClient(). + SetTimeout(5 * time.Second) + + return &DummyTelegram{ + CommonClient: &CommonClient{ + Client: client, + }, + config: conf, + } +} + type TelegramMessage struct { ChatID string `json:"chat_id"` Text string `json:"text"` diff --git a/internal/services/git.go b/internal/services/git.go index 1d9800f..4671cc6 100644 --- a/internal/services/git.go +++ b/internal/services/git.go @@ -86,7 +86,6 @@ func (gb *Git) defaultGroupAsCollaborator(git *domain.Git) (*domain.Git, error) Perm: "push", } - // respURL := "/orgs/mobilerino/teams/devs/repos/mobilerino/" + git.Name respURL := fmt.Sprintf("/orgs/%s/teams/devs/repos/%s/%s", gb.conf.OrgName, gb.conf.OrgName, git.Name) resp, _ := gb.R(). From e5feb563600525125f694312e7455af4063867c4 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:54:20 +0500 Subject: [PATCH 08/20] fixed: unnecessary information in project response --- internal/domain/models.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/internal/domain/models.go b/internal/domain/models.go index 5f320b9..3cef45d 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -2,6 +2,7 @@ package domain import ( "fmt" + "strings" "time" ) @@ -143,22 +144,31 @@ type Git struct { type Project struct { ID string `json:"id"` //15 - ShortName string `json:"shortName"` //key-15 + Key string `json:"shortName"` //key-15 Name string `json:"name"` //default project name ChannelID string `json:"channel_id"` //123412341234 ProjectGit string `json:"project_git"` //https://github.com/mobilerino/dap-108 BuildGit string `json:"build_git"` //https://github.com/mobilerino/dap-108-build - Cloud string `json:"cloud"` //http://82.151.222.22:7000/f/86658 + Folder string `json:"cloud"` //http://82.151.222.22:7000/f/86658 } func (p *Project) DiscordString() string { - return fmt.Sprintf( - "## Project info:\n> 🔑 key: %s\n> 📂 folder: %s\n> 👾 project git: %s\n> 🚀 build git: %s\n", - p.ShortName, - p.Cloud, - p.ProjectGit, - p.BuildGit, - ) + var builder strings.Builder + builder.WriteString(fmt.Sprintf("## Project `%s`:\n> 🔑 key: %s", p.Name, p.Key)) + + if p.Folder != "" { + builder.WriteString(fmt.Sprintf("\n> 📂 folder: %s", p.Folder)) + } + + if p.ProjectGit != "" { + builder.WriteString(fmt.Sprintf("\n> 👾 project git: %s", p.ProjectGit)) + } + + if p.BuildGit != "" { + builder.WriteString(fmt.Sprintf("\n> 🚀 build git: %s", p.BuildGit)) + } + + return builder.String() } From 4c6ed95819dbfac506aa06e3d647ef3f1eff7b85 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:54:40 +0500 Subject: [PATCH 09/20] rename config var --- internal/domain/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/domain/config.go b/internal/domain/config.go index 3659895..9afd2d6 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -62,8 +62,9 @@ type ApplicationConfig struct { // InitConfig // InitConfig function reads provided file and setup envirmental variables; -func InitConfig(envFilePath string) Config { - err := godotenv.Load(envFilePath) +func InitConfig(env string) Config { + log.Printf("loading ENV from: %s", env) + err := godotenv.Load(env) if err != nil { log.Fatal("Error while loading env file") } From b9614e4c45ac77872672d9351bbf83d82a40c841 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:55:11 +0500 Subject: [PATCH 10/20] =?UTF-8?q?add=20"=D0=BD=D0=B5=20=D0=BD=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=D1=82"=20tag=20to=20tags=20preset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/discord/discord.go | 347 +++++++++++++++++++++++--------------- 1 file changed, 213 insertions(+), 134 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index 66964b9..c4ff776 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -20,99 +20,102 @@ var ( projectRepo string = "project_repo" buildRepo string = "build_repo" nameOption string = "repo_name" -) - -var tags = []discordgo.ForumTag{ - { - Name: "В работе", - Moderated: true, - EmojiName: "👩‍🍳", - }, - { - Name: "Готово", - Moderated: true, - EmojiName: "✅", - }, -} - -var commands = []discordgo.ApplicationCommand{ - { - Name: "ping", - Description: "pongs in a reply", - }, - { - Name: "init_project", - Description: "Connect project with Coda ID", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "key", - Description: "Project's key from Coda.io", - Required: true, - MinLength: &minLength, - }, + tagsPreset = [3]discordgo.ForumTag{ + { + Name: "В работе", + Moderated: true, + EmojiName: "👩‍🍳", }, - }, - { - Name: "project", - Description: "Create new development ticket", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: "project_name", - Description: "Temporary project name", - Required: true, - MinLength: &minLength, - }, + { + Name: "Готово", + Moderated: true, + EmojiName: "✅", }, - }, - { - Name: "info", - Description: "Get project's info", - }, - { - Name: "repo", - Description: "Creates repository of selected type. Name used for projects channels only", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: repoType, - Description: "The type of repo", - Required: true, - Choices: []*discordgo.ApplicationCommandOptionChoice{ - { - Name: "Unity project repo", - Value: projectRepo, - }, - { - Name: "XCode build repo", - Value: buildRepo, - }, + { + Name: "Не начат", + Moderated: true, + EmojiName: "🚧", + }, + } + commands = []discordgo.ApplicationCommand{ + { + Name: "ping", + Description: "pongs in a reply", + }, + { + Name: "init_project", + Description: "Connect project with Coda ID", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "key", + Description: "Project's key from Coda.io", + Required: true, + MinLength: &minLength, }, }, - { - Type: discordgo.ApplicationCommandOptionString, - Name: nameOption, - Description: "Type the repository's name", - Required: false, - MinLength: &minLength, + }, + { + Name: "project", + Description: "Create new development ticket", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "project_name", + Description: "Temporary project name", + Required: true, + MinLength: &minLength, + }, }, }, - }, - { - Name: "folder", - Description: "Command for cloud folder creation", - Options: []*discordgo.ApplicationCommandOption{ - { - Type: discordgo.ApplicationCommandOptionString, - Name: nameOption, - Description: "Type the folder's name", - Required: false, - MinLength: &minLength, + { + Name: "info", + Description: "Get project's info", + }, + { + Name: "repo", + Description: "Creates repository of selected type. Name used for projects channels only", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: repoType, + Description: "The type of repo", + Required: true, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Unity project repo", + Value: projectRepo, + }, + { + Name: "XCode build repo", + Value: buildRepo, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the repository's name", + Required: false, + MinLength: &minLength, + }, }, }, - }, -} + { + Name: "folder", + Description: "Command for cloud folder creation", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the folder's name", + Required: false, + MinLength: &minLength, + }, + }, + }, + } +) func initBotWith(token string) *discordgo.Session { discord, err := discordgo.New("Bot " + token) @@ -128,87 +131,122 @@ type DiscordOptions struct { Controller *controller.WorkflowController } -func Run(conf domain.Config, opts DiscordOptions) error { +// Моментальный ответ для избежания столкновения с протуханием токена +func initialResponse(s *discordgo.Session, i *discordgo.InteractionCreate) { - s := initBotWith(conf.Discord.Token) + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } - h := discord_handler.New( - opts.Controller, - &conf.Discord, - services.NewDummyClient(conf.Telegram), - tags, - ) + s.InteractionRespond(i.Interaction, &initialResponse) +} - commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} +// Определяем канал и реджектим запрос, если пишут в лс: +func isRejected(s *discordgo.Session, i *discordgo.InteractionCreate) bool { - for _, cmd := range commands { - var f func(s *discordgo.Session, i *discordgo.InteractionCreate) + dchan, err := s.Channel(i.ChannelID) + if err != nil { + return true + } - switch cmd.Name { + if dchan.Type == discordgo.ChannelTypeDM { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Yo, fella! I'm not working in private!", + }, + }) + return true + } + return false +} + +func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handler.Handler) { + initialResponse(s, i) + + if isRejected(s, i) { + return + } + + // Определяем тип взаимодействия и хэндлим правильной функцией: + switch i.Type { + case discordgo.InteractionApplicationCommand: + cmd := i.ApplicationCommandData().Name + + switch cmd { case "ping": - f = h.Ping + h.Ping(s, i) case "project": - f = h.CreateTicket + h.CreateProject(s, i) case "info": - f = h.ProjectInfo + h.ProjectInfo(s, i) case "repo": - f = h.CreateGit + h.CreateGit(s, i) case "folder": - f = h.CreateFolder + h.CreateFolder(s, i) case "init_project": - f = h.InitChannelAsProject + h.InitChannelAsProject(s, i) } - commandHandlers[cmd.Name] = f - } - - componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ - "task_start": h.HandleTaskButtons, - "task_close": h.HandleTaskButtons, - } - - s.AddHandler(h.ListenPosts) - - s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - h.AllInteractions(s, i) - - switch i.Type { - case discordgo.InteractionApplicationCommand: - if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { - h(s, i) - } - case discordgo.InteractionMessageComponent: - if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { - h(s, i) - } + case discordgo.InteractionMessageComponent: + c := i.MessageComponentData().CustomID + switch c { + case "task_start": + h.HandleTaskButtons(s, i) + case "task_close": + h.HandleTaskButtons(s, i) } - }) - - if err := s.Open(); err != nil { - return fmt.Errorf("cannot open the session: %v", err) } +} - // UPDATE FORUM IF NEEDED: +func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTag, error) { + log.Println("Updating forum chan...") + + // Get tasks channel instance: forum, err := s.Channel(conf.Discord.IsTaskForum) if err != nil { - log.Print(err) + return nil, err + } + + // Map all pre-set tags + var tagsMap = map[string]discordgo.ForumTag{} + for _, t := range forum.AvailableTags { + tagsMap[t.Name] = t + } + + // Result tags array + tags := forum.AvailableTags + + // Check if preset tag exists into current channel + for i := 0; i < len(tagsPreset); i++ { + _, ok := tagsMap[tagsPreset[i].Name] + if !ok { + tags = append(tags, tagsPreset[i]) + } } dchan, err := s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ AvailableTags: &tags, }) if err != nil { - log.Fatal(err) + return nil, err } log.Printf("Channel %s with ID %s propagated by tags:", dchan.Name, dchan.ID) for _, t := range dchan.AvailableTags { - log.Printf("N: %s, ID: %s", t.Name, t.ID) + fmt.Printf("N: %s, ID: %s", t.Name, t.ID) } + return dchan.AvailableTags, nil +} + +func commandRegistration(s *discordgo.Session, commands []discordgo.ApplicationCommand) []*discordgo.ApplicationCommand { log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand - for _, cmd := range commands { cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", &cmd) if err != nil { @@ -217,7 +255,48 @@ func Run(conf domain.Config, opts DiscordOptions) error { cmds = append(cmds, cmd) log.Println(cmd.Name + " command added") } + return cmds +} +func Run(conf *domain.Config, opts DiscordOptions) error { + + // bot init + s := initBotWith(conf.Discord.Token) + + // Init new handler + h := discord_handler.New( + opts.Controller, + &conf.Discord, + services.NewDummyClient(conf.Telegram), + ) + + // Add posts listener + s.AddHandler(h.ListenPosts) + + // Add interactions handlers + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + + route(s, i, *h) + }) + + // session opening + if err := s.Open(); err != nil { + return fmt.Errorf("cannot open the session: %v", err) + } + + // Forum update + tags, err := updateForum(conf, s) + if err != nil { + log.Println(err.Error()) + } + + //Update handler with tags: + h.SetAvailableTags(tags) + + // commands registration + cmds := commandRegistration(s, commands) + + // gracefull shutdown defer s.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) From bbe678fa464fcdf9ba963a9894820de54e9b168b Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:56:22 +0500 Subject: [PATCH 11/20] - middleware for whole interactions array - Create Project handler method refactoring --- client/discord/discord_handler/common.go | 29 --- .../discord_handler/discord_handler.go | 186 ++++++++++-------- 2 files changed, 101 insertions(+), 114 deletions(-) diff --git a/client/discord/discord_handler/common.go b/client/discord/discord_handler/common.go index 913d954..99d52d0 100644 --- a/client/discord/discord_handler/common.go +++ b/client/discord/discord_handler/common.go @@ -21,35 +21,6 @@ func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discor } } -func (h *Handler) AllInteractions(s *discordgo.Session, i *discordgo.InteractionCreate) { - - dchan, err := s.Channel(i.ChannelID) - if err != nil { - return - } - - if dchan.Type == discordgo.ChannelTypeDM { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Yo, fella! I'm not working in private!", - }, - }) - return - } - - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) -} - // setFlag // sets tag with In progress and Done text to discords channel; func (h *Handler) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { diff --git a/client/discord/discord_handler/discord_handler.go b/client/discord/discord_handler/discord_handler.go index dd9e8fe..3b29402 100644 --- a/client/discord/discord_handler/discord_handler.go +++ b/client/discord/discord_handler/discord_handler.go @@ -7,6 +7,7 @@ import ( "ticket-pimp/adapters" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" + "ticket-pimp/internal/helpers" "github.com/bwmarrin/discordgo" ) @@ -22,23 +23,30 @@ func New( controller *controller.WorkflowController, conf *domain.DiscordConfig, tg adapters.IDummyTelegram, - tags []discordgo.ForumTag, ) *Handler { return &Handler{ controller: controller, conf: conf, telegramDummyClient: tg, - tags: tags, } } +func (h *Handler) SetAvailableTags(tags []discordgo.ForumTag) { + h.tags = append(h.tags, tags...) +} + func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) { - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Pong to: " + i.Member.User.Mention(), - }, + var content string = fmt.Sprintf( + "**Pong to:** %s\n**App ID:** %s\n**Guild ID:** %s\nC**hannelID:** %s", + i.Member.User.Mention(), + i.AppID, + i.GuildID, + i.ChannelID, + ) + + _, err := s.FollowupMessageCreate(i.Interaction, false, &discordgo.WebhookParams{ + Content: content, }) if err != nil { log.Println(err) @@ -46,8 +54,10 @@ func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) { } // ListenPosts -// ..listens to new posts in specific channel -// to act them like a task +/* + ..listens to new posts in specific channel + to act them like a task +*/ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) { // Check if thread starter is not a bot, and thread started at the tasks channel; @@ -77,7 +87,7 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) return } - // [x] -- Отредактировать Thread name как для задачи + // Отредактировать Thread name как для задачи _, err = s.ChannelEditComplex(th.ID, &discordgo.ChannelEdit{ Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), }) @@ -118,16 +128,10 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) } } -// handleTaskBurrons +// handleTaskButtons // .. handler function to work with the Action Buttons over a task func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Send an empty interaction response; ---------------------------------------------------------------- - // s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - // Type: discordgo.InteractionResponseUpdateMessage, - // Data: &discordgo.InteractionResponseData{}, - // }) - // Get assignee value; --------------------------------------------------------------------------------- user := i.Member.User.Mention() @@ -212,6 +216,11 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti } } +// CreateFolder +/* + - creates project's cloud folder; + - writed folder link to db; +*/ func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCreate) { const ( nameOption string = "folder_name" @@ -276,6 +285,11 @@ func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCre h.defaultFollowUp(result, s, i) } +// CreateGit +/* + -creates project's git repository; + - writed git link to db; +*/ func (h *Handler) CreateGit(s *discordgo.Session, i *discordgo.InteractionCreate) { const ( repoType = "repo_type" @@ -352,20 +366,12 @@ func (h *Handler) CreateGit(s *discordgo.Session, i *discordgo.InteractionCreate h.defaultFollowUp(result, s, i) } -// PROJECT +// ProjectInfo +/* + Message in chat with related project information +*/ func (h *Handler) ProjectInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - var result string // Get channel from the request @@ -392,19 +398,12 @@ func (h *Handler) ProjectInfo(s *discordgo.Session, i *discordgo.InteractionCrea h.defaultFollowUp(result, s, i) } +// InitChannelAsProject +/* + - makes channel-project raw in the db storage; +*/ func (h *Handler) InitChannelAsProject(s *discordgo.Session, i *discordgo.InteractionCreate) { - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) - var result string // Get channel from the request @@ -454,18 +453,12 @@ func (h *Handler) InitChannelAsProject(s *discordgo.Session, i *discordgo.Intera h.defaultFollowUp(result, s, i) } -func (h *Handler) CreateTicket(s *discordgo.Session, i *discordgo.InteractionCreate) { - - // Моментальный ответ для избежания столкновения с протуханием токена - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) +// CreateProject +/* + - creates new proejct in the db; + - creates new channel and writes it to db; +*/ +func (h *Handler) CreateProject(s *discordgo.Session, i *discordgo.InteractionCreate) { var result string // Access options in the order provided by the user. @@ -477,40 +470,63 @@ func (h *Handler) CreateTicket(s *discordgo.Session, i *discordgo.InteractionCre optionMap[opt.Name] = opt } - if option, ok := optionMap["project_name"]; ok { - dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText) - if err != nil { - result = fmt.Sprintf("chan creation problem: %v\n", err) - } else { - p, err := h.controller.ProjectCreate(context.TODO(), domain.Project{ - ChannelID: dchan.ID, - }) - if err != nil { - result = fmt.Sprintf("unable to create project: %v\n", err) - _, err := s.ChannelDelete(dchan.ID) - if err != nil { - result += fmt.Sprintf("\nunable to clean channel: %v\n", err) - } - } else { - edit := discordgo.ChannelEdit{ - Name: p.ShortName, - ParentID: "1150719794853716028", - } - - dchan, err = s.ChannelEdit(dchan.ID, &edit) - if err != nil { - result = fmt.Sprintf("channel %s created, but unable to edit follow up message: %v\n", p.ShortName, err) - - } else { - _, err = s.ChannelMessageSend(dchan.ID, "Hello!") - if err != nil { - log.Printf("message send problem: %v\n", err) - } - result = "Project " + p.ShortName + "Was created" - } - } - } + // Get Project's title from the request: + var projectTitle string + if option, ok := optionMap["project_name"]; !ok { + return + } else { + projectTitle = option.StringValue() } + // Create channel with specified title + dchan, err := s.GuildChannelCreate(i.GuildID, projectTitle, discordgo.ChannelTypeGuildText) + if err != nil { + result = fmt.Sprintf("chan creation problem: %v\n", err) + h.defaultFollowUp(result, s, i) + return + } + + // Create DB raw with new project: + p, err := h.controller.ProjectCreate(context.TODO(), domain.Project{ + ChannelID: dchan.ID, + Name: projectTitle, + }) + + if err != nil { + result = fmt.Sprintf("unable to create project: %v\n", err) + + // Revert channel creation: + _, err := s.ChannelDelete(dchan.ID) + if err != nil { + result += fmt.Sprintf("\nunable to clean channel: %v\n", err) + } + + h.defaultFollowUp(result, s, i) + return + } + + // Edit created channel: + edit := discordgo.ChannelEdit{ + Name: p.Key + "-" + helpers.Cut(projectTitle), + ParentID: h.conf.IsProjectChannel, + } + + dchan, err = s.ChannelEdit(dchan.ID, &edit) + if err != nil { + result = fmt.Sprintf("channel %s created, but unable to edit follow up message: %v\n", p.Key, err) + h.defaultFollowUp(result, s, i) + return + + } + + // Отправить сообщение о создании проекта: + _, err = s.ChannelMessageSend(dchan.ID, p.DiscordString()) + if err != nil { + log.Printf("message send problem: %v\n", err) + result = "Project was created, but there is some problem with init channel message" + h.defaultFollowUp(result, s, i) + } + result = fmt.Sprintf("Project was created: https://discord.com/channels/%s/%s", i.GuildID, dchan.ID) + h.defaultFollowUp(result, s, i) } From ee856cb8b070634cd3f4d721ebd70cc578cf5f7d Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:56:37 +0500 Subject: [PATCH 12/20] - working with flags main func --- cmd/main.go | 74 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 94c641c..a9f5072 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "flag" "fmt" "log" "os" @@ -22,18 +23,26 @@ import ( "golang.org/x/sync/errgroup" ) -const ( - envfile = ".env" - migrationfile = "../internal/storage/migrate" - // production env: - // envfile = "../docker/prod.env" - // migrationfile = "../internal/storage/migrate" -) +const migrationfile = "../internal/storage/migrate" func main() { log.Print("started") - config := domain.InitConfig(envfile) - run(config) + + env := flag.Int("env", -1, "0 for development env file, 1 for production environment run, missing flag for build") + flag.Parse() + var envPath string + + switch *env { + case 0: + envPath = ".env" + case 1: + envPath = "../docker/prod.env" + default: + envPath = "prod.env" + } + + config := domain.InitConfig(envPath) + run(&config) } func Go(ctx context.Context, fns ...func(context.Context) error) error { @@ -49,23 +58,7 @@ func Go(ctx context.Context, fns ...func(context.Context) error) error { return group.Wait() } -func run(conf domain.Config) { - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) - defer cancel() - - // -- DB connection init -- START - connString := fmt.Sprintf( - "postgresql://%s:%s@%s:%s/%s", - conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, - ) - conn, err := pgxpool.New( - ctx, - connString) - if err != nil { - log.Fatalf("DB connection failed: %v", err) - } - // -- DB connection init -- END - +func applyMigrations(connString string) { // Aply migrations: dbConnConfig, err := pgxpool.ParseConfig(connString) @@ -87,26 +80,47 @@ func run(conf domain.Config) { fmt.Printf("Applied %d migrations!\n", n) db.Close() +} - // +func run(conf *domain.Config) { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) + defer cancel() + // -- DB connection init -- START + connString := fmt.Sprintf( + "postgresql://%s:%s@%s:%s/%s", + conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, + ) + conn, err := pgxpool.New( + ctx, + connString) + if err != nil { + log.Fatalf("DB connection failed: %v", err) + } + // -- DB connection init -- END + + // Aply migrations: + applyMigrations(connString) + + // Init services instances: gitService := services.NewGit(conf.Git) cloudService := services.NewCloud(conf.Cloud) codaService := services.NewCodaClient(conf.Coda) - // Инициализация контроллера: + // Controller instance init: controller := controller.NewWorkflowController( gitService, cloudService, codaService, conn, + conf, ) Go(ctx, func(ctx context.Context) error { opts := discord.DiscordOptions{ Controller: controller, - Config: &conf, + Config: conf, } if err := discord.Run(conf, opts); err != nil { return errors.Errorf("discord bot cannot be runned: %v", err) @@ -118,7 +132,7 @@ func run(conf domain.Config) { GitService: gitService, CloudService: cloudService, Coda: codaService, - AppConfig: &conf, + AppConfig: conf, Controller: controller, } From cd3f414ee0e64277501b9b67df53420adebda62d Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:57:31 +0500 Subject: [PATCH 13/20] rename vars: - ShortName -> Key - Cloud -> Folder --- internal/controller/control_folder.go | 12 ++++++------ internal/controller/control_git.go | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/controller/control_folder.go b/internal/controller/control_folder.go index 7a8f61b..e138c0c 100644 --- a/internal/controller/control_folder.go +++ b/internal/controller/control_folder.go @@ -35,13 +35,13 @@ func (wc *WorkflowController) CreateFolder(ctx context.Context, req FolderReques if project != nil { switch { - case project.Cloud != "": + case project.Folder != "": return &ProjectResponse{ Project: project, Message: nil, } - case project.ShortName != "": - name = project.ShortName + case project.Key != "": + name = project.Key case req.InsertedName != "": name = req.InsertedName } @@ -65,11 +65,11 @@ func (wc *WorkflowController) CreateFolder(ctx context.Context, req FolderReques result = ProjectResponse{ Project: &domain.Project{ ID: string(dbticket.ID), - ShortName: dbticket.Key.String, + Key: dbticket.Key.String, ChannelID: dbticket.Channelid.String, ProjectGit: dbticket.ProjectGit.String, BuildGit: dbticket.BuildGit.String, - Cloud: dbticket.Folder.String, + Folder: dbticket.Folder.String, }, Message: response.ErrMessage, } @@ -78,7 +78,7 @@ func (wc *WorkflowController) CreateFolder(ctx context.Context, req FolderReques response := wc.ICloud.CreateFolder(req.InsertedName) result = ProjectResponse{ Project: &domain.Project{ - Cloud: response.Folder.PrivateURL, + Folder: response.Folder.PrivateURL, }, Message: response.ErrMessage, } diff --git a/internal/controller/control_git.go b/internal/controller/control_git.go index 44a38d9..928006e 100644 --- a/internal/controller/control_git.go +++ b/internal/controller/control_git.go @@ -24,8 +24,8 @@ func (wc *WorkflowController) createGitForExistingProject(ctx context.Context, r dbticket db.Ticket ) switch { - case p.ShortName != "": - name = p.ShortName + case p.Key != "": + name = p.Key if req.IsBuildGit { name += "-build" } @@ -81,11 +81,11 @@ func (wc *WorkflowController) createGitForExistingProject(ctx context.Context, r return &ProjectResponse{ Project: &domain.Project{ ID: string(dbticket.ID), - ShortName: dbticket.Key.String, + Key: dbticket.Key.String, ChannelID: dbticket.Channelid.String, ProjectGit: dbticket.ProjectGit.String, BuildGit: dbticket.BuildGit.String, - Cloud: dbticket.Folder.String, + Folder: dbticket.Folder.String, }, Message: err, } From b50d98d961ce4c08cecde24a091259fdf292a2e1 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:58:17 +0500 Subject: [PATCH 14/20] rename shortname, key; --- internal/controller/control_project.go | 14 ++++--- internal/controller/control_task.go | 53 +++++++++++++------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/internal/controller/control_project.go b/internal/controller/control_project.go index d04cbb3..b7692d2 100644 --- a/internal/controller/control_project.go +++ b/internal/controller/control_project.go @@ -41,17 +41,19 @@ func (wc *WorkflowController) ProjectCreate(ctx context.Context, project domain. return nil, err } - project.ShortName = fmt.Sprintf( + project.Key = fmt.Sprintf( "%s-%d", appconfig.TicketKey.String, appconfig.TicketID.Int32, ) + // Set ID from the DB raw: project.ID = string(appconfig.TicketID.Int32) projectRow, err := qtx.CreateTicket(ctx, db.CreateTicketParams{ - Key: pgtype.Text{String: project.ShortName, Valid: true}, + Key: pgtype.Text{String: project.Key, Valid: true}, Channelid: pgtype.Text{String: project.ChannelID, Valid: true}, + Title: pgtype.Text{String: project.Name, Valid: true}, }) if err != nil { tx.Rollback(ctx) @@ -75,12 +77,12 @@ func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id stri } else { proj = domain.Project{ ID: string(dbTicket.ID), - ShortName: dbTicket.Key.String, + Key: dbTicket.Key.String, Name: dbTicket.Key.String, ChannelID: dbTicket.Channelid.String, ProjectGit: dbTicket.ProjectGit.String, BuildGit: dbTicket.BuildGit.String, - Cloud: dbTicket.Folder.String, + Folder: dbTicket.Folder.String, } } return &proj, nil @@ -108,11 +110,11 @@ func (wc *WorkflowController) InitProjectInChannel(ctx context.Context, channelI return &domain.Project{ ID: string(dbTicket.ID), - ShortName: dbTicket.Key.String, + Key: dbTicket.Key.String, Name: dbTicket.Key.String, ChannelID: dbTicket.Channelid.String, ProjectGit: dbTicket.ProjectGit.String, BuildGit: dbTicket.BuildGit.String, - Cloud: dbTicket.Folder.String, + Folder: dbTicket.Folder.String, }, nil } diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go index b992ca7..3b4ae14 100644 --- a/internal/controller/control_task.go +++ b/internal/controller/control_task.go @@ -3,7 +3,6 @@ package controller import ( "context" "fmt" - "os" "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" "time" @@ -42,17 +41,16 @@ func (wc *WorkflowController) WriteTaskToDB(t *domain.Task) (*domain.Task, error // InitTask /* -Runs the following: - - Use WriteTaskToDB method to make a new task row in the db; - - init new discord bot instance; - - + Runs the following: + - Use WriteTaskToDB method to make a new task row in the db; + - init new discord bot instance; -Possible errors: - - db record couldn't be created; - - bot couldn't be inited; - - bot session couldn't be started; - - thread couldn't be started; - - first task message couldn't be edited; + Possible errors: + - db record couldn't be created; + - bot couldn't be inited; + - bot session couldn't be started; + - thread couldn't be started; + - first task message couldn't be edited; */ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { @@ -61,14 +59,7 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { return nil, fmt.Errorf("unable to create task at the db: %v", err) } - // Инициализируем новый клиент дискорда - // [ ] Нездоровое получение параметров клиента из os.. - var ( - token = os.Getenv("DISCORD_TOKEN") - forumChannelID = os.Getenv("TASKS_CHANNEL") - ) - - s, err := discordgo.New("Bot " + token) + s, err := discordgo.New("Bot " + wc.conf.Discord.Token) if err != nil { return task, fmt.Errorf("unable to create discord session: %v", err) } @@ -100,9 +91,10 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { } th, err := s.ForumThreadStartComplex( - forumChannelID, + wc.conf.Discord.IsTaskForum, &discordgo.ThreadStart{ - Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), + Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), + AppliedTags: []string{"Не начат"}, }, &msg, ) @@ -126,25 +118,34 @@ func (wc *WorkflowController) UpdateTasksMessageID(ctx context.Context, msgID st return err } -func (wc *WorkflowController) UpdateTask(id string, opt int, user string) (*TaskConvertable, error) { +// UpdateTask +/* + - updates task by message Id + - with an action: + 0 for 'in progress' state; + 1 for 'done' state; + - and assignee.. +*/ +func (wc *WorkflowController) UpdateTask(messageId string, opt int, assignee string) (*TaskConvertable, error) { var ( err error dbtask db.Task ) + switch opt { case 0: dbtask, err = wc.q.StartTask(context.TODO(), db.StartTaskParams{ UpdatedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, - Assignee: pgtype.Text{String: user, Valid: true}, - Messageid: pgtype.Text{String: id, Valid: true}, + Assignee: pgtype.Text{String: assignee, Valid: true}, + Messageid: pgtype.Text{String: messageId, Valid: true}, }) return &TaskConvertable{&dbtask}, err case 1: dbtask, err = wc.q.CloseTask(context.TODO(), db.CloseTaskParams{ DeletedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, - Assignee: pgtype.Text{String: user, Valid: true}, - Messageid: pgtype.Text{String: id, Valid: true}, + Assignee: pgtype.Text{String: assignee, Valid: true}, + Messageid: pgtype.Text{String: messageId, Valid: true}, }) return &TaskConvertable{&dbtask}, err } From afbf89b01399f387df56339b77f007d94323c077 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 14:58:44 +0500 Subject: [PATCH 15/20] - removed tags preset from controller --- internal/controller/controller.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c3e4840..14f5515 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,7 +5,6 @@ import ( "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" - "github.com/bwmarrin/discordgo" "github.com/jackc/pgx/v5/pgxpool" ) @@ -15,7 +14,7 @@ type WorkflowController struct { ICoda adapters.ICoda pool *pgxpool.Pool q *db.Queries - ATags []discordgo.ForumTag + conf *domain.Config } func NewWorkflowController( @@ -23,6 +22,7 @@ func NewWorkflowController( cloud adapters.ICloud, coda adapters.ICoda, pool *pgxpool.Pool, + conf *domain.Config, ) *WorkflowController { return &WorkflowController{ IGit: git, @@ -30,6 +30,7 @@ func NewWorkflowController( ICoda: coda, pool: pool, q: db.New(pool), + conf: conf, } } From eb05bf847010e84b16ec45ca79d6a1d79c0032fb Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 24 Nov 2023 15:18:30 +0500 Subject: [PATCH 16/20] project name at the DB and everywhere --- internal/controller/control_folder.go | 1 + internal/controller/control_git.go | 1 + internal/storage/db/models.go | 1 + internal/storage/db/queries.sql.go | 31 ++++++++++++------- .../migrate/0002_projectName_to_tickets.sql | 7 +++++ internal/storage/sqlc/queries.sql | 4 +-- 6 files changed, 32 insertions(+), 13 deletions(-) create mode 100644 internal/storage/migrate/0002_projectName_to_tickets.sql diff --git a/internal/controller/control_folder.go b/internal/controller/control_folder.go index e138c0c..9ca52e1 100644 --- a/internal/controller/control_folder.go +++ b/internal/controller/control_folder.go @@ -65,6 +65,7 @@ func (wc *WorkflowController) CreateFolder(ctx context.Context, req FolderReques result = ProjectResponse{ Project: &domain.Project{ ID: string(dbticket.ID), + Name: dbticket.Title.String, Key: dbticket.Key.String, ChannelID: dbticket.Channelid.String, ProjectGit: dbticket.ProjectGit.String, diff --git a/internal/controller/control_git.go b/internal/controller/control_git.go index 928006e..6d27967 100644 --- a/internal/controller/control_git.go +++ b/internal/controller/control_git.go @@ -81,6 +81,7 @@ func (wc *WorkflowController) createGitForExistingProject(ctx context.Context, r return &ProjectResponse{ Project: &domain.Project{ ID: string(dbticket.ID), + Name: dbticket.Title.String, Key: dbticket.Key.String, ChannelID: dbticket.Channelid.String, ProjectGit: dbticket.ProjectGit.String, diff --git a/internal/storage/db/models.go b/internal/storage/db/models.go index 6af8f66..0e46b8e 100644 --- a/internal/storage/db/models.go +++ b/internal/storage/db/models.go @@ -35,4 +35,5 @@ type Ticket struct { CreatedAt pgtype.Timestamptz DeletedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz + Title pgtype.Text } diff --git a/internal/storage/db/queries.sql.go b/internal/storage/db/queries.sql.go index 13685a7..fbad40c 100644 --- a/internal/storage/db/queries.sql.go +++ b/internal/storage/db/queries.sql.go @@ -43,20 +43,21 @@ func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, err const createTicket = `-- name: CreateTicket :one INSERT INTO tickets ( - key, channelID + key, channelID, title ) VALUES ( - $1, $2 + $1, $2, $3 ) - RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at + RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title ` type CreateTicketParams struct { Key pgtype.Text Channelid pgtype.Text + Title pgtype.Text } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { - row := q.db.QueryRow(ctx, createTicket, arg.Key, arg.Channelid) + row := q.db.QueryRow(ctx, createTicket, arg.Key, arg.Channelid, arg.Title) var i Ticket err := row.Scan( &i.ID, @@ -68,6 +69,7 @@ func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Tic &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } @@ -145,7 +147,7 @@ func (q *Queries) GetTaskByMessage(ctx context.Context, messageid pgtype.Text) ( } const getTicketByChannelID = `-- name: GetTicketByChannelID :one -SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets WHERE channelID = $1 +SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title FROM tickets WHERE channelID = $1 ` func (q *Queries) GetTicketByChannelID(ctx context.Context, channelid pgtype.Text) (Ticket, error) { @@ -161,12 +163,13 @@ func (q *Queries) GetTicketByChannelID(ctx context.Context, channelid pgtype.Tex &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } const getTicketByID = `-- name: GetTicketByID :one -SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets WHERE id = $1 +SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title FROM tickets WHERE id = $1 ` func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) { @@ -182,6 +185,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) { &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } @@ -253,7 +257,7 @@ func (q *Queries) ListTasksByCreator(ctx context.Context, creatorLink pgtype.Tex } const listTickets = `-- name: ListTickets :many -SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets WHERE deleted_at IS NULL +SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title FROM tickets WHERE deleted_at IS NULL ` func (q *Queries) ListTickets(ctx context.Context) ([]Ticket, error) { @@ -275,6 +279,7 @@ func (q *Queries) ListTickets(ctx context.Context) ([]Ticket, error) { &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ); err != nil { return nil, err } @@ -287,7 +292,7 @@ func (q *Queries) ListTickets(ctx context.Context) ([]Ticket, error) { } const listTicketsWithDeleted = `-- name: ListTicketsWithDeleted :many -SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets +SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title FROM tickets ` func (q *Queries) ListTicketsWithDeleted(ctx context.Context) ([]Ticket, error) { @@ -309,6 +314,7 @@ func (q *Queries) ListTicketsWithDeleted(ctx context.Context) ([]Ticket, error) &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ); err != nil { return nil, err } @@ -383,7 +389,7 @@ const updateTicketBuildGit = `-- name: UpdateTicketBuildGit :one UPDATE tickets SET build_git = $1, updated_at = $2 WHERE channelID = $3 -RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at +RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title ` type UpdateTicketBuildGitParams struct { @@ -405,6 +411,7 @@ func (q *Queries) UpdateTicketBuildGit(ctx context.Context, arg UpdateTicketBuil &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } @@ -434,7 +441,7 @@ const updateTicketFolder = `-- name: UpdateTicketFolder :one UPDATE tickets SET folder = $1, updated_at = $2 WHERE channelID = $3 -RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at +RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title ` type UpdateTicketFolderParams struct { @@ -456,6 +463,7 @@ func (q *Queries) UpdateTicketFolder(ctx context.Context, arg UpdateTicketFolder &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } @@ -464,7 +472,7 @@ const updateTicketProjectGit = `-- name: UpdateTicketProjectGit :one UPDATE tickets SET project_git = $1, updated_at = $2 WHERE channelID = $3 -RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at +RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at, title ` type UpdateTicketProjectGitParams struct { @@ -486,6 +494,7 @@ func (q *Queries) UpdateTicketProjectGit(ctx context.Context, arg UpdateTicketPr &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, + &i.Title, ) return i, err } diff --git a/internal/storage/migrate/0002_projectName_to_tickets.sql b/internal/storage/migrate/0002_projectName_to_tickets.sql new file mode 100644 index 0000000..9d88f53 --- /dev/null +++ b/internal/storage/migrate/0002_projectName_to_tickets.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE tickets +ADD COLUMN title VARCHAR(100); + +-- +migrate Down +ALTER TABLE tickets +DROP COLUMN title; \ No newline at end of file diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql index cae5e62..a721d02 100644 --- a/internal/storage/sqlc/queries.sql +++ b/internal/storage/sqlc/queries.sql @@ -9,9 +9,9 @@ RETURNING *; -- name: CreateTicket :one INSERT INTO tickets ( - key, channelID + key, channelID, title ) VALUES ( - $1, $2 + $1, $2, $3 ) RETURNING *; From 19f6064066fec1561626bd3786453eb8eede5e3e Mon Sep 17 00:00:00 2001 From: naudachu Date: Sat, 25 Nov 2023 20:19:40 +0500 Subject: [PATCH 17/20] - finished coda usage --- .gitignore | 4 +- adapters/adapters.go | 2 +- client/discord/discord.go | 39 +++++++++++---- client/discord/discord_handler/common.go | 17 ++++--- client/telegram/telegram.go | 8 ++-- cmd/main.go | 44 ++++++++--------- internal/controller/control_coda.go | 27 +++++++++++ internal/controller/control_project.go | 24 +++++----- internal/controller/control_task.go | 2 +- internal/controller/controller.go | 2 +- internal/domain/config.go | 1 + internal/domain/models.go | 12 +++++ internal/domain/telegram.go | 7 --- internal/{services => external}/client.go | 2 +- internal/{services => external}/cloud.go | 2 +- internal/{services => external}/coda.go | 47 +++++++++++++++++-- .../{services => external}/dummy_telegram.go | 2 +- internal/{services => external}/git.go | 2 +- 18 files changed, 170 insertions(+), 74 deletions(-) create mode 100644 internal/controller/control_coda.go delete mode 100644 internal/domain/telegram.go rename internal/{services => external}/client.go (97%) rename internal/{services => external}/cloud.go (99%) rename internal/{services => external}/coda.go (70%) rename internal/{services => external}/dummy_telegram.go (98%) rename internal/{services => external}/git.go (99%) diff --git a/.gitignore b/.gitignore index 22ed4fe..622605e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vscode/ +.idea/ +.github +docker/ **/**/*.env -docker/** \ No newline at end of file diff --git a/adapters/adapters.go b/adapters/adapters.go index 84d0181..44da1e6 100644 --- a/adapters/adapters.go +++ b/adapters/adapters.go @@ -16,7 +16,7 @@ type ICloud interface { type ICoda interface { ListDocs() - CreateApp(task domain.CodaApplication) + CreateApp(task domain.CodaApplication) (string, error) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) GetRowLink(id string) (string, error) } diff --git a/client/discord/discord.go b/client/discord/discord.go index c4ff776..aab5fde 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -9,7 +9,7 @@ import ( "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" - "ticket-pimp/internal/services" + "ticket-pimp/internal/external" "github.com/bwmarrin/discordgo" ) @@ -42,6 +42,10 @@ var ( Name: "ping", Description: "pongs in a reply", }, + { + Name: "coda_ticket", + Description: "Creates ticket in Coda.io w/ provided info", + }, { Name: "init_project", Description: "Connect project with Coda ID", @@ -166,7 +170,7 @@ func isRejected(s *discordgo.Session, i *discordgo.InteractionCreate) bool { } func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handler.Handler) { - initialResponse(s, i) + // initialResponse(s, i) if isRejected(s, i) { return @@ -175,9 +179,8 @@ func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handl // Определяем тип взаимодействия и хэндлим правильной функцией: switch i.Type { case discordgo.InteractionApplicationCommand: - cmd := i.ApplicationCommandData().Name - switch cmd { + switch i.ApplicationCommandData().Name { case "ping": h.Ping(s, i) case "project": @@ -190,10 +193,13 @@ func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handl h.CreateFolder(s, i) case "init_project": h.InitChannelAsProject(s, i) + case "coda_ticket": + h.CreateCoda(s, i) } + case discordgo.InteractionMessageComponent: - c := i.MessageComponentData().CustomID - switch c { + + switch i.MessageComponentData().CustomID { case "task_start": h.HandleTaskButtons(s, i) case "task_close": @@ -221,10 +227,11 @@ func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTa // Result tags array tags := forum.AvailableTags - // Check if preset tag exists into current channel + // Check if preset tag exists into current channel.. for i := 0; i < len(tagsPreset); i++ { _, ok := tagsMap[tagsPreset[i].Name] if !ok { + // .. and append them if they aren't tags = append(tags, tagsPreset[i]) } } @@ -241,6 +248,22 @@ func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTa fmt.Printf("N: %s, ID: %s", t.Name, t.ID) } + // Update config w/ tags: + confTags := make(map[domain.TaskState]string) + + for _, tag := range dchan.AvailableTags { + switch tag.Name { + case "Не начат": + confTags[domain.State(0)] = tag.ID + case "В работе": + confTags[domain.State(1)] = tag.ID + case "Готово": + confTags[domain.State(2)] = tag.ID + } + } + + conf.Discord.Tags = confTags + return dchan.AvailableTags, nil } @@ -267,7 +290,7 @@ func Run(conf *domain.Config, opts DiscordOptions) error { h := discord_handler.New( opts.Controller, &conf.Discord, - services.NewDummyClient(conf.Telegram), + external.NewDummyClient(conf.Telegram), ) // Add posts listener diff --git a/client/discord/discord_handler/common.go b/client/discord/discord_handler/common.go index 99d52d0..3519c1f 100644 --- a/client/discord/discord_handler/common.go +++ b/client/discord/discord_handler/common.go @@ -1,24 +1,23 @@ package discord_handler import ( - "fmt" - "github.com/bwmarrin/discordgo" ) func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { // Sending result: - _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + /*_, err := */ + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ Content: answer, }) - if err != nil { - s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ - Content: fmt.Sprintf("Something went wrong: %v", err), - }) - return - } + // if err != nil { + // s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + // Content: fmt.Sprintf("Something went wrong: %v", err), + // }) + // return + // } } // setFlag diff --git a/client/telegram/telegram.go b/client/telegram/telegram.go index 6f81531..5e71200 100644 --- a/client/telegram/telegram.go +++ b/client/telegram/telegram.go @@ -6,16 +6,16 @@ import ( "ticket-pimp/client/telegram/telegram_handler" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" - "ticket-pimp/internal/services" + "ticket-pimp/internal/external" "github.com/mr-linch/go-tg" "github.com/mr-linch/go-tg/tgb" ) type TelegramOptions struct { - GitService *services.Git - CloudService *services.Cloud - Coda *services.Coda + GitService *external.Git + CloudService *external.Cloud + Coda *external.Coda AppConfig *domain.Config Controller *controller.WorkflowController } diff --git a/cmd/main.go b/cmd/main.go index a9f5072..0a9e9d9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,7 +11,7 @@ import ( "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" - "ticket-pimp/internal/services" + "ticket-pimp/internal/external" "ticket-pimp/client/discord" "ticket-pimp/client/telegram" @@ -59,7 +59,7 @@ func Go(ctx context.Context, fns ...func(context.Context) error) error { } func applyMigrations(connString string) { - // Aply migrations: + // Apply migrations: dbConnConfig, err := pgxpool.ParseConfig(connString) if err != nil { @@ -79,17 +79,20 @@ func applyMigrations(connString string) { } fmt.Printf("Applied %d migrations!\n", n) - db.Close() + err = db.Close() + if err != nil { + log.Fatal("unable to close db connection") + } } -func run(conf *domain.Config) { +func run(c *domain.Config) { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) defer cancel() // -- DB connection init -- START connString := fmt.Sprintf( "postgresql://%s:%s@%s:%s/%s", - conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, + c.DB.User, c.DB.Pass, c.DB.Host, c.DB.Port, c.DB.Name, ) conn, err := pgxpool.New( ctx, @@ -97,42 +100,41 @@ func run(conf *domain.Config) { if err != nil { log.Fatalf("DB connection failed: %v", err) } - // -- DB connection init -- END - // Aply migrations: + // Apply migrations: applyMigrations(connString) // Init services instances: - gitService := services.NewGit(conf.Git) - cloudService := services.NewCloud(conf.Cloud) - codaService := services.NewCodaClient(conf.Coda) + git := external.NewGit(c.Git) + cloud := external.NewCloud(c.Cloud) + coda := external.NewCoda(c.Coda) // Controller instance init: - controller := controller.NewWorkflowController( - gitService, - cloudService, - codaService, + controller := controller.NewApp( + git, + cloud, + coda, conn, - conf, + c, ) Go(ctx, func(ctx context.Context) error { opts := discord.DiscordOptions{ Controller: controller, - Config: conf, + Config: c, } - if err := discord.Run(conf, opts); err != nil { + if err := discord.Run(c, opts); err != nil { return errors.Errorf("discord bot cannot be runned: %v", err) } return nil }, func(ctx context.Context) error { opts := telegram.TelegramOptions{ - GitService: gitService, - CloudService: cloudService, - Coda: codaService, - AppConfig: conf, + GitService: git, + CloudService: cloud, + Coda: coda, + AppConfig: c, Controller: controller, } diff --git a/internal/controller/control_coda.go b/internal/controller/control_coda.go new file mode 100644 index 0000000..b0798ed --- /dev/null +++ b/internal/controller/control_coda.go @@ -0,0 +1,27 @@ +package controller + +import ( + "context" + "fmt" + "ticket-pimp/internal/domain" +) + +func (wc *WorkflowController) CreateCoda(guildID string, chanID string) (string, error) { + + p, err := wc.GetProjectByChannelID(context.TODO(), chanID) + if err != nil { + return "", err + } + + requestResult, err := wc.ICoda.CreateApp(domain.CodaApplication{ + ID: p.Key, + Summary: p.Name, + URL: fmt.Sprintf("https://discord.com/channels/%s/%s", guildID, chanID), + Git: p.ProjectGit, + GitBuild: p.BuildGit, + Folder: p.Folder, + }) + + return requestResult, err + +} diff --git a/internal/controller/control_project.go b/internal/controller/control_project.go index b7692d2..0242846 100644 --- a/internal/controller/control_project.go +++ b/internal/controller/control_project.go @@ -3,6 +3,7 @@ package controller import ( "context" "fmt" + "strconv" "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" @@ -67,25 +68,24 @@ func (wc *WorkflowController) ProjectCreate(ctx context.Context, project domain. } func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id string) (*domain.Project, error) { - var proj domain.Project + dbTicket, err := wc.q.GetTicketByChannelID(ctx, pgtype.Text{String: id, Valid: true}) if err != nil { if err == pgx.ErrNoRows { return nil, nil } return nil, err - } else { - proj = domain.Project{ - ID: string(dbTicket.ID), - Key: dbTicket.Key.String, - Name: dbTicket.Key.String, - ChannelID: dbTicket.Channelid.String, - ProjectGit: dbTicket.ProjectGit.String, - BuildGit: dbTicket.BuildGit.String, - Folder: dbTicket.Folder.String, - } } - return &proj, nil + + return &domain.Project{ + ID: strconv.Itoa(int(dbTicket.ID)), + Key: dbTicket.Key.String, + Name: dbTicket.Title.String, + ChannelID: dbTicket.Channelid.String, + ProjectGit: dbTicket.ProjectGit.String, + BuildGit: dbTicket.BuildGit.String, + Folder: dbTicket.Folder.String, + }, nil } // Saves current channel as project's channel; diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go index 3b4ae14..345ee28 100644 --- a/internal/controller/control_task.go +++ b/internal/controller/control_task.go @@ -94,7 +94,7 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { wc.conf.Discord.IsTaskForum, &discordgo.ThreadStart{ Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), - AppliedTags: []string{"Не начат"}, + AppliedTags: []string{wc.conf.Discord.Tags[domain.NewTaskState()]}, }, &msg, ) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 14f5515..0d3bb7b 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -17,7 +17,7 @@ type WorkflowController struct { conf *domain.Config } -func NewWorkflowController( +func NewApp( git adapters.IGit, cloud adapters.ICloud, coda adapters.ICoda, diff --git a/internal/domain/config.go b/internal/domain/config.go index 9afd2d6..ef31927 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -53,6 +53,7 @@ type DiscordConfig struct { Token string IsProjectChannel string IsTaskForum string + Tags map[TaskState]string } type ApplicationConfig struct { diff --git a/internal/domain/models.go b/internal/domain/models.go index 3cef45d..cc0201b 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -83,6 +83,18 @@ const ( done ) +func State(i int) TaskState { + switch i { + case 0: + return new + case 1: + return inprogress + case 2: + return done + } + return -1 +} + func NewTaskState() TaskState { return TaskState(0) } diff --git a/internal/domain/telegram.go b/internal/domain/telegram.go deleted file mode 100644 index 11cbaa3..0000000 --- a/internal/domain/telegram.go +++ /dev/null @@ -1,7 +0,0 @@ -package domain - -type TgUser struct { - ID string - Name string - TgLink string -} diff --git a/internal/services/client.go b/internal/external/client.go similarity index 97% rename from internal/services/client.go rename to internal/external/client.go index 8d56dcd..c51f56a 100644 --- a/internal/services/client.go +++ b/internal/external/client.go @@ -1,4 +1,4 @@ -package services +package external import ( "fmt" diff --git a/internal/services/cloud.go b/internal/external/cloud.go similarity index 99% rename from internal/services/cloud.go rename to internal/external/cloud.go index b4f1692..152e43d 100644 --- a/internal/services/cloud.go +++ b/internal/external/cloud.go @@ -1,4 +1,4 @@ -package services +package external import ( "errors" diff --git a/internal/services/coda.go b/internal/external/coda.go similarity index 70% rename from internal/services/coda.go rename to internal/external/coda.go index 3cf1c5a..6d04afe 100644 --- a/internal/services/coda.go +++ b/internal/external/coda.go @@ -1,8 +1,10 @@ -package services +package external import ( + "errors" "fmt" "log" + "strings" "ticket-pimp/internal/domain" "time" @@ -14,7 +16,7 @@ type Coda struct { Config domain.CodaConfig } -func NewCodaClient(conf domain.CodaConfig) *Coda { +func NewCoda(conf domain.CodaConfig) *Coda { client := NewClient(). SetTimeout(15 * time.Second). @@ -47,14 +49,49 @@ func (c *Coda) ListDocs() { log.Print(resp) } -func (c *Coda) CreateApp(task domain.CodaApplication) { - resp, _ := c.R(). +type CodaWebhookResponse struct { + ReqID string `json:"requestId"` +} + +func (c *Coda) CreateApp(task domain.CodaApplication) (string, error) { + + var whResponse CodaWebhookResponse + c.R(). SetBody(task). SetContentType("application/json"). + SetSuccessResult(&whResponse). SetBearerAuthToken(c.Config.Develop). Post("/docs/Ic3IZpQ3Wk/hooks/automation/grid-auto-NlUwM7F7Cr") - fmt.Print(resp) + if whResponse.ReqID == "" { + return "", errors.New("coda responded w/o mutate id") + } + + var ( + // mutate string + sep string = ":" + ) + + if !strings.Contains(whResponse.ReqID, sep) { + return "", fmt.Errorf("unexpected coda response: %s", whResponse.ReqID) + } + + // arr := strings.Split(whResponse.ReqID, sep) + // if arr[0] == "mutate" { + // mutate = arr[1] + // } + + // mutateResponse, err := c.R(). + // SetContentType("application/json"). + // SetBearerAuthToken(c.Config.Develop). + // Get(fmt.Sprintf("/mutationStatus/%s", mutate)) + + // if err != nil { + // return "", fmt.Errorf("unable to get coda mutate result: %s", mutate) + // } + + // _ = mutateResponse + return whResponse.ReqID, nil } func (c *Coda) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) { diff --git a/internal/services/dummy_telegram.go b/internal/external/dummy_telegram.go similarity index 98% rename from internal/services/dummy_telegram.go rename to internal/external/dummy_telegram.go index 79ace7e..867a311 100644 --- a/internal/services/dummy_telegram.go +++ b/internal/external/dummy_telegram.go @@ -1,4 +1,4 @@ -package services +package external import ( "ticket-pimp/internal/domain" diff --git a/internal/services/git.go b/internal/external/git.go similarity index 99% rename from internal/services/git.go rename to internal/external/git.go index 4671cc6..8bf4aa3 100644 --- a/internal/services/git.go +++ b/internal/external/git.go @@ -1,4 +1,4 @@ -package services +package external import ( "fmt" From 2be4b1dfd426c09a5d2b7bae894a3fa76a10d5f5 Mon Sep 17 00:00:00 2001 From: naudachu Date: Mon, 27 Nov 2023 17:44:32 +0500 Subject: [PATCH 18/20] - router implementation --- client/discord/discord.go | 103 +++---------- .../discord_handler/discord_handler.go | 142 ++++++++++++++++-- client/discord/discord_router/router.go | 74 +++++++++ 3 files changed, 228 insertions(+), 91 deletions(-) create mode 100644 client/discord/discord_router/router.go diff --git a/client/discord/discord.go b/client/discord/discord.go index aab5fde..dd5a908 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -6,6 +6,7 @@ import ( "os" "os/signal" "ticket-pimp/client/discord/discord_handler" + "ticket-pimp/client/discord/discord_router" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" @@ -19,7 +20,7 @@ var ( repoType string = "repo_type" projectRepo string = "project_repo" buildRepo string = "build_repo" - nameOption string = "repo_name" + nameOption string = "name" tagsPreset = [3]discordgo.ForumTag{ { Name: "В работе", @@ -135,79 +136,6 @@ type DiscordOptions struct { Controller *controller.WorkflowController } -// Моментальный ответ для избежания столкновения с протуханием токена -func initialResponse(s *discordgo.Session, i *discordgo.InteractionCreate) { - - initialResponse := discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Flags: discordgo.MessageFlagsEphemeral, - Content: "👩‍🍳 Cooking your query..", - }, - } - - s.InteractionRespond(i.Interaction, &initialResponse) -} - -// Определяем канал и реджектим запрос, если пишут в лс: -func isRejected(s *discordgo.Session, i *discordgo.InteractionCreate) bool { - - dchan, err := s.Channel(i.ChannelID) - if err != nil { - return true - } - - if dchan.Type == discordgo.ChannelTypeDM { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Yo, fella! I'm not working in private!", - }, - }) - return true - } - return false -} - -func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handler.Handler) { - // initialResponse(s, i) - - if isRejected(s, i) { - return - } - - // Определяем тип взаимодействия и хэндлим правильной функцией: - switch i.Type { - case discordgo.InteractionApplicationCommand: - - switch i.ApplicationCommandData().Name { - case "ping": - h.Ping(s, i) - case "project": - h.CreateProject(s, i) - case "info": - h.ProjectInfo(s, i) - case "repo": - h.CreateGit(s, i) - case "folder": - h.CreateFolder(s, i) - case "init_project": - h.InitChannelAsProject(s, i) - case "coda_ticket": - h.CreateCoda(s, i) - } - - case discordgo.InteractionMessageComponent: - - switch i.MessageComponentData().CustomID { - case "task_start": - h.HandleTaskButtons(s, i) - case "task_close": - h.HandleTaskButtons(s, i) - } - } -} - func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTag, error) { log.Println("Updating forum chan...") @@ -293,14 +221,33 @@ func Run(conf *domain.Config, opts DiscordOptions) error { external.NewDummyClient(conf.Telegram), ) + r := discord_router.NewApp(s) + + var commonMw = []discord_router.Middleware{ + h.WithInitialResponse, + h.RejectPM, + } + + // Handle commands + r. + Route("ping", r.Wrapped(h.Ping, commonMw...)). + Route("project", r.Wrapped(h.CreateProject, commonMw...)). + Route("info", r.Wrapped(h.ProjectInfo, commonMw...)). + Route("repo", r.Wrapped(h.CreateGit, commonMw...)). + Route("folder", r.Wrapped(h.CreateFolder, commonMw...)). + Route("init_project", r.Wrapped(h.InitChannelAsProject, commonMw...)). + Route("coda_ticket", r.Wrapped(h.CreateCoda, commonMw...)) + + // Handle components + r. + Route("task_start", h.HandleTaskButtons). + Route("task_close", h.HandleTaskButtons) + // Add posts listener s.AddHandler(h.ListenPosts) // Add interactions handlers - s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - - route(s, i, *h) - }) + s.AddHandler(r.Serve) // session opening if err := s.Open(); err != nil { diff --git a/client/discord/discord_handler/discord_handler.go b/client/discord/discord_handler/discord_handler.go index 3b29402..0e45531 100644 --- a/client/discord/discord_handler/discord_handler.go +++ b/client/discord/discord_handler/discord_handler.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "ticket-pimp/adapters" + "ticket-pimp/client/discord/discord_router" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" "ticket-pimp/internal/helpers" @@ -31,6 +32,69 @@ func New( } } +func (h *Handler) RejectPM(f discord_router.HandlerFunc) discord_router.HandlerFunc { + return func(s *discordgo.Session, i *discordgo.InteractionCreate) { + + dchan, err := s.Channel(i.ChannelID) + if err != nil { + log.Println(err) + return + } + + if dchan.Type != discordgo.ChannelTypeDM { + f(s, i) + return + } + + respondWithReject(s, i) + } +} + +func respondWithReject(s *discordgo.Session, i *discordgo.InteractionCreate) { + + var content = "Yo, fella! I'm not working in private!" + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: content, + }, + }) + + if err == nil { + return + } + + _, err = s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: &content, + }) + + if err != nil { + log.Printf("unable to edit answer with unknown error %v", err) + } +} + +// Моментальный ответ для избежания столкновения с протуханием токена +func (h *Handler) WithInitialResponse(f discord_router.HandlerFunc) discord_router.HandlerFunc { + return func(s *discordgo.Session, i *discordgo.InteractionCreate) { + + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + err := s.InteractionRespond(i.Interaction, &initialResponse) + if err != nil { + log.Println(err) + } + + f(s, i) + } +} + func (h *Handler) SetAvailableTags(tags []discordgo.ForumTag) { h.tags = append(h.tags, tags...) } @@ -45,9 +109,10 @@ func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) { i.ChannelID, ) - _, err := s.FollowupMessageCreate(i.Interaction, false, &discordgo.WebhookParams{ - Content: content, + _, err := s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ + Content: &content, }) + if err != nil { log.Println(err) } @@ -65,8 +130,10 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) return } + // Get all messages from the channel: msgs, _ := s.ChannelMessages(th.ID, 1, "", "", "") + // Take the first one: msg, _ := s.ChannelMessage(th.ID, msgs[0].ID) if msg.Author.ID == s.State.User.ID { @@ -88,8 +155,14 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) } // Отредактировать Thread name как для задачи + appliedTags := []string{ + h.conf.Tags[domain.NewTaskState()], + // h.tags[2].ID, + } + _, err = s.ChannelEditComplex(th.ID, &discordgo.ChannelEdit{ - Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), + Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), + AppliedTags: &appliedTags, }) if err != nil { log.Printf("th edition is not complete: %v", err) @@ -132,6 +205,12 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) // .. handler function to work with the Action Buttons over a task func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{}, + }) + _ = err + // Get assignee value; --------------------------------------------------------------------------------- user := i.Member.User.Mention() @@ -209,7 +288,6 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti log.Printf("th start message edition is not complete: %v", err) } - // [ ] Устанавливаем тэги статуса на тред --------------------------------------------------------------------- err = h.setFlag(s, i, &tag) if err != nil { log.Printf("error while `start` tag setting: %v", err) @@ -291,12 +369,14 @@ func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCre - writed git link to db; */ func (h *Handler) CreateGit(s *discordgo.Session, i *discordgo.InteractionCreate) { + const ( - repoType = "repo_type" - projectRepo = "project_repo" - buildRepo = "build_repo" - nameOption = "repo_name" + typeOfRepo = "repo_type" + projectRepoType = "project_repo" + buildRepoType = "build_repo" + nameOption = "repo_name" ) + // Моментальный ответ для избежания столкновения с протуханием токена initialResponse := discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -322,18 +402,22 @@ func (h *Handler) CreateGit(s *discordgo.Session, i *discordgo.InteractionCreate // Creating request: var req controller.GitRequest + name, insertedValueNotNil := optionMap[nameOption] - isBuild := optionMap[repoType] + + isBuild := optionMap[typeOfRepo] + switch isBuild.StringValue() { - case buildRepo: + case buildRepoType: req.IsBuildGit = true - case projectRepo: + case projectRepoType: req.IsBuildGit = false } - dchan, err := s.Channel(i.ChannelID) + dchan, err := s.Channel(i.ChannelID) if err != nil { - log.Printf("error while identifying channel: %v", err) + h.defaultFollowUp("error while identifying channel: %v", s, i) + return } else { if dchan.ParentID == h.conf.IsProjectChannel { @@ -530,3 +614,35 @@ func (h *Handler) CreateProject(s *discordgo.Session, i *discordgo.InteractionCr h.defaultFollowUp(result, s, i) } + +// CreateCoda +/* + - sends request to Coda.io; +*/ +func (h *Handler) CreateCoda(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Get channel from the request + dchan, err := s.Channel(i.ChannelID) + if err != nil { + h.defaultFollowUp("unable to get channel from the message", s, i) + return + } + + if dchan.ParentID != h.conf.IsProjectChannel { + h.defaultFollowUp("This channel is not at the project's group", s, i) + return + } + + //[ ] достать проект из базы и послать в коду + result, err := h.controller.CreateCoda(i.GuildID, dchan.ID) + if err != nil { + h.defaultFollowUp(fmt.Sprintf("unable to create coda: %v", err), s, i) + return + } + + if err != nil { + result += fmt.Sprintf("\nexecuted w/ error: %v", err) + } + + h.defaultFollowUp(result, s, i) +} diff --git a/client/discord/discord_router/router.go b/client/discord/discord_router/router.go new file mode 100644 index 0000000..6a1ad94 --- /dev/null +++ b/client/discord/discord_router/router.go @@ -0,0 +1,74 @@ +package discord_router + +import "github.com/bwmarrin/discordgo" + +type HandlerFunc func(*discordgo.Session, *discordgo.InteractionCreate) + +type RouteEntry struct { + CommandName string + Handler HandlerFunc +} + +func (re *RouteEntry) Match(i *discordgo.InteractionCreate) bool { + switch i.Type { + case discordgo.InteractionApplicationCommand: + if i.ApplicationCommandData().Name != re.CommandName { + return false + } + case discordgo.InteractionMessageComponent: + if i.MessageComponentData().CustomID != re.CommandName { + return false + } + } + + return true +} + +type Router struct { + session *discordgo.Session + routes []RouteEntry +} + +func NewApp(s *discordgo.Session) *Router { + return &Router{ + session: s, + } +} + +func (r *Router) Route(cmd string, handlerFunc HandlerFunc) *Router { + + r.routes = append(r.routes, RouteEntry{ + CommandName: cmd, + Handler: handlerFunc, + }) + + return r +} + +func (r *Router) Serve(s *discordgo.Session, i *discordgo.InteractionCreate) { + for _, e := range r.routes { + ok := e.Match(i) + if ok { + e.Handler(s, i) + } + } + //[ ] Is there something like 404?! +} + +type Middleware func(HandlerFunc) HandlerFunc + +func (r *Router) Wrapped(f HandlerFunc, m ...Middleware) HandlerFunc { + + if len(m) < 1 { + return f + } + + wrapped := f + + // loop in reverse to preserve middleware order + for i := len(m) - 1; i >= 0; i-- { + wrapped = m[i](wrapped) + } + + return wrapped +} From 5d6adb2a9af776c4b4a1d7a1ea3c9f1490b5075a Mon Sep 17 00:00:00 2001 From: naudachu Date: Mon, 27 Nov 2023 18:18:45 +0500 Subject: [PATCH 19/20] simple routeer finished --- client/discord/discord.go | 6 ++---- client/discord/discord_handler/discord_handler.go | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index dd5a908..10895f2 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -236,10 +236,8 @@ func Run(conf *domain.Config, opts DiscordOptions) error { Route("repo", r.Wrapped(h.CreateGit, commonMw...)). Route("folder", r.Wrapped(h.CreateFolder, commonMw...)). Route("init_project", r.Wrapped(h.InitChannelAsProject, commonMw...)). - Route("coda_ticket", r.Wrapped(h.CreateCoda, commonMw...)) - - // Handle components - r. + Route("coda_ticket", r.Wrapped(h.CreateCoda, commonMw...)). + // and components Route("task_start", h.HandleTaskButtons). Route("task_close", h.HandleTaskButtons) diff --git a/client/discord/discord_handler/discord_handler.go b/client/discord/discord_handler/discord_handler.go index 0e45531..a94ebbf 100644 --- a/client/discord/discord_handler/discord_handler.go +++ b/client/discord/discord_handler/discord_handler.go @@ -5,7 +5,7 @@ import ( "fmt" "log" "ticket-pimp/adapters" - "ticket-pimp/client/discord/discord_router" + router "ticket-pimp/client/discord/discord_router" "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" "ticket-pimp/internal/helpers" @@ -32,7 +32,7 @@ func New( } } -func (h *Handler) RejectPM(f discord_router.HandlerFunc) discord_router.HandlerFunc { +func (h *Handler) RejectPM(f router.HandlerFunc) router.HandlerFunc { return func(s *discordgo.Session, i *discordgo.InteractionCreate) { dchan, err := s.Channel(i.ChannelID) @@ -75,7 +75,7 @@ func respondWithReject(s *discordgo.Session, i *discordgo.InteractionCreate) { } // Моментальный ответ для избежания столкновения с протуханием токена -func (h *Handler) WithInitialResponse(f discord_router.HandlerFunc) discord_router.HandlerFunc { +func (h *Handler) WithInitialResponse(f router.HandlerFunc) router.HandlerFunc { return func(s *discordgo.Session, i *discordgo.InteractionCreate) { initialResponse := discordgo.InteractionResponse{ From 0776156d33a130e85dcc28a004ea384f97bca245 Mon Sep 17 00:00:00 2001 From: naudachu Date: Mon, 27 Nov 2023 19:05:12 +0500 Subject: [PATCH 20/20] router simple groups --- client/discord/discord.go | 20 +++++---- client/discord/discord_router/router.go | 57 ++++++++++++++++++------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index 10895f2..d1dbbf4 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -228,16 +228,18 @@ func Run(conf *domain.Config, opts DiscordOptions) error { h.RejectPM, } - // Handle commands + r.Use(commonMw...). + Route("ping", h.Ping). + Route("project", h.CreateProject). + Route("info", h.ProjectInfo). + Route("repo", h.CreateGit). + Route("folder", h.CreateFolder). + Route("init_project", h.InitChannelAsProject). + Route("coda_ticket", h.CreateCoda) + + // and components r. - Route("ping", r.Wrapped(h.Ping, commonMw...)). - Route("project", r.Wrapped(h.CreateProject, commonMw...)). - Route("info", r.Wrapped(h.ProjectInfo, commonMw...)). - Route("repo", r.Wrapped(h.CreateGit, commonMw...)). - Route("folder", r.Wrapped(h.CreateFolder, commonMw...)). - Route("init_project", r.Wrapped(h.InitChannelAsProject, commonMw...)). - Route("coda_ticket", r.Wrapped(h.CreateCoda, commonMw...)). - // and components + /*Use().*/ // Combining into group duplicates replies Route("task_start", h.HandleTaskButtons). Route("task_close", h.HandleTaskButtons) diff --git a/client/discord/discord_router/router.go b/client/discord/discord_router/router.go index 6a1ad94..a6e1de3 100644 --- a/client/discord/discord_router/router.go +++ b/client/discord/discord_router/router.go @@ -27,6 +27,7 @@ func (re *RouteEntry) Match(i *discordgo.InteractionCreate) bool { type Router struct { session *discordgo.Session routes []RouteEntry + group []Group } func NewApp(s *discordgo.Session) *Router { @@ -52,23 +53,49 @@ func (r *Router) Serve(s *discordgo.Session, i *discordgo.InteractionCreate) { e.Handler(s, i) } } - //[ ] Is there something like 404?! + + for _, g := range r.group { + for _, e := range g.routes { + ok := e.Match(i) + if ok { + if len(g.middleware) < 1 { + e.Handler(s, i) + } + + wrapped := e.Handler + + // loop in reverse to preserve middleware order + for i := len(g.middleware) - 1; i >= 0; i-- { + wrapped = g.middleware[i](wrapped) + } + wrapped(s, i) + } + } + } } type Middleware func(HandlerFunc) HandlerFunc -func (r *Router) Wrapped(f HandlerFunc, m ...Middleware) HandlerFunc { - - if len(m) < 1 { - return f - } - - wrapped := f - - // loop in reverse to preserve middleware order - for i := len(m) - 1; i >= 0; i-- { - wrapped = m[i](wrapped) - } - - return wrapped +type Group struct { + routes []RouteEntry + middleware []Middleware +} + +func (r *Router) Use(m ...Middleware) *Group { + + r.group = append(r.group, Group{ + routes: []RouteEntry{}, + middleware: m, + }) + + return &r.group[len(r.group)-1] +} + +func (g *Group) Route(cmd string, handlerFunc HandlerFunc) *Group { + + g.routes = append(g.routes, RouteEntry{ + CommandName: cmd, + Handler: handlerFunc, + }) + return g }