diff --git a/botcli/botcli.go b/botcli/botcli.go index bad5215..97e20fd 100644 --- a/botcli/botcli.go +++ b/botcli/botcli.go @@ -1,10 +1,13 @@ package botcli import ( + "context" "os" "strconv" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/transport" "github.com/spf13/cobra" "go.uber.org/zap" ) @@ -19,14 +22,17 @@ type Callback func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []s // A CLI program, with subcommands that help configuring and running a Delta Chat bot. type BotCli struct { - AppName string - AppDir string - RootCmd *cobra.Command - Logger *zap.SugaredLogger - cmdsMap map[string]Callback - parsedCmd *_ParsedCmd - onInit Callback - onStart Callback + AppName string + // AppDir can be set by the --folder flag in command line + AppDir string + // SelectedAddr can be set by the --account flag in command line, if empty it means "all accounts" + SelectedAddr string + RootCmd *cobra.Command + Logger *zap.SugaredLogger + cmdsMap map[string]Callback + parsedCmd *_ParsedCmd + onInit Callback + onStart Callback } // Create a new BotCli instance. @@ -64,21 +70,24 @@ func (self *BotCli) Start() error { if err != nil { return err } - rpc := deltachat.NewRpcIO() - rpc.AccountsDir = getAccountsDir(self.AppDir) - defer rpc.Stop() - if err := rpc.Start(); err != nil { + + trans := transport.NewIOTransport() + trans.AccountsDir = getAccountsDir(self.AppDir) + rpc := &deltachat.Rpc{Context: context.Background(), Transport: trans} + defer trans.Close() + if err := trans.Open(); err != nil { self.Logger.Panicf("Failed to start RPC server, read https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-server for installation instructions. Error message: %v", err) } - bot := deltachat.NewBotFromAccountManager(&deltachat.AccountManager{Rpc: rpc}) - bot.On(deltachat.EventInfo{}, func(bot *deltachat.Bot, event deltachat.Event) { - self.Logger.Info(event.(deltachat.EventInfo).Msg) + + bot := deltachat.NewBot(rpc) + bot.On(deltachat.EventInfo{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.Logger.Infof("[acc=%v] %v", accId, event.(deltachat.EventInfo).Msg) }) - bot.On(deltachat.EventWarning{}, func(bot *deltachat.Bot, event deltachat.Event) { - self.Logger.Warn(event.(deltachat.EventWarning).Msg) + bot.On(deltachat.EventWarning{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.Logger.Warnf("[acc=%v] %v", accId, event.(deltachat.EventWarning).Msg) }) - bot.On(deltachat.EventError{}, func(bot *deltachat.Bot, event deltachat.Event) { - self.Logger.Error(event.(deltachat.EventError).Msg) + bot.On(deltachat.EventError{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.Logger.Errorf("[acc=%v] %v", accId, event.(deltachat.EventError).Msg) }) if self.onInit != nil { self.onInit(self, bot, self.parsedCmd.cmd, self.parsedCmd.args) @@ -105,97 +114,142 @@ func (self *BotCli) AddCommand(cmd *cobra.Command, callback Callback) { // Store a custom program setting in the given bot. The setting is specific to your application. // // The setting is stored using Bot.SetUiConfig() and the key is prefixed with BotCli.AppName. -func (self *BotCli) SetConfig(bot *deltachat.Bot, key, value string) error { - return bot.SetUiConfig(self.AppName+"."+key, value) +func (self *BotCli) SetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string, value option.Option[string]) error { + return bot.SetUiConfig(accId, self.AppName+"."+key, value) } // Get a custom program setting from the given bot. The setting is specific to your application. // // The setting is retrieved using Bot.GetUiConfig() and the key is prefixed with BotCli.AppName. -func (self *BotCli) GetConfig(bot *deltachat.Bot, key string) (string, error) { - return bot.GetUiConfig(self.AppName + "." + key) +func (self *BotCli) GetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string) (option.Option[string], error) { + return bot.GetUiConfig(accId, self.AppName+"."+key) } // Get the group of bot administrators. -func (self *BotCli) AdminChat(bot *deltachat.Bot) (*deltachat.Chat, error) { - if !bot.IsConfigured() { - return nil, &BotNotConfiguredErr{} +func (self *BotCli) AdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + return 0, &BotNotConfiguredErr{} } - value, err := self.GetConfig(bot, "admin-chat") + value, err := self.GetConfig(bot, accId, "admin-chat") if err != nil { - return nil, err + return 0, err } - var chat *deltachat.Chat + var chatId deltachat.ChatId - if value != "" { - chatId, err := strconv.ParseUint(value, 10, 0) + if value.IsSome() { + chatIdInt, err := strconv.ParseUint(value.Unwrap(), 10, 0) if err != nil { - return nil, err + return 0, err } - chat = &deltachat.Chat{Account: bot.Account, Id: deltachat.ChatId(chatId)} + chatId = deltachat.ChatId(chatIdInt) var selfInGroup bool - contacts, err := chat.Contacts() + contacts, err := bot.Rpc.GetChatContacts(accId, chatId) if err != nil { - return nil, err + return 0, err } - me := bot.Me() - for _, contact := range contacts { - if me.Id == contact.Id { + for _, contactId := range contacts { + if contactId == deltachat.ContactSelf { selfInGroup = true break } } if !selfInGroup { - value = "" + value = option.None[string]() } } - if value == "" { - chat, err = self.ResetAdminChat(bot) + if value.IsNone() { + chatId, err = self.ResetAdminChat(bot, accId) if err != nil { - return nil, err + return 0, err } } - return chat, nil + return chatId, nil } // Reset the group of bot administrators, all the members of the old group are no longer admins. -func (self *BotCli) ResetAdminChat(bot *deltachat.Bot) (*deltachat.Chat, error) { - if !bot.IsConfigured() { - return nil, &BotNotConfiguredErr{} +func (self *BotCli) ResetAdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + return 0, &BotNotConfiguredErr{} } - chat, err := bot.Account.CreateGroup("Bot Administrators", true) + chatId, err := bot.Rpc.CreateGroupChat(accId, "Bot Administrators", true) if err != nil { - return nil, err + return 0, err } - value := strconv.FormatUint(uint64(chat.Id), 10) - err = self.SetConfig(bot, "admin-chat", value) + value := strconv.FormatUint(uint64(chatId), 10) + err = self.SetConfig(bot, accId, "admin-chat", option.Some(value)) if err != nil { - return nil, err + return 0, err } - return chat, nil + return chatId, nil } // Returns true if contact is in the bot administrators group, false otherwise. -func (self *BotCli) IsAdmin(bot *deltachat.Bot, contact *deltachat.Contact) (bool, error) { - chat, err := self.AdminChat(bot) +func (self *BotCli) IsAdmin(bot *deltachat.Bot, accId deltachat.AccountId, contactId deltachat.ContactId) (bool, error) { + chatId, err := self.AdminChat(bot, accId) if err != nil { return false, err } - contacts, err := chat.Contacts() + contacts, err := bot.Rpc.GetChatContacts(accId, chatId) if err != nil { return false, err } - for _, member := range contacts { - if contact.Id == member.Id { + for _, memberId := range contacts { + if contactId == memberId { return true, nil } } return false, nil } + +// Get account for address, if no account exists create a new one +func (self *BotCli) GetOrCreateAccount(rpc *deltachat.Rpc, addr string) (deltachat.AccountId, error) { + accId, err := self.GetAccount(rpc, addr) + if err != nil { + accId, err = rpc.AddAccount() + if err != nil { + return 0, err + } + rpc.SetConfig(accId, "addr", option.Some(addr)) //nolint:errcheck + } + return accId, nil +} + +// Get account for address, if no account exists with the given address, an error is returned +func (self *BotCli) GetAccount(rpc *deltachat.Rpc, addr string) (deltachat.AccountId, error) { + chatIdInt, err := strconv.ParseUint(addr, 10, 0) + if err == nil { + return deltachat.AccountId(chatIdInt), nil + } + + accounts, _ := rpc.GetAllAccountIds() + for _, accId := range accounts { + addr2, _ := self.GetAddress(rpc, accId) + if addr == addr2 { + return accId, nil + } + } + return 0, &AccountNotFoundErr{Addr: addr} +} + +// Get the address of the given account +func (self *BotCli) GetAddress(rpc *deltachat.Rpc, accId deltachat.AccountId) (string, error) { + var addr option.Option[string] + var err error + isConf, err := rpc.IsConfigured(accId) + if err != nil { + return "", err + } + if isConf { + addr, err = rpc.GetConfig(accId, "configured_addr") + } else { + addr, err = rpc.GetConfig(accId, "addr") + } + return addr.UnwrapOr(""), err +} diff --git a/botcli/botcli_test.go b/botcli/botcli_test.go index 23ffb11..3cdd6e2 100644 --- a/botcli/botcli_test.go +++ b/botcli/botcli_test.go @@ -3,47 +3,37 @@ package botcli import ( "testing" - "github.com/deltachat/deltachat-rpc-client-go/acfactory" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) func TestBotCli_SetConfig(t *testing.T) { t.Parallel() - bot := acfactory.OnlineBot() - defer acfactory.StopRpc(bot) - - cli := New("testbot") - assert.Nil(t, cli.SetConfig(bot, "testkey", "testing")) - value, err := cli.GetConfig(bot, "testkey") - assert.Nil(t, err) - assert.Equal(t, "testing", value) + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { + cli := New("testbot") + assert.Nil(t, cli.SetConfig(bot, accId, "testkey", option.Some("testing"))) + value, err := cli.GetConfig(bot, accId, "testkey") + assert.Nil(t, err) + assert.Equal(t, "testing", value.UnwrapOr("")) + }) } func TestBotCli_AdminChat(t *testing.T) { t.Parallel() - bot := acfactory.OnlineBot() - defer acfactory.StopRpc(bot) - - cli := New("testbot") - chat1, err := cli.AdminChat(bot) - assert.Nil(t, err) - chat2, err := cli.ResetAdminChat(bot) - assert.Nil(t, err) - assert.NotEqual(t, chat2.Id, chat1.Id) - - isAdmin, err := cli.IsAdmin(bot, bot.Me()) - assert.Nil(t, err) - assert.True(t, isAdmin) - - acfactory.StopRpc(bot) - _, err = cli.AdminChat(bot) - assert.NotNil(t, err) - _, err = cli.ResetAdminChat(bot) - assert.NotNil(t, err) - _, err = cli.IsAdmin(bot, bot.Me()) - assert.NotNil(t, err) + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { + cli := New("testbot") + chatId1, err := cli.AdminChat(bot, accId) + assert.Nil(t, err) + chatId2, err := cli.ResetAdminChat(bot, accId) + assert.Nil(t, err) + assert.NotEqual(t, chatId2, chatId1) + + isAdmin, err := cli.IsAdmin(bot, accId, deltachat.ContactSelf) + assert.Nil(t, err) + assert.True(t, isAdmin) + }) } func TestBotCli_AddCommand(t *testing.T) { @@ -79,22 +69,15 @@ func TestBotCli_OnBotStart(t *testing.T) { cliBot.Stop() } -func TestBotCli_OnBotInit(t *testing.T) { +func TestBotCli_serve(t *testing.T) { t.Parallel() cli := New("testbot") - onEventInfoCalled := make(chan deltachat.Event, 1) onNewMsgCalled := make(chan *deltachat.MsgSnapshot, 1) var cliBot *deltachat.Bot cli.OnBotInit(func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { cliBot = bot - bot.On(deltachat.EventInfo{}, func(bot *deltachat.Bot, event deltachat.Event) { - select { - case onEventInfoCalled <- event: - default: - } - }) - bot.OnNewMsg(func(bot *deltachat.Bot, msg *deltachat.Message) { - snapshot, _ := msg.Snapshot() + bot.OnNewMsg(func(bot *deltachat.Bot, accId deltachat.AccountId, msgId deltachat.MsgId) { + snapshot, _ := bot.Rpc.GetMessage(accId, msgId) select { case onNewMsgCalled <- snapshot: default: @@ -109,70 +92,50 @@ func TestBotCli_OnBotInit(t *testing.T) { } defer cliBot.Stop() - user := acfactory.OnlineAccount() - defer acfactory.StopRpc(user) + acfactory.WithOnlineAccount(func(rpc *deltachat.Rpc, accId deltachat.AccountId) { + chatWithBot := acfactory.CreateChat(rpc, accId, cliBot.Rpc, 1) - assert.IsType(t, deltachat.EventInfo{}, <-onEventInfoCalled) - - chatWithBot, err := acfactory.CreateChat(user, cliBot.Account) - assert.Nil(t, err) - - _, err = chatWithBot.SendText("hi") - assert.Nil(t, err) - msg := <-onNewMsgCalled - assert.Equal(t, "hi", msg.Text) + _, err := rpc.MiscSendTextMessage(accId, chatWithBot, "hi") + assert.Nil(t, err) + msg := <-onNewMsgCalled + assert.Equal(t, "hi", msg.Text) + }) } func TestInitCallback(t *testing.T) { t.Parallel() - acc := acfactory.UnconfiguredAccount() - defer acfactory.StopRpc(acc) - - addr, err := acc.GetConfig("addr") - assert.Nil(t, err) - err = acc.SetConfig("addr", "") - assert.Nil(t, err) - password, err := acc.GetConfig("mail_pw") - assert.Nil(t, err) - err = acc.SetConfig("mail_pw", "") - assert.Nil(t, err) + acfactory.WithUnconfiguredAccount(func(rpc *deltachat.Rpc, accId deltachat.AccountId) { + addr, err := rpc.GetConfig(accId, "addr") + assert.Nil(t, err) + password, err := rpc.GetConfig(accId, "mail_pw") + assert.Nil(t, err) + err = rpc.SetConfig(accId, "mail_pw", option.None[string]()) + assert.Nil(t, err) + configured, _ := rpc.IsConfigured(accId) + assert.False(t, configured) + + cli := New("testbot") + cli.OnBotInit(func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + bot.Rpc = rpc + }) + _, err = RunCli(cli, "init", addr.Unwrap(), password.Unwrap()) + assert.Nil(t, err) - cli := New("testbot") - cli.OnBotInit(func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.Account = acc + configured, _ = rpc.IsConfigured(accId) + assert.True(t, configured) }) - - configured, _ := acc.IsConfigured() - assert.False(t, configured) - - _, err = RunCli(cli, "init", addr, password) - assert.Nil(t, err) - - configured, _ = acc.IsConfigured() - assert.True(t, configured) } func TestConfigCallback(t *testing.T) { t.Parallel() var err error - var cliBot *deltachat.Bot cli := New("testbot") - cli.OnBotInit(func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - cliBot = bot - }) _, err = RunCli(cli, "config", "addr") assert.Nil(t, err) _, err = RunCli(cli, "config", "addr", "test@example.com") assert.Nil(t, err) - - assert.Nil(t, cliBot.Account.Manager.Rpc.Start()) - defer acfactory.StopRpc(cliBot) - - addr, err := cliBot.GetConfig("addr") - assert.Nil(t, err) - assert.Equal(t, "test@example.com", addr) } func TestQrCallback(t *testing.T) { diff --git a/botcli/cmd.go b/botcli/cmd.go index 09c84be..54199bb 100644 --- a/botcli/cmd.go +++ b/botcli/cmd.go @@ -2,24 +2,39 @@ package botcli import ( "fmt" - "strconv" "strings" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" "github.com/spf13/cobra" ) func initializeRootCmd(cli *BotCli) { defDir := getDefaultAppDir(cli.AppName) cli.RootCmd.PersistentFlags().StringVarP(&cli.AppDir, "folder", "f", defDir, "program's data folder") + cli.RootCmd.PersistentFlags().StringVarP(&cli.SelectedAddr, "account", "a", "", "operate over this account only when running any subcommand") initCmd := &cobra.Command{ Use: "init", - Short: "initialize the Delta Chat account", + Short: "do initial login configuration of a new Delta Chat account, if the account already exist, the credentials are updated", Args: cobra.ExactArgs(2), } cli.AddCommand(initCmd, initCallback) + listCmd := &cobra.Command{ + Use: "list", + Short: "show a list of existing bot accounts", + Args: cobra.ExactArgs(0), + } + cli.AddCommand(listCmd, listCallback) + + removeCmd := &cobra.Command{ + Use: "remove", + Short: "remove Delta Chat accounts from the bot", + Args: cobra.ExactArgs(0), + } + cli.AddCommand(removeCmd, removeCallback) + configCmd := &cobra.Command{ Use: "config", Short: "set/get account configuration values", @@ -53,16 +68,39 @@ func initializeRootCmd(cli *BotCli) { } func initCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.On(deltachat.EventConfigureProgress{}, func(bot *deltachat.Bot, event deltachat.Event) { + bot.On(deltachat.EventConfigureProgress{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { ev := event.(deltachat.EventConfigureProgress) - cli.Logger.Infof("Configuration progress: %v", ev.Progress) + addr, _ := cli.GetAddress(bot.Rpc, accId) + if addr == "" { + addr = fmt.Sprintf("account #%v", accId) + } + cli.Logger.Infof("[%v] Configuration progress: %v", addr, ev.Progress) }) + var accId deltachat.AccountId + var err error + if cli.SelectedAddr == "" { // auto-select based on first argument (or create a new one if not found) + accId, err = cli.GetOrCreateAccount(bot.Rpc, args[0]) + } else { // re-configure the selected account + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + if err == nil { + _, err = cli.GetAccount(bot.Rpc, args[0]) + if err == nil { + cli.Logger.Errorf("Configuration failed: an account with address %q already exists", args[0]) + return + } + } + } + if err != nil { + cli.Logger.Errorf("Configuration failed: %v", err) + return + } + go func() { - if err := bot.Configure(args[0], args[1]); err != nil { + if err := bot.Configure(accId, args[0], args[1]); err != nil { cli.Logger.Errorf("Configuration failed: %v", err) } else { - cli.Logger.Info("Account configured successfully.") + cli.Logger.Infof("Account %q configured successfully.", args[0]) } bot.Stop() }() @@ -70,49 +108,122 @@ func initCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []st } func configCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - var val string var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // set config for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + configForAcc(cli, bot, cmd, args, accId) + fmt.Println("") + } +} + +func configForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { if len(args) == 0 { - val, _ := bot.GetConfig("sys.config_keys") - for _, key := range strings.Fields(val) { - val, _ := bot.GetConfig(key) - fmt.Printf("%v=%q\n", key, val) + keys, _ := bot.Rpc.GetConfig(accId, "sys.config_keys") + for _, key := range strings.Fields(keys.Unwrap()) { + val, _ := bot.Rpc.GetConfig(accId, key) + fmt.Printf("%v=%q\n", key, val.UnwrapOr("")) } return } + var val option.Option[string] + var err error if len(args) == 2 { - err = bot.SetConfig(args[0], args[1]) + err = bot.Rpc.SetConfig(accId, args[0], option.Some(args[1])) } if err == nil { - val, err = bot.GetConfig(args[0]) + val, err = bot.Rpc.GetConfig(accId, args[0]) } if err == nil { - fmt.Printf("%v=%v\n", args[0], val) + fmt.Printf("%v=%v\n", args[0], val.UnwrapOr("")) } else { cli.Logger.Error(err) } } func serveCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if bot.IsConfigured() { + if cli.SelectedAddr != "" { + cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") + return + } + + accounts, err := bot.Rpc.GetAllAccountIds() + if err != nil { + cli.Logger.Error(err) + return + } + var addrs []string + for _, accId := range accounts { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + cli.Logger.Errorf("account #%v not configured", accId) + } else { + addr, _ := bot.Rpc.GetConfig(accId, "configured_addr") + if addr.UnwrapOr("") != "" { + addrs = append(addrs, addr.Unwrap()) + } + } + } + if len(addrs) != 0 { + cli.Logger.Infof("Listening at: %v", strings.Join(addrs, ", ")) if cli.onStart != nil { cli.onStart(cli, bot, cmd, args) } bot.Run() //nolint:errcheck - } else { - cli.Logger.Error("account not configured") } } func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if bot.IsConfigured() { - qrdata, _, err := bot.Account.QrCode() + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + qrForAcc(cli, bot, cmd, args, accId, addr) + fmt.Println("") + } +} + +func qrForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId, addr string) { + if isConf, _ := bot.Rpc.IsConfigured(accId); isConf { + qrdata, _, err := bot.Rpc.GetChatSecurejoinQrCodeSvg(accId, option.None[deltachat.ChatId]()) if err != nil { cli.Logger.Errorf("Failed to generate QR: %v", err) return } - addr, _ := bot.GetConfig("configured_addr") fmt.Println("Scan this QR to verify", addr) invert, _ := cmd.Flags().GetBool("invert") printQr(qrdata, invert) @@ -123,7 +234,34 @@ func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []stri } func adminCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if !bot.IsConfigured() { + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + adminForAcc(cli, bot, cmd, args, accId) + fmt.Println("") + } +} + +func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { cli.Logger.Error("account not configured") return } @@ -135,64 +273,79 @@ func adminCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []s cli.Logger.Errorf(errMsg, err) return } - var value string - if !reset { - value, err = cli.GetConfig(bot, "admin-chat") - if err != nil { - cli.Logger.Errorf(errMsg, err) - return - } + var chatId deltachat.ChatId + if reset { + chatId, err = cli.ResetAdminChat(bot, accId) + } else { + chatId, err = cli.AdminChat(bot, accId) + } + if err != nil { + cli.Logger.Errorf(errMsg, err) + return } - var chat *deltachat.Chat + qrdata, _, err := bot.Rpc.GetChatSecurejoinQrCodeSvg(accId, option.Some(chatId)) + if err != nil { + cli.Logger.Errorf(errMsg, err) + return + } - if value != "" { - chatId, err := strconv.ParseUint(value, 10, 0) - if err != nil { - cli.Logger.Errorf(errMsg, err) - return - } - chat = &deltachat.Chat{Account: bot.Account, Id: deltachat.ChatId(chatId)} - var selfInGroup bool - contacts, err := chat.Contacts() - if err != nil { - cli.Logger.Errorf(errMsg, err) - return - } - me := bot.Me() - for _, contact := range contacts { - if me.Id == contact.Id { - selfInGroup = true - break - } - } - if !selfInGroup { - value = "" - } + fmt.Println("Scan this QR to become bot administrator") + invert, _ := cmd.Flags().GetBool("invert") + printQr(qrdata, invert) + fmt.Println(qrdata) +} + +func listCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + if cli.SelectedAddr != "" { + cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") + return } - if value == "" { - chat, err = bot.Account.CreateGroup("Bot Administrators", true) + accounts, err := bot.Rpc.GetAllAccountIds() + if err != nil { + cli.Logger.Error(err) + return + } + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) if err != nil { - cli.Logger.Errorf(errMsg, err) - return + cli.Logger.Error(err) + continue } - value = strconv.FormatUint(uint64(chat.Id), 10) - err = cli.SetConfig(bot, "admin-chat", value) - if err != nil { - cli.Logger.Errorf(errMsg, err) - return + + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + addr = addr + " (not configured)" } + fmt.Printf("#%v - %v\n", accId, addr) } +} - qrdata, _, err := chat.QrCode() +func removeCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } if err != nil { - cli.Logger.Errorf(errMsg, err) + cli.Logger.Error(err) return } - fmt.Println("Scan this QR to become bot administrator") - invert, _ := cmd.Flags().GetBool("invert") - printQr(qrdata, invert) - fmt.Println(qrdata) + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + } + err = bot.Rpc.RemoveAccount(accId) + if err != nil { + cli.Logger.Error(err) + } else { + cli.Logger.Infof("Account #%v (%q) removed successfully.", accId, addr) + } + } } diff --git a/botcli/errors.go b/botcli/errors.go index ff29de1..ad66ad6 100644 --- a/botcli/errors.go +++ b/botcli/errors.go @@ -6,3 +6,10 @@ type BotNotConfiguredErr struct{} func (self *BotNotConfiguredErr) Error() string { return "bot account not configured" } + +// The account was not found. +type AccountNotFoundErr struct{ Addr string } + +func (self *AccountNotFoundErr) Error() string { + return "account not found: " + self.Addr +} diff --git a/botcli/main_test.go b/botcli/main_test.go index 732c42d..9ba63f2 100644 --- a/botcli/main_test.go +++ b/botcli/main_test.go @@ -5,14 +5,24 @@ import ( "path/filepath" "testing" - "github.com/deltachat/deltachat-rpc-client-go/acfactory" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/transport" ) +var acfactory *deltachat.AcFactory + +func TestMain(m *testing.M) { + acfactory = &deltachat.AcFactory{} + acfactory.TearUp() + defer acfactory.TearDown() + m.Run() +} + func RunConfiguredCli(cli *BotCli, args ...string) (output string, err error) { - bot := acfactory.OnlineBot() - acfactory.StopRpc(bot) - dir := filepath.Dir(bot.Account.Manager.Rpc.(*deltachat.RpcIO).AccountsDir) + var dir string + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { + dir = filepath.Dir(bot.Rpc.Transport.(*transport.IOTransport).AccountsDir) + }) args = append([]string{"-f=" + dir}, args...) return runCli(cli, args...) } @@ -31,17 +41,3 @@ func runCli(cli *BotCli, args ...string) (output string, err error) { err = cli.Start() return buf.String(), err } - -func TestMain(m *testing.M) { - cfg := map[string]string{ - "mail_server": "localhost", - "send_server": "localhost", - "mail_port": "3143", - "send_port": "3025", - "mail_security": "3", - "send_security": "3", - } - acfactory.TearUp(cfg) - defer acfactory.TearDown() - m.Run() -} diff --git a/examples/echobot.go b/examples/echobot.go index eb2dc25..cc5f891 100644 --- a/examples/echobot.go +++ b/examples/echobot.go @@ -14,29 +14,33 @@ func main() { // add an "info" CLI subcommand as example infoCmd := &cobra.Command{ Use: "info", - Short: "display information about the Delta Chat core running in this system", + Short: "display information about the Delta Chat core running in this system or about an specific account if one was selected with -a/--account", Args: cobra.ExactArgs(0), } cli.AddCommand(infoCmd, func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - sysinfo, _ := bot.Account.Manager.SystemInfo() - for key, val := range sysinfo { + var info map[string]string + if cli.SelectedAddr == "" { // no account selected with --a/--account, show system info + info, _ = bot.Rpc.GetSystemInfo() + } else { // account selected, show info about that account + accId, _ := cli.GetAccount(bot.Rpc, cli.SelectedAddr) + info, _ = bot.Rpc.GetInfo(accId) + } + for key, val := range info { fmt.Printf("%v=%#v\n", key, val) } }) // incoming message handling cli.OnBotInit(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.OnNewMsg(func(bot *deltachat.Bot, msg *deltachat.Message) { - snapshot, _ := msg.Snapshot() - chat := &deltachat.Chat{msg.Account, snapshot.ChatId} - if snapshot.Text != "" { // ignore messages without text - chat.SendText(snapshot.Text) + bot.OnNewMsg(func(bot *deltachat.Bot, accId deltachat.AccountId, msgId deltachat.MsgId) { + msg, _ := bot.Rpc.GetMessage(accId, msgId) + if msg.FromId > deltachat.ContactLastSpecial && msg.Text != "" { + bot.Rpc.MiscSendTextMessage(accId, msg.ChatId, msg.Text) } }) }) cli.OnBotStart(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - addr, _ := bot.GetConfig("configured_addr") - cli.Logger.Infof("Listening at: %v", addr) + cli.Logger.Info("OnBotStart event triggered: bot started!") }) cli.Start() } diff --git a/examples/infobot.go b/examples/infobot.go index 9a107a3..028e28f 100644 --- a/examples/infobot.go +++ b/examples/infobot.go @@ -13,24 +13,26 @@ import ( var cli *botcli.BotCli = botcli.New("infobot") // Process messages sent to the group of administrators and allow to run privileged commands there. -func onNewMsg(bot *deltachat.Bot, msg *deltachat.Message) { - snapshot, _ := msg.Snapshot() - chat := &deltachat.Chat{snapshot.Account, snapshot.ChatId} - sender := &deltachat.Contact{snapshot.Account, snapshot.FromId} - isAdmin, _ := cli.IsAdmin(bot, sender) - adminChat, _ := cli.AdminChat(bot) - if !isAdmin || chat.Id != adminChat.Id { +func onNewMsg(bot *deltachat.Bot, accId deltachat.AccountId, msgId deltachat.MsgId) { + msg, _ := bot.Rpc.GetMessage(accId, msgId) + if msg.FromId <= deltachat.ContactLastSpecial { // ignore message from self return } - switch snapshot.Text { - case "/info": - info, _ := bot.Account.Info() - var text string - for key, value := range info { - text += key + "=" + value + "\n" + adminChatId, _ := cli.AdminChat(bot, accId) + if msg.ChatId == adminChatId { + isAdmin, _ := cli.IsAdmin(bot, accId, msg.FromId) + if isAdmin { + switch msg.Text { + case "/info": + info, _ := bot.Rpc.GetInfo(accId) + var text string + for key, value := range info { + text += key + "=" + value + "\n" + } + bot.Rpc.MiscSendTextMessage(accId, msg.ChatId, text) + } } - chat.SendText(text) } } diff --git a/go.mod b/go.mod index 11823e3..0fe467f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/deltachat-bot/deltabot-cli-go go 1.19 require ( - github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230417222922-fd102c51053c + github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230803004831-c41345b8ffd8 github.com/mdp/qrterminal/v3 v3.0.0 github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.2 diff --git a/go.sum b/go.sum index eed9d82..91ebd7b 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,14 @@ 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/deltachat/deltachat-rpc-client-go v0.17.1-0.20230417222922-fd102c51053c h1:tZAf1PDTjlbH6FXdt738I8JZwCyWph1A6tni+0TjR3A= github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230417222922-fd102c51053c/go.mod h1:2ISiMA742xpBiTP+KOLiTSNfZdKZChh6cnuvA10sqkA= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801182605-e724f863c117 h1:Mi9v3Mao6D8UEIZMsq/swA59p2+Z87LpEVsrjh4ffkU= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801182605-e724f863c117/go.mod h1:2ISiMA742xpBiTP+KOLiTSNfZdKZChh6cnuvA10sqkA= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801190630-0ba4181fa8c2 h1:KGcH/Je2F08OstMivSDcQigswXmrOSMcTjh+aLIatBQ= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801190630-0ba4181fa8c2/go.mod h1:2ISiMA742xpBiTP+KOLiTSNfZdKZChh6cnuvA10sqkA= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801225118-4e2d5167d2cc h1:fPUVjSDaqiGtCTZS+eFxjXs+gc0GDWwwgaTHn7TggSc= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230801225118-4e2d5167d2cc/go.mod h1:2ISiMA742xpBiTP+KOLiTSNfZdKZChh6cnuvA10sqkA= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230803004831-c41345b8ffd8 h1:Oej8o1RdxEpQg+XpI1qDD+nRd5jMcuEbOk7Wx4y2hDQ= +github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230803004831-c41345b8ffd8/go.mod h1:2ISiMA742xpBiTP+KOLiTSNfZdKZChh6cnuvA10sqkA= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index ad14cfb..e89652f 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -22,7 +22,7 @@ fi if ! command -v deltachat-rpc-server &> /dev/null then echo "deltachat-rpc-server could not be found, installing..." - curl -L "https://github.com/deltachat/deltachat-core-rust/releases/download/v1.112.6/deltachat-rpc-server-x86_64" -o ~/.cargo/bin/deltachat-rpc-server + curl -L "https://github.com/deltachat/deltachat-core-rust/releases/download/v1.118.0/deltachat-rpc-server-x86_64" -o ~/.cargo/bin/deltachat-rpc-server chmod +x ~/.cargo/bin/deltachat-rpc-server fi