- finished coda usage

This commit is contained in:
naudachu 2023-11-25 20:19:40 +05:00
parent eb05bf8470
commit 19f6064066
18 changed files with 170 additions and 74 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
.vscode/ .vscode/
.idea/
.github
docker/
**/**/*.env **/**/*.env
docker/**

View File

@ -16,7 +16,7 @@ type ICloud interface {
type ICoda interface { type ICoda interface {
ListDocs() ListDocs()
CreateApp(task domain.CodaApplication) CreateApp(task domain.CodaApplication) (string, error)
CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error)
GetRowLink(id string) (string, error) GetRowLink(id string) (string, error)
} }

View File

@ -9,7 +9,7 @@ import (
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services" "ticket-pimp/internal/external"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
@ -42,6 +42,10 @@ var (
Name: "ping", Name: "ping",
Description: "pongs in a reply", Description: "pongs in a reply",
}, },
{
Name: "coda_ticket",
Description: "Creates ticket in Coda.io w/ provided info",
},
{ {
Name: "init_project", Name: "init_project",
Description: "Connect project with Coda ID", Description: "Connect project with Coda ID",
@ -166,7 +170,7 @@ func isRejected(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
} }
func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handler.Handler) { func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handler.Handler) {
initialResponse(s, i) // initialResponse(s, i)
if isRejected(s, i) { if isRejected(s, i) {
return return
@ -175,9 +179,8 @@ func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handl
// Определяем тип взаимодействия и хэндлим правильной функцией: // Определяем тип взаимодействия и хэндлим правильной функцией:
switch i.Type { switch i.Type {
case discordgo.InteractionApplicationCommand: case discordgo.InteractionApplicationCommand:
cmd := i.ApplicationCommandData().Name
switch cmd { switch i.ApplicationCommandData().Name {
case "ping": case "ping":
h.Ping(s, i) h.Ping(s, i)
case "project": case "project":
@ -190,10 +193,13 @@ func route(s *discordgo.Session, i *discordgo.InteractionCreate, h discord_handl
h.CreateFolder(s, i) h.CreateFolder(s, i)
case "init_project": case "init_project":
h.InitChannelAsProject(s, i) h.InitChannelAsProject(s, i)
case "coda_ticket":
h.CreateCoda(s, i)
} }
case discordgo.InteractionMessageComponent: case discordgo.InteractionMessageComponent:
c := i.MessageComponentData().CustomID
switch c { switch i.MessageComponentData().CustomID {
case "task_start": case "task_start":
h.HandleTaskButtons(s, i) h.HandleTaskButtons(s, i)
case "task_close": case "task_close":
@ -221,10 +227,11 @@ func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTa
// Result tags array // Result tags array
tags := forum.AvailableTags tags := forum.AvailableTags
// Check if preset tag exists into current channel // Check if preset tag exists into current channel..
for i := 0; i < len(tagsPreset); i++ { for i := 0; i < len(tagsPreset); i++ {
_, ok := tagsMap[tagsPreset[i].Name] _, ok := tagsMap[tagsPreset[i].Name]
if !ok { if !ok {
// .. and append them if they aren't
tags = append(tags, tagsPreset[i]) tags = append(tags, tagsPreset[i])
} }
} }
@ -241,6 +248,22 @@ func updateForum(conf *domain.Config, s *discordgo.Session) ([]discordgo.ForumTa
fmt.Printf("N: %s, ID: %s", t.Name, t.ID) fmt.Printf("N: %s, ID: %s", t.Name, t.ID)
} }
// Update config w/ tags:
confTags := make(map[domain.TaskState]string)
for _, tag := range dchan.AvailableTags {
switch tag.Name {
case "Не начат":
confTags[domain.State(0)] = tag.ID
case "В работе":
confTags[domain.State(1)] = tag.ID
case "Готово":
confTags[domain.State(2)] = tag.ID
}
}
conf.Discord.Tags = confTags
return dchan.AvailableTags, nil return dchan.AvailableTags, nil
} }
@ -267,7 +290,7 @@ func Run(conf *domain.Config, opts DiscordOptions) error {
h := discord_handler.New( h := discord_handler.New(
opts.Controller, opts.Controller,
&conf.Discord, &conf.Discord,
services.NewDummyClient(conf.Telegram), external.NewDummyClient(conf.Telegram),
) )
// Add posts listener // Add posts listener

View File

@ -1,24 +1,23 @@
package discord_handler package discord_handler
import ( import (
"fmt"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { func (h *Handler) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) {
// Sending result: // Sending result:
_, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ /*_, err := */
s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: answer, Content: answer,
}) })
if err != nil { // if err != nil {
s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ // s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: fmt.Sprintf("Something went wrong: %v", err), // Content: fmt.Sprintf("Something went wrong: %v", err),
}) // })
return // return
} // }
} }
// setFlag // setFlag

View File

