From da119ab4d5a2f69aeaabbb66439375d835973326 Mon Sep 17 00:00:00 2001 From: naudachu Date: Mon, 13 Nov 2023 19:10:56 +0500 Subject: [PATCH 01/12] - initial commit --- client/discord/discord.go | 20 +++- .../discord/handler/handle_external_task.go | 92 +++++++++++++++++++ client/discord/handler/handle_folder.go | 9 +- client/discord/handler/handle_git.go | 4 +- client/discord/handler/handle_ping.go | 4 +- client/discord/handler/handle_ticket.go | 17 ++-- client/discord/handler/handler.go | 30 ++++-- internal/controller/control_task.go | 79 ++++++++++++++++ internal/domain/models.go | 66 ++----------- internal/storage/db/models.go | 12 +++ internal/storage/db/queries.sql.go | 84 +++++++++++++++++ internal/storage/migrate/0002_init_config.sql | 2 +- internal/storage/migrate/003_init_tasks.sql | 17 ++++ internal/storage/sqlc/queries.sql | 22 ++++- 14 files changed, 370 insertions(+), 88 deletions(-) create mode 100644 client/discord/handler/handle_external_task.go create mode 100644 internal/controller/control_task.go create mode 100644 internal/storage/migrate/003_init_tasks.sql diff --git a/client/discord/discord.go b/client/discord/discord.go index 83960fb..c009d6e 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -33,13 +33,25 @@ func Run(conf domain.Config, opts DiscordOptions) error { router := handler.InitRouter(*opts.Controller, &conf.Discord) 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 } + componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ + "task_start": router.Components[0].Handler, + "task_close": router.Components[1].Handler, + } + session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { - if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { - h(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) + } } }) @@ -50,7 +62,7 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand var logString []string - for _, h := range router.Routes { + for _, h := range router.Commands { cmd, err := session.ApplicationCommandCreate(session.State.User.ID, "", &h.Command) if err != nil { log.Panicf("Cannot create '%v' command: %v", h.Command.Name, err) diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go new file mode 100644 index 0000000..fd65c84 --- /dev/null +++ b/client/discord/handler/handle_external_task.go @@ -0,0 +1,92 @@ +package handler + +import ( + "log" + + "github.com/bwmarrin/discordgo" +) + +func (h *router) CreateExternalTask() CommandRoute { + return CommandRoute{ + + Command: discordgo.ApplicationCommand{ + Name: "test", + Description: "Buttons test", + }, + + 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) + + go func() { + h.controller.InitTask("something like a default description") + }() + }, + } +} + +func (h *router) StartTask() ComponentRoute { + return ComponentRoute{ + Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { + + h.controller.UpdateTask(i.Message.ID, 0) + + newMsg := 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: false, + CustomID: "task_close", + }, + }, + }, + }, + } + + editedM, err := s.ChannelMessageEditComplex(&newMsg) + if err != nil { + log.Println("edition NOT complete, ", err) + } + + log.Print(editedM) + + }, + } +} + +func (h *router) CloseTask() ComponentRoute { + return ComponentRoute{ + Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { + + h.controller.UpdateTask(i.Message.ID, 1) + + newMsg := discordgo.MessageEdit{ + Channel: i.ChannelID, + ID: i.Message.ID, + Components: []discordgo.MessageComponent{}, + } + + editedM, err := s.ChannelMessageEditComplex(&newMsg) + if err != nil { + log.Println("edition NOT complete, ", err) + } + + log.Print(editedM) + + }, + } +} diff --git a/client/discord/handler/handle_folder.go b/client/discord/handler/handle_folder.go index f9fcc8c..82d860e 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 (h *router) CreateFolderHandler(nameMinLenght int) CommandRoute { const ( nameOption string = "folder_name" ) - return route{ + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "folder", @@ -79,7 +79,10 @@ func (h *router) CreateFolderHandler(nameMinLenght int) route { if resp.Project == nil { result = "Надо написать имя для папки, или создать папку из проекта!" } else { - result = resp.Project.DiscordString() + "Errors: " + resp.Message.Error() + result = resp.Project.DiscordString() + if resp.Message != nil { + result += "Errors: " + resp.Message.Error() + } } h.defaultFollowUp(result, s, i) diff --git a/client/discord/handler/handle_git.go b/client/discord/handler/handle_git.go index a9149c3..6a62152 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 (h *router) CreateRepoHandler(repoNameMinLength int) CommandRoute { const ( repoType = "repo_type" projectRepo = "project_repo" @@ -16,7 +16,7 @@ func (h *router) CreateRepoHandler(repoNameMinLength int) route { nameOption = "repo_name" ) - return route{ + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "repo", Description: "Command for repository creation", diff --git a/client/discord/handler/handle_ping.go b/client/discord/handler/handle_ping.go index b44b88c..833d11c 100644 --- a/client/discord/handler/handle_ping.go +++ b/client/discord/handler/handle_ping.go @@ -6,8 +6,8 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) Ping() route { - return route{ +func (h *router) Ping() CommandRoute { + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "ping", Description: "pongs in a reply", diff --git a/client/discord/handler/handle_ticket.go b/client/discord/handler/handle_ticket.go index 0e71c33..2e7f790 100644 --- a/client/discord/handler/handle_ticket.go +++ b/client/discord/handler/handle_ticket.go @@ -9,8 +9,8 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) GetInfo() route { - return route{ +func (h *router) GetInfo() CommandRoute { + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "info", Description: "Get project's info", @@ -52,11 +52,11 @@ func (h *router) GetInfo() route { } } -func (h *router) InitProjectFromChannel(minLength int) route { +func (h *router) InitProjectFromChannel(minLength int) CommandRoute { const ( keyOption = "key" ) - return route{ + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "init_project", Description: "Connect project with Coda ID", @@ -121,7 +121,10 @@ func (h *router) InitProjectFromChannel(minLength int) route { if err != nil { result = fmt.Sprintf("unable to init project: %v", err) } else { - result = project.DiscordString() + "Errors: " + errMsg.Error() + result = project.DiscordString() + if errMsg != nil { + result += "Errors: " + errMsg.Error() + } } } } @@ -131,8 +134,8 @@ func (h *router) InitProjectFromChannel(minLength int) route { } } -func (h *router) CreateTicketHandler(repoNameMinLength int) route { - return route{ +func (h *router) CreateTicketHandler(repoNameMinLength int) CommandRoute { + return CommandRoute{ Command: discordgo.ApplicationCommand{ Name: "project", Description: "Create new development ticket", diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index 96398e4..d6ddaf9 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -9,7 +9,8 @@ import ( ) type router struct { - Routes []route + Commands []CommandRoute + Components []ComponentRoute controller controller.WorkflowController conf *domain.DiscordConfig } @@ -18,14 +19,18 @@ type router struct { func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *router { var r router - r.Routes = append( - r.Routes, - r.CreateRepoHandler(3), - r.CreateFolderHandler(3), - r.Ping(), - r.CreateTicketHandler(3), - r.InitProjectFromChannel(3), - r.GetInfo(), + r.Commands = append(r.Commands, + // r.CreateRepoHandler(3), + // r.CreateFolderHandler(3), + // r.Ping(), + // r.CreateTicketHandler(3), + // r.InitProjectFromChannel(3), + // r.GetInfo(), + r.CreateExternalTask(), + ) + r.Components = append(r.Components, + r.StartTask(), + r.CloseTask(), ) r.controller = wc r.conf = conf @@ -33,11 +38,16 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *r return &r } -type route struct { +type CommandRoute struct { Command discordgo.ApplicationCommand Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) } +type ComponentRoute struct { + Component discordgo.MessageComponent + Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) +} + func (h *router) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { // Sending result: diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go new file mode 100644 index 0000000..a6ef764 --- /dev/null +++ b/internal/controller/control_task.go @@ -0,0 +1,79 @@ +package controller + +import ( + "context" + "log" + "os" + "ticket-pimp/internal/storage/db" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/jackc/pgx/v5/pgtype" +) + +func (wc *WorkflowController) InitTask(description string) { + var ( + token = os.Getenv("DISCORD_TOKEN") + channel = os.Getenv("TASKS_CHANNEL") + ) + discord, err := discordgo.New("Bot " + token) + if err != nil { + log.Fatalf("unable to create discord session: %v", err) + // [ ] Что делать, если не получилось создать задачу? + } + + if err := discord.Open(); err != nil { + // [ ] Что делать, если не получилось создать задачу? + log.Printf("cannot open the session: %v", err) + } + + msg := discordgo.MessageSend{ + Content: description, + 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", + }, + }, + }, + }, + } + + st, err := discord.ChannelMessageSendComplex(channel, &msg) + if err != nil { + log.Println("unable to send task message") + } + + t, err := wc.q.InsertTask(context.TODO(), pgtype.Text{String: st.ID, Valid: true}) + if err != nil { + log.Println("unable to insert task") + } + + _ = t + +} + +func (wc *WorkflowController) UpdateTask(id string, opt int) { + switch opt { + case 0: + wc.q.StartTask(context.TODO(), db.StartTaskParams{ + StartedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + Messageid: pgtype.Text{String: id, Valid: true}, + }) + case 1: + wc.q.CloseTask(context.TODO(), db.CloseTaskParams{ + ClosedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + Messageid: pgtype.Text{String: id, Valid: true}, + }) + } +} diff --git a/internal/domain/models.go b/internal/domain/models.go index c41f1bd..fa222a8 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -68,10 +68,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 +79,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..06edc5e 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 + StartedAt pgtype.Timestamptz + ClosedAt pgtype.Timestamptz + 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..b72b3be 100644 --- a/internal/storage/db/queries.sql.go +++ b/internal/storage/db/queries.sql.go @@ -11,6 +11,35 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const closeTask = `-- name: CloseTask :one +UPDATE tasks +SET closed_at = $1 +WHERE messageID = $2 +RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at +` + +type CloseTaskParams struct { + ClosedAt pgtype.Timestamptz + Messageid pgtype.Text +} + +func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, closeTask, arg.ClosedAt, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.StartedAt, + &i.ClosedAt, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + const createTicket = `-- name: CreateTicket :one INSERT INTO tickets ( key, channelID @@ -114,6 +143,32 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) { return i, err } +const insertTask = `-- name: InsertTask :one +INSERT INTO tasks ( + messageID +) VALUES ( + $1 + ) + RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at +` + +func (q *Queries) InsertTask(ctx context.Context, messageid pgtype.Text) (Task, error) { + row := q.db.QueryRow(ctx, insertTask, messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.StartedAt, + &i.ClosedAt, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + 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 +250,35 @@ func (q *Queries) SetNewConfig(ctx context.Context) (Appconfig, error) { return i, err } +const startTask = `-- name: StartTask :one +UPDATE tasks +SET started_at = $1 +WHERE messageID = $2 +RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at +` + +type StartTaskParams struct { + StartedAt pgtype.Timestamptz + Messageid pgtype.Text +} + +func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, startTask, arg.StartedAt, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.StartedAt, + &i.ClosedAt, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + const updateTicketBuildGit = `-- name: UpdateTicketBuildGit :one UPDATE tickets SET build_git = $1, updated_at = $2 diff --git a/internal/storage/migrate/0002_init_config.sql b/internal/storage/migrate/0002_init_config.sql index f8dc281..b5fd33e 100644 --- a/internal/storage/migrate/0002_init_config.sql +++ b/internal/storage/migrate/0002_init_config.sql @@ -5,7 +5,7 @@ CREATE TABLE appconfig ( ); -- +migrate Up -INSERT INTO appconfig (ticket_key, ticket_id) VALUES ('DAP', 100); +INSERT INTO appconfig (ticket_key, ticket_id) VALUES ('P', 100); -- +migrate Down DROP TABLE appconfig; \ No newline at end of file diff --git a/internal/storage/migrate/003_init_tasks.sql b/internal/storage/migrate/003_init_tasks.sql new file mode 100644 index 0000000..c47f75b --- /dev/null +++ b/internal/storage/migrate/003_init_tasks.sql @@ -0,0 +1,17 @@ +-- +migrate Up +CREATE TABLE tasks ( + id SERIAL PRIMARY KEY, + creator VARCHAR(255), + creator_link VARCHAR(255), + messageID VARCHAR(255), + + started_at TIMESTAMPTZ, + closed_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ DEFAULT current_timestamp, + deleted_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +); + +-- +migrate Down +DROP TABLE tasks; \ No newline at end of file diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql index d5320ee..11f57a1 100644 --- a/internal/storage/sqlc/queries.sql +++ b/internal/storage/sqlc/queries.sql @@ -52,4 +52,24 @@ 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 ( + messageID +) VALUES ( + $1 + ) + RETURNING *; + +-- name: StartTask :one +UPDATE tasks +SET started_at = $1 +WHERE messageID = $2 +RETURNING *; + +-- name: CloseTask :one +UPDATE tasks +SET closed_at = $1 +WHERE messageID = $2 +RETURNING *; \ No newline at end of file From 77b8bd8abb5f53f276b41424b9a8c9294488d8df Mon Sep 17 00:00:00 2001 From: naudachu Date: Tue, 14 Nov 2023 12:43:42 +0500 Subject: [PATCH 02/12] - farm task handler; --- .../discord/handler/handle_external_task.go | 6 +-- client/telegram/handler/handle_farmtask.go | 47 +++++++++++-------- client/telegram/handler/handler.go | 20 ++++---- client/telegram/telegram.go | 6 +-- cmd/main.go | 2 +- internal/controller/control_task.go | 38 +++++++++++---- internal/storage/db/models.go | 1 + internal/storage/db/queries.sql.go | 23 ++++++--- internal/storage/migrate/003_init_tasks.sql | 3 ++ internal/storage/sqlc/queries.sql | 4 +- 10 files changed, 98 insertions(+), 52 deletions(-) diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go index fd65c84..6210143 100644 --- a/client/discord/handler/handle_external_task.go +++ b/client/discord/handler/handle_external_task.go @@ -27,9 +27,9 @@ func (h *router) CreateExternalTask() CommandRoute { s.InteractionRespond(i.Interaction, &initialResponse) - go func() { - h.controller.InitTask("something like a default description") - }() + // go func() { + // h.controller.InitTask("something like a default description") + // }() }, } } diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/handler/handle_farmtask.go index 87e5e35..1c55a21 100644 --- a/client/telegram/handler/handle_farmtask.go +++ b/client/telegram/handler/handle_farmtask.go @@ -2,8 +2,6 @@ package handler import ( "context" - "fmt" - "log" "strings" "ticket-pimp/internal/domain" @@ -38,33 +36,42 @@ 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) - } + err := h.controller.InitTask(t) - err = mu.Answer(fmt.Sprintf("Задача с id: %s была создана, жду ссылку", id)).DoVoid(ctx) - if err != nil { - log.Println("бот не смог ответить про создание задачи") - } + // Coda.io was deprecated! - url, err := h.coda.GetRowLink(id) + // 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) + // } + + // 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 + // t.URL = url + // answer := tg.HTML.Text( + // tg.HTML.Line(tg.HTML.Link("🤘 Задача", t.URL), "была создана!")) + // h.LogMessage(ctx, mu, answer) + // return mu.Answer(answer). + // ReplyToMessageID(msgID).ParseMode(tg.HTML).DisableWebPagePreview(true).DoVoid(ctx) answer := tg.HTML.Text( - tg.HTML.Line(tg.HTML.Link("🤘 Задача", t.URL), "была создана!")) + tg.HTML.Line("🤘 Задача была создана!")) 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/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..0ae0db8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -63,11 +63,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/internal/controller/control_task.go b/internal/controller/control_task.go index a6ef764..305bc2b 100644 --- a/internal/controller/control_task.go +++ b/internal/controller/control_task.go @@ -2,8 +2,10 @@ package controller import ( "context" + "fmt" "log" "os" + "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" "time" @@ -11,7 +13,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (wc *WorkflowController) InitTask(description string) { +func (wc *WorkflowController) InitTask(t *domain.Task) error { var ( token = os.Getenv("DISCORD_TOKEN") channel = os.Getenv("TASKS_CHANNEL") @@ -27,8 +29,31 @@ func (wc *WorkflowController) InitTask(description string) { log.Printf("cannot open the session: %v", err) } + // dbtask, err := wc.q.InsertTask(context.TODO(), pgtype.Text{String: st.ID, Valid: true}) + 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 { + log.Println("unable to insert task") + } + + content := fmt.Sprintf( + "## TaskID: %d\nCreated by: %s\n\n%s", + dbtask.ID, + t.Creator, + t.Description, + ) + msg := discordgo.MessageSend{ - Content: description, + Content: content, Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ @@ -54,12 +79,9 @@ func (wc *WorkflowController) InitTask(description string) { log.Println("unable to send task message") } - t, err := wc.q.InsertTask(context.TODO(), pgtype.Text{String: st.ID, Valid: true}) - if err != nil { - log.Println("unable to insert task") - } - - _ = t + _ = dbtask + _ = st + return err } diff --git a/internal/storage/db/models.go b/internal/storage/db/models.go index 06edc5e..290ff56 100644 --- a/internal/storage/db/models.go +++ b/internal/storage/db/models.go @@ -18,6 +18,7 @@ type Task struct { Creator pgtype.Text CreatorLink pgtype.Text Messageid pgtype.Text + Description pgtype.Text StartedAt pgtype.Timestamptz ClosedAt pgtype.Timestamptz CreatedAt pgtype.Timestamptz diff --git a/internal/storage/db/queries.sql.go b/internal/storage/db/queries.sql.go index b72b3be..eaabec2 100644 --- a/internal/storage/db/queries.sql.go +++ b/internal/storage/db/queries.sql.go @@ -15,7 +15,7 @@ const closeTask = `-- name: CloseTask :one UPDATE tasks SET closed_at = $1 WHERE messageID = $2 -RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at +RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at ` type CloseTaskParams struct { @@ -31,6 +31,7 @@ func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, err &i.Creator, &i.CreatorLink, &i.Messageid, + &i.Description, &i.StartedAt, &i.ClosedAt, &i.CreatedAt, @@ -145,21 +146,28 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) { const insertTask = `-- name: InsertTask :one INSERT INTO tasks ( - messageID + creator, creator_link, description ) VALUES ( - $1 + $1, $2, $3 ) - RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at + RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at ` -func (q *Queries) InsertTask(ctx context.Context, messageid pgtype.Text) (Task, error) { - row := q.db.QueryRow(ctx, insertTask, messageid) +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.StartedAt, &i.ClosedAt, &i.CreatedAt, @@ -254,7 +262,7 @@ const startTask = `-- name: StartTask :one UPDATE tasks SET started_at = $1 WHERE messageID = $2 -RETURNING id, creator, creator_link, messageid, started_at, closed_at, created_at, deleted_at, updated_at +RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at ` type StartTaskParams struct { @@ -270,6 +278,7 @@ func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, err &i.Creator, &i.CreatorLink, &i.Messageid, + &i.Description, &i.StartedAt, &i.ClosedAt, &i.CreatedAt, diff --git a/internal/storage/migrate/003_init_tasks.sql b/internal/storage/migrate/003_init_tasks.sql index c47f75b..dd7323f 100644 --- a/internal/storage/migrate/003_init_tasks.sql +++ b/internal/storage/migrate/003_init_tasks.sql @@ -5,6 +5,9 @@ CREATE TABLE tasks ( creator_link VARCHAR(255), messageID VARCHAR(255), + description TEXT, + + started_at TIMESTAMPTZ, closed_at TIMESTAMPTZ, diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql index 11f57a1..c0c6a28 100644 --- a/internal/storage/sqlc/queries.sql +++ b/internal/storage/sqlc/queries.sql @@ -56,9 +56,9 @@ UPDATE tickets SET deleted_at = current_timestamp WHERE key = $1; -- name: InsertTask :one INSERT INTO tasks ( - messageID + creator, creator_link, description ) VALUES ( - $1 + $1, $2, $3 ) RETURNING *; From 0df0b21198680ddc19a3b3746243e39e5f263137 Mon Sep 17 00:00:00 2001 From: naudachu Date: Tue, 14 Nov 2023 20:26:10 +0500 Subject: [PATCH 03/12] - Handle discordgo.ForumTags; --- client/discord/discord.go | 18 ++- .../discord/handler/handle_external_task.go | 62 +++++++-- client/discord/handler/handle_ping.go | 12 +- client/discord/handler/handler.go | 17 ++- client/telegram/handler/handle_farmtask.go | 64 ++++----- internal/controller/control_task.go | 90 ++++++++----- internal/controller/controller.go | 27 ++++ internal/domain/models.go | 46 ++++++- internal/storage/db/models.go | 3 +- internal/storage/db/queries.sql.go | 125 +++++++++++++++--- ...003_init_tasks.sql => 0003_init_tasks.sql} | 5 +- internal/storage/sqlc/queries.sql | 24 +++- 12 files changed, 386 insertions(+), 107 deletions(-) rename internal/storage/migrate/{003_init_tasks.sql => 0003_init_tasks.sql} (80%) diff --git a/client/discord/discord.go b/client/discord/discord.go index c009d6e..1109b3e 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -59,6 +59,22 @@ func Run(conf domain.Config, opts DiscordOptions) error { return fmt.Errorf("cannot open the session: %v", err) } + // UPDATE FORUM IF NEEDED: + + forum, err := session.Channel(os.Getenv("TASKS_CHANNEL")) + if err != nil { + log.Print(err) + } + + forum, err = session.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ + AvailableTags: &router.Tags, + }) + if err != nil { + log.Fatal(err) + } else { + + } + log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand var logString []string @@ -73,6 +89,7 @@ func Run(conf domain.Config, opts DiscordOptions) error { log.Println("Following commands added:") log.Println(logString) + defer session.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) @@ -86,6 +103,5 @@ func Run(conf domain.Config, opts DiscordOptions) error { 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 index 6210143..61a1c99 100644 --- a/client/discord/handler/handle_external_task.go +++ b/client/discord/handler/handle_external_task.go @@ -6,6 +6,34 @@ import ( "github.com/bwmarrin/discordgo" ) +func (h *router) 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 { + log.Print(tag.Name) + thE, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ + AppliedTags: &[]string{some.ID}, + }) + _, _ = thE, err + } + } + } + + return nil +} + func (h *router) CreateExternalTask() CommandRoute { return CommandRoute{ @@ -38,9 +66,20 @@ func (h *router) StartTask() ComponentRoute { return ComponentRoute{ Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { - h.controller.UpdateTask(i.Message.ID, 0) + user := i.Member.User.Mention() + + convertable, err := h.controller.UpdateTask(i.Message.ID, 0, user) + if err != nil { + + } + + newContent := convertable.ExtractDomain().StartedMessage() + if err != nil { + newContent += "\n `In progress` action produced with error:" + err.Error() + } newMsg := discordgo.MessageEdit{ + Content: &newContent, Channel: i.ChannelID, ID: i.Message.ID, Components: []discordgo.MessageComponent{ @@ -57,13 +96,12 @@ func (h *router) StartTask() ComponentRoute { }, } - editedM, err := s.ChannelMessageEditComplex(&newMsg) + h.setFlag(s, i, &h.Tags[0]) + + _, err = s.ChannelMessageEditComplex(&newMsg) if err != nil { log.Println("edition NOT complete, ", err) } - - log.Print(editedM) - }, } } @@ -72,20 +110,28 @@ func (h *router) CloseTask() ComponentRoute { return ComponentRoute{ Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { - h.controller.UpdateTask(i.Message.ID, 1) + user := i.Member.User.Mention() + convertable, err := h.controller.UpdateTask(i.Message.ID, 1, user) + + newContent := convertable.ExtractDomain().ClosedMessage() + if err != nil { + newContent += "\n `Close` action produced with error:" + err.Error() + } newMsg := discordgo.MessageEdit{ + Content: &newContent, Channel: i.ChannelID, ID: i.Message.ID, Components: []discordgo.MessageComponent{}, } - editedM, err := s.ChannelMessageEditComplex(&newMsg) + msgE, err := s.ChannelMessageEditComplex(&newMsg) if err != nil { log.Println("edition NOT complete, ", err) } - log.Print(editedM) + _ = msgE + h.setFlag(s, i, &h.Tags[1]) }, } diff --git a/client/discord/handler/handle_ping.go b/client/discord/handler/handle_ping.go index 833d11c..1faa7d4 100644 --- a/client/discord/handler/handle_ping.go +++ b/client/discord/handler/handle_ping.go @@ -13,13 +13,19 @@ func (h *router) Ping() CommandRoute { 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", + TTS: false, + Content: "Pong to: " + i.Member.User.Mention(), + Components: []discordgo.MessageComponent{}, + Embeds: []*discordgo.MessageEmbed{}, + Files: []*discordgo.File{}, + Flags: 0, + Choices: []*discordgo.ApplicationCommandOptionChoice{}, + CustomID: "", + Title: "", }, }) if err != nil { diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index d6ddaf9..d6b2266 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -11,6 +11,8 @@ import ( type router struct { Commands []CommandRoute Components []ComponentRoute + Tags []discordgo.ForumTag + controller controller.WorkflowController conf *domain.DiscordConfig } @@ -22,7 +24,7 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *r r.Commands = append(r.Commands, // r.CreateRepoHandler(3), // r.CreateFolderHandler(3), - // r.Ping(), + r.Ping(), // r.CreateTicketHandler(3), // r.InitProjectFromChannel(3), // r.GetInfo(), @@ -34,6 +36,19 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *r ) r.controller = wc r.conf = conf + r.Tags = append( + r.Tags, + discordgo.ForumTag{ + Name: "В работе", + Moderated: true, + EmojiName: "👩‍🍳", + }, + + discordgo.ForumTag{ + Name: "Готово", + Moderated: true, + EmojiName: "✅", + }) return &r } diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/handler/handle_farmtask.go index 1c55a21..8274ec9 100644 --- a/client/telegram/handler/handle_farmtask.go +++ b/client/telegram/handler/handle_farmtask.go @@ -2,6 +2,7 @@ package handler import ( "context" + "strconv" "strings" "ticket-pimp/internal/domain" @@ -11,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 @@ -36,42 +45,35 @@ func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) er mu.Chat.ID.PeerID(), ) - err := h.controller.InitTask(t) + conv, err := h.controller.InitTask(t) - // Coda.io was deprecated! - - // 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) - // } - - // 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), "была создана!")) - // h.LogMessage(ctx, mu, answer) - // return mu.Answer(answer). - // ReplyToMessageID(msgID).ParseMode(tg.HTML).DisableWebPagePreview(true).DoVoid(ctx) - answer := tg.HTML.Text( - tg.HTML.Line("🤘 Задача была создана!")) + 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"), + ), + ) + 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.Bold("Task ID: "), + tg.HTML.Code(i), + tg.HTML.Text(" was created"), + ), + ) + } + h.LogMessage(ctx, mu, answer) return mu.Answer(answer). ReplyToMessageID(msgID).ParseMode(tg.HTML).DisableWebPagePreview(true).DoVoid(ctx) diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go index 305bc2b..032537d 100644 --- a/internal/controller/control_task.go +++ b/internal/controller/control_task.go @@ -3,8 +3,8 @@ package controller import ( "context" "fmt" - "log" "os" + "strconv" "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" "time" @@ -13,23 +13,9 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (wc *WorkflowController) InitTask(t *domain.Task) error { - var ( - token = os.Getenv("DISCORD_TOKEN") - channel = os.Getenv("TASKS_CHANNEL") - ) - discord, err := discordgo.New("Bot " + token) - if err != nil { - log.Fatalf("unable to create discord session: %v", err) - // [ ] Что делать, если не получилось создать задачу? - } +func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { - if err := discord.Open(); err != nil { - // [ ] Что делать, если не получилось создать задачу? - log.Printf("cannot open the session: %v", err) - } - - // dbtask, err := wc.q.InsertTask(context.TODO(), pgtype.Text{String: st.ID, Valid: true}) + // Записываем в базу созданную задачу ------------------------------------------ dbtask, err := wc.q.InsertTask(context.TODO(), db.InsertTaskParams{ Creator: pgtype.Text{String: t.Creator, Valid: true}, CreatorLink: pgtype.Text{ @@ -42,18 +28,32 @@ func (wc *WorkflowController) InitTask(t *domain.Task) error { }, }) if err != nil { - log.Println("unable to insert task") + return nil, fmt.Errorf("unable to create task at the db: %v", err) } + // ------------------------------------------------------------------------------------ - content := fmt.Sprintf( - "## TaskID: %d\nCreated by: %s\n\n%s", - dbtask.ID, - t.Creator, - t.Description, + task := newConvertable(&dbtask).ExtractDomain() + + // Инициализируем новый клиент дискорда + // [ ] Нездоровое получение параметров клиента из 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: content, + Content: task.NotStartedMessage(), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ @@ -74,28 +74,50 @@ func (wc *WorkflowController) InitTask(t *domain.Task) error { }, } - st, err := discord.ChannelMessageSendComplex(channel, &msg) + th, err := s.ForumThreadStartComplex( + forumChannelID, + &discordgo.ThreadStart{ + Name: "Task ID:" + strconv.Itoa(int(task.ID)), + }, + &msg, + ) if err != nil { - log.Println("unable to send task message") + return task, fmt.Errorf("unable to update channel: %v", err) } - _ = dbtask - _ = st - return err + err = wc.q.UpdateTaskWithMessageID(context.TODO(), db.UpdateTaskWithMessageIDParams{ + Messageid: pgtype.Text{String: th.ID, Valid: true}, + ID: dbtask.ID, + }) + if err != nil { + return task, fmt.Errorf("unable to set discord message to task: %v", err) + } + return task, nil } -func (wc *WorkflowController) UpdateTask(id string, opt int) { +func (wc *WorkflowController) UpdateTask(id string, opt int, user string) (*TaskConvertable, error) { + var ( + err error + dbtask db.Task + ) switch opt { case 0: - wc.q.StartTask(context.TODO(), db.StartTaskParams{ - StartedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + 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: - wc.q.CloseTask(context.TODO(), db.CloseTaskParams{ - ClosedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + 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/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/models.go b/internal/domain/models.go index fa222a8..5cc59e1 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,55 @@ 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 } +func (t *Task) NotStartedMessage() string { + + return fmt.Sprintf( + "## TaskID: %d\nCreated by: %s\n>>> %s\n", + t.ID, + t.Creator, + t.Description, + ) +} + +func (t *Task) StartedMessage() string { + + return fmt.Sprintf( + "## TaskID: %d\nCreated by: %s\n Assignee: %s\n🚀 Started at: %s\n>>> %s\n", + t.ID, + t.Creator, + t.Assignee, + t.UpdatedAt, + t.Description, + ) +} + +func (t *Task) ClosedMessage() string { + + return fmt.Sprintf( + "## TaskID: %d\nCreated by: %s\n Assignee: %s\n✅ Closed at: %s\n>>> %s\n", + t.ID, + t.Creator, + t.Assignee, + t.DeletedAt, + t.Description, + ) +} + func NewTask(summ, desc, c, cLink string) *Task { return &Task{ Summary: summ, diff --git a/internal/storage/db/models.go b/internal/storage/db/models.go index 290ff56..6af8f66 100644 --- a/internal/storage/db/models.go +++ b/internal/storage/db/models.go @@ -19,8 +19,7 @@ type Task struct { CreatorLink pgtype.Text Messageid pgtype.Text Description pgtype.Text - StartedAt pgtype.Timestamptz - ClosedAt pgtype.Timestamptz + Assignee pgtype.Text CreatedAt pgtype.Timestamptz DeletedAt pgtype.Timestamptz UpdatedAt pgtype.Timestamptz diff --git a/internal/storage/db/queries.sql.go b/internal/storage/db/queries.sql.go index eaabec2..13685a7 100644 --- a/internal/storage/db/queries.sql.go +++ b/internal/storage/db/queries.sql.go @@ -13,18 +13,19 @@ import ( const closeTask = `-- name: CloseTask :one UPDATE tasks -SET closed_at = $1 -WHERE messageID = $2 -RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at +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 { - ClosedAt pgtype.Timestamptz + 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.ClosedAt, arg.Messageid) + row := q.db.QueryRow(ctx, closeTask, arg.DeletedAt, arg.Assignee, arg.Messageid) var i Task err := row.Scan( &i.ID, @@ -32,8 +33,7 @@ func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, err &i.CreatorLink, &i.Messageid, &i.Description, - &i.StartedAt, - &i.ClosedAt, + &i.Assignee, &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, @@ -102,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 ` @@ -150,7 +192,7 @@ INSERT INTO tasks ( ) VALUES ( $1, $2, $3 ) - RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at + RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at ` type InsertTaskParams struct { @@ -168,8 +210,7 @@ func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, e &i.CreatorLink, &i.Messageid, &i.Description, - &i.StartedAt, - &i.ClosedAt, + &i.Assignee, &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, @@ -177,6 +218,40 @@ func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, e 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 ` @@ -260,18 +335,19 @@ func (q *Queries) SetNewConfig(ctx context.Context) (Appconfig, error) { const startTask = `-- name: StartTask :one UPDATE tasks -SET started_at = $1 -WHERE messageID = $2 -RETURNING id, creator, creator_link, messageid, description, started_at, closed_at, created_at, deleted_at, updated_at +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 { - StartedAt pgtype.Timestamptz + 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.StartedAt, arg.Messageid) + row := q.db.QueryRow(ctx, startTask, arg.UpdatedAt, arg.Assignee, arg.Messageid) var i Task err := row.Scan( &i.ID, @@ -279,8 +355,7 @@ func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, err &i.CreatorLink, &i.Messageid, &i.Description, - &i.StartedAt, - &i.ClosedAt, + &i.Assignee, &i.CreatedAt, &i.DeletedAt, &i.UpdatedAt, @@ -288,6 +363,22 @@ func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, err 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/migrate/003_init_tasks.sql b/internal/storage/migrate/0003_init_tasks.sql similarity index 80% rename from internal/storage/migrate/003_init_tasks.sql rename to internal/storage/migrate/0003_init_tasks.sql index dd7323f..81d440c 100644 --- a/internal/storage/migrate/003_init_tasks.sql +++ b/internal/storage/migrate/0003_init_tasks.sql @@ -6,10 +6,7 @@ CREATE TABLE tasks ( messageID VARCHAR(255), description TEXT, - - - started_at TIMESTAMPTZ, - closed_at TIMESTAMPTZ, + assignee VARCHAR(255), created_at TIMESTAMPTZ DEFAULT current_timestamp, deleted_at TIMESTAMPTZ, diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql index c0c6a28..cae5e62 100644 --- a/internal/storage/sqlc/queries.sql +++ b/internal/storage/sqlc/queries.sql @@ -62,14 +62,28 @@ INSERT INTO tasks ( ) RETURNING *; +-- name: UpdateTaskWithMessageID :exec +UPDATE tasks +SET messageID = $1 +WHERE id = $2; + -- name: StartTask :one UPDATE tasks -SET started_at = $1 -WHERE messageID = $2 +SET updated_at = $1, assignee = $2 +WHERE messageID = $3 RETURNING *; -- name: CloseTask :one UPDATE tasks -SET closed_at = $1 -WHERE messageID = $2 -RETURNING *; \ No newline at end of file +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 From c0e7a3918a1a4bfebb66c05e3c174134f7c642fc Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:46:45 +0500 Subject: [PATCH 04/12] check for restricted private messaging to bot --- client/discord/discord.go | 55 ++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index 1109b3e..802b6c4 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -21,14 +21,14 @@ func initBotWith(token string) *discordgo.Session { } type DiscordOptions struct { - AppConfig *domain.Config + Config *domain.Config Controller *controller.WorkflowController } 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) @@ -39,47 +39,78 @@ func Run(conf domain.Config, opts DiscordOptions) error { componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ "task_start": router.Components[0].Handler, - "task_close": router.Components[1].Handler, + "task_close": router.Components[0].Handler, } - session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + s.AddHandler(router.ListenPostsHandler) + + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Type { case discordgo.InteractionApplicationCommand: if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + 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 + } + h(s, i) } case discordgo.InteractionMessageComponent: if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { + 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 + } + 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 := session.Channel(os.Getenv("TASKS_CHANNEL")) + forum, err := s.Channel(conf.Discord.IsProjectChannel) if err != nil { log.Print(err) } - forum, err = session.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ + _, err = s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ AvailableTags: &router.Tags, }) if err != nil { log.Fatal(err) - } else { - } log.Println("Adding commands...") var cmds []*discordgo.ApplicationCommand var logString []string for _, h := range router.Commands { - cmd, err := session.ApplicationCommandCreate(session.State.User.ID, "", &h.Command) + 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) } @@ -90,7 +121,7 @@ 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 @@ -98,7 +129,7 @@ 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) } From 972e6564ea7a7df5c145d7edb9a902f1aa090619 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:47:21 +0500 Subject: [PATCH 05/12] finished farm tasks; added new post listener; --- .../discord/handler/handle_external_task.go | 232 +++++++++++------- 1 file changed, 143 insertions(+), 89 deletions(-) diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go index 61a1c99..6726d25 100644 --- a/client/discord/handler/handle_external_task.go +++ b/client/discord/handler/handle_external_task.go @@ -1,12 +1,15 @@ package handler import ( + "context" + "fmt" "log" + "ticket-pimp/internal/domain" "github.com/bwmarrin/discordgo" ) -func (h *router) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { +func (c *client) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { th, err := s.Channel(i.ChannelID) if err != nil { @@ -22,11 +25,12 @@ func (h *router) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, t if len(forum.AvailableTags) != 0 { for _, some := range forum.AvailableTags { if some.Name == tag.Name { - log.Print(tag.Name) - thE, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ + _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ AppliedTags: &[]string{some.ID}, }) - _, _ = thE, err + if err != nil { + return err + } } } } @@ -34,105 +38,155 @@ func (h *router) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, t return nil } -func (h *router) CreateExternalTask() CommandRoute { - return CommandRoute{ +func (c *client) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) { - Command: discordgo.ApplicationCommand{ - Name: "test", - Description: "Buttons test", - }, - - 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) - - // go func() { - // h.controller.InitTask("something like a default description") - // }() - }, + // 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 } -} -func (h *router) StartTask() ComponentRoute { - return ComponentRoute{ - Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { + msgs, _ := s.ChannelMessages(th.ID, 1, "", "", "") - user := i.Member.User.Mention() + msg, _ := s.ChannelMessage(th.ID, msgs[0].ID) - convertable, err := h.controller.UpdateTask(i.Message.ID, 0, user) - if err != nil { + if msg.Author.ID == s.State.User.ID { + return + } - } + content := th.Name + content += "\n" + msg.Content - newContent := convertable.ExtractDomain().StartedMessage() - if err != nil { - newContent += "\n `In progress` action produced with error:" + err.Error() - } + user, _ := s.GuildMember(th.GuildID, msg.Author.ID) - newMsg := discordgo.MessageEdit{ - Content: &newContent, - Channel: i.ChannelID, - ID: i.Message.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.ActionsRow{ - Components: []discordgo.MessageComponent{ - discordgo.Button{ - Label: "Close", - Style: discordgo.DangerButton, - Disabled: false, - CustomID: "task_close", - }, - }, + discordgo.Button{ + Label: "Start", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "task_start", + }, + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: true, + CustomID: "task_close", }, }, - } - - h.setFlag(s, i, &h.Tags[0]) - - _, err = s.ChannelMessageEditComplex(&newMsg) - if err != nil { - log.Println("edition NOT complete, ", err) - } + }, }, + }) + 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 (h *router) CloseTask() ComponentRoute { - return ComponentRoute{ - Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) { - - user := i.Member.User.Mention() - convertable, err := h.controller.UpdateTask(i.Message.ID, 1, user) - - newContent := convertable.ExtractDomain().ClosedMessage() - if err != nil { - newContent += "\n `Close` action produced with error:" + err.Error() - } - - newMsg := discordgo.MessageEdit{ - Content: &newContent, - Channel: i.ChannelID, - ID: i.Message.ID, - Components: []discordgo.MessageComponent{}, - } - - msgE, err := s.ChannelMessageEditComplex(&newMsg) - if err != nil { - log.Println("edition NOT complete, ", err) - } - - _ = msgE - h.setFlag(s, i, &h.Tags[1]) - - }, +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() + ) + + // Check what flow was touched: ------------------------------------------------------------------------- + switch i.Interaction.MessageComponentData().CustomID { + case "task_start": + opt = 0 + doneButtonIsDisabled = false + state = domain.InrpogressTaskState() + case "task_close": + opt = 1 + doneButtonIsDisabled = true + state = domain.DoneTaskState() + } + + // 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: ------------------------------------------------------------------- + newContent := convertable. + ExtractDomain(). + DiscordMessage(state) + + // 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) } } From f677ec5986a1d36ffa83082fa97a173b17a1447b Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:47:47 +0500 Subject: [PATCH 06/12] - added separate functions as handlers; --- client/discord/handler/handle_folder.go | 128 ++++---- client/discord/handler/handle_git.go | 138 +++++---- client/discord/handler/handle_ping.go | 38 +-- client/discord/handler/handle_ticket.go | 332 +++++++++++---------- client/telegram/handler/handle_farmtask.go | 18 +- 5 files changed, 338 insertions(+), 316 deletions(-) diff --git a/client/discord/handler/handle_folder.go b/client/discord/handler/handle_folder.go index 82d860e..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) CommandRoute { +func (c *client) CreateFolderHandler(nameMinLenght int) Command { const ( nameOption string = "folder_name" ) - return CommandRoute{ + return Command{ Command: discordgo.ApplicationCommand{ Name: "folder", @@ -28,64 +28,70 @@ func (h *router) CreateFolderHandler(nameMinLenght int) CommandRoute { }, }, - 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() - if resp.Message != nil { - result += "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 6a62152..071172c 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) CommandRoute { +func (c *client) CreateRepoHandler(repoNameMinLength int) Command { const ( repoType = "repo_type" projectRepo = "project_repo" @@ -16,10 +16,10 @@ func (h *router) CreateRepoHandler(repoNameMinLength int) CommandRoute { nameOption = "repo_name" ) - return CommandRoute{ + 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,75 @@ func (h *router) CreateRepoHandler(repoNameMinLength int) CommandRoute { }, }, }, - 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 { + result = resp.Message.Error() + } else { + result = resp.Project.DiscordString() + "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 1faa7d4..4f91795 100644 --- a/client/discord/handler/handle_ping.go +++ b/client/discord/handler/handle_ping.go @@ -6,31 +6,25 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) Ping() CommandRoute { - return CommandRoute{ +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) { - - err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - TTS: false, - Content: "Pong to: " + i.Member.User.Mention(), - Components: []discordgo.MessageComponent{}, - Embeds: []*discordgo.MessageEmbed{}, - Files: []*discordgo.File{}, - Flags: 0, - Choices: []*discordgo.ApplicationCommandOptionChoice{}, - CustomID: "", - Title: "", - }, - }) - 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 2e7f790..337b73d 100644 --- a/client/discord/handler/handle_ticket.go +++ b/client/discord/handler/handle_ticket.go @@ -9,133 +9,133 @@ import ( "github.com/bwmarrin/discordgo" ) -func (h *router) GetInfo() CommandRoute { - return CommandRoute{ +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) CommandRoute { - const ( - keyOption = "key" - ) - return CommandRoute{ +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 { + result = project.DiscordString() + if err != nil { + result += "Errors: " + err.Error() + } + } + } + + 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() - if errMsg != nil { - result += "Errors: " + errMsg.Error() - } - } - } - } - - h.defaultFollowUp(result, s, i) - }, + Handler: c.initProjectFromChannel, } } -func (h *router) CreateTicketHandler(repoNameMinLength int) CommandRoute { - return CommandRoute{ +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", @@ -149,65 +149,67 @@ func (h *router) CreateTicketHandler(repoNameMinLength int) CommandRoute { }, }, }, - 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 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 + } + } + } + } + + c.defaultFollowUp(result, s, i) +} diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/handler/handle_farmtask.go index 8274ec9..128c120 100644 --- a/client/telegram/handler/handle_farmtask.go +++ b/client/telegram/handler/handle_farmtask.go @@ -60,6 +60,11 @@ func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) er 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( @@ -67,9 +72,16 @@ func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) er tg.HTML.Bold("I'm unable to work with files, but"), ), tg.HTML.Line( - tg.HTML.Bold("Task ID: "), - tg.HTML.Code(i), - tg.HTML.Text(" was created"), + 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"), + "чтобы отслеживать статус по задаче", ), ) } From d42b590a14d4776e12caf964b835ccfbcc7b6564 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:48:03 +0500 Subject: [PATCH 07/12] small changes; --- client/discord/handler/handler.go | 49 +++++++++--------- client/telegram/handler/handler_test.go | 34 ------------- cmd/main.go | 2 +- internal/controller/control_git.go | 9 ++-- internal/controller/control_project.go | 2 +- internal/controller/control_task.go | 54 +++++++++++++++----- internal/controller/control_workflow.go | 7 +++ internal/domain/config.go | 2 + internal/domain/models.go | 66 +++++++++++++++---------- 9 files changed, 126 insertions(+), 99 deletions(-) delete mode 100644 client/telegram/handler/handler_test.go diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index d6b2266..c6dc733 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -8,34 +8,35 @@ import ( "github.com/bwmarrin/discordgo" ) -type router struct { - Commands []CommandRoute - Components []ComponentRoute - Tags []discordgo.ForumTag +type client struct { + Commands []Command + Components []Component + ListenPostsHandler func(s *discordgo.Session, th *discordgo.ThreadCreate) + Tags []discordgo.ForumTag controller controller.WorkflowController conf *domain.DiscordConfig } // Подключение роутов к Discord боту -func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *router { +func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *client { - var r router - r.Commands = append(r.Commands, - // r.CreateRepoHandler(3), - // r.CreateFolderHandler(3), - r.Ping(), - // r.CreateTicketHandler(3), - // r.InitProjectFromChannel(3), - // r.GetInfo(), - r.CreateExternalTask(), - ) - r.Components = append(r.Components, - r.StartTask(), - r.CloseTask(), - ) + 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.Tags = append( r.Tags, discordgo.ForumTag{ @@ -49,21 +50,25 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *r Moderated: true, EmojiName: "✅", }) + r.ListenPostsHandler = r.ListenPosts return &r } -type CommandRoute struct { +// +// Подключение роутов к Discord боту + +type Command struct { Command discordgo.ApplicationCommand Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) } -type ComponentRoute struct { +type Component struct { Component discordgo.MessageComponent Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) } -func (h *router) defaultFollowUp(answer string, 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/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/cmd/main.go b/cmd/main.go index 0ae0db8..b8ad6ac 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -55,7 +55,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) 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 index 032537d..b992ca7 100644 --- a/internal/controller/control_task.go +++ b/internal/controller/control_task.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strconv" "ticket-pimp/internal/domain" "ticket-pimp/internal/storage/db" "time" @@ -13,9 +12,14 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { - - // Записываем в базу созданную задачу ------------------------------------------ +// 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{ @@ -33,6 +37,29 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { // ------------------------------------------------------------------------------------ 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.. @@ -44,16 +71,14 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { 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.NotStartedMessage(), + Content: task.DiscordMessage(domain.NewTaskState()), Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ @@ -77,7 +102,7 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { th, err := s.ForumThreadStartComplex( forumChannelID, &discordgo.ThreadStart{ - Name: "Task ID:" + strconv.Itoa(int(task.ID)), + Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), }, &msg, ) @@ -85,10 +110,7 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { return task, fmt.Errorf("unable to update channel: %v", err) } - err = wc.q.UpdateTaskWithMessageID(context.TODO(), db.UpdateTaskWithMessageIDParams{ - Messageid: pgtype.Text{String: th.ID, Valid: true}, - ID: dbtask.ID, - }) + 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) } @@ -96,6 +118,14 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { 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 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/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 5cc59e1..48ec870 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -69,38 +69,52 @@ type Task struct { URL string } -func (t *Task) NotStartedMessage() string { +type TaskState int - return fmt.Sprintf( - "## TaskID: %d\nCreated by: %s\n>>> %s\n", - t.ID, - t.Creator, - t.Description, - ) +const ( + new TaskState = iota + inprogress + done +) + +func NewTaskState() TaskState { + return TaskState(0) } -func (t *Task) StartedMessage() string { - - return fmt.Sprintf( - "## TaskID: %d\nCreated by: %s\n Assignee: %s\n🚀 Started at: %s\n>>> %s\n", - t.ID, - t.Creator, - t.Assignee, - t.UpdatedAt, - t.Description, - ) +func InrpogressTaskState() TaskState { + return TaskState(1) } -func (t *Task) ClosedMessage() string { +func DoneTaskState() TaskState { + return TaskState(2) +} - return fmt.Sprintf( - "## TaskID: %d\nCreated by: %s\n Assignee: %s\n✅ Closed at: %s\n>>> %s\n", - t.ID, - t.Creator, - t.Assignee, - t.DeletedAt, - t.Description, - ) +// 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 { From 6e69c64c420d8d722f99f75c7de932d1ccaa6636 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:56:24 +0500 Subject: [PATCH 08/12] - check private messages into separate function; --- client/discord/discord.go | 56 +++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index 802b6c4..d421a66 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -1,6 +1,7 @@ package discord import ( + "errors" "fmt" "log" "os" @@ -25,6 +26,24 @@ type DiscordOptions struct { 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 @@ -42,46 +61,21 @@ func Run(conf domain.Config, opts DiscordOptions) error { "task_close": router.Components[0].Handler, } - s.AddHandler(router.ListenPostsHandler) + 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 { - 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 - } - h(s, i) } case discordgo.InteractionMessageComponent: if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { - 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 - } - h(s, i) } } From 6e94e8acc4c68daaa125bf527e891bce20f17c8d Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 13:56:37 +0500 Subject: [PATCH 09/12] f --- client/discord/handler/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index c6dc733..b4f7324 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -50,7 +50,7 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *c Moderated: true, EmojiName: "✅", }) - r.ListenPostsHandler = r.ListenPosts + // r.ListenPostsHandler = r.ListenPosts return &r } From 2c24b73a82a2de925a75f54de7e3729a6dbe5536 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 14:02:11 +0500 Subject: [PATCH 10/12] - remove field for post listener; --- client/discord/handler/handler.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go index b4f7324..5f31ea0 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -9,10 +9,10 @@ import ( ) type client struct { - Commands []Command - Components []Component - ListenPostsHandler func(s *discordgo.Session, th *discordgo.ThreadCreate) - Tags []discordgo.ForumTag + Commands []Command + Components []Component + + Tags []discordgo.ForumTag controller controller.WorkflowController conf *domain.DiscordConfig @@ -50,7 +50,6 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *c Moderated: true, EmojiName: "✅", }) - // r.ListenPostsHandler = r.ListenPosts return &r } From afa3e03a27f611be52a60fa8d0ff2708c74e9a51 Mon Sep 17 00:00:00 2001 From: naudachu Date: Fri, 17 Nov 2023 14:24:14 +0500 Subject: [PATCH 11/12] - send task status to creator in TG --- client/discord/discord.go | 2 +- .../discord/handler/handle_external_task.go | 63 ++++++++++++++++++- client/discord/handler/handler.go | 4 +- 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/client/discord/discord.go b/client/discord/discord.go index d421a66..8c7fd88 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -49,7 +49,7 @@ func Run(conf domain.Config, opts DiscordOptions) error { 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.Commands { diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go index 6726d25..b41dd5c 100644 --- a/client/discord/handler/handle_external_task.go +++ b/client/discord/handler/handle_external_task.go @@ -7,6 +7,7 @@ import ( "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 { @@ -129,6 +130,7 @@ func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.Interaction opt int = -1 doneButtonIsDisabled bool = false state domain.TaskState = domain.NewTaskState() + message string ) // Check what flow was touched: ------------------------------------------------------------------------- @@ -137,10 +139,12 @@ func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.Interaction 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 -------------------------------------------------------------------------- @@ -151,9 +155,18 @@ func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.Interaction } // Map DB's response to domain.Task: ------------------------------------------------------------------- - newContent := convertable. - ExtractDomain(). - DiscordMessage(state) + 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{ @@ -190,3 +203,47 @@ func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.Interaction 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/handler.go b/client/discord/handler/handler.go index 5f31ea0..cb64fc3 100644 --- a/client/discord/handler/handler.go +++ b/client/discord/handler/handler.go @@ -16,10 +16,11 @@ type client struct { controller controller.WorkflowController conf *domain.DiscordConfig + tgConf *domain.TelegramConfig } // Подключение роутов к Discord боту -func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *client { +func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tgConf *domain.TelegramConfig) *client { var r client r.controller = wc @@ -50,6 +51,7 @@ func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig) *c Moderated: true, EmojiName: "✅", }) + r.tgConf = tgConf return &r } From e99506032b7920fd5561eb1c423d87659d580a44 Mon Sep 17 00:00:00 2001 From: naudachu Date: Mon, 20 Nov 2023 14:33:30 +0500 Subject: [PATCH 12/12] - final devops magick; --- Dockerfile | 1 + client/discord/discord.go | 2 + client/discord/handler/handle_git.go | 11 ++++- client/discord/handler/handle_ticket.go | 15 ++++--- cmd/main.go | 40 ++++++++++++++++--- compose.yaml | 25 ++++++++++++ go.mod | 4 ++ go.sum | 10 +++++ internal/storage/dbconfig.yml | 2 +- .../storage/migrate/0001_init_tickets.sql | 15 ------- .../migrate/0001_initial_migration.sql | 39 ++++++++++++++++++ internal/storage/migrate/0002_init_config.sql | 11 ----- internal/storage/migrate/0003_init_tasks.sql | 17 -------- 13 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 compose.yaml delete mode 100644 internal/storage/migrate/0001_init_tickets.sql create mode 100644 internal/storage/migrate/0001_initial_migration.sql delete mode 100644 internal/storage/migrate/0002_init_config.sql delete mode 100644 internal/storage/migrate/0003_init_tasks.sql 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 8c7fd88..5e2d06f 100644 --- a/client/discord/discord.go +++ b/client/discord/discord.go @@ -18,6 +18,7 @@ func initBotWith(token string) *discordgo.Session { if err != nil { log.Fatalf("unable to create discord session: %v", err) } + return discord } @@ -41,6 +42,7 @@ func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) }) return errors.New("no private messages! lol") } + return nil } diff --git a/client/discord/handler/handle_git.go b/client/discord/handler/handle_git.go index 071172c..f41ff60 100644 --- a/client/discord/handler/handle_git.go +++ b/client/discord/handler/handle_git.go @@ -112,9 +112,16 @@ func (c *client) createRepoHandler(s *discordgo.Session, i *discordgo.Interactio // Making request: resp := c.controller.CreateGit(context.TODO(), req) if resp.Project == nil { - result = resp.Message.Error() + if resp.Message != nil { + result = resp.Message.Error() + } } else { - result = resp.Project.DiscordString() + "Errors: " + resp.Message.Error() + + 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_ticket.go b/client/discord/handler/handle_ticket.go index 337b73d..f904e5d 100644 --- a/client/discord/handler/handle_ticket.go +++ b/client/discord/handler/handle_ticket.go @@ -43,10 +43,15 @@ func (c *client) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { if err != nil { result = err.Error() } else { - result = project.DiscordString() - if err != nil { - result += "Errors: " + err.Error() + if project != nil { + result = project.DiscordString() + if err != nil { + result += "Errors: " + err.Error() + } + } else { + result = "Something wrong with retrieving project from db" } + } } @@ -198,14 +203,14 @@ func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.Interact dchan, err = s.ChannelEdit(dchan.ID, &edit) if err != nil { - result = fmt.Sprintf("channel created, but unable to edit: %v\n", err) + 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 = dchan.ID + result = "Project " + p.ShortName + "Was created" } } } diff --git a/cmd/main.go b/cmd/main.go index b8ad6ac..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) 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/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 b5fd33e..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 ('P', 100); - --- +migrate Down -DROP TABLE appconfig; \ No newline at end of file diff --git a/internal/storage/migrate/0003_init_tasks.sql b/internal/storage/migrate/0003_init_tasks.sql deleted file mode 100644 index 81d440c..0000000 --- a/internal/storage/migrate/0003_init_tasks.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +migrate Up -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 tasks; \ No newline at end of file