Skip to content

Commit 0cc3d7e

Browse files
authored
Merge pull request #25 from xdevplatform/xurl-webhooks
xurl webhooks
2 parents ca87c93 + f5c7c2c commit 0cc3d7e

File tree

5 files changed

+263
-3
lines changed

5 files changed

+263
-3
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ You can also force streaming mode for any endpoint using the `--stream` or `-s`
113113
xurl -s /2/users/me
114114
```
115115

116+
### Temporary Webhook Setup
117+
118+
`xurl` can help you quickly set up a temporary webhook URL to receive events from the X API. This is useful for development and testing.
119+
120+
1. **Start the local webhook server with ngrok:**
121+
122+
Run the `webhook start` command. This will start a local server and use ngrok to create a public URL that forwards to your local server. You will be prompted for your ngrok authtoken if it's not already configured via the `NGROK_AUTHTOKEN` environment variable.
123+
124+
```bash
125+
xurl webhook start
126+
# Or with a specific port and output file for POST bodies
127+
xurl webhook start -p 8081 -o webhook_events.log
128+
```
129+
130+
The command will output an ngrok URL (e.g., `https://your-unique-id.ngrok-free.app/webhook`). Note this URL.
131+
132+
2. **Register the webhook with the X API:**
133+
134+
Use the ngrok URL obtained in the previous step to register your webhook. You'll typically use app authentication for this.
135+
136+
```bash
137+
# Replace https://your-ngrok-url.ngrok-free.app/webhook with the actual URL from the previous step
138+
xurl --auth app /2/webhooks -d '{"url": "<your ngrok url>"}' -X POST
139+
```
140+
141+
Your local `xurl webhook start` server will then handle the CRC handshake from Twitter and log incoming POST events (and write them to a file if `-o` was used).
142+
116143
### Media Upload
117144
118145
The tool supports uploading media files to the X API using the chunked upload process.

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ Examples:
8888
rootCmd.AddCommand(CreateAuthCommand(auth))
8989
rootCmd.AddCommand(CreateMediaCommand(auth))
9090
rootCmd.AddCommand(CreateVersionCommand())
91+
rootCmd.AddCommand(CreateWebhookCommand(auth))
9192

9293
return rootCmd
9394
}