@ -6,16 +6,16 @@ import (
"ticket-pimp/client/telegram/telegram_handler" "ticket-pimp/client/telegram/telegram_handler"
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services" "ticket-pimp/internal/external"
"github.com/mr-linch/go-tg" "github.com/mr-linch/go-tg"
"github.com/mr-linch/go-tg/tgb" "github.com/mr-linch/go-tg/tgb"
) )
type TelegramOptions struct { type TelegramOptions struct {
GitService *services.Git GitService *external.Git
CloudService *services.Cloud CloudService *external.Cloud
Coda *services.Coda Coda *external.Coda
AppConfig *domain.Config AppConfig *domain.Config
Controller *controller.WorkflowController Controller *controller.WorkflowController
} }

View File

@ -11,7 +11,7 @@ import (
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services" "ticket-pimp/internal/external"
"ticket-pimp/client/discord" "ticket-pimp/client/discord"
"ticket-pimp/client/telegram" "ticket-pimp/client/telegram"
@ -59,7 +59,7 @@ func Go(ctx context.Context, fns ...func(context.Context) error) error {
} }
func applyMigrations(connString string) { func applyMigrations(connString string) {
// Aply migrations: // Apply migrations:
dbConnConfig, err := pgxpool.ParseConfig(connString) dbConnConfig, err := pgxpool.ParseConfig(connString)
if err != nil { if err != nil {
@ -79,17 +79,20 @@ func applyMigrations(connString string) {
} }
fmt.Printf("Applied %d migrations!\n", n) fmt.Printf("Applied %d migrations!\n", n)
db.Close() err = db.Close()
if err != nil {
log.Fatal("unable to close db connection")
}
} }
func run(conf *domain.Config) { func run(c *domain.Config) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM)
defer cancel() defer cancel()
// -- DB connection init -- START // -- DB connection init -- START
connString := fmt.Sprintf( connString := fmt.Sprintf(
"postgresql://%s:%s@%s:%s/%s", "postgresql://%s:%s@%s:%s/%s",
conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, c.DB.User, c.DB.Pass, c.DB.Host, c.DB.Port, c.DB.Name,
) )
conn, err := pgxpool.New( conn, err := pgxpool.New(
ctx, ctx,
@ -97,42 +100,41 @@ func run(conf *domain.Config) {
if err != nil { if err != nil {
log.Fatalf("DB connection failed: %v", err) log.Fatalf("DB connection failed: %v", err)
} }
// -- DB connection init -- END
// Aply migrations: // Apply migrations:
applyMigrations(connString) applyMigrations(connString)
// Init services instances: // Init services instances:
gitService := services.NewGit(conf.Git) git := external.NewGit(c.Git)
cloudService := services.NewCloud(conf.Cloud) cloud := external.NewCloud(c.Cloud)
codaService := services.NewCodaClient(conf.Coda) coda := external.NewCoda(c.Coda)
// Controller instance init: // Controller instance init:
controller := controller.NewWorkflowController( controller := controller.NewApp(
gitService, git,
cloudService, cloud,
codaService, coda,
conn, conn,
conf, c,
) )
Go(ctx, Go(ctx,
func(ctx context.Context) error { func(ctx context.Context) error {
opts := discord.DiscordOptions{ opts := discord.DiscordOptions{
Controller: controller, Controller: controller,
Config: conf, Config: c,
} }
if err := discord.Run(conf, opts); err != nil { if err := discord.Run(c, opts); err != nil {
return errors.Errorf("discord bot cannot be runned: %v", err) return errors.Errorf("discord bot cannot be runned: %v", err)
} }
return nil return nil
}, },
func(ctx context.Context) error { func(ctx context.Context) error {
opts := telegram.TelegramOptions{ opts := telegram.TelegramOptions{
GitService: gitService, GitService: git,
CloudService: cloudService, CloudService: cloud,
Coda: codaService, Coda: coda,
AppConfig: conf, AppConfig: c,
Controller: controller, Controller: controller,
} }

View File

@ -0,0 +1,27 @@
package controller
import (
"context"
"fmt"
"ticket-pimp/internal/domain"
)
func (wc *WorkflowController) CreateCoda(guildID string, chanID string) (string, error) {
p, err := wc.GetProjectByChannelID(context.TODO(), chanID)
if err != nil {
return "", err
}
requestResult, err := wc.ICoda.CreateApp(domain.CodaApplication{
ID: p.Key,
Summary: p.Name,
URL: fmt.Sprintf("https://discord.com/channels/%s/%s", guildID, chanID),
Git: p.ProjectGit,
GitBuild: p.BuildGit,
Folder: p.Folder,
})
return requestResult, err
}

View File

@ -3,6 +3,7 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/storage/db" "ticket-pimp/internal/storage/db"
@ -67,25 +68,24 @@ func (wc *WorkflowController) ProjectCreate(ctx context.Context, project domain.
} }
func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id string) (*domain.Project, error) { func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id string) (*domain.Project, error) {
var proj domain.Project
dbTicket, err := wc.q.GetTicketByChannelID(ctx, pgtype.Text{String: id, Valid: true}) dbTicket, err := wc.q.GetTicketByChannelID(ctx, pgtype.Text{String: id, Valid: true})
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
return nil, nil return nil, nil
} }
return nil, err return nil, err
} else {
proj = domain.Project{
ID: string(dbTicket.ID),
Key: dbTicket.Key.String,
Name: dbTicket.Key.String,
ChannelID: dbTicket.Channelid.String,
ProjectGit: dbTicket.ProjectGit.String,
BuildGit: dbTicket.BuildGit.String,
Folder: dbTicket.Folder.String,
}
} }
return &proj, nil
return &domain.Project{
ID: strconv.Itoa(int(dbTicket.ID)),
Key: dbTicket.Key.String,
Name: dbTicket.Title.String,
ChannelID: dbTicket.Channelid.String,
ProjectGit: dbTicket.ProjectGit.String,
BuildGit: dbTicket.BuildGit.String,
Folder: dbTicket.Folder.String,
}, nil
} }
// Saves current channel as project's channel; // Saves current channel as project's channel;

View File

@ -94,7 +94,7 @@ func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) {
wc.conf.Discord.IsTaskForum, wc.conf.Discord.IsTaskForum,
&discordgo.ThreadStart{ &discordgo.ThreadStart{
Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator),
AppliedTags: []string{"Не начат"}, AppliedTags: []string{wc.conf.Discord.Tags[domain.NewTaskState()]},
}, },
&msg, &msg,
) )

