simplify discord architecture;

This commit is contained in:
naudachu 2023-11-23 12:08:42 +05:00
parent 6cecec06b6
commit 86b00e03b6
19 changed files with 269 additions and 859 deletions

View File

@ -1,18 +1,119 @@
package discord package discord
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"ticket-pimp/client/discord/router" "ticket-pimp/client/discord/discord_handler"
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
var (
minLength int = 3
repoType string = "repo_type"
projectRepo string = "project_repo"
buildRepo string = "build_repo"
nameOption string = "repo_name"
)
var tags = []discordgo.ForumTag{
{
Name: "В работе",
Moderated: true,
EmojiName: "👩‍🍳",
},
{
Name: "Готово",
Moderated: true,
EmojiName: "✅",
},
}
var commands = []discordgo.ApplicationCommand{
{
Name: "ping",
Description: "pongs in a reply",
},
{
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,
},
},
},
{
Name: "project",
Description: "Create new development ticket",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "project_name",
Description: "Temporary project name",
Required: true,
MinLength: &minLength,
},
},
},
{
Name: "info",
Description: "Get project's info",
},
{
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: &minLength,
},
},
},
{
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: &minLength,
},
},
},
}
func initBotWith(token string) *discordgo.Session { func initBotWith(token string) *discordgo.Session {
discord, err := discordgo.New("Bot " + token) discord, err := discordgo.New("Bot " + token)
if err != nil { if err != nil {
@ -27,60 +128,48 @@ type DiscordOptions struct {
Controller *controller.WorkflowController Controller *controller.WorkflowController
} }
func checkPrivateMessaging(s *discordgo.Session, i *discordgo.InteractionCreate) error {
// Моментальный ответ для избежания столкновения с протуханием токена
initialResponse := discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "👩‍🍳 Cooking your query..",
},
}
s.InteractionRespond(i.Interaction, &initialResponse)
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 { func Run(conf domain.Config, opts DiscordOptions) error {
token := conf.Discord.Token
s := initBotWith(token) s := initBotWith(conf.Discord.Token)
router := router.InitRouter(*opts.Controller, &conf.Discord, &conf.Telegram) h := discord_handler.New(
opts.Controller,
&conf.Discord,
services.NewDummyClient(conf.Telegram),
tags,
)
commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){} commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){}
for _, handler := range router.Commands {
commandHandlers[handler.Command.Name] = handler.Handler for _, cmd := range commands {
var f func(s *discordgo.Session, i *discordgo.InteractionCreate)
switch cmd.Name {
case "ping":
f = h.Ping
case "project":
f = h.CreateTicket
case "info":
f = h.ProjectInfo
case "repo":
f = h.CreateGit
case "folder":
f = h.CreateFolder
case "init_project":
f = h.InitChannelAsProject
}
commandHandlers[cmd.Name] = f
} }
componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ componentsHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"task_start": router.Components[0].Handler, "task_start": h.HandleTaskButtons,
"task_close": router.Components[0].Handler, "task_close": h.HandleTaskButtons,
} }
s.AddHandler(router.ListenPosts) s.AddHandler(h.ListenPosts)
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := checkPrivateMessaging(s, i) h.AllInteractions(s, i)
if err != nil {
return
}
switch i.Type { switch i.Type {
case discordgo.InteractionApplicationCommand: case discordgo.InteractionApplicationCommand:
@ -105,20 +194,8 @@ func Run(conf domain.Config, opts DiscordOptions) error {
log.Print(err) log.Print(err)
} }
tagsAvailable := []discordgo.ForumTag{
{
Name: "В работе",
Moderated: true,
EmojiName: "👩‍🍳",
},
{
Name: "Готово",
Moderated: true,
EmojiName: "✅",
},
}
dchan, err := s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{ dchan, err := s.ChannelEditComplex(forum.ID, &discordgo.ChannelEdit{
AvailableTags: &tagsAvailable, AvailableTags: &tags,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -131,19 +208,16 @@ func Run(conf domain.Config, opts DiscordOptions) error {
log.Println("Adding commands...") log.Println("Adding commands...")
var cmds []*discordgo.ApplicationCommand var cmds []*discordgo.ApplicationCommand
var logString []string
for _, h := range router.Commands { for _, cmd := range commands {
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", &h.Command) cmd, err := s.ApplicationCommandCreate(s.State.User.ID, "", &cmd)
if err != nil { if err != nil {
log.Panicf("Cannot create '%v' command: %v", h.Command.Name, err) log.Panicf("Cannot create '%v' command: %v", cmd.Name, err)
} }
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
logString = append(logString, cmd.Name) log.Println(cmd.Name + " command added")
} }
log.Println("Following commands added:")
log.Println(logString)
defer s.Close() defer s.Close()
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt) signal.Notify(stop, os.Interrupt)

View File

@ -0,0 +1,82 @@
package discord_handler
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
func (h *Handler) 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
}
}
func (h *Handler) AllInteractions(s *discordgo.Session, i *discordgo.InteractionCreate) {
dchan, err := s.Channel(i.ChannelID)
if err != nil {
return
}
if dchan.Type == discordgo.ChannelTypeDM {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Yo, fella! I'm not working in private!",
},
})
return
}
// Моментальный ответ для избежания столкновения с протуханием токена
initialResponse := discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "👩‍🍳 Cooking your query..",
},
}
s.InteractionRespond(i.Interaction, &initialResponse)
}
// setFlag
// sets tag with In progress and Done text to discords channel;
func (h *Handler) 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
}

