Merge branch 'instant-reply' into discord-bot-integration

This commit is contained in:
naudachu 2023-11-09 16:29:12 +05:00
commit b5f798ad8e
24 changed files with 1102 additions and 419 deletions

View File

@ -7,7 +7,6 @@ RUN apk add git
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags '-extldflags "-static"' -tags timetzdata ./cmd/main.go RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags '-extldflags "-static"' -tags timetzdata ./cmd/main.go
FROM scratch FROM scratch
# the test program:
COPY --from=app-builder /go/bin/main /ticket-pimp 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/cmd/prod.env /
# the tls certificates: # the tls certificates:

View File

@ -62,7 +62,6 @@ func run(conf domain.Config) {
} }
}() }()
// go func() {
opts := telegram.TelegramOptions{ opts := telegram.TelegramOptions{
// TicketsRepo: db, // TicketsRepo: db,
GitService: gitService, GitService: gitService,
@ -75,6 +74,4 @@ func run(conf domain.Config) {
log.Fatalf("telegram bot cannot be runned: %v", err) log.Fatalf("telegram bot cannot be runned: %v", err)
defer os.Exit(1) defer os.Exit(1)
} }
// }()
} }

View File

@ -12,12 +12,12 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
func initBotWith(token string) (*discordgo.Session, error) { func initBotWith(token string) *discordgo.Session {
discord, err := discordgo.New("Bot " + token) discord, err := discordgo.New("Bot " + token)
if err != nil { if err != nil {
return nil, err log.Fatalf("unable to create discord session: %v", err)
} }
return discord, nil return discord
} }
type DiscordOptions struct { type DiscordOptions struct {
@ -28,10 +28,7 @@ type DiscordOptions struct {
func Run(conf domain.Config, opts DiscordOptions) error { func Run(conf domain.Config, opts DiscordOptions) error {
token := conf.Discord.Token token := conf.Discord.Token
session, err := initBotWith(token) session := initBotWith(token)
if err != nil {
return err
}
router := handler.InitRouter(*opts.Controller) router := handler.InitRouter(*opts.Controller)
@ -41,7 +38,6 @@ func Run(conf domain.Config, opts DiscordOptions) error {
} }
session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i) h(s, i)
} }
@ -73,7 +69,7 @@ func Run(conf domain.Config, opts DiscordOptions) error {
log.Println("Removing commands...") log.Println("Removing commands...")
for _, h := range cmds { for _, h := range cmds {
err = session.ApplicationCommandDelete(session.State.User.ID, "", h.ID) err := session.ApplicationCommandDelete(session.State.User.ID, "", h.ID)
if err != nil { if err != nil {
log.Panicf("Cannot delete '%v' command: %v", h.Name, err) log.Panicf("Cannot delete '%v' command: %v", h.Name, err)
} }

View File

@ -0,0 +1,107 @@
package handler
import (
"context"
"fmt"
"log"
"strconv"
"ticket-pimp/internal/controller"
"github.com/bwmarrin/discordgo"
)
func (h *router) CreateFolderHandler(nameMinLenght int) route {
const (
nameOption string = "folder_name"
)
return route{
Command: discordgo.ApplicationCommand{
Name: "folder",
Description: "Command for cloud folder creation",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: nameOption,
Description: "Type the folder's name",
Required: false,
MinLength: &nameMinLenght,
},
},
},
Handler: 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 == strconv.Itoa(1150719794853716028) {
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 = fmt.Sprintf(
"## Project info:\n🔑 key: %s\n📂 folder: %s\n👾 project git: %s\n🚀 build git: %s\n\nErrors: %v",
resp.Project.ShortName,
resp.Project.Cloud,
resp.Project.ProjectGit,
resp.Project.BuildGit,
resp.Message,
)
}
// Sending result:
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: result,
})
if err != nil {
s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: fmt.Sprintf("Something went wrong: %v", err),
})
return
}
},
}
}

View File

@ -0,0 +1,131 @@
package handler
import (
"context"
"fmt"
"log"
"strconv"
"ticket-pimp/internal/controller"
"github.com/bwmarrin/discordgo"
)
func (h *router) CreateRepoHandler(repoNameMinLength int) route {
const (
repoType = "repo_type"
projectRepo = "project_repo"
buildRepo = "build_repo"
nameOption = "repo_name"
)
return route{
Command: discordgo.ApplicationCommand{
Name: "repo",
Description: "Command for repository creation",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: repoType,
Description: "The type of repo",
Required: true,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Unity project repo",
Value: projectRepo,
},
{
Name: "XCode build repo",
Value: buildRepo,
},
},
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: nameOption,
Description: "Type the repository's name",
Required: false,
MinLength: &repoNameMinLength,
},
},
},
Handler: 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 == strconv.Itoa(1150719794853716028) {
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 = fmt.Sprintf(
"## Project info:\n🔑 key: %s\n📂 folder: %s\n👾 project git: %s\n🚀 build git: %s\n\nErrors: %v",
resp.Project.ShortName,
resp.Project.Cloud,
resp.Project.ProjectGit,
resp.Project.BuildGit,
resp.Message,
)
}
// Sending result:
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: result,
})
if err != nil {
s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: fmt.Sprintf("Something went wrong: %v", err),
})
return
}
},
}
}