cli/webhook.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package cli
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"crypto/hmac"
7+
"crypto/sha256"
8+
"encoding/base64"
9+
"encoding/json"
10+
"fmt"
11+
"io"
12+
"log"
13+
"net/http"
14+
"os"
15+
"strings"
16+
17+
"github.com/fatih/color"
18+
"github.com/spf13/cobra"
19+
"github.com/tidwall/pretty"
20+
"golang.ngrok.com/ngrok"
21+
"golang.ngrok.com/ngrok/config"
22+
"xurl/auth"
23+
)
24+
25+
var webhookPort int
26+
var outputFileName string // To store the output file name from the flag
27+
var quietMode bool // To store the quiet flag state
28+
var prettyMode bool // To store the pretty-print flag state
29+
30+
// CreateWebhookCommand creates the webhook command and its subcommands.
31+
func CreateWebhookCommand(authInstance *auth.Auth) *cobra.Command {
32+
webhookCmd := &cobra.Command{
33+
Use: "webhook",
34+
Short: "Manage webhooks for the X API",
35+
Long: `Manages X API webhooks. Currently supports starting a local server with an ngrok tunnel to handle CRC checks.`,
36+
}
37+
38+
webhookStartCmd := &cobra.Command{
39+
Use: "start",
40+
Short: "Start a local webhook server with an ngrok tunnel",
41+
Long: `Starts a local HTTP server and an ngrok tunnel to listen for X API webhook events, including CRC checks. POST request bodies can be saved to a file using the -o flag. Use -q for quieter console logging of POST events. Use -p to pretty-print JSON POST bodies in the console.`,
42+
Run: func(cmd *cobra.Command, args []string) {
43+
color.Cyan("Starting webhook server with ngrok...")
44+
45+
if authInstance == nil || authInstance.TokenStore == nil {
46+
color.Red("Error: Authentication module not initialized properly.")
47+
os.Exit(1)
48+
}
49+
50+
oauth1Token := authInstance.TokenStore.GetOAuth1Tokens()
51+
if oauth1Token == nil || oauth1Token.OAuth1 == nil || oauth1Token.OAuth1.ConsumerSecret == "" {
52+
color.Red("Error: OAuth 1.0a consumer secret not found. Please configure OAuth 1.0a credentials using 'xurl auth oauth1'.")
53+
os.Exit(1)
54+
}
55+
consumerSecret := oauth1Token.OAuth1.ConsumerSecret
56+
57+
// Handle output file if -o flag is used
58+
var outputFile *os.File
59+
var errOpenFile error
60+
if outputFileName != "" {
61+
outputFile, errOpenFile = os.OpenFile(outputFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
62+
if errOpenFile != nil {
63+
color.Red("Error opening output file %s: %v", outputFileName, errOpenFile)
64+
os.Exit(1)
65+
}
66+
defer outputFile.Close()
67+
color.Green("Logging POST request bodies to: %s", outputFileName)
68+
}
69+
70+
// Prompt for ngrok authtoken
71+
color.Yellow("Enter your ngrok authtoken (leave empty to try NGROK_AUTHTOKEN env var): ")
72+
reader := bufio.NewReader(os.Stdin)
73+
ngrokAuthToken, _ := reader.ReadString('\n')
74+
ngrokAuthToken = strings.TrimSpace(ngrokAuthToken)
75+
76+
ctx := context.Background()
77+
var tunnelOpts []ngrok.ConnectOption
78+
if ngrokAuthToken != "" {
79+
tunnelOpts = append(tunnelOpts, ngrok.WithAuthtoken(ngrokAuthToken))
80+
} else {
81+
color.Cyan("Attempting to use NGROK_AUTHTOKEN environment variable for ngrok authentication.")
82+
tunnelOpts = append(tunnelOpts, ngrok.WithAuthtokenFromEnv()) // Fallback to env
83+
}
84+
85+
forwardToAddr := fmt.Sprintf("localhost:%d", webhookPort)
86+
color.Cyan("Configuring ngrok to forward to local port: %s", color.MagentaString("%d", webhookPort))
87+
88+
ngrokListener, err := ngrok.Listen(ctx,
89+
config.HTTPEndpoint(
90+
config.WithForwardsTo(forwardToAddr),
91+
),
92+
tunnelOpts...,
93+
)
94+
if err != nil {
95+
color.Red("Error starting ngrok tunnel: %v", err)
96+
os.Exit(1)
97+
}
98+
defer ngrokListener.Close()
99+
100+
color.Green("Ngrok tunnel established!")
101+
fmt.Printf(" Forwarding URL: %s -> %s\n", color.HiGreenString(ngrokListener.URL()), color.MagentaString(forwardToAddr))
102+
color.Yellow("Use this URL for your X API webhook registration: %s/webhook", color.HiGreenString(ngrokListener.URL()))
103+
104+
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
105+
if r.Method == http.MethodGet {
106+
crcToken := r.URL.Query().Get("crc_token")
107+
if crcToken == "" {
108+
http.Error(w, "Error: crc_token missing from request", http.StatusBadRequest)
109+
log.Printf("[WARN] Received GET /webhook without crc_token")
110+
return
111+
}
112+
log.Printf("[INFO] Received GET %s%s with crc_token: %s", color.BlueString(r.Host), color.BlueString(r.URL.Path), color.YellowString(crcToken))
113+
114+
mac := hmac.New(sha256.New, []byte(consumerSecret))
115+
mac.Write([]byte(crcToken))
116+
hashedToken := mac.Sum(nil)
117+
encodedToken := base64.StdEncoding.EncodeToString(hashedToken)
118+
119+
response := map[string]string{
120+
"response_token": "sha256=" + encodedToken,
121+
}
122+
w.Header().Set("Content-Type", "application/json")
123+
json.NewEncoder(w).Encode(response)
124+
log.Printf("[INFO] Responded to CRC check with token: %s", color.GreenString(response["response_token"]))
125+
126+
} else if r.Method == http.MethodPost {
127+
bodyBytes, err := io.ReadAll(r.Body)
128+
if err != nil {
129+
http.Error(w, "Error reading request body", http.StatusInternalServerError)
130+
log.Printf("[ERROR] Error reading POST body: %v", err)
131+
return
132+
}
133+
defer r.Body.Close()
134+
135+
if quietMode {
136+
log.Printf("[INFO] Received POST %s%s event (quiet mode).", color.BlueString(r.Host), color.BlueString(r.URL.Path))
137+
} else {
138+
log.Printf("[INFO] Received POST %s%s event:", color.BlueString(r.Host), color.BlueString(r.URL.Path))
139+
if prettyMode {
140+
// Attempt to pretty-print if it's JSON
141+
var jsonData interface{}
142+
if json.Unmarshal(bodyBytes, &jsonData) == nil {
143+
prettyColored := pretty.Color(pretty.Pretty(bodyBytes), pretty.TerminalStyle)
144+
log.Printf("[DATA] Body:\n%s", string(prettyColored))
145+
} else {
146+
// Not valid JSON or some other error, print as raw string
147+
log.Printf("[DATA] Body (raw, not valid JSON for pretty print):\n%s", string(bodyBytes))
148+
}
149+
} else {
150+
log.Printf("[DATA] Body: %s", string(bodyBytes))
151+
}
152+
}
153+
154+
// Write to output file if specified
155+
if outputFile != nil {
156+
if _, err := outputFile.Write(bodyBytes); err != nil {
157+
log.Printf("[ERROR] Error writing POST body to output file %s: %v", outputFileName, err)
158+
} else {
159+
// Add a separator for readability
160+
if _, err := outputFile.WriteString("\n--------------------\n"); err != nil {
161+
log.Printf("[ERROR] Error writing separator to output file %s: %v", outputFileName, err)
162+
}
163+
log.Printf("[INFO] POST body written to %s", color.GreenString(outputFileName))
164+
}
165+
}
166+
167+
w.WriteHeader(http.StatusOK)
168+
} else {
169+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
170+
}
171+
})
172+
173+
color.Cyan("Starting local HTTP server to handle requests from ngrok tunnel (forwarded from %s)...", color.HiGreenString(ngrokListener.URL()))
174+
if err := http.Serve(ngrokListener, nil); err != nil {
175+
if err != http.ErrServerClosed {
176+
color.Red("HTTP server error: %v", err)
177+
os.Exit(1)
178+
} else {
179+
color.Yellow("HTTP server closed gracefully.")
180+
}
181+
}
182+
color.Yellow("Webhook server and ngrok tunnel shut down.")
183+
},
184+
}
185+
186+
webhookStartCmd.Flags().IntVarP(&webhookPort, "port", "p", 8080, "Local port for the webhook server to listen on (ngrok will forward to this port)")
187+
webhookStartCmd.Flags().StringVarP(&outputFileName, "output", "o", "", "File to write incoming POST request bodies to")
188+
webhookStartCmd.Flags().BoolVarP(&quietMode, "quiet", "q", false, "Enable quiet mode (logs only that a POST event was received, not the full body to console)")
189+
webhookStartCmd.Flags().BoolVarP(&prettyMode, "pretty", "P", false, "Pretty-print JSON POST bodies in console output (ignored if -q is used)")
190+
191+
webhookCmd.AddCommand(webhookStartCmd)
192+
return webhookCmd
193+
}