View File

@ -1,4 +1,4 @@
package handler package discord_handler
import ( import (
"context" "context"
@ -14,18 +14,21 @@ import (
type Handler struct { type Handler struct {
controller *controller.WorkflowController controller *controller.WorkflowController
conf *domain.DiscordConfig conf *domain.DiscordConfig
tg adapters.IDummyTelegram telegramDummyClient adapters.IDummyTelegram
tags []discordgo.ForumTag
} }
func NewHandler( func New(
controller *controller.WorkflowController, controller *controller.WorkflowController,
conf *domain.DiscordConfig, conf *domain.DiscordConfig,
tg adapters.IDummyTelegram, tg adapters.IDummyTelegram,
tags []discordgo.ForumTag,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
controller: controller, controller: controller,
conf: conf, conf: conf,
tg: tg, telegramDummyClient: tg,
tags: tags,
} }
} }
@ -42,37 +45,6 @@ func (h *Handler) Ping(s *discordgo.Session, i *discordgo.InteractionCreate) {
} }
} }
// setFlag
// sets tag with In progress and Done text to discords channel;
func (h *Handler) 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
}
// ListenPosts // ListenPosts
// ..listens to new posts in specific channel // ..listens to new posts in specific channel
// to act them like a task // to act them like a task
@ -151,10 +123,10 @@ func (h *Handler) ListenPosts(s *discordgo.Session, th *discordgo.ThreadCreate)
func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Send an empty interaction response; ---------------------------------------------------------------- // Send an empty interaction response; ----------------------------------------------------------------
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ // s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage, // Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{}, // Data: &discordgo.InteractionResponseData{},
}) // })
// Get assignee value; --------------------------------------------------------------------------------- // Get assignee value; ---------------------------------------------------------------------------------
user := i.Member.User.Mention() user := i.Member.User.Mention()
@ -164,6 +136,7 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti
doneButtonIsDisabled bool = false doneButtonIsDisabled bool = false
state domain.TaskState = domain.NewTaskState() state domain.TaskState = domain.NewTaskState()
message string message string
tag discordgo.ForumTag
) )
// Check what flow was touched: ------------------------------------------------------------------------- // Check what flow was touched: -------------------------------------------------------------------------
@ -173,11 +146,13 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti
doneButtonIsDisabled = false doneButtonIsDisabled = false
state = domain.InrpogressTaskState() state = domain.InrpogressTaskState()
message = "взята в работу" message = "взята в работу"
tag = h.tags[0]
case "task_close": case "task_close":
opt = 1 opt = 1
doneButtonIsDisabled = true doneButtonIsDisabled = true
state = domain.DoneTaskState() state = domain.DoneTaskState()
message = "выполнена" message = "выполнена"
tag = h.tags[1]
} }
// Send the task update to db -------------------------------------------------------------------------- // Send the task update to db --------------------------------------------------------------------------
@ -196,7 +171,7 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti
// Send message to the creator in Telegram: ------------------------------------------------------------- // Send message to the creator in Telegram: -------------------------------------------------------------
if task.CreatorLink != "" { if task.CreatorLink != "" {
h.tg.DummyNotification( h.telegramDummyClient.DummyNotification(
task.CreatorLink, task.CreatorLink,
fmt.Sprintf("Task ID: %d %s", task.ID, message)) fmt.Sprintf("Task ID: %d %s", task.ID, message))
} }
@ -231,10 +206,10 @@ func (h *Handler) HandleTaskButtons(s *discordgo.Session, i *discordgo.Interacti
} }
// [ ] Устанавливаем тэги статуса на тред --------------------------------------------------------------------- // [ ] Устанавливаем тэги статуса на тред ---------------------------------------------------------------------
// err = h.setFlag(s, i, &h.Tags[opt]) err = h.setFlag(s, i, &tag)
// if err != nil { if err != nil {
// log.Printf("error while `start` tag setting: %v", err) log.Printf("error while `start` tag setting: %v", err)
// } }
} }
func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCreate) { func (h *Handler) CreateFolder(s *discordgo.Session, i *discordgo.InteractionCreate) {

View File

@ -1,22 +0,0 @@
package handler
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
func (h *Handler) 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
}
}