View File

@ -0,0 +1,30 @@
package handler
import (
"log"
"github.com/bwmarrin/discordgo"
)
func (h *router) Ping() route {
return route{
Command: discordgo.ApplicationCommand{
Name: "ping",
Description: "pongs in a reply",
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
log.Println("ok, I'm here..")
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "`pong`",
Title: "Pong reply",
},
})
if err != nil {
log.Println(err)
}
},
}
}

View File

@ -0,0 +1,78 @@
package handler
import (
"context"
"fmt"
"log"
"ticket-pimp/internal/domain"
"github.com/bwmarrin/discordgo"
)
func (h *router) CreateTicketHandler(repoNameMinLength int) route {
return route{
Command: discordgo.ApplicationCommand{
Name: "project",
Description: "Create new development ticket",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "project_name",
Description: "Temporary project name",
Required: true,
MinLength: &repoNameMinLength,
},
},
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var result string
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
// Or convert the slice into a map
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
if option, ok := optionMap["project_name"]; ok {
dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText)
if err != nil {
result = fmt.Sprintf("chan creation problem: %v\n", err)
} else {
p, err := h.controller.ProjectCreate(context.TODO(), domain.Project{
ChannelID: dchan.ID,
})
if err != nil {
result = fmt.Sprintf("unable to create project: %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
}
}
}
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
// Ignore type for now, they will be discussed in "responses"
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: result,
},
})
},
}
}

View File

