diff --git a/.gitignore b/.gitignore index 8d60935..22ed4fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode/ -**/**/*.env \ No newline at end of file +**/**/*.env +docker/** \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index f69a1c9..3002884 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,9 @@ RUN apk add git RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags '-extldflags "-static"' -tags timetzdata ./cmd/main.go FROM scratch -# the test program: 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/bot/handler/application.go b/bot/handler/application.go deleted file mode 100644 index e9fb9ab..0000000 --- a/bot/handler/application.go +++ /dev/null @@ -1,48 +0,0 @@ -package handler - -import ( - "context" - "errors" - "strconv" - "strings" - - "github.com/mr-linch/go-tg" - "github.com/mr-linch/go-tg/tgb" -) - -func (h *Handler) DevelopmentTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - str := strings.Replace(mu.Text, "/new", "", 1) - - if str == "" { - return errors.New("empty command provided") - } - - issueKeyStr, err := h.workflow.Workflow(str, h.key, h.id) - - if err != nil { - answer := errorAnswer(err.Error()) - h.LogMessage(ctx, mu, answer) - return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) - } - - i, err := strconv.Atoi(h.id) - if err != nil { - return errors.New("problem with conversion id to int") - } - h.id = strconv.Itoa(i + 1) - - answer := newTicketAnswer(issueKeyStr) - h.LogMessage(ctx, mu, answer) - return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) -} - -func newTicketAnswer(name string) string { - return tg.HTML.Text( - tg.HTML.Line( - "🤘 Ticket ", - name, - " has been created!", - ), - ) -} diff --git a/bot/handler/farmtask.go b/bot/handler/farmtask.go deleted file mode 100644 index 87e5e35..0000000 --- a/bot/handler/farmtask.go +++ /dev/null @@ -1,71 +0,0 @@ -package handler - -import ( - "context" - "fmt" - "log" - "strings" - "ticket-pimp/internal/domain" - - "github.com/mr-linch/go-tg" - "github.com/mr-linch/go-tg/tgb" -) - -func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - msgID := mu.Message.ID - - taskText := strings.TrimSpace(strings.Replace(mu.Text, "/task", "", 1)) - - var summaryTail string - - sentances := strings.Split(taskText, "\n") - if len(sentances) < 2 { - words := strings.Split(taskText, " ") - if len(words) > 2 { - summaryTail = strings.Join(words[0:2], " ") - } else { - summaryTail = strings.Join(words, " ") - } - } else { - summaryTail = sentances[0] - } - - t := domain.NewTask( - summaryTail, - taskText, - mu.From.Username.PeerID(), - 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 = 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) -} diff --git a/bot/handler/handler.go b/bot/handler/handler.go deleted file mode 100644 index d6c74aa..0000000 --- a/bot/handler/handler.go +++ /dev/null @@ -1,29 +0,0 @@ -package handler - -import ( - "ticket-pimp/bot/controller" - "ticket-pimp/internal/services" -) - -type Handler struct { - workflow controller.IWorkflowController - git services.IGit - cloud services.ICloud - coda services.ICoda - key string - id string -} - -func NewHandler( - git services.IGit, - cloud services.ICloud, - coda services.ICoda, -) *Handler { - - return &Handler{ - workflow: controller.NewWorkflowController(git, cloud, coda), - git: git, - cloud: cloud, - coda: coda, - } -} diff --git a/bot/handler/handler_test.go b/bot/handler/handler_test.go deleted file mode 100644 index d871058..0000000 --- a/bot/handler/handler_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package handler - -import ( - "testing" - "ticket-pimp/internal/domain" -) - -type test struct { - arg domain.Git - expected string -} - -var tests = []test{ - {domain.Git{ - Name: "text", - FullName: "", - Private: false, - Url: "", - CloneUrl: "", - HtmlUrl: "https://reddit.com/", - SshUrl: "", - }, "Repo text has been created!"}, -} - -func TestPrepareAnswer(t *testing.T) { - - for _, test := range tests { - g := newGit(&test.arg) - - if output := g.PrepareAnswer(); output != test.expected { - t.Errorf("Output %q not equal to expected %q", output, test.expected) - } - } -} diff --git a/client/discord/discord.go b/client/discord/discord.go new file mode 100644 index 0000000..5e2d06f --- /dev/null +++ b/client/discord/discord.go @@ -0,0 +1,134 @@ +package discord + +import ( + "errors" + "fmt" + "log" + "os" + "os/signal" + "ticket-pimp/client/discord/handler" + "ticket-pimp/internal/controller" + "ticket-pimp/internal/domain" + + "github.com/bwmarrin/discordgo" +) + +func initBotWith(token string) *discordgo.Session { + discord, err := discordgo.New("Bot " + token) + if err != nil { + log.Fatalf("unable to create discord session: %v", err) + } + + return discord +} + +type DiscordOptions struct { + Config *domain.Config + Controller *controller.WorkflowController +} + +func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) error { + dchan, err := s.Channel(i.ChannelID) + if err != nil { + return err + } + + if dchan.Type == discordgo.ChannelTypeDM { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Yo, fella! I'm not working in private!", + }, + }) + return errors.New("no private messages! lol") + } + + return nil +} + +func Run(conf domain.Config, opts DiscordOptions) error { + token := conf.Discord.Token + + s := initBotWith(token) + + router := handler.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) + + commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} + 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[0].Handler, + } + + s.AddHandler(router.ListenPosts) + + s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + err := checkPrivateMessaging(s, i) + if err != nil { + return + } + + switch i.Type { + case discordgo.InteractionApplicationCommand: + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + case discordgo.InteractionMessageComponent: + if h, ok := componentsHandlers[i.MessageComponentData().CustomID]; ok { + h(s, i) + } + } + }) + + if err := s.Open(); err != nil { + return fmt.Errorf("cannot open the session: %v", err) + } + + // UPDATE FORUM IF NEEDED: + + // forum, err := session.Channel(os.Getenv("TASKS_CHANNEL")) + forum, err := s.Channel(conf.Discord.IsProjectChannel) + if err != nil { + log.Print(err) + } + + _, err = s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ + AvailableTags: &router.Tags, + }) + if err != nil { + log.Fatal(err) + } + + log.Println("Adding commands...") + var cmds []*discordgo.ApplicationCommand + var logString []string + for _, h := range router.Commands { + cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "1103928338898235462", &h.Command) + if err != nil { + log.Panicf("Cannot create '%v' command: %v", h.Command.Name, err) + } + cmds = append(cmds, cmd) + logString = append(logString, cmd.Name) + } + + log.Println("Following commands added:") + log.Println(logString) + + defer s.Close() + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + <-stop + log.Println("Graceful shutdown") + + log.Println("Removing commands...") + for _, h := range cmds { + err := s.ApplicationCommandDelete(s.State.User.ID, "", h.ID) + if err != nil { + log.Panicf("Cannot delete '%v' command: %v", h.Name, err) + } + } + return nil +} diff --git a/client/discord/handler/handle_external_task.go b/client/discord/handler/handle_external_task.go new file mode 100644 index 0000000..b41dd5c --- /dev/null +++ b/client/discord/handler/handle_external_task.go @@ -0,0 +1,249 @@ +package handler + +import ( + "context" + "fmt" + "log" + "ticket-pimp/internal/domain" + + "github.com/bwmarrin/discordgo" + "github.com/imroc/req/v3" +) + +func (c *client) setFlag(s *discordgo.Session, i *discordgo.InteractionCreate, tag *discordgo.ForumTag) error { + + th, err := s.Channel(i.ChannelID) + if err != nil { + return err + } + + forum, err := s.Channel(th.ParentID) + if err != nil { + return err + } + + // Проверка на существование тега в списке тегов: + if len(forum.AvailableTags) != 0 { + for _, some := range forum.AvailableTags { + if some.Name == tag.Name { + _, err := s.ChannelEditComplex(i.ChannelID, &discordgo.ChannelEdit{ + AppliedTags: &[]string{some.ID}, + }) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (c *client) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate) { + + // Check if thread starter is not a bot, and thread started at the tasks channel; + if th.ParentID != c.conf.IsTaskForum || th.OwnerID == s.State.User.ID { + return + } + + msgs, _ := s.ChannelMessages(th.ID, 1, "", "", "") + + msg, _ := s.ChannelMessage(th.ID, msgs[0].ID) + + if msg.Author.ID == s.State.User.ID { + return + } + + content := th.Name + content += "\n" + msg.Content + + user, _ := s.GuildMember(th.GuildID, msg.Author.ID) + + t, err := c.controller.WriteTaskToDB(&domain.Task{ + Description: content, + Creator: user.User.Mention(), + }) + if err != nil { + s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to write task to db, %v", err)) + return + } + + // [x] -- Отредактировать Thread name как для задачи + _, err = s.ChannelEditComplex(th.ID, &discordgo.ChannelEdit{ + Name: fmt.Sprintf("Task ID: %d, by %s", t.ID, t.Creator), + }) + if err != nil { + log.Printf("th edition is not complete: %v", err) + } + + // Fix the original task message: + taskMessage, err := s.ChannelMessageSendComplex(th.ID, &discordgo.MessageSend{ + Content: content, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Start", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "task_start", + }, + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: true, + CustomID: "task_close", + }, + }, + }, + }, + }) + if err != nil { + log.Printf("th start message edition is not complete: %v", err) + } + + err = c.controller.UpdateTasksMessageID(context.TODO(), taskMessage.ID, t.ID) + if err != nil { + s.ChannelMessageSend(th.ID, fmt.Sprintf("unable to update task at the db, %v", err)) + return + } +} + +func (c *client) HandleTaskButtons() Component { + return Component{ + Handler: c.handleTaskButton, + } +} + +func (c *client) handleTaskButton(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Send an empty interaction response; ---------------------------------------------------------------- + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{}, + }) + + // Get assignee value; --------------------------------------------------------------------------------- + user := i.Member.User.Mention() + + var ( + opt int = -1 + doneButtonIsDisabled bool = false + state domain.TaskState = domain.NewTaskState() + message string + ) + + // Check what flow was touched: ------------------------------------------------------------------------- + switch i.Interaction.MessageComponentData().CustomID { + case "task_start": + opt = 0 + doneButtonIsDisabled = false + state = domain.InrpogressTaskState() + message = "взята в работу" + case "task_close": + opt = 1 + doneButtonIsDisabled = true + state = domain.DoneTaskState() + message = "выполнена" + } + + // Send the task update to db -------------------------------------------------------------------------- + convertable, err := c.controller.UpdateTask(i.Message.ID, opt, user) + if err != nil { + s.ChannelMessageSend(i.ChannelID, fmt.Sprintf("Unable to update task at the db w/ error: %v", err)) + return + } + + // Map DB's response to domain.Task: ------------------------------------------------------------------- + task := convertable. + ExtractDomain() + + newContent := task.DiscordMessage(state) + + // Send message to the creator in Telegram: ------------------------------------------------------------- + + if task.CreatorLink != "" { + c.sendTelegramMessageToCreator( + task.CreatorLink, + fmt.Sprintf("Task ID: %d %s", task.ID, message)) + } + + // Send a message to the thread about the task was started: --------------------------------------------- + _, err = s.ChannelMessageSendComplex(i.ChannelID, &discordgo.MessageSend{ + Content: newContent, + }) + if err != nil { + log.Printf("error while sending start task message: %v", err) + } + + // Fix the original task message: ---------------------------------------------------------------------- + _, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: i.ChannelID, + ID: i.Message.ID, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: doneButtonIsDisabled, + CustomID: "task_close", + }, + }, + }, + }, + }) + if err != nil { + log.Printf("th start message edition is not complete: %v", err) + } + + // Устанавливаем тэги статуса на тред --------------------------------------------------------------------- + err = c.setFlag(s, i, &c.Tags[opt]) + if err != nil { + log.Printf("error while `start` tag setting: %v", err) + } +} + +type TelegramMessage struct { + ChatID string `json:"chat_id"` + Text string `json:"text"` + DisableNotification bool `json:"disable_notification"` + ParseMode string `json:"parse_mode"` + DisablePreview bool `json:"disable_web_page_preview"` +} + +func (c *client) sendTelegramMessageToCreator(tgChatID string, text string) { + + http := req.C() + http.R(). + SetBody(&TelegramMessage{ + ChatID: tgChatID, + Text: text, + DisableNotification: true, + ParseMode: "HTML", + DisablePreview: true, + }). + Post("https://api.telegram.org/bot" + c.tgConf.Token + "/sendMessage") + // [HTTP Kit Marlerino]::POST( + // "https://api.telegram.org/bot" + Config.botToken + + // "/sendMessage", + // "", + // Object( + // "chat_id", + // thisRow.[Creator ID].ToText(), + // "text", + // Format( + // "{2} взята в работу", + // thisRow.ObjectLink().ToText(), + // "Задача" + // ), + // "disable_notification", + // true, + // "parse_mode", + // "HTML", + // "disable_web_page_preview", + // true + // ) + // ) + +} diff --git a/client/discord/handler/handle_folder.go b/client/discord/handler/handle_folder.go new file mode 100644 index 0000000..ab7a6e7 --- /dev/null +++ b/client/discord/handler/handle_folder.go @@ -0,0 +1,97 @@ +package handler + +import ( + "context" + "log" + "ticket-pimp/internal/controller" + + "github.com/bwmarrin/discordgo" +) + +func (c *client) CreateFolderHandler(nameMinLenght int) Command { + const ( + nameOption string = "folder_name" + ) + return Command{ + + Command: discordgo.ApplicationCommand{ + Name: "folder", + Description: "Command for cloud folder creation", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the folder's name", + Required: false, + MinLength: &nameMinLenght, + }, + }, + }, + + Handler: c.createFolderHandler, + } +} + +func (c *client) createFolderHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + const ( + nameOption string = "folder_name" + ) + + // Моментальный ответ для избежания столкновения с протуханием токена + 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 new file mode 100644 index 0000000..f41ff60 --- /dev/null +++ b/client/discord/handler/handle_git.go @@ -0,0 +1,127 @@ +package handler + +import ( + "context" + "log" + "ticket-pimp/internal/controller" + + "github.com/bwmarrin/discordgo" +) + +func (c *client) CreateRepoHandler(repoNameMinLength int) Command { + const ( + repoType = "repo_type" + projectRepo = "project_repo" + buildRepo = "build_repo" + nameOption = "repo_name" + ) + + return Command{ + Command: discordgo.ApplicationCommand{ + Name: "repo", + Description: "Creates repository of selected type. Name used for projects channels only", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: repoType, + Description: "The type of repo", + Required: true, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Unity project repo", + Value: projectRepo, + }, + { + Name: "XCode build repo", + Value: buildRepo, + }, + }, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: nameOption, + Description: "Type the repository's name", + Required: false, + MinLength: &repoNameMinLength, + }, + }, + }, + Handler: c.createRepoHandler, + } +} + +func (c *client) createRepoHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + const ( + repoType = "repo_type" + projectRepo = "project_repo" + buildRepo = "build_repo" + nameOption = "repo_name" + ) + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + // Определение переменной для ответа + var result string = "unexpected result" + + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + // Creating request: + var req controller.GitRequest + name, insertedValueNotNil := optionMap[nameOption] + isBuild := optionMap[repoType] + switch isBuild.StringValue() { + case buildRepo: + req.IsBuildGit = true + case projectRepo: + req.IsBuildGit = false + } + dchan, err := s.Channel(i.ChannelID) + + if err != nil { + log.Printf("error while identifying channel: %v", err) + } else { + + if dchan.ParentID == c.conf.IsProjectChannel { + req.ChannelID = dchan.ID + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + } else { + req.ChannelID = "" + if insertedValueNotNil { + req.InsertedName = name.StringValue() + } + } + } + + // Making request: + resp := c.controller.CreateGit(context.TODO(), req) + if resp.Project == nil { + if resp.Message != nil { + result = resp.Message.Error() + } + } else { + + result = resp.Project.DiscordString() + if resp.Message != nil { + result += "Errors: " + resp.Message.Error() + } + + } + c.defaultFollowUp(result, s, i) +} diff --git a/client/discord/handler/handle_ping.go b/client/discord/handler/handle_ping.go new file mode 100644 index 0000000..4f91795 --- /dev/null +++ b/client/discord/handler/handle_ping.go @@ -0,0 +1,30 @@ +package handler + +import ( + "log" + + "github.com/bwmarrin/discordgo" +) + +func (c *client) Ping() Command { + return Command{ + Command: discordgo.ApplicationCommand{ + Name: "ping", + Description: "pongs in a reply", + }, + Handler: c.ping, + } +} + +func (c *client) ping(s *discordgo.Session, i *discordgo.InteractionCreate) { + + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Pong to: " + i.Member.User.Mention(), + }, + }) + if err != nil { + log.Println(err) + } +} diff --git a/client/discord/handler/handle_ticket.go b/client/discord/handler/handle_ticket.go new file mode 100644 index 0000000..f904e5d --- /dev/null +++ b/client/discord/handler/handle_ticket.go @@ -0,0 +1,220 @@ +package handler + +import ( + "context" + "fmt" + "log" + "ticket-pimp/internal/domain" + + "github.com/bwmarrin/discordgo" +) + +func (c *client) GetInfo() Command { + return Command{ + Command: discordgo.ApplicationCommand{ + Name: "info", + Description: "Get project's info", + }, + Handler: c.getInfo, + } +} + +func (c *client) getInfo(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + var result string + + // Get channel from the request + dchan, err := s.Channel(i.ChannelID) + if err != nil { + result = "unable to get channel from the message" + } else { + project, err := c.controller.GetProjectByChannelID(context.TODO(), dchan.ID) + if err != nil { + result = err.Error() + } else { + if project != nil { + result = project.DiscordString() + if err != nil { + result += "Errors: " + err.Error() + } + } else { + result = "Something wrong with retrieving project from db" + } + + } + } + + c.defaultFollowUp(result, s, i) +} + +func (c *client) InitProjectFromChannel(minLength int) Command { + return Command{ + Command: discordgo.ApplicationCommand{ + Name: "init_project", + Description: "Connect project with Coda ID", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "key", + Description: "Project's key from Coda.io", + Required: true, + MinLength: &minLength, + }, + }, + }, + Handler: c.initProjectFromChannel, + } +} + +func (c *client) initProjectFromChannel(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + 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", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "project_name", + Description: "Temporary project name", + Required: true, + MinLength: &repoNameMinLength, + }, + }, + }, + Handler: c.createTicketHandler, + } +} + +func (c *client) createTicketHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Моментальный ответ для избежания столкновения с протуханием токена + initialResponse := discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "👩‍🍳 Cooking your query..", + }, + } + + s.InteractionRespond(i.Interaction, &initialResponse) + + var result string + // Access options in the order provided by the user. + options := i.ApplicationCommandData().Options + + // Or convert the slice into a map + optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options)) + for _, opt := range options { + optionMap[opt.Name] = opt + } + + if option, ok := optionMap["project_name"]; ok { + dchan, err := s.GuildChannelCreate(i.GuildID, option.StringValue(), discordgo.ChannelTypeGuildText) + if err != nil { + result = fmt.Sprintf("chan creation problem: %v\n", err) + } else { + p, err := c.controller.ProjectCreate(context.TODO(), domain.Project{ + ChannelID: dchan.ID, + }) + if err != nil { + result = fmt.Sprintf("unable to create project: %v\n", err) + _, err := s.ChannelDelete(dchan.ID) + if err != nil { + result += fmt.Sprintf("\nunable to clean channel: %v\n", err) + } + } else { + edit := discordgo.ChannelEdit{ + Name: p.ShortName, + ParentID: "1150719794853716028", + } + + dchan, err = s.ChannelEdit(dchan.ID, &edit) + if err != nil { + result = fmt.Sprintf("channel %s created, but unable to edit follow up message: %v\n", p.ShortName, err) + + } else { + _, err = s.ChannelMessageSend(dchan.ID, "Hello!") + if err != nil { + log.Printf("message send problem: %v\n", err) + } + result = "Project " + p.ShortName + "Was created" + } + } + } + } + + c.defaultFollowUp(result, s, i) +} diff --git a/client/discord/handler/handler.go b/client/discord/handler/handler.go new file mode 100644 index 0000000..cb64fc3 --- /dev/null +++ b/client/discord/handler/handler.go @@ -0,0 +1,85 @@ +package handler + +import ( + "fmt" + "ticket-pimp/internal/controller" + "ticket-pimp/internal/domain" + + "github.com/bwmarrin/discordgo" +) + +type client struct { + Commands []Command + Components []Component + + Tags []discordgo.ForumTag + + controller controller.WorkflowController + conf *domain.DiscordConfig + tgConf *domain.TelegramConfig +} + +// Подключение роутов к Discord боту +func InitRouter(wc controller.WorkflowController, conf *domain.DiscordConfig, tgConf *domain.TelegramConfig) *client { + + var r client + r.controller = wc + r.conf = conf + + r.Commands = append(r.Commands, + r.CreateRepoHandler(3), + r.CreateFolderHandler(3), + r.Ping(), + r.CreateTicketHandler(3), + r.InitProjectFromChannel(3), + r.GetInfo(), + ) + r.Components = append(r.Components, + r.HandleTaskButtons(), + ) + + r.Tags = append( + r.Tags, + discordgo.ForumTag{ + Name: "В работе", + Moderated: true, + EmojiName: "👩‍🍳", + }, + + discordgo.ForumTag{ + Name: "Готово", + Moderated: true, + EmojiName: "✅", + }) + r.tgConf = tgConf + + return &r +} + +// +// Подключение роутов к Discord боту + +type Command struct { + Command discordgo.ApplicationCommand + Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) +} + +type Component struct { + Component discordgo.MessageComponent + Handler func(s *discordgo.Session, i *discordgo.InteractionCreate) +} + +func (h *client) defaultFollowUp(answer string, s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Sending result: + _, err := s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: answer, + }) + + if err != nil { + s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{ + Content: fmt.Sprintf("Something went wrong: %v", err), + }) + return + } +} diff --git a/client/telegram/handler/handle_application.go b/client/telegram/handler/handle_application.go new file mode 100644 index 0000000..a24f673 --- /dev/null +++ b/client/telegram/handler/handle_application.go @@ -0,0 +1,38 @@ +package handler + +// func (h *Handler) DevelopmentTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { + +// str := strings.Replace(mu.Text, "/new", "", 1) + +// if str == "" { +// return errors.New("empty command provided") +// } + +// issueKeyStr, err := h.workflow.Workflow(str, h.key, h.id) + +// if err != nil { +// answer := errorAnswer(err.Error()) +// h.LogMessage(ctx, mu, answer) +// return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) +// } + +// i, err := strconv.Atoi(h.id) +// if err != nil { +// return errors.New("problem with conversion id to int") +// } +// h.id = strconv.Itoa(i + 1) + +// answer := newTicketAnswer(issueKeyStr) +// h.LogMessage(ctx, mu, answer) +// return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) +// } + +// func newTicketAnswer(name string) string { +// return tg.HTML.Text( +// tg.HTML.Line( +// "🤘 Ticket ", +// name, +// " has been created!", +// ), +// ) +// } diff --git a/client/telegram/handler/handle_farmtask.go b/client/telegram/handler/handle_farmtask.go new file mode 100644 index 0000000..128c120 --- /dev/null +++ b/client/telegram/handler/handle_farmtask.go @@ -0,0 +1,92 @@ +package handler + +import ( + "context" + "strconv" + "strings" + "ticket-pimp/internal/domain" + + "github.com/mr-linch/go-tg" + "github.com/mr-linch/go-tg/tgb" +) + +func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { + + var ( + taskText string = "" + answer string = "" + ) + + 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 + + sentances := strings.Split(taskText, "\n") + if len(sentances) < 2 { + words := strings.Split(taskText, " ") + if len(words) > 2 { + summaryTail = strings.Join(words[0:2], " ") + } else { + summaryTail = strings.Join(words, " ") + } + } else { + summaryTail = sentances[0] + } + + t := domain.NewTask( + summaryTail, + taskText, + mu.From.Username.PeerID(), + mu.Chat.ID.PeerID(), + ) + + conv, err := h.controller.InitTask(t) + + if err != nil { + answer := err.Error() + h.LogMessage(ctx, mu, answer) + return err + } + + i := strconv.Itoa(int(conv.ID)) + answer = tg.HTML.Text( + tg.HTML.Line( + tg.HTML.Bold("Task ID: "), + tg.HTML.Code(i), + tg.HTML.Text(" was created"), + ), + tg.HTML.Line( + "Заходи в наш", + tg.HTML.Link("discord", "https://discord.gg/RHdzK3kUr7"), + "чтобы отслеживать статус по задаче", + ), + ) + if mu.Caption != "" { + answer = tg.HTML.Text( + tg.HTML.Line( + tg.HTML.Bold("I'm unable to work with files, but"), + ), + tg.HTML.Line( + tg.HTML.Italic( + tg.HTML.Bold("Task ID: "), + tg.HTML.Code(i), + tg.HTML.Text(" was created")), + ), + + tg.HTML.Line( + "Заходи в наш", + tg.HTML.Link("discord", "https://discord.gg/RHdzK3kUr7"), + "чтобы отслеживать статус по задаче", + ), + ) + } + + h.LogMessage(ctx, mu, answer) + return mu.Answer(answer). + ReplyToMessageID(msgID).ParseMode(tg.HTML).DisableWebPagePreview(true).DoVoid(ctx) +} diff --git a/bot/handler/folder.go b/client/telegram/handler/handle_folder.go similarity index 81% rename from bot/handler/folder.go rename to client/telegram/handler/handle_folder.go index 460d7af..fa88377 100644 --- a/bot/handler/folder.go +++ b/client/telegram/handler/handle_folder.go @@ -17,10 +17,10 @@ func (h *Handler) NewFolderHandler(ctx context.Context, mu *tgb.MessageUpdate) e return errors.New("empty command provided") } - cloud, err := h.cloud.CreateFolder(str) + resp := h.cloud.CreateFolder(str) - if err != nil { - answer := errorAnswer(err.Error()) + if resp.ErrMessage != nil { + answer := errorAnswer(resp.ErrMessage.Error()) h.LogMessage(ctx, mu, answer) 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( tg.HTML.Line( "✨ Shiny folder", - tg.HTML.Link(cloud.Title, cloud.PrivateURL), + tg.HTML.Link(resp.Folder.Title, resp.Folder.PrivateURL), "has been created!", ), ) diff --git a/bot/handler/git.go b/client/telegram/handler/handle_git.go similarity index 84% rename from bot/handler/git.go rename to client/telegram/handler/handle_git.go index 8627da7..52e0a76 100644 --- a/bot/handler/git.go +++ b/client/telegram/handler/handle_git.go @@ -19,12 +19,12 @@ type git struct { ssh string } -func newGit(domain *domain.Git) *git { +func newGit(g *domain.Git) *git { return &git{ - name: domain.Name, - url: domain.HtmlUrl, - git: domain.CloneUrl, - ssh: fmt.Sprintf("ssh://%s/%s.git", domain.SshUrl, domain.FullName), + name: g.Name, + url: g.HtmlUrl, + git: g.CloneUrl, + ssh: fmt.Sprintf("ssh://%s/%s.git", g.SshUrl, g.FullName), } } diff --git a/bot/handler/init.go b/client/telegram/handler/handle_init.go similarity index 100% rename from bot/handler/init.go rename to client/telegram/handler/handle_init.go diff --git a/client/telegram/handler/handler.go b/client/telegram/handler/handler.go new file mode 100644 index 0000000..da3a326 --- /dev/null +++ b/client/telegram/handler/handler.go @@ -0,0 +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 + 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, + controller: controller, + } +} diff --git a/client/telegram/telegram.go b/client/telegram/telegram.go new file mode 100644 index 0000000..63fb7d8 --- /dev/null +++ b/client/telegram/telegram.go @@ -0,0 +1,53 @@ +package telegram + +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" +) + +type TelegramOptions struct { + GitService *services.Git + CloudService *services.Cloud + Coda *services.Coda + AppConfig *domain.Config + Controller *controller.WorkflowController +} + +// runTgBot ... +// ..function creates new Telegram BOT instance +// ..throw env variables through bot's handlers +// ..setup tg bot router; +// and finally returns tgb.Poller +func Run(ctx context.Context, opts TelegramOptions) error { + + log.Print("Start telegram bot init..") + client := tg.New(opts.AppConfig.Telegram.Token) + + h := handler.NewHandler( + opts.GitService, + opts.CloudService, + opts.Coda, + opts.Controller, + ) + + router := tgb.NewRouter(). + Message(h.Init, tgb.Command("init")). + Message(h.PingHandler, tgb.Command("ping")). + // Message(h.DevelopmentTaskHandler, tgb.TextHasPrefix("/new")). + // Message(h.NewRepoHandler, tgb.TextHasPrefix("/repo")). + // Message(h.NewFolderHandler, tgb.TextHasPrefix("/folder")). + Message(h.FarmTaskHandler, tgb.TextHasPrefix("/task")) + + log.Print("Success init. Start poller.") + return tgb.NewPoller( + router, + client, + ).Run(ctx) +} diff --git a/cmd/main.go b/cmd/main.go index 5896e5f..42329fa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,77 +7,101 @@ import ( "os" "os/signal" "syscall" - "ticket-pimp/bot/handler" + + "ticket-pimp/internal/controller" + "ticket-pimp/internal/domain" "ticket-pimp/internal/services" - "github.com/joho/godotenv" - "github.com/mr-linch/go-tg" - "github.com/mr-linch/go-tg/tgb" + "ticket-pimp/client/discord" + "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") - env("prod.env") - ctx := context.Background() + config := domain.InitConfig(envfile) + run(config) +} - ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, os.Kill, syscall.SIGTERM) +func run(conf domain.Config) { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) defer cancel() - if err := runBot(ctx); err != nil { - fmt.Println(err) + // -- DB connection init -- START + connString := fmt.Sprintf( + "postgresql://%s:%s@%s:%s/%s", + conf.DB.User, conf.DB.Pass, conf.DB.Host, conf.DB.Port, conf.DB.Name, + ) + conn, err := pgxpool.New( + ctx, + connString) + if err != nil { + log.Fatalf("DB connection failed: %v", err) + } + // -- DB connection init -- END + + // Aply migrations: + + 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) + + // Инициализация контроллера: + controller := controller.NewWorkflowController( + gitService, + cloudService, + codaService, + conn, + ) + + go func() { + opts := discord.DiscordOptions{ + Controller: controller, + Config: &conf, + } + if err := discord.Run(conf, opts); err != nil { + log.Fatalf("discord bot cannot be runned: %v", err) + } + }() + + opts := telegram.TelegramOptions{ + GitService: gitService, + CloudService: cloudService, + Coda: codaService, + AppConfig: &conf, + Controller: controller, + } + + if err := telegram.Run(ctx, opts); err != nil { + log.Fatalf("telegram bot cannot be runned: %v", err) defer os.Exit(1) } } - -// env -// env function reads provided file and setup envirmental variables; -func env(envFilePath string) { - err := godotenv.Load(envFilePath) - if err != nil { - log.Fatal("Error while loading env file") - } -} - -// runBot ... -// ..function creates new Telegram BOT instance -// ..throw env variables through bot's handlers -// ..setup tg bot router; -// and finally returns tgb.Poller -func runBot(ctx context.Context) error { - - client := tg.New(os.Getenv("TG_API")) - - gitService := services.NewGit( - os.Getenv("GIT_BASE_URL"), - os.Getenv("GIT_TOKEN"), - ) - - cloudService := services.NewCloud( - os.Getenv("CLOUD_BASE_URL"), - os.Getenv("CLOUD_USER"), - os.Getenv("CLOUD_PASS"), - ) - - coda := services.NewCodaClient( - os.Getenv("CODA_TOKEN1"), - ) - - h := handler.NewHandler( - gitService, - cloudService, - coda, - ) - - router := tgb.NewRouter(). - Message(h.Init, tgb.Command("init")). - Message(h.PingHandler, tgb.Command("ping")). - Message(h.DevelopmentTaskHandler, tgb.TextHasPrefix("/new")). - Message(h.NewRepoHandler, tgb.TextHasPrefix("/repo")). - Message(h.NewFolderHandler, tgb.TextHasPrefix("/folder")). - Message(h.FarmTaskHandler, tgb.TextHasPrefix("/task")) - - return tgb.NewPoller( - router, - client, - ).Run(ctx) -} 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/docker/gitbucket/Dockerfile b/docker/gitbucket/Dockerfile deleted file mode 100644 index f7587fb..0000000 --- a/docker/gitbucket/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM adoptopenjdk/openjdk11-openj9 - -ARG GITBUCKET_HOME - -# create home -RUN mkdir -p $GITBUCKET_HOME - -# mark volumes -VOLUME $GITBUCKET_HOME/repositories -VOLUME $GITBUCKET_HOME/data -VOLUME $GITBUCKET_HOME/gist -VOLUME $GITBUCKET_HOME/plugins - -# Port for web page and Port for SSH access to git repository (Optional) -EXPOSE 8080 8443 29418 - -COPY server-WIN-DOMAIN-CA.cer / -COPY SSLPoke.java / - -# ADD https://github.com/gitbucket/gitbucket/releases/download/4.38.4/gitbucket.war $GITBUCKET_HOME/gitbucket.war -COPY gitbucket.war $GITBUCKET_HOME - -RUN keytool -importcert -file /server-WIN-DOMAIN-CA.cer -alias "server-WIN-DOMAIN-CA" -cacerts -storepass changeit -noprompt - -# set environment -WORKDIR $GITBUCKET_HOME - -CMD ["sh", "-c", "java $JAVA_OPTS -jar $GITBUCKET_HOME/gitbucket.war"] \ No newline at end of file diff --git a/docker/gitbucket/SSLPoke.java b/docker/gitbucket/SSLPoke.java deleted file mode 100644 index b46dcce..0000000 --- a/docker/gitbucket/SSLPoke.java +++ /dev/null @@ -1,40 +0,0 @@ -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import java.io.*; - -/** Establish a SSL connection to a host and port, writes a byte and - * prints the response. See - * http://confluence.atlassian.com/display/JIRA/Connecting+to+SSL+services - */ -public class SSLPoke { - public static void main(String[] args) { - if (args.length != 2) { - System.out.println("Usage: "+SSLPoke.class.getName()+" "); - System.exit(1); - } - try { - SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(args[0], Integer.parseInt(args[1])); - - SSLParameters sslparams = new SSLParameters(); - sslparams.setEndpointIdentificationAlgorithm("HTTPS"); - sslsocket.setSSLParameters(sslparams); - - InputStream in = sslsocket.getInputStream(); - OutputStream out = sslsocket.getOutputStream(); - - // Write a test byte to get a reaction :) - out.write(1); - - while (in.available() > 0) { - System.out.print(in.read()); - } - System.out.println("Successfully connected"); - - } catch (Exception exception) { - exception.printStackTrace(); - System.exit(1); - } - } -} \ No newline at end of file diff --git a/docker/gitbucket/docker-compose.yml b/docker/gitbucket/docker-compose.yml deleted file mode 100644 index 48fdcbf..0000000 --- a/docker/gitbucket/docker-compose.yml +++ /dev/null @@ -1,68 +0,0 @@ -version: "3" - -volumes: - mysql: - driver: local - -services: - gitbucket: - build: - context: . - dockerfile: Dockerfile - args: - GITBUCKET_HOME: /mnt/gitbucket - container_name: gitbucket_server - restart: always - ports: - - ${HTTP_PORT}:8080 - - ${HTTPS_PORT}:8443 - # Optional for SSH: - - ${SSH_PORT}:29418 - depends_on: - mariadb: - condition: service_healthy - environment: - - JAVA_OPTS=-Dcom.sun.net.ssl.checkRevocation=false -Dcom.sun.security.enableAIAcaIssuers=true -Docsp.enable=fase -Dtrust_all_cert=true -Djdk.tls.client.protocols=TLSv1.2 -Dcom.sun.net.ssl.enableECC=false - - GITBUCKET_HOME=/mnt/gitbucket - - GITBUCKET_CONNECTORS=http - - GITBUCKET_HOST=0.0.0.0 - - GITBUCKET_PORT=8080 - - GITBUCKET_SECUREPORT=8443 - - GITBUCKET_REDIRECTHTTPS=false - - GITBUCKET_PREFIX=/ - - GITBUCKET_MAXFILESIZE=4294967296 - - GITBUCKET_UPLOADTIMEOUT=120000 - - GITBUCKET_JETTYIDLETIMEOUT=600000 - - GITBUCKET_DB_URL=jdbc:mariadb://mariadb/gitbucket?useUnicode=true&characterEncoding=utf8 - - GITBUCKET_DB_USER=gitbucket - - GITBUCKET_DB_PASSWORD=gitbucket - healthcheck: - test: ["CMD", "/usr/bin/healthcheck"] - interval: 30s - timeout: 10s - retries: 5 - volumes: - - ../disks/disk1/gitbucket_data/repositories/:/mnt/gitbucket/repositories/ - - ../disks/disk1/gitbucket_data/data/:/mnt/gitbucket/data/ - - ../disks/disk1/gitbucket_data/gist/:/mnt/gitbucket/gist/ - - ../disks/disk1/gitbucket_data/plugins/:/mnt/gitbucket/plugins/ - - mariadb: - image: mariadb:10.6 - container_name: gitbucket_mariadb - restart: always - ports: - - ${DB_PORT}:3306 - environment: - - MYSQL_ROOT_PASSWORD=gitbucket - - MYSQL_USER=gitbucket - - MYSQL_PASSWORD=gitbucket - - MYSQL_DATABASE=gitbucket - command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=gitbucket"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - - mysql:/var/lib/mysql \ No newline at end of file diff --git a/docker/gitbucket/gitbucket.war b/docker/gitbucket/gitbucket.war deleted file mode 100644 index c63e40a..0000000 Binary files a/docker/gitbucket/gitbucket.war and /dev/null differ diff --git a/docker/gitbucket/server-WIN-DOMAIN-CA.cer b/docker/gitbucket/server-WIN-DOMAIN-CA.cer deleted file mode 100644 index 906db20..0000000 --- a/docker/gitbucket/server-WIN-DOMAIN-CA.cer +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDuTCCAqGgAwIBAgIQcZXcA798CYtB1LtewpzPijANBgkqhkiG9w0BAQsFADBu -MRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxHjAcBgoJkiaJk/IsZAEZFg5tYXJsZXJp -bm9ncm91cDEWMBQGCgmSJomT8ixkARkWBnNlcnZlcjEdMBsGA1UEAxMUc2VydmVy -LVdJTi1ET01BSU4tQ0EwIBcNMjIwODE1MTI1NTM3WhgPMjEyMjA4MTUxMzA1Mzda -MG4xFTATBgoJkiaJk/IsZAEZFgVsb2NhbDEeMBwGCgmSJomT8ixkARkWDm1hcmxl -cmlub2dyb3VwMRYwFAYKCZImiZPyLGQBGRYGc2VydmVyMR0wGwYDVQQDExRzZXJ2 -ZXItV0lOLURPTUFJTi1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMBJwWymu7Csp+y8EMiPNo5drsQE58+enNCk13OIemQON5LZwY++BnvIrHvnaCof -Cdb9U2QyWV+Iehv/lNXcBkT6dJCTxUVmn6rXMmwNnBJmGuy0D6APIEPVBO3S7+/R -E4az8ikpVezbqAw/LiSyVMEQFaAEK8RlleBScLw9gpXmcWKljCrO/z+2wO1EGI3z -a1+qFLynQLeyusRj6rvo0U4RTyXdKaA++Ojhx3DV1NopHoSJ7CqXR728ubWK0fvY -0JGMg2ijS1cle7yC96+9ZSjdtktI/9M+8SG/tbpXcpW1bzGM0wCWOos1hLehks7V -c1pBEuVGSKSdtyF/FK2C7YUCAwEAAaNRME8wCwYDVR0PBAQDAgGGMA8GA1UdEwEB -/wQFMAMBAf8wHQYDVR0OBBYEFL+/yzfaRM/Rcrc9oYEzZf3STypqMBAGCSsGAQQB -gjcVAQQDAgEAMA0GCSqGSIb3DQEBCwUAA4IBAQA7j2n9ZeKDwbmWCM2P1F9+6EZ8 -+RZ+PEdxqs3aDo6S0Ckr/iopHA9PoexKGVEQN6bzaYxb0XTEbpJ89oI299weJF+W -63zID/lpEzsaSlTWpuj8KBudVoKsJmwJK2akQ2T3MmSiz7drtFOyh5wl9vhoi2cv -H3xhpqOYgh6iRwfwXMLo2YyS4vG6UZtq1GnU/drwVb1gHCoMdz7sFIdxlaw85Ub+ -dlicykBNu60dbvM/iI0qSG25PerjEoOWZAglrPWpsQ0Zy+ChV0lWSp6FIYHjT3Rj -3TUo18LwIUKyorRErOTO4vgYHpqaA/RkAfl0C3cUujMw9eAp1fyd9FH0UcDo ------END CERTIFICATE----- diff --git a/docker/owncloud/docker-compose.yml b/docker/owncloud/docker-compose.yml deleted file mode 100644 index 4218d74..0000000 --- a/docker/owncloud/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -version: "3" - -volumes: - mysql: - driver: local - redis: - driver: local - -services: - owncloud: - image: owncloud/server:${OWNCLOUD_VERSION} - container_name: owncloud_server - restart: always - ports: - - ${HTTP_PORT}:8080 - depends_on: - - mariadb - - redis - environment: - - OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN} - - OWNCLOUD_TRUSTED_DOMAINS=${OWNCLOUD_TRUSTED_DOMAINS} - - OWNCLOUD_DB_TYPE=mysql - - OWNCLOUD_DB_NAME=owncloud - - OWNCLOUD_DB_USERNAME=owncloud - - OWNCLOUD_DB_PASSWORD=owncloud - - OWNCLOUD_DB_HOST=mariadb - - OWNCLOUD_ADMIN_USERNAME=${ADMIN_USERNAME} - - OWNCLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} - - OWNCLOUD_MYSQL_UTF8MB4=true - - OWNCLOUD_REDIS_ENABLED=true - - OWNCLOUD_REDIS_HOST=redis - healthcheck: - test: ["CMD", "/usr/bin/healthcheck"] - interval: 30s - timeout: 10s - retries: 5 - volumes: - - ../disks/disk1/owncloud_data/:/mnt/data - - ../disks/:/mnt/disks - - mariadb: - image: mariadb:10.6 # minimum required ownCloud version is 10.9 - container_name: owncloud_mariadb - restart: always - ports: - - ${DB_PORT}:3306 - environment: - - MYSQL_ROOT_PASSWORD=owncloud - - MYSQL_USER=owncloud - - MYSQL_PASSWORD=owncloud - - MYSQL_DATABASE=owncloud - command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-u", "root", "--password=owncloud"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - - mysql:/var/lib/mysql - - redis: - image: redis:6 - container_name: owncloud_redis - restart: always - command: ["--databases", "1"] - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - - redis:/data diff --git a/go.mod b/go.mod index ad1f020..c4183a4 100644 --- a/go.mod +++ b/go.mod @@ -3,65 +3,41 @@ module ticket-pimp go 1.20 require ( + github.com/bwmarrin/discordgo v0.27.1 github.com/imroc/req/v3 v3.35.2 + github.com/jackc/pgx/v5 v5.4.3 github.com/joho/godotenv v1.5.1 github.com/mr-linch/go-tg v0.9.1 ) require ( - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/artsafin/coda-go-client v1.0.4 // indirect - github.com/bwmarrin/discordgo v0.27.1 // indirect - github.com/bytedance/sonic v1.9.2 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deepmap/oapi-codegen v1.13.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.1 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/goccy/go-json v0.10.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/labstack/echo/v4 v4.10.2 // indirect - github.com/labstack/gommon v0.4.0 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pmezard/go-difflib v1.0.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/stretchr/objx v0.5.0 // indirect + github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/stretchr/testify v1.8.4 // indirect - github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 // indirect github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.10.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.11.0 // indirect + golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.10.0 // indirect golang.org/x/tools v0.9.3 // indirect google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c87b99e..683817c 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,21 @@ -github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= -github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= -github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/artsafin/coda-go-client v1.0.4 h1:FiqMEMgWVpGKEGYBvdhH4lJHPD569vIaOFbF0Q0YX2U= -github.com/artsafin/coda-go-client v1.0.4/go.mod h1:WFyZA4RSVNCAnLoQEDMerb+eKxbxMP6+CqbcHmabSHU= -github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM= -github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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/deepmap/oapi-codegen v1.13.0 h1:cnFHelhsRQbYvanCUAbRSn/ZpkUb1HPRlQcu8YqSORQ= -github.com/deepmap/oapi-codegen v1.13.0/go.mod h1:Amy7tbubKY9qkZOXqymI3Z6xSbndmu+atMJheLdyg44= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +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-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= -github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -58,43 +25,24 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/imroc/req/v3 v3.35.2 h1:psklb5GyhKugTsGQy6seydK/8vMKBJj4R0Bwc/OQahw= github.com/imroc/req/v3 v3.35.2/go.mod h1:c8dXW9N3SJn/DuKVjHHmryV2WO7At9bFtnu1rloiFoo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= +github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= -github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= -github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= -github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= -github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= -github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mr-linch/go-tg v0.9.1 h1:4KNe7zwFG6svgM9w6pIcH3R7QWa6hIK8tCisQiFRCpU= github.com/mr-linch/go-tg v0.9.1/go.mod h1:276w69YW4pEo3ZYta+LQe4v/ut2w2h1ksP4ziBWkK98= github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +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= @@ -106,44 +54,22 @@ 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/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2 h1:VsBj3UD2xyAOu7kJw6O/2jjG2UXLFoBzihqDU9Ofg9M= -github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= @@ -155,32 +81,22 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -193,14 +109,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/controller/control_folder.go b/internal/controller/control_folder.go new file mode 100644 index 0000000..7a8f61b --- /dev/null +++ b/internal/controller/control_folder.go @@ -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 +} diff --git a/internal/controller/control_git.go b/internal/controller/control_git.go new file mode 100644 index 0000000..077a509 --- /dev/null +++ b/internal/controller/control_git.go @@ -0,0 +1,171 @@ +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 { + 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{ + 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 { + 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 { + 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("передано пустое имя"), + } + } + } +} diff --git a/internal/controller/control_project.go b/internal/controller/control_project.go new file mode 100644 index 0000000..d04cbb3 --- /dev/null +++ b/internal/controller/control_project.go @@ -0,0 +1,118 @@ +package controller + +import ( + "context" + "fmt" + "ticket-pimp/internal/domain" + "ticket-pimp/internal/storage/db" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +func (wc *WorkflowController) Get(ctx context.Context) (*domain.ApplicationConfig, error) { + c, err := wc.q.GetConfig(ctx) + return &domain.ApplicationConfig{ + Key: c.TicketKey.String, + ID: int(c.TicketID.Int32), + }, err +} + +func (wc *WorkflowController) NewKey(ctx context.Context) (*domain.ApplicationConfig, error) { + c, err := wc.q.SetNewConfig(ctx) + return &domain.ApplicationConfig{ + Key: c.TicketKey.String, + ID: int(c.TicketID.Int32), + }, err +} + +func (wc *WorkflowController) ProjectCreate(ctx context.Context, project domain.Project) (*domain.Project, error) { + + tx, err := wc.pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + + qtx := wc.q.WithTx(tx) + + appconfig, err := qtx.SetNewConfig(ctx) + if err != nil { + return nil, err + } + + project.ShortName = fmt.Sprintf( + "%s-%d", + appconfig.TicketKey.String, + appconfig.TicketID.Int32, + ) + + project.ID = string(appconfig.TicketID.Int32) + + projectRow, err := qtx.CreateTicket(ctx, db.CreateTicketParams{ + Key: pgtype.Text{String: project.ShortName, Valid: true}, + Channelid: pgtype.Text{String: project.ChannelID, Valid: true}, + }) + if err != nil { + tx.Rollback(ctx) + return nil, err + } else { + tx.Commit(ctx) + fmt.Println(projectRow) + } + + return &project, nil +} + +func (wc *WorkflowController) GetProjectByChannelID(ctx context.Context, id string) (*domain.Project, error) { + var proj domain.Project + dbTicket, err := wc.q.GetTicketByChannelID(ctx, pgtype.Text{String: id, Valid: true}) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return nil, err + } else { + proj = domain.Project{ + ID: string(dbTicket.ID), + ShortName: dbTicket.Key.String, + Name: dbTicket.Key.String, + ChannelID: dbTicket.Channelid.String, + ProjectGit: dbTicket.ProjectGit.String, + BuildGit: dbTicket.BuildGit.String, + Cloud: dbTicket.Folder.String, + } + } + 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{ + Key: pgtype.Text{String: key, Valid: true}, + Channelid: pgtype.Text{String: channelID, Valid: true}, + }, + ) + if err != nil { + return nil, err + } + } else { + if err != nil { + return nil, err + } + } + + return &domain.Project{ + ID: string(dbTicket.ID), + ShortName: dbTicket.Key.String, + Name: dbTicket.Key.String, + ChannelID: dbTicket.Channelid.String, + ProjectGit: dbTicket.ProjectGit.String, + BuildGit: dbTicket.BuildGit.String, + Cloud: dbTicket.Folder.String, + }, nil +} diff --git a/internal/controller/control_task.go b/internal/controller/control_task.go new file mode 100644 index 0000000..b992ca7 --- /dev/null +++ b/internal/controller/control_task.go @@ -0,0 +1,153 @@ +package controller + +import ( + "context" + "fmt" + "os" + "ticket-pimp/internal/domain" + "ticket-pimp/internal/storage/db" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/jackc/pgx/v5/pgtype" +) + +// WriteTaskToDB +/* +Makes an SQL query to create new tasks row from domain.Task entity + - Creator field: telegram nickname or Discord's Mention(); + - Creator link (tg ID) in telegram case; + - Description from telegram/discord message bodies; +*/ +func (wc *WorkflowController) WriteTaskToDB(t *domain.Task) (*domain.Task, error) { + dbtask, err := wc.q.InsertTask(context.TODO(), db.InsertTaskParams{ + Creator: pgtype.Text{String: t.Creator, Valid: true}, + CreatorLink: pgtype.Text{ + String: t.CreatorLink, + Valid: true, + }, + Description: pgtype.Text{ + String: t.Description, + Valid: true, + }, + }) + if err != nil { + return nil, fmt.Errorf("unable to create task at the db: %v", err) + } + // ------------------------------------------------------------------------------------ + + task := newConvertable(&dbtask).ExtractDomain() + return task, nil +} + +// InitTask +/* +Runs the following: + - Use WriteTaskToDB method to make a new task row in the db; + - init new discord bot instance; + - + +Possible errors: + - db record couldn't be created; + - bot couldn't be inited; + - bot session couldn't be started; + - thread couldn't be started; + - first task message couldn't be edited; +*/ +func (wc *WorkflowController) InitTask(t *domain.Task) (*domain.Task, error) { + + task, err := wc.WriteTaskToDB(t) + if err != nil { + return nil, fmt.Errorf("unable to create task at the db: %v", err) + } + + // Инициализируем новый клиент дискорда + // [ ] Нездоровое получение параметров клиента из os.. + var ( + token = os.Getenv("DISCORD_TOKEN") + forumChannelID = os.Getenv("TASKS_CHANNEL") + ) + + s, err := discordgo.New("Bot " + token) + if err != nil { + return task, fmt.Errorf("unable to create discord session: %v", err) + } + + if err := s.Open(); err != nil { + return task, fmt.Errorf("cannot open the session: %v", err) + } + + msg := discordgo.MessageSend{ + Content: task.DiscordMessage(domain.NewTaskState()), + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "Start", + Style: discordgo.SuccessButton, + Disabled: false, + CustomID: "task_start", + }, + discordgo.Button{ + Label: "Close", + Style: discordgo.DangerButton, + Disabled: true, + CustomID: "task_close", + }, + }, + }, + }, + } + + th, err := s.ForumThreadStartComplex( + forumChannelID, + &discordgo.ThreadStart{ + Name: fmt.Sprintf("Task ID: %d, by %s", task.ID, task.Creator), + }, + &msg, + ) + if err != nil { + return task, fmt.Errorf("unable to update channel: %v", err) + } + + err = wc.UpdateTasksMessageID(context.TODO(), th.ID, task.ID) + if err != nil { + return task, fmt.Errorf("unable to set discord message to task: %v", err) + } + + return task, nil +} + +func (wc *WorkflowController) UpdateTasksMessageID(ctx context.Context, msgID string, taskID int32) error { + err := wc.q.UpdateTaskWithMessageID(context.TODO(), db.UpdateTaskWithMessageIDParams{ + Messageid: pgtype.Text{String: msgID, Valid: true}, + ID: taskID, + }) + return err +} + +func (wc *WorkflowController) UpdateTask(id string, opt int, user string) (*TaskConvertable, error) { + var ( + err error + dbtask db.Task + ) + switch opt { + case 0: + dbtask, err = wc.q.StartTask(context.TODO(), db.StartTaskParams{ + UpdatedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + Assignee: pgtype.Text{String: user, Valid: true}, + Messageid: pgtype.Text{String: id, Valid: true}, + }) + return &TaskConvertable{&dbtask}, err + + case 1: + dbtask, err = wc.q.CloseTask(context.TODO(), db.CloseTaskParams{ + DeletedAt: pgtype.Timestamptz{Time: time.Now(), InfinityModifier: 0, Valid: true}, + Assignee: pgtype.Text{String: user, Valid: true}, + Messageid: pgtype.Text{String: id, Valid: true}, + }) + return &TaskConvertable{&dbtask}, err + } + + return &TaskConvertable{&dbtask}, nil +} diff --git a/bot/controller/controller.go b/internal/controller/control_workflow.go similarity index 57% rename from bot/controller/controller.go rename to internal/controller/control_workflow.go index cb02d23..d1fd033 100644 --- a/bot/controller/controller.go +++ b/internal/controller/control_workflow.go @@ -1,37 +1,25 @@ package controller import ( + "context" "fmt" + "log" "strings" "sync" "ticket-pimp/internal/domain" - "ticket-pimp/internal/services" + "ticket-pimp/internal/storage/db" + + "github.com/jackc/pgx/v5/pgtype" ) -type WorkflowController struct { - iGit services.IGit - iCloud services.ICloud - iCoda services.ICoda -} - -func NewWorkflowController( - git services.IGit, - cloud services.ICloud, - coda services.ICoda, -) *WorkflowController { - return &WorkflowController{ - iGit: git, - iCloud: cloud, - - iCoda: coda, - } -} - -type IWorkflowController interface { - Workflow(name, key, id string) (string, error) -} - -func (wc *WorkflowController) Workflow(name, key, id string) (string, error) { +// 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) @@ -45,17 +33,18 @@ func (wc *WorkflowController) Workflow(name, key, id string) (string, error) { go func(ref **domain.Git) { defer wg.Done() - *ref, _ = wc.iGit.CreateRepo(appKey) + *ref, _ = wc.IGit.CreateRepo(appKey) }(&git) go func(ref **domain.Git) { defer wg.Done() - *ref, _ = wc.iGit.CreateRepo(appKey + "-build") + *ref, _ = wc.IGit.CreateRepo(appKey + "-build") }(&gitBuild) go func(ref **domain.Folder) { defer wg.Done() - *ref, _ = wc.iCloud.CreateFolder(appKey) + + *ref = wc.ICloud.CreateFolder(appKey).Folder }(&cloud) wg.Wait() @@ -79,8 +68,18 @@ func (wc *WorkflowController) Workflow(name, key, id string) (string, error) { } else { cloudResult = cloud.PrivateURL } + ctx := context.TODO() - wc.iCoda.CreateApp(domain.CodaApplication{ + 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, diff --git a/internal/controller/controller.go b/internal/controller/controller.go new file mode 100644 index 0000000..ecea95d --- /dev/null +++ b/internal/controller/controller.go @@ -0,0 +1,64 @@ +package controller + +import ( + "ticket-pimp/internal/domain" + "ticket-pimp/internal/services" + "ticket-pimp/internal/storage/db" + + "github.com/bwmarrin/discordgo" + "github.com/jackc/pgx/v5/pgxpool" +) + +type WorkflowController struct { + IGit services.IGit + ICloud services.ICloud + ICoda services.ICoda + pool *pgxpool.Pool + q *db.Queries + ATags []discordgo.ForumTag +} + +func NewWorkflowController( + git services.IGit, + cloud services.ICloud, + coda services.ICoda, + pool *pgxpool.Pool, +) *WorkflowController { + return &WorkflowController{ + IGit: git, + ICloud: cloud, + ICoda: coda, + pool: pool, + q: db.New(pool), + } +} + +type ProjectResponse struct { + Project *domain.Project + Message error +} + +type TaskConvertable struct { + *db.Task +} + +func newConvertable(db *db.Task) *TaskConvertable { + return &TaskConvertable{ + Task: db, + } +} + +func (t *TaskConvertable) ExtractDomain() *domain.Task { + return &domain.Task{ + ID: t.ID, + // Summary: "", + Description: t.Description.String, + Creator: t.Creator.String, + CreatorLink: t.CreatorLink.String, + Assignee: t.Assignee.String, + CreatedAt: t.CreatedAt.Time, + DeletedAt: t.DeletedAt.Time, + UpdatedAt: t.UpdatedAt.Time, + // URL: "", + } +} diff --git a/internal/domain/config.go b/internal/domain/config.go new file mode 100644 index 0000000..b2f891a --- /dev/null +++ b/internal/domain/config.go @@ -0,0 +1,102 @@ +package domain + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +type Config struct { + Git GitConfig + Cloud CloudConfig + Coda CodaConfig + DB DBConfig + Telegram TelegramConfig + Discord DiscordConfig +} + +type GitConfig struct { + BaseUrl string + Token string + User string +} + +type CloudConfig struct { + BaseUrl string + User string + Pass string + RootDir string +} + +type CodaConfig struct { + Farm string + Develop string +} + +type DBConfig struct { + Host string + Port string + Name string + User string + Pass string + SslMode string +} + +type TelegramConfig struct { + Token string +} + +type DiscordConfig struct { + Token string + IsProjectChannel string + IsTaskForum string +} + +type ApplicationConfig struct { + Key string + ID int +} + +// InitConfig +// InitConfig function reads provided file and setup envirmental variables; +func InitConfig(envFilePath string) Config { + err := godotenv.Load(envFilePath) + if err != nil { + log.Fatal("Error while loading env file") + } + + return Config{ + Git: GitConfig{ + BaseUrl: os.Getenv("GIT_BASE_URL"), + Token: os.Getenv("GIT_TOKEN"), + User: os.Getenv("GIT_USER"), + }, + Cloud: CloudConfig{ + BaseUrl: os.Getenv("CLOUD_BASE_URL"), + User: os.Getenv("CLOUD_USER"), + Pass: os.Getenv("CLOUD_PASS"), + RootDir: os.Getenv("ROOTDIR"), + }, + Coda: CodaConfig{ + Farm: os.Getenv("CODA_TOKEN1"), + Develop: os.Getenv("CODA_TOKEN2"), + }, + DB: DBConfig{ + Host: os.Getenv("DB_HOST"), + Port: os.Getenv("DB_PORT"), + Name: os.Getenv("DB_NAME"), + User: os.Getenv("DB_USER"), + Pass: os.Getenv("DB_PASS"), + SslMode: os.Getenv("SSLMODE"), + }, + Telegram: TelegramConfig{ + Token: os.Getenv("TG_API"), + }, + 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 2d4aeff..48ec870 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -1,6 +1,9 @@ package domain -import "fmt" +import ( + "fmt" + "time" +) type Folder struct { Title string // k @@ -51,14 +54,69 @@ func (r *Row) NewCell(col string, value string) *Row { } type Task struct { + ID int32 Summary string Description string Creator string CreatorLink string + Assignee string + + CreatedAt time.Time + DeletedAt time.Time + UpdatedAt time.Time + URL string } +type TaskState int + +const ( + new TaskState = iota + inprogress + done +) + +func NewTaskState() TaskState { + return TaskState(0) +} + +func InrpogressTaskState() TaskState { + return TaskState(1) +} + +func DoneTaskState() TaskState { + return TaskState(2) +} + +// Creates a string for discordgo.DiscordMessage.Content +// State: New task; +func (t *Task) DiscordMessage(ts TaskState) string { + switch ts { + case new: + return fmt.Sprintf( + "Created at: %s \n>>> %s\n", + t.CreatedAt, + t.Description, + ) + case inprogress: + return fmt.Sprintf( + "**TaskID: %d** Started by: %s\n🚀 Started at: %s\n", + t.ID, + t.Assignee, + t.UpdatedAt, + ) + case done: + return fmt.Sprintf( + "**TaskID: %d** Closed by: %s\n✅ Closed at: %s\n", + t.ID, + t.Assignee, + t.DeletedAt, + ) + } + return "task state not provided" +} + func NewTask(summ, desc, c, cLink string) *Task { return &Task{ Summary: summ, @@ -68,10 +126,6 @@ func NewTask(summ, desc, c, cLink string) *Task { } } -type ConversionLog struct { - Advertiser []string -} - type Git struct { Name string `json:"name"` // "poop" FullName string `json:"full_name"` // "developer/poop" @@ -83,53 +137,23 @@ type Git struct { } type Project struct { - ID string `json:"id"` - ShortName string `json:"shortName"` - Name string `json:"name"` + 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"` //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 } -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 +func (p *Project) DiscordString() string { + + return fmt.Sprintf( + "## Project info:\n> 🔑 key: %s\n> 📂 folder: %s\n> 👾 project git: %s\n> 🚀 build git: %s\n", + p.ShortName, + p.Cloud, + p.ProjectGit, + p.BuildGit, + ) } diff --git a/internal/helpers/error_message.go b/internal/helpers/error_message.go new file mode 100644 index 0000000..970e15c --- /dev/null +++ b/internal/helpers/error_message.go @@ -0,0 +1,5 @@ +package helpers + +type ErrorMessage struct { + Message string `json:"message"` +} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 8a12bfd..4dc555a 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -7,7 +7,7 @@ import ( "strings" ) -func GitNaming(input string) string { +func Cut(input string) string { // Remove leading and trailing whitespace input = strings.TrimSpace(input) diff --git a/internal/helpers/helpers_test.go b/internal/helpers/helpers_test.go index 3e877a4..2c08798 100644 --- a/internal/helpers/helpers_test.go +++ b/internal/helpers/helpers_test.go @@ -18,7 +18,7 @@ var tests = []test{ func TestGitNaming(t *testing.T) { for _, test := range tests { - if output := GitNaming(test.arg); output != test.expected { + if output := Cut(test.arg); output != test.expected { t.Errorf("Output %q not equal to expected %q", output, test.expected) } } diff --git a/internal/services/cloud.go b/internal/services/cloud.go index 517c7c2..b5a748e 100644 --- a/internal/services/cloud.go +++ b/internal/services/cloud.go @@ -1,9 +1,11 @@ package services import ( + "errors" "fmt" - "os" + "log" "strconv" + "strings" "ticket-pimp/internal/domain" "ticket-pimp/internal/helpers" "time" @@ -11,60 +13,98 @@ import ( type Cloud struct { *CommonClient + Config domain.CloudConfig } type ICloud interface { - CreateFolder(name string) (*domain.Folder, error) + CreateFolder(name string) Response } -func NewCloud(base, user, pass string) *Cloud { +func NewCloud(conf domain.CloudConfig) *Cloud { client := NewClient(). SetTimeout(5*time.Second). - SetCommonBasicAuth(user, pass). - SetBaseURL(base) + SetCommonBasicAuth(conf.User, conf.Pass). + SetBaseURL(conf.BaseUrl) return &Cloud{ CommonClient: &CommonClient{ client, }, + Config: conf, } } -func (c *Cloud) CreateFolder(name string) (*domain.Folder, error) { - rootDir := os.Getenv("ROOTDIR") - user := os.Getenv("CLOUD_USER") +type Response struct { + Folder *domain.Folder + ErrMessage error +} + +func (c *Cloud) CreateFolder(name string) Response { + var R Response + + rootDir := c.Config.RootDir + user := c.Config.User davPath := "/remote.php/dav/files/" parentPath := "/apps/files/?dir=" - name = helpers.GitNaming(name) + name = helpers.Cut(name) - cloud := domain.Folder{ + R.Folder = &domain.Folder{ Title: name, PrivateURL: "", } + // cloud := domain.Folder{ + // Title: name, + // PrivateURL: "", + // } + 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) + if err != nil { // Error handling. + log.Println("error:", err) + + // Херовая обработка ошибки: + // error while cloud folder creation: bad response, raw content: + // + // + // Sabre\DAV\Exception\MethodNotAllowed + // The resource you tried to create already exists + // + 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() { // 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 - if err := c.setPrivateURL(requestPath, &cloud); err != nil { - return &cloud, err + if err := c.setPrivateURL(requestPath, R.Folder); err != nil { + return R } - } else { - fmt.Println(resp.Status) } - return &cloud, nil + return R } func (c *Cloud) setPrivateURL(requestPath string, cloud *domain.Folder) error { diff --git a/internal/services/coda.go b/internal/services/coda.go index b62b8cb..9c10b61 100644 --- a/internal/services/coda.go +++ b/internal/services/coda.go @@ -3,7 +3,6 @@ package services import ( "fmt" "log" - "os" "ticket-pimp/internal/domain" "time" @@ -12,6 +11,7 @@ import ( type Coda struct { *CommonClient + Config domain.CodaConfig } type ICoda interface { @@ -21,17 +21,18 @@ type ICoda interface { GetRowLink(id string) (string, error) } -func NewCodaClient(token string) *Coda { +func NewCodaClient(conf domain.CodaConfig) *Coda { client := NewClient(). SetTimeout(15 * time.Second). - SetCommonBearerAuthToken(token). + SetCommonBearerAuthToken(conf.Farm). SetBaseURL("https://coda.io/apis/v1") return &Coda{ CommonClient: &CommonClient{ client, }, + Config: conf, } } @@ -57,7 +58,7 @@ func (c *Coda) CreateApp(task domain.CodaApplication) { resp, _ := c.R(). SetBody(task). SetContentType("application/json"). - SetBearerAuthToken(os.Getenv("CODA_TOKEN2")). + SetBearerAuthToken(c.Config.Develop). Post("/docs/Ic3IZpQ3Wk/hooks/automation/grid-auto-NlUwM7F7Cr") fmt.Print(resp) diff --git a/internal/services/git.go b/internal/services/git.go index 657ab34..b3c86ba 100644 --- a/internal/services/git.go +++ b/internal/services/git.go @@ -15,10 +15,10 @@ type IGit interface { CreateRepo(name string) (*domain.Git, error) } -func NewGit(base, token string) *Git { +func NewGit(conf domain.GitConfig) *Git { headers := map[string]string{ "Accept": "application/vnd.github+json", - "Authorization": "Token " + token, + "Authorization": "Token " + conf.Token, "X-GitHub-Api-Version": "2022-11-28", "Content-Type": "application/json", } @@ -26,7 +26,7 @@ func NewGit(base, token string) *Git { client := NewClient(). SetTimeout(5 * time.Second). SetCommonHeaders(headers). - SetBaseURL(base) + SetBaseURL(conf.BaseUrl) return &Git{ CommonClient: &CommonClient{client}, @@ -49,19 +49,15 @@ func (gb *Git) CreateRepo(name string) (*domain.Git, error) { return repo, nil } -type request struct { +type gitCreateRequest struct { Name string `json:"name"` Private bool `json:"private"` } -type permissionRequest struct { - Perm string `json:"permission"` -} - func (gb *Git) newRepo(name string) (*domain.Git, error) { - name = helpers.GitNaming(name) + name = helpers.Cut(name) - payload := request{ + payload := gitCreateRequest{ Name: name, Private: true, } @@ -81,13 +77,16 @@ func (gb *Git) newRepo(name string) (*domain.Git, error) { return &git, nil } +type gitSetPermissionRequest struct { + Perm string `json:"permission"` +} + func (gb *Git) defaultGroupAsCollaborator(git *domain.Git) (*domain.Git, error) { - payload := permissionRequest{ + payload := gitSetPermissionRequest{ Perm: "push", } - ///orgs/manurlerino/teams/devs/repos/manurlerino/x respURL := "/orgs/mobilerino/teams/devs/repos/mobilerino/" + git.Name resp, _ := gb.R(). diff --git a/internal/storage/db/db.go b/internal/storage/db/db.go new file mode 100644 index 0000000..7d918df --- /dev/null +++ b/internal/storage/db/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/storage/db/models.go b/internal/storage/db/models.go new file mode 100644 index 0000000..6af8f66 --- /dev/null +++ b/internal/storage/db/models.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 + +package db + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type Appconfig struct { + TicketKey pgtype.Text + TicketID pgtype.Int4 +} + +type Task struct { + ID int32 + Creator pgtype.Text + CreatorLink pgtype.Text + Messageid pgtype.Text + Description pgtype.Text + Assignee pgtype.Text + CreatedAt pgtype.Timestamptz + DeletedAt pgtype.Timestamptz + UpdatedAt pgtype.Timestamptz +} + +type Ticket struct { + ID int32 + Key pgtype.Text + Channelid pgtype.Text + ProjectGit pgtype.Text + BuildGit pgtype.Text + Folder 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 new file mode 100644 index 0000000..13685a7 --- /dev/null +++ b/internal/storage/db/queries.sql.go @@ -0,0 +1,491 @@ +// 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 closeTask = `-- name: CloseTask :one +UPDATE tasks +SET deleted_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type CloseTaskParams struct { + DeletedAt pgtype.Timestamptz + Assignee pgtype.Text + Messageid pgtype.Text +} + +func (q *Queries) CloseTask(ctx context.Context, arg CloseTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, closeTask, arg.DeletedAt, arg.Assignee, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createTicket = `-- name: CreateTicket :one +INSERT INTO tickets ( + key, channelID +) 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 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 +` + +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 insertTask = `-- name: InsertTask :one +INSERT INTO tasks ( + creator, creator_link, description +) VALUES ( + $1, $2, $3 + ) + RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type InsertTaskParams struct { + Creator pgtype.Text + CreatorLink pgtype.Text + Description pgtype.Text +} + +func (q *Queries) InsertTask(ctx context.Context, arg InsertTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, insertTask, arg.Creator, arg.CreatorLink, arg.Description) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const listTasksByCreator = `-- name: ListTasksByCreator :many +SELECT id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at FROM tasks WHERE creator_link = $1 AND deleted_at is NULL +` + +func (q *Queries) ListTasksByCreator(ctx context.Context, creatorLink pgtype.Text) ([]Task, error) { + rows, err := q.db.Query(ctx, listTasksByCreator, creatorLink) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Task + for rows.Next() { + var i Task + if err := rows.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listTickets = `-- name: ListTickets :many +SELECT id, key, channelid, project_git, build_git, folder, created_at, deleted_at, updated_at FROM tickets WHERE deleted_at IS NULL +` + +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 startTask = `-- name: StartTask :one +UPDATE tasks +SET updated_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING id, creator, creator_link, messageid, description, assignee, created_at, deleted_at, updated_at +` + +type StartTaskParams struct { + UpdatedAt pgtype.Timestamptz + Assignee pgtype.Text + Messageid pgtype.Text +} + +func (q *Queries) StartTask(ctx context.Context, arg StartTaskParams) (Task, error) { + row := q.db.QueryRow(ctx, startTask, arg.UpdatedAt, arg.Assignee, arg.Messageid) + var i Task + err := row.Scan( + &i.ID, + &i.Creator, + &i.CreatorLink, + &i.Messageid, + &i.Description, + &i.Assignee, + &i.CreatedAt, + &i.DeletedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateTaskWithMessageID = `-- name: UpdateTaskWithMessageID :exec +UPDATE tasks +SET messageID = $1 +WHERE id = $2 +` + +type UpdateTaskWithMessageIDParams struct { + Messageid pgtype.Text + ID int32 +} + +func (q *Queries) UpdateTaskWithMessageID(ctx context.Context, arg UpdateTaskWithMessageIDParams) error { + _, err := q.db.Exec(ctx, updateTaskWithMessageID, arg.Messageid, arg.ID) + return err +} + +const updateTicketBuildGit = `-- name: UpdateTicketBuildGit :one +UPDATE tickets +SET build_git = $1, updated_at = $2 +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 +} diff --git a/internal/storage/dbconfig.yml b/internal/storage/dbconfig.yml new file mode 100644 index 0000000..f38dded --- /dev/null +++ b/internal/storage/dbconfig.yml @@ -0,0 +1,4 @@ +development: + dialect: postgres + 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_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/sqlc.yaml b/internal/storage/sqlc.yaml new file mode 100644 index 0000000..41f4594 --- /dev/null +++ b/internal/storage/sqlc.yaml @@ -0,0 +1,10 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "sqlc/queries.sql" + schema: "migrate" + gen: + go: + package: "db" + sql_package: "pgx/v5" + out: "db" \ No newline at end of file diff --git a/internal/storage/sqlc/queries.sql b/internal/storage/sqlc/queries.sql new file mode 100644 index 0000000..cae5e62 --- /dev/null +++ b/internal/storage/sqlc/queries.sql @@ -0,0 +1,89 @@ +-- name: GetConfig :one +SELECT ticket_key, ticket_id +FROM appconfig; + +-- name: SetNewConfig :one +UPDATE appconfig +SET ticket_id = ticket_id + 1 +RETURNING *; + +-- name: CreateTicket :one +INSERT INTO tickets ( + key, channelID +) VALUES ( + $1, $2 + ) + 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 +SELECT * FROM tickets WHERE deleted_at IS NULL; + +-- name: ListTicketsWithDeleted :many +SELECT * FROM tickets; + +-- name: GetTicketByID :one +SELECT * FROM tickets WHERE id = $1; + +-- name: GetTicketByChannelID :one +SELECT * FROM tickets WHERE channelID = $1; + +-- name: UpdateTicketByID :exec +UPDATE tickets SET project_git = $1, build_git = $2, folder = $3 WHERE id = $4; + +-- name: DeleteTicketByID :exec +UPDATE tickets SET deleted_at = current_timestamp WHERE id = $1; + +-- name: DeleteTicketByKey :exec +UPDATE tickets SET deleted_at = current_timestamp WHERE key = $1; + +-- name: InsertTask :one +INSERT INTO tasks ( + creator, creator_link, description +) VALUES ( + $1, $2, $3 + ) + RETURNING *; + +-- name: UpdateTaskWithMessageID :exec +UPDATE tasks +SET messageID = $1 +WHERE id = $2; + +-- name: StartTask :one +UPDATE tasks +SET updated_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING *; + +-- name: CloseTask :one +UPDATE tasks +SET deleted_at = $1, assignee = $2 +WHERE messageID = $3 +RETURNING *; + +-- name: GetTaskByMessage :one +SELECT * FROM tasks WHERE messageID = $1; + +-- name: ListTasksByCreator :many +SELECT * FROM tasks WHERE creator_link = $1 AND deleted_at is NULL; + +-- name: GetTaskByID :one +SELECT * FROM tasks WHERE id = $1; \ No newline at end of file diff --git a/readme.md b/readme.md index ef08ed8..ff3c68c 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,23 @@ # Сборка и запуск: -## Первые шаги делаю на локальной машине: +Первые шаги делаю на локальной машине: 1. Поменять в коде файл окружения на '.env' 2. Собрать контейнер: `docker build -t naudachu/ticket-pimp:latest --pull .` 3. Затолкать контейнер в docker hub: `docker push naudachu/ticket-pimp:latest` -## Далее с сервера: +Далее с сервера: 1. Вытягиваем новый образ: `docker pull naudachu/ticket-pimp` 2. Запускаем в фоне: `docker run -d naudachu/ticket-pimp` -## Инициализация бота: +Инициализация бота: 1. Написать в спам-чат команду: `init *{app key}* *{next ID}*` + + +# Migrations / sqlc: +``` + cd ${PROJECT_FOLDER}/internal/storage + sql-migrate up +``` + +# Repository code-gen +Запулить докер sqlc: `docker pull sqlc/sqlc` +Запустить команду из корня проекта: `cd ./internal/storage && docker run --rm -v "$(pwd):/src" -w /src sqlc/sqlc generate` \ No newline at end of file