View File

@ -1,222 +0,0 @@
package router
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) {
// 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"`
}
// [ ] As a separate service?
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")
}

View File

@ -1,86 +0,0 @@
package router
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"
)
// Определение переменной для ответа
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)
}

View File

@ -1,117 +0,0 @@
package router
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"
)
// Определение переменной для ответа
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)
}

View File

@ -1,30 +0,0 @@
package router
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)
}
}

View File

@ -1,188 +0,0 @@
package router
import (
"context"
"fmt"
"log"
"ticket-pimp/internal/domain"
"ticket-pimp/internal/helpers"
"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) {
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) {
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) {
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 + "-" + helpers.Cut(option.StringValue()),
ParentID: c.conf.IsProjectChannel,
}
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)
}

View File

@ -1,71 +0,0 @@
package router
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.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
}
}

View File

@ -3,7 +3,7 @@ package telegram
import ( import (
"context" "context"
"log" "log"
"ticket-pimp/client/telegram/handler" "ticket-pimp/client/telegram/telegram_handler"
"ticket-pimp/internal/controller" "ticket-pimp/internal/controller"
"ticket-pimp/internal/domain" "ticket-pimp/internal/domain"
"ticket-pimp/internal/services" "ticket-pimp/internal/services"
@ -30,7 +30,7 @@ func Run(ctx context.Context, opts TelegramOptions) error {
log.Print("Start telegram bot init..") log.Print("Start telegram bot init..")
client := tg.New(opts.AppConfig.Telegram.Token) client := tg.New(opts.AppConfig.Telegram.Token)
h := handler.NewHandler( h := telegram_handler.NewHandler(
opts.GitService, opts.GitService,
opts.CloudService, opts.CloudService,
opts.Coda, opts.Coda,

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
// func (h *Handler) DevelopmentTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { // func (h *Handler) DevelopmentTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error {

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package handler package telegram_handler
import ( import (
"ticket-pimp/adapters" "ticket-pimp/adapters"

View File

@ -1,12 +1,28 @@
package services package services
import "ticket-pimp/internal/domain" import (
"ticket-pimp/internal/domain"
"time"
)
type DummyTelegram struct { type DummyTelegram struct {
*CommonClient *CommonClient
config domain.TelegramConfig config domain.TelegramConfig
} }
func NewDummyClient(conf domain.TelegramConfig) *DummyTelegram {
client := NewClient().
SetTimeout(5 * time.Second)
return &DummyTelegram{
CommonClient: &CommonClient{
Client: client,
},
config: conf,
}
}
type TelegramMessage struct { type TelegramMessage struct {
ChatID string `json:"chat_id"` ChatID string `json:"chat_id"`
Text string `json:"text"` Text string `json:"text"`

View File

@ -86,7 +86,6 @@ func (gb *Git) defaultGroupAsCollaborator(git *domain.Git) (*domain.Git, error)
Perm: "push", Perm: "push",
} }
// respURL := "/orgs/mobilerino/teams/devs/repos/mobilerino/" + git.Name
respURL := fmt.Sprintf("/orgs/%s/teams/devs/repos/%s/%s", gb.conf.OrgName, gb.conf.OrgName, git.Name) respURL := fmt.Sprintf("/orgs/%s/teams/devs/repos/%s/%s", gb.conf.OrgName, gb.conf.OrgName, git.Name)
resp, _ := gb.R(). resp, _ := gb.R().