@ -1,11 +1,7 @@
package handler package handler
import ( import (
"context"
"fmt"
"log"
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
@ -21,10 +17,10 @@ func InitRouter(wc controller.WorkflowController) *router {
var r router var r router
r.Routes = append( r.Routes = append(
r.Routes, r.Routes,
// r.CreateRepoHandler(3), r.CreateRepoHandler(3),
r.CreateFolderHandler(3), r.CreateFolderHandler(3),
r.Ping(), r.Ping(),
// r.CreateTicketHandler(3), r.CreateTicketHandler(3),
) )
r.controller = wc r.controller = wc
@ -35,284 +31,3 @@ type route struct {
Command discordgo.ApplicationCommand Command discordgo.ApplicationCommand
Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) Handler func(s *discordgo.Session, i *discordgo.InteractionCreate)
} }
func (h *router) Ping() route {
return route{
Command: discordgo.ApplicationCommand{
Name: "ping",
Description: "pongs in a reply",
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
log.Println("ok, I'm here..")
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredMessageUpdate,
Data: &discordgo.InteractionResponseData{
Content: "`pong`",
Title: "Pong reply",
},
})
},
}
}
func (h *router) CreateFolderHandler(nameMinLenght int) route {
const (
nameOption string = "folder_name"
)
return route{
Command: discordgo.ApplicationCommand{
Name: "folder",
Description: "Command for cloud folder creation",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: nameOption,
Description: "Type the folder's name",
Required: false,
MinLength: &nameMinLenght,
},
},
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var result string
resp := discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Content: "Folder is going to be created..",
},
}
err := s.InteractionRespond(i.Interaction, &resp)
if err != nil {
log.Println(err)
return
}
options := i.ApplicationCommandData().Options
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
var str string = ""
project, err := h.controller.GetProjectByChannelID(context.TODO(), i.ChannelID)
if err != nil {
result = fmt.Sprintf("unable to retrieve project from db, error: %v", err)
} else {
if project == nil {
if option, ok := optionMap[nameOption]; ok {
str = option.StringValue()
} else {
str = ""
}
} else {
str = project.ShortName
}
if str == "" {
result = "Ты, либо в проекте директорию создавай, либо имя напиши, блет!"
} else {
f, err := h.controller.ICloud.CreateFolder(str)
if err != nil {
result = fmt.Sprintf("error while cloud folder creation: %v", err)
} else {
result = fmt.Sprint(f.PrivateURL)
}
}
}
// resp = discordgo.InteractionResponse{
// // Ignore type for now, they will be discussed in "responses"
// Type: discordgo.InteractionResponseUpdateMessage,
// Data: &discordgo.InteractionResponseData{
// Content: result,
// Title: "📂 Folder was created",
// },
// }
webhookEdit := discordgo.WebhookEdit{
Content: &result,
}
s.InteractionResponseEdit(i.Interaction, &webhookEdit)
// discerr := s.InteractionRespond(i.Interaction, &resp)
// if discerr != nil {
// log.Println(discerr)
// }
},
}
}
func (h *router) CreateRepoHandler(repoNameMinLength int) route {
const (
projectRepo = "project_repo"
buildRepo = "build_repo"
)
return route{
Command: discordgo.ApplicationCommand{
Name: "repo",
Description: "Command for repository creation",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "repo_type",
Description: "The type of repo",
Required: true,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Unity project repo",
Value: projectRepo,
},
{
Name: "XCode build repo",
Value: buildRepo,
},
},
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "repo_name",
Description: "Type the repository's name",
Required: false,
MinLength: &repoNameMinLength,
},
},
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var result string
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
// Or convert the slice into a map
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
var str string = ""
project, err := h.controller.GetProjectByChannelID(context.TODO(), i.ChannelID)
if err != nil {
result = fmt.Sprintf("unable to retrieve project from db, error: %v", err)
} else {
var suffix string
if option, ok := optionMap["repo_type"]; ok {
switch option.Value {
case projectRepo:
suffix = ""
case buildRepo:
suffix = "-build"
}
}
if project == nil {
if option, ok := optionMap["repo_name"]; ok {
str = option.StringValue()
} else {
str = ""
}
} else {
str = project.ShortName
}
if str == "" {
result = "Ты, либо в проекте репо создавай, либо имя напиши, блет!"
} else {
str = str + suffix
// var g *domain.Git
g, err := h.controller.IGit.CreateRepo(str)
if err != nil {
result = fmt.Sprintf("error while repo creation: %v", err)
} else {
result = "🚀 " + g.HtmlUrl + " was created"
}
}
}
resp := &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: result,
},
}
s.InteractionRespond(i.Interaction, resp)
},
}
}
func (h *router) CreateTicketHandler(repoNameMinLength int) route {
return route{
Command: discordgo.ApplicationCommand{
Name: "project",
Description: "Create new development ticket",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "project_name",
Description: "Temporary project name",
Required: true,
MinLength: &repoNameMinLength,
},
},
},
Handler: func(s *discordgo.Session, i *discordgo.InteractionCreate) {
var result string
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
// Or convert the slice into a map
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
if option, ok := optionMap["project_name"]; ok {
dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText)
if err != nil {
result = fmt.Sprintf("chan creation problem: %v\n", err)
} else {
p, err := h.controller.ProjectCreate(context.TODO(), domain.Project{
ChannelID: dchan.ID,
})
if err != nil {
result = fmt.Sprintf("unable to create project: %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
}
}
}
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
// Ignore type for now, they will be discussed in "responses"
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: result,
},
})
},
}
}

View File

@ -0,0 +1,94 @@
package controller
import (
"context"
"errors"
"fmt"
"log"
"ticket-pimp/internal/domain"
"ticket-pimp/internal/storage/db"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
type FolderRequest struct {
ChannelID string
InsertedName string
}
func (wc *WorkflowController) CreateFolder(ctx context.Context, req FolderRequest) *ProjectResponse {
project, err := wc.GetProjectByChannelID(ctx, req.ChannelID)
if err != nil {
return &ProjectResponse{
Project: nil,
Message: fmt.Errorf("unable to retrieve project from db: %v", err),
}
}
var (
name string
dbticket db.Ticket
result ProjectResponse
)
if project != nil {
switch {
case project.Cloud != "":
return &ProjectResponse{
Project: project,
Message: nil,
}
case project.ShortName != "":
name = project.ShortName
case req.InsertedName != "":
name = req.InsertedName
}
response := wc.ICloud.CreateFolder(name)
dbticket, err = wc.q.UpdateTicketFolder(
ctx,
db.UpdateTicketFolderParams{
Folder: pgtype.Text{String: response.Folder.PrivateURL, Valid: true},
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true},
Channelid: pgtype.Text{String: req.ChannelID, Valid: true},
})
if err != nil {
log.Printf("unable to scan row from db: %v", err)
return &ProjectResponse{
Project: project,
Message: fmt.Errorf("unable to update project: %v", err),
}
}
result = ProjectResponse{
Project: &domain.Project{
ID: string(dbticket.ID),
ShortName: dbticket.Key.String,
ChannelID: dbticket.Channelid.String,
ProjectGit: dbticket.ProjectGit.String,
BuildGit: dbticket.BuildGit.String,
Cloud: dbticket.Folder.String,
},
Message: response.ErrMessage,
}
} else {
if req.InsertedName != "" {
response := wc.ICloud.CreateFolder(req.InsertedName)
result = ProjectResponse{
Project: &domain.Project{
Cloud: response.Folder.PrivateURL,
},
Message: response.ErrMessage,
}
} else {
return &ProjectResponse{
Project: nil,
Message: errors.New("передано пустое имя"),
}
}
}
return &result
}

