diff --git a/Dockerfile b/Dockerfile index 9b75311..3002884 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags '-extldflags "-sta FROM scratch COPY --from=app-builder /go/bin/main /ticket-pimp COPY --from=app-builder /go/src/ticket-pimp/cmd/prod.env / +COPY --from=app-builder /go/src/ticket-pimp/internal/storage/migrate/* /internal/storage/migrate/ # the tls certificates: # NB: this pulls directly from the upstream image, which already has ca-certificates: COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ diff --git a/client/discord/discord.go b/client/discord/discord.go index 83960fb..5e2d06f 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -1,6 +1,7 @@ package discord import ( + "errors" "fmt" "log" "os" @@ -17,41 +18,95 @@ func initBotWith(token string) *discordgo.Session { if err != nil { log.Fatalf("unable to create discord session: %v", err) } + return discord } type DiscordOptions struct { - AppConfig *domain.Config + Config *domain.Config Controller *controller.WorkflowController } +func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) error { + 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 - session := initBotWith(token) + s := initBotWith(token) - router := handler.InitRouter(*opts.Controller, &conf.Discord) + router := handler.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} - for _, handler := range router.Routes { + for _, handler := range router.Commands { commandHandlers[handler.Command.Name] = handler.Handler } - session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { - h(s, i) + componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "task_start": router.Components[0].Handler, + "task_close": router.Components[0].Handler, + } + + s.AddHandler(router.ListenPosts) + + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := checkPrivateMessaging(s, i) + if err != nil { + return + } + + 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) + } } }) - if err := session.Open(); err != nil { + if err := s.Open(); err != nil { return fmt.Errorf("cannot open the session: %v", err) } + // UPDATE FORUM IF NEEDED: + + // forum, err := session.Channel(os.Getenv("TASKS_CHANNEL")) + forum, err := s.Channel(conf.Discord.IsProjectChannel) + if err != nil { + log.Print(err) + } + + _, err = s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ + AvailableTags: &router.Tags, + }) + if err != nil { + log.Fatal(err) + } + log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand var logString []string - for _, h := range router.Routes { - cmd, err := session.ApplicationCommandCreate(session.State.User.ID, "", &h.Command) + for _, h := range router.Commands { + cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "1103928338898235462", &h.Command) if err != nil { log.Panicf("Cannot create '%v' command: %v", h.Command.Name, err) } @@ -61,7 +116,8 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Println("Following commands added:") log.Println(logString) - defer session.Close() + + defer s.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop @@ -69,11 +125,10 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Println("Removing commands...") for _, h := range cmds { - err := session.ApplicationCommandDelete(session.State.User.ID, "", h.ID) + err := s.ApplicationCommandDelete(s.State.User.ID, "", h.ID) if err != nil { log.Panicf("Cannot delete '%v' command: %v", h.Name, err) } } - return nil } diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go new file mode 100644 index 0000000..b41dd5c --- /dev/null +++ b/client/discord/handler/handle_external_task.go @@ -0,0 +1,249 @@ +package handler + +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) { + + // 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 := 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"` +} + +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") + // [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/handler/handle_folder.go index f9fcc8c..ab7a6e7 100644 --- a/client/discord/handler/handle_folder.go +++ b/client/discord/handler/handle_folder.go @@ -8,11 +8,11 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) CreateFolderHandler(nameMinLenght int) route { +func (c *client) CreateFolderHandler(nameMinLenght int) Command { const ( nameOption string = "folder_name" ) - return route{ + return Command{ Command: discordgo.ApplicationCommand{ Name: "folder", @@ -28,61 +28,70 @@ func (h *router) CreateFolderHandler(nameMinLenght int) route { }, }, - Handler: func(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 = "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() + "Errors: " + resp.Message.Error() - } - - h.defaultFollowUp(result, s, i) - }, + Handler: c.createFolderHandler, } } + +func (c *client) 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 == 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/handler/handle_git.go b/client/discord/handler/handle_git.go index a9149c3..f41ff60 100644 --- a/client/discord/handler/handle_git.go +++ b/client/discord/handler/handle_git.go @@ -8,7 +8,7 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) CreateRepoHandler(repoNameMinLength int) route { +func (c *client) CreateRepoHandler(repoNameMinLength int) Command { const ( repoType = "repo_type" projectRepo = "project_repo" @@ -16,10 +16,10 @@ func (h *router) CreateRepoHandler(repoNameMinLength int) route { nameOption = "repo_name" ) - return route{ + return Command{ Command: discordgo.ApplicationCommand{ Name: "repo", - Description: "Command for repository creation", + Description: "Creates repository of selected type. Name used for projects channels only", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, @@ -46,67 +46,82 @@ func (h *router) CreateRepoHandler(repoNameMinLength int) route { }, }, }, - Handler: func(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 = "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 { - result = resp.Message.Error() - } else { - result = resp.Project.DiscordString() + "Errors: " + resp.Message.Error() - } - h.defaultFollowUp(result, s, i) - }, + 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" + ) + // Моментальный ответ для избежания столкновения с протуханием токена + 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 == 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/handler/handle_ping.go b/client/discord/handler/handle_ping.go index b44b88c..4f91795 100644 --- a/client/discord/handler/handle_ping.go +++ b/client/discord/handler/handle_ping.go @@ -6,25 +6,25 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) Ping() route { - return route{ +func (c *client) Ping() Command { + return Command{ Command: discordgo.ApplicationCommand{ Name: "ping", Description: "pongs in a reply", }, - Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { - log.Println("ok, I'm here..") - - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "`pong`", - Title: "Pong reply", - }, - }) - if err != nil { - log.Println(err) - } - }, + 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/handler/handle_ticket.go b/client/discord/handler/handle_ticket.go index 0e71c33..f904e5d 100644 --- a/client/discord/handler/handle_ticket.go +++ b/client/discord/handler/handle_ticket.go @@ -9,130 +9,138 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) GetInfo() route { - return route{ +func (c *client) GetInfo() Command { + return Command{ Command: discordgo.ApplicationCommand{ Name: "info", Description: "Get project's info", }, - Handler: func(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 { - result = project.DiscordString() - if err != nil { - result += "Errors: " + err.Error() - } - } - - } - - h.defaultFollowUp(result, s, i) - }, + Handler: c.getInfo, } } -func (h *router) InitProjectFromChannel(minLength int) route { - const ( - keyOption = "key" - ) - return route{ +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 + 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: keyOption, + Name: "key", Description: "Project's key from Coda.io", Required: true, MinLength: &minLength, }, }, }, - Handler: func(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[keyOption]; 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() + "Errors: " + errMsg.Error() - } - } - } - - h.defaultFollowUp(result, s, i) - }, + Handler: c.initProjectFromChannel, } } -func (h *router) CreateTicketHandler(repoNameMinLength int) route { - return route{ +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 + 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", @@ -146,65 +154,67 @@ func (h *router) CreateTicketHandler(repoNameMinLength int) route { }, }, }, - Handler: func(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 created, but unable to edit: %v\n", err) - - } else { - _, err = s.ChannelMessageSend(dchan.ID, "Hello!") - if err != nil { - log.Printf("message send problem: %v\n", err) - } - result = dchan.ID - } - } - } - } - - h.defaultFollowUp(result, s, i) - }, + Handler: c.createTicketHandler, } } + +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 + + // 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, + 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" + } + } + } + } + + c.defaultFollowUp(result, s, i) +} diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index 96398e4..cb64fc3 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -8,18 +8,25 @@ import ( "github.com/bwmarrin/discordgo" ) -type router struct { - Routes []route +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) *router { +func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tgConf *domain.TelegramConfig) *client { - var r router - r.Routes = append( - r.Routes, + var r client + r.controller = wc + r.conf = conf + + r.Commands = append(r.Commands, r.CreateRepoHandler(3), r.CreateFolderHandler(3), r.Ping(), @@ -27,18 +34,42 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *r r.InitProjectFromChannel(3), r.GetInfo(), ) - r.controller = wc - r.conf = conf + 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 } -type route struct { +// +// Подключение роутов к Discord боту + +type Command struct { Command discordgo.ApplicationCommand Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) } -func (h *router) defaultFollowUp(answer string, 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{ diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/handler/handle_farmtask.go index 87e5e35..128c120 100644 --- a/client/telegram/handler/handle_farmtask.go +++ b/client/telegram/handler/handle_farmtask.go @@ -2,8 +2,7 @@ package handler import ( "context" - "fmt" - "log" + "strconv" "strings" "ticket-pimp/internal/domain" @@ -13,9 +12,17 @@ import ( func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - msgID := mu.Message.ID + var ( + taskText string = "" + answer string = "" + ) - taskText := strings.TrimSpace(strings.Replace(mu.Text, "/task", "", 1)) + msgID := mu.Message.ID + if mu.Caption != "" { + taskText = strings.TrimSpace(strings.Replace(mu.Caption, "/task", "", 1)) + } else { + taskText = strings.TrimSpace(strings.Replace(mu.Text, "/task", "", 1)) + } var summaryTail string @@ -38,33 +45,47 @@ func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) er mu.Chat.ID.PeerID(), ) - id, err := h.coda.CreateTask(t.Summary, t.Description, t.Creator, t.CreatorLink) - if err != nil { - answer := errorAnswer(err.Error()) - h.LogMessage(ctx, mu, answer) - return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) - } - if id == "" { - answer := errorAnswer("task wasn't created") - h.LogMessage(ctx, mu, answer) - return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) - } + conv, err := h.controller.InitTask(t) - err = mu.Answer(fmt.Sprintf("Задача с id: %s была создана, жду ссылку", id)).DoVoid(ctx) - if err != nil { - log.Println("бот не смог ответить про создание задачи") - } - - url, err := h.coda.GetRowLink(id) if err != nil { answer := err.Error() h.LogMessage(ctx, mu, answer) return err } - t.URL = url - answer := tg.HTML.Text( - tg.HTML.Line(tg.HTML.Link("🤘 Задача", t.URL), "была создана!")) + i := strconv.Itoa(int(conv.ID)) + answer = tg.HTML.Text( + tg.HTML.Line( + tg.HTML.Bold("Task ID: "), + tg.HTML.Code(i), + tg.HTML.Text(" was created"), + ), + tg.HTML.Line( + "Заходи в наш", + tg.HTML.Link("discord", "https://discord.gg/RHdzK3kUr7"), + "чтобы отслеживать статус по задаче", + ), + ) + if mu.Caption != "" { + answer = tg.HTML.Text( + tg.HTML.Line( + tg.HTML.Bold("I'm unable to work with files, but"), + ), + tg.HTML.Line( + tg.HTML.Italic( + tg.HTML.Bold("Task ID: "), + tg.HTML.Code(i), + tg.HTML.Text(" was created")), + ), + + tg.HTML.Line( + "Заходи в наш", + tg.HTML.Link("discord", "https://discord.gg/RHdzK3kUr7"), + "чтобы отслеживать статус по задаче", + ), + ) + } + h.LogMessage(ctx, mu, answer) return mu.Answer(answer). ReplyToMessageID(msgID).ParseMode(tg.HTML).DisableWebPagePreview(true).DoVoid(ctx) diff --git a/client/telegram/handler/handler.go b/client/telegram/handler/handler.go index 7aa7f0e..da3a326 100644 --- a/client/telegram/handler/handler.go +++ b/client/telegram/handler/handler.go @@ -1,26 +1,30 @@ package handler import ( + "ticket-pimp/internal/controller" "ticket-pimp/internal/services" ) type Handler struct { - git services.IGit - cloud services.ICloud - coda services.ICoda - key string - id string + git services.IGit + cloud services.ICloud + coda services.ICoda + key string + id string + controller *controller.WorkflowController } func NewHandler( git services.IGit, cloud services.ICloud, coda services.ICoda, + controller *controller.WorkflowController, ) *Handler { return &Handler{ - git: git, - cloud: cloud, - coda: coda, + git: git, + cloud: cloud, + coda: coda, + controller: controller, } } diff --git a/client/telegram/handler/handler_test.go b/client/telegram/handler/handler_test.go deleted file mode 100644 index d871058..0000000 --- a/client/telegram/handler/handler_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package handler - -import ( - "testing" - "ticket-pimp/internal/domain" -) - -type test struct { - arg domain.Git - expected string -} - -var tests = []test{ - {domain.Git{ - Name: "text", - FullName: "", - Private: false, - Url: "", - CloneUrl: "", - HtmlUrl: "https://reddit.com/", - SshUrl: "", - }, "Repo text has been created!"}, -} - -func TestPrepareAnswer(t *testing.T) { - - for _, test := range tests { - g := newGit(&test.arg) - - if output := g.PrepareAnswer(); output != test.expected { - t.Errorf("Output %q not equal to expected %q", output, test.expected) - } - } -} diff --git a/client/telegram/telegram.go b/client/telegram/telegram.go index d756363..63fb7d8 100644 --- a/client/telegram/telegram.go +++ b/client/telegram/telegram.go @@ -4,21 +4,20 @@ import ( "context" "log" "ticket-pimp/client/telegram/handler" + "ticket-pimp/internal/controller" "ticket-pimp/internal/domain" "ticket-pimp/internal/services" "github.com/mr-linch/go-tg" "github.com/mr-linch/go-tg/tgb" - - tickets "ticket-pimp/internal/storage/db" ) type TelegramOptions struct { - TicketsRepo *tickets.Queries GitService *services.Git CloudService *services.Cloud Coda *services.Coda AppConfig *domain.Config + Controller *controller.WorkflowController } // runTgBot ... @@ -35,6 +34,7 @@ func Run(ctx context.Context, opts TelegramOptions) error { opts.GitService, opts.CloudService, opts.Coda, + opts.Controller, ) router := tgb.NewRouter(). diff --git a/cmd/main.go b/cmd/main.go index 5e3387b..42329fa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,11 +16,18 @@ import ( "ticket-pimp/client/telegram" "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/stdlib" + migrate "github.com/rubenv/sql-migrate" +) + +const ( + envfile = "prod.env" + migrationfile = "internal/storage/migrate" ) func main() { log.Print("started") - config := domain.InitConfig("develop.env") + config := domain.InitConfig(envfile) run(config) } @@ -29,17 +36,40 @@ func run(conf domain.Config) { 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, - fmt.Sprintf( - "postgresql://%s:%s@%s:%s/%s", - conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, - )) + connString) if err != nil { log.Fatalf("DB connection failed: %v", err) } // -- DB connection init -- END + // Aply migrations: + + dbConnConfig, err := pgxpool.ParseConfig(connString) + if err != nil { + log.Fatalf("unable to parse connString: %v", err) + } + + migrations := &migrate.FileMigrationSource{ + Dir: migrationfile, + } + + db := stdlib.OpenDB(*dbConnConfig.ConnConfig) + + const dialect = "postgres" + n, err := migrate.Exec(db, dialect, migrations, migrate.Up) + if err != nil { + log.Fatalf("unable to handle migrations: %v", err) + } + fmt.Printf("Applied %d migrations!\n", n) + + // + gitService := services.NewGit(conf.Git) cloudService := services.NewCloud(conf.Cloud) codaService := services.NewCodaClient(conf.Coda) @@ -55,7 +85,7 @@ func run(conf domain.Config) { go func() { opts := discord.DiscordOptions{ Controller: controller, - AppConfig: &conf, + Config: &conf, } if err := discord.Run(conf, opts); err != nil { log.Fatalf("discord bot cannot be runned: %v", err) @@ -63,11 +93,11 @@ func run(conf domain.Config) { }() opts := telegram.TelegramOptions{ - // TicketsRepo: db, GitService: gitService, CloudService: cloudService, Coda: codaService, AppConfig: &conf, + Controller: controller, } if err := telegram.Run(ctx, opts); err != nil { diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..abeda0f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,25 @@ +services: + ticket-pimp: + container_name: pimp + image: naudachu/ticket-pimp + ports: + - "8080:8080" + depends_on: + postgres: + condition: service_healthy + postgres: + container_name: db + image: "postgres:16.1-alpine3.18" + environment: + POSTGRES_DB: "tickets" + POSTGRES_USER: "postgres" + POSTGRES_PASSWORD: "postgres" + volumes: + - db:./postgres-data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-d", "tickets", "-U", "postgres" ] + interval: 10s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/go.mod b/go.mod index 6b32eba..c4183a4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect @@ -19,12 +20,15 @@ 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 diff --git a/go.sum b/go.sum index a42806f..683817c 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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-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= @@ -27,6 +29,8 @@ 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= @@ -38,6 +42,8 @@ github.com/mr-linch/go-tg v0.9.1/go.mod h1:276w69YW4pEo3ZYta+LQe4v/ut2w2h1ksP4zi github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +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/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= @@ -48,8 +54,11 @@ 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/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/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= @@ -105,3 +114,4 @@ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= diff --git a/internal/controller/control_git.go b/internal/controller/control_git.go index c9e1f0d..077a509 100644 --- a/internal/controller/control_git.go +++ b/internal/controller/control_git.go @@ -92,8 +92,13 @@ func (wc *WorkflowController) createGitForExistingProject(ctx context.Context, r } func (wc *WorkflowController) CreateGit(ctx context.Context, req GitRequest) *ProjectResponse { + if req.ChannelID == "" { + return &ProjectResponse{ + Project: nil, + Message: errors.New("empty channel string"), + } + } - // [ ] Валидация на пустой канал? p, err := wc.GetProjectByChannelID(ctx, req.ChannelID) if err != nil { return &ProjectResponse{ @@ -116,7 +121,6 @@ func (wc *WorkflowController) CreateGit(ctx context.Context, req GitRequest) *Pr Message: errors.New("build git already exists"), } } else { - // [x] return wc.createGitForExistingProject(ctx, req, p) } case p != nil && !req.IsBuildGit: @@ -126,7 +130,6 @@ func (wc *WorkflowController) CreateGit(ctx context.Context, req GitRequest) *Pr Message: errors.New("project git already exists"), } } else { - // [x] return wc.createGitForExistingProject(ctx, req, p) } default: diff --git a/internal/controller/control_project.go b/internal/controller/control_project.go index bc95ff6..d04cbb3 100644 --- a/internal/controller/control_project.go +++ b/internal/controller/control_project.go @@ -86,10 +86,10 @@ func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id stri return &proj, nil } +// Saves current channel as project's channel; func (wc *WorkflowController) InitProjectInChannel(ctx context.Context, channelID string, key string) (*domain.Project, error) { dbTicket, err := wc.q.GetTicketByChannelID(ctx, pgtype.Text{String: channelID, Valid: true}) if err == pgx.ErrNoRows { - // [ ] Логика инициализации проекта dbTicket, err = wc.q.CreateTicket( ctx, db.CreateTicketParams{ diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go new file mode 100644 index 0000000..b992ca7 --- /dev/null +++ b/internal/controller/control_task.go @@ -0,0 +1,153 @@ +package controller + +import ( + "context" + "fmt" + "os" + "ticket-pimp/internal/domain" + "ticket-pimp/internal/storage/db" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/jackc/pgx/v5/pgtype" +) + +// WriteTaskToDB +/* +Makes an SQL query to create new tasks row from domain.Task entity + - Creator field: telegram nickname or Discord's Mention(); + - Creator link (tg ID) in telegram case; + - Description from telegram/discord message bodies; +*/ +func (wc *WorkflowController) WriteTaskToDB(t *domain.Task) (*domain.Task, error) { + dbtask, err := wc.q.InsertTask(context.TODO(), db.InsertTaskParams{ + Creator: pgtype.Text{String: t.Creator, Valid: true}, + CreatorLink: pgtype.Text{ + String: t.CreatorLink, + Valid: true, + }, + Description: pgtype.Text{ + String: t.Description, + Valid: true, + }, + }) + if err != nil { + return nil, fmt.Errorf("unable to create task at the db: %v", err) + } + // ------------------------------------------------------------------------------------ + + task := newConvertable(&dbtask).ExtractDomain() + return task, nil +} + +// InitTask +/* +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; +*/ +func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { + + task, err := wc.WriteTaskToDB(t) + if err != nil { + 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) + if err != nil { + return task, fmt.Errorf("unable to create discord session: %v", err) + } + + if err := s.Open(); err != nil { + return task, fmt.Errorf("cannot open the session: %v", err) + } + + msg := discordgo.MessageSend{ + Content: task.DiscordMessage(domain.NewTaskState()), + 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", + }, + }, + }, + }, + } + + th, err := s.ForumThreadStartComplex( + forumChannelID, + &discordgo.ThreadStart{ + Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), + }, + &msg, + ) + if err != nil { + return task, fmt.Errorf("unable to update channel: %v", err) + } + + err = wc.UpdateTasksMessageID(context.TODO(), th.ID, task.ID) + if err != nil { + return task, fmt.Errorf("unable to set discord message to task: %v", err) + } + + return task, nil +} + +func (wc *WorkflowController) UpdateTasksMessageID(ctx context.Context, msgID string, taskID int32) error { + err := wc.q.UpdateTaskWithMessageID(context.TODO(), db.UpdateTaskWithMessageIDParams{ + Messageid: pgtype.Text{String: msgID, Valid: true}, + ID: taskID, + }) + return err +} + +func (wc *WorkflowController) UpdateTask(id string, opt int, user 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}, + }) + 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}, + }) + return &TaskConvertable{&dbtask}, err + } + + return &TaskConvertable{&dbtask}, nil +} diff --git a/internal/controller/control_workflow.go b/internal/controller/control_workflow.go index c24fdbb..d1fd033 100644 --- a/internal/controller/control_workflow.go +++ b/internal/controller/control_workflow.go @@ -12,6 +12,13 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +// FullProjectInit +/* + Deprecated method to create a project with all related data: + - git; + - git for the project's build; + - cloud folder; +*/ func (wc *WorkflowController) FullProjectInit(name, key, id string) (string, error) { appKey := fmt.Sprintf("%s-%s", key, id) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index c65d97d..ecea95d 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -5,6 +5,7 @@ import ( "ticket-pimp/internal/services" "ticket-pimp/internal/storage/db" + "github.com/bwmarrin/discordgo" "github.com/jackc/pgx/v5/pgxpool" ) @@ -14,6 +15,7 @@ type WorkflowController struct { ICoda services.ICoda pool *pgxpool.Pool q *db.Queries + ATags []discordgo.ForumTag } func NewWorkflowController( @@ -35,3 +37,28 @@ type ProjectResponse struct { Project *domain.Project Message error } + +type TaskConvertable struct { + *db.Task +} + +func newConvertable(db *db.Task) *TaskConvertable { + return &TaskConvertable{ + Task: db, + } +} + +func (t *TaskConvertable) ExtractDomain() *domain.Task { + return &domain.Task{ + ID: t.ID, + // Summary: "", + Description: t.Description.String, + Creator: t.Creator.String, + CreatorLink: t.CreatorLink.String, + Assignee: t.Assignee.String, + CreatedAt: t.CreatedAt.Time, + DeletedAt: t.DeletedAt.Time, + UpdatedAt: t.UpdatedAt.Time, + // URL: "", + } +} diff --git a/internal/domain/config.go b/internal/domain/config.go index fb02231..b2f891a 100644 --- a/internal/domain/config.go +++ b/internal/domain/config.go @@ -50,6 +50,7 @@ type TelegramConfig struct { type DiscordConfig struct { Token string IsProjectChannel string + IsTaskForum string } type ApplicationConfig struct { @@ -95,6 +96,7 @@ func InitConfig(envFilePath string) Config { Discord: DiscordConfig{ Token: os.Getenv("DISCORD_TOKEN"), IsProjectChannel: os.Getenv("PROJECTS_CHANNEL_GROUP"), + IsTaskForum: os.Getenv("TASKS_CHANNEL"), }, } } diff --git a/internal/domain/models.go b/internal/domain/models.go index c41f1bd..48ec870 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -1,6 +1,9 @@ package domain -import "fmt" +import ( + "fmt" + "time" +) type Folder struct { Title string // k @@ -51,14 +54,69 @@ func (r *Row) NewCell(col string, value string) *Row { } type Task struct { + ID int32 Summary string Description string Creator string CreatorLink string + Assignee string + + CreatedAt time.Time + DeletedAt time.Time + UpdatedAt time.Time + URL string } +type TaskState int + +const ( + new TaskState = iota + inprogress + done +) + +func NewTaskState() TaskState { + return TaskState(0) +} + +func InrpogressTaskState() TaskState { + return TaskState(1) +} + +func DoneTaskState() TaskState { + return TaskState(2) +} + +// Creates a string for discordgo.DiscordMessage.Content +// State: New task; +func (t *Task) DiscordMessage(ts TaskState) string { + switch ts { + case new: + return fmt.Sprintf( + "Created at: %s \n>>> %s\n", + t.CreatedAt, + t.Description, + ) + case inprogress: + return fmt.Sprintf( + "**TaskID: %d** Started by: %s\n🚀 Started at: %s\n", + t.ID, + t.Assignee, + t.UpdatedAt, + ) + case done: + return fmt.Sprintf( + "**TaskID: %d** Closed by: %s\n✅ Closed at: %s\n", + t.ID, + t.Assignee, + t.DeletedAt, + ) + } + return "task state not provided" +} + func NewTask(summ, desc, c, cLink string) *Task { return &Task{ Summary: summ, @@ -68,10 +126,6 @@ func NewTask(summ, desc, c, cLink string) *Task { } } -type ConversionLog struct { - Advertiser []string -} - type Git struct { Name string `json:"name"` // "poop" FullName string `json:"full_name"` // "developer/poop" @@ -83,69 +137,23 @@ type Git struct { } type Project struct { - ID string `json:"id"` - ShortName string `json:"shortName"` - Name string `json:"name"` - ChannelID string `json:"channel_id"` + ID string `json:"id"` //15 + ShortName string `json:"shortName"` //key-15 + Name string `json:"name"` //default project name + ChannelID string `json:"channel_id"` //123412341234 - ProjectGit string `json:"project_git"` - BuildGit string `json:"build_git"` - Cloud string `json:"cloud"` + 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 } 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", + "## 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, ) } - -type ProjectID struct { - ID string `json:"id"` -} - -type IssueCreateRequest struct { - ProjectID ProjectID `json:"project"` - Key string `json:"idReadable"` - ID string `json:"id"` - Summary string `json:"summary"` - Description string `json:"description"` -} - -// [ ] try `,omitempty` to remove extra struct; - -type IssueUpdateRequest struct { - IssueCreateRequest - CustomFields []CustomField `json:"customFields"` -} - -type CustomField struct { - Name string `json:"name"` - Type string `json:"$type"` - Value string `json:"value"` -} - -type ProjectsList struct { - Projects []Project -} - -// Find needed project.ID in the project's list -func (plist *ProjectsList) FindProjectByName(searchName string) (string, error) { - - projectID := "" - - for _, elem := range plist.Projects { - if elem.ShortName == searchName { - projectID = elem.ID - } - } - - if projectID == "" { - return "", fmt.Errorf("project %s doesn't exist", searchName) - } - return projectID, nil -} diff --git a/internal/storage/db/models.go b/internal/storage/db/models.go index a8f702f..6af8f66 100644 --- a/internal/storage/db/models.go +++ b/internal/storage/db/models.go @@ -13,6 +13,18 @@ type Appconfig struct { TicketID pgtype.Int4 } +type Task struct { + ID int32 + Creator pgtype.Text + CreatorLink pgtype.Text + Messageid pgtype.Text + Description pgtype.Text + Assignee pgtype.Text + CreatedAt pgtype.Timestamptz + DeletedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + type Ticket struct { ID int32 Key pgtype.Text diff --git a/internal/storage/db/queries.sql.go b/internal/storage/db/queries.sql.go index a425106..13685a7 100644 --- a/internal/storage/db/queries.sql.go +++ b/internal/storage/db/queries.sql.go @@ -11,6 +11,36 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const closeTask = `-- name: CloseTask :one +UPDATE tasks +SET deleted_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type CloseTaskParams struct { + DeletedAt pgtype.Timestamptz + Assignee pgtype.Text + Messageid pgtype.Text +} + +func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, closeTask, arg.DeletedAt, arg.Assignee, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + const createTicket = `-- name: CreateTicket :one INSERT INTO tickets ( key, channelID @@ -72,6 +102,48 @@ func (q *Queries) GetConfig(ctx context.Context) (Appconfig, error) { return i, err } +const getTaskByID = `-- name: GetTaskByID :one +SELECT id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at FROM tasks WHERE id = $1 +` + +func (q *Queries) GetTaskByID(ctx context.Context, id int32) (Task, error) { + row := q.db.QueryRow(ctx, getTaskByID, id) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getTaskByMessage = `-- name: GetTaskByMessage :one +SELECT id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at FROM tasks WHERE messageID = $1 +` + +func (q *Queries) GetTaskByMessage(ctx context.Context, messageid pgtype.Text) (Task, error) { + row := q.db.QueryRow(ctx, getTaskByMessage, messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + 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 ` @@ -114,6 +186,72 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) { return i, err } +const insertTask = `-- name: InsertTask :one +INSERT INTO tasks ( + creator, creator_link, description +) VALUES ( + $1, $2, $3 + ) + RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type InsertTaskParams struct { + Creator pgtype.Text + CreatorLink pgtype.Text + Description pgtype.Text +} + +func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, insertTask, arg.Creator, arg.CreatorLink, arg.Description) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listTasksByCreator = `-- name: ListTasksByCreator :many +SELECT id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at FROM tasks WHERE creator_link = $1 AND deleted_at is NULL +` + +func (q *Queries) ListTasksByCreator(ctx context.Context, creatorLink pgtype.Text) ([]Task, error) { + rows, err := q.db.Query(ctx, listTasksByCreator, creatorLink) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Task + for rows.Next() { + var i Task + if err := rows.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + 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 ` @@ -195,6 +333,52 @@ func (q *Queries) SetNewConfig(ctx context.Context) (Appconfig, error) { return i, err } +const startTask = `-- name: StartTask :one +UPDATE tasks +SET updated_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type StartTaskParams struct { + UpdatedAt pgtype.Timestamptz + Assignee pgtype.Text + Messageid pgtype.Text +} + +func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, startTask, arg.UpdatedAt, arg.Assignee, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateTaskWithMessageID = `-- name: UpdateTaskWithMessageID :exec +UPDATE tasks +SET messageID = $1 +WHERE id = $2 +` + +type UpdateTaskWithMessageIDParams struct { + Messageid pgtype.Text + ID int32 +} + +func (q *Queries) UpdateTaskWithMessageID(ctx context.Context, arg UpdateTaskWithMessageIDParams) error { + _, err := q.db.Exec(ctx, updateTaskWithMessageID, arg.Messageid, arg.ID) + return err +} + const updateTicketBuildGit = `-- name: UpdateTicketBuildGit :one UPDATE tickets SET build_git = $1, updated_at = $2 diff --git a/internal/storage/dbconfig.yml b/internal/storage/dbconfig.yml index 14a3c91..f38dded 100644 --- a/internal/storage/dbconfig.yml +++ b/internal/storage/dbconfig.yml @@ -1,4 +1,4 @@ development: dialect: postgres - datasource: host=localhost dbname=tickets user=postgres password=postgres sslmode=disable + datasource: host=postgres dbname=tickets user=postgres password=postgres sslmode=disable dir: migrate \ No newline at end of file diff --git a/internal/storage/migrate/0001_init_tickets.sql b/internal/storage/migrate/0001_init_tickets.sql deleted file mode 100644 index 5a67451..0000000 --- a/internal/storage/migrate/0001_init_tickets.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +migrate Up -CREATE TABLE tickets ( - id SERIAL PRIMARY KEY, - key VARCHAR(10), - channelID VARCHAR(255), - project_git VARCHAR(255), - build_git VARCHAR(255), - folder VARCHAR(255), - created_at TIMESTAMPTZ DEFAULT current_timestamp, - deleted_at TIMESTAMPTZ, - updated_at TIMESTAMPTZ -); - --- +migrate Down -DROP TABLE tickets; \ No newline at end of file diff --git a/internal/storage/migrate/0001_initial_migration.sql b/internal/storage/migrate/0001_initial_migration.sql new file mode 100644 index 0000000..fa83763 --- /dev/null +++ b/internal/storage/migrate/0001_initial_migration.sql @@ -0,0 +1,39 @@ +-- +migrate Up +CREATE TABLE appconfig ( + ticket_key VARCHAR(5), + ticket_id INT +); + +INSERT INTO appconfig (ticket_key, ticket_id) VALUES ('xpp', 1); + +CREATE TABLE tickets ( + id SERIAL PRIMARY KEY, + key VARCHAR(10), + channelID VARCHAR(255), + project_git VARCHAR(255), + build_git VARCHAR(255), + folder VARCHAR(255), + + created_at TIMESTAMPTZ DEFAULT current_timestamp, + deleted_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); + +CREATE TABLE tasks ( + id SERIAL PRIMARY KEY, + creator VARCHAR(255), + creator_link VARCHAR(255), + messageID VARCHAR(255), + + description TEXT, + assignee VARCHAR(255), + + created_at TIMESTAMPTZ DEFAULT current_timestamp, + deleted_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); + +-- +migrate Down +DROP TABLE tickets; +DROP TABLE appconfig; +DROP TABLE tasks; \ No newline at end of file diff --git a/internal/storage/migrate/0002_init_config.sql b/internal/storage/migrate/0002_init_config.sql deleted file mode 100644 index f8dc281..0000000 --- a/internal/storage/migrate/0002_init_config.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +migrate Up -CREATE TABLE appconfig ( - ticket_key VARCHAR(5), - ticket_id INT -); - --- +migrate Up -INSERT INTO appconfig (ticket_key, ticket_id) VALUES ('DAP', 100); - --- +migrate Down -DROP TABLE appconfig; \ No newline at end of file diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql index d5320ee..cae5e62 100644 --- a/internal/storage/sqlc/queries.sql +++ b/internal/storage/sqlc/queries.sql @@ -52,4 +52,38 @@ UPDATE tickets SET project_git = $1, build_git = $2, folder = $3 WHERE id = $4; UPDATE tickets SET deleted_at = current_timestamp WHERE id = $1; -- name: DeleteTicketByKey :exec -UPDATE tickets SET deleted_at = current_timestamp WHERE key = $1; \ No newline at end of file +UPDATE tickets SET deleted_at = current_timestamp WHERE key = $1; + +-- name: InsertTask :one +INSERT INTO tasks ( + creator, creator_link, description +) VALUES ( + $1, $2, $3 + ) + RETURNING *; + +-- name: UpdateTaskWithMessageID :exec +UPDATE tasks +SET messageID = $1 +WHERE id = $2; + +-- name: StartTask :one +UPDATE tasks +SET updated_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING *; + +-- name: CloseTask :one +UPDATE tasks +SET deleted_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING *; + +-- name: GetTaskByMessage :one +SELECT * FROM tasks WHERE messageID = $1; + +-- name: ListTasksByCreator :many +SELECT * FROM tasks WHERE creator_link = $1 AND deleted_at is NULL; + +-- name: GetTaskByID :one +SELECT * FROM tasks WHERE id = $1; \ No newline at end of file