go.mod

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,26 @@ require (
1212

1313
require (
1414
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/go-stack/stack v1.8.1 // indirect
1516
github.com/golang/protobuf v1.5.3 // indirect
17+
github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible // indirect
18+
github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect
1619
github.com/inconshreveable/mousetrap v1.1.0 // indirect
20+
github.com/jpillora/backoff v1.0.0 // indirect
1721
github.com/mattn/go-colorable v0.1.13 // indirect
1822
github.com/mattn/go-isatty v0.0.20 // indirect
1923
github.com/pmezard/go-difflib v1.0.0 // indirect
2024
github.com/spf13/pflag v1.0.5 // indirect
2125
github.com/stretchr/objx v0.5.2 // indirect
22-
golang.org/x/net v0.22.0 // indirect
23-
golang.org/x/sys v0.25.0 // indirect
26+
github.com/tidwall/pretty v1.2.1 // indirect
27+
go.uber.org/multierr v1.11.0 // indirect
28+
golang.ngrok.com/muxado/v2 v2.0.1 // indirect
29+
golang.ngrok.com/ngrok v1.13.0 // indirect
30+
golang.org/x/net v0.30.0 // indirect
31+
golang.org/x/sync v0.8.0 // indirect
32+
golang.org/x/sys v0.26.0 // indirect
33+
golang.org/x/term v0.25.0 // indirect
2434
google.golang.org/appengine v1.6.7 // indirect
25-
google.golang.org/protobuf v1.31.0 // indirect
35+
google.golang.org/protobuf v1.35.1 // indirect
36+
gopkg.in/yaml.v2 v2.4.0 // indirect
2637
)

go.sum

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,23 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
55
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
6+
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
7+
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
68
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
79
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
810
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
911
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
1012
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1113
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
1214
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
15+
github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible h1:VryeOTiaZfAzwx8xBcID1KlJCeoWSIpsNbSk+/D2LNk=
16+
github.com/inconshreveable/log15 v3.0.0-testing.5+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
17+
github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjHe47D3bbAB9BgMrc3DYA=
18+
github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94=
1319
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
1420
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
21+
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
22+
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
1523
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
1624
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
1725
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -28,17 +36,33 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
2836
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2937
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
3038
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
39+
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
40+
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
41+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
42+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
43+
golang.ngrok.com/muxado/v2 v2.0.1 h1:jM9i6Pom6GGmnPrHKNR6OJRrUoHFkSZlJ3/S0zqdVpY=
44+
golang.ngrok.com/muxado/v2 v2.0.1/go.mod h1:wzxJYX4xiAtmwumzL+QsukVwFRXmPNv86vB8RPpOxyM=
45+
golang.ngrok.com/ngrok v1.13.0 h1:6SeOS+DAeIaHlkDmNH5waFHv0xjlavOV3wml0Z59/8k=
46+
golang.ngrok.com/ngrok v1.13.0/go.mod h1:BKOMdoZXfD4w6o3EtE7Cu9TVbaUWBqptrZRWnVcAuI4=
3147
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
3248
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
3349
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
3450
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
51+
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
52+
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
3553
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
3654
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
55+
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
56+
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
3757
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3858
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
3959
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4060
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
4161
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
62+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
63+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
64+
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
65+
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
4266
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
4367
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
4468
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -49,7 +73,11 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
4973
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
5074
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
5175
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
76+
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
77+
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
5278
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
5379
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
80+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
81+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
5482
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5583
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)