View File

@ -0,0 +1,168 @@
package controller
import (
"context"
"errors"
"fmt"
"log"
"ticket-pimp/internal/domain"
"ticket-pimp/internal/storage/db"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
type GitRequest struct {
ChannelID string
InsertedName string
IsBuildGit bool
}
func (wc *WorkflowController) createGitForExistingProject(ctx context.Context, req GitRequest, p *domain.Project) *ProjectResponse {
var (
name string = ""
dbticket db.Ticket
)
switch {
case p.ShortName != "":
name = p.ShortName
if req.IsBuildGit {
name += "-build"
}
case req.InsertedName != "":
name = req.InsertedName
if req.IsBuildGit {
name += "-build"
}
}
// response := wc.ICloud.CreateFolder(name)
git, err := wc.IGit.CreateRepo(name)
if err != nil {
return &ProjectResponse{
Project: p,
Message: fmt.Errorf("unable to create git w/ an error: %v", err),
}
}
if req.IsBuildGit {
dbticket, err = wc.q.UpdateTicketBuildGit(
ctx,
db.UpdateTicketBuildGitParams{
BuildGit: pgtype.Text{String: git.HtmlUrl, Valid: true},
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true},
Channelid: pgtype.Text{String: req.ChannelID, Valid: true},
})
if err != nil {
log.Printf("unable to scan row from db: %v", err)
return &ProjectResponse{
Project: p,
Message: fmt.Errorf("unable to update project: %v", err),
}
}
} else {
dbticket, err = wc.q.UpdateTicketProjectGit(
ctx,
db.UpdateTicketProjectGitParams{
ProjectGit: pgtype.Text{String: git.HtmlUrl, Valid: true},
UpdatedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true},
Channelid: pgtype.Text{String: req.ChannelID, Valid: true},
},
)
if err != nil {
log.Printf("unable to scan row from db: %v", err)
return &ProjectResponse{
Project: p,
Message: fmt.Errorf("unable to update project: %v", err),
}
}
}
return &ProjectResponse{
Project: &domain.Project{
ID: string(dbticket.ID),
ShortName: dbticket.Key.String,
ChannelID: dbticket.Channelid.String,
ProjectGit: dbticket.ProjectGit.String,
BuildGit: dbticket.BuildGit.String,
Cloud: dbticket.Folder.String,
},
Message: err,
}
}
func (wc *WorkflowController) CreateGit(ctx context.Context, req GitRequest) *ProjectResponse {
// [ ] Валидация на пустой канал?
p, err := wc.GetProjectByChannelID(ctx, req.ChannelID)
if err != nil {
return &ProjectResponse{
Project: nil,
Message: fmt.Errorf("unable to retrieve project from db: %v", err),
}
}
// var (
// name string
// dbticket db.Ticket
// result ProjectResponse
// )
switch {
case p != nil && req.IsBuildGit:
if p.BuildGit != "" {
return &ProjectResponse{
Project: p,
Message: errors.New("build git already exists"),
}
} else {
// [x]
return wc.createGitForExistingProject(ctx, req, p)
}
case p != nil && !req.IsBuildGit:
if p.ProjectGit != "" {
return &ProjectResponse{
Project: p,
Message: errors.New("project git already exists"),
}
} else {
// [x]
return wc.createGitForExistingProject(ctx, req, p)
}
default:
if req.InsertedName != "" {
if req.IsBuildGit {
req.InsertedName += "-build"
}
git, err := wc.IGit.CreateRepo(req.InsertedName)
if err != nil || git == nil {
return &ProjectResponse{
Project: nil,
Message: err,
}
} else {
if req.IsBuildGit {
return &ProjectResponse{
Project: &domain.Project{
BuildGit: git.HtmlUrl,
},
Message: err,
}
}
return &ProjectResponse{
Project: &domain.Project{
ProjectGit: git.HtmlUrl,
},
Message: err,
}
}
} else {
return &ProjectResponse{
Project: nil,
Message: errors.New("передано пустое имя"),
}
}
}
}

View File

