├── internal ├── utils │ ├── utils.go │ └── utils_test.go ├── global │ ├── buildinfo.go │ └── config.go └── config │ └── config.go ├── cmd └── cli │ ├── action │ ├── action.go │ └── pulsoid.go │ └── main.go ├── .gitignore ├── README.md ├── config_example.toml ├── go.mod ├── .github └── workflows │ └── release.yaml ├── pkg └── pulsoid │ └── pulsoid.go ├── LICENSE ├── Makefile └── go.sum /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /internal/global/buildinfo.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | var ( 4 | AppVersion = "development" 5 | ) 6 | -------------------------------------------------------------------------------- /cmd/cli/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func Action(c *cli.Context) error { 8 | NewPulsoid().Start() 9 | return nil 10 | } 11 | -------------------------------------------------------------------------------- /internal/global/config.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "github.com/gizmo-ds/pulsoid-vrchat-osc/internal/config" 4 | 5 | var Config *config.Config 6 | 7 | func LoadConfig(filename string) error { 8 | conf, err := config.LoadFormFile(filename) 9 | if err != nil { 10 | return err 11 | } 12 | Config = conf 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /.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 | build 18 | .idea 19 | .vscode 20 | 21 | /config.toml 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pulsoid-vrchat-osc 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gizmo-ds/pulsoid-vrchat-osc?style=flat-square) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/gizmo-ds/pulsoid-vrchat-osc/release.yaml?branch=main&label=CI&style=flat-square)](https://github.com/gizmo-ds/pulsoid-vrchat-osc/actions/workflows/release.yaml) 5 | [![Release](https://img.shields.io/github/v/release/gizmo-ds/pulsoid-vrchat-osc.svg?include_prereleases&style=flat-square)](https://github.com/gizmo-ds/pulsoid-vrchat-osc/releases/latest) 6 | [![License](https://img.shields.io/github/license/gizmo-ds/pulsoid-vrchat-osc?style=flat-square)](./LICENSE) 7 | -------------------------------------------------------------------------------- /config_example.toml: -------------------------------------------------------------------------------- 1 | # Please rename this file to `config.toml` before use 2 | 3 | # [required] Pulsoid Widget ID 4 | widget_id = "" 5 | 6 | # [option] osc listen address, if empty then disable osc server 7 | address = "127.0.0.1:10001" 8 | # [option] only send heart rate to these avatars, if empty then send to all avatars 9 | enable_avatars = [ 10 | "", 11 | ] 12 | 13 | # osc parameters, if empty then not send message 14 | # [option] range: heart rage 0~254 map to -1.0f~1.0f 15 | float_parameter_name = "OSC_HeartRate_Float" 16 | # [option] range: heart range 0~255 17 | int_parameter_name = "OSC_HeartRate_Int" 18 | 19 | # [required] VRChat OSC port 20 | vrchat.port = 9000 21 | 22 | # trace, debug, info, warn, error, fatal, panic, disabled 23 | logger.level = "info" 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gizmo-ds/pulsoid-vrchat-osc 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/go-resty/resty/v2 v2.12.0 8 | github.com/gorilla/websocket v1.5.1 9 | github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 10 | github.com/rs/xid v1.5.0 11 | github.com/rs/zerolog v1.32.0 12 | github.com/urfave/cli/v2 v2.27.1 13 | ) 14 | 15 | require ( 16 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/pkg/errors v0.9.1 // indirect 20 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 21 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 22 | golang.org/x/net v0.24.0 // indirect 23 | golang.org/x/sys v0.19.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: [v*] 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | release: 13 | name: Release new version 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version: 'stable' 21 | cache: true 22 | 23 | - name: Test 24 | run: go test ./... 25 | 26 | - name: Build 27 | if: startsWith(github.ref, 'refs/tags/') 28 | run: make clean && make && make sha256sum 29 | 30 | - name: Release 31 | uses: softprops/action-gh-release@v1 32 | if: startsWith(github.ref, 'refs/tags/') 33 | with: 34 | files: | 35 | build/*.zip 36 | build/*.zip.sha256 37 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | type Config struct { 10 | Address string `toml:"address"` 11 | WidgetID string `toml:"widget_id"` 12 | EnableAvatars []string `toml:"enable_avatars"` 13 | FloatParameterName string `toml:"float_parameter_name"` 14 | IntParameterName string `toml:"int_parameter_name"` 15 | VRChat struct { 16 | Port int `toml:"port"` 17 | } `toml:"vrchat"` 18 | Logger struct { 19 | Level any `toml:"level"` 20 | } `toml:"logger"` 21 | } 22 | 23 | func LoadFormFile(path string) (*Config, error) { 24 | file, err := os.Open(path) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer file.Close() 29 | 30 | var config Config 31 | if _, err = toml.NewDecoder(file).Decode(&config); err != nil { 32 | return nil, err 33 | } 34 | return &config, nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/pulsoid/pulsoid.go: -------------------------------------------------------------------------------- 1 | package pulsoid 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/go-resty/resty/v2" 7 | "github.com/rs/xid" 8 | ) 9 | 10 | type WebSocketResult struct { 11 | Timestamp int64 `json:"timestamp"` 12 | Data struct { 13 | HeartRate int `json:"heartRate"` 14 | } `json:"data"` 15 | } 16 | 17 | func GetRamielUrl(widgetId string) (string, error) { 18 | var result struct { 19 | Result struct { 20 | RamielUrl string `json:"ramielUrl"` 21 | } `json:"result"` 22 | Error struct { 23 | Message string `json:"message"` 24 | } `json:"error"` 25 | } 26 | _, err := resty.New().R(). 27 | SetResult(&result). 28 | SetBody(map[string]any{ 29 | "method": "getWidget", 30 | "jsonrpc": "2.0", 31 | "params": map[string]any{"widgetId": widgetId}, 32 | "id": xid.New().String(), 33 | }). 34 | Post("https://pulsoid.net/v1/api/public/rpc") 35 | if err != nil { 36 | return "", err 37 | } 38 | if result.Error.Message != "" { 39 | return "", fmt.Errorf(result.Error.Message) 40 | } 41 | return result.Result.RamielUrl, err 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gizmo 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=pulsoid-vrchat-osc 2 | MAIN=cmd/cli/main.go 3 | PKGNAME=github.com/gizmo-ds/pulsoid-vrchat-osc 4 | OUTDIR=build 5 | VERSION=$(shell git describe --tags --always --dirty) 6 | FLAGS+=-trimpath 7 | FLAGS+=-tags timetzdata 8 | FLAGS+=-ldflags "-s -w -X $(PKGNAME)/internal/global.AppVersion=$(VERSION)" 9 | export CGO_ENABLED=0 10 | 11 | PLATFORMS := windows 12 | 13 | all: build-all compress zip sha256sum 14 | 15 | initialize: 16 | @mkdir -p $(OUTDIR) 17 | @cp config_example.toml $(OUTDIR)/config.toml 18 | @cp README*.md LICENSE $(OUTDIR) 19 | 20 | build-all: $(PLATFORMS) 21 | 22 | generate: 23 | go generate ./... 24 | 25 | $(PLATFORMS): generate 26 | GOOS=$@ GOARCH=amd64 go build $(FLAGS) -o $(OUTDIR)/$(NAME)-$@$(if $(filter windows,$@),.exe) $(MAIN) 27 | 28 | sha256sum: zip 29 | @cd $(OUTDIR); sha256sum *.zip > sha256.txt 30 | 31 | zip: 32 | @cp config_example.toml $(OUTDIR)/config.toml 33 | for platform in $(PLATFORMS); do \ 34 | zip -jq9 $(OUTDIR)/$(NAME)-$$platform.zip $(OUTDIR)/$(NAME)-$$platform* $(OUTDIR)/config.toml README*.md LICENSE; \ 35 | done 36 | 37 | compress: 38 | @if [ -n "$(shell command -v upx 2> /dev/null)" ]; then for file in build/*.exe; do upx $$file; done; fi 39 | 40 | clean: 41 | @rm -rf $(OUTDIR)/* 42 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/gizmo-ds/pulsoid-vrchat-osc/cmd/cli/action" 10 | "github.com/gizmo-ds/pulsoid-vrchat-osc/internal/global" 11 | "github.com/rs/zerolog" 12 | "github.com/rs/zerolog/log" 13 | "github.com/rs/zerolog/pkgerrors" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | func init() { 18 | zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack 19 | } 20 | 21 | func main() { 22 | _ = (&cli.App{ 23 | Name: "pulsoid-vrchat-osc", 24 | Version: global.AppVersion, 25 | Suggest: true, 26 | Flags: []cli.Flag{ 27 | &cli.BoolFlag{ 28 | Name: "no-color", 29 | Usage: "Disable color output", 30 | }, 31 | &cli.StringFlag{ 32 | Name: "log-level", 33 | Usage: fmt.Sprintf("Set the log level\n(%s)", 34 | strings.Join([]string{ 35 | zerolog.LevelDebugValue, zerolog.LevelInfoValue, zerolog.LevelWarnValue, 36 | zerolog.LevelErrorValue, zerolog.LevelFatalValue, zerolog.LevelPanicValue, 37 | }, ", "), 38 | ), 39 | Value: zerolog.LevelInfoValue, 40 | }, 41 | &cli.StringFlag{ 42 | Name: "config", 43 | Aliases: []string{"c"}, 44 | Usage: "Path to config file", 45 | Value: "config.toml", 46 | }, 47 | }, 48 | Before: func(c *cli.Context) error { 49 | err := global.LoadConfig(c.String("config")) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | logLevelStr := c.String("log-level") 55 | if !c.IsSet("log-level") && global.Config.Logger.Level != nil { 56 | // TODO: 兼容`v0.1.3`及之前的配置文件 57 | switch reflect.TypeOf(global.Config.Logger.Level).Kind() { 58 | case reflect.String: 59 | logLevelStr = global.Config.Logger.Level.(string) 60 | case reflect.Int64: 61 | if s := zerolog.Level(global.Config.Logger.Level.(int64)).String(); s != "" { 62 | logLevelStr = s 63 | } 64 | default: 65 | log.Fatal().Msg("Invalid log level") 66 | } 67 | } 68 | logLevel, err := zerolog.ParseLevel(logLevelStr) 69 | if err != nil { 70 | return err 71 | } 72 | zerolog.SetGlobalLevel(logLevel) 73 | 74 | log.Logger = zerolog.New(zerolog.ConsoleWriter{ 75 | Out: os.Stderr, 76 | NoColor: c.Bool("no-color"), 77 | }).With().Timestamp().Logger() 78 | return nil 79 | }, 80 | ExitErrHandler: func(c *cli.Context, err error) { 81 | if err != nil { 82 | log.Fatal().Err(err).Msg("Failed to run command") 83 | } 84 | }, 85 | Action: action.Action, 86 | }).Run(os.Args) 87 | } 88 | -------------------------------------------------------------------------------- /cmd/cli/action/pulsoid.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gizmo-ds/pulsoid-vrchat-osc/internal/global" 9 | "github.com/gizmo-ds/pulsoid-vrchat-osc/pkg/pulsoid" 10 | "github.com/gorilla/websocket" 11 | "github.com/hypebeast/go-osc/osc" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | type Pulsoid struct { 16 | RamielUrl string 17 | WidgetID string 18 | client *osc.Client 19 | enabled bool 20 | } 21 | 22 | func NewPulsoid() *Pulsoid { 23 | p := &Pulsoid{ 24 | WidgetID: global.Config.WidgetID, 25 | client: osc.NewClient("127.0.0.1", global.Config.VRChat.Port), 26 | enabled: true, 27 | } 28 | return p 29 | } 30 | 31 | func (p *Pulsoid) startOscServer() { 32 | if global.Config.Address == "" || len(global.Config.EnableAvatars) == 0 { 33 | log.Info().Msg("OSC server disabled") 34 | return 35 | } 36 | d := osc.NewStandardDispatcher() 37 | _ = d.AddMsgHandler("*", func(msg *osc.Message) { 38 | if msg.Address == "/avatar/change" { 39 | if id, ok := msg.Arguments[0].(string); ok { 40 | log.Debug().Str("AvatarID", id).Msg("Avatar changed") 41 | enabled := false 42 | for _, eid := range global.Config.EnableAvatars { 43 | if eid == id || (eid == "local" && strings.HasPrefix(id, "local:")) { 44 | enabled = true 45 | break 46 | } 47 | } 48 | log.Info(). 49 | Bool("Enabled", enabled). 50 | Msg("OSC enabled") 51 | p.enabled = enabled 52 | } 53 | } 54 | }) 55 | server := &osc.Server{ 56 | Addr: global.Config.Address, 57 | Dispatcher: d, 58 | } 59 | if err := server.ListenAndServe(); err != nil { 60 | log.Fatal().Err(err).Msg("Failed to start OSC server") 61 | } 62 | } 63 | 64 | func (p *Pulsoid) Start() { 65 | go p.startOscServer() 66 | 67 | p.GetRamielUrl() 68 | conn, _, err := websocket.DefaultDialer.Dial(p.RamielUrl, nil) 69 | if err != nil { 70 | log.Fatal().Err(err).Caller().Msg("Could not connect to pulsoid") 71 | } 72 | defer conn.Close() 73 | 74 | log.Info().Msg("Pulsoid connected") 75 | 76 | for { 77 | var result pulsoid.WebSocketResult 78 | err = conn.ReadJSON(&result) 79 | if err != nil { 80 | var e *websocket.CloseError 81 | if errors.As(err, &e) { 82 | log.Error().Err(e).Msg("Pulsoid disconnected") 83 | return 84 | } 85 | log.Error().Err(err).Msg("Could not read from pulsoid") 86 | continue 87 | } 88 | if p.enabled { 89 | if global.Config.FloatParameterName == "" && global.Config.IntParameterName == "" { 90 | log.Warn().Int("HeartRate", result.Data.HeartRate).Msg("HeartRate OSC parameters config is empty") 91 | continue 92 | } 93 | 94 | bundle := osc.NewBundle(time.Now()) 95 | 96 | if global.Config.FloatParameterName != "" { 97 | address := "/avatar/parameters/" + global.Config.FloatParameterName 98 | value := float32(clampIntRange(result.Data.HeartRate, 0, 254))/127 - 1 99 | 100 | floatMsg := osc.NewMessage(address) 101 | floatMsg.Append(value) 102 | bundle.Append(floatMsg) 103 | 104 | log.Info().Float32(address, value).Msg("HeartRate OSC packet added") 105 | } 106 | 107 | if global.Config.IntParameterName != "" { 108 | address := "/avatar/parameters/" + global.Config.IntParameterName 109 | value := int32(clampIntRange(result.Data.HeartRate, 0, 255)) 110 | 111 | intMsg := osc.NewMessage(address) 112 | intMsg.Append(value) 113 | bundle.Append(intMsg) 114 | log.Info().Int32(address, value).Msg("HeartRate OSC packet added") 115 | } 116 | 117 | if err = p.client.Send(bundle); err != nil { 118 | log.Error().Err(err).Msg("Could not send OSC message") 119 | continue 120 | } 121 | log.Info().Int("HeartRate", result.Data.HeartRate).Msg("HeartRate OSC sent") 122 | } 123 | } 124 | } 125 | 126 | func (p *Pulsoid) GetRamielUrl() { 127 | u, err := pulsoid.GetRamielUrl(p.WidgetID) 128 | if err != nil { 129 | log.Fatal().Err(err).Caller().Msg("GetRamielUrl") 130 | } else { 131 | p.RamielUrl = u 132 | } 133 | } 134 | 135 | func clampIntRange(number int, min int, max int) int { 136 | if number < min { 137 | number = min 138 | } 139 | if number > max { 140 | number = max 141 | } 142 | return number 143 | } 144 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= 9 | github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= 10 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 11 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 12 | github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 13 | github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5 h1:fqwINudmUrvGCuw+e3tedZ2UJ0hklSw6t8UPomctKyQ= 14 | github.com/hypebeast/go-osc v0.0.0-20220308234300-cec5a8a1e5f5/go.mod h1:lqMjoCs0y0GoRRujSPZRBaGb4c5ER6TfkFKSClxkMbY= 15 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 16 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 17 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 18 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 19 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 20 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 21 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 22 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 23 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= 25 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 26 | github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= 27 | github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 28 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 29 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 30 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 31 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 32 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 33 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 34 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= 35 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= 36 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 37 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 38 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 39 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 40 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 41 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 42 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 43 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 44 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 45 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 46 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 47 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 48 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 49 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 50 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 51 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 52 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 53 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 57 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 67 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 68 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 69 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 70 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 71 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 72 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 73 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 74 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 75 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 76 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 79 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 80 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 81 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 82 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 83 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 84 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 85 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 86 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 87 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 88 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 89 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | --------------------------------------------------------------------------------