From 6578db52461d1c91b5e1cabb23c5a3a1fb6ff1c7 Mon Sep 17 00:00:00 2001 From: Alex Kataev Date: Fri, 19 Apr 2024 12:07:10 +0500 Subject: [PATCH 1/2] rename cli to tgsend --- .goreleaser.yaml | 10 +++++----- cmd/{tg => tgsend}/main.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename cmd/{tg => tgsend}/main.go (97%) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ffa9d5e..9ff5732 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,10 +1,10 @@ project_name: tg builds: - - id: tg - main: ./cmd/tg + - id: tgsend + main: ./cmd/tgsend ldflags: - -s -w - binary: tg + binary: tgsend goos: - darwin - linux @@ -15,9 +15,9 @@ builds: - goos: linux goarch: arm64 archives: - - id: tg + - id: tgsend builds: - - tg + - tgsend name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}' format: tar.gz files: diff --git a/cmd/tg/main.go b/cmd/tgsend/main.go similarity index 97% rename from cmd/tg/main.go rename to cmd/tgsend/main.go index 36b946d..f501635 100644 --- a/cmd/tg/main.go +++ b/cmd/tgsend/main.go @@ -19,7 +19,7 @@ func logFatal(log *slog.Logger, msg string) { } func main() { - fset := flag.NewFlagSet("", flag.ContinueOnError) + fset := flag.NewFlagSet("tgsend", flag.ContinueOnError) fset.SetOutput(io.Discard) token := fset.String("token", "", "token (environment TG_TOKEN)") From cb6c6b43b342a1580d143bf5fe210836b07b5700 Mon Sep 17 00:00:00 2001 From: Alex Kataev Date: Fri, 19 Apr 2024 12:15:58 +0500 Subject: [PATCH 2/2] add new cli --- .goreleaser.yaml | 21 +++ cmd/tg/internal/cmd/cmd.go | 266 +++++++++++++++++++++++++++++++++++++ cmd/tg/main.go | 182 +++++++++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 cmd/tg/internal/cmd/cmd.go create mode 100644 cmd/tg/main.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 9ff5732..ab6185a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -14,6 +14,20 @@ builds: ignore: - goos: linux goarch: arm64 + - id: tg + main: ./cmd/tg + ldflags: + - -s -w + binary: tg + goos: + - darwin + - linux + goarch: + - amd64 + - arm64 + ignore: + - goos: linux + goarch: arm64 archives: - id: tgsend builds: @@ -22,6 +36,13 @@ archives: format: tar.gz files: - xyz* + - id: tg + builds: + - tg + name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}' + format: tar.gz + files: + - xyz* checksum: name_template: '{{ .ProjectName }}_v{{ .Version }}_checksums.txt' algorithm: sha256 diff --git a/cmd/tg/internal/cmd/cmd.go b/cmd/tg/internal/cmd/cmd.go new file mode 100644 index 0000000..58be037 --- /dev/null +++ b/cmd/tg/internal/cmd/cmd.go @@ -0,0 +1,266 @@ +package cmd + +import ( + "errors" + "flag" + "fmt" + "io" + "os" + "path" + "reflect" + "sort" + "strings" +) + +type FlagFunc func(*flag.FlagSet) + +func EmptyFlagFunc() func(*flag.FlagSet) { + return func(_ *flag.FlagSet) {} +} + +type RunFunc func() error + +type command struct { + name string + desc string + flag *flag.FlagSet + run RunFunc +} + +type Commander struct { + output io.Writer + cmds map[string]*command +} + +const rootCmd = "_" + +func New() *Commander { + cm := new(Commander) + cm.output = os.Stderr + cm.cmds = make(map[string]*command) + + cm.cmds[rootCmd] = new(command) + cm.cmds[rootCmd].name = path.Base(os.Args[0]) + cm.cmds[rootCmd].flag = flag.NewFlagSet("", flag.ContinueOnError) + cm.cmds[rootCmd].flag.SetOutput(io.Discard) + + return cm +} + +func (c *Commander) SetOutput(out io.Writer) { + c.output = out +} + +func defaultValue(value flag.Value, defValue string) string { + flagType := reflect.TypeOf(value) + + var newValue reflect.Value + + if flagType.Kind() == reflect.Pointer { + newValue = reflect.New(flagType.Elem()) + } else { + newValue = reflect.Zero(flagType) + } + + newFlag, ok := newValue.Interface().(flag.Value) + if !ok || defValue == newFlag.String() || defValue == "" { + return "" + } + + if newValue.String() == "<*flag.stringValue Value>" { + return fmt.Sprintf(" (default %q)", defValue) + } + + return fmt.Sprintf(" (default %v)", defValue) +} + +func (c *Commander) usage(fset *flag.FlagSet) { + maxLen := 0 + + fset.VisitAll(func(ff *flag.Flag) { + name, _ := flag.UnquoteUsage(ff) + + strLen := len(name) + len(ff.Name) + + if strLen > maxLen { + maxLen = strLen + } + }) + + maxLen += 4 + + fset.VisitAll(func(ff *flag.Flag) { + name, _ := flag.UnquoteUsage(ff) + + spaces := strings.Repeat(" ", maxLen-(len(name)+len(ff.Name))) + def := defaultValue(ff.Value, ff.DefValue) + + fmt.Fprintf(c.output, " --%s %s%s%s%s\n", ff.Name, name, spaces, ff.Usage, def) + }) +} + +func (c *Commander) Root(name string, fn FlagFunc) { + c.cmds[rootCmd].name = name + + fn(c.cmds[rootCmd].flag) +} + +func (c *Commander) rootHelp() { + fmt.Fprintf(c.output, "Usage:\n %s [flags]", c.cmds[rootCmd].name) + + if len(c.cmds) > 1 { + fmt.Fprintf(c.output, " [command]\n\nAvailable Commands:\n") + + cmds := make([]string, 0, len(c.cmds)) + + maxCmdLen := 0 + + for cmd := range c.cmds { + if cmd == "_" { + continue + } + + cmds = append(cmds, cmd) + + if len(cmd) > maxCmdLen { + maxCmdLen = len(cmd) + } + } + + sort.Strings(cmds) + + maxCmdLen += 4 + + for _, cmd := range cmds { + spaces := strings.Repeat(" ", maxCmdLen-len(cmd)) + + fmt.Fprintf(c.output, " %s%s%s\n", cmd, spaces, c.cmds[cmd].desc) + } + } else { + fmt.Fprint(c.output, "\n") + } + + fmt.Fprint(c.output, "\nFlags:\n") + + c.usage(c.cmds[rootCmd].flag) +} + +func (c *Commander) rootError(err error) { + fmt.Fprintf(c.output, "Error: %s\n\nRun '%s --help' for usage.\n", err, c.cmds[rootCmd].name) +} + +func (c *Commander) Command(name, desc string, fn FlagFunc, runFn RunFunc) { + fset := flag.NewFlagSet(name, flag.ContinueOnError) + fset.SetOutput(io.Discard) + + fn(fset) + + c.cmds[name] = &command{ + name: name, + desc: desc, + flag: fset, + run: runFn, + } +} + +func (c *Commander) commandHelp(name string) { + if cmd, ok := c.cmds[name]; ok { + if cmd.desc != "" { + fmt.Fprintf(c.output, "Description: %s\n\n", cmd.desc) + } + } + + flags := false + + c.cmds[name].flag.VisitAll( + func(*flag.Flag) { + if !flags { + flags = true + } + }, + ) + + fmt.Fprintf(c.output, "Usage:\n %s [global flags] %s", c.cmds[rootCmd].name, name) + + if flags { + fmt.Fprint(c.output, " [flags]\n\nFlags:\n") + + if cmd, ok := c.cmds[name]; ok { + c.usage(cmd.flag) + } + } else { + fmt.Fprint(c.output, "\n") + } + + fmt.Fprint(c.output, "\nGlobal Flags:\n") + + c.usage(c.cmds[rootCmd].flag) +} + +func (c *Commander) commandError(name string, err error) { + fmt.Fprintf(c.output, "Error: %s\n\nRun '%s %s--help' for usage.\n", err, c.cmds[rootCmd].name, name) +} + +func (c *Commander) Run() { //nolint:cyclop + if err := c.cmds[rootCmd].flag.Parse(os.Args[1:]); err != nil { + if errors.Is(flag.ErrHelp, err) { + c.rootHelp() + } else { + c.rootError(err) + } + + os.Exit(1) + } + + if len(c.cmds[rootCmd].flag.Args()) == 0 { + c.rootHelp() + + os.Exit(1) + } + + name := c.cmds[rootCmd].flag.Args()[0] + args := c.cmds[rootCmd].flag.Args()[1:] + + cmd, ok := c.cmds[name] + if !ok || name == rootCmd { + err := fmt.Sprintf("unknown command %q", name) + + c.rootError(errors.New(err)) //nolint:goerr113 + + os.Exit(1) + } + + if len(args) == 0 { + flags := false + + c.cmds[name].flag.VisitAll(func(_ *flag.Flag) { + if !flags { + flags = true + } + }) + + if flags { + c.commandHelp(name) + + os.Exit(1) + } + } + + if err := cmd.flag.Parse(args); err != nil { + if errors.Is(flag.ErrHelp, err) { + c.commandHelp(name) + } else { + c.commandError(name, err) + } + + os.Exit(1) + } + + if err := cmd.run(); err != nil { + fmt.Fprintln(c.output, "Error: ", err.Error()) + + os.Exit(1) + } + + os.Exit(0) +} diff --git a/cmd/tg/main.go b/cmd/tg/main.go new file mode 100644 index 0000000..9a7b35b --- /dev/null +++ b/cmd/tg/main.go @@ -0,0 +1,182 @@ +//nolint:wrapcheck +package main + +import ( + "context" + "errors" + "flag" + "io" + "log/slog" + "os" + + "github.com/a-kataev/tg" + "github.com/a-kataev/tg/cmd/tg/internal/cmd" +) + +type flags struct { + token string + chatID int64 + text string + parseMode string + messageID int64 + messageThreadID int64 + disableWebPagePreview bool + disableNotification bool + protectContent bool +} + +func (f *flags) tokenFormEnv() { + if f.token == "" { + f.token = os.Getenv("TG_TOKEN") + } +} + +func (f *flags) textFromPipe() error { + if f.text == "-" { + stdin, err := io.ReadAll(io.LimitReader(os.Stdin, int64(tg.MaxTextSize))) + if err != nil { + if !errors.Is(err, io.EOF) { + return err + } + } + + f.text = string(stdin) + } + + return nil +} + +func (f *flags) rootFlags() func(*flag.FlagSet) { + return func(fset *flag.FlagSet) { + fset.StringVar(&f.token, "token", "", "bot token") + } +} + +func (f *flags) sendFlags() func(*flag.FlagSet) { + return func(fset *flag.FlagSet) { + fset.Int64Var(&f.chatID, "chat-id", 0, "chat id") + fset.StringVar(&f.text, "text", "", "text (use - for read pipe)") + fset.StringVar(&f.parseMode, "parse-mode", "Markdown", "parse mode") + fset.Int64Var(&f.messageThreadID, "message-thread-id", 0, "message thread id") + fset.BoolVar(&f.disableWebPagePreview, "disable-web-page-preview", false, "disable web page preview") + fset.BoolVar(&f.disableNotification, "disable-notification", false, "disable notification") + fset.BoolVar(&f.protectContent, "protect-content", false, "protect content") + } +} + +func (f *flags) sendRun(ctx context.Context, log *slog.Logger) func() error { + return func() error { + f.tokenFormEnv() + + if err := f.textFromPipe(); err != nil { + return err + } + + client, err := tg.NewClient(f.token) + if err != nil { + return err + } + + msg, err := client.SendMessage(ctx, f.chatID, f.text, + tg.ParseModeSendOption(tg.ParseMode(f.parseMode)), + tg.MessageThreadIDSendOption(f.messageThreadID), + tg.DisableWebPagePreviewSendOption(f.disableWebPagePreview), + tg.DisableNotificationSendOption(f.disableNotification), + tg.ProtectContentSendOption(f.protectContent), + ) + if err != nil { + return err + } + + log.Info("Success send message", + slog.Int64("chat_id", f.chatID), + slog.Any("message_id", msg.MessageID), + ) + + return nil + } +} + +func (f *flags) editFlags() func(*flag.FlagSet) { + return func(fset *flag.FlagSet) { + fset.Int64Var(&f.chatID, "chat-id", 0, "chat id") + fset.StringVar(&f.text, "text", "", "text (use - for read pipe)") + fset.StringVar(&f.parseMode, "parse-mode", "Markdown", "parse mode") + fset.Int64Var(&f.messageID, "message-id", 0, "message id") + } +} + +func (f *flags) editRun(ctx context.Context, log *slog.Logger) func() error { + return func() error { + f.tokenFormEnv() + + if err := f.textFromPipe(); err != nil { + return err + } + + client, err := tg.NewClient(f.token) + if err != nil { + return err + } + + msg, err := client.EditMessage(ctx, f.chatID, f.messageID, f.text, + tg.ParseModeEditOption(tg.ParseMode(f.parseMode)), + ) + if err != nil { + return err + } + + log.Info("Success edit message", + slog.Int64("chat_id", f.chatID), + slog.Any("message_id", msg.MessageID), + ) + + return nil + } +} + +func (f *flags) deleteFlags() func(*flag.FlagSet) { + return func(fset *flag.FlagSet) { + fset.Int64Var(&f.chatID, "chat-id", 0, "chat id") + fset.Int64Var(&f.messageID, "message-id", 0, "message id") + } +} + +func (f *flags) deleteRun(ctx context.Context, log *slog.Logger) func() error { + return func() error { + f.tokenFormEnv() + + client, err := tg.NewClient(f.token) + if err != nil { + return err + } + + _, err = client.DeleteMessage(ctx, f.chatID, f.messageID) + if err != nil { + return err + } + + log.Info("Success delete message", + slog.Int64("chat_id", f.chatID), + slog.Any("message_id", f.messageID), + ) + + return nil + } +} + +func main() { + ctx := context.Background() + log := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + app := cmd.New() + + flags := new(flags) + + app.Root("tg", flags.rootFlags()) + app.Command("send", "send message", flags.sendFlags(), flags.sendRun(ctx, log)) + app.Command("edit", "edit message", flags.editFlags(), flags.editRun(ctx, log)) + app.Command("delete", "delete message", flags.deleteFlags(), flags.deleteRun(ctx, log)) + + app.Run() +}