@ -78,6 +78,9 @@ func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id stri
ShortName: dbTicket.Key.String, ShortName: dbTicket.Key.String,
Name: dbTicket.Key.String, Name: dbTicket.Key.String,
ChannelID: dbTicket.Channelid.String, ChannelID: dbTicket.Channelid.String,
ProjectGit: dbTicket.ProjectGit.String,
BuildGit: dbTicket.BuildGit.String,
Cloud: dbTicket.Folder.String,
} }
} }
return &proj, nil return &proj, nil

View File

@ -0,0 +1,84 @@
package controller
import (
"context"
"fmt"
"log"
"strings"
"sync"
"ticket-pimp/internal/domain"
"ticket-pimp/internal/storage/db"
"github.com/jackc/pgx/v5/pgtype"
)
func (wc *WorkflowController) FullProjectInit(name, key, id string) (string, error) {
appKey := fmt.Sprintf("%s-%s", key, id)
var (
git, gitBuild *domain.Git
cloud *domain.Folder
)
var wg sync.WaitGroup
wg.Add(3)
go func(ref **domain.Git) {
defer wg.Done()
*ref, _ = wc.IGit.CreateRepo(appKey)
}(&git)
go func(ref **domain.Git) {
defer wg.Done()
*ref, _ = wc.IGit.CreateRepo(appKey + "-build")
}(&gitBuild)
go func(ref **domain.Folder) {
defer wg.Done()
*ref = wc.ICloud.CreateFolder(appKey).Folder
}(&cloud)
wg.Wait()
var gitResult, gitBuildResult, cloudResult string
if git == nil {
gitResult = "cannot create git"
} else {
gitResult = git.HtmlUrl
}
if gitBuild == nil {
gitBuildResult = "cannot create git"
} else {
gitBuildResult = fmt.Sprintf("ssh://%s/%s.git", gitBuild.SshUrl, gitBuild.FullName)
}
if cloud == nil {
cloudResult = "cannot create folder"
} else {
cloudResult = cloud.PrivateURL
}
ctx := context.TODO()
insertedTicket, err := wc.q.CreateTicket(ctx, db.CreateTicketParams{
Key: pgtype.Text{String: appKey, Valid: true},
Channelid: pgtype.Text{},
})
if err != nil {
log.Fatal(err)
}
log.Print(insertedTicket)
wc.ICoda.CreateApp(domain.CodaApplication{
ID: appKey,
Summary: strings.TrimSpace(name),
Git: gitResult,
GitBuild: gitBuildResult,
Folder: cloudResult,
})
return appKey, nil
}

View File

@ -1,16 +1,10 @@
package controller package controller
import ( import (
"context"
"fmt"
"log"
"strings"
"sync"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services" "ticket-pimp/internal/services"
"ticket-pimp/internal/storage/db" "ticket-pimp/internal/storage/db"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@ -37,72 +31,7 @@ func NewWorkflowController(
} }
} }
func (wc *WorkflowController) FullProjectInit(name, key, id string) (string, error) { type ProjectResponse struct {
Project *domain.Project
appKey := fmt.Sprintf("%s-%s", key, id) Message error
var (
git, gitBuild *domain.Git
cloud *domain.Folder
)
var wg sync.WaitGroup
wg.Add(3)
go func(ref **domain.Git) {
defer wg.Done()
*ref, _ = wc.IGit.CreateRepo(appKey)
}(&git)
go func(ref **domain.Git) {
defer wg.Done()
*ref, _ = wc.IGit.CreateRepo(appKey + "-build")
}(&gitBuild)
go func(ref **domain.Folder) {
defer wg.Done()
*ref, _ = wc.ICloud.CreateFolder(appKey)
}(&cloud)
wg.Wait()
var gitResult, gitBuildResult, cloudResult string
if git == nil {
gitResult = "cannot create git"
} else {
gitResult = git.HtmlUrl
}
if gitBuild == nil {
gitBuildResult = "cannot create git"
} else {
gitBuildResult = fmt.Sprintf("ssh://%s/%s.git", gitBuild.SshUrl, gitBuild.FullName)
}
if cloud == nil {
cloudResult = "cannot create folder"
} else {
cloudResult = cloud.PrivateURL
}
ctx := context.TODO()
insertedTicket, err := wc.q.CreateTicket(ctx, db.CreateTicketParams{
Key: pgtype.Text{String: appKey, Valid: true},
Channelid: pgtype.Text{},
})
if err != nil {
log.Fatal(err)
}
log.Print(insertedTicket)
wc.ICoda.CreateApp(domain.CodaApplication{
ID: appKey,
Summary: strings.TrimSpace(name),
Git: gitResult,
GitBuild: gitBuildResult,
Folder: cloudResult,
})
return appKey, nil
} }

