├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── config.go ├── root.go ├── run.go └── version.go ├── config └── config.go ├── go.mod ├── go.sum ├── goreleaser.Dockerfile ├── internal ├── discord │ └── discord.go ├── mixin │ └── mixin.go ├── telegram │ └── telegram.go └── wechat │ └── wechat.go ├── main.go ├── service ├── service.go └── service_test.go └── store └── store.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Go 11 | uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.19 14 | - name: Test 15 | run: go test ./... 16 | - name: Build 17 | run: go build 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | if: ${{ github.repository_owner != github.actor }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.19 20 | 21 | - name: "Docker login" 22 | run: docker login ghcr.io -u docker -p ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v3 26 | with: 27 | version: latest 28 | args: release --rm-dist 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | AWS_ACCESS_KEY_ID: ${{ secrets.BE_DEPLOYER_AWS_KEY }} 32 | AWS_SECRET_ACCESS_KEY: ${{ secrets.BE_DEPLOYER_AWS_SECRET }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | /PAL9000 18 | /config.yaml 19 | /keystore.json 20 | /keystore_*.json 21 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - main: . 7 | id: PAL9000 8 | binary: PAL9000 9 | goos: 10 | - linux 11 | goarch: 12 | - amd64 13 | env: 14 | - CGO_ENABLED=0 15 | ldflags: 16 | - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} 17 | dockers: 18 | - id: PAL9000 19 | goos: linux 20 | goarch: amd64 21 | dockerfile: goreleaser.Dockerfile 22 | image_templates: 23 | - "ghcr.io/pandodao/pal9000:latest" 24 | - "ghcr.io/pandodao/pal9000:{{ .Major }}" 25 | - "ghcr.io/pandodao/pal9000:{{ .Major }}.{{ .Minor }}" 26 | - "ghcr.io/pandodao/pal9000:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" 27 | blobs: 28 | - provider: s3 29 | region: us-east-2 30 | bucket: goreleaser-builds 31 | folder: "PAL9000" 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20-alpine3.17 AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN CGO_ENABLED=0 go build -trimpath 5 | 6 | FROM alpine:3.17 7 | WORKDIR /app 8 | COPY --from=builder /app/PAL9000 . 9 | ENTRYPOINT ["/app/PAL9000"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pando 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAL9000 2 | A bot wrapper for connecting to the botastic service https://developers.pando.im/guide/pal9000.html 3 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pandodao/PAL9000/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // configCmd represents the config command 11 | var configCmd = &cobra.Command{ 12 | Use: "config", 13 | Short: "Display default config", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | showExample, _ := cmd.Flags().GetBool("example") 16 | if showExample { 17 | fmt.Println(config.ExampleConfig()) 18 | } else { 19 | fmt.Println(config.DefaultConfig()) 20 | } 21 | }, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand(configCmd) 26 | configCmd.Flags().BoolP("example", "e", false, "Display example config") 27 | } 28 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | cfgFile string 11 | ) 12 | 13 | // rootCmd represents the base command when called without any subcommands 14 | var rootCmd = &cobra.Command{ 15 | Use: "PAL9000", 16 | Short: "PAL9000 is a tool to connect to botastic APIs", 17 | Long: `With pal9000, you can easily deploy your own bot application using botastic`, 18 | } 19 | 20 | func Execute() { 21 | err := rootCmd.Execute() 22 | if err != nil { 23 | os.Exit(1) 24 | } 25 | } 26 | 27 | func init() { 28 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.yaml", "config file (default is config.yaml)") 29 | } 30 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pandodao/PAL9000/config" 8 | "github.com/pandodao/PAL9000/internal/discord" 9 | "github.com/pandodao/PAL9000/internal/mixin" 10 | "github.com/pandodao/PAL9000/internal/telegram" 11 | "github.com/pandodao/PAL9000/internal/wechat" 12 | "github.com/pandodao/PAL9000/service" 13 | "github.com/pandodao/PAL9000/store" 14 | "github.com/spf13/cobra" 15 | "golang.org/x/sync/errgroup" 16 | ) 17 | 18 | type configKey struct{} 19 | 20 | // runCmd represents the run command 21 | var runCmd = &cobra.Command{ 22 | Use: "run", 23 | Short: "Run all bots by config", 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | cfg, err := config.Init(cfgFile) 26 | if err != nil { 27 | return err 28 | } 29 | cmd.SetContext(context.WithValue(cmd.Context(), configKey{}, cfg)) 30 | ctx := cmd.Context() 31 | 32 | startHandler := func(h *service.Handler, name string, adapterCfg config.AdapterConfig) error { 33 | fmt.Printf("Starting adapter, name: %s, driver: %s\n", name, adapterCfg.Driver) 34 | return h.Start(ctx) 35 | } 36 | 37 | g := errgroup.Group{} 38 | for _, name := range cfg.Adapters.Enabled { 39 | name := name 40 | adapter := cfg.Adapters.Items[name] 41 | switch adapter.Driver { 42 | case "mixin": 43 | g.Go(func() error { 44 | b, err := mixin.Init(ctx, name, *adapter.Mixin) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | h := service.NewHandler(getGeneralConfig(cfg.General, adapter.Mixin.GeneralConfig), store.NewMemoryStore(), b) 50 | return startHandler(h, name, adapter) 51 | }) 52 | case "telegram": 53 | g.Go(func() error { 54 | b, err := telegram.Init(name, *adapter.Telegram) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | h := service.NewHandler(getGeneralConfig(cfg.General, adapter.Telegram.GeneralConfig), store.NewMemoryStore(), b) 60 | return startHandler(h, name, adapter) 61 | }) 62 | case "discord": 63 | g.Go(func() error { 64 | b := discord.New(name, *adapter.Discord) 65 | h := service.NewHandler(getGeneralConfig(cfg.General, adapter.Discord.GeneralConfig), store.NewMemoryStore(), b) 66 | return startHandler(h, name, adapter) 67 | }) 68 | case "wechat": 69 | g.Go(func() error { 70 | b := wechat.New(name, *adapter.WeChat) 71 | h := service.NewHandler(getGeneralConfig(cfg.General, adapter.WeChat.GeneralConfig), store.NewMemoryStore(), b) 72 | return startHandler(h, name, adapter) 73 | }) 74 | } 75 | } 76 | 77 | return g.Wait() 78 | }, 79 | } 80 | 81 | func init() { 82 | rootCmd.AddCommand(runCmd) 83 | } 84 | 85 | func getGeneralConfig(defaultCfg, overrideCfg config.GeneralConfig) config.GeneralConfig { 86 | cfg := defaultCfg 87 | if overrideCfg.Bot != nil { 88 | cfg.Bot = overrideCfg.Bot 89 | } 90 | if overrideCfg.Botastic != nil { 91 | cfg.Botastic = overrideCfg.Botastic 92 | } 93 | if overrideCfg.Options != nil { 94 | cfg.Options = overrideCfg.Options 95 | } 96 | 97 | return cfg 98 | } 99 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | versionRevision = "unknown" 12 | versionTime = "unknown" 13 | versionModified = "unknown" 14 | ) 15 | 16 | // versionCmd represents the version command 17 | var versionCmd = &cobra.Command{ 18 | Use: "version", 19 | Short: "Display version info", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | m := map[string]string{ 22 | "revision": versionRevision, 23 | "time": versionTime, 24 | "modified": versionModified, 25 | } 26 | for k, v := range m { 27 | fmt.Printf("%s: %s\n", k, v) 28 | } 29 | }, 30 | } 31 | 32 | func init() { 33 | rootCmd.AddCommand(versionCmd) 34 | 35 | info, ok := debug.ReadBuildInfo() 36 | if !ok { 37 | panic("no version info") 38 | } 39 | 40 | m := map[string]func(string){ 41 | "vcs.revision": func(s string) { versionRevision = s }, 42 | "vcs.time": func(s string) { versionTime = s }, 43 | "vcs.modified": func(s string) { versionModified = s }, 44 | } 45 | for _, kv := range info.Settings { 46 | if f, ok := m[kv.Key]; ok { 47 | f(kv.Value) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type Config struct { 11 | General GeneralConfig `yaml:"general"` 12 | Adapters AdaptersConfig `yaml:"adapters"` 13 | } 14 | 15 | func (s *Config) String() string { 16 | data, _ := yaml.Marshal(s) 17 | return string(data) 18 | } 19 | 20 | type BotConfig struct { 21 | BotID uint64 `yaml:"bot_id"` 22 | Lang string `yaml:"lang"` 23 | } 24 | 25 | type BotasticConfig struct { 26 | AppId string `yaml:"app_id"` 27 | Host string `yaml:"host"` 28 | Debug bool `yaml:"debug"` 29 | } 30 | 31 | type GeneralOptionsConfig struct { 32 | IgnoreIfError bool `yaml:"ignore_if_error"` 33 | FormatLinks bool `yaml:"format_links"` 34 | } 35 | 36 | type GeneralConfig struct { 37 | Options *GeneralOptionsConfig `yaml:"options,omitempty"` 38 | Bot *BotConfig `yaml:"bot,omitempty"` 39 | Botastic *BotasticConfig `yaml:"botastic,omitempty"` 40 | } 41 | 42 | type AdaptersConfig struct { 43 | Enabled []string `yaml:"enabled"` 44 | Items map[string]AdapterConfig `yaml:"items"` 45 | } 46 | 47 | type AdapterConfig struct { 48 | Driver string `yaml:"driver"` 49 | Mixin *MixinConfig `yaml:"mixin,omitempty"` 50 | Telegram *TelegramConfig `yaml:"telegram,omitempty"` 51 | Discord *DiscordConfig `yaml:"discord,omitempty"` 52 | WeChat *WeChatConfig `yaml:"wechat,omitempty"` 53 | } 54 | 55 | type WeChatConfig struct { 56 | GeneralConfig `yaml:",inline"` 57 | 58 | Address string `yaml:"address"` 59 | Path string `yaml:"path"` 60 | Token string `yaml:"token"` 61 | } 62 | 63 | type MixinConfig struct { 64 | GeneralConfig `yaml:",inline"` 65 | 66 | Keystore string `yaml:"keystore"` // base64 encoded keystore (json format) 67 | Whitelist []string `yaml:"whitelist"` 68 | MessageCacheExpiration int64 `yaml:"message_cache_expiration"` 69 | } 70 | 71 | type TelegramConfig struct { 72 | GeneralConfig `yaml:",inline"` 73 | 74 | Debug bool `yaml:"debug"` 75 | Token string `yaml:"token"` 76 | Whitelist []string `yaml:"whitelist"` 77 | } 78 | 79 | type DiscordConfig struct { 80 | GeneralConfig `yaml:",inline"` 81 | 82 | Token string `yaml:"token"` 83 | Whitelist []string `yaml:"whitelist"` 84 | } 85 | 86 | func DefaultConfig() *Config { 87 | return &Config{ 88 | General: GeneralConfig{ 89 | Options: &GeneralOptionsConfig{ 90 | IgnoreIfError: true, 91 | }, 92 | Bot: &BotConfig{ 93 | Lang: "en", 94 | }, 95 | }, 96 | } 97 | } 98 | 99 | func ExampleConfig() *Config { 100 | return &Config{ 101 | General: GeneralConfig{ 102 | Bot: &BotConfig{ 103 | BotID: 1, 104 | Lang: "en", 105 | }, 106 | Botastic: &BotasticConfig{ 107 | AppId: "cab1582e-9c30-4d1e-9246-a5c80f74f8f9", 108 | Host: "https://botastic-api.pando.im", 109 | Debug: true, 110 | }, 111 | }, 112 | Adapters: AdaptersConfig{ 113 | Enabled: []string{"test_mixin", "test_telegram", "test_discord", "test_wechat"}, 114 | Items: map[string]AdapterConfig{ 115 | "test_mixin": { 116 | Driver: "mixin", 117 | Mixin: &MixinConfig{ 118 | Keystore: "base64 encoded keystore", 119 | Whitelist: []string{"7000104111", "a8d4e38e-9317-4529-8ca9-4289d4668111"}, 120 | MessageCacheExpiration: 60 * 60 * 24, 121 | }, 122 | }, 123 | "test_telegram": { 124 | Driver: "telegram", 125 | Telegram: &TelegramConfig{ 126 | Debug: true, 127 | Token: "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ", 128 | Whitelist: []string{"-10540154212", "xx"}, 129 | GeneralConfig: GeneralConfig{ 130 | Bot: &BotConfig{ 131 | BotID: 2, 132 | Lang: "zh", 133 | }, 134 | Botastic: &BotasticConfig{ 135 | AppId: "cab1582e-9c30-4d1e-9246-a5c80f74f8f9", 136 | Host: "https://botastic-api.pando.im", 137 | }, 138 | }, 139 | }, 140 | }, 141 | "test_discord": { 142 | Driver: "discord", 143 | Discord: &DiscordConfig{ 144 | Token: "1234567890", 145 | Whitelist: []string{"1093104389113266186"}, 146 | }, 147 | }, 148 | "test_wechat": { 149 | Driver: "wechat", 150 | WeChat: &WeChatConfig{ 151 | Address: ":8080", 152 | Path: "/wechat", 153 | Token: "123456", 154 | }, 155 | }, 156 | }, 157 | }, 158 | } 159 | } 160 | 161 | func (c Config) validate() error { 162 | for _, name := range c.Adapters.Enabled { 163 | if _, ok := c.Adapters.Items[name]; !ok { 164 | return fmt.Errorf("adapter not found: %s", name) 165 | } 166 | } 167 | for name, c := range c.Adapters.Items { 168 | switch c.Driver { 169 | case "mixin": 170 | if c.Mixin == nil { 171 | return fmt.Errorf("config not found, name: %s, driver: %s", name, c.Driver) 172 | } 173 | case "telegram": 174 | if c.Telegram == nil { 175 | return fmt.Errorf("config not found, name: %s, driver: %s", name, c.Driver) 176 | } 177 | case "discord": 178 | if c.Discord == nil { 179 | return fmt.Errorf("config not found, name: %s, driver: %s", name, c.Driver) 180 | } 181 | case "wechat": 182 | if c.WeChat == nil { 183 | return fmt.Errorf("config not found, name: %s, driver: %s", name, c.Driver) 184 | } 185 | default: 186 | return fmt.Errorf("invalid driver, name: %s, driver: %s", name, c.Driver) 187 | } 188 | } 189 | return nil 190 | } 191 | 192 | func Init(fp string) (*Config, error) { 193 | c := DefaultConfig() 194 | 195 | data, err := ioutil.ReadFile(fp) 196 | if err != nil { 197 | return nil, fmt.Errorf("ioutil.ReadFile error: %w", err) 198 | } 199 | 200 | if err := yaml.Unmarshal(data, c); err != nil { 201 | return nil, fmt.Errorf("yaml.Unmarshal error: %w", err) 202 | } 203 | 204 | if err := c.validate(); err != nil { 205 | return nil, fmt.Errorf("validate error: %w", err) 206 | } 207 | 208 | return c, nil 209 | } 210 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pandodao/PAL9000 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.27.1 7 | github.com/fox-one/mixin-sdk-go v1.7.9 8 | github.com/fox-one/pkg/uuid v0.0.1 9 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 10 | github.com/pandodao/botastic-go v0.0.2 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/sirupsen/logrus v1.9.0 13 | github.com/spf13/cobra v1.6.1 14 | golang.org/x/sync v0.2.0 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | filippo.io/edwards25519 v1.0.0 // indirect 20 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect 21 | github.com/fox-one/msgpack v1.0.0 // indirect 22 | github.com/go-resty/resty/v2 v2.7.0 // indirect 23 | github.com/gofrs/uuid v4.4.0+incompatible // indirect 24 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 25 | github.com/golang/protobuf v1.5.3 // indirect 26 | github.com/gorilla/websocket v1.5.0 // indirect 27 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 28 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 29 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect 30 | github.com/shopspring/decimal v1.3.1 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | github.com/vmihailenco/tagparser v0.1.2 // indirect 33 | github.com/zeebo/blake3 v0.2.3 // indirect 34 | golang.org/x/crypto v0.9.0 // indirect 35 | golang.org/x/net v0.10.0 // indirect 36 | golang.org/x/sys v0.8.0 // indirect 37 | google.golang.org/appengine v1.6.7 // indirect 38 | google.golang.org/protobuf v1.30.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= 3 | filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 6 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 7 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 8 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 9 | github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= 10 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= 11 | github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= 12 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 13 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 14 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 15 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 16 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 17 | github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= 18 | github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 19 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 20 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 29 | github.com/fox-one/mixin-sdk-go v1.7.9 h1:pYln3slgNDZfAjJsipRI3Uv1088iBfy6oB2hEdfo8yk= 30 | github.com/fox-one/mixin-sdk-go v1.7.9/go.mod h1:DCY3NZO2ZP+Vmk9prOlA10/9ZpT+3XDXmP0zyi0GzbU= 31 | github.com/fox-one/msgpack v1.0.0 h1:atr4La29WdMPCoddlRAPK2e1yhBJ2cEFF+2X93KY5Vs= 32 | github.com/fox-one/msgpack v1.0.0/go.mod h1:Gf/g5JQGPkB0JrQvfxCu8ZXm4jqXsCPe89mFe8i3vms= 33 | github.com/fox-one/pkg/uuid v0.0.1 h1:ojw5qedSgjhnuiA3el1PzwoWr6/YCkGp2spoKCWv2WU= 34 | github.com/fox-one/pkg/uuid v0.0.1/go.mod h1:1XTcyCUoLnhroms+lrHzvIQOuu9pp/1BCX+0CrarZLg= 35 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 36 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 37 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 38 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 39 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 40 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 41 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 42 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 43 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 44 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 45 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 46 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 48 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 50 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 51 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 52 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 53 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 54 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 55 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 56 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 57 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 58 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 59 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 60 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 61 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 62 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 63 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 64 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 65 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 66 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 68 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 69 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 70 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 71 | github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= 72 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 73 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 74 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 75 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 76 | github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= 77 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 78 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 79 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 80 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 81 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 82 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 83 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 84 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 85 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 86 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 87 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 88 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= 89 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= 90 | github.com/pandodao/botastic-go v0.0.2 h1:jVF+F/WKGmO7tnXCDOQFSlzm21Qasy/+8VdNV/n+LP8= 91 | github.com/pandodao/botastic-go v0.0.2/go.mod h1:ADFMMhpKA6nVFk0kinNpaNtzf8c6WG/xecHRj4YzF14= 92 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 93 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 94 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 95 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 96 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 97 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 98 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 99 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 100 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 101 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 102 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 103 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 104 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 105 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 108 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 109 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 110 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 112 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 113 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 114 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 115 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 116 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 117 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 118 | github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= 119 | github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 120 | github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= 121 | github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= 122 | github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= 123 | github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= 124 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 125 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 126 | golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 127 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 128 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 129 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 130 | golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= 131 | golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= 132 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 133 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 134 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 135 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 136 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 137 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 138 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 141 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 143 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 144 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 145 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 146 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 147 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 148 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 149 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 150 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 151 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 152 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 153 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 154 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 155 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 157 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 158 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= 161 | golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 162 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 163 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 174 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 175 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 176 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 177 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 178 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 179 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 180 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 181 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 182 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 183 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 184 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 185 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 186 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 187 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 188 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 189 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 190 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 191 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 192 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 193 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 194 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 195 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 196 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 197 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 198 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 200 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 202 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 203 | google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= 204 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 205 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 206 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 207 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 208 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 209 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 210 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 211 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 212 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 213 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 214 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 215 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 216 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 217 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 218 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 219 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 220 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 221 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 222 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 223 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 224 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 225 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 226 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 227 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 228 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 229 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 230 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 231 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 232 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 233 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 234 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 235 | -------------------------------------------------------------------------------- /goreleaser.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.17 2 | WORKDIR /app 3 | COPY PAL9000 . 4 | ENTRYPOINT ["/app/PAL9000"] 5 | -------------------------------------------------------------------------------- /internal/discord/discord.go: -------------------------------------------------------------------------------- 1 | package discord 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | "github.com/pandodao/PAL9000/config" 11 | "github.com/pandodao/PAL9000/service" 12 | ) 13 | 14 | var _ service.Adapter = (*Bot)(nil) 15 | 16 | type messageKey struct{} 17 | type sessionKey struct{} 18 | 19 | type Bot struct { 20 | name string 21 | cfg config.DiscordConfig 22 | } 23 | 24 | func New(name string, cfg config.DiscordConfig) *Bot { 25 | return &Bot{ 26 | name: name, 27 | cfg: cfg, 28 | } 29 | } 30 | 31 | func (b *Bot) GetName() string { 32 | return b.name 33 | } 34 | 35 | func (b *Bot) GetMessageChan(ctx context.Context) <-chan *service.Message { 36 | msgChan := make(chan *service.Message) 37 | 38 | dg, _ := discordgo.New("Bot " + b.cfg.Token) 39 | dg.Identify.Intents = discordgo.IntentGuildMessages | discordgo.IntentDirectMessages | discordgo.IntentMessageContent 40 | dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { 41 | if m.Author.ID == s.State.User.ID { 42 | return 43 | } 44 | 45 | // only text message 46 | if m.Type != discordgo.MessageTypeDefault && m.Type != discordgo.MessageTypeReply { 47 | return 48 | } 49 | 50 | allowed := len(b.cfg.Whitelist) == 0 51 | for _, id := range b.cfg.Whitelist { 52 | if id == m.Author.ID || (m.GuildID != "" && id == m.GuildID) { 53 | allowed = true 54 | break 55 | } 56 | } 57 | 58 | if !allowed { 59 | return 60 | } 61 | 62 | prefix := fmt.Sprintf("<@%s>", s.State.User.ID) 63 | 64 | if m.GuildID != "" { 65 | // return if not mentioned or not reply to bot 66 | if !(strings.HasPrefix(m.Content, prefix) || (m.ReferencedMessage != nil && m.ReferencedMessage.Author.ID == s.State.User.ID)) { 67 | return 68 | } 69 | } 70 | 71 | replyContent := "" 72 | if m.ReferencedMessage != nil { 73 | replyContent = m.ReferencedMessage.Content 74 | } 75 | 76 | content := strings.TrimSpace(strings.TrimPrefix(m.Content, prefix)) 77 | ctx = context.WithValue(ctx, messageKey{}, m) 78 | ctx = context.WithValue(ctx, sessionKey{}, s) 79 | 80 | msgChan <- &service.Message{ 81 | Context: ctx, 82 | ReplyContent: replyContent, 83 | UserIdentity: m.Author.ID, 84 | Content: content, 85 | ConvKey: m.ChannelID, 86 | } 87 | }) 88 | 89 | go func() { 90 | if err := dg.Open(); err != nil { 91 | log.Printf("error opening connection to Discord, %v\n", err) 92 | } 93 | 94 | select { 95 | case <-ctx.Done(): 96 | dg.Close() 97 | close(msgChan) 98 | return 99 | } 100 | }() 101 | 102 | return msgChan 103 | } 104 | 105 | func (b *Bot) HandleResult(req *service.Message, r *service.Result) { 106 | if r.Err != nil && r.IgnoreIfError { 107 | return 108 | } 109 | text := "" 110 | if r.Err != nil { 111 | text = r.Err.Error() 112 | } else { 113 | text = r.ConvTurn.Response 114 | } 115 | msg := req.Context.Value(messageKey{}).(*discordgo.MessageCreate) 116 | s := req.Context.Value(sessionKey{}).(*discordgo.Session) 117 | if _, err := s.ChannelMessageSend(msg.ChannelID, text); err != nil { 118 | log.Printf("error sending message to Discord, %v\n", err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/mixin/mixin.go: -------------------------------------------------------------------------------- 1 | package mixin 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fox-one/mixin-sdk-go" 13 | "github.com/fox-one/pkg/uuid" 14 | "github.com/pandodao/PAL9000/config" 15 | "github.com/pandodao/PAL9000/service" 16 | "github.com/patrickmn/go-cache" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type Message struct { 21 | Content string 22 | UserID string 23 | } 24 | 25 | type ( 26 | messageKey struct{} 27 | userKey struct{} 28 | convKey struct{} 29 | ) 30 | 31 | var _ service.Adapter = (*Bot)(nil) 32 | 33 | type Bot struct { 34 | name string 35 | convMap map[string]*mixin.Conversation 36 | userMap map[string]*mixin.User 37 | 38 | client *mixin.Client 39 | msgChan chan *service.Message 40 | me *mixin.User 41 | cfg config.MixinConfig 42 | logger logrus.FieldLogger 43 | messageCache *cache.Cache 44 | } 45 | 46 | func Init(ctx context.Context, name string, cfg config.MixinConfig) (*Bot, error) { 47 | data, err := base64.StdEncoding.DecodeString(cfg.Keystore) 48 | if err != nil { 49 | return nil, fmt.Errorf("base64 decode keystore error: %w", err) 50 | } 51 | 52 | var keystore mixin.Keystore 53 | if err := json.Unmarshal(data, &keystore); err != nil { 54 | return nil, fmt.Errorf("json unmarshal keystore error: %w", err) 55 | } 56 | 57 | client, err := mixin.NewFromKeystore(&keystore) 58 | if err != nil { 59 | return nil, fmt.Errorf("mixin.NewFromKeystore error: %w", err) 60 | } 61 | 62 | me, err := client.UserMe(ctx) 63 | if err != nil { 64 | return nil, fmt.Errorf("mixinClient.UserMe error: %w", err) 65 | } 66 | 67 | if cfg.MessageCacheExpiration == 0 { 68 | cfg.MessageCacheExpiration = 60 * 60 * 24 69 | } 70 | 71 | return &Bot{ 72 | name: name, 73 | convMap: make(map[string]*mixin.Conversation), 74 | userMap: make(map[string]*mixin.User), 75 | client: client, 76 | msgChan: make(chan *service.Message), 77 | cfg: cfg, 78 | me: me, 79 | logger: logrus.WithField("adapter", "mixin").WithField("name", name), 80 | messageCache: cache.New(time.Duration(cfg.MessageCacheExpiration)*time.Second, 10*time.Minute), 81 | }, nil 82 | } 83 | 84 | func (b *Bot) GetName() string { 85 | return b.name 86 | } 87 | 88 | func (b *Bot) GetMessageChan(ctx context.Context) <-chan *service.Message { 89 | go func() { 90 | for { 91 | b.logger.Info("start to get message") 92 | if err := b.client.LoopBlaze(ctx, mixin.BlazeListenFunc(b.run)); err != nil { 93 | b.logger.WithError(err).Error("loop blaze error") 94 | } 95 | 96 | select { 97 | case <-ctx.Done(): 98 | b.logger.Info("get message chan done") 99 | close(b.msgChan) 100 | return 101 | case <-time.After(time.Second): 102 | } 103 | } 104 | }() 105 | 106 | return b.msgChan 107 | } 108 | 109 | func (b *Bot) HandleResult(req *service.Message, r *service.Result) { 110 | defer close(req.DoneChan) 111 | 112 | b.logger.WithField("result", r).Info("get result") 113 | if r.Err != nil && r.IgnoreIfError { 114 | b.logger.WithError(r.Err).Error("ignore error") 115 | return 116 | } 117 | 118 | msg := req.Context.Value(messageKey{}).(*mixin.MessageView) 119 | user := req.Context.Value(userKey{}).(*mixin.User) 120 | conv := req.Context.Value(convKey{}).(*mixin.Conversation) 121 | 122 | mq := &mixin.MessageRequest{ 123 | ConversationID: msg.ConversationID, 124 | MessageID: uuid.Modify(msg.MessageID, "reply"), 125 | Category: msg.Category, 126 | } 127 | 128 | text := "" 129 | if r.Err != nil { 130 | text = r.Err.Error() 131 | } else { 132 | text = r.ConvTurn.Response 133 | } 134 | 135 | b.messageCache.Add(mq.MessageID, &Message{ 136 | Content: text, 137 | UserID: b.me.UserID, 138 | }, cache.DefaultExpiration) 139 | 140 | if conv.Category == mixin.ConversationCategoryGroup { 141 | text = fmt.Sprintf("> @%s %s\n\n%s", user.IdentityNumber, req.Content, text) 142 | } 143 | mq.Data = base64.StdEncoding.EncodeToString([]byte(text)) 144 | if err := b.client.SendMessage(req.Context, mq); err != nil { 145 | b.logger.WithError(err).Error("send message error") 146 | } 147 | } 148 | 149 | func (b *Bot) run(ctx context.Context, msg *mixin.MessageView, userID string) error { 150 | b.logger.WithField("msg", msg).Info("in run func, get message") 151 | 152 | if msg.Category != mixin.MessageCategoryPlainText { 153 | return nil 154 | } 155 | if uuid.IsNil(msg.UserID) { 156 | return nil 157 | } 158 | conv, err := b.getConversation(ctx, msg.ConversationID) 159 | if err != nil { 160 | log.Println("getConversation error:", err) 161 | return nil 162 | } 163 | user, err := b.getUser(ctx, msg.UserID) 164 | if err != nil { 165 | log.Println("getUser error:", err) 166 | return nil 167 | } 168 | if user.IdentityNumber == "0" { 169 | log.Println("user is not a messenger user, ignored") 170 | return nil 171 | } 172 | 173 | data, err := base64.StdEncoding.DecodeString(msg.Data) 174 | if err != nil { 175 | return nil 176 | } 177 | content := string(data) 178 | prefix := fmt.Sprintf("@%s", b.me.IdentityNumber) 179 | 180 | b.messageCache.Add(msg.MessageID, &Message{ 181 | Content: strings.TrimPrefix(content, prefix), 182 | }, cache.DefaultExpiration) 183 | 184 | allowed := len(b.cfg.Whitelist) == 0 185 | for _, id := range b.cfg.Whitelist { 186 | if id == user.IdentityNumber || conv.ConversationID == id { 187 | allowed = true 188 | break 189 | } 190 | } 191 | if !allowed { 192 | return nil 193 | } 194 | 195 | conversationKey := msg.ConversationID + ":" + msg.UserID 196 | 197 | var quoteMessage *Message 198 | if msg.QuoteMessageID != "" { 199 | if v, ok := b.messageCache.Get(msg.QuoteMessageID); ok { 200 | quoteMessage = v.(*Message) 201 | } 202 | } 203 | 204 | // super group bot 205 | if strings.HasPrefix(user.IdentityNumber, "700") { 206 | if quoteMessage == nil || quoteMessage.UserID != b.me.UserID { 207 | if !strings.HasPrefix(content, prefix) || msg.RepresentativeID == "" { 208 | return nil 209 | } 210 | } 211 | conversationKey = msg.ConversationID + ":" + msg.RepresentativeID 212 | } 213 | 214 | replyContent := "" 215 | if quoteMessage != nil { 216 | replyContent = quoteMessage.Content 217 | } 218 | content = strings.TrimSpace(strings.TrimPrefix(content, prefix)) 219 | 220 | ctx = context.WithValue(ctx, messageKey{}, msg) 221 | ctx = context.WithValue(ctx, userKey{}, user) 222 | ctx = context.WithValue(ctx, convKey{}, conv) 223 | 224 | doneChan := make(chan struct{}) 225 | b.msgChan <- &service.Message{ 226 | Context: ctx, 227 | UserIdentity: msg.UserID, 228 | ConvKey: conversationKey, 229 | ReplyContent: replyContent, 230 | Content: content, 231 | DoneChan: doneChan, 232 | } 233 | 234 | <-doneChan 235 | return nil 236 | } 237 | 238 | func (b *Bot) getConversation(ctx context.Context, convID string) (*mixin.Conversation, error) { 239 | if conv, ok := b.convMap[convID]; ok { 240 | return conv, nil 241 | } 242 | conv, err := b.client.ReadConversation(ctx, convID) 243 | if err != nil { 244 | return nil, err 245 | } 246 | b.convMap[convID] = conv 247 | return conv, nil 248 | } 249 | 250 | func (b *Bot) getUser(ctx context.Context, userID string) (*mixin.User, error) { 251 | if user, ok := b.userMap[userID]; ok { 252 | return user, nil 253 | } 254 | user, err := b.client.ReadUser(ctx, userID) 255 | if err != nil { 256 | return nil, err 257 | } 258 | b.userMap[userID] = user 259 | return user, nil 260 | } 261 | -------------------------------------------------------------------------------- /internal/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 10 | "github.com/pandodao/PAL9000/config" 11 | "github.com/pandodao/PAL9000/service" 12 | ) 13 | 14 | var _ service.Adapter = (*Bot)(nil) 15 | 16 | type ( 17 | messageKey struct{} 18 | ) 19 | 20 | type Bot struct { 21 | name string 22 | cfg config.TelegramConfig 23 | client *tgbotapi.BotAPI 24 | } 25 | 26 | func Init(name string, cfg config.TelegramConfig) (*Bot, error) { 27 | bot, err := tgbotapi.NewBotAPI(cfg.Token) 28 | if err != nil { 29 | return nil, err 30 | } 31 | bot.Debug = cfg.Debug 32 | 33 | return &Bot{ 34 | name: name, 35 | cfg: cfg, 36 | client: bot, 37 | }, nil 38 | } 39 | 40 | func (b *Bot) GetName() string { 41 | return b.name 42 | } 43 | 44 | func (b *Bot) GetMessageChan(ctx context.Context) <-chan *service.Message { 45 | msgChan := make(chan *service.Message) 46 | go func() { 47 | u := tgbotapi.NewUpdate(0) 48 | updates := b.client.GetUpdatesChan(u) 49 | for update := range updates { 50 | if update.Message == nil || update.Message.Chat == nil || update.Message.Text == "" { 51 | continue 52 | } 53 | 54 | allowed := len(b.cfg.Whitelist) == 0 55 | for _, id := range b.cfg.Whitelist { 56 | if strconv.FormatInt(update.Message.Chat.ID, 10) == id || strconv.FormatInt(update.Message.From.ID, 10) == id { 57 | allowed = true 58 | break 59 | } 60 | } 61 | if !allowed { 62 | continue 63 | } 64 | 65 | prefix := "@" + b.client.Self.UserName 66 | if update.Message.Chat.IsGroup() || update.Message.Chat.IsSuperGroup() { 67 | if update.Message.ReplyToMessage == nil || update.Message.ReplyToMessage.From.ID != b.client.Self.ID { 68 | if !strings.HasPrefix(update.Message.Text, prefix) { 69 | continue 70 | } 71 | } 72 | } 73 | replyContent := "" 74 | if update.Message.ReplyToMessage != nil { 75 | replyContent = update.Message.ReplyToMessage.Text 76 | } 77 | 78 | content := strings.TrimSpace(strings.TrimPrefix(update.Message.Text, prefix)) 79 | messageCtx := context.WithValue(ctx, messageKey{}, update.Message) 80 | msgChan <- &service.Message{ 81 | ReplyContent: replyContent, 82 | Context: messageCtx, 83 | Content: content, 84 | UserIdentity: strconv.FormatInt(update.Message.From.ID, 10), 85 | ConvKey: strconv.FormatInt(update.Message.Chat.ID, 10), 86 | } 87 | } 88 | select { 89 | case <-ctx.Done(): 90 | b.client.StopReceivingUpdates() 91 | close(msgChan) 92 | return 93 | } 94 | }() 95 | 96 | return msgChan 97 | } 98 | 99 | func (b *Bot) HandleResult(req *service.Message, r *service.Result) { 100 | if r.Err != nil && r.IgnoreIfError { 101 | return 102 | } 103 | text := "" 104 | if r.Err != nil { 105 | text = r.Err.Error() 106 | } else { 107 | text = r.ConvTurn.Response 108 | } 109 | msg := req.Context.Value(messageKey{}).(*tgbotapi.Message) 110 | reply := tgbotapi.NewMessage(msg.Chat.ID, text) 111 | // reply.ReplyToMessageID = msg.MessageID 112 | if _, err := b.client.Send(reply); err != nil { 113 | fmt.Printf("send reply failed: %v\n", err) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /internal/wechat/wechat.go: -------------------------------------------------------------------------------- 1 | package wechat 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "encoding/xml" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "sort" 13 | "strings" 14 | "time" 15 | 16 | "github.com/pandodao/PAL9000/config" 17 | "github.com/pandodao/PAL9000/service" 18 | ) 19 | 20 | type httpRequsetKey struct{} 21 | type httpResponseKey struct{} 22 | type rawMessageKey struct{} 23 | 24 | type TextMessage struct { 25 | XMLName xml.Name `xml:"xml"` 26 | ToUserName string `xml:"ToUserName"` 27 | FromUserName string `xml:"FromUserName"` 28 | CreateTime int64 `xml:"CreateTime"` 29 | MsgType string `xml:"MsgType"` 30 | Content string `xml:"Content"` 31 | MsgId int64 `xml:"MsgId"` 32 | } 33 | 34 | type Bot struct { 35 | name string 36 | cfg config.WeChatConfig 37 | } 38 | 39 | func New(name string, cfg config.WeChatConfig) *Bot { 40 | return &Bot{ 41 | name: name, 42 | cfg: cfg, 43 | } 44 | } 45 | 46 | func (b *Bot) GetName() string { 47 | return b.name 48 | } 49 | 50 | func (b *Bot) GetMessageChan(ctx context.Context) <-chan *service.Message { 51 | msgChan := make(chan *service.Message) 52 | go func() { 53 | server := &http.Server{ 54 | Addr: b.cfg.Address, 55 | Handler: http.DefaultServeMux, 56 | } 57 | 58 | validateSignature := func(signature, timestamp, nonce string) bool { 59 | params := []string{b.cfg.Token, timestamp, nonce} 60 | sort.Strings(params) 61 | combined := strings.Join(params, "") 62 | 63 | hash := sha1.New() 64 | hash.Write([]byte(combined)) 65 | hashStr := hex.EncodeToString(hash.Sum(nil)) 66 | 67 | return hashStr == signature 68 | } 69 | 70 | http.HandleFunc(b.cfg.Path, func(w http.ResponseWriter, r *http.Request) { 71 | r.ParseForm() 72 | signature := r.Form.Get("signature") 73 | timestamp := r.Form.Get("timestamp") 74 | nonce := r.Form.Get("nonce") 75 | echostr := r.Form.Get("echostr") 76 | 77 | if !validateSignature(signature, timestamp, nonce) { 78 | http.Error(w, "Invalid signature", http.StatusForbidden) 79 | return 80 | } 81 | 82 | if r.Method == "GET" { 83 | w.Write([]byte(echostr)) 84 | return 85 | } 86 | 87 | body, err := io.ReadAll(r.Body) 88 | if err != nil { 89 | http.Error(w, "Failed to read request body", http.StatusBadRequest) 90 | return 91 | } 92 | fmt.Println(string(body)) 93 | 94 | var receivedMessage TextMessage 95 | err = xml.Unmarshal(body, &receivedMessage) 96 | if err != nil { 97 | http.Error(w, "Failed to parse request body", http.StatusBadRequest) 98 | return 99 | } 100 | 101 | ctx = r.Context() 102 | ctx = context.WithValue(ctx, httpRequsetKey{}, r) 103 | ctx = context.WithValue(ctx, httpResponseKey{}, w) 104 | ctx = context.WithValue(ctx, rawMessageKey{}, receivedMessage) 105 | doneChan := make(chan struct{}) 106 | msgChan <- &service.Message{ 107 | Context: ctx, 108 | UserIdentity: receivedMessage.FromUserName, 109 | ConvKey: receivedMessage.FromUserName, 110 | Content: receivedMessage.Content, 111 | DoneChan: doneChan, 112 | } 113 | <-doneChan 114 | }) 115 | 116 | go func() { 117 | fmt.Printf("wechat HTTP server run at: %s\n", b.cfg.Address) 118 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 119 | log.Fatalf("listen: %s\n", err) 120 | } 121 | }() 122 | 123 | <-ctx.Done() 124 | 125 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 126 | defer cancel() 127 | 128 | if err := server.Shutdown(ctx); err != nil { 129 | log.Printf("Server forced to shutdown: %v\n", err) 130 | } else { 131 | log.Println("Server gracefully stopped") 132 | } 133 | }() 134 | 135 | return msgChan 136 | } 137 | 138 | func (b *Bot) HandleResult(req *service.Message, r *service.Result) { 139 | defer close(req.DoneChan) 140 | 141 | w := req.Context.Value(httpResponseKey{}).(http.ResponseWriter) 142 | if r.Err != nil && r.IgnoreIfError { 143 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 144 | w.Write([]byte("")) 145 | return 146 | } 147 | receivedMessage := req.Context.Value(rawMessageKey{}).(TextMessage) 148 | 149 | text := "" 150 | if r.Err != nil { 151 | text = r.Err.Error() 152 | } else { 153 | text = r.ConvTurn.Response 154 | } 155 | 156 | responseMessage := TextMessage{ 157 | ToUserName: receivedMessage.FromUserName, 158 | FromUserName: receivedMessage.ToUserName, 159 | CreateTime: time.Now().Unix(), 160 | MsgType: "text", 161 | Content: text, 162 | } 163 | 164 | responseXML, err := xml.MarshalIndent(responseMessage, "", " ") 165 | if err != nil { 166 | http.Error(w, "Failed to create response", http.StatusInternalServerError) 167 | return 168 | } 169 | 170 | w.Header().Set("Content-Type", "application/xml; charset=utf-8") 171 | w.Write(responseXML) 172 | } 173 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/pandodao/PAL9000/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/pandodao/PAL9000/config" 10 | "github.com/pandodao/PAL9000/store" 11 | "github.com/pandodao/botastic-go" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var ( 16 | linkRegex = regexp.MustCompile(`https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`) 17 | ) 18 | 19 | type Adapter interface { 20 | GetName() string 21 | GetMessageChan(ctx context.Context) <-chan *Message 22 | HandleResult(message *Message, result *Result) 23 | } 24 | 25 | type Handler struct { 26 | cfg config.GeneralConfig 27 | client *botastic.Client 28 | store store.Store 29 | adapter Adapter 30 | logger *logrus.Entry 31 | } 32 | 33 | type Message struct { 34 | Context context.Context 35 | BotID uint64 36 | Lang string 37 | 38 | UserIdentity string 39 | ConvKey string 40 | Content string 41 | ReplyContent string 42 | 43 | DoneChan chan struct{} 44 | } 45 | 46 | type Result struct { 47 | ConvTurn *botastic.ConvTurn 48 | Err error 49 | IgnoreIfError bool 50 | } 51 | 52 | func NewHandler(cfg config.GeneralConfig, store store.Store, adapter Adapter) *Handler { 53 | client := botastic.New(cfg.Botastic.AppId, "", botastic.WithDebug(cfg.Botastic.Debug), botastic.WithHost(cfg.Botastic.Host)) 54 | return &Handler{ 55 | cfg: cfg, 56 | client: client, 57 | store: store, 58 | adapter: adapter, 59 | logger: logrus.WithField("adapter", fmt.Sprintf("%T", adapter)).WithField("component", "service").WithField("adapter_name", adapter.GetName()), 60 | } 61 | } 62 | 63 | func (h *Handler) Start(ctx context.Context) error { 64 | msgChan := h.adapter.GetMessageChan(ctx) 65 | 66 | for { 67 | select { 68 | case msg := <-msgChan: 69 | h.logger.WithField("msg", msg).Info("received message") 70 | if msg.BotID == 0 { 71 | msg.BotID = h.cfg.Bot.BotID 72 | } 73 | if msg.Lang == "" { 74 | msg.Lang = h.cfg.Bot.Lang 75 | } 76 | 77 | turn, err := h.handleMessage(ctx, msg) 78 | h.logger.WithFields(logrus.Fields{ 79 | "turn": turn, 80 | "result_err": err, 81 | }).Info("handled message") 82 | h.adapter.HandleResult(msg, &Result{ 83 | ConvTurn: turn, 84 | IgnoreIfError: h.cfg.Options.IgnoreIfError, 85 | Err: err, 86 | }) 87 | case <-ctx.Done(): 88 | return ctx.Err() 89 | } 90 | } 91 | } 92 | 93 | func (h *Handler) handleMessage(ctx context.Context, m *Message) (*botastic.ConvTurn, error) { 94 | conv, err := h.store.GetConversationByKey(m.ConvKey) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | if conv == nil { 100 | conv, err = h.client.CreateConversation(ctx, botastic.CreateConversationRequest{ 101 | BotID: m.BotID, 102 | UserIdentity: m.UserIdentity, 103 | Lang: m.Lang, 104 | }) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | if err := h.store.SetConversation(m.ConvKey, conv); err != nil { 110 | return nil, err 111 | } 112 | } 113 | 114 | content := "" 115 | if m.ReplyContent != "" { 116 | content = fmt.Sprintf(`"%s" `, m.ReplyContent) 117 | } 118 | content += m.Content 119 | 120 | convTurn, err := h.client.PostToConversation(ctx, botastic.PostToConversationPayloadRequest{ 121 | ConversationID: conv.ID, 122 | Content: content, 123 | Category: "plain-text", 124 | }) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | turn, err := h.client.GetConvTurn(ctx, conv.ID, convTurn.ID, true) 130 | if err != nil { 131 | // TODO: retry 132 | return nil, err 133 | } 134 | if turn.Status != 2 { 135 | return nil, fmt.Errorf("unexpected status: %d", turn.Status) 136 | } 137 | 138 | if h.cfg.Options.FormatLinks && turn.Response != "" { 139 | turn.Response = formatLink(turn.Response) 140 | } 141 | 142 | return turn, nil 143 | } 144 | 145 | func formatLink(str string) string { 146 | isSpace := func(c byte) bool { 147 | return c == ' ' || c == '\t' || c == '\n' || c == '\r' 148 | } 149 | matches := linkRegex.FindAllStringSubmatchIndex(str, -1) 150 | var result strings.Builder 151 | lastIdx := 0 152 | for _, match := range matches { 153 | start, end := match[0], match[1] 154 | result.WriteString(str[lastIdx:start]) 155 | 156 | if start > 0 && !isSpace(str[start-1]) { 157 | result.WriteString(" ") 158 | } 159 | 160 | result.WriteString(str[start:end]) 161 | 162 | if end < len(str) && !isSpace(str[end]) { 163 | result.WriteString(" ") 164 | } 165 | 166 | lastIdx = end 167 | } 168 | 169 | result.WriteString(str[lastIdx:]) 170 | return result.String() 171 | } 172 | -------------------------------------------------------------------------------- /service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "testing" 4 | 5 | func TestFormatLink(t *testing.T) { 6 | cases := []struct { 7 | input string 8 | want string 9 | }{ 10 | { 11 | input: "Pando的Web应用程序是Pando Proto,可在https://app.pando.im上获得。这个Web应用程序的目标是为用户提供一个统一的界面,用于访问所有Pando协议和产品。目前,已经将4swap协议集成到Web应用程序中,并且将弃用4swap的旧Web应用程序(https://app.4swap.org)。将来,Leaf协议和Rings协议也将融合到新的Pando Web应用程序中。", 12 | want: "Pando的Web应用程序是Pando Proto,可在 https://app.pando.im 上获得。这个Web应用程序的目标是为用户提供一个统一的界面,用于访问所有Pando协议和产品。目前,已经将4swap协议集成到Web应用程序中,并且将弃用4swap的旧Web应用程序( https://app.4swap.org )。将来,Leaf协议和Rings协议也将融合到新的Pando Web应用程序中。", 13 | }, 14 | { 15 | input: "您可以通过Google Play下载和安装Mixin。如果要下载Apk,请通过浏览器打开https://mixin.one/mm或https://mixin-www.zeromesh.net/mm。对于iOS用户,请查看https://channel.mixinbots.com/dl。桌面版本请在浏览器中打开https://mixin.one/mm。移动设备至少支持Android 7.0+和iOS 13.0+。", 16 | want: "您可以通过Google Play下载和安装Mixin。如果要下载Apk,请通过浏览器打开 https://mixin.one/mm 或 https://mixin-www.zeromesh.net/mm 。对于iOS用户,请查看 https://channel.mixinbots.com/dl 。桌面版本请在浏览器中打开 https://mixin.one/mm 。移动设备至少支持Android 7.0+和iOS 13.0+。", 17 | }, 18 | { 19 | input: "Fennec的地址是 https://pando.im/wallet/ 。", 20 | want: "Fennec的地址是 https://pando.im/wallet/ 。", 21 | }, 22 | { 23 | input: "没有链接不应该改变", 24 | want: "没有链接不应该改变", 25 | }, 26 | } 27 | 28 | for _, c := range cases { 29 | t.Run(c.input, func(t *testing.T) { 30 | got := formatLink(c.input) 31 | if got != c.want { 32 | t.Errorf("FormatLink(%q) == %q, want %q", c.input, got, c.want) 33 | } 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/pandodao/botastic-go" 7 | ) 8 | 9 | type Store interface { 10 | GetConversationByKey(key string) (*botastic.Conversation, error) 11 | SetConversation(key string, conv *botastic.Conversation) error 12 | } 13 | 14 | type MemoryStore struct { 15 | convLock sync.Mutex 16 | convMap map[string]*botastic.Conversation 17 | } 18 | 19 | func NewMemoryStore() *MemoryStore { 20 | return &MemoryStore{ 21 | convMap: make(map[string]*botastic.Conversation), 22 | } 23 | } 24 | 25 | func (s *MemoryStore) GetConversationByKey(key string) (*botastic.Conversation, error) { 26 | s.convLock.Lock() 27 | defer s.convLock.Unlock() 28 | 29 | return s.convMap[key], nil 30 | } 31 | 32 | func (s *MemoryStore) SetConversation(key string, conv *botastic.Conversation) error { 33 | s.convLock.Lock() 34 | defer s.convLock.Unlock() 35 | 36 | s.convMap[key] = conv 37 | return nil 38 | } 39 | --------------------------------------------------------------------------------