diff --git a/bot/controller/controller.go b/bot/controller/controller.go index 1ef2847..8d90272 100644 --- a/bot/controller/controller.go +++ b/bot/controller/controller.go @@ -1,15 +1,11 @@ package controller import ( - "encoding/csv" "fmt" - "io" "strings" "sync" "ticket-pimp/internal/domain" "ticket-pimp/internal/services" - - "github.com/imroc/req/v3" ) type WorkflowController struct { @@ -38,96 +34,6 @@ func NewWorkflowController( type IWorkflowController interface { Workflow(name string) (string, error) - - NewTask(summ, desc, c, cLink string) *Task - CreateTask(t *Task) (*Task, error) - - ThrowConversions(f io.ReadCloser, appID string, token string) *domain.ConversionLog -} - -func (wc *WorkflowController) ThrowConversions(f io.ReadCloser, appID string, token string) *domain.ConversionLog { - c := req.C(). - SetBaseURL("https://graph.facebook.com/v15.0/"). - DevMode() - - const currency = "USD" - - r := csv.NewReader(f) - - conversionLog := domain.ConversionLog{} - - for { - record, err := r.Read() - if err == io.EOF { - break - } - if err != nil { - return nil - } - - advertiser := strings.Split(record[0], ";")[0] - - params := map[string]string{ - "advertiser_id": advertiser, - "event": "CUSTOM_APP_EVENTS", - "application_tracking_enabled": "1", - "advertiser_tracking_enabled": "1", - "custom_events": `[{"_eventName":"fb_mobile_purchase"}]`, - } - - res, _ := c.R(). - SetQueryString(token). - SetQueryParams(params). - Post(appID + "/activities") - - if res.Err != nil { - conversionLog.Advertiser = append(conversionLog.Advertiser, advertiser) - } - - } - - return &conversionLog -} - -type Task struct { - Summary string - Description string - Creator string - CreatorLink string - - Key string - URL string -} - -func (wc *WorkflowController) NewTask(summ, desc, c, cLink string) *Task { - return &Task{ - Summary: summ, - Description: desc, - Creator: c, - CreatorLink: cLink, - } -} - -func (wc *WorkflowController) CreateTask(t *Task) (*Task, error) { - - yt := wc.additionalYT - - projectID, err := yt.GetProjectIDByName("E") - if err != nil { - return nil, err - } - - t.Description += fmt.Sprintf("\n\n Created by: [%s](%s)", t.Creator, t.CreatorLink) - - issue, err := yt.CreateIssue(projectID, t.Creator+" | "+t.Summary, t.Description) - if err != nil { - return nil, err - } - - t.Key = issue.Key - t.URL = fmt.Sprintf("https://mobmarlerino.youtrack.cloud/issue/%s", issue.Key) - - return t, nil } func (wc *WorkflowController) Workflow(name string) (string, error) { @@ -192,7 +98,7 @@ func (wc *WorkflowController) Workflow(name string) (string, error) { cloudResult = cloud.PrivateURL } - wc.iCoda.CreateApp(domain.CodaIssue{ + wc.iCoda.CreateApp(domain.CodaApplication{ ID: issue.Key, Summary: strings.TrimSpace(name), Git: gitResult, diff --git a/bot/handler/application.go b/bot/handler/application.go new file mode 100644 index 0000000..b44b9d7 --- /dev/null +++ b/bot/handler/application.go @@ -0,0 +1,42 @@ +package handler + +import ( + "context" + "errors" + "fmt" + "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) + + if err != nil { + answer := errorAnswer(err.Error()) + h.LogMessage(ctx, mu, answer) + return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) + } + + 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 ", + tg.HTML.Link(name, fmt.Sprintf("https://marlerino.youtrack.cloud/issue/%s", name)), + "has been created!", + ), + ) +} diff --git a/bot/handler/farmtask.go b/bot/handler/farmtask.go new file mode 100644 index 0000000..87e5e35 --- /dev/null +++ b/bot/handler/farmtask.go @@ -0,0 +1,71 @@ +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/folder.go b/bot/handler/folder.go new file mode 100644 index 0000000..460d7af --- /dev/null +++ b/bot/handler/folder.go @@ -0,0 +1,48 @@ +package handler + +import ( + "context" + "errors" + "strings" + + "github.com/mr-linch/go-tg" + "github.com/mr-linch/go-tg/tgb" +) + +func (h *Handler) NewFolderHandler(ctx context.Context, mu *tgb.MessageUpdate) error { + + str := strings.Replace(mu.Text, "/folder", "", 1) + + if str == "" { + return errors.New("empty command provided") + } + + cloud, err := h.cloud.CreateFolder(str) + + if err != nil { + answer := errorAnswer(err.Error()) + h.LogMessage(ctx, mu, answer) + return mu.Answer(answer).ParseMode(tg.HTML).DoVoid(ctx) + } + + answer := tg.HTML.Text( + tg.HTML.Line( + "✨ Shiny folder", + tg.HTML.Link(cloud.Title, cloud.PrivateURL), + "has been created!", + ), + ) + + h.LogMessage(ctx, mu, answer) + return mu.Answer(answer). + ParseMode(tg.HTML). + DoVoid(ctx) +} + +func errorAnswer(errorMsg string) string { + return tg.HTML.Text( + tg.HTML.Line( + tg.HTML.Italic(errorMsg), + ), + ) +} diff --git a/bot/handler/git.go b/bot/handler/git.go new file mode 100644 index 0000000..8627da7 --- /dev/null +++ b/bot/handler/git.go @@ -0,0 +1,62 @@ +package handler + +import ( + "context" + "errors" + "fmt" + "strings" + "ticket-pimp/internal/domain" + + "github.com/mr-linch/go-tg" + "github.com/mr-linch/go-tg/tgb" +) + +type git struct { + name string + url string + + git string + ssh string +} + +func newGit(domain *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), + } +} + +// FYI: Telegram doesn't renders this hyperlink, if the url is localhost 🤷‍♂️ +func (g *git) PrepareAnswer() string { + return tg.HTML.Text( + tg.HTML.Line( + "Repo ", + tg.HTML.Link(g.name, g.url), + "has been created!", + ), + ) +} + +func (h *Handler) NewRepoHandler(ctx context.Context, mu *tgb.MessageUpdate) error { + + str := strings.Replace(mu.Text, "/repo", "", 1) + + if str == "" { + return errors.New("empty command provided") + } + + var g *domain.Git + + g, err := h.git.CreateRepo(str) + + if err != nil { + return mu.Answer(errorAnswer(err.Error())).ParseMode(tg.HTML).DoVoid(ctx) + } + + resp := newGit(g).PrepareAnswer() + h.LogMessage(ctx, mu, resp) + + return mu.Answer(resp).ParseMode(tg.HTML).DoVoid(ctx) +} diff --git a/bot/handler/handler.go b/bot/handler/handler.go index 54c66b2..15496fb 100644 --- a/bot/handler/handler.go +++ b/bot/handler/handler.go @@ -1,23 +1,15 @@ package handler import ( - "context" - "errors" - "fmt" - "log" - "strings" "ticket-pimp/bot/controller" - "ticket-pimp/internal/domain" "ticket-pimp/internal/services" - - "github.com/mr-linch/go-tg" - "github.com/mr-linch/go-tg/tgb" ) type Handler struct { workflow controller.IWorkflowController git services.IGit cloud services.ICloud + coda services.ICoda } func NewHandler( @@ -32,202 +24,6 @@ func NewHandler( workflow: controller.NewWorkflowController(git, cloud, devyt, farmyt, coda), git: git, cloud: cloud, + coda: coda, } } - -func (h *Handler) PingHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - return mu.Answer("pong").DoVoid(ctx) -} - -type git struct { - name string - url string - - git string - ssh string -} - -func newGit(domain *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), - } -} - -// FYI: Telegram doesn't renders this hyperlink, if the url is localhost 🤷‍♂️ -func (g *git) PrepareAnswer() string { - return tg.HTML.Text( - tg.HTML.Line( - "Repo ", - tg.HTML.Link(g.name, g.url), - "has been created!", - ), - ) -} - -func (h *Handler) NewRepoHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - str := strings.Replace(mu.Text, "/repo", "", 1) - - if str == "" { - return errors.New("empty command provided") - } - - var g *domain.Git - - g, err := h.git.CreateRepo(str) - - if err != nil { - return mu.Answer(errorAnswer(err.Error())).ParseMode(tg.HTML).DoVoid(ctx) - } - - resp := newGit(g).PrepareAnswer() - - return mu.Answer(resp).ParseMode(tg.HTML).DoVoid(ctx) -} - -func (h *Handler) NewFolderHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - str := strings.Replace(mu.Text, "/folder", "", 1) - - if str == "" { - return errors.New("empty command provided") - } - - cloud, err := h.cloud.CreateFolder(str) - - if err != nil { - return mu.Answer(errorAnswer(err.Error())).ParseMode(tg.HTML).DoVoid(ctx) - } - - answer := tg.HTML.Text( - tg.HTML.Line( - "✨ Shiny folder", - tg.HTML.Link(cloud.Title, cloud.PrivateURL), - "has been created!", - ), - ) - - return mu.Answer(answer). - ParseMode(tg.HTML). - DoVoid(ctx) -} - -func errorAnswer(errorMsg string) string { - return tg.HTML.Text( - tg.HTML.Line( - tg.HTML.Italic(errorMsg), - ), - ) -} - -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) - - if err != nil { - return mu.Answer(errorAnswer(err.Error())).ParseMode(tg.HTML).DoVoid(ctx) - } - - return mu.Answer(newTicketAnswer(issueKeyStr)).ParseMode(tg.HTML).DoVoid(ctx) -} - -func newTicketAnswer(name string) string { - return tg.HTML.Text( - tg.HTML.Line( - "🤘 Ticket ", - tg.HTML.Link(name, fmt.Sprintf("https://marlerino.youtrack.cloud/issue/%s", name)), - "has been created!", - ), - ) -} - -func (h *Handler) FarmTaskHandler(ctx context.Context, mu *tgb.MessageUpdate) error { - - taskText := strings.TrimSpace(strings.Replace(mu.Text, "/task", "", 1)) - words := strings.Split(taskText, " ") - - var summaryTail string - if len(words) > 3 { - summaryTail = strings.Join(words[0:3], " ") - } else { - summaryTail = strings.Join(words, " ") - } - - task := h.workflow.NewTask( - summaryTail, - taskText, - mu.From.Username.PeerID(), - mu.From.Username.Link(), - ) - - createdTicket, err := h.workflow.CreateTask(task) - if err != nil { - return mu.Answer(errorAnswer(err.Error())).ParseMode(tg.HTML).DoVoid(ctx) - } - - return mu.Answer(tg.HTML.Text( - tg.HTML.Line( - "🤘 Задача", - tg.HTML.Link(createdTicket.Key, createdTicket.URL), - "была создана!", - ), - )).ParseMode(tg.HTML).DoVoid(ctx) -} - -func (h *Handler) NewConversion(ctx context.Context, mu *tgb.MessageUpdate) error { - msg := strings.TrimSpace(strings.Replace(mu.Caption, "/conversion", "", 1)) - - appID, token := normalizeToken(msg) - - fid := mu.Update.Message.Document.FileID - - client := mu.Client - - file, err := client.GetFile(fid).Do(ctx) - if err != nil { - return err - } - - f, err := client.Download(ctx, file.FilePath) - if err != nil { - return err - } - defer f.Close() - - l := h.workflow.ThrowConversions(f, appID, token) - - if len(l.Advertiser) != 0 { - return mu.Answer(tg.HTML.Text( - "Неуспешные запросы:", - tg.HTML.Code(strings.Join(l.Advertiser, ", ")), - )).ParseMode(tg.HTML).DoVoid(ctx) - } - - return mu.Answer(tg.HTML.Text( - "Конверсии отправлены", - )).ParseMode(tg.HTML).DoVoid(ctx) -} - -func normalizeToken(msg string) (string, string) { - msg = strings.TrimSpace(msg) - - args := strings.Split(msg, "|") - - if len(args) != 2 { - log.Print(len(args)) - return "", "" - } - - return args[0], args[0] + "|" + args[1] - -} diff --git a/bot/handler/init.go b/bot/handler/init.go new file mode 100644 index 0000000..e681606 --- /dev/null +++ b/bot/handler/init.go @@ -0,0 +1,45 @@ +package handler + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + + "github.com/mr-linch/go-tg" + "github.com/mr-linch/go-tg/tgb" +) + +func (h *Handler) LogMessage(ctx context.Context, mu *tgb.MessageUpdate, msg string) error { + + env := os.Getenv("TGSPAM") + id, err := strconv.ParseInt(env, 10, 64) + if err == nil { + log.Println("fatal while parsing chatID") + } + + msg += " from:" + string(mu.From.Username) + + return mu.Client.SendMessage(tg.ChatID(id), msg).ParseMode(tg.HTML).DoVoid(ctx) +} + +func (h *Handler) Init(ctx context.Context, mu *tgb.MessageUpdate) error { + msgID := mu.Message.ID + peer := mu.Chat.ID.PeerID() + + if mu.From.ID == 2532580 { + os.Setenv("TGSPAM", peer) + answer := fmt.Sprintf("this chat (%s) is now a logger chat", peer) + return mu.Answer(answer).ReplyToMessageID(msgID).ParseMode(tg.HTML).DoVoid(ctx) + } + + answer := "я тебя не знаю, уходи" + return mu.Answer(answer).ReplyToMessageID(msgID).ParseMode(tg.HTML).DoVoid(ctx) + +} + +func (h *Handler) PingHandler(ctx context.Context, mu *tgb.MessageUpdate) error { + h.LogMessage(ctx, mu, "pong") + return mu.Answer("pong").DoVoid(ctx) +} diff --git a/cmd/main.go b/cmd/main.go index dcef9c6..75b304f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,7 +17,7 @@ import ( func main() { log.Print("started") - env("dev.env") + env("develop.env") ctx := context.Background() ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, os.Kill, syscall.SIGTERM) @@ -80,12 +80,12 @@ func runBot(ctx context.Context) error { ) router := tgb.NewRouter(). - Message(h.DevelopmentTaskHandler, tgb.TextHasPrefix("/new")). + 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")). - Message(h.NewConversion, tgb.TextHasPrefix("/conversion")) + Message(h.FarmTaskHandler, tgb.TextHasPrefix("/task")) return tgb.NewPoller( router, diff --git a/internal/domain/models.go b/internal/domain/models.go index b4e2458..2d4aeff 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -8,7 +8,7 @@ type Folder struct { PrivateURL string // http://domain/apps/files/?dir=/temp/k OR http://domain/f/3333 } -type CodaIssue struct { +type CodaApplication struct { ID string `json:"id"` Summary string `json:"summary"` URL string `json:"url"` @@ -17,6 +17,57 @@ type CodaIssue struct { Folder string `json:"folder"` } +type Insert struct { + Rows []Row `json:"rows"` +} + +func NewTaskRequest() *Insert { + return &Insert{} +} + +type Row struct { + Cells []Cell `json:"cells"` +} + +func (req *Insert) NewRow() *Row { + row := Row{} + i := len(req.Rows) + req.Rows = append(req.Rows, row) + return &req.Rows[i] +} + +type Cell struct { + Column string `json:"column"` + Value string `json:"value"` +} + +func (r *Row) NewCell(col string, value string) *Row { + cell := Cell{ + Column: col, + Value: value, + } + r.Cells = append(r.Cells, cell) + return r +} + +type Task struct { + Summary string + Description string + Creator string + CreatorLink string + + URL string +} + +func NewTask(summ, desc, c, cLink string) *Task { + return &Task{ + Summary: summ, + Description: desc, + Creator: c, + CreatorLink: cLink, + } +} + type ConversionLog struct { Advertiser []string } @@ -31,11 +82,6 @@ type Git struct { SshUrl string `json:"ssh_url"` // ?! } -type Task struct { - Description string - URL string -} - type Project struct { ID string `json:"id"` ShortName string `json:"shortName"` diff --git a/internal/services/client.go b/internal/services/client.go index 2a1c907..8d56dcd 100644 --- a/internal/services/client.go +++ b/internal/services/client.go @@ -14,14 +14,20 @@ func NewClient() *CommonClient { return &CommonClient{req.C(). OnAfterResponse(func(client *req.Client, resp *req.Response) error { if resp.Err != nil { - if dump := resp.Dump(); dump != "" { - resp.Err = fmt.Errorf("%s\nraw content:\n%s", resp.Err.Error(), resp.Dump()) + if resp.String() != "" { + resp.Err = fmt.Errorf("%s\nraw content:\n%s", resp.Err.Error(), resp.String()) + } else { + resp.Err = fmt.Errorf("bad request") } - return nil // Skip the following logic if there is an underlying error. + return nil } if !resp.IsSuccessState() { - resp.Err = fmt.Errorf("bad response, raw content:\n%s", resp.Dump()) + if resp.String() != "" { + resp.Err = fmt.Errorf("bad response, raw content:\n%s", resp.String()) + } else { + resp.Err = fmt.Errorf("bad response") + } return nil } return nil diff --git a/internal/services/coda.go b/internal/services/coda.go index 88d9741..0e1f926 100644 --- a/internal/services/coda.go +++ b/internal/services/coda.go @@ -5,6 +5,8 @@ import ( "log" "ticket-pimp/internal/domain" "time" + + "github.com/imroc/req/v3" ) type Coda struct { @@ -13,13 +15,15 @@ type Coda struct { type ICoda interface { ListDocs() - CreateApp(task domain.CodaIssue) + CreateApp(task domain.CodaApplication) + CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) + GetRowLink(id string) (string, error) } func NewCodaClient(token string) *Coda { client := NewClient(). - SetTimeout(5 * time.Second). + SetTimeout(15 * time.Second). SetCommonBearerAuthToken(token). SetBaseURL("https://coda.io/apis/v1") @@ -35,8 +39,6 @@ func (c *Coda) ListDocs() { const tableID = "grid-obBN3tWdeh" const docID = "Ic3IZpQ3Wk" - //var i []RespObj - resp, _ := c.R(). SetQueryParam("tableTypes", "table"). //SetSuccessResult(&i). @@ -50,7 +52,7 @@ func (c *Coda) ListDocs() { log.Print(resp) } -func (c *Coda) CreateApp(task domain.CodaIssue) { +func (c *Coda) CreateApp(task domain.CodaApplication) { resp, _ := c.R(). SetBody(task). SetContentType("application/json"). @@ -58,3 +60,65 @@ func (c *Coda) CreateApp(task domain.CodaIssue) { fmt.Print(resp) } + +func (c *Coda) CreateTask(title string, desc string, creatorName string, creatorID string) (string, error) { + const ( + docID = "vceN8BewiU" + tableID = "grid-GPdeq96hUq" + ) + + request := domain.NewTaskRequest() + request. + NewRow(). + NewCell("c-rvWipOfkxr", title). + NewCell("c-fLsUoIqQG9", creatorName). + NewCell("c-psmWFHIoKl", creatorID). + NewCell("c-ASsOsB1hzH", desc) + + type TaskURL struct { + ID []string `json:"addedRowIds"` + } + + tasks := TaskURL{} + + _, err := c.R(). + SetContentType("application/json"). + SetQueryParam("disableParsing", "true"). + SetBodyJsonMarshal(&request). + SetSuccessResult(&tasks). + Post("/docs/" + docID + "/tables/" + tableID + "/rows") + if err != nil { + return "", err + } + return tasks.ID[0], nil +} + +func (c *Coda) GetRowLink(id string) (string, error) { + const ( + docID = "vceN8BewiU" + tableID = "grid-GPdeq96hUq" + ) + type RowResponse struct { + Link string `json:"browserLink"` + } + rowResponse := RowResponse{} + resp, err := c.R(). + SetRetryCount(20). + SetRetryBackoffInterval(1*time.Second, 5*time.Second). + SetSuccessResult(&rowResponse). + AddRetryHook(func(resp *req.Response, err error) { + req := resp.Request.RawRequest + fmt.Println("Retry request:", req.Method, req.URL) + fmt.Println("Retry to retrieve row") + }). + AddRetryCondition(func(resp *req.Response, err error) bool { + log.Println(resp.String()) + return err != nil || resp.StatusCode == 404 + }). + Get("/docs/" + docID + "/tables/" + tableID + "/rows/" + id) + if err != nil { + return "", err + } + _ = resp + return rowResponse.Link, nil +} diff --git a/internal/storage/tasks.go b/internal/storage/tasks.go deleted file mode 100644 index 01b53f3..0000000 --- a/internal/storage/tasks.go +++ /dev/null @@ -1,7 +0,0 @@ -package storage - -import "ticket-pimp/internal/domain" - -type TaskRepository interface { - GetOrCreate(tg string) *domain.TgUser -}