View File

@ -1,22 +0,0 @@
package controller
import (
"context"
"ticket-pimp/internal/domain"
db "ticket-pimp/internal/storage/db"
)
type IConfigController interface {
Get(context.Context) (domain.ApplicationConfig, error)
Update(context.Context) (domain.ApplicationConfig, error)
}
type AppConfig struct {
db *db.Queries
}
func NewAppConfig(db *db.Queries) AppConfig {
return AppConfig{
db: db,
}
}

View File

@ -87,6 +87,10 @@ type Project struct {
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
Name string `json:"name"` Name string `json:"name"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
ProjectGit string `json:"project_git"`
BuildGit string `json:"build_git"`
Cloud string `json:"cloud"`
} }
type ProjectID struct { type ProjectID struct {

View File

@ -0,0 +1,5 @@
package helpers
type ErrorMessage struct {
Message string `json:"message"`
}

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
) )
func GitNaming(input string) string { func ValidNaming(input string) string {
// Remove leading and trailing whitespace // Remove leading and trailing whitespace
input = strings.TrimSpace(input) input = strings.TrimSpace(input)

View File

@ -18,7 +18,7 @@ var tests = []test{
func TestGitNaming(t *testing.T) { func TestGitNaming(t *testing.T) {
for _, test := range tests { for _, test := range tests {
if output := GitNaming(test.arg); output != test.expected { if output := ValidNaming(test.arg); output != test.expected {
t.Errorf("Output %q not equal to expected %q", output, test.expected) t.Errorf("Output %q not equal to expected %q", output, test.expected)
} }
} }

View File

@ -1,8 +1,11 @@
package services package services
import ( import (
"errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/helpers" "ticket-pimp/internal/helpers"
"time" "time"
@ -14,7 +17,7 @@ type Cloud struct {
} }
type ICloud interface { type ICloud interface {
CreateFolder(name string) (*domain.Folder, error) CreateFolder(name string) Response
} }
func NewCloud(conf domain.CloudConfig) *Cloud { func NewCloud(conf domain.CloudConfig) *Cloud {
@ -32,40 +35,76 @@ func NewCloud(conf domain.CloudConfig) *Cloud {
} }
} }
func (c *Cloud) CreateFolder(name string) (*domain.Folder, error) { type Response struct {
Folder *domain.Folder
ErrMessage error
}
func (c *Cloud) CreateFolder(name string) Response {
var R Response
rootDir := c.Config.RootDir rootDir := c.Config.RootDir
user := c.Config.User user := c.Config.User
davPath := "/remote.php/dav/files/" davPath := "/remote.php/dav/files/"
parentPath := "/apps/files/?dir=" parentPath := "/apps/files/?dir="
name = helpers.GitNaming(name) name = helpers.ValidNaming(name)
cloud := domain.Folder{ R.Folder = &domain.Folder{
Title: name, Title: name,
PrivateURL: "", PrivateURL: "",
} }
// cloud := domain.Folder{
// Title: name,
// PrivateURL: "",
// }
requestPath := davPath + user + rootDir + name requestPath := davPath + user + rootDir + name
cloud.PathTo = parentPath + rootDir + name R.Folder.PathTo = parentPath + rootDir + name
resp, _ := c.R(). var errMessage helpers.ErrorMessage
resp, err := c.R().
SetErrorResult(&errMessage).
Send("MKCOL", requestPath) Send("MKCOL", requestPath)
if err != nil { // Error handling.
log.Println("error:", err)
// Херовая обработка ошибки:
// error while cloud folder creation: bad response, raw content:
// <?xml version="1.0" encoding="utf-8"?>
// <d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
// <s:exception>Sabre\DAV\Exception\MethodNotAllowed</s:exception>
// <s:message>The resource you tried to create already exists</s:message>
// </d:error>
if strings.Contains(err.Error(), "already exists") {
R.Folder.PrivateURL = c.BaseURL + R.Folder.PathTo
R.ErrMessage = errors.New("guess, that folder already exists")
// Try to set short URL to the d entity
if err := c.setPrivateURL(requestPath, R.Folder); err != nil {
R.ErrMessage = err
return R
}
return R
}
return R
}
if resp.IsSuccessState() { if resp.IsSuccessState() {
// Set stupid URL to the d entity // Set stupid URL to the d entity
cloud.PrivateURL = c.BaseURL + cloud.PathTo R.Folder.PrivateURL = c.BaseURL + R.Folder.PathTo
// Try to set short URL to the d entity // Try to set short URL to the d entity
if err := c.setPrivateURL(requestPath, &cloud); err != nil { if err := c.setPrivateURL(requestPath, R.Folder); err != nil {
return &cloud, err return R
} }
} else {
fmt.Println(resp.Status)
} }
return &cloud, nil return R
} }
func (c *Cloud) setPrivateURL(requestPath string, cloud *domain.Folder) error { func (c *Cloud) setPrivateURL(requestPath string, cloud *domain.Folder) error {

View File

@ -55,7 +55,7 @@ type gitCreateRequest struct {
} }
func (gb *Git) newRepo(name string) (*domain.Git, error) { func (gb *Git) newRepo(name string) (*domain.Git, error) {
name = helpers.GitNaming(name) name = helpers.ValidNaming(name)
payload := gitCreateRequest{ payload := gitCreateRequest{
Name: name, Name: name,

View File

@ -0,0 +1,307 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.23.0
// source: queries.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createTicket = `-- name: CreateTicket :one
INSERT INTO tickets (
key, channelID
) VALUES (
$1, $2
)
RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at
`
type CreateTicketParams struct {
Key pgtype.Text
Channelid pgtype.Text
}
func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) {
row := q.db.QueryRow(ctx, createTicket, arg.Key, arg.Channelid)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&i.CreatedAt,
&i.DeletedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteTicketByID = `-- name: DeleteTicketByID :exec
UPDATE tickets SET deleted_at = current_timestamp WHERE id = $1
`
func (q *Queries) DeleteTicketByID(ctx context.Context, id int32) error {
_, err := q.db.Exec(ctx, deleteTicketByID, id)
return err
}
const deleteTicketByKey = `-- name: DeleteTicketByKey :exec
UPDATE tickets SET deleted_at = current_timestamp WHERE key = $1
`
func (q *Queries) DeleteTicketByKey(ctx context.Context, key pgtype.Text) error {
_, err := q.db.Exec(ctx, deleteTicketByKey, key)
return err
}
const getConfig = `-- name: GetConfig :one
SELECT ticket_key, ticket_id
FROM appconfig
`
func (q *Queries) GetConfig(ctx context.Context) (Appconfig, error) {
row := q.db.QueryRow(ctx, getConfig)
var i Appconfig
err := row.Scan(&i.TicketKey, &i.TicketID)
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
`
func (q *Queries) GetTicketByChannelID(ctx context.Context, channelid pgtype.Text) (Ticket, error) {
row := q.db.QueryRow(ctx, getTicketByChannelID, channelid)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&i.CreatedAt,
&i.DeletedAt,
&i.UpdatedAt,
)
return i, err
}
const getTicketByID = `-- name: GetTicketByID :one
SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets WHERE id = $1
`
func (q *Queries) GetTicketByID(ctx context.Context, id int32) (Ticket, error) {
row := q.db.QueryRow(ctx, getTicketByID, id)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&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
`
func (q *Queries) ListTickets(ctx context.Context) ([]Ticket, error) {
rows, err := q.db.Query(ctx, listTickets)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Ticket
for rows.Next() {
var i Ticket
if err := rows.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&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 listTicketsWithDeleted = `-- name: ListTicketsWithDeleted :many
SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets
`
func (q *Queries) ListTicketsWithDeleted(ctx context.Context) ([]Ticket, error) {
rows, err := q.db.Query(ctx, listTicketsWithDeleted)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Ticket
for rows.Next() {
var i Ticket
if err := rows.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&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 setNewConfig = `-- name: SetNewConfig :one
UPDATE appconfig
SET ticket_id = ticket_id + 1
RETURNING ticket_key, ticket_id
`
func (q *Queries) SetNewConfig(ctx context.Context) (Appconfig, error) {
row := q.db.QueryRow(ctx, setNewConfig)
var i Appconfig
err := row.Scan(&i.TicketKey, &i.TicketID)
return i, err
}
const updateTicketBuildGit = `-- name: UpdateTicketBuildGit :one
UPDATE tickets
SET build_git = $1, updated_at = $2
WHERE channelID = $3
RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at
`
type UpdateTicketBuildGitParams struct {
BuildGit pgtype.Text
UpdatedAt pgtype.Timestamptz
Channelid pgtype.Text
}
func (q *Queries) UpdateTicketBuildGit(ctx context.Context, arg UpdateTicketBuildGitParams) (Ticket, error) {
row := q.db.QueryRow(ctx, updateTicketBuildGit, arg.BuildGit, arg.UpdatedAt, arg.Channelid)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&i.CreatedAt,
&i.DeletedAt,
&i.UpdatedAt,
)
return i, err
}
const updateTicketByID = `-- name: UpdateTicketByID :exec
UPDATE tickets SET project_git = $1, build_git = $2, folder = $3 WHERE id = $4
`
type UpdateTicketByIDParams struct {
ProjectGit pgtype.Text
BuildGit pgtype.Text
Folder pgtype.Text
ID int32
}
func (q *Queries) UpdateTicketByID(ctx context.Context, arg UpdateTicketByIDParams) error {
_, err := q.db.Exec(ctx, updateTicketByID,
arg.ProjectGit,
arg.BuildGit,
arg.Folder,
arg.ID,
)
return err
}
const updateTicketFolder = `-- name: UpdateTicketFolder :one
UPDATE tickets
SET folder = $1, updated_at = $2
WHERE channelID = $3
RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at
`
type UpdateTicketFolderParams struct {
Folder pgtype.Text
UpdatedAt pgtype.Timestamptz
Channelid pgtype.Text
}
func (q *Queries) UpdateTicketFolder(ctx context.Context, arg UpdateTicketFolderParams) (Ticket, error) {
row := q.db.QueryRow(ctx, updateTicketFolder, arg.Folder, arg.UpdatedAt, arg.Channelid)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&i.CreatedAt,
&i.DeletedAt,
&i.UpdatedAt,
)
return i, err
}
const updateTicketProjectGit = `-- name: UpdateTicketProjectGit :one
UPDATE tickets
SET project_git = $1, updated_at = $2
WHERE channelID = $3
RETURNING id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at
`
type UpdateTicketProjectGitParams struct {
ProjectGit pgtype.Text
UpdatedAt pgtype.Timestamptz
Channelid pgtype.Text
}
func (q *Queries) UpdateTicketProjectGit(ctx context.Context, arg UpdateTicketProjectGitParams) (Ticket, error) {
row := q.db.QueryRow(ctx, updateTicketProjectGit, arg.ProjectGit, arg.UpdatedAt, arg.Channelid)
var i Ticket
err := row.Scan(
&i.ID,
&i.Key,
&i.Channelid,
&i.ProjectGit,
&i.BuildGit,
&i.Folder,
&i.CreatedAt,
&i.DeletedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -15,6 +15,24 @@ INSERT INTO tickets (
) )
RETURNING *; RETURNING *;
-- name: UpdateTicketFolder :one
UPDATE tickets
SET folder = $1, updated_at = $2
WHERE channelID = $3
RETURNING *;
-- name: UpdateTicketProjectGit :one
UPDATE tickets
SET project_git = $1, updated_at = $2
WHERE channelID = $3
RETURNING *;
-- name: UpdateTicketBuildGit :one
UPDATE tickets
SET build_git = $1, updated_at = $2
WHERE channelID = $3
RETURNING *;
-- name: ListTickets :many -- name: ListTickets :many
SELECT * FROM tickets WHERE deleted_at IS NULL; SELECT * FROM tickets WHERE deleted_at IS NULL;

View File

@ -17,10 +17,10 @@ func (h *Handler) NewFolderHandler(ctx context.Context, mu *tgb.MessageUpdate) e
return errors.New("empty command provided") return errors.New("empty command provided")
} }
cloud, err := h.cloud.CreateFolder(str) resp := h.cloud.CreateFolder(str)
if err != nil { if resp.ErrMessage != nil {
answer := errorAnswer(err.Error()) answer := errorAnswer(resp.ErrMessage.Error())
h.LogMessage(ctx, mu, answer) h.LogMessage(ctx, mu, answer)
return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx)
} }
@ -28,7 +28,7 @@ func (h *Handler) NewFolderHandler(ctx context.Context, mu *tgb.MessageUpdate) e
answer := tg.HTML.Text( answer := tg.HTML.Text(
tg.HTML.Line( tg.HTML.Line(
"✨ Shiny folder", "✨ Shiny folder",
tg.HTML.Link(cloud.Title, cloud.PrivateURL), tg.HTML.Link(resp.Folder.Title, resp.Folder.PrivateURL),
"has been created!", "has been created!",
), ),
) )

View File

@ -45,6 +45,7 @@ func Run(ctx context.Context, opts TelegramOptions) error {
// Message(h.NewFolderHandler, tgb.TextHasPrefix("/folder")). // Message(h.NewFolderHandler, tgb.TextHasPrefix("/folder")).
Message(h.FarmTaskHandler, tgb.TextHasPrefix("/task")) Message(h.FarmTaskHandler, tgb.TextHasPrefix("/task"))
log.Print("Success init. Start poller.")
return tgb.NewPoller( return tgb.NewPoller(
router, router,
client, client,