diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c7d69..11f4fd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,20 @@ name: CI on: push: branches: [ "master", "main" ] + paths: + - "examples/**" + - "scripts/**" + - "v2/**" pull_request: branches: [ "master", "main" ] + paths: + - "examples/**" + - "scripts/**" + - "v2/**" jobs: - build: + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -23,7 +31,6 @@ jobs: - name: Test run: | export TEST_DEBUG=1 - export TEST_EXTRA_TAGS=" " bash ./scripts/run_tests.sh - name: Coverage Badge - Generate @@ -58,12 +65,3 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.head_ref }} - - services: - mail_server: - image: ghcr.io/deltachat/mail-server-tester:release - ports: - - 3025:25 - - 3143:143 - - 3465:465 - - 3993:993 diff --git a/.gitignore b/.gitignore index 69d3082..7b93c21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *~ -*.out \ No newline at end of file +*.out +accounts/ +examples/*/executable* +examples/*/go.sum diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index ef8019f..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,38 +0,0 @@ -# Changelog - -## v1.2.14 - -## Changed - -- breaking: update `github.com/chatmail/rpc-client-go` to `v1.2.14` -- breaking: `qr` subcommand renamed to `link` and no QR is print, only invite link -- breaking: modified `Callback` type to accept an additional `*BotCli` parameter -- updated to breaking changes in `deltachat-rpc-client-go v0.17.1-0.20230417222922-fd102c51053c` - -## v0.4.0 - -## Added - -- add more tests and code coverage -- add `BotCli.SetConfig()` and `BotCli.GetConfig()` -- add `BotCli.AdminChat()`, `BotCli.ResetAdminChat()` and `BotCli.IsAdmin()` - -## Changed - -- adapted to work with recent API changes in deltachat-rpc-client-go v0.16.1-0.20230413050235-ac4cbf9913e8 -- `BotCli.Start()` now returns an error instead of calling `os.Exit(1)` - -## v0.3.0 - -- add `qr` subcommand -- switch to zap logger -- update configAction() to print a new line in the returned config value -- panic if deltachat-rpc-server can't be started and provide hint to installation instructions - -## v0.2.0 - -- log info/warning/error core events by default - -## v0.1.0 - -- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0138468 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +## Contributing + +After doing your modifications make sure all tests pass, and add tests to ensure +code coverage of your new modifications. + +### Running the test suite + +To run the integration tests run: + +``` +./scripts/run_tests.sh +``` + +The `run_tests.sh` script will install `deltachat-rpc-server` (if needed) +and run all tests. + +### Updating dependencies + +``` +cd v2 +go get -u ./... +``` + +To update the `deltachat-rpc-server` program in CI, update `scripts/run_tests.sh` diff --git a/README.md b/README.md index f10dde6..c00814a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ creating the bot CLI. ## Install ```sh -go get -u github.com/deltachat-bot/deltabot-cli-go +go get -u github.com/deltachat-bot/deltabot-cli-go/v2 ``` ### Installing deltachat-rpc-server @@ -28,8 +28,8 @@ https://github.com/chatmail/core/tree/main/deltachat-rpc-server Example echo-bot written with deltabot-cli: - - + + ```go package main @@ -69,14 +69,7 @@ go run ./echobot.go serve Use `go run ./echobot.go --help` to see all the available options. -Check the [examples folder](https://github.com/deltachat-bot/deltabot-cli-go/tree/master/examples) for -more examples. +Check the [examples folder](./examples) for more examples. This package depends on https://github.com/chatmail/rpc-client-go library, check its documentation to better understand how to use the Delta Chat API. - -## Template project - -To help you quickly creating new bots, we have prepared a project template with all the basic -boilerplate, including unit tests, linter and GitHub CI to test and release your bot. Check it here: -https://github.com/deltachat-bot/echobot-go diff --git a/examples/echobot.go b/examples/echobot/echobot.go similarity index 61% rename from examples/echobot.go rename to examples/echobot/echobot.go index 91d2115..06371e0 100644 --- a/examples/echobot.go +++ b/examples/echobot/echobot.go @@ -1,25 +1,26 @@ package main import ( - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/deltachat-bot/deltabot-cli-go/botcli" + "github.com/chatmail/rpc-client-go/v2/deltachat" + "github.com/deltachat-bot/deltabot-cli-go/v2/botcli" "github.com/spf13/cobra" ) func main() { cli := botcli.New("echobot") - // incoming message handling cli.OnBotInit(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.OnNewMsg(func(bot *deltachat.Bot, accId deltachat.AccountId, msgId deltachat.MsgId) { + // incoming message handling + bot.OnNewMsg(func(bot *deltachat.Bot, accId uint32, msgId uint32) { msg, _ := bot.Rpc.GetMessage(accId, msgId) if msg.FromId > deltachat.ContactLastSpecial && msg.Text != "" { - bot.Rpc.MiscSendTextMessage(accId, msg.ChatId, msg.Text) + _, _ = bot.Rpc.SendMsg(accId, msg.ChatId, deltachat.MessageData{Text: &msg.Text}) } }) }) cli.OnBotStart(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { cli.Logger.Info("OnBotStart event triggered: bot is about to start!") }) - cli.Start() + + _ = cli.Start() } diff --git a/examples/echobot/go.mod b/examples/echobot/go.mod new file mode 100644 index 0000000..aec36dd --- /dev/null +++ b/examples/echobot/go.mod @@ -0,0 +1,19 @@ +module executable + +go 1.25.1 + +require ( + github.com/chatmail/rpc-client-go/v2 v2.0.1 + github.com/deltachat-bot/deltabot-cli-go/v2 v2.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/creachadair/jrpc2 v1.1.2 // indirect + github.com/creachadair/mds v0.8.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/sync v0.6.0 // indirect +) diff --git a/examples/go.mod b/examples/go.mod deleted file mode 100644 index e69de29..0000000 diff --git a/examples/infobot/go.mod b/examples/infobot/go.mod new file mode 100644 index 0000000..aec36dd --- /dev/null +++ b/examples/infobot/go.mod @@ -0,0 +1,19 @@ +module executable + +go 1.25.1 + +require ( + github.com/chatmail/rpc-client-go/v2 v2.0.1 + github.com/deltachat-bot/deltabot-cli-go/v2 v2.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/creachadair/jrpc2 v1.1.2 // indirect + github.com/creachadair/mds v0.8.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/sync v0.6.0 // indirect +) diff --git a/examples/infobot.go b/examples/infobot/infobot.go similarity index 56% rename from examples/infobot.go rename to examples/infobot/infobot.go index 697c2f1..22ce7c0 100644 --- a/examples/infobot.go +++ b/examples/infobot/infobot.go @@ -1,39 +1,36 @@ // This example demonstrates how to create bots that have administrators. // -// The bot has the /info command that can only be executed by bot administrators in the admins chat. -// To become admin you must use the `admin` subcommand in the cli, and scan the QR that will be shown. +// The bot has the /info command that can only be executed by bot administrators (members of the admins chat). +// To become admin you must use the `admin` subcommand in the cli, and open the invite link that will be shown. package main import ( "fmt" - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/deltachat-bot/deltabot-cli-go/botcli" + "github.com/chatmail/rpc-client-go/v2/deltachat" + "github.com/deltachat-bot/deltabot-cli-go/v2/botcli" "github.com/spf13/cobra" ) 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, accId deltachat.AccountId, msgId deltachat.MsgId) { +// Process messages sent by administrators. +func onNewMsg(bot *deltachat.Bot, accId uint32, msgId uint32) { msg, _ := bot.Rpc.GetMessage(accId, msgId) if msg.FromId <= deltachat.ContactLastSpecial { // ignore message from self return } - 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) + 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.SendMsg(accId, msg.ChatId, deltachat.MessageData{Text: &text}) } } } @@ -47,11 +44,10 @@ func main() { } cli.AddCommand(infoCmd, func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { var info map[string]string - if cli.SelectedAddr == "" { // no account selected with --a/--account, show system info + if cli.SelectedAccount == 0 { // 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) + info, _ = bot.Rpc.GetInfo(cli.SelectedAccount) } for key, val := range info { fmt.Printf("%v=%#v\n", key, val) @@ -61,5 +57,5 @@ func main() { cli.OnBotInit(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { bot.OnNewMsg(onNewMsg) }) - cli.Start() + _ = cli.Start() } diff --git a/examples/webxdcbot/api.go b/examples/webxdcbot/api.go new file mode 100644 index 0000000..0732164 --- /dev/null +++ b/examples/webxdcbot/api.go @@ -0,0 +1,36 @@ +/* +RPC API definitions. + +Here you can define functions that can be called on the mini-app side. + +``` +webxdc.sendUpdate({payload: {id: "1", method: "Noop", params: []}}, ""); +``` +*/ +package main + +import ( + "github.com/deltachat-bot/deltabot-cli-go/v2/xdcrpc" +) + +// You must put your available RPC API in a custom type +type API struct{} + +// Function without arguments or return value +func (api *API) Noop() { + // do nothing +} + +// Function with return value but no *xdcrpc.Error +func (api *API) Echo(text string) string { + return text +} + +// Function that might return an xdcrpc.Error. +// Functions must return `*xdcrpc.Error` instead of `error` +func (api *API) Divide(a int, b int) (int, *xdcrpc.Error) { + if b == 0 { + return 0, &xdcrpc.Error{Code: 1, Message: "Division by zero"} + } + return a / b, nil +} diff --git a/examples/webxdcbot/go.mod b/examples/webxdcbot/go.mod new file mode 100644 index 0000000..aec36dd --- /dev/null +++ b/examples/webxdcbot/go.mod @@ -0,0 +1,19 @@ +module executable + +go 1.25.1 + +require ( + github.com/chatmail/rpc-client-go/v2 v2.0.1 + github.com/deltachat-bot/deltabot-cli-go/v2 v2.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/creachadair/jrpc2 v1.1.2 // indirect + github.com/creachadair/mds v0.8.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/sync v0.6.0 // indirect +) diff --git a/examples/webxdcbot/webxdc.go b/examples/webxdcbot/webxdc.go new file mode 100644 index 0000000..eb5f080 --- /dev/null +++ b/examples/webxdcbot/webxdc.go @@ -0,0 +1,53 @@ +/* +# Webxdc RPC Example + +This is an example bot project using the `xdcrpc` package for +communication between the backend bot and a frontend webxdc app. + +To run the bot: + +```sh +go run . dcaccount:nine.testrun.org +``` + +NOTE: For this example to work, a app.xdc file must be provided +in thecurrent working dir. +*/ +package main + +import ( + "github.com/chatmail/rpc-client-go/v2/deltachat" + "github.com/deltachat-bot/deltabot-cli-go/v2/botcli" + "github.com/deltachat-bot/deltabot-cli-go/v2/xdcrpc" + "github.com/spf13/cobra" +) + +var cli = botcli.New("webxdcbot") + +func main() { + cli.OnBotInit(func(cli *botcli.BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + bot.OnUnhandledEvent(onEvent) + bot.OnNewMsg(onNewMsg) + }) + _ = cli.Start() +} + +func onEvent(bot *deltachat.Bot, accId uint32, event deltachat.EventType) { + switch ev := event.(type) { + case *deltachat.EventTypeWebxdcStatusUpdate: + _ = xdcrpc.HandleMessage(bot.Rpc, accId, ev.MsgId, ev.StatusUpdateSerial, &API{}) + } +} + +func onNewMsg(bot *deltachat.Bot, accId uint32, msgId uint32) { + msg, _ := bot.Rpc.GetMessage(accId, msgId) + logger := cli.GetLogger(accId).With("chat", msg.ChatId) + if msg.FromId > deltachat.ContactLastSpecial { + logger.Info("message received, sending the mini-app") + file := "app.xdc" + _, err := bot.Rpc.SendMsg(accId, msg.ChatId, deltachat.MessageData{File: &file}) + if err != nil { + logger.Error(err) + } + } +} diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index c0ab642..ae48d56 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -1,4 +1,7 @@ #!/bin/env bash +set -euo pipefail + +PKG='github.com/deltachat-bot/deltabot-cli-go' echo "Checking code with gofmt..." OUTPUT=`gofmt -d .` @@ -16,34 +19,32 @@ then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0 fi -if ! golangci-lint run -then - exit 1 -fi +cd v2 && golangci-lint run && cd .. if ! command -v deltachat-rpc-server &> /dev/null then echo "deltachat-rpc-server not found, installing..." - curl -L https://github.com/chatmail/core/releases/download/v2.14.0/deltachat-rpc-server-x86_64-linux --output deltachat-rpc-server + curl -L https://github.com/chatmail/core/releases/download/v2.44.0/deltachat-rpc-server-x86_64-linux --output deltachat-rpc-server chmod +x deltachat-rpc-server export PATH=`pwd`:"$PATH" fi -if ! command -v courtney &> /dev/null -then - echo "courtney not found, installing..." - go install github.com/dave/courtney@master -fi - # test examples -for i in ./examples/*.go +for i in examples/* do - echo "Testing examples: $i" - if ! go build -v "$i" - then - exit 1 - fi + echo "Testing: $i" + cd "$i" + go mod edit -replace=$PKG/v2=../../v2 + go mod tidy + golangci-lint run + go build -v + go test -v + go mod edit -dropreplace $PKG/v2 + cd ../.. done +echo "Done testing examples" -courtney -v -t="./..." ${TEST_EXTRA_TAGS:--t="-parallel=1"} -go tool cover -func=coverage.out -o=coverage-percent.out +cd v2 +# add -parallel=1 to avoid running tests in parallel +go test -v ./... -coverprofile coverage.out +go tool cover -func=coverage.out -o=../coverage-percent.out diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..21b3708 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,4 @@ +# deltabot-cli for Go + +This is the reference documentation, a quick introduction can be found at: +https://github.com/deltachat-bot/deltabot-cli-go/ diff --git a/botcli/botcli.go b/v2/botcli/botcli.go similarity index 53% rename from botcli/botcli.go rename to v2/botcli/botcli.go index 38293eb..0f6b506 100644 --- a/botcli/botcli.go +++ b/v2/botcli/botcli.go @@ -5,9 +5,7 @@ import ( "os" "strconv" - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/chatmail/rpc-client-go/deltachat/option" - "github.com/chatmail/rpc-client-go/deltachat/transport" + "github.com/chatmail/rpc-client-go/v2/deltachat" "github.com/spf13/cobra" "go.uber.org/zap" ) @@ -25,14 +23,14 @@ type BotCli struct { 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 + // SelectedAccount can be set by the --account flag in command line, if empty it means "all accounts" + SelectedAccount uint32 + RootCmd *cobra.Command + Logger *zap.SugaredLogger + cmdsMap map[string]Callback + parsedCmd *_ParsedCmd + onInit Callback + onStart Callback } // Create a new BotCli instance. @@ -71,7 +69,7 @@ func (botcli *BotCli) Start() error { return err } - trans := transport.NewIOTransport() + trans := deltachat.NewIOTransport() trans.AccountsDir = getAccountsDir(botcli.AppDir) rpc := &deltachat.Rpc{Context: context.Background(), Transport: trans} defer trans.Close() @@ -86,14 +84,14 @@ func (botcli *BotCli) Start() error { botcli.Logger.Infof("Running deltachat core %v", info["deltachat_core_version"]) bot := deltachat.NewBot(rpc) - bot.On(deltachat.EventInfo{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { - botcli.GetLogger(accId).Info(event.(deltachat.EventInfo).Msg) + bot.On(&deltachat.EventTypeInfo{}, func(bot *deltachat.Bot, accId uint32, event deltachat.EventType) { + botcli.GetLogger(accId).Info(event.(*deltachat.EventTypeInfo).Msg) }) - bot.On(deltachat.EventWarning{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { - botcli.GetLogger(accId).Warn(event.(deltachat.EventWarning).Msg) + bot.On(&deltachat.EventTypeWarning{}, func(bot *deltachat.Bot, accId uint32, event deltachat.EventType) { + botcli.GetLogger(accId).Warn(event.(*deltachat.EventTypeWarning).Msg) }) - bot.On(deltachat.EventError{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { - botcli.GetLogger(accId).Error(event.(deltachat.EventError).Msg) + bot.On(&deltachat.EventTypeError{}, func(bot *deltachat.Bot, accId uint32, event deltachat.EventType) { + botcli.GetLogger(accId).Error(event.(*deltachat.EventTypeError).Msg) }) if botcli.onInit != nil { botcli.onInit(botcli, bot, botcli.parsedCmd.cmd, botcli.parsedCmd.args) @@ -106,7 +104,7 @@ func (botcli *BotCli) Start() error { } // Get a logger for the given account. -func (botcli *BotCli) GetLogger(accId deltachat.AccountId) *zap.SugaredLogger { +func (botcli *BotCli) GetLogger(accId uint32) *zap.SugaredLogger { return botcli.Logger.With("acc", accId) } @@ -123,21 +121,17 @@ func (botcli *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 (botcli *BotCli) SetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string, value option.Option[string]) error { - return bot.SetUiConfig(accId, botcli.AppName+"."+key, value) +func (botcli *BotCli) SetConfig(bot *deltachat.Bot, accId uint32, key string, value *string) error { + return bot.Rpc.SetConfig(accId, "ui."+botcli.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 (botcli *BotCli) GetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string) (option.Option[string], error) { - return bot.GetUiConfig(accId, botcli.AppName+"."+key) +func (botcli *BotCli) GetConfig(bot *deltachat.Bot, accId uint32, key string) (*string, error) { + return bot.Rpc.GetConfig(accId, "ui."+botcli.AppName+"."+key) } // Get the group of bot administrators. -func (botcli *BotCli) AdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { +func (botcli *BotCli) AdminChat(bot *deltachat.Bot, accId uint32) (uint32, error) { if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { return 0, &BotNotConfiguredErr{} } @@ -147,24 +141,24 @@ func (botcli *BotCli) AdminChat(bot *deltachat.Bot, accId deltachat.AccountId) ( return 0, err } - var chatId deltachat.ChatId + var chatId uint32 - if value.IsSome() { - chatIdInt, err := strconv.ParseUint(value.Unwrap(), 10, 0) + if value != nil { + chatIdInt, err := strconv.ParseUint(*value, 10, 0) if err != nil { return 0, err } - chatId = deltachat.ChatId(chatIdInt) + chatId = uint32(chatIdInt) selfInGroup, err := bot.Rpc.CanSend(accId, chatId) if err != nil { return 0, err } if !selfInGroup { - value = option.None[string]() + value = nil } } - if value.IsNone() { + if value == nil { chatId, err = botcli.ResetAdminChat(bot, accId) if err != nil { return 0, err @@ -175,17 +169,17 @@ func (botcli *BotCli) AdminChat(bot *deltachat.Bot, accId deltachat.AccountId) ( } // Reset the group of bot administrators, all the members of the old group are no longer admins. -func (botcli *BotCli) ResetAdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { +func (botcli *BotCli) ResetAdminChat(bot *deltachat.Bot, accId uint32) (uint32, error) { if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { return 0, &BotNotConfiguredErr{} } - chatId, err := bot.Rpc.CreateGroupChat(accId, "Bot Administrators", true) + chatId, err := bot.Rpc.CreateGroupChat(accId, "Bot Administrators", false) if err != nil { return 0, err } value := strconv.FormatUint(uint64(chatId), 10) - err = botcli.SetConfig(bot, accId, "admin-chat", option.Some(value)) + err = botcli.SetConfig(bot, accId, "admin-chat", &value) if err != nil { return 0, err } @@ -194,7 +188,7 @@ func (botcli *BotCli) ResetAdminChat(bot *deltachat.Bot, accId deltachat.Account } // Returns true if contact is in the bot administrators group, false otherwise. -func (botcli *BotCli) IsAdmin(bot *deltachat.Bot, accId deltachat.AccountId, contactId deltachat.ContactId) (bool, error) { +func (botcli *BotCli) IsAdmin(bot *deltachat.Bot, accId uint32, contactId uint32) (bool, error) { chatId, err := botcli.AdminChat(bot, accId) if err != nil { return false, err @@ -211,49 +205,3 @@ func (botcli *BotCli) IsAdmin(bot *deltachat.Bot, accId deltachat.AccountId, con return false, nil } - -// Get account for address, if no account exists create a new one -func (botcli *BotCli) GetOrCreateAccount(rpc *deltachat.Rpc, addr string) (deltachat.AccountId, error) { - accId, err := botcli.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 (botcli *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, _ := botcli.GetAddress(rpc, accId) - if addr == addr2 { - return accId, nil - } - } - return 0, &AccountNotFoundErr{Addr: addr} -} - -// Get the address of the given account -func (botcli *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/v2/botcli/botcli_test.go similarity index 64% rename from botcli/botcli_test.go rename to v2/botcli/botcli_test.go index 998e06d..83fecfa 100644 --- a/botcli/botcli_test.go +++ b/v2/botcli/botcli_test.go @@ -1,38 +1,40 @@ package botcli import ( + "fmt" "testing" - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/chatmail/rpc-client-go/deltachat/option" + "github.com/chatmail/rpc-client-go/v2/deltachat" "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBotCli_SetConfig(t *testing.T) { t.Parallel() - acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId uint32) { cli := New("testbot") - assert.Nil(t, cli.SetConfig(bot, accId, "testkey", option.Some("testing"))) + testVal := "testing" + require.Nil(t, cli.SetConfig(bot, accId, "testkey", &testVal)) value, err := cli.GetConfig(bot, accId, "testkey") - assert.Nil(t, err) - assert.Equal(t, "testing", value.UnwrapOr("")) + require.Nil(t, err) + require.NotNil(t, value) + require.Equal(t, testVal, *value) }) } func TestBotCli_AdminChat(t *testing.T) { t.Parallel() - acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId uint32) { cli := New("testbot") chatId1, err := cli.AdminChat(bot, accId) - assert.Nil(t, err) + require.Nil(t, err) chatId2, err := cli.ResetAdminChat(bot, accId) - assert.Nil(t, err) - assert.NotEqual(t, chatId2, chatId1) + require.Nil(t, err) + require.NotEqual(t, chatId2, chatId1) isAdmin, err := cli.IsAdmin(bot, accId, deltachat.ContactSelf) - assert.Nil(t, err) - assert.True(t, isAdmin) + require.Nil(t, err) + require.True(t, isAdmin) }) } @@ -49,8 +51,8 @@ func TestBotCli_AddCommand(t *testing.T) { called = true }) _, err := RunCli(cli, "test") - assert.Nil(t, err) - assert.True(t, called) + require.Nil(t, err) + require.True(t, called) } func TestBotCli_OnBotStart(t *testing.T) { @@ -69,14 +71,14 @@ func TestBotCli_OnBotStart(t *testing.T) { func TestBotCli_serve(t *testing.T) { t.Parallel() cli := New("testbot") - onNewMsgCalled := make(chan *deltachat.MsgSnapshot, 1) + onNewMsgCalled := make(chan *deltachat.Message, 1) var cliBot *deltachat.Bot cli.OnBotInit(func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { cliBot = bot - bot.OnNewMsg(func(bot *deltachat.Bot, accId deltachat.AccountId, msgId deltachat.MsgId) { + bot.OnNewMsg(func(bot *deltachat.Bot, accId uint32, msgId uint32) { snapshot, _ := bot.Rpc.GetMessage(accId, msgId) select { - case onNewMsgCalled <- snapshot: + case onNewMsgCalled <- &snapshot: default: } }) @@ -86,37 +88,31 @@ func TestBotCli_serve(t *testing.T) { } defer cliBot.Stop() - acfactory.WithOnlineAccount(func(rpc *deltachat.Rpc, accId deltachat.AccountId) { + acfactory.WithOnlineAccount(func(rpc *deltachat.Rpc, accId uint32) { chatWithBot := acfactory.CreateChat(rpc, accId, cliBot.Rpc, 1) _, err := rpc.MiscSendTextMessage(accId, chatWithBot, "hi") - assert.Nil(t, err) + require.Nil(t, err) msg := <-onNewMsgCalled - assert.Equal(t, "hi", msg.Text) + require.Equal(t, "hi", msg.Text) }) } func TestInitCallback(t *testing.T) { t.Parallel() - 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) + acfactory.WithUnconfiguredAccount(func(rpc *deltachat.Rpc, accId uint32) { configured, _ := rpc.IsConfigured(accId) - assert.False(t, configured) + require.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) + _, err := RunCli(cli, "init", fmt.Sprintf("-a=%v", accId), acfactory.ConfigQr) + require.Nil(t, err) configured, _ = rpc.IsConfigured(accId) - assert.True(t, configured) + require.True(t, configured) }) } @@ -126,10 +122,10 @@ func TestConfigCallback(t *testing.T) { cli := New("testbot") _, err = RunCli(cli, "config", "addr") - assert.Nil(t, err) + require.Nil(t, err) _, err = RunCli(cli, "config", "addr", "test@example.com") - assert.Nil(t, err) + require.Nil(t, err) } func TestQrCallback(t *testing.T) { @@ -137,10 +133,10 @@ func TestQrCallback(t *testing.T) { var err error cli := New("testbot") _, err = RunCli(cli, "link") - assert.Nil(t, err) + require.Nil(t, err) _, err = RunConfiguredCli(cli, "link") - assert.Nil(t, err) + require.Nil(t, err) } func TestAdminCallback(t *testing.T) { @@ -148,11 +144,11 @@ func TestAdminCallback(t *testing.T) { var err error cli := New("testbot") _, err = RunCli(cli, "admin") - assert.Nil(t, err) + require.Nil(t, err) _, err = RunConfiguredCli(cli, "admin") - assert.Nil(t, err) + require.Nil(t, err) _, err = RunConfiguredCli(cli, "admin", "-r") - assert.Nil(t, err) + require.Nil(t, err) } diff --git a/botcli/cmd.go b/v2/botcli/cmd.go similarity index 61% rename from botcli/cmd.go rename to v2/botcli/cmd.go index fc85ab7..f592c2c 100644 --- a/botcli/cmd.go +++ b/v2/botcli/cmd.go @@ -4,20 +4,19 @@ import ( "fmt" "strings" - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/chatmail/rpc-client-go/deltachat/option" + "github.com/chatmail/rpc-client-go/v2/deltachat" "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") + cli.RootCmd.PersistentFlags().Uint32VarP(&cli.SelectedAccount, "account", "a", 0, "operate over this account ID only when running any subcommand") initCmd := &cobra.Command{ Use: "init", - Short: "do initial login configuration of a new Delta Chat account, if the account already exist, the credentials are updated", - Args: cobra.ExactArgs(2), + Short: "do initial login configuration of a new Delta Chat account. If only one argument is given it must be a configuration URI (ex. dcaccount:)", + Args: cobra.RangeArgs(1, 2), } cli.AddCommand(initCmd, initCallback) @@ -66,39 +65,40 @@ func initializeRootCmd(cli *BotCli) { } func initCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.On(deltachat.EventConfigureProgress{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { - ev := event.(deltachat.EventConfigureProgress) - addr, _ := cli.GetAddress(bot.Rpc, accId) - if addr == "" { - addr = fmt.Sprintf("account #%v", accId) - } - cli.Logger.Infof("[%v] Configuration progress: %v", addr, ev.Progress) + bot.On(&deltachat.EventTypeConfigureProgress{}, func(bot *deltachat.Bot, accId uint32, event deltachat.EventType) { + ev := event.(*deltachat.EventTypeConfigureProgress) + cli.Logger.Infof("[account #%v] Configuration progress: %v", accId, ev.Progress) }) - var accId deltachat.AccountId + var accId uint32 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 cli.SelectedAccount == 0 { // create a new account + accId, err = bot.Rpc.AddAccount() + } else { // add relay to the selected account + accId = cli.SelectedAccount + } + + if err == nil { + botFlag := "1" + err = bot.Rpc.SetConfig(accId, "bot", &botFlag) } + if err != nil { cli.Logger.Errorf("Configuration failed: %v", err) return } go func() { - if err := bot.Configure(accId, args[0], args[1]); err != nil { + if len(args) == 2 { + params := deltachat.EnteredLoginParam{Addr: args[0], Password: args[1]} + err = bot.Rpc.AddOrUpdateTransport(accId, params) + } else { + err = bot.Rpc.AddTransportFromQr(accId, args[0]) + } + if err != nil { cli.Logger.Errorf("Configuration failed: %v", err) } else { - cli.Logger.Infof("Account %q configured successfully.", args[0]) + cli.Logger.Infof("Account configured successfully.") } bot.Stop() }() @@ -107,13 +107,11 @@ 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 err error - var accounts []deltachat.AccountId - if cli.SelectedAddr == "" { // set config for all accounts + var accounts []uint32 + if cli.SelectedAccount == 0 { // 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} + accounts = []uint32{cli.SelectedAccount} } if err != nil { cli.Logger.Error(err) @@ -121,12 +119,7 @@ func configCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args [] } 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) + fmt.Printf("Account #%v:\n", accId) configForAcc(cli, bot, cmd, args, accId) fmt.Println("") } @@ -136,33 +129,41 @@ func configCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args [] } } -func configForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { +func configForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId uint32) { if len(args) == 0 { keys, _ := bot.Rpc.GetConfig(accId, "sys.config_keys") - for _, key := range strings.Fields(keys.Unwrap()) { + for _, key := range strings.Fields(*keys) { val, _ := bot.Rpc.GetConfig(accId, key) - fmt.Printf("%v=%q\n", key, val.UnwrapOr("")) + var strval string + if val != nil { + strval = *val + } + fmt.Printf("%v=%q\n", key, strval) } return } - var val option.Option[string] + var val *string var err error if len(args) == 2 { - err = bot.Rpc.SetConfig(accId, args[0], option.Some(args[1])) + err = bot.Rpc.SetConfig(accId, args[0], &args[1]) } if err == nil { val, err = bot.Rpc.GetConfig(accId, args[0]) } if err == nil { - fmt.Printf("%v=%v\n", args[0], val.UnwrapOr("")) + var strval string + if val != nil { + strval = *val + } + fmt.Printf("%v=%v\n", args[0], strval) } else { cli.Logger.Error(err) } } func serveCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if cli.SelectedAddr != "" { + if cli.SelectedAccount != 0 { cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") return } @@ -172,19 +173,17 @@ func serveCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []s cli.Logger.Error(err) return } - var addrs []string + var inviteLinks []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()) - } + inviteLink, _ := bot.Rpc.GetChatSecurejoinQrCode(accId, nil) + inviteLinks = append(inviteLinks, inviteLink) } } - if len(addrs) != 0 { - cli.Logger.Infof("Listening at: %v", strings.Join(addrs, ", ")) + if len(inviteLinks) != 0 { + cli.Logger.Infof("Listening at: %v", strings.Join(inviteLinks, "\n")) if cli.onStart != nil { cli.onStart(cli, bot, cmd, args) } @@ -196,13 +195,11 @@ func serveCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []s func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { var err error - var accounts []deltachat.AccountId - if cli.SelectedAddr == "" { // for all accounts + var accounts []uint32 + if cli.SelectedAccount == 0 { // 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} + accounts = []uint32{cli.SelectedAccount} } if err != nil { cli.Logger.Error(err) @@ -210,13 +207,8 @@ func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []stri } 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.Printf("Account #%v:\n", accId) + qrForAcc(cli, bot, cmd, args, accId) fmt.Println("") } @@ -225,9 +217,9 @@ func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []stri } } -func qrForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId, addr string) { +func qrForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId uint32) { if isConf, _ := bot.Rpc.IsConfigured(accId); isConf { - qrdata, err := bot.Rpc.GetChatSecurejoinQrCode(accId, option.None[deltachat.ChatId]()) + qrdata, err := bot.Rpc.GetChatSecurejoinQrCode(accId, nil) if err != nil { cli.Logger.Errorf("Failed to generate invite link: %v", err) return @@ -240,16 +232,14 @@ func qrForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string func adminCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { var err error - var accounts []deltachat.AccountId - if cli.SelectedAddr == "" { // for all accounts + var accounts []uint32 + if cli.SelectedAccount == 0 { // for all accounts accounts, err = bot.Rpc.GetAllAccountIds() if err == nil && len(accounts) == 0 { cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") } } else { - var accId deltachat.AccountId - accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) - accounts = []deltachat.AccountId{accId} + accounts = []uint32{cli.SelectedAccount} } if err != nil { cli.Logger.Error(err) @@ -257,18 +247,13 @@ func adminCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []s } 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) + fmt.Printf("Account #%v:\n", accId) adminForAcc(cli, bot, cmd, args, accId) fmt.Println("") } } -func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { +func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId uint32) { if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { cli.Logger.Error("account not configured") return @@ -281,7 +266,7 @@ func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []str cli.Logger.Errorf(errMsg, err) return } - var chatId deltachat.ChatId + var chatId uint32 if reset { chatId, err = cli.ResetAdminChat(bot, accId) } else { @@ -292,7 +277,7 @@ func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []str return } - qrdata, err := bot.Rpc.GetChatSecurejoinQrCode(accId, option.Some(chatId)) + qrdata, err := bot.Rpc.GetChatSecurejoinQrCode(accId, &chatId) if err != nil { cli.Logger.Errorf(errMsg, err) return @@ -303,7 +288,7 @@ func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []str } func listCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if cli.SelectedAddr != "" { + if cli.SelectedAccount != 0 { cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") return } @@ -314,31 +299,37 @@ func listCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []st return } for _, accId := range accounts { - addr, err := cli.GetAddress(bot.Rpc, accId) + relays, err := bot.Rpc.ListTransports(accId) if err != nil { cli.Logger.Error(err) continue } - if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { - addr = addr + " (not configured)" + var addrs string + for index, relay := range relays { + if index == 0 { + addrs = relay.Addr + } else { + addrs += ", " + relay.Addr + } + } + if addrs == "" { + addrs = "(not configured)" } - fmt.Printf("#%v - %v\n", accId, addr) + fmt.Printf("#%v - %v\n", accId, addrs) } } 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 + var accounts []uint32 + if cli.SelectedAccount == 0 { // for all accounts accounts, err = bot.Rpc.GetAllAccountIds() if err == nil && len(accounts) == 0 { cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") } } else { - var accId deltachat.AccountId - accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) - accounts = []deltachat.AccountId{accId} + accounts = []uint32{cli.SelectedAccount} } if err != nil { cli.Logger.Error(err) @@ -351,15 +342,11 @@ func removeCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args [] } 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) + cli.Logger.Infof("Account #%v removed successfully.", accId) } } } diff --git a/botcli/errors.go b/v2/botcli/errors.go similarity index 100% rename from botcli/errors.go rename to v2/botcli/errors.go diff --git a/botcli/logger.go b/v2/botcli/logger.go similarity index 100% rename from botcli/logger.go rename to v2/botcli/logger.go diff --git a/botcli/main_test.go b/v2/botcli/main_test.go similarity index 76% rename from botcli/main_test.go rename to v2/botcli/main_test.go index 01b94ed..98c2048 100644 --- a/botcli/main_test.go +++ b/v2/botcli/main_test.go @@ -5,8 +5,7 @@ import ( "path/filepath" "testing" - "github.com/chatmail/rpc-client-go/deltachat" - "github.com/chatmail/rpc-client-go/deltachat/transport" + "github.com/chatmail/rpc-client-go/v2/deltachat" ) var acfactory *deltachat.AcFactory @@ -20,8 +19,8 @@ func TestMain(m *testing.M) { func RunConfiguredCli(cli *BotCli, args ...string) (output string, err error) { var dir string - acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId deltachat.AccountId) { - dir = filepath.Dir(bot.Rpc.Transport.(*transport.IOTransport).AccountsDir) + acfactory.WithOnlineBot(func(bot *deltachat.Bot, accId uint32) { + dir = filepath.Dir(bot.Rpc.Transport.(*deltachat.IOTransport).AccountsDir) }) args = append([]string{"-f=" + dir}, args...) return runCli(cli, args...) diff --git a/botcli/util.go b/v2/botcli/util.go similarity index 100% rename from botcli/util.go rename to v2/botcli/util.go diff --git a/go.mod b/v2/go.mod similarity index 84% rename from go.mod rename to v2/go.mod index bc2c4fe..96282e7 100644 --- a/go.mod +++ b/v2/go.mod @@ -1,9 +1,9 @@ -module github.com/deltachat-bot/deltabot-cli-go +module github.com/deltachat-bot/deltabot-cli-go/v2 go 1.25 require ( - github.com/chatmail/rpc-client-go v1.2.42 + github.com/chatmail/rpc-client-go/v2 v2.0.1 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.2 go.uber.org/zap v1.26.0 diff --git a/go.sum b/v2/go.sum similarity index 94% rename from go.sum rename to v2/go.sum index 900758a..29bed86 100644 --- a/go.sum +++ b/v2/go.sum @@ -1,5 +1,5 @@ -github.com/chatmail/rpc-client-go v1.2.42 h1:R/W93QU1vbkHwhDheUHw8BJFbqt8KOC14O5mIRYtvvM= -github.com/chatmail/rpc-client-go v1.2.42/go.mod h1:bBX+syYUeyuaGd5VQCjTtbj9q4V29xnHPjkBr6rzG2A= +github.com/chatmail/rpc-client-go/v2 v2.0.1 h1:IVTE3kkDypwAl5xP/vi+6qH+u9kZZDuN/fY9oNrfAbU= +github.com/chatmail/rpc-client-go/v2 v2.0.1/go.mod h1:kvYyHTHjjtWHRIp8H9JnbPFhmBHr1YUqB3IrOZhoG3s= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/jrpc2 v1.1.2 h1:UOYMipEFYlwd5qmcvs9GZBurn3oXt1UDIX5JLjWWFzo= github.com/creachadair/jrpc2 v1.1.2/go.mod h1:JcCe2Eny3lIvVwZLm92WXyU+tNUgTBWFCLMsfNkjEGk= diff --git a/v2/xdcrpc/xdcrpc.go b/v2/xdcrpc/xdcrpc.go new file mode 100644 index 0000000..2c58dbb --- /dev/null +++ b/v2/xdcrpc/xdcrpc.go @@ -0,0 +1,200 @@ +// xdcrpc package helps with the communication between bots and WebXDC apps via JSON-RPC 1.0 +package xdcrpc + +import ( + "encoding/json" + "errors" + "reflect" + + "github.com/chatmail/rpc-client-go/v2/deltachat" +) + +type ErrorCode int + +const ( + // The method does not exist / is not available + MethodNotFoud ErrorCode = -32601 + // Invalid JSON was received by the server + ParseError ErrorCode = -32700 + // The JSON sent is not a valid Request object + InvalidRequest ErrorCode = -32600 + // Invalid method parameter(s) + InvalidParams ErrorCode = -32602 +) + +// Request sent by the frontend app +type Request struct { + Id string `json:"id,omitempty"` + Method string `json:"method"` + Params []any `json:"params"` +} + +type _Request struct { + Id string `json:"id,omitempty"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` +} + +// Response sent by the bot +type Response struct { + Id string `json:"id,omitempty"` + Result any `json:"result"` + Error *Error `json:"error"` +} + +// Error data sent by the bot in ErrorResponse +type Error struct { + Code ErrorCode `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Data any `json:"data,omitempty"` +} + +type StatusUpdate[T any] struct { + Info string `json:"info,omitempty"` + Summary string `json:"summary,omitempty"` + Document string `json:"document,omitempty"` + Payload T `json:"payload,omitempty"` + Serial uint32 `json:"serial,omitempty"` + MaxSerial uint32 `json:"max_serial,omitempty"` +} + +type SelfMessageErr struct { +} + +func (error *SelfMessageErr) Error() string { + return "RPC message seems to be from self" +} + +func HandleMessage(rpc *deltachat.Rpc, accId uint32, msgId uint32, serial uint32, api any) error { + rawUpdate, err := GetUpdate(rpc, accId, msgId, serial) + if err != nil { + return err + } + if IsFromSelf(rawUpdate) { + return &SelfMessageErr{} + } + if response := GetResponse(api, rawUpdate); response != nil { + return SendPayload(rpc, accId, msgId, response) + } + return nil +} + +func GetResponse(api any, rawUpdate []byte) *Response { + response := &Response{} + var update StatusUpdate[_Request] + err := json.Unmarshal(rawUpdate, &update) + if err != nil { + response.Error = &Error{Code: ParseError, Message: "Parse error"} + return response + } + request := update.Payload + response.Id = request.Id + + valOf := reflect.ValueOf(api) + method := valOf.MethodByName(request.Method) + if !method.IsValid() || method.IsNil() { + if request.Id != "" { + response.Error = &Error{Code: MethodNotFoud, Message: "Method not found"} + return response + } + return nil + } + + invalidParamsErr := &Error{Code: InvalidParams, Message: "Invalid params"} + + argsCount := method.Type().NumIn() + if len(request.Params) != argsCount { + response.Error = invalidParamsErr + return response + } + + callArgs := make([]reflect.Value, argsCount) + if argsCount > 0 { + for i := 0; i < argsCount; i++ { + argType := method.Type().In(i) + val := reflect.New(argType).Interface() + err = json.Unmarshal(request.Params[i], val) + if err != nil { + response.Error = invalidParamsErr + return response + } + callArgs[i] = reflect.ValueOf(val).Elem() + } + } + + result := method.Call(callArgs) + if request.Id == "" { + return nil + } + + var returnValues []any + for _, valOf := range result { + valAny := valOf.Interface() + switch val := valAny.(type) { + case *Error: + response.Error = val + default: + returnValues = append(returnValues, val) + } + } + count := len(returnValues) + switch { + case count == 1: + response.Result = returnValues[0] + case count > 1: + response.Result = returnValues + } + return response +} + +// Return true if the raw status update is from self, false otherwise +func IsFromSelf(rawUpdate []byte) bool { + var update StatusUpdate[map[string]json.RawMessage] + if err := json.Unmarshal(rawUpdate, &update); err != nil { + return false + } + if _, ok := update.Payload["result"]; ok { + return true + } + if _, ok := update.Payload["error"]; ok { + return true + } + return false +} + +// Get all setatus updates with serial greater than the given serial +func GetUpdates(rpc *deltachat.Rpc, accId uint32, msgId uint32, serial uint32) ([]json.RawMessage, error) { + var rawUpdates []json.RawMessage + data, err := rpc.GetWebxdcStatusUpdates(accId, msgId, serial) + if err != nil { + return rawUpdates, err + } + err = json.Unmarshal([]byte(data), &rawUpdates) + return rawUpdates, err +} + +// Get the status update with the given serial +func GetUpdate(rpc *deltachat.Rpc, accId uint32, msgId uint32, serial uint32) (json.RawMessage, error) { + rawUpdates, err := GetUpdates(rpc, accId, msgId, serial-1) + if err != nil { + return nil, err + } + if len(rawUpdates) > 0 { + return rawUpdates[0], nil + } + return nil, errors.New("no new status update was found") +} + +// Send a WebXDC status update +func SendUpdate[T any](rpc *deltachat.Rpc, accId uint32, msgId uint32, update StatusUpdate[T], description string) error { + data, err := json.Marshal(update) + if err != nil { + return err + } + return rpc.SendWebxdcStatusUpdate(accId, msgId, string(data), &description) +} + +// Send a WebXDC status update with the given payload +func SendPayload[T any](rpc *deltachat.Rpc, accId uint32, msgId uint32, payload T) error { + return SendUpdate(rpc, accId, msgId, StatusUpdate[T]{Payload: payload}, "") +}