View File

@ -17,7 +17,7 @@ type WorkflowController struct {
conf *domain.Config conf *domain.Config
} }
func NewWorkflowController( func NewApp(
git adapters.IGit, git adapters.IGit,
cloud adapters.ICloud, cloud adapters.ICloud,
coda adapters.ICoda, coda adapters.ICoda,

View File

@ -53,6 +53,7 @@ type DiscordConfig struct {
Token string Token string
IsProjectChannel string IsProjectChannel string
IsTaskForum string IsTaskForum string
Tags map[TaskState]string
} }
type ApplicationConfig struct { type ApplicationConfig struct {

View File

@ -83,6 +83,18 @@ const (
done done
) )
func State(i int) TaskState {
switch i {
case 0:
return new
case 1:
return inprogress
case 2:
return done
}
return -1
}
func NewTaskState() TaskState { func NewTaskState() TaskState {
return TaskState(0) return TaskState(0)
} }

View File

@ -1,7 +0,0 @@
package domain
type TgUser struct {
ID string
Name string
TgLink string
}

View File

@ -1,4 +1,4 @@
package services package external
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package services package external
import ( import (
"errors" "errors"

View File

@ -1,8 +1,10 @@
package services package external
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"strings"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"time" "time"
@ -14,7 +16,7 @@ type Coda struct {
Config domain.CodaConfig Config domain.CodaConfig
} }
func NewCodaClient(conf domain.CodaConfig) *Coda { func NewCoda(conf domain.CodaConfig) *Coda {
client := NewClient(). client := NewClient().
SetTimeout(15 * time.Second). SetTimeout(15 * time.Second).
@ -47,14 +49,49 @@ func (c *Coda) ListDocs() {
log.Print(resp) log.Print(resp)
} }
func (c *Coda) CreateApp(task domain.CodaApplication) { type CodaWebhookResponse struct {
resp, _ := c.R(). ReqID string `json:"requestId"`
}
func (c *Coda) CreateApp(task domain.CodaApplication) (string, error) {
var whResponse CodaWebhookResponse
c.R().
SetBody(task). SetBody(task).
SetContentType("application/json"). SetContentType("application/json").
SetSuccessResult(&whResponse).
SetBearerAuthToken(c.Config.Develop). SetBearerAuthToken(c.Config.Develop).
Post("/docs/Ic3IZpQ3Wk/hooks/automation/grid-auto-NlUwM7F7Cr") Post("/docs/Ic3IZpQ3Wk/hooks/automation/grid-auto-NlUwM7F7Cr")
fmt.Print(resp) if whResponse.ReqID == "" {
return "", errors.New("coda responded w/o mutate id")
}
var (
// mutate string
sep string = ":"
)
if !strings.Contains(whResponse.ReqID, sep) {
return "", fmt.Errorf("unexpected coda response: %s", whResponse.ReqID)
}
// arr := strings.Split(whResponse.ReqID, sep)
// if arr[0] == "mutate" {
// mutate = arr[1]
// }
// mutateResponse, err := c.R().
// SetContentType("application/json").
// SetBearerAuthToken(c.Config.Develop).
// Get(fmt.Sprintf("/mutationStatus/%s", mutate))
// if err != nil {
// return "", fmt.Errorf("unable to get coda mutate result: %s", mutate)
// }
// _ = mutateResponse
return whResponse.ReqID, nil
} }
func (c *Coda) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) { func (c *Coda) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) {

View File

@ -1,4 +1,4 @@
package services package external
import ( import (
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"

View File

@ -1,4 +1,4 @@
package services package external
import ( import (
"fmt" "fmt"