commit 35a0b2b71556a92bf0f1f9ee3d9028ba1f5c1dcf Author: karti Date: Wed Apr 15 21:14:47 2026 -0700 feat: initial release with 48 Mattermost MCP tools - 27 base tools: channels, messaging, users, reactions, files, DMs - Team management: invite/remove users, get stats, list members - Slash commands: execute /remind, /poll, etc. - Webhook management: incoming and outgoing webhooks - System tools: server config, logs, bulk status updates - Channel admin: create, invite, leave, delete channels - Read-only mode for safe exploration - Dual token support (bot + PAT) for enhanced security - Apache 2.0 licensed diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cda2299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Binary +mattermost-mcp-server +mcp-server + +# Log files +*.log + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Build directories +dist/ +build/ + +# Internal tracking and QA (not for repo) +.sisyphus/ +.opencode/ +evidence/ +final-qa/ + +# Test reports (generated) +*test_report*.md +*test_results*.md +*token_test*.md + +# Backup files +*.backup +*.bak + +# Environment files +.env +.env.local diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7c84df --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,70 @@ +# Changelog + +All notable changes to the Mattermost MCP Server will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-04-12 + +### Added +- Initial release of Mattermost MCP Server +- Support for 22 MCP tools categorized as Read, Write, and Admin operations + +#### Read Operations (11) +- `mattermost_list_channels` - List accessible channels in a team +- `mattermost_get_channel_by_name` - Get a channel by name in a team +- `mattermost_get_channel_info` - Get detailed channel information including member count +- `mattermost_search_users` - Search for users by username, email, or display name +- `mattermost_list_teams` - List all teams the bot has access to +- `mattermost_search_posts` - Search for posts/messages in a team using search terms +- `mattermost_get_thread` - Read all messages in a thread conversation +- `mattermost_get_unread_count` - Get unread message counts for all channels in a team +- `mattermost_mark_channel_read` - Mark a channel as read (clear unread notifications) +- `mattermost_get_user_status` - Get the online status of a user (online, away, dnd, offline) +- `mattermost_get_channel_messages` - Read message history from a channel + +#### Write Operations (8) +- `mattermost_send_message` - Send a message to a channel or user +- `mattermost_edit_message` - Edit an existing message +- `mattermost_delete_message` - Delete a message from a channel +- `mattermost_create_dm` - Create a direct message channel with a user +- `mattermost_add_reaction` - Add an emoji reaction to a message +- `mattermost_remove_reaction` - Remove an emoji reaction from a message +- `mattermost_upload_file` - Upload a file to a channel or user +- `mattermost_download_file` - Download a file from a message + +#### Admin Operations (4) +- `mattermost_create_channel` - Create a new channel (public or private) in a team +- `mattermost_invite_to_channel` - Invite a user to a channel +- `mattermost_delete_channel` - Delete/archive a channel (soft delete by default) +- `mattermost_leave_channel` - Remove self from a channel + +### Features +- Multiple authentication methods (Bot tokens, PAT, User tokens) +- Read-only mode for safe exploration (`--read-only` flag) +- Debug logging support (`--debug` flag) +- Configuration via environment variables or CLI flags +- Comprehensive error handling with descriptive messages +- File upload security with validation and restrictions +- Integration tests for core functionality +- OpenClaw and Claude Code configuration support + +### Security +- Path traversal prevention for file operations +- Dangerous file extension blocking +- File size limits (50MB maximum) +- MIME type whitelist validation for uploads +- Token-safe logging (tokens are never logged) +- TLS verification enabled by default +- `--insecure` flag available only for development/testing + +## [Unreleased] + +### Planned +- WebSocket support for real-time notifications +- Additional search capabilities (file search, user directory) +- Message threading improvements +- Bulk operations support +- Rate limiting and throttling options +- Extended file type support for uploads diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..506262f --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has advised the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 Karti AI + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6a23470 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +# Mattermost MCP Server Makefile + +.PHONY: build test clean dev lint + +BINARY_NAME=mattermost-mcp-server +VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S') +LDFLAGS=-ldflags "-X main.Version=$(VERSION)" + +build: + go build $(LDFLAGS) -o $(BINARY_NAME) . + +test: + go test -v ./... + +clean: + rm -f $(BINARY_NAME) + go clean + +dev: + go run . -log-level=debug + +lint: + golangci-lint run + +mod: + go mod tidy + go mod verify + +.DEFAULT_GOAL := build diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4cd580 --- /dev/null +++ b/README.md @@ -0,0 +1,351 @@ +# Mattermost MCP Server + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) +[![Go Version](https://img.shields.io/badge/Go-1.23%2B-brightgreen.svg)](go.mod) + +A Model Context Protocol (MCP) server for Mattermost, enabling AI assistants to interact with your Mattermost workspace. This server provides tools for sending messages, managing channels, searching users, and handling file uploads/downloads. + +## Features + +- **27 Total Tools**: Comprehensive coverage of Mattermost operations +- **Read Operations**: Teams, channels, users, messages, reactions, files +- **Write Operations**: Send/edit/delete messages, reactions, channel management, file operations +- **Read-Only Mode**: Safe mode with only read operations enabled +- **Dual Token Support**: Bot token for reads, PAT for writes (enhanced security) +- **Debug Logging**: Optional verbose logging for troubleshooting + +## Installation + +### Method 1: Using Go Install + +```bash +go install github.com/karti-ai/mattermost-mcp-server@latest +``` + +### Method 2: Binary Download + +Download the latest binary from the [releases page](https://github.com/karti-ai/mattermost-mcp-server/releases): + +```bash +curl -L -o mattermost-mcp-server https://github.com/karti-ai/mattermost-mcp-server/releases/latest/download/mattermost-mcp-server-linux-amd64 +chmod +x mattermost-mcp-server +``` + +### Method 3: Build from Source + +```bash +git clone https://github.com/karti-ai/mattermost-mcp-server.git +cd mattermost-mcp-server +go build -ldflags "-s -w" -o mattermost-mcp-server . +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `MATTERMOST_HOST` | Mattermost server URL (e.g., `https://mattermost.example.com`) | Yes | +| `MATTERMOST_TOKEN` | Access token (bot, PAT, or user) | Yes* | +| `MATTERMOST_BOT_TOKEN` | Bot token (alternative to TOKEN) | Yes* | +| `MATTERMOST_PAT` | Personal Access Token (alternative to TOKEN) | Yes* | +| `MATTERMOST_TEAM` | Default team name | No | + +*At least one token type is required. + +### CLI Flags + +| Flag | Short | Description | +|------|-------|-------------| +| `-host` | `-H` | Mattermost server URL | +| `-token` | `-t` | Mattermost access token | +| `-bot-token` | | Mattermost bot token | +| `-pat` | | Mattermost personal access token | +| `-team` | | Mattermost team name | +| `-debug` | `-d` | Enable debug logging | +| `-insecure` | | Allow insecure TLS connections | +| `-read-only` | `-r` | Enable read-only mode | +| `-version` | `-v` | Show version and exit | + +### Example .env File + +```bash +# Mattermost Configuration +MATTERMOST_HOST=https://mattermost.company.com +MATTERMOST_TOKEN=your-bot-token-here +MATTERMOST_TEAM=general + +# Optional: Enable debug logging +# DEBUG=true +``` + +## Available Tools + +The server exposes **27 tools** grouped by category: + +### Team Operations (1) +| Tool | Description | +|------|-------------| +| `mattermost_list_teams` | List all teams the bot has access to | + +### Channel Operations (5) +| Tool | Description | +|------|-------------| +| `mattermost_list_channels` | List all accessible channels in a team | +| `mattermost_get_channel_by_name` | Get a channel by name in a team | +| `mattermost_get_channel_info` | Get detailed channel info with member count | +| `mattermost_list_channel_members` | List all members of a channel | +| `mattermost_mark_channel_read` | Mark a channel as read (clear unread) | + +### Channel Admin (4) +| Tool | Description | +|------|-------------| +| `mattermost_create_channel` | Create a new public or private channel | +| `mattermost_invite_to_channel` | Invite a user to a channel | +| `mattermost_leave_channel` | Leave a channel | +| `mattermost_delete_channel` | Delete/archive a channel | + +### Messaging (6) +| Tool | Description | +|------|-------------| +| `mattermost_send_message` | Send a message (supports thread replies) | +| `mattermost_edit_message` | Edit an existing message | +| `mattermost_delete_message` | Delete a message | +| `mattermost_get_channel_messages` | Read message history with pagination | +| `mattermost_get_thread` | Read all messages in a thread | +| `mattermost_search_posts` | Search messages in a team | + +### User Operations (4) +| Tool | Description | +|------|-------------| +| `mattermost_search_users` | Search users by username, email, or name | +| `mattermost_get_user` | Get a specific user by ID or username | +| `mattermost_get_user_status` | Get online status (online/away/dnd/offline) | +| `mattermost_update_user_status` | Set your status (requires PAT) | + +### Direct Messages (2) +| Tool | Description | +|------|-------------| +| `mattermost_create_dm` | Create a 1:1 direct message channel | +| `mattermost_create_group_dm` | Create a group DM with multiple users | + +### Reactions (3) +| Tool | Description | +|------|-------------| +| `mattermost_add_reaction` | Add an emoji reaction to a message | +| `mattermost_remove_reaction` | Remove a reaction from a message | +| `mattermost_list_reactions` | List all reactions on a message | + +### File Operations (2) +| Tool | Description | +|------|-------------| +| `mattermost_upload_file` | Upload a file to a channel | +| `mattermost_download_file` | Download a file from a message | + +### Unread/Notifications (1) +| Tool | Description | +|------|-------------| +| `mattermost_get_unread_count` | Get unread message counts per channel | + +### Read-Only Mode + +When running with `--read-only` or `-r` flag, only the 14 read tools are available, preventing any modifications to your Mattermost workspace. + +## OpenClaw Setup + +Add the following to your `~/.config/openclaw/openclaw.json`: + +```json +{ + "mcpServers": { + "mattermost": { + "command": "mattermost-mcp-server", + "env": { + "MATTERMOST_HOST": "https://mattermost.example.com", + "MATTERMOST_TOKEN": "your-token-here", + "MATTERMOST_TEAM": "general" + } + } + } +} +``` + +With read-only mode: + +```json +{ + "mcpServers": { + "mattermost": { + "command": "mattermost-mcp-server", + "args": ["--read-only"], + "env": { + "MATTERMOST_HOST": "https://mattermost.example.com", + "MATTERMOST_TOKEN": "your-token-here" + } + } + } +} +``` + +## Claude Code Setup + +### Method 1: Using .mcp.json + +Create `.mcp.json` in your project root: + +```json +{ + "mcpServers": { + "mattermost": { + "command": "mattermost-mcp-server", + "env": { + "MATTERMOST_HOST": "https://mattermost.example.com", + "MATTERMOST_TOKEN": "your-token-here", + "MATTERMOST_TEAM": "general" + } + } + } +} +``` + +### Method 2: CLI Setup + +```bash +# Add to Claude Code configuration +claude config add mcpServer mattermost mattermost-mcp-server + +# Set environment variables +claude config set mattermost.env.MATTERMOST_HOST https://mattermost.example.com +claude config set mattermost.env.MATTERMOST_TOKEN your-token-here +``` + +### Method 3: Direct Command + +```bash +claude --mcp mattermost mattermost-mcp-server -- -H https://mattermost.example.com -t your-token-here +``` + +## Security Considerations + +### Token Storage + +- **Never commit tokens to version control** - Always use environment variables +- **Use bot tokens when possible** - Bot tokens have limited permissions and are safer than user tokens +- **Rotate tokens regularly** - Change tokens periodically and after any suspected compromise +- **Use secrets management** - For production, use tools like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets + +### Read-Only Mode + +- **Enable for safe exploration** - Use `--read-only` flag when first testing or giving AI assistants limited access +- **Audit before write access** - Review all write tool descriptions before disabling read-only mode +- **Principle of least privilege** - Only enable write access when absolutely necessary + +### File Upload Restrictions + +- The server allows file uploads to Mattermost channels +- Configure Mattermost server-side file upload limits and restrictions +- Be aware that AI assistants can upload arbitrary files if given write access +- Consider read-only mode if file upload functionality is not required + +### TLS/SSL + +- **Always use HTTPS** in production +- The `--insecure` flag disables TLS certificate verification - **only use for development/testing** +- Ensure your Mattermost server has valid TLS certificates + +### Network Security + +- Run the MCP server on localhost or behind a firewall +- Do not expose the MCP server to untrusted networks +- Use VPN or SSH tunnels when accessing remote Mattermost servers + +## Usage Examples + +### Listing Channels + +``` +Please list all channels in the general team +``` + +### Sending a Message + +``` +Send a message to the "announcements" channel saying "Meeting starts in 5 minutes" +``` + +### Searching Users + +``` +Find users with "john" in their username +``` + +### Managing Reactions + +``` +Add a :thumbsup: reaction to the last message in the general channel +``` + +### File Operations + +``` +Upload the report.pdf file to the marketing channel +``` + +## Troubleshooting + +### Debug Mode + +Enable debug logging to see detailed request/response information: + +```bash +mattermost-mcp-server -d +``` + +### Common Issues + +1. **Connection refused**: Check that `MATTERMOST_HOST` includes the full URL with protocol (http/https) +2. **401 Unauthorized**: Verify your token is valid and has not expired +3. **403 Forbidden**: Ensure the token has appropriate permissions for the operations you're trying to perform +4. **Team not found**: Verify the team name exactly matches the URL slug in Mattermost + +## Development + +### Building + +```bash +go build -ldflags "-s -w" -o mattermost-mcp-server . +``` + +### Testing + +```bash +go test ./... +``` + +### Smoke Tests + +```bash +# Build and verify +make build + +# Run tests +./mattermost-mcp-server --version +./mattermost-mcp-server --help +``` + +## License + +Apache License 2.0 - See [LICENSE](LICENSE) for details. + +## Contributing + +Contributions are welcome! Feel free to open issues and pull requests. + +## Support + +- **Issues**: [GitHub Issues](https://github.com/karti-ai/mattermost-mcp-server/issues) +- **Discussions**: [GitHub Discussions](https://github.com/karti-ai/mattermost-mcp-server/discussions) + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for version history and changes. diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..f69e263 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/karti-ai/mattermost-mcp-server/operation" + pkgflag "github.com/karti-ai/mattermost-mcp-server/pkg/flag" + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/server" +) + +func Execute() { + // Parse CLI flags + flag.StringVar(&pkgflag.Token, "token", os.Getenv("MATTERMOST_TOKEN"), "Mattermost access token") + flag.StringVar(&pkgflag.Token, "t", os.Getenv("MATTERMOST_TOKEN"), "Mattermost access token (short)") + flag.StringVar(&pkgflag.BotToken, "bot-token", os.Getenv("MATTERMOST_BOT_TOKEN"), "Mattermost bot token") + flag.StringVar(&pkgflag.PAT, "pat", os.Getenv("MATTERMOST_PAT"), "Mattermost personal access token") + flag.StringVar(&pkgflag.Host, "host", os.Getenv("MATTERMOST_HOST"), "Mattermost server URL") + flag.StringVar(&pkgflag.Host, "H", os.Getenv("MATTERMOST_HOST"), "Mattermost server URL (short)") + flag.StringVar(&pkgflag.Team, "team", os.Getenv("MATTERMOST_TEAM"), "Mattermost team (optional)") + flag.BoolVar(&pkgflag.ReadOnly, "read-only", os.Getenv("MATTERMOST_READONLY") == "true", "Enable read-only mode") + flag.BoolVar(&pkgflag.ReadOnly, "r", os.Getenv("MATTERMOST_READONLY") == "true", "Enable read-only mode (short)") + flag.BoolVar(&pkgflag.Debug, "debug", os.Getenv("MATTERMOST_DEBUG") == "true", "Enable debug logging") + flag.BoolVar(&pkgflag.Debug, "d", os.Getenv("MATTERMOST_DEBUG") == "true", "Enable debug logging (short)") + flag.BoolVar(&pkgflag.Insecure, "insecure", os.Getenv("MATTERMOST_INSECURE") == "true", "Allow insecure TLS connections") + flag.BoolVar(&pkgflag.ShowVersion, "version", false, "Show version and exit") + flag.BoolVar(&pkgflag.ShowVersion, "v", false, "Show version (short)") + + flag.Parse() + + if pkgflag.ShowVersion { + fmt.Printf("mattermost-mcp-server %s\n", pkgflag.Version) + os.Exit(0) + } + + // Validate at least one token is provided + if pkgflag.Token == "" && pkgflag.BotToken == "" && pkgflag.PAT == "" { + fmt.Fprintln(os.Stderr, "Error: At least one token is required (via --token, --bot-token, --pat, or environment variables)") + flag.Usage() + os.Exit(1) + } + + if pkgflag.Host == "" { + fmt.Fprintln(os.Stderr, "Error: Mattermost host is required (via --host or MATTERMOST_HOST environment variable)") + flag.Usage() + os.Exit(1) + } + + // Initialize logging + logLevel := "info" + if pkgflag.Debug { + logLevel = "debug" + } + if err := log.Initialize(logLevel); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize logging: %v\n", err) + os.Exit(1) + } + defer log.Sync() + + log.Infof("Starting mattermost-mcp-server %s", pkgflag.Version) + log.Infof("Host: %s", pkgflag.Host) + log.Infof("Read-only mode: %v", pkgflag.ReadOnly) + + client := mattermost.NewClient(pkgflag.Host, pkgflag.BotToken, pkgflag.PAT) + mattermost.SetGlobalClient(client) + log.Infof("Initialized Mattermost client for host: %s", pkgflag.Host) + + // Create MCP server + mcpServer := server.NewMCPServer( + "mattermost-mcp", + pkgflag.Version, + ) + + // Register tools + tools := operation.Register() + for _, tool := range tools { + mcpServer.AddTool(tool.Tool, tool.Handler) + } + + log.Infof("Registered %d tools", len(tools)) + + // Set up graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigChan + log.Info("Received shutdown signal, shutting down gracefully...") + }() + + // Start stdio server + log.Info("Starting stdio server...") + if err := server.ServeStdio(mcpServer); err != nil { + log.Errorf("Server error: %v", err) + os.Exit(1) + } + + log.Info("Server stopped") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cd0b866 --- /dev/null +++ b/go.mod @@ -0,0 +1,64 @@ +module github.com/karti-ai/mattermost-mcp-server + +go 1.23.0 + +require ( + github.com/mark3labs/mcp-go v0.44.0 + github.com/mattermost/mattermost-server/v6 v6.7.2 + github.com/stretchr/testify v1.10.0 + go.uber.org/zap v1.27.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/graph-gophers/graphql-go v1.3.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.15.1 // indirect + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect + github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect + github.com/mattermost/logr/v2 v2.0.15 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.24 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/philhofer/fwd v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/tinylib/msgp v1.1.6 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wiggin77/merror v1.0.3 // indirect + github.com/wiggin77/srslog v1.0.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect + golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect + golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5cf1f49 --- /dev/null +++ b/go.sum @@ -0,0 +1,2220 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/spanner v1.24.0/go.mod h1:EZI0yH1D/PrXK0XH9Ba5LGXTXWeqZv0ClOD/19a0Z58= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.sajari.com/docconv v1.2.0/go.mod h1:r8yfCP6OKbZ9Xkd87aBa4nfpk6ud/PoyLwex3n6cXSc= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/HdrHistogram/hdrhistogram-go v0.9.0/go.mod h1:nxrse8/Tzg2tg3DZcZjm6qEclQKK70g0KxO61gFFZD4= +github.com/JalfResi/justext v0.0.0-20170829062021-c0282dea7198/go.mod h1:0SURuH1rsE8aVWvutuMZghRNrNrYEUzibzJfhEYR8L0= +github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/advancedlogic/GoOse v0.0.0-20191112112754-e742535969c1/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w= +github.com/advancedlogic/GoOse v0.0.0-20210820140952-9d5822d4a625/go.mod h1:f3HCSN1fBWjcpGtXyM119MJgeQl838v6so/PQOqvE1w= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= +github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= +github.com/araddon/dateparse v0.0.0-20200409225146-d820a6159ab1/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= +github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= +github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= +github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.2.2/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blevesearch/bleve/v2 v2.3.2/go.mod h1:96+xE5pZUOsr3Y4vHzV1cBC837xZCpwLlX0hrrxnvIg= +github.com/blevesearch/bleve_index_api v1.0.1/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= +github.com/blevesearch/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= +github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= +github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= +github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= +github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= +github.com/blevesearch/scorch_segment_api/v2 v2.1.0/go.mod h1:uch7xyyO/Alxkuxa+CGs79vw0QY8BENSBjg6Mw5L5DE= +github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= +github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= +github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= +github.com/blevesearch/upsidedown_store_api v1.0.1/go.mod h1:MQDVGpHZrpe3Uy26zJBf/a8h0FZY6xJbthIMm8myH2Q= +github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= +github.com/blevesearch/zapx/v11 v11.3.3/go.mod h1:YzTfUm4kS3e8OmTXDHVV8OzC5MWPO/VPJZQgPNVb4Lc= +github.com/blevesearch/zapx/v12 v12.3.3/go.mod h1:RMl6lOZqF+sTxKvhQDJ5yK2LT3Mu7E2p/jGdjAaiRxs= +github.com/blevesearch/zapx/v13 v13.3.3/go.mod h1:eppobNM35U4C22yDvTuxV9xPqo10pwfP/jugL4INWG4= +github.com/blevesearch/zapx/v14 v14.3.3/go.mod h1:zXNcVzukh0AvG57oUtT1T0ndi09H0kELNaNmekEy0jw= +github.com/blevesearch/zapx/v15 v15.3.3/go.mod h1:C+f/97ZzTzK6vt/7sVlZdzZxKu+5+j4SrGCvr9dJzaY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= +github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhui/dktest v0.3.7/go.mod h1:nYMOkafiA07WchSwKnKFUSbGMb2hMm5DrCGiXYG6gwM= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 h1:AQLr//nh20BzN3hIWj2+/Gt3FwSs8Nwo/nz4hMIcLPg= +github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573/go.mod h1:eBvb3i++NHDH4Ugo9qCvMw8t0mTSctaEa5blJbWcNxs= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-asn1-ber/asn1-ber v1.3.2-0.20191121212151-29be175fc3a3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-asn1-ber/asn1-ber v1.5.3 h1:u7utq56RUFiynqUzgVMFDymapcOtQ/MZkh3H4QYkxag= +github.com/go-asn1-ber/asn1-ber v1.5.3/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= +github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-migrate/migrate/v4 v4.15.1/go.mod h1:/CrBenUbcDqsW29jGTR/XFqCfVi/Y6mHXlooCcSOJMQ= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/dataloader/v6 v6.0.0/go.mod h1:J15OZSnOoZgMkijpbZcwCmglIDYqlUiTEE1xLPbyqZM= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= +github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= +github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jaytaylor/html2text v0.0.0-20211105163654-bc68cce691ba/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.2.3/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= +github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= +github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= +github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= +github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= +github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/levigross/exp-html v0.0.0-20120902181939-8df60c69a8f5/go.mod h1:QMe2wuKJ0o7zIVE8AqiT8rd8epmm6WDIZ2wyuBqYPzM= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mark3labs/mcp-go v0.44.0 h1:OlYfcVviAnwNN40QZUrrzU0QZjq3En7rCU5X09a/B7I= +github.com/mark3labs/mcp-go v0.44.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= +github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= +github.com/mattermost/gosaml2 v0.3.3/go.mod h1:Z429EIOiEi9kbq6yHoApfzlcXpa6dzRDc6pO+Vy2Ksk= +github.com/mattermost/gziphandler v0.0.1/go.mod h1:CvvZR7sXqhj81V2swXuQY7T04Ccc89u7W7pHNPKev8g= +github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ0KNm4yZxxFvC1nvRz/gY/Daa35aI= +github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= +github.com/mattermost/logr/v2 v2.0.15 h1:+WNbGcsc3dBao65eXlceB6dTILNJRIrvubnsTl3zBew= +github.com/mattermost/logr/v2 v2.0.15/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg= +github.com/mattermost/mattermost-server/v6 v6.7.2 h1:rRss2/R5LNbyc/P1OA4kSWuVq+rmnxwepuwGpTwL+U4= +github.com/mattermost/mattermost-server/v6 v6.7.2/go.mod h1:b/iDf7Jn2Pd2jWGzaznoVNT811JZpemdmNGP7M/a7Ao= +github.com/mattermost/morph v0.0.0-20220401091636-39f834798da8/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw= +github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.24 h1:HPlHiET6L5gIgrHRaw1xFo1OaN4bEP/082asWh3WJtI= +github.com/minio/minio-go/v7 v7.0.24/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/olivere/elastic v6.2.37+incompatible/go.mod h1:J+q1zQJTgAz9woqsbVRqGeB5G1iqDKVBWLNSYW8yfJ8= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/gosseract/v2 v2.2.4/go.mod h1:ahOp/kHojnOMGv1RaUnR0jwY5JVa6BYKhYAS8nbMLSo= +github.com/otiai10/gosseract/v2 v2.3.1/go.mod h1:2ZOGgdTIXQzCS5f+N1HkcXRgDX6K3ZoYe3Yvo++cpp4= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo= +github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rudderlabs/analytics-go v3.3.2+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= +github.com/russellhaering/goxmldsig v1.2.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/splitio/go-client/v6 v6.1.0/go.mod h1:CEGAEFT99Fwb32ZIRcnZoXTMXddtB6IIpTmt3RP8mnM= +github.com/splitio/go-split-commons/v3 v3.1.0/go.mod h1:29NCy20oAS4ZMy4qkwTd6277eieVDonx4V/aeDU/wUQ= +github.com/splitio/go-toolkit/v4 v4.2.0/go.mod h1:EdIHN0yzB1GTXDYQc0KdKvnjkO/jfUM2YqHVYfhD3Wo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/throttled/throttled v2.2.5+incompatible/go.mod h1:0BjlrEGQmvxps+HuXLsyRdqpSRvJpq0PNIsOtqP9Nos= +github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw= +github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/tylerb/graceful v1.2.15/go.mod h1:LPYTbOYmUTdabwRt0TGhLllQ0MUNbs0Y5q1WXJOI9II= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg= +github.com/wiggin77/merror v1.0.3 h1:8+ZHV+aSnJoYghE3EUThl15C6rvF2TYRSvOSBjdmNR8= +github.com/wiggin77/merror v1.0.3/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= +github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= +github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= +golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb h1:PVGECzEo9Y3uOidtkHGdd347NjLtITfJFO9BxFpmRoo= +golang.org/x/sys v0.0.0-20220403205710-6acee93ad0eb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210726143408-b02e89920bf0/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/olivere/elastic.v6 v6.2.37/go.mod h1:2cTT8Z+/LcArSWpCgvZqBgt3VOqXiy7v00w12Lz8bd4= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= +modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= +modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM= +modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= +modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= +modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= +modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= +modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= +modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= +modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= +modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= +modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= +modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= +modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= +modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y= +modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= +modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo= +modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2f1f754 --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "runtime/debug" + + "github.com/karti-ai/mattermost-mcp-server/cmd" + "github.com/karti-ai/mattermost-mcp-server/pkg/flag" +) + +var Version = "dev" + +func init() { + if Version == "dev" { + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" { + Version = info.Main.Version + } + } + flag.Version = Version +} + +func main() { + cmd.Execute() +} diff --git a/operation/channel/admin.go b/operation/channel/admin.go new file mode 100644 index 0000000..1a041a9 --- /dev/null +++ b/operation/channel/admin.go @@ -0,0 +1,220 @@ +package channel + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mattermost/mattermost-server/v6/model" +) + +var AdminTool = tool.New() + +const ( + CreateChannelToolName = "mattermost_create_channel" + InviteToChannelToolName = "mattermost_invite_to_channel" + DeleteChannelToolName = "mattermost_delete_channel" + LeaveChannelToolName = "mattermost_leave_channel" +) + +var ( + CreateChannelTool = mcp.NewTool( + CreateChannelToolName, + mcp.WithDescription("Create a new channel (public or private) in a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team to create channel in")), + mcp.WithString("name", mcp.Required(), mcp.Description("Channel name (lowercase, no spaces, 2-64 characters)")), + mcp.WithString("display_name", mcp.Required(), mcp.Description("Display name for the channel (2-64 characters)")), + mcp.WithString("type", mcp.Required(), mcp.Description("Channel type: 'O' for public, 'P' for private")), + mcp.WithString("purpose", mcp.Description("Channel description/purpose (optional)")), + ) + + InviteToChannelTool = mcp.NewTool( + InviteToChannelToolName, + mcp.WithDescription("Invite a user to a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to invite user to")), + mcp.WithString("user_id", mcp.Required(), mcp.Description("User to invite")), + ) + + DeleteChannelTool = mcp.NewTool( + DeleteChannelToolName, + mcp.WithDescription("Delete/archive a channel (soft delete by default)"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to delete")), + mcp.WithBoolean("permanent", mcp.Description("Permanently delete instead of archive (default: false)")), + ) + + LeaveChannelTool = mcp.NewTool( + LeaveChannelToolName, + mcp.WithDescription("Remove self from a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel to leave")), + ) +) + +func init() { + registerAdminTools() +} + +func registerAdminTools() { + tools := []server.ServerTool{ + {Tool: CreateChannelTool, Handler: CreateChannelFn}, + {Tool: InviteToChannelTool, Handler: InviteToChannelFn}, + {Tool: DeleteChannelTool, Handler: DeleteChannelFn}, + {Tool: LeaveChannelTool, Handler: LeaveChannelFn}, + } + for _, t := range tools { + AdminTool.RegisterWrite(t) + } +} + +func CreateChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called CreateChannelFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + name, err := params.GetString(args, "name") + if err != nil { + return to.Error(fmt.Errorf("[name] %v", err)), nil + } + + displayName, err := params.GetString(args, "display_name") + if err != nil { + return to.Error(fmt.Errorf("[display_name] %v", err)), nil + } + + channelType, err := params.GetString(args, "type") + if err != nil { + return to.Error(fmt.Errorf("[type] %v", err)), nil + } + + if channelType != "O" && channelType != "P" { + return to.Error(fmt.Errorf("[type] must be 'O' (public) or 'P' (private)")), nil + } + + purpose := params.GetOptionalString(args, "purpose", "") + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + channel := &model.Channel{ + TeamId: teamID, + Name: name, + DisplayName: displayName, + Type: model.ChannelType(channelType), + Purpose: purpose, + } + + created, err := client.CreateChannel(ctx, channel) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to create channel: %v", err)), nil + } + + return to.Result(SlimChannel(created)), nil +} + +func InviteToChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called InviteToChannelFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + userID, err := params.GetString(args, "user_id") + if err != nil { + return to.Error(fmt.Errorf("[user_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + member, err := client.AddChannelMember(ctx, channelID, userID) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to invite user to channel: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "channel_id": channelID, + "user_id": userID, + "member": member, + "message": "User invited to channel successfully", + }), nil +} + +func DeleteChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called DeleteChannelFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + _ = params.GetOptionalBool(args, "permanent", false) + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.DeleteChannel(ctx, channelID) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to delete channel: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "channel_id": channelID, + "message": "Channel deleted successfully", + }), nil +} + +func LeaveChannelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called LeaveChannelFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + user, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + err = client.RemoveChannelMember(ctx, channelID, user.Id) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to leave channel: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "channel_id": channelID, + "user_id": user.Id, + "message": "Successfully left the channel", + }), nil +} diff --git a/operation/channel/channel.go b/operation/channel/channel.go new file mode 100644 index 0000000..42590c9 --- /dev/null +++ b/operation/channel/channel.go @@ -0,0 +1,207 @@ +package channel + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + ListChannelsToolName = "mattermost_list_channels" + GetChannelByNameToolName = "mattermost_get_channel_by_name" + GetChannelInfoToolName = "mattermost_get_channel_info" + ListChannelMembersToolName = "mattermost_list_channel_members" +) + +var ( + ListChannelsTool = mcp.NewTool( + ListChannelsToolName, + mcp.WithDescription("List accessible channels for the authenticated user in a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list channels from")), + mcp.WithNumber("limit", mcp.Description("Maximum number of results to return (default 30)")), + ) + + GetChannelByNameTool = mcp.NewTool( + GetChannelByNameToolName, + mcp.WithDescription("Get a channel by name in a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to search in")), + mcp.WithString("channel_name", mcp.Required(), mcp.Description("Channel name to find (e.g., \"general\", \"social\", \"trading-desk\")")), + ) + + GetChannelInfoTool = mcp.NewTool( + GetChannelInfoToolName, + mcp.WithDescription("Get detailed channel information including member count, purpose, etc."), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to get info for")), + ) + + ListChannelMembersTool = mcp.NewTool( + ListChannelMembersToolName, + mcp.WithDescription("List all members of a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to list members for")), + mcp.WithNumber("page", mcp.Description("Page number for pagination (default 0)")), + mcp.WithNumber("per_page", mcp.Description("Members per page (default 60, max 200)")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: ListChannelsTool, Handler: ListChannelsFn}, + {Tool: GetChannelByNameTool, Handler: GetChannelByNameFn}, + {Tool: GetChannelInfoTool, Handler: GetChannelInfoFn}, + {Tool: ListChannelMembersTool, Handler: ListChannelMembersFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func ListChannelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called ListChannelsFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + limit := params.GetOptionalInt(args, "limit", 30) + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + user, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + channels, err := client.GetChannelsForTeamForUser(ctx, teamID, user.Id, false) + if err != nil { + return to.Error(fmt.Errorf("[channels] failed to list channels: %v", err)), nil + } + + if len(channels) > int(limit) { + channels = channels[:limit] + } + + results := make([]map[string]interface{}, 0, len(channels)) + for _, ch := range channels { + results = append(results, SlimChannel(ch)) + } + + return to.Result(map[string]interface{}{ + "channels": results, + "count": len(results), + }), nil +} + +func GetChannelByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called GetChannelByNameFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + channelName, err := params.GetString(args, "channel_name") + if err != nil { + return to.Error(fmt.Errorf("[channel_name] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + channel, err := client.GetChannelByName(ctx, teamID, channelName) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to get channel: %v", err)), nil + } + + return to.Result(SlimChannel(channel)), nil +} + +func GetChannelInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called GetChannelInfoFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + channel, err := client.GetChannel(ctx, channelID) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to get channel: %v", err)), nil + } + + stats, err := client.GetChannelStats(ctx, channelID) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to get channel stats: %v", err)), nil + } + + return to.Result(DetailedChannel(channel, stats.MemberCount)), nil +} + +func ListChannelMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called ListChannelMembersFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + page := params.GetOptionalInt(args, "page", 0) + perPage := params.GetOptionalInt(args, "per_page", 60) + if perPage > 200 { + perPage = 200 + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + members, err := client.GetChannelMembers(ctx, channelID, int(page), int(perPage)) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to list members: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(members)) + for _, m := range members { + results = append(results, SlimChannelMember(m)) + } + + return to.Result(map[string]interface{}{ + "members": results, + "count": len(results), + "channel_id": channelID, + "page": page, + "per_page": perPage, + }), nil +} diff --git a/operation/channel/channel_test.go b/operation/channel/channel_test.go new file mode 100644 index 0000000..a33fae3 --- /dev/null +++ b/operation/channel/channel_test.go @@ -0,0 +1,108 @@ +package channel + +import ( + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimChannel(t *testing.T) { + c := &model.Channel{ + Id: "channel123", + Name: "general", + DisplayName: "General", + Type: "O", + TeamId: "team456", + } + + slim := SlimChannel(c) + assert.NotNil(t, slim) + assert.Equal(t, "channel123", slim["id"]) + assert.Equal(t, "general", slim["name"]) + assert.Equal(t, "General", slim["display_name"]) + assert.Equal(t, "O", slim["type"]) + assert.Equal(t, "team456", slim["team_id"]) +} + +func TestSlimChannel_Nil(t *testing.T) { + slim := SlimChannel(nil) + assert.Nil(t, slim) +} + +func TestSlimChannel_DifferentTypes(t *testing.T) { + tests := []struct { + name string + chType model.ChannelType + expected string + }{ + {"Open channel", model.ChannelTypeOpen, "O"}, + {"Private channel", model.ChannelTypePrivate, "P"}, + {"Direct message", model.ChannelTypeDirect, "D"}, + {"Group message", model.ChannelTypeGroup, "G"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &model.Channel{ + Id: "channel123", + Name: "test-channel", + DisplayName: "Test Channel", + Type: tt.chType, + TeamId: "team456", + } + slim := SlimChannel(c) + assert.Equal(t, tt.expected, slim["type"]) + }) + } +} + +func TestToolRegistration(t *testing.T) { + tools := Tool.Tools() + assert.Len(t, tools, 5) + + toolNames := make(map[string]bool) + for _, t := range tools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[ListChannelsToolName], "ListChannels tool should be registered") + assert.True(t, toolNames[GetChannelByNameToolName], "GetChannelByName tool should be registered") + assert.True(t, toolNames[GetChannelInfoToolName], "GetChannelInfo tool should be registered") + assert.True(t, toolNames[GetUnreadCountToolName], "GetUnreadCount tool should be registered") + assert.True(t, toolNames[MarkChannelReadToolName], "MarkChannelRead tool should be registered") +} + +func TestListChannelsFn_MissingTeamId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: ListChannelsToolName, + Arguments: map[string]interface{}{}, + }, + } + + result, err := ListChannelsFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestListChannelsFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: ListChannelsToolName, + Arguments: map[string]interface{}{ + "team_id": "team123", + }, + }, + } + + result, err := ListChannelsFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} diff --git a/operation/channel/slim.go b/operation/channel/slim.go new file mode 100644 index 0000000..3b1872d --- /dev/null +++ b/operation/channel/slim.go @@ -0,0 +1,45 @@ +package channel + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimChannel(c *model.Channel) map[string]interface{} { + if c == nil { + return nil + } + return map[string]interface{}{ + "id": c.Id, + "name": c.Name, + "display_name": c.DisplayName, + "type": string(c.Type), + "team_id": c.TeamId, + } +} + +func DetailedChannel(c *model.Channel, memberCount int64) map[string]interface{} { + if c == nil { + return nil + } + return map[string]interface{}{ + "id": c.Id, + "name": c.Name, + "display_name": c.DisplayName, + "type": string(c.Type), + "team_id": c.TeamId, + "purpose": c.Purpose, + "header": c.Header, + "creator_id": c.CreatorId, + "create_at": c.CreateAt, + "member_count": memberCount, + } +} + +func SlimChannelMember(m model.ChannelMember) map[string]interface{} { + return map[string]interface{}{ + "user_id": m.UserId, + "channel_id": m.ChannelId, + "roles": m.Roles, + "last_viewed": m.LastViewedAt, + "msg_count": m.MsgCount, + "mention_count": m.MentionCount, + } +} diff --git a/operation/channel/unread.go b/operation/channel/unread.go new file mode 100644 index 0000000..f93a167 --- /dev/null +++ b/operation/channel/unread.go @@ -0,0 +1,120 @@ +package channel + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetUnreadCountToolName = "mattermost_get_unread_count" + MarkChannelReadToolName = "mattermost_mark_channel_read" +) + +var ( + GetUnreadCountTool = mcp.NewTool( + GetUnreadCountToolName, + mcp.WithDescription("Get unread message counts for all channels in a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to get unread counts for")), + ) + + MarkChannelReadTool = mcp.NewTool( + MarkChannelReadToolName, + mcp.WithDescription("Mark a channel as read (clear unread notifications)"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to mark as read")), + ) +) + +func init() { + registerUnreadTools() +} + +func registerUnreadTools() { + tools := []server.ServerTool{ + {Tool: GetUnreadCountTool, Handler: GetUnreadCountFn}, + {Tool: MarkChannelReadTool, Handler: MarkChannelReadFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func GetUnreadCountFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called GetUnreadCountFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + user, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + members, err := client.GetChannelMembersForUser(ctx, user.Id, teamID) + if err != nil { + return to.Error(fmt.Errorf("[channels] failed to get channel members: %v", err)), nil + } + + memberResults := make([]map[string]interface{}, 0, len(members)) + for _, member := range members { + memberResults = append(memberResults, map[string]interface{}{ + "channel_id": member.ChannelId, + "user_id": member.UserId, + "unread_messages": member.MsgCount, + "unread_mentions": member.MentionCount, + "last_viewed_at": member.LastViewedAt, + }) + } + + return to.Result(map[string]interface{}{ + "channels": memberResults, + "count": len(memberResults), + }), nil +} + +func MarkChannelReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Channel] Called MarkChannelReadFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + user, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + _, err = client.MarkChannelAsRead(ctx, channelID, user.Id) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to mark channel as read: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "channel_id": channelID, + "message": "Channel marked as read successfully", + }), nil +} diff --git a/operation/command/command.go b/operation/command/command.go new file mode 100644 index 0000000..2169bfc --- /dev/null +++ b/operation/command/command.go @@ -0,0 +1,75 @@ +package command + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + ExecuteSlashCommandToolName = "mattermost_execute_slash_command" +) + +var ( + ExecuteSlashCommandTool = mcp.NewTool( + ExecuteSlashCommandToolName, + mcp.WithDescription("Execute a slash command in a channel (e.g., /remind, /poll)"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to execute command in")), + mcp.WithString("command", mcp.Required(), mcp.Description("Slash command to execute (e.g., /remind @channel meeting in 10 minutes)")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: ExecuteSlashCommandTool, Handler: ExecuteSlashCommandFn}, + } + for _, t := range tools { + Tool.RegisterWrite(t) + } +} + +func ExecuteSlashCommandFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Command] Called ExecuteSlashCommandFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + command, err := params.GetString(args, "command") + if err != nil { + return to.Error(fmt.Errorf("[command] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + resp, err := client.ExecuteSlashCommand(ctx, channelID, command) + if err != nil { + return to.Error(fmt.Errorf("[command] failed to execute: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "response": resp.ResponseType, + "text": resp.Text, + "goto_location": resp.GotoLocation, + }), nil +} diff --git a/operation/dm/dm.go b/operation/dm/dm.go new file mode 100644 index 0000000..bfb2c59 --- /dev/null +++ b/operation/dm/dm.go @@ -0,0 +1,115 @@ +package dm + +import ( + "context" + "fmt" + "strings" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + CreateDMToolName = "mattermost_create_dm" + CreateGroupDMToolName = "mattermost_create_group_dm" +) + +var ( + CreateDMTool = mcp.NewTool( + CreateDMToolName, + mcp.WithDescription("Create direct message channel with user"), + mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to DM with")), + ) + + CreateGroupDMTool = mcp.NewTool( + CreateGroupDMToolName, + mcp.WithDescription("Create group DM channel with multiple users"), + mcp.WithString("user_ids", mcp.Required(), mcp.Description("Comma-separated list of user IDs to include in group DM")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: CreateDMTool, Handler: CreateDMFn}, + {Tool: CreateGroupDMTool, Handler: CreateGroupDMFn}, + } + for _, t := range tools { + Tool.RegisterWrite(t) + } +} + +func CreateDMFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[DM] Called CreateDMFn") + + args := req.GetArguments() + + userId, err := params.GetString(args, "user_id") + if err != nil { + return to.Error(fmt.Errorf("[user_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + // Get current user to get our own user ID + me, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[me] failed to get current user: %v", err)), nil + } + + // Create direct message channel between current user and target user + channel, err := client.CreateDirectChannel(ctx, me.Id, userId) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to create DM: %v", err)), nil + } + + return to.Result(SlimChannel(channel)), nil +} + +func CreateGroupDMFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[DM] Called CreateGroupDMFn") + + args := req.GetArguments() + + userIDsStr, err := params.GetString(args, "user_ids") + if err != nil { + return to.Error(fmt.Errorf("[user_ids] %v", err)), nil + } + + // Parse comma-separated user IDs + userIDs := strings.Split(userIDsStr, ",") + if len(userIDs) < 2 { + return to.Error(fmt.Errorf("[user_ids] at least 2 user IDs required for group DM (got %d)", len(userIDs))), nil + } + + // Trim whitespace from each ID + for i, id := range userIDs { + userIDs[i] = strings.TrimSpace(id) + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + // Create group DM channel + channel, err := client.CreateGroupChannel(ctx, userIDs) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to create group DM: %v", err)), nil + } + + return to.Result(SlimChannel(channel)), nil +} diff --git a/operation/dm/dm_test.go b/operation/dm/dm_test.go new file mode 100644 index 0000000..c7fabcd --- /dev/null +++ b/operation/dm/dm_test.go @@ -0,0 +1,77 @@ +package dm + +import ( + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimChannel(t *testing.T) { + c := &model.Channel{ + Id: "channel123", + Name: "user1__user2", + DisplayName: "user1, user2", + Type: model.ChannelTypeDirect, + TeamId: "", + } + + slim := SlimChannel(c) + assert.NotNil(t, slim) + assert.Equal(t, "channel123", slim["id"]) + assert.Equal(t, "user1__user2", slim["name"]) + assert.Equal(t, "user1, user2", slim["display_name"]) + assert.Equal(t, model.ChannelTypeDirect, slim["type"]) + assert.Equal(t, "", slim["team_id"]) +} + +func TestSlimChannel_Nil(t *testing.T) { + slim := SlimChannel(nil) + assert.Nil(t, slim) +} + +func TestToolRegistration(t *testing.T) { + tools := Tool.Tools() + assert.Len(t, tools, 1) + + toolNames := make(map[string]bool) + for _, t := range tools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[CreateDMToolName], "CreateDM tool should be registered") +} + +func TestCreateDMFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": "user123", + }, + }, + } + + result, err := CreateDMFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestCreateDMFn_MissingUserId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: CreateDMToolName, + Arguments: map[string]interface{}{}, + }, + } + + result, err := CreateDMFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} diff --git a/operation/dm/slim.go b/operation/dm/slim.go new file mode 100644 index 0000000..330da6a --- /dev/null +++ b/operation/dm/slim.go @@ -0,0 +1,16 @@ +package dm + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimChannel(c *model.Channel) map[string]interface{} { + if c == nil { + return nil + } + return map[string]interface{}{ + "id": c.Id, + "name": c.Name, + "display_name": c.DisplayName, + "type": c.Type, + "team_id": c.TeamId, + } +} diff --git a/operation/file/file.go b/operation/file/file.go new file mode 100644 index 0000000..4d744e9 --- /dev/null +++ b/operation/file/file.go @@ -0,0 +1,191 @@ +package file + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/karti-ai/mattermost-mcp-server/pkg/file" + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mattermost/mattermost-server/v6/model" +) + +var Tool = tool.New() + +const ( + UploadFileToolName = "mattermost_upload_file" + DownloadFileToolName = "mattermost_download_file" +) + +var ( + UploadFileTool = mcp.NewTool( + UploadFileToolName, + mcp.WithDescription("Upload file to channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to upload file to")), + mcp.WithString("file_path", mcp.Required(), mcp.Description("Local file path to upload")), + mcp.WithString("message", mcp.Description("Message to post with file (optional)")), + ) + + DownloadFileTool = mcp.NewTool( + DownloadFileToolName, + mcp.WithDescription("Download file from Mattermost"), + mcp.WithString("file_id", mcp.Required(), mcp.Description("File ID to download")), + mcp.WithString("download_path", mcp.Required(), mcp.Description("Local path where file should be saved")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: UploadFileTool, Handler: UploadFileFn}, + {Tool: DownloadFileTool, Handler: DownloadFileFn}, + } + for _, t := range tools { + Tool.RegisterWrite(t) + } +} + +func UploadFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[File] Called UploadFileFn") + + args := req.GetArguments() + + channelId, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + filePath, err := params.GetString(args, "file_path") + if err != nil { + return to.Error(fmt.Errorf("[file_path] %v", err)), nil + } + + message := params.GetOptionalString(args, "message", "") + + if !file.IsValidPath(filePath) { + return to.Error(fmt.Errorf("[file_path] path traversal detected: %s", filePath)), nil + } + + fileInfo, err := file.GetFileInfo(filePath) + if err != nil { + return to.Error(fmt.Errorf("[file_path] failed to access file: %v", err)), nil + } + + if fileInfo.IsDir() { + return to.Error(fmt.Errorf("[file_path] path is a directory, not a file: %s", filePath)), nil + } + + if err := file.ValidateFileSize(fileInfo.Size()); err != nil { + return to.Error(fmt.Errorf("[file_path] %v", err)), nil + } + + if err := file.ValidateFilename(fileInfo.Name()); err != nil { + return to.Error(fmt.Errorf("[file_path] %v", err)), nil + } + + data, err := os.ReadFile(filePath) + if err != nil { + return to.Error(fmt.Errorf("[file_path] failed to read file: %v", err)), nil + } + + if _, err := file.ValidateMimeType(data); err != nil { + return to.Error(fmt.Errorf("[file_path] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + filename := filepath.Base(filePath) + uploadResp, err := client.UploadFile(ctx, data, channelId, filename) + if err != nil { + return to.Error(fmt.Errorf("[upload] failed to upload file: %v", err)), nil + } + + if message != "" && len(uploadResp.FileInfos) > 0 { + post := &model.Post{ + ChannelId: channelId, + Message: message, + FileIds: []string{uploadResp.FileInfos[0].Id}, + } + _, err := client.CreatePost(ctx, post) + if err != nil { + return to.Result(map[string]interface{}{ + "success": true, + "file_id": uploadResp.FileInfos[0].Id, + "file_infos": SlimFileInfos(uploadResp.FileInfos), + "warning": fmt.Sprintf("File uploaded but message failed to post: %v", err), + }), nil + } + } + + return to.Result(map[string]interface{}{ + "success": true, + "file_id": uploadResp.FileInfos[0].Id, + "file_infos": SlimFileInfos(uploadResp.FileInfos), + }), nil +} + +func DownloadFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[File] Called DownloadFileFn") + + args := req.GetArguments() + + fileId, err := params.GetString(args, "file_id") + if err != nil { + return to.Error(fmt.Errorf("[file_id] %v", err)), nil + } + + downloadPath, err := params.GetString(args, "download_path") + if err != nil { + return to.Error(fmt.Errorf("[download_path] %v", err)), nil + } + + if !file.IsValidPath(downloadPath) { + return to.Error(fmt.Errorf("[download_path] path traversal detected: %s", downloadPath)), nil + } + + if file.Exists(downloadPath) { + return to.Error(fmt.Errorf("[download_path] file already exists: %s", downloadPath)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + data, _, err := client.GetFile(ctx, fileId) + if err != nil { + return to.Error(fmt.Errorf("[download] failed to download file: %v", err)), nil + } + + if int64(len(data)) > file.MaxFileSize { + return to.Error(fmt.Errorf("[download] downloaded file size %d exceeds maximum allowed %d", len(data), file.MaxFileSize)), nil + } + + if err := file.CheckDiskSpace(downloadPath, int64(len(data))); err != nil { + return to.Error(fmt.Errorf("[download_path] %v", err)), nil + } + + if err := os.WriteFile(downloadPath, data, 0644); err != nil { + return to.Error(fmt.Errorf("[download_path] failed to save file: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "file_id": fileId, + "download_path": downloadPath, + "size": len(data), + }), nil +} diff --git a/operation/file/file_test.go b/operation/file/file_test.go new file mode 100644 index 0000000..a25a293 --- /dev/null +++ b/operation/file/file_test.go @@ -0,0 +1,494 @@ +package file + +import ( + "os" + "path/filepath" + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/file" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimFileInfo(t *testing.T) { + f := &model.FileInfo{ + Id: "file123", + Name: "test.pdf", + Extension: "pdf", + Size: 1024, + MimeType: "application/pdf", + ChannelId: "channel456", + CreateAt: 1234567890000, + } + + slim := SlimFileInfo(f) + assert.NotNil(t, slim) + assert.Equal(t, "file123", slim["id"]) + assert.Equal(t, "test.pdf", slim["name"]) + assert.Equal(t, "pdf", slim["extension"]) + assert.Equal(t, int64(1024), slim["size"]) + assert.Equal(t, "application/pdf", slim["mime_type"]) + assert.Equal(t, "channel456", slim["channel_id"]) + assert.Equal(t, int64(1234567890000), slim["create_at"]) +} + +func TestSlimFileInfo_Nil(t *testing.T) { + slim := SlimFileInfo(nil) + assert.Nil(t, slim) +} + +func TestSlimFileInfos(t *testing.T) { + infos := []*model.FileInfo{ + { + Id: "file1", + Name: "test1.pdf", + Extension: "pdf", + Size: 1024, + MimeType: "application/pdf", + ChannelId: "channel1", + CreateAt: 1234567890000, + }, + { + Id: "file2", + Name: "test2.png", + Extension: "png", + Size: 2048, + MimeType: "image/png", + ChannelId: "channel2", + CreateAt: 1234567890001, + }, + } + + slim := SlimFileInfos(infos) + assert.NotNil(t, slim) + assert.Len(t, slim, 2) + assert.Equal(t, "file1", slim[0]["id"]) + assert.Equal(t, "file2", slim[1]["id"]) +} + +func TestSlimFileInfos_Nil(t *testing.T) { + slim := SlimFileInfos(nil) + assert.Nil(t, slim) +} + +func TestSlimFileInfos_WithNilItem(t *testing.T) { + infos := []*model.FileInfo{ + { + Id: "file1", + Name: "test1.pdf", + Extension: "pdf", + Size: 1024, + MimeType: "application/pdf", + ChannelId: "channel1", + CreateAt: 1234567890000, + }, + nil, + { + Id: "file2", + Name: "test2.png", + Extension: "png", + Size: 2048, + MimeType: "image/png", + ChannelId: "channel2", + CreateAt: 1234567890001, + }, + } + + slim := SlimFileInfos(infos) + assert.NotNil(t, slim) + assert.Len(t, slim, 2) +} + +func TestToolRegistration(t *testing.T) { + tools := Tool.Tools() + assert.Len(t, tools, 2) + + toolNames := make(map[string]bool) + for _, t := range tools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[UploadFileToolName], "UploadFile tool should be registered") + assert.True(t, toolNames[DownloadFileToolName], "DownloadFile tool should be registered") +} + +func TestUploadFileFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": "/tmp/test.txt", + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestUploadFileFn_MissingChannelId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "file_path": "/tmp/test.txt", + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestUploadFileFn_MissingFilePath(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestUploadFileFn_PathTraversal(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": "../../../etc/passwd", + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "path traversal") +} + +func TestUploadFileFn_AbsolutePath(t *testing.T) { + tmpDir := t.TempDir() + testFile := filepath.Join(tmpDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + assert.NoError(t, err) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": testFile, + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "client not initialized") +} + +func TestUploadFileFn_DangerousExtension(t *testing.T) { + tmpDir := t.TempDir() + dangerousFile := filepath.Join(tmpDir, "malicious.sh") + err := os.WriteFile(dangerousFile, []byte("#!/bin/bash\necho 'pwned'"), 0644) + assert.NoError(t, err) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": dangerousFile, + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "not allowed") +} + +func TestUploadFileFn_NonExistentFile(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": "/tmp/nonexistent_file_12345.txt", + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "failed to access") +} + +func TestUploadFileFn_DirectoryInsteadOfFile(t *testing.T) { + tmpDir := t.TempDir() + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: UploadFileToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "file_path": tmpDir, + }, + }, + } + + result, err := UploadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "directory") +} + +func TestDownloadFileFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DownloadFileToolName, + Arguments: map[string]interface{}{ + "file_id": "file123", + "download_path": "/tmp/downloaded.txt", + }, + }, + } + + result, err := DownloadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestDownloadFileFn_MissingFileId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DownloadFileToolName, + Arguments: map[string]interface{}{ + "download_path": "/tmp/downloaded.txt", + }, + }, + } + + result, err := DownloadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestDownloadFileFn_MissingDownloadPath(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DownloadFileToolName, + Arguments: map[string]interface{}{ + "file_id": "file123", + }, + }, + } + + result, err := DownloadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestDownloadFileFn_PathTraversal(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DownloadFileToolName, + Arguments: map[string]interface{}{ + "file_id": "file123", + "download_path": "../../../etc/passwd", + }, + }, + } + + result, err := DownloadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "path traversal") +} + +func TestDownloadFileFn_FileAlreadyExists(t *testing.T) { + tmpDir := t.TempDir() + existingFile := filepath.Join(tmpDir, "exists.txt") + err := os.WriteFile(existingFile, []byte("existing content"), 0644) + assert.NoError(t, err) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DownloadFileToolName, + Arguments: map[string]interface{}{ + "file_id": "file123", + "download_path": existingFile, + }, + }, + } + + result, err := DownloadFileFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) + assert.Contains(t, result.Content[0].(mcp.TextContent).Text, "already exists") +} + +func TestFileSecurityValidation(t *testing.T) { + t.Run("IsValidPath allows safe relative paths", func(t *testing.T) { + assert.True(t, file.IsValidPath("document.pdf")) + assert.True(t, file.IsValidPath("subdir/file.png")) + assert.True(t, file.IsValidPath("./file.txt")) + }) + + t.Run("IsValidPath blocks path traversal", func(t *testing.T) { + assert.False(t, file.IsValidPath("../file.txt")) + assert.False(t, file.IsValidPath("../../etc/passwd")) + assert.False(t, file.IsValidPath("subdir/../../../etc/passwd")) + }) + + t.Run("IsValidPath allows absolute paths without traversal", func(t *testing.T) { + assert.True(t, file.IsValidPath("/etc/passwd")) + assert.True(t, file.IsValidPath("/tmp/file.txt")) + }) + + t.Run("IsDangerousExtension blocks dangerous types", func(t *testing.T) { + assert.True(t, file.IsDangerousExtension("file.exe")) + assert.True(t, file.IsDangerousExtension("script.sh")) + assert.True(t, file.IsDangerousExtension("run.bat")) + assert.True(t, file.IsDangerousExtension("malicious.js")) + }) + + t.Run("IsDangerousExtension allows safe types", func(t *testing.T) { + assert.False(t, file.IsDangerousExtension("document.pdf")) + assert.False(t, file.IsDangerousExtension("image.png")) + assert.False(t, file.IsDangerousExtension("notes.txt")) + }) + + t.Run("ValidateFileSize blocks oversized files", func(t *testing.T) { + err := file.ValidateFileSize(100 * 1024 * 1024) // 100MB + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + }) + + t.Run("ValidateFileSize allows files under limit", func(t *testing.T) { + err := file.ValidateFileSize(10 * 1024 * 1024) // 10MB + assert.NoError(t, err) + }) + + t.Run("IsAllowedMimeType allows whitelist types", func(t *testing.T) { + assert.True(t, file.IsAllowedMimeType("image/jpeg")) + assert.True(t, file.IsAllowedMimeType("image/png")) + assert.True(t, file.IsAllowedMimeType("application/pdf")) + assert.True(t, file.IsAllowedMimeType("text/plain")) + }) + + t.Run("IsAllowedMimeType blocks non-whitelist types", func(t *testing.T) { + assert.False(t, file.IsAllowedMimeType("application/x-executable")) + assert.False(t, file.IsAllowedMimeType("application/x-sh")) + assert.False(t, file.IsAllowedMimeType("text/html")) + }) + + t.Run("DetectMimeType detects file types correctly", func(t *testing.T) { + pngData := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + assert.Equal(t, "image/png", file.DetectMimeType(pngData)) + + textData := []byte("Hello, World!") + assert.Equal(t, "text/plain; charset=utf-8", file.DetectMimeType(textData)) + }) +} + +func TestSanitizePath(t *testing.T) { + t.Run("SanitizePath returns clean path for valid input", func(t *testing.T) { + path, err := file.SanitizePath("subdir//file.txt") + assert.NoError(t, err) + assert.Equal(t, "subdir/file.txt", path) + }) + + t.Run("SanitizePath rejects traversal attempts", func(t *testing.T) { + _, err := file.SanitizePath("../file.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "path traversal") + }) + + t.Run("SanitizePath allows absolute paths without traversal", func(t *testing.T) { + path, err := file.SanitizePath("/etc/passwd") + assert.NoError(t, err) + assert.Equal(t, "/etc/passwd", path) + }) +} + +func TestValidateFilename(t *testing.T) { + t.Run("ValidateFilename rejects empty filename", func(t *testing.T) { + err := file.ValidateFilename("") + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot be empty") + }) + + t.Run("ValidateFilename rejects dangerous extensions", func(t *testing.T) { + err := file.ValidateFilename("malicious.exe") + assert.Error(t, err) + assert.Contains(t, err.Error(), "not allowed") + }) + + t.Run("ValidateFilename accepts safe filenames", func(t *testing.T) { + err := file.ValidateFilename("document.pdf") + assert.NoError(t, err) + }) + + t.Run("ValidateFilename rejects null bytes", func(t *testing.T) { + err := file.ValidateFilename("file\x00.txt") + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid characters") + }) +} + +func TestCheckDiskSpace(t *testing.T) { + t.Run("CheckDiskSpace succeeds for writable directory", func(t *testing.T) { + tmpDir := t.TempDir() + testPath := filepath.Join(tmpDir, "subdir", "file.txt") + err := file.CheckDiskSpace(testPath, 1024) + assert.NoError(t, err) + }) +} + +func TestValidateMimeType(t *testing.T) { + t.Run("ValidateMimeType accepts PNG images", func(t *testing.T) { + pngData := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52} + mimeType, err := file.ValidateMimeType(pngData) + assert.NoError(t, err) + assert.Equal(t, "image/png", mimeType) + }) + + t.Run("ValidateMimeType accepts plain text", func(t *testing.T) { + textData := []byte("Hello, World! This is plain text.") + mimeType, err := file.ValidateMimeType(textData) + assert.NoError(t, err) + assert.Contains(t, mimeType, "text/plain") + }) +} diff --git a/operation/file/slim.go b/operation/file/slim.go new file mode 100644 index 0000000..35fb4f8 --- /dev/null +++ b/operation/file/slim.go @@ -0,0 +1,31 @@ +package file + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimFileInfo(f *model.FileInfo) map[string]interface{} { + if f == nil { + return nil + } + return map[string]interface{}{ + "id": f.Id, + "name": f.Name, + "extension": f.Extension, + "size": f.Size, + "mime_type": f.MimeType, + "channel_id": f.ChannelId, + "create_at": f.CreateAt, + } +} + +func SlimFileInfos(infos []*model.FileInfo) []map[string]interface{} { + if infos == nil { + return nil + } + result := make([]map[string]interface{}, 0, len(infos)) + for _, info := range infos { + if slim := SlimFileInfo(info); slim != nil { + result = append(result, slim) + } + } + return result +} diff --git a/operation/integration_test.go b/operation/integration_test.go new file mode 100644 index 0000000..23e4386 --- /dev/null +++ b/operation/integration_test.go @@ -0,0 +1,1433 @@ +package operation + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/karti-ai/mattermost-mcp-server/operation/channel" + "github.com/karti-ai/mattermost-mcp-server/operation/dm" + "github.com/karti-ai/mattermost-mcp-server/operation/file" + "github.com/karti-ai/mattermost-mcp-server/operation/messaging" + "github.com/karti-ai/mattermost-mcp-server/operation/reaction" + "github.com/karti-ai/mattermost-mcp-server/operation/team" + "github.com/karti-ai/mattermost-mcp-server/operation/user" + mmerrors "github.com/karti-ai/mattermost-mcp-server/pkg/errors" + "github.com/karti-ai/mattermost-mcp-server/pkg/flag" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockMattermostServer creates a mock Mattermost server for testing +type mockMattermostServer struct { + server *httptest.Server + users map[string]*model.User + channels map[string]*model.Channel + posts map[string]*model.Post + reactions map[string]*model.Reaction + callHistory []mockCall +} + +type mockCall struct { + Method string + Path string + Body interface{} +} + +func newMockMattermostServer() *mockMattermostServer { + m := &mockMattermostServer{ + users: make(map[string]*model.User), + channels: make(map[string]*model.Channel), + posts: make(map[string]*model.Post), + reactions: make(map[string]*model.Reaction), + } + + m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m.handleRequest(w, r) + })) + + return m +} + +func (m *mockMattermostServer) handleRequest(w http.ResponseWriter, r *http.Request) { + m.callHistory = append(m.callHistory, mockCall{ + Method: r.Method, + Path: r.URL.Path, + }) + + w.Header().Set("Content-Type", "application/json") + + switch { + case r.URL.Path == "/api/v4/users/me": + m.handleGetMe(w, r) + case r.URL.Path == "/api/v4/users/search": + m.handleSearchUsers(w, r) + case r.URL.Path == "/api/v4/channels/direct": + m.handleCreateDirectChannel(w, r) + case r.URL.Path == "/api/v4/posts": + m.handleCreatePost(w, r) + case r.Method == "PUT" && len(r.URL.Path) > 14 && strings.HasPrefix(r.URL.Path, "/api/v4/posts/"): + m.handleUpdatePost(w, r) + case r.Method == "DELETE" && len(r.URL.Path) > 14 && strings.HasPrefix(r.URL.Path, "/api/v4/posts/"): + m.handleDeletePost(w, r) + case r.URL.Path == "/api/v4/reactions": + m.handleReaction(w, r) + case r.Method == "GET" && len(r.URL.Path) > 17 && strings.HasPrefix(r.URL.Path, "/api/v4/channels/"): + m.handleGetChannel(w, r) + case len(r.URL.Path) > 13 && strings.HasPrefix(r.URL.Path, "/api/v4/users/"): + m.handleGetChannelsForTeamForUser(w, r) + default: + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "Not found"}) + } +} + +func (m *mockMattermostServer) handleGetMe(w http.ResponseWriter, r *http.Request) { + user := &model.User{ + Id: "user-current", + Username: "currentuser", + Email: "current@example.com", + FirstName: "Current", + LastName: "User", + Roles: "system_user", + } + json.NewEncoder(w).Encode(user) +} + +func (m *mockMattermostServer) handleSearchUsers(w http.ResponseWriter, r *http.Request) { + var search model.UserSearch + json.NewDecoder(r.Body).Decode(&search) + + users := []*model.User{} + for _, u := range m.users { + if search.Term == "" || + u.Username == search.Term || + u.Email == search.Term || + u.FirstName+" "+u.LastName == search.Term { + users = append(users, u) + } + } + + // Add default user if no results + if len(users) == 0 && search.Term != "" { + users = append(users, &model.User{ + Id: "user-" + search.Term, + Username: search.Term, + Email: search.Term + "@example.com", + FirstName: "Test", + LastName: "User", + Roles: "system_user", + }) + } + + json.NewEncoder(w).Encode(users) +} + +func (m *mockMattermostServer) handleCreateDirectChannel(w http.ResponseWriter, r *http.Request) { + var userIds []string + json.NewDecoder(r.Body).Decode(&userIds) + + if len(userIds) != 2 { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"message": "Need exactly 2 user IDs"}) + return + } + + channel := &model.Channel{ + Id: "dm-" + userIds[0] + "-" + userIds[1], + Name: userIds[0] + "__" + userIds[1], + DisplayName: "Direct Message", + Type: model.ChannelTypeDirect, + TeamId: "", + } + m.channels[channel.Id] = channel + json.NewEncoder(w).Encode(channel) +} + +func (m *mockMattermostServer) handleCreatePost(w http.ResponseWriter, r *http.Request) { + var post model.Post + json.NewDecoder(r.Body).Decode(&post) + + post.Id = "post-" + fmt.Sprintf("%d", time.Now().UnixNano()) + post.CreateAt = time.Now().UnixMilli() + post.UpdateAt = post.CreateAt + m.posts[post.Id] = &post + + json.NewEncoder(w).Encode(post) +} + +func (m *mockMattermostServer) handleUpdatePost(w http.ResponseWriter, r *http.Request) { + postID := r.URL.Path[14:] // Remove "/api/v4/posts/" + var post model.Post + json.NewDecoder(r.Body).Decode(&post) + + if existing, ok := m.posts[postID]; ok { + existing.Message = post.Message + existing.UpdateAt = time.Now().UnixMilli() + json.NewEncoder(w).Encode(existing) + return + } + + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "Post not found"}) +} + +func (m *mockMattermostServer) handleDeletePost(w http.ResponseWriter, r *http.Request) { + postID := r.URL.Path[14:] // Remove "/api/v4/posts/" + + if _, ok := m.posts[postID]; ok { + delete(m.posts, postID) + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "OK"}) + return + } + + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "Post not found"}) +} + +func (m *mockMattermostServer) handleReaction(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "POST": + var reaction model.Reaction + json.NewDecoder(r.Body).Decode(&reaction) + reaction.CreateAt = time.Now().UnixMilli() + m.reactions[reaction.PostId+"_"+reaction.EmojiName] = &reaction + json.NewEncoder(w).Encode(reaction) + case "DELETE": + var reaction model.Reaction + json.NewDecoder(r.Body).Decode(&reaction) + delete(m.reactions, reaction.PostId+"_"+reaction.EmojiName) + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"status": "OK"}) + } +} + +func (m *mockMattermostServer) handleGetChannel(w http.ResponseWriter, r *http.Request) { + channelID := r.URL.Path[17:] // Remove "/api/v4/channels/" + + // Check if it's a channels endpoint + if len(channelID) > 8 && channelID[len(channelID)-8:] == "/posts" { + // This is a posts request, handle separately + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{"posts": []map[string]interface{}{}}) + return + } + + if channel, ok := m.channels[channelID]; ok { + json.NewEncoder(w).Encode(channel) + return + } + + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "Channel not found"}) +} + +func (m *mockMattermostServer) handleGetChannelsForTeamForUser(w http.ResponseWriter, r *http.Request) { + // Path format: /api/v4/users/{userID}/teams/{teamID}/channels + channels := []*model.Channel{ + { + Id: "channel-1", + Name: "town-square", + DisplayName: "Town Square", + Type: model.ChannelTypeOpen, + TeamId: "team-1", + }, + { + Id: "channel-2", + Name: "off-topic", + DisplayName: "Off-Topic", + Type: model.ChannelTypeOpen, + TeamId: "team-1", + }, + { + Id: "channel-3", + Name: "private-channel", + DisplayName: "Private Channel", + Type: model.ChannelTypePrivate, + TeamId: "team-1", + }, + } + json.NewEncoder(w).Encode(channels) +} + +func (m *mockMattermostServer) URL() string { + return m.server.URL +} + +func (m *mockMattermostServer) Close() { + m.server.Close() +} + +func (m *mockMattermostServer) GetCallHistory() []mockCall { + return m.callHistory +} + +func (m *mockMattermostServer) ClearHistory() { + m.callHistory = nil +} + +func setupMockClient(mockServer *mockMattermostServer) *mattermost.Client { + client := mattermost.NewClient(mockServer.URL(), "test-bot-token", "test-pat") + mattermost.SetGlobalClient(client) + return client +} + +// ==================== Cross-Tool Workflow Tests ==================== + +func TestIntegration_DMWorkflow_CreateDMSendMessageAddReaction(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + // Step 1: Search for a user + searchReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: user.SearchUsersToolName, + Arguments: map[string]interface{}{ + "term": "testuser", + }, + }, + } + + searchResult, err := user.SearchUsersFn(context.Background(), searchReq) + require.NoError(t, err) + require.NotNil(t, searchResult) + require.False(t, searchResult.IsError, "Search should succeed") + + // Parse search result to get user ID + var searchData map[string]interface{} + err = json.Unmarshal([]byte(searchResult.Content[0].(mcp.TextContent).Text), &searchData) + require.NoError(t, err) + + users := searchData["users"].([]interface{}) + require.Greater(t, len(users), 0, "Should find at least one user") + + userData := users[0].(map[string]interface{}) + userID := userData["id"].(string) + + // Step 2: Create DM channel with the user + dmReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": userID, + }, + }, + } + + dmResult, err := dm.CreateDMFn(context.Background(), dmReq) + require.NoError(t, err) + require.NotNil(t, dmResult) + require.False(t, dmResult.IsError, "DM creation should succeed") + + // Parse DM result to get channel ID + var dmData map[string]interface{} + err = json.Unmarshal([]byte(dmResult.Content[0].(mcp.TextContent).Text), &dmData) + require.NoError(t, err) + + channelID := dmData["id"].(string) + require.NotEmpty(t, channelID) + + // Step 3: Send a message to the DM channel + msgReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": channelID, + "message": "Hello from integration test!", + }, + }, + } + + msgResult, err := messaging.SendMessageFn(context.Background(), msgReq) + require.NoError(t, err) + require.NotNil(t, msgResult) + require.False(t, msgResult.IsError, "Message send should succeed") + + // Parse message result to get post ID + var msgData map[string]interface{} + err = json.Unmarshal([]byte(msgResult.Content[0].(mcp.TextContent).Text), &msgData) + require.NoError(t, err) + + postID := msgData["id"].(string) + require.NotEmpty(t, postID) + + // Step 4: Add a reaction to the message + reactionReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.AddReactionToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + "emoji_name": "thumbsup", + }, + }, + } + + reactionResult, err := reaction.AddReactionFn(context.Background(), reactionReq) + require.NoError(t, err) + require.NotNil(t, reactionResult) + require.False(t, reactionResult.IsError, "Reaction add should succeed") + + // Verify the reaction result + var reactionData map[string]interface{} + err = json.Unmarshal([]byte(reactionResult.Content[0].(mcp.TextContent).Text), &reactionData) + require.NoError(t, err) + + assert.Equal(t, postID, reactionData["post_id"]) + assert.Equal(t, "thumbsup", reactionData["emoji_name"]) + + // Verify call history shows the workflow sequence + history := mock.GetCallHistory() + require.GreaterOrEqual(t, len(history), 4, "Should have at least 4 API calls") +} + +func TestIntegration_ChannelWorkflow_ListSendEditDelete(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + // Step 1: List channels for a team + listReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: channel.ListChannelsToolName, + Arguments: map[string]interface{}{ + "team_id": "team-1", + "limit": 10, + }, + }, + } + + listResult, err := channel.ListChannelsFn(context.Background(), listReq) + require.NoError(t, err) + require.NotNil(t, listResult) + require.False(t, listResult.IsError, "List channels should succeed") + + // Parse channel list result + var listData map[string]interface{} + err = json.Unmarshal([]byte(listResult.Content[0].(mcp.TextContent).Text), &listData) + require.NoError(t, err) + + channels := listData["channels"].([]interface{}) + require.GreaterOrEqual(t, len(channels), 1, "Should have at least one channel") + + channelData := channels[0].(map[string]interface{}) + channelID := channelData["id"].(string) + + // Step 2: Send a message to the channel + msgReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": channelID, + "message": "Original message", + }, + }, + } + + msgResult, err := messaging.SendMessageFn(context.Background(), msgReq) + require.NoError(t, err) + require.NotNil(t, msgResult) + require.False(t, msgResult.IsError) + + var msgData map[string]interface{} + err = json.Unmarshal([]byte(msgResult.Content[0].(mcp.TextContent).Text), &msgData) + require.NoError(t, err) + + postID := msgData["id"].(string) + + // Step 3: Edit the message + editReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.EditMessageToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + "message": "Edited message", + }, + }, + } + + editResult, err := messaging.EditMessageFn(context.Background(), editReq) + require.NoError(t, err) + require.NotNil(t, editResult) + require.False(t, editResult.IsError, "Edit message should succeed") + + // Verify edit result + var editData map[string]interface{} + err = json.Unmarshal([]byte(editResult.Content[0].(mcp.TextContent).Text), &editData) + require.NoError(t, err) + + assert.Equal(t, "Edited message", editData["message"]) + + // Step 4: Delete the message + deleteReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.DeleteMessageToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + }, + }, + } + + deleteResult, err := messaging.DeleteMessageFn(context.Background(), deleteReq) + require.NoError(t, err) + require.NotNil(t, deleteResult) + require.False(t, deleteResult.IsError, "Delete message should succeed") + + // Verify delete result + var deleteData map[string]interface{} + err = json.Unmarshal([]byte(deleteResult.Content[0].(mcp.TextContent).Text), &deleteData) + require.NoError(t, err) + + assert.True(t, deleteData["success"].(bool)) + assert.Equal(t, postID, deleteData["post_id"]) +} + +func TestIntegration_SearchUserCreateDMSendMessage(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + // Add a specific user to search for + targetUser := &model.User{ + Id: "user-target-123", + Username: "targetuser", + Email: "target@example.com", + FirstName: "Target", + LastName: "User", + Roles: "system_user", + } + mock.users[targetUser.Id] = targetUser + + // Step 1: Search for the user + searchReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: user.SearchUsersToolName, + Arguments: map[string]interface{}{ + "term": "targetuser", + "team_id": "team-1", + "limit": 5, + }, + }, + } + + searchResult, err := user.SearchUsersFn(context.Background(), searchReq) + require.NoError(t, err) + require.NotNil(t, searchResult) + require.False(t, searchResult.IsError) + + var searchData map[string]interface{} + err = json.Unmarshal([]byte(searchResult.Content[0].(mcp.TextContent).Text), &searchData) + require.NoError(t, err) + + // Step 2: Create DM with found user + foundUsers := searchData["users"].([]interface{}) + require.GreaterOrEqual(t, len(foundUsers), 1) + + foundUser := foundUsers[0].(map[string]interface{}) + userID := foundUser["id"].(string) + + dmReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": userID, + }, + }, + } + + dmResult, err := dm.CreateDMFn(context.Background(), dmReq) + require.NoError(t, err) + require.NotNil(t, dmResult) + require.False(t, dmResult.IsError) + + var dmData map[string]interface{} + err = json.Unmarshal([]byte(dmResult.Content[0].(mcp.TextContent).Text), &dmData) + require.NoError(t, err) + + channelID := dmData["id"].(string) + + // Step 3: Send message in DM + msgReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": channelID, + "message": "Message to target user", + }, + }, + } + + msgResult, err := messaging.SendMessageFn(context.Background(), msgReq) + require.NoError(t, err) + require.NotNil(t, msgResult) + require.False(t, msgResult.IsError) + + var msgData map[string]interface{} + err = json.Unmarshal([]byte(msgResult.Content[0].(mcp.TextContent).Text), &msgData) + require.NoError(t, err) + + assert.Equal(t, channelID, msgData["channel_id"]) + assert.Equal(t, "Message to target user", msgData["message"]) +} + +// ==================== Read-Only Mode Tests ==================== + +func TestIntegration_ReadOnlyMode_WriteToolsExcluded(t *testing.T) { + // Save original read-only setting + originalReadOnly := flag.ReadOnly + defer func() { + flag.ReadOnly = originalReadOnly + }() + + // Test with read-only = false (full access) + flag.ReadOnly = false + allTools := Register() + + // Count tools + writeToolCount := 0 + readToolCount := 0 + for _, tool := range allTools { + switch tool.Tool.Name { + case messaging.SendMessageToolName, messaging.EditMessageToolName, messaging.DeleteMessageToolName, + dm.CreateDMToolName, reaction.AddReactionToolName, reaction.RemoveReactionToolName, + file.UploadFileToolName, file.DownloadFileToolName: + writeToolCount++ + case channel.ListChannelsToolName, user.SearchUsersToolName, + team.ListTeamsToolName, messaging.GetChannelMessagesToolName, messaging.GetThreadToolName, + messaging.SearchPostsToolName, user.GetUserStatusToolName, + channel.GetChannelByNameToolName, channel.GetUnreadCountToolName, channel.MarkChannelReadToolName: + readToolCount++ + } + } + + assert.Greater(t, writeToolCount, 0, "Should have write tools in non-read-only mode") + assert.Greater(t, readToolCount, 0, "Should have read tools in non-read-only mode") + + // Test with read-only = true (read-only access) + flag.ReadOnly = true + readOnlyTools := Register() + + // In read-only mode, only read tools should be present + for _, tool := range readOnlyTools { + switch tool.Tool.Name { + case messaging.SendMessageToolName, messaging.EditMessageToolName, messaging.DeleteMessageToolName, + dm.CreateDMToolName, reaction.AddReactionToolName, reaction.RemoveReactionToolName: + t.Errorf("Write tool %s should not be present in read-only mode", tool.Tool.Name) + } + } + + // Verify read tools are still present + foundReadTools := 0 + for _, tool := range readOnlyTools { + switch tool.Tool.Name { + case channel.ListChannelsToolName, user.SearchUsersToolName, + team.ListTeamsToolName, messaging.GetChannelMessagesToolName, messaging.GetThreadToolName, + messaging.SearchPostsToolName, user.GetUserStatusToolName, + channel.GetChannelByNameToolName, channel.GetUnreadCountToolName, channel.MarkChannelReadToolName: + foundReadTools++ + } + } + + assert.Greater(t, foundReadTools, 0, "Read tools should be present in read-only mode") +} + +func TestIntegration_ReadOnlyMode_ToolCountVerification(t *testing.T) { + originalReadOnly := flag.ReadOnly + defer func() { + flag.ReadOnly = originalReadOnly + }() + + // Full mode - should have all tools + flag.ReadOnly = false + fullTools := Register() + fullCount := len(fullTools) + + // Read-only mode - should have only read tools + flag.ReadOnly = true + readOnlyTools := Register() + readOnlyCount := len(readOnlyTools) + + // Read-only should have fewer tools + assert.Less(t, readOnlyCount, fullCount, "Read-only mode should have fewer tools than full mode") + + // Verify the difference is the count of write tools + flag.ReadOnly = false + tools := Register() + writeCount := 0 + readCount := 0 + for _, tool := range tools { + switch tool.Tool.Name { + case messaging.SendMessageToolName, messaging.EditMessageToolName, messaging.DeleteMessageToolName, + dm.CreateDMToolName, reaction.AddReactionToolName, reaction.RemoveReactionToolName, + file.UploadFileToolName, file.DownloadFileToolName, + channel.CreateChannelToolName, channel.InviteToChannelToolName, channel.DeleteChannelToolName, channel.LeaveChannelToolName: + writeCount++ + case channel.ListChannelsToolName, user.SearchUsersToolName, + team.ListTeamsToolName, messaging.GetChannelMessagesToolName, messaging.GetThreadToolName, + messaging.SearchPostsToolName, user.GetUserStatusToolName, + channel.GetChannelByNameToolName, channel.GetUnreadCountToolName, channel.MarkChannelReadToolName, + channel.GetChannelInfoToolName: + readCount++ + } + } + + assert.Equal(t, fullCount, writeCount+readCount, "Total tools should equal write + read tools") + assert.Equal(t, readCount, readOnlyCount, "Read-only mode should only expose read tools") +} + +// ==================== Error Propagation Tests ==================== + +func TestIntegration_ErrorPropagation_ClientNotInitialized(t *testing.T) { + // Ensure no client is set + mattermost.SetGlobalClient(nil) + + testCases := []struct { + name string + toolName string + handler func(interface{}, mcp.CallToolRequest) (*mcp.CallToolResult, error) + request mcp.CallToolRequest + }{ + { + name: "SendMessage", + toolName: messaging.SendMessageToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return messaging.SendMessageFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "message": "test", + }, + }, + }, + }, + { + name: "EditMessage", + toolName: messaging.EditMessageToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return messaging.EditMessageFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.EditMessageToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + "message": "test", + }, + }, + }, + }, + { + name: "DeleteMessage", + toolName: messaging.DeleteMessageToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return messaging.DeleteMessageFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.DeleteMessageToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + }, + }, + }, + }, + { + name: "CreateDM", + toolName: dm.CreateDMToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return dm.CreateDMFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": "user123", + }, + }, + }, + }, + { + name: "AddReaction", + toolName: reaction.AddReactionToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return reaction.AddReactionFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.AddReactionToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + "emoji_name": "thumbsup", + }, + }, + }, + }, + { + name: "RemoveReaction", + toolName: reaction.RemoveReactionToolName, + handler: func(_ interface{}, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + return reaction.RemoveReactionFn(context.Background(), req) + }, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.RemoveReactionToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + "emoji_name": "thumbsup", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tc.handler(nil, tc.request) + assert.NoError(t, err) // Handler should not return error, just error result + assert.NotNil(t, result) + assert.True(t, result.IsError, "Should return error when client not initialized") + + // Verify error message contains expected text + errText := result.Content[0].(mcp.TextContent).Text + assert.Contains(t, errText, "client not initialized") + }) + } +} + +func TestIntegration_ErrorPropagation_InvalidParams(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + testCases := []struct { + name string + toolName string + handler func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) + request mcp.CallToolRequest + expectedErr string + }{ + { + name: "SendMessage_MissingChannelId", + toolName: messaging.SendMessageToolName, + handler: messaging.SendMessageFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "message": "test message", + }, + }, + }, + expectedErr: "channel_id", + }, + { + name: "SendMessage_MissingMessage", + toolName: messaging.SendMessageToolName, + handler: messaging.SendMessageFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + }, + }, + }, + expectedErr: "message", + }, + { + name: "EditMessage_MissingPostId", + toolName: messaging.EditMessageToolName, + handler: messaging.EditMessageFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.EditMessageToolName, + Arguments: map[string]interface{}{ + "message": "updated", + }, + }, + }, + expectedErr: "post_id", + }, + { + name: "DeleteMessage_MissingPostId", + toolName: messaging.DeleteMessageToolName, + handler: messaging.DeleteMessageFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.DeleteMessageToolName, + Arguments: map[string]interface{}{}, + }, + }, + expectedErr: "post_id", + }, + { + name: "CreateDM_MissingUserId", + toolName: dm.CreateDMToolName, + handler: dm.CreateDMFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{}, + }, + }, + expectedErr: "user_id", + }, + { + name: "AddReaction_MissingPostId", + toolName: reaction.AddReactionToolName, + handler: reaction.AddReactionFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.AddReactionToolName, + Arguments: map[string]interface{}{ + "emoji_name": "thumbsup", + }, + }, + }, + expectedErr: "post_id", + }, + { + name: "AddReaction_MissingEmojiName", + toolName: reaction.AddReactionToolName, + handler: reaction.AddReactionFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.AddReactionToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + }, + }, + }, + expectedErr: "emoji_name", + }, + { + name: "ListChannels_MissingTeamId", + toolName: channel.ListChannelsToolName, + handler: channel.ListChannelsFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: channel.ListChannelsToolName, + Arguments: map[string]interface{}{}, + }, + }, + expectedErr: "team_id", + }, + { + name: "SearchUsers_MissingTerm", + toolName: user.SearchUsersToolName, + handler: user.SearchUsersFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: user.SearchUsersToolName, + Arguments: map[string]interface{}{}, + }, + }, + expectedErr: "term", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tc.handler(nil, tc.request) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError, "Should return error for missing required parameter") + + errText := result.Content[0].(mcp.TextContent).Text + assert.Contains(t, errText, tc.expectedErr) + }) + } +} + +// ==================== Enhanced Error Tests ==================== + +func TestIntegration_ErrorEnhancedError_Translation(t *testing.T) { + testCases := []struct { + name string + err error + context map[string]string + expectedCategory mmerrors.ErrorCategory + isNotFound bool + isAuthError bool + }{ + { + name: "404 not found error", + err: fmt.Errorf("request failed with status 404: channel not found"), + context: map[string]string{"operation": "GetChannel", "channel_id": "abc123"}, + expectedCategory: mmerrors.CategoryUnknown, + isNotFound: true, + }, + { + name: "401 authentication error", + err: fmt.Errorf("request failed with status 401: unauthorized"), + context: map[string]string{"operation": "GetUser"}, + expectedCategory: mmerrors.CategoryAuth, + isAuthError: true, + }, + { + name: "403 permission denied", + err: fmt.Errorf("request failed with status 403: forbidden"), + context: map[string]string{"operation": "CreatePost"}, + expectedCategory: mmerrors.CategoryAuth, + isAuthError: true, + }, + { + name: "500 server error", + err: fmt.Errorf("request failed with status 500: internal server error"), + context: map[string]string{"operation": "SearchUsers"}, + expectedCategory: mmerrors.CategoryUnknown, + }, + { + name: "Network timeout error", + err: fmt.Errorf("request timeout: connection to server timed out"), + context: map[string]string{"operation": "SendMessage"}, + expectedCategory: mmerrors.CategoryNetwork, + }, + { + name: "Connection refused error", + err: fmt.Errorf("connection refused to mattermost.example.com:443"), + context: map[string]string{"operation": "GetMe"}, + expectedCategory: mmerrors.CategoryNetwork, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + translated := mmerrors.TranslateError(tc.err, tc.context) + require.NotNil(t, translated) + + var enhanced *mmerrors.EnhancedError + require.True(t, errors.As(translated, &enhanced), "Should be an EnhancedError") + + assert.Equal(t, tc.expectedCategory, enhanced.Category) + assert.Equal(t, tc.isNotFound, mmerrors.IsNotFound(translated)) + assert.Equal(t, tc.isAuthError, mmerrors.IsAuthError(translated)) + + // Verify context is preserved + for key, value := range tc.context { + assert.Equal(t, value, enhanced.Context[key]) + } + + // Verify formatting works + formatted := enhanced.Format() + assert.NotEmpty(t, formatted) + assert.Contains(t, formatted, tc.context["operation"]) + + detailed := enhanced.FormatDetailed() + assert.NotEmpty(t, detailed) + }) + } +} + +func TestIntegration_ErrorEnhancedError_NilHandling(t *testing.T) { + // Test that nil errors are handled properly + assert.Nil(t, mmerrors.TranslateError(nil, nil)) + assert.Nil(t, mmerrors.Wrap(nil, "operation")) + + // Test that nil error checking functions return false + assert.False(t, mmerrors.IsNotFound(nil)) + assert.False(t, mmerrors.IsAuthError(nil)) + assert.False(t, mmerrors.IsTimeout(nil)) + assert.False(t, mmerrors.IsNetworkError(nil)) + assert.False(t, mmerrors.IsServerError(nil)) +} + +func TestIntegration_ErrorEnhancedError_FluentAPI(t *testing.T) { + original := fmt.Errorf("original error") + + enhanced := mmerrors.TranslateError(original, nil).(*mmerrors.EnhancedError) + + // Test fluent API chaining + result := enhanced. + WithOperation("TestOperation"). + WithParam("param1", "value1"). + WithParam("param2", "value2"). + WithContext("extra", "context") + + // Should return same instance for chaining + assert.Equal(t, enhanced, result) + + // Verify all values set + assert.Equal(t, "TestOperation", enhanced.Operation) + assert.Equal(t, "value1", enhanced.Context["param1"]) + assert.Equal(t, "value2", enhanced.Context["param2"]) + assert.Equal(t, "context", enhanced.Context["extra"]) + + // Verify original is preserved + assert.Equal(t, original, enhanced.Unwrap()) +} + +func TestIntegration_ErrorHTTPStatusCodes(t *testing.T) { + testCases := []struct { + name string + err error + isUnauthorized bool + isForbidden bool + isNotFound bool + isServerError bool + }{ + { + name: "401 Unauthorized", + err: &testHTTPError{status: 401, message: "unauthorized"}, + isUnauthorized: true, + isForbidden: false, + isNotFound: false, + isServerError: false, + }, + { + name: "403 Forbidden", + err: &testHTTPError{status: 403, message: "forbidden"}, + isUnauthorized: false, + isForbidden: true, + isNotFound: false, + isServerError: false, + }, + { + name: "404 Not Found", + err: &testHTTPError{status: 404, message: "not found"}, + isUnauthorized: false, + isForbidden: false, + isNotFound: true, + isServerError: false, + }, + { + name: "500 Internal Server Error", + err: &testHTTPError{status: 500, message: "internal error"}, + isUnauthorized: false, + isForbidden: false, + isNotFound: false, + isServerError: true, + }, + { + name: "502 Bad Gateway", + err: &testHTTPError{status: 502, message: "bad gateway"}, + isUnauthorized: false, + isForbidden: false, + isNotFound: false, + isServerError: true, + }, + { + name: "503 Service Unavailable", + err: &testHTTPError{status: 503, message: "service unavailable"}, + isUnauthorized: false, + isForbidden: false, + isNotFound: false, + isServerError: true, + }, + { + name: "200 OK", + err: &testHTTPError{status: 200, message: "ok"}, + isUnauthorized: false, + isForbidden: false, + isNotFound: false, + isServerError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.isUnauthorized, mmerrors.IsUnauthorized(tc.err)) + assert.Equal(t, tc.isForbidden, mmerrors.IsForbidden(tc.err)) + assert.Equal(t, tc.isNotFound, mmerrors.IsNotFoundHTTP(tc.err)) + assert.Equal(t, tc.isServerError, mmerrors.IsServerError(tc.err)) + }) + } +} + +// testHTTPError implements HTTPError interface for testing +type testHTTPError struct { + status int + message string +} + +func (e *testHTTPError) Error() string { return e.message } +func (e *testHTTPError) Status() int { return e.status } + +// ==================== Cross-Tool Error Consistency Tests ==================== + +func TestIntegration_CrossToolErrorConsistency(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + // Start server but return errors to test error handling + originalHandler := mock.server.Config.Handler + mock.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate various error conditions + switch r.URL.Path { + case "/api/v4/users/me": + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"message": "authentication failed"}) + case "/api/v4/posts": + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"message": "permission denied"}) + case "/api/v4/channels/direct": + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "user not found"}) + default: + originalHandler.ServeHTTP(w, r) + } + }) + + setupMockClient(mock) + + testCases := []struct { + name string + toolName string + handler func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) + request mcp.CallToolRequest + expectedError bool + errorCheck func(error) bool + }{ + { + name: "DM Creation - Auth Error", + toolName: dm.CreateDMToolName, + handler: dm.CreateDMFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": "user123", + }, + }, + }, + expectedError: true, + }, + { + name: "Send Message - Forbidden", + toolName: messaging.SendMessageToolName, + handler: messaging.SendMessageFn, + request: mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "message": "test", + }, + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := tc.handler(context.Background(), tc.request) + assert.NoError(t, err) // Handler shouldn't return error + assert.NotNil(t, result) + assert.True(t, result.IsError, "Should return error result") + }) + } +} + +// ==================== End-to-End Complex Workflow Tests ==================== + +func TestIntegration_ComplexWorkflow_MultiStepWithRollback(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + // Complex workflow: Search user → Create DM → Send message → Add reaction → Remove reaction → Delete message + + // Step 1: Search for user + searchReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: user.SearchUsersToolName, + Arguments: map[string]interface{}{ + "term": "workflowuser", + "limit": 1, + }, + }, + } + + searchResult, err := user.SearchUsersFn(context.Background(), searchReq) + require.NoError(t, err) + require.False(t, searchResult.IsError) + + var searchData map[string]interface{} + json.Unmarshal([]byte(searchResult.Content[0].(mcp.TextContent).Text), &searchData) + users := searchData["users"].([]interface{}) + require.GreaterOrEqual(t, len(users), 1) + userID := users[0].(map[string]interface{})["id"].(string) + + // Step 2: Create DM + dmReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": userID, + }, + }, + } + + dmResult, err := dm.CreateDMFn(context.Background(), dmReq) + require.NoError(t, err) + require.False(t, dmResult.IsError) + + var dmData map[string]interface{} + json.Unmarshal([]byte(dmResult.Content[0].(mcp.TextContent).Text), &dmData) + channelID := dmData["id"].(string) + + // Step 3: Send message + msgReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": channelID, + "message": "Complex workflow test", + }, + }, + } + + msgResult, err := messaging.SendMessageFn(context.Background(), msgReq) + require.NoError(t, err) + require.False(t, msgResult.IsError) + + var msgData map[string]interface{} + json.Unmarshal([]byte(msgResult.Content[0].(mcp.TextContent).Text), &msgData) + postID := msgData["id"].(string) + + // Step 4: Add reaction + addReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.AddReactionToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + "emoji_name": "heart", + }, + }, + } + + addResult, err := reaction.AddReactionFn(context.Background(), addReq) + require.NoError(t, err) + require.False(t, addResult.IsError) + + // Step 5: Remove reaction + removeReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: reaction.RemoveReactionToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + "emoji_name": "heart", + }, + }, + } + + removeResult, err := reaction.RemoveReactionFn(context.Background(), removeReq) + require.NoError(t, err) + require.False(t, removeResult.IsError) + + var removeData map[string]interface{} + json.Unmarshal([]byte(removeResult.Content[0].(mcp.TextContent).Text), &removeData) + assert.True(t, removeData["success"].(bool)) + assert.Equal(t, postID, removeData["post_id"]) + + // Step 6: Delete message (cleanup) + deleteReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: messaging.DeleteMessageToolName, + Arguments: map[string]interface{}{ + "post_id": postID, + }, + }, + } + + deleteResult, err := messaging.DeleteMessageFn(context.Background(), deleteReq) + require.NoError(t, err) + require.False(t, deleteResult.IsError) + + var deleteData map[string]interface{} + json.Unmarshal([]byte(deleteResult.Content[0].(mcp.TextContent).Text), &deleteData) + assert.True(t, deleteData["success"].(bool)) +} + +func TestIntegration_WorkflowErrorChaining_FirstFailureStops(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + // Setup mock to fail on DM creation + mock.server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v4/users/me": + user := &model.User{ + Id: "user-current", + Username: "currentuser", + Email: "current@example.com", + FirstName: "Current", + LastName: "User", + Roles: "system_user", + } + json.NewEncoder(w).Encode(user) + case "/api/v4/channels/direct": + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"message": "cannot create DM with yourself"}) + default: + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"message": "not found"}) + } + }) + + setupMockClient(mock) + + // Try to create DM with ourselves (which will fail) + dmReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: dm.CreateDMToolName, + Arguments: map[string]interface{}{ + "user_id": "user-current", // Same as current user + }, + }, + } + + // This should fail + result, err := dm.CreateDMFn(context.Background(), dmReq) + require.NoError(t, err) + require.NotNil(t, result) + require.True(t, result.IsError, "DM creation should fail") + + // Verify the error message is descriptive + errText := result.Content[0].(mcp.TextContent).Text + assert.NotEmpty(t, errText) +} + +func TestDebugChannelList(t *testing.T) { + mock := newMockMattermostServer() + defer mock.Close() + + setupMockClient(mock) + + listReq := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: channel.ListChannelsToolName, + Arguments: map[string]interface{}{ + "team_id": "team-1", + "limit": 10, + }, + }, + } + + listResult, err := channel.ListChannelsFn(context.Background(), listReq) + require.NoError(t, err) + require.NotNil(t, listResult) + + if listResult.IsError { + t.Fatalf("Error result: %s", listResult.Content[0].(mcp.TextContent).Text) + } +} diff --git a/operation/messaging/messages.go b/operation/messaging/messages.go new file mode 100644 index 0000000..8e771cc --- /dev/null +++ b/operation/messaging/messages.go @@ -0,0 +1,85 @@ +package messaging + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetChannelMessagesToolName = "mattermost_get_channel_messages" +) + +var ( + GetChannelMessagesTool = mcp.NewTool( + GetChannelMessagesToolName, + mcp.WithDescription("Read message history from a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to read from")), + mcp.WithNumber("limit", mcp.Description("Number of messages to return (default 60, max 200)")), + mcp.WithString("before", mcp.Description("Get messages before this post ID (for pagination)")), + mcp.WithString("after", mcp.Description("Get messages after this post ID (for pagination)")), + ) +) + +func init() { + registerGetChannelMessagesTool() +} + +func registerGetChannelMessagesTool() { + tools := []server.ServerTool{ + {Tool: GetChannelMessagesTool, Handler: GetChannelMessagesFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func GetChannelMessagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called GetChannelMessagesFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + limit := params.GetOptionalInt(args, "limit", 60) + if limit > 200 { + limit = 200 + } + if limit < 1 { + limit = 60 + } + + before := params.GetOptionalString(args, "before", "") + after := params.GetOptionalString(args, "after", "") + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + posts, err := client.GetChannelPosts(ctx, channelID, int(limit), before, after) + if err != nil { + return to.Error(fmt.Errorf("[posts] failed to get channel messages: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(posts.Posts)) + for _, post := range posts.Posts { + results = append(results, SlimPost(post)) + } + + return to.Result(map[string]interface{}{ + "posts": results, + "count": len(results), + "channel_id": channelID, + "has_more": len(results) == int(limit), + }), nil +} diff --git a/operation/messaging/messaging.go b/operation/messaging/messaging.go new file mode 100644 index 0000000..e43ca90 --- /dev/null +++ b/operation/messaging/messaging.go @@ -0,0 +1,369 @@ +package messaging + +import ( + "context" + "fmt" + "strings" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mattermost/mattermost-server/v6/model" +) + +var Tool = tool.New() + +const ( + SendMessageToolName = "mattermost_send_message" + EditMessageToolName = "mattermost_edit_message" + DeleteMessageToolName = "mattermost_delete_message" + BulkDeleteMessagesToolName = "mattermost_bulk_delete_messages" + PinPostToolName = "mattermost_pin_post" + UnpinPostToolName = "mattermost_unpin_post" + GetPinnedPostsToolName = "mattermost_get_pinned_posts" + GetPostToolName = "mattermost_get_post" +) + +var ( + SendMessageTool = mcp.NewTool( + SendMessageToolName, + mcp.WithDescription("Send a message to a Mattermost channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to send the message to")), + mcp.WithString("message", mcp.Required(), mcp.Description("Message content to send")), + mcp.WithString("thread_id", mcp.Description("Thread ID for replies (optional). If provided, message will be posted as a reply in the thread")), + ) + + EditMessageTool = mcp.NewTool( + EditMessageToolName, + mcp.WithDescription("Edit an existing message in Mattermost"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID of the message to edit")), + mcp.WithString("message", mcp.Required(), mcp.Description("New message content")), + ) + + DeleteMessageTool = mcp.NewTool( + DeleteMessageToolName, + mcp.WithDescription("Delete a message from Mattermost"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID of the message to delete")), + ) + + BulkDeleteMessagesTool = mcp.NewTool( + BulkDeleteMessagesToolName, + mcp.WithDescription("Delete multiple messages at once (up to 100)"), + mcp.WithString("post_ids", mcp.Required(), mcp.Description("Comma-separated list of post IDs to delete (max 100)")), + ) + + PinPostTool = mcp.NewTool( + PinPostToolName, + mcp.WithDescription("Pin a post to a channel"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to pin")), + ) + + UnpinPostTool = mcp.NewTool( + UnpinPostToolName, + mcp.WithDescription("Unpin a post from a channel"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to unpin")), + ) + + GetPinnedPostsTool = mcp.NewTool( + GetPinnedPostsToolName, + mcp.WithDescription("Get all pinned posts in a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to get pinned posts from")), + ) + + GetPostTool = mcp.NewTool( + GetPostToolName, + mcp.WithDescription("Get a single post by its ID"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to retrieve")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: SendMessageTool, Handler: SendMessageFn}, + {Tool: EditMessageTool, Handler: EditMessageFn}, + {Tool: DeleteMessageTool, Handler: DeleteMessageFn}, + {Tool: BulkDeleteMessagesTool, Handler: BulkDeleteMessagesFn}, + {Tool: PinPostTool, Handler: PinPostFn}, + {Tool: UnpinPostTool, Handler: UnpinPostFn}, + {Tool: GetPinnedPostsTool, Handler: GetPinnedPostsFn}, + {Tool: GetPostTool, Handler: GetPostFn}, + } + for _, t := range tools { + if t.Tool.Name == GetPinnedPostsToolName || t.Tool.Name == GetPostToolName { + Tool.RegisterRead(t) + } else { + Tool.RegisterWrite(t) + } + } +} + +func SendMessageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called SendMessageFn") + + args := req.GetArguments() + + channelId, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + message, err := params.GetString(args, "message") + if err != nil { + return to.Error(fmt.Errorf("[message] %v", err)), nil + } + + threadId := params.GetOptionalString(args, "thread_id", "") + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + post := &model.Post{ + ChannelId: channelId, + Message: message, + } + + if threadId != "" { + post.RootId = threadId + } + + result, err := client.CreatePost(ctx, post) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to send message: %v", err)), nil + } + + return to.Result(SlimPost(result)), nil +} + +func EditMessageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called EditMessageFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + message, err := params.GetString(args, "message") + if err != nil { + return to.Error(fmt.Errorf("[message] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + post := &model.Post{ + Message: message, + } + + result, err := client.UpdatePost(ctx, postId, post) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to edit message: %v", err)), nil + } + + return to.Result(SlimPost(result)), nil +} + +func DeleteMessageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called DeleteMessageFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.DeletePost(ctx, postId) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to delete message: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "post_id": postId, + "message": "Message deleted successfully", + }), nil +} + +func BulkDeleteMessagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called BulkDeleteMessagesFn") + + args := req.GetArguments() + + postIDsStr, err := params.GetString(args, "post_ids") + if err != nil { + return to.Error(fmt.Errorf("[post_ids] %v", err)), nil + } + + postIDs := strings.Split(postIDsStr, ",") + if len(postIDs) > 100 { + return to.Error(fmt.Errorf("[post_ids] too many post IDs (max 100, got %d)", len(postIDs))), nil + } + + for i, id := range postIDs { + postIDs[i] = strings.TrimSpace(id) + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + deleted := 0 + failed := 0 + var failedIDs []string + var lastError string + + for _, postID := range postIDs { + err := client.DeletePost(ctx, postID) + if err != nil { + failed++ + failedIDs = append(failedIDs, postID) + lastError = err.Error() + } else { + deleted++ + } + } + + result := map[string]interface{}{ + "total": len(postIDs), + "deleted": deleted, + "failed": failed, + "successful": deleted == len(postIDs), + } + + if failed > 0 { + result["failed_ids"] = failedIDs + result["last_error"] = lastError + return to.Result(result), nil + } + + return to.Result(result), nil +} + +func PinPostFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called PinPostFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.PinPost(ctx, postId) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to pin: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "post_id": postId, + "message": "Post pinned successfully", + }), nil +} + +func UnpinPostFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called UnpinPostFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.UnpinPost(ctx, postId) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to unpin: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "post_id": postId, + "message": "Post unpinned successfully", + }), nil +} + +func GetPinnedPostsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called GetPinnedPostsFn") + + args := req.GetArguments() + + channelId, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + posts, err := client.GetPinnedPosts(ctx, channelId) + if err != nil { + return to.Error(fmt.Errorf("[channel] failed to get pinned posts: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(posts.Posts)) + for _, post := range posts.Posts { + results = append(results, SlimPost(post)) + } + + return to.Result(map[string]interface{}{ + "posts": results, + "count": len(results), + "channel_id": channelId, + }), nil +} + +func GetPostFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called GetPostFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + post, err := client.GetPost(ctx, postId) + if err != nil { + return to.Error(fmt.Errorf("[post] failed to get: %v", err)), nil + } + + return to.Result(SlimPost(post)), nil +} diff --git a/operation/messaging/messaging_test.go b/operation/messaging/messaging_test.go new file mode 100644 index 0000000..cb66b62 --- /dev/null +++ b/operation/messaging/messaging_test.go @@ -0,0 +1,192 @@ +package messaging + +import ( + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimPost(t *testing.T) { + p := &model.Post{ + Id: "abc123", + ChannelId: "channel456", + UserId: "user789", + Message: "Hello World", + CreateAt: 1234567890000, + UpdateAt: 1234567890001, + } + + slim := SlimPost(p) + assert.NotNil(t, slim) + assert.Equal(t, "abc123", slim["id"]) + assert.Equal(t, "channel456", slim["channel_id"]) + assert.Equal(t, "user789", slim["user_id"]) + assert.Equal(t, "Hello World", slim["message"]) + assert.Equal(t, int64(1234567890000), slim["create_at"]) + assert.Equal(t, int64(1234567890001), slim["update_at"]) +} + +func TestSlimPost_Nil(t *testing.T) { + slim := SlimPost(nil) + assert.Nil(t, slim) +} + +func TestToolRegistration(t *testing.T) { + writeTools := Tool.WriteTools() + assert.Len(t, writeTools, 3) + + readTools := Tool.ReadTools() + assert.Len(t, readTools, 3) + + toolNames := make(map[string]bool) + for _, t := range writeTools { + toolNames[t.Tool.Name] = true + } + for _, t := range readTools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[SendMessageToolName], "SendMessage tool should be registered") + assert.True(t, toolNames[EditMessageToolName], "EditMessage tool should be registered") + assert.True(t, toolNames[DeleteMessageToolName], "DeleteMessage tool should be registered") + assert.True(t, toolNames[GetChannelMessagesToolName], "GetChannelMessages tool should be registered") + assert.True(t, toolNames[GetThreadToolName], "GetThread tool should be registered") + assert.True(t, toolNames[SearchPostsToolName], "SearchPosts tool should be registered") +} + +func TestSendMessageFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + "message": "Test message", + }, + }, + } + + result, err := SendMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestEditMessageFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: EditMessageToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + "message": "Updated message", + }, + }, + } + + result, err := EditMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestDeleteMessageFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DeleteMessageToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + }, + }, + } + + result, err := DeleteMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestSendMessageFn_MissingChannelId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: SendMessageToolName, + Arguments: map[string]interface{}{ + "message": "Test message", + }, + }, + } + + result, err := SendMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestSendMessageFn_MissingMessage(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: SendMessageToolName, + Arguments: map[string]interface{}{ + "channel_id": "channel123", + }, + }, + } + + result, err := SendMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestEditMessageFn_MissingPostId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: EditMessageToolName, + Arguments: map[string]interface{}{ + "message": "Updated message", + }, + }, + } + + result, err := EditMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestEditMessageFn_MissingMessage(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: EditMessageToolName, + Arguments: map[string]interface{}{ + "post_id": "post123", + }, + }, + } + + result, err := EditMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestDeleteMessageFn_MissingPostId(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: DeleteMessageToolName, + Arguments: map[string]interface{}{}, + }, + } + + result, err := DeleteMessageFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} diff --git a/operation/messaging/search.go b/operation/messaging/search.go new file mode 100644 index 0000000..de28965 --- /dev/null +++ b/operation/messaging/search.go @@ -0,0 +1,85 @@ +package messaging + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + SearchPostsToolName = "mattermost_search_posts" +) + +var ( + SearchPostsTool = mcp.NewTool( + SearchPostsToolName, + mcp.WithDescription("Search for posts/messages in a team using search terms"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to search in")), + mcp.WithString("terms", mcp.Required(), mcp.Description("Search terms (e.g., \"BTCUSD\", \"error\", \"meeting\")")), + mcp.WithBoolean("is_or_search", mcp.Description("Use OR logic instead of AND (default: false)")), + mcp.WithNumber("limit", mcp.Description("Maximum number of results to return (default 30)")), + ) +) + +func init() { + registerSearchTools() +} + +func registerSearchTools() { + tools := []server.ServerTool{ + {Tool: SearchPostsTool, Handler: SearchPostsFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func SearchPostsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called SearchPostsFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + terms, err := params.GetString(args, "terms") + if err != nil { + return to.Error(fmt.Errorf("[terms] %v", err)), nil + } + + isOrSearch := params.GetOptionalBool(args, "is_or_search", false) + limit := params.GetOptionalInt(args, "limit", 30) + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + results, err := client.SearchPosts(ctx, teamID, terms, isOrSearch) + if err != nil { + return to.Error(fmt.Errorf("[search] failed to search posts: %v", err)), nil + } + + posts := results.ToSlice() + if len(posts) > int(limit) { + posts = posts[:limit] + } + + postResults := make([]map[string]interface{}, 0, len(posts)) + for _, post := range posts { + postResults = append(postResults, SlimPost(post)) + } + + return to.Result(map[string]interface{}{ + "posts": postResults, + "count": len(postResults), + }), nil +} diff --git a/operation/messaging/slim.go b/operation/messaging/slim.go new file mode 100644 index 0000000..c5c6571 --- /dev/null +++ b/operation/messaging/slim.go @@ -0,0 +1,17 @@ +package messaging + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimPost(p *model.Post) map[string]interface{} { + if p == nil { + return nil + } + return map[string]interface{}{ + "id": p.Id, + "channel_id": p.ChannelId, + "user_id": p.UserId, + "message": p.Message, + "create_at": p.CreateAt, + "update_at": p.UpdateAt, + } +} diff --git a/operation/messaging/thread.go b/operation/messaging/thread.go new file mode 100644 index 0000000..1a75070 --- /dev/null +++ b/operation/messaging/thread.go @@ -0,0 +1,71 @@ +package messaging + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetThreadToolName = "mattermost_get_thread" +) + +var ( + GetThreadTool = mcp.NewTool( + GetThreadToolName, + mcp.WithDescription("Read all messages in a thread conversation"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("The root post ID of the thread")), + ) +) + +func init() { + registerGetThreadTool() +} + +func registerGetThreadTool() { + tools := []server.ServerTool{ + {Tool: GetThreadTool, Handler: GetThreadFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func GetThreadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Messaging] Called GetThreadFn") + + args := req.GetArguments() + + postID, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + thread, err := client.GetPostThread(ctx, postID) + if err != nil { + return to.Error(fmt.Errorf("[thread] failed to get thread: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(thread.Posts)) + for _, post := range thread.Posts { + results = append(results, SlimPost(post)) + } + + return to.Result(map[string]interface{}{ + "posts": results, + "count": len(results), + "root_id": postID, + "has_replies": len(results) > 1, + }), nil +} diff --git a/operation/operation.go b/operation/operation.go new file mode 100644 index 0000000..9d6df98 --- /dev/null +++ b/operation/operation.go @@ -0,0 +1,99 @@ +package operation + +import ( + "github.com/karti-ai/mattermost-mcp-server/operation/channel" + "github.com/karti-ai/mattermost-mcp-server/operation/command" + "github.com/karti-ai/mattermost-mcp-server/operation/dm" + "github.com/karti-ai/mattermost-mcp-server/operation/file" + "github.com/karti-ai/mattermost-mcp-server/operation/messaging" + "github.com/karti-ai/mattermost-mcp-server/operation/outgoing" + "github.com/karti-ai/mattermost-mcp-server/operation/reaction" + "github.com/karti-ai/mattermost-mcp-server/operation/system" + "github.com/karti-ai/mattermost-mcp-server/operation/team" + "github.com/karti-ai/mattermost-mcp-server/operation/user" + "github.com/karti-ai/mattermost-mcp-server/operation/webhook" + "github.com/karti-ai/mattermost-mcp-server/pkg/flag" + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/server" +) + +func Register() []server.ServerTool { + log.Infof("Registering tools for Mattermost MCP server %s", flag.Version) + + tools := tool.New() + + for _, t := range messaging.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range file.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range dm.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range reaction.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range channel.AdminTool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range messaging.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range file.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range channel.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range user.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range team.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range dm.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range reaction.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range webhook.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range webhook.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range command.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range outgoing.Tool.WriteTools() { + tools.RegisterWrite(t) + } + + for _, t := range outgoing.Tool.ReadTools() { + tools.RegisterRead(t) + } + + for _, t := range system.Tool.ReadTools() { + tools.RegisterRead(t) + } + + return tools.Tools() +} diff --git a/operation/outgoing/outgoing.go b/operation/outgoing/outgoing.go new file mode 100644 index 0000000..5cb490c --- /dev/null +++ b/operation/outgoing/outgoing.go @@ -0,0 +1,187 @@ +package outgoing + +import ( + "context" + "fmt" + "strings" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + CreateOutgoingWebhookToolName = "mattermost_create_outgoing_webhook" + ListOutgoingWebhooksToolName = "mattermost_list_outgoing_webhooks" + DeleteOutgoingWebhookToolName = "mattermost_delete_outgoing_webhook" +) + +var ( + CreateOutgoingWebhookTool = mcp.NewTool( + CreateOutgoingWebhookToolName, + mcp.WithDescription("Create an outgoing webhook that triggers on specific words"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID for the webhook")), + mcp.WithString("display_name", mcp.Required(), mcp.Description("Display name for the webhook")), + mcp.WithString("trigger_words", mcp.Required(), mcp.Description("Comma-separated list of words that trigger the webhook")), + mcp.WithString("callback_url", mcp.Required(), mcp.Description("URL to POST to when triggered")), + ) + + ListOutgoingWebhooksTool = mcp.NewTool( + ListOutgoingWebhooksToolName, + mcp.WithDescription("List outgoing webhooks for a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list webhooks for")), + mcp.WithNumber("page", mcp.Description("Page number (default 0)")), + mcp.WithNumber("per_page", mcp.Description("Items per page (default 20, max 100)")), + ) + + DeleteOutgoingWebhookTool = mcp.NewTool( + DeleteOutgoingWebhookToolName, + mcp.WithDescription("Delete an outgoing webhook"), + mcp.WithString("webhook_id", mcp.Required(), mcp.Description("Webhook ID to delete")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: CreateOutgoingWebhookTool, Handler: CreateOutgoingWebhookFn}, + {Tool: ListOutgoingWebhooksTool, Handler: ListOutgoingWebhooksFn}, + {Tool: DeleteOutgoingWebhookTool, Handler: DeleteOutgoingWebhookFn}, + } + for _, t := range tools { + if t.Tool.Name == ListOutgoingWebhooksToolName { + Tool.RegisterRead(t) + } else { + Tool.RegisterWrite(t) + } + } +} + +func CreateOutgoingWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Outgoing] Called CreateOutgoingWebhookFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + displayName, err := params.GetString(args, "display_name") + if err != nil { + return to.Error(fmt.Errorf("[display_name] %v", err)), nil + } + + triggerWordsStr, err := params.GetString(args, "trigger_words") + if err != nil { + return to.Error(fmt.Errorf("[trigger_words] %v", err)), nil + } + triggerWords := strings.Split(triggerWordsStr, ",") + for i, word := range triggerWords { + triggerWords[i] = strings.TrimSpace(word) + } + + callbackURL, err := params.GetString(args, "callback_url") + if err != nil { + return to.Error(fmt.Errorf("[callback_url] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + hook, err := client.CreateOutgoingWebhook(ctx, teamID, displayName, triggerWords, callbackURL) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to create: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "id": hook.Id, + "team_id": hook.TeamId, + "display_name": hook.DisplayName, + "trigger_words": hook.TriggerWords, + "callback_url": callbackURL, + "message": "Outgoing webhook created successfully", + }), nil +} + +func ListOutgoingWebhooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Outgoing] Called ListOutgoingWebhooksFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + page := params.GetOptionalInt(args, "page", 0) + perPage := params.GetOptionalInt(args, "per_page", 20) + if perPage > 100 { + perPage = 100 + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + hooks, err := client.ListOutgoingWebhooks(ctx, teamID, int(page), int(perPage)) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to list: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(hooks)) + for _, hook := range hooks { + results = append(results, map[string]interface{}{ + "id": hook.Id, + "team_id": hook.TeamId, + "display_name": hook.DisplayName, + "trigger_words": hook.TriggerWords, + }) + } + + return to.Result(map[string]interface{}{ + "webhooks": results, + "count": len(results), + "page": page, + "per_page": perPage, + }), nil +} + +func DeleteOutgoingWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Outgoing] Called DeleteOutgoingWebhookFn") + + args := req.GetArguments() + + webhookID, err := params.GetString(args, "webhook_id") + if err != nil { + return to.Error(fmt.Errorf("[webhook_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.DeleteOutgoingWebhook(ctx, webhookID) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to delete: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "webhook_id": webhookID, + "message": "Outgoing webhook deleted successfully", + }), nil +} diff --git a/operation/reaction/reaction.go b/operation/reaction/reaction.go new file mode 100644 index 0000000..3845cba --- /dev/null +++ b/operation/reaction/reaction.go @@ -0,0 +1,181 @@ +package reaction + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mattermost/mattermost-server/v6/model" +) + +var Tool = tool.New() + +const ( + AddReactionToolName = "mattermost_add_reaction" + RemoveReactionToolName = "mattermost_remove_reaction" + ListReactionsToolName = "mattermost_list_reactions" +) + +var ( + AddReactionTool = mcp.NewTool( + AddReactionToolName, + mcp.WithDescription("Add emoji reaction to post"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to add reaction to")), + mcp.WithString("emoji_name", mcp.Required(), mcp.Description("Emoji name without colons (e.g., thumbsup, not :thumbsup:)")), + ) + + RemoveReactionTool = mcp.NewTool( + RemoveReactionToolName, + mcp.WithDescription("Remove emoji reaction from post"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to remove reaction from")), + mcp.WithString("emoji_name", mcp.Required(), mcp.Description("Emoji name without colons")), + ) + + ListReactionsTool = mcp.NewTool( + ListReactionsToolName, + mcp.WithDescription("List all emoji reactions on a post"), + mcp.WithString("post_id", mcp.Required(), mcp.Description("Post ID to get reactions for")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: AddReactionTool, Handler: AddReactionFn}, + {Tool: RemoveReactionTool, Handler: RemoveReactionFn}, + {Tool: ListReactionsTool, Handler: ListReactionsFn}, + } + for _, t := range tools { + if t.Tool.Name == ListReactionsToolName { + Tool.RegisterRead(t) + } else { + Tool.RegisterWrite(t) + } + } +} + +func AddReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Reaction] Called AddReactionFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + emojiName, err := params.GetString(args, "emoji_name") + if err != nil { + return to.Error(fmt.Errorf("[emoji_name] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + // Get current user ID + me, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + reaction := &model.Reaction{ + UserId: me.Id, + PostId: postId, + EmojiName: emojiName, + } + + result, err := client.SaveReaction(ctx, reaction) + if err != nil { + return to.Error(fmt.Errorf("[reaction] failed to add reaction: %v", err)), nil + } + + return to.Result(SlimReaction(result)), nil +} + +func RemoveReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Reaction] Called RemoveReactionFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + emojiName, err := params.GetString(args, "emoji_name") + if err != nil { + return to.Error(fmt.Errorf("[emoji_name] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + // Get current user ID + me, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + reaction := &model.Reaction{ + UserId: me.Id, + PostId: postId, + EmojiName: emojiName, + } + + err = client.DeleteReaction(ctx, reaction) + if err != nil { + return to.Error(fmt.Errorf("[reaction] failed to remove reaction: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "post_id": postId, + "emoji_name": emojiName, + "message": "Reaction removed successfully", + }), nil +} + +func ListReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Reaction] Called ListReactionsFn") + + args := req.GetArguments() + + postId, err := params.GetString(args, "post_id") + if err != nil { + return to.Error(fmt.Errorf("[post_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + reactions, err := client.GetReactions(ctx, postId) + if err != nil { + return to.Error(fmt.Errorf("[reaction] failed to list reactions: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(reactions)) + for _, r := range reactions { + results = append(results, SlimReaction(r)) + } + + return to.Result(map[string]interface{}{ + "reactions": results, + "count": len(results), + "post_id": postId, + }), nil +} diff --git a/operation/reaction/reaction_test.go b/operation/reaction/reaction_test.go new file mode 100644 index 0000000..cd38ec6 --- /dev/null +++ b/operation/reaction/reaction_test.go @@ -0,0 +1,78 @@ +package reaction + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimReaction(t *testing.T) { + tests := []struct { + name string + reaction *model.Reaction + expected map[string]interface{} + }{ + { + name: "valid reaction", + reaction: &model.Reaction{ + UserId: "user123", + PostId: "post456", + EmojiName: "thumbsup", + CreateAt: 1234567890, + }, + expected: map[string]interface{}{ + "user_id": "user123", + "post_id": "post456", + "emoji_name": "thumbsup", + "create_at": int64(1234567890), + }, + }, + { + name: "nil reaction", + reaction: nil, + expected: nil, + }, + { + name: "reaction with different emoji", + reaction: &model.Reaction{ + UserId: "user789", + PostId: "post012", + EmojiName: "rocket", + CreateAt: 9876543210, + }, + expected: map[string]interface{}{ + "user_id": "user789", + "post_id": "post012", + "emoji_name": "rocket", + "create_at": int64(9876543210), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := SlimReaction(tt.reaction) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestToolConstants(t *testing.T) { + assert.Equal(t, "mattermost_add_reaction", AddReactionToolName) + assert.Equal(t, "mattermost_remove_reaction", RemoveReactionToolName) +} + +func TestToolRegistration(t *testing.T) { + // Verify tools are registered + tools := Tool.Tools() + assert.Len(t, tools, 2) + + toolNames := make([]string, len(tools)) + for i, t := range tools { + toolNames[i] = t.Tool.Name + } + + assert.Contains(t, toolNames, AddReactionToolName) + assert.Contains(t, toolNames, RemoveReactionToolName) +} diff --git a/operation/reaction/slim.go b/operation/reaction/slim.go new file mode 100644 index 0000000..e27cdf9 --- /dev/null +++ b/operation/reaction/slim.go @@ -0,0 +1,15 @@ +package reaction + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimReaction(r *model.Reaction) map[string]interface{} { + if r == nil { + return nil + } + return map[string]interface{}{ + "user_id": r.UserId, + "post_id": r.PostId, + "emoji_name": r.EmojiName, + "create_at": r.CreateAt, + } +} diff --git a/operation/system/system.go b/operation/system/system.go new file mode 100644 index 0000000..961f16c --- /dev/null +++ b/operation/system/system.go @@ -0,0 +1,100 @@ +package system + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + GetSystemLogsToolName = "mattermost_get_system_logs" + GetServerConfigToolName = "mattermost_get_server_config" +) + +var ( + GetSystemLogsTool = mcp.NewTool( + GetSystemLogsToolName, + mcp.WithDescription("Get system logs (requires admin privileges)"), + mcp.WithNumber("page", mcp.Description("Page number (default 0)")), + mcp.WithNumber("per_page", mcp.Description("Log lines per page (default 100, max 500)")), + ) + + GetServerConfigTool = mcp.NewTool( + GetServerConfigToolName, + mcp.WithDescription("Get server configuration (requires admin privileges)"), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: GetSystemLogsTool, Handler: GetSystemLogsFn}, + {Tool: GetServerConfigTool, Handler: GetServerConfigFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func GetSystemLogsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[System] Called GetSystemLogsFn") + + args := req.GetArguments() + + page := params.GetOptionalInt(args, "page", 0) + perPage := params.GetOptionalInt(args, "per_page", 100) + if perPage > 500 { + perPage = 500 + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + logs, _, err := client.GetSystemLogs(ctx, int(page), int(perPage)) + if err != nil { + return to.Error(fmt.Errorf("[system] failed to get logs: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "logs": logs, + "count": len(logs), + "page": page, + "per_page": perPage, + }), nil +} + +func GetServerConfigFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[System] Called GetServerConfigFn") + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + config, err := client.GetConfig(ctx) + if err != nil { + return to.Error(fmt.Errorf("[system] failed to get config: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "site_name": config.TeamSettings.SiteName, + "max_notifications_per_channel": config.TeamSettings.MaxNotificationsPerChannel, + "enable_custom_emoji": config.ServiceSettings.EnableCustomEmoji, + "enable_link_previews": config.ServiceSettings.EnableLinkPreviews, + "enable_public_channels": config.TeamSettings.EnableOpenServer, + }), nil +} diff --git a/operation/team/slim.go b/operation/team/slim.go new file mode 100644 index 0000000..cfb1f1b --- /dev/null +++ b/operation/team/slim.go @@ -0,0 +1,15 @@ +package team + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimTeam(t *model.Team) map[string]interface{} { + if t == nil { + return nil + } + return map[string]interface{}{ + "id": t.Id, + "name": t.Name, + "display_name": t.DisplayName, + "description": t.Description, + } +} diff --git a/operation/team/team.go b/operation/team/team.go new file mode 100644 index 0000000..23df31e --- /dev/null +++ b/operation/team/team.go @@ -0,0 +1,245 @@ +package team + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + ListTeamsToolName = "mattermost_list_teams" + ListTeamMembersToolName = "mattermost_list_team_members" + InviteUserToTeamToolName = "mattermost_invite_user_to_team" + RemoveUserFromTeamToolName = "mattermost_remove_user_from_team" + GetTeamStatsToolName = "mattermost_get_team_stats" +) + +var ( + ListTeamsTool = mcp.NewTool( + ListTeamsToolName, + mcp.WithDescription("List all teams the bot has access to"), + ) + + ListTeamMembersTool = mcp.NewTool( + ListTeamMembersToolName, + mcp.WithDescription("List all members of a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list members for")), + mcp.WithNumber("page", mcp.Description("Page number (default 0)")), + mcp.WithNumber("per_page", mcp.Description("Members per page (default 60, max 200)")), + ) + + InviteUserToTeamTool = mcp.NewTool( + InviteUserToTeamToolName, + mcp.WithDescription("Invite/add a user to a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to invite user to")), + mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to invite")), + ) + + RemoveUserFromTeamTool = mcp.NewTool( + RemoveUserFromTeamToolName, + mcp.WithDescription("Remove a user from a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to remove user from")), + mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to remove")), + ) + + GetTeamStatsTool = mcp.NewTool( + GetTeamStatsToolName, + mcp.WithDescription("Get statistics for a team (member count, etc.)"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to get stats for")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: ListTeamsTool, Handler: ListTeamsFn}, + {Tool: ListTeamMembersTool, Handler: ListTeamMembersFn}, + {Tool: GetTeamStatsTool, Handler: GetTeamStatsFn}, + {Tool: InviteUserToTeamTool, Handler: InviteUserToTeamFn}, + {Tool: RemoveUserFromTeamTool, Handler: RemoveUserFromTeamFn}, + } + for _, t := range tools { + if t.Tool.Name == InviteUserToTeamToolName || t.Tool.Name == RemoveUserFromTeamToolName { + Tool.RegisterWrite(t) + } else { + Tool.RegisterRead(t) + } + } +} + +func ListTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Team] Called ListTeamsFn") + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + user, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get current user: %v", err)), nil + } + + teams, err := client.GetTeamsForUser(ctx, user.Id) + if err != nil { + return to.Error(fmt.Errorf("[teams] failed to list teams: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(teams)) + for _, t := range teams { + results = append(results, SlimTeam(t)) + } + + return to.Result(map[string]interface{}{ + "teams": results, + "count": len(results), + }), nil +} + +func ListTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Team] Called ListTeamMembersFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + page := params.GetOptionalInt(args, "page", 0) + perPage := params.GetOptionalInt(args, "per_page", 60) + if perPage > 200 { + perPage = 200 + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + members, err := client.ListTeamMembers(ctx, teamID, int(page), int(perPage)) + if err != nil { + return to.Error(fmt.Errorf("[team] failed to list members: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(members)) + for _, m := range members { + results = append(results, map[string]interface{}{ + "user_id": m.UserId, + "roles": m.Roles, + }) + } + + return to.Result(map[string]interface{}{ + "members": results, + "count": len(results), + "team_id": teamID, + "page": page, + "per_page": perPage, + }), nil +} + +func InviteUserToTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Team] Called InviteUserToTeamFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + userID, err := params.GetString(args, "user_id") + if err != nil { + return to.Error(fmt.Errorf("[user_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + _, err = client.InviteUserToTeam(ctx, teamID, userID) + if err != nil { + return to.Error(fmt.Errorf("[team] failed to invite user: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "team_id": teamID, + "user_id": userID, + "message": "User invited to team successfully", + }), nil +} + +func RemoveUserFromTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Team] Called RemoveUserFromTeamFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + userID, err := params.GetString(args, "user_id") + if err != nil { + return to.Error(fmt.Errorf("[user_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.RemoveUserFromTeam(ctx, teamID, userID) + if err != nil { + return to.Error(fmt.Errorf("[team] failed to remove user: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "team_id": teamID, + "user_id": userID, + "message": "User removed from team successfully", + }), nil +} + +func GetTeamStatsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Team] Called GetTeamStatsFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + stats, err := client.GetTeamStats(ctx, teamID) + if err != nil { + return to.Error(fmt.Errorf("[team] failed to get stats: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "team_id": teamID, + "total_members": stats.TotalMemberCount, + "active_members": stats.ActiveMemberCount, + }), nil +} diff --git a/operation/team/team_test.go b/operation/team/team_test.go new file mode 100644 index 0000000..f7aa6c4 --- /dev/null +++ b/operation/team/team_test.go @@ -0,0 +1,59 @@ +package team + +import ( + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimTeam(t *testing.T) { + team := &model.Team{ + Id: "team123", + Name: "my-team", + DisplayName: "My Team", + Description: "A test team for unit testing", + } + + slim := SlimTeam(team) + assert.NotNil(t, slim) + assert.Equal(t, "team123", slim["id"]) + assert.Equal(t, "my-team", slim["name"]) + assert.Equal(t, "My Team", slim["display_name"]) + assert.Equal(t, "A test team for unit testing", slim["description"]) +} + +func TestSlimTeam_Nil(t *testing.T) { + slim := SlimTeam(nil) + assert.Nil(t, slim) +} + +func TestToolRegistration(t *testing.T) { + tools := Tool.Tools() + assert.Len(t, tools, 1) + + toolNames := make(map[string]bool) + for _, t := range tools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[ListTeamsToolName], "ListTeams tool should be registered") +} + +func TestListTeamsFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: ListTeamsToolName, + Arguments: map[string]interface{}{}, + }, + } + + result, err := ListTeamsFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} diff --git a/operation/user/slim.go b/operation/user/slim.go new file mode 100644 index 0000000..dfce80e --- /dev/null +++ b/operation/user/slim.go @@ -0,0 +1,17 @@ +package user + +import "github.com/mattermost/mattermost-server/v6/model" + +func SlimUser(u *model.User) map[string]interface{} { + if u == nil { + return nil + } + return map[string]interface{}{ + "id": u.Id, + "username": u.Username, + "email": u.Email, + "first_name": u.FirstName, + "last_name": u.LastName, + "roles": u.Roles, + } +} diff --git a/operation/user/status.go b/operation/user/status.go new file mode 100644 index 0000000..7b5a636 --- /dev/null +++ b/operation/user/status.go @@ -0,0 +1,171 @@ +package user + +import ( + "context" + "fmt" + "strings" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetUserStatusToolName = "mattermost_get_user_status" + UpdateUserStatusToolName = "mattermost_update_user_status" + GetUsersStatusBulkToolName = "mattermost_get_users_status_bulk" +) + +var ( + GetUserStatusTool = mcp.NewTool( + GetUserStatusToolName, + mcp.WithDescription("Get the online status of a user (online, away, dnd, offline)"), + mcp.WithString("user_id", mcp.Required(), mcp.Description("User ID to check status for")), + ) + + UpdateUserStatusTool = mcp.NewTool( + UpdateUserStatusToolName, + mcp.WithDescription("Update your status (online, away, dnd, offline)"), + mcp.WithString("status", mcp.Required(), mcp.Description("Status to set: online, away, dnd, or offline")), + ) + + GetUsersStatusBulkTool = mcp.NewTool( + GetUsersStatusBulkToolName, + mcp.WithDescription("Get status for multiple users at once (up to 100)"), + mcp.WithString("user_ids", mcp.Required(), mcp.Description("Comma-separated list of user IDs (max 100)")), + ) +) + +func init() { + registerStatusTools() +} + +func registerStatusTools() { + tools := []server.ServerTool{ + {Tool: GetUserStatusTool, Handler: GetUserStatusFn}, + {Tool: UpdateUserStatusTool, Handler: UpdateUserStatusFn}, + {Tool: GetUsersStatusBulkTool, Handler: GetUsersStatusBulkFn}, + } + for _, t := range tools { + if t.Tool.Name == UpdateUserStatusToolName { + Tool.RegisterWrite(t) + } else { + Tool.RegisterRead(t) + } + } +} + +func GetUserStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[User] Called GetUserStatusFn") + + args := req.GetArguments() + + userID, err := params.GetString(args, "user_id") + if err != nil { + return to.Error(fmt.Errorf("[user_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + status, err := client.GetUserStatus(ctx, userID) + if err != nil { + return to.Error(fmt.Errorf("[status] failed to get user status: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "user_id": status.UserId, + "status": status.Status, + "manual": status.Manual, + "last_activity_at": status.LastActivityAt, + }), nil +} + +func UpdateUserStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[User] Called UpdateUserStatusFn") + + args := req.GetArguments() + + status, err := params.GetString(args, "status") + if err != nil { + return to.Error(fmt.Errorf("[status] %v", err)), nil + } + + // Validate status value + validStatuses := map[string]bool{"online": true, "away": true, "dnd": true, "offline": true} + if !validStatuses[status] { + return to.Error(fmt.Errorf("[status] invalid status '%s', must be one of: online, away, dnd, offline", status)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + // Get current user ID + me, err := client.GetMe(ctx) + if err != nil { + return to.Error(fmt.Errorf("[me] failed to get current user: %v", err)), nil + } + + updatedStatus, err := client.UpdateUserStatus(ctx, me.Id, status) + if err != nil { + return to.Error(fmt.Errorf("[status] failed to update status: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "user_id": updatedStatus.UserId, + "status": updatedStatus.Status, + "message": "Status updated successfully", + }), nil +} + +func GetUsersStatusBulkFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[User] Called GetUsersStatusBulkFn") + + args := req.GetArguments() + + userIDsStr, err := params.GetString(args, "user_ids") + if err != nil { + return to.Error(fmt.Errorf("[user_ids] %v", err)), nil + } + + userIDs := strings.Split(userIDsStr, ",") + if len(userIDs) > 100 { + return to.Error(fmt.Errorf("[user_ids] too many user IDs (max 100, got %d)", len(userIDs))), nil + } + + for i, id := range userIDs { + userIDs[i] = strings.TrimSpace(id) + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + statuses, err := client.GetUsersStatus(ctx, userIDs) + if err != nil { + return to.Error(fmt.Errorf("[status] failed to get users status: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(statuses)) + for _, status := range statuses { + results = append(results, map[string]interface{}{ + "user_id": status.UserId, + "status": status.Status, + "manual": status.Manual, + "last_activity_at": status.LastActivityAt, + }) + } + + return to.Result(map[string]interface{}{ + "statuses": results, + "count": len(results), + }), nil +} diff --git a/operation/user/user.go b/operation/user/user.go new file mode 100644 index 0000000..e8dd42e --- /dev/null +++ b/operation/user/user.go @@ -0,0 +1,129 @@ +package user + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mattermost/mattermost-server/v6/model" +) + +var Tool = tool.New() + +const ( + SearchUsersToolName = "mattermost_search_users" + GetUserToolName = "mattermost_get_user" +) + +var ( + SearchUsersTool = mcp.NewTool( + SearchUsersToolName, + mcp.WithDescription("Search users by term"), + mcp.WithString("term", mcp.Required(), mcp.Description("Search term (username, email, name)")), + mcp.WithString("team_id", mcp.Description("Limit to team (optional)")), + mcp.WithNumber("limit", mcp.Description("Max results (default 30)")), + ) + + GetUserTool = mcp.NewTool( + GetUserToolName, + mcp.WithDescription("Get a specific user by ID or username"), + mcp.WithString("user_id", mcp.Description("User ID to look up (optional if username provided)")), + mcp.WithString("username", mcp.Description("Username to look up (optional if user_id provided)")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: SearchUsersTool, Handler: SearchUsersFn}, + {Tool: GetUserTool, Handler: GetUserFn}, + } + for _, t := range tools { + Tool.RegisterRead(t) + } +} + +func SearchUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[User] Called SearchUsersFn") + + args := req.GetArguments() + + term, err := params.GetString(args, "term") + if err != nil { + return to.Error(fmt.Errorf("[term] %v", err)), nil + } + + teamID := params.GetOptionalString(args, "team_id", "") + limit := params.GetOptionalInt(args, "limit", 30) + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + search := &model.UserSearch{ + Term: term, + Limit: int(limit), + } + + if teamID != "" { + search.TeamId = teamID + } + + users, err := client.SearchUsers(ctx, search) + if err != nil { + return to.Error(fmt.Errorf("[users] failed to search users: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(users)) + for _, u := range users { + results = append(results, SlimUser(u)) + } + + return to.Result(map[string]interface{}{ + "users": results, + "count": len(results), + }), nil +} + +func GetUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[User] Called GetUserFn") + + args := req.GetArguments() + + userID := params.GetOptionalString(args, "user_id", "") + username := params.GetOptionalString(args, "username", "") + + if userID == "" && username == "" { + return to.Error(fmt.Errorf("[user] either user_id or username must be provided")), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + var user *model.User + var err error + + if userID != "" { + user, err = client.GetUser(ctx, userID) + } else { + user, err = client.GetUserByUsername(ctx, username) + } + + if err != nil { + return to.Error(fmt.Errorf("[user] failed to get user: %v", err)), nil + } + + return to.Result(SlimUser(user)), nil +} diff --git a/operation/user/user_test.go b/operation/user/user_test.go new file mode 100644 index 0000000..3eeab82 --- /dev/null +++ b/operation/user/user_test.go @@ -0,0 +1,80 @@ +package user + +import ( + "testing" + + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mattermost/mattermost-server/v6/model" + "github.com/stretchr/testify/assert" +) + +func TestSlimUser(t *testing.T) { + u := &model.User{ + Id: "user123", + Username: "johndoe", + Email: "john@example.com", + FirstName: "John", + LastName: "Doe", + Roles: "system_user", + } + + slim := SlimUser(u) + assert.NotNil(t, slim) + assert.Equal(t, "user123", slim["id"]) + assert.Equal(t, "johndoe", slim["username"]) + assert.Equal(t, "john@example.com", slim["email"]) + assert.Equal(t, "John", slim["first_name"]) + assert.Equal(t, "Doe", slim["last_name"]) + assert.Equal(t, "system_user", slim["roles"]) +} + +func TestSlimUser_Nil(t *testing.T) { + slim := SlimUser(nil) + assert.Nil(t, slim) +} + +func TestToolRegistration(t *testing.T) { + tools := Tool.Tools() + assert.Len(t, tools, 2) + + toolNames := make(map[string]bool) + for _, t := range tools { + toolNames[t.Tool.Name] = true + } + + assert.True(t, toolNames[SearchUsersToolName], "SearchUsers tool should be registered") + assert.True(t, toolNames[GetUserStatusToolName], "GetUserStatus tool should be registered") +} + +func TestSearchUsersFn_ClientNotInitialized(t *testing.T) { + mattermost.SetGlobalClient(nil) + + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: SearchUsersToolName, + Arguments: map[string]interface{}{ + "term": "john", + }, + }, + } + + result, err := SearchUsersFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} + +func TestSearchUsersFn_MissingTerm(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: SearchUsersToolName, + Arguments: map[string]interface{}{}, + }, + } + + result, err := SearchUsersFn(nil, req) + assert.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsError) +} diff --git a/operation/webhook/webhook.go b/operation/webhook/webhook.go new file mode 100644 index 0000000..3f9158a --- /dev/null +++ b/operation/webhook/webhook.go @@ -0,0 +1,168 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/karti-ai/mattermost-mcp-server/pkg/mattermost" + "github.com/karti-ai/mattermost-mcp-server/pkg/params" + "github.com/karti-ai/mattermost-mcp-server/pkg/to" + "github.com/karti-ai/mattermost-mcp-server/pkg/tool" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + CreateIncomingWebhookToolName = "mattermost_create_incoming_webhook" + ListIncomingWebhooksToolName = "mattermost_list_incoming_webhooks" + DeleteIncomingWebhookToolName = "mattermost_delete_incoming_webhook" +) + +var ( + CreateIncomingWebhookTool = mcp.NewTool( + CreateIncomingWebhookToolName, + mcp.WithDescription("Create an incoming webhook for a channel"), + mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to create webhook for")), + mcp.WithString("display_name", mcp.Required(), mcp.Description("Display name for the webhook")), + ) + + ListIncomingWebhooksTool = mcp.NewTool( + ListIncomingWebhooksToolName, + mcp.WithDescription("List incoming webhooks for a team"), + mcp.WithString("team_id", mcp.Required(), mcp.Description("Team ID to list webhooks for")), + mcp.WithNumber("page", mcp.Description("Page number (default 0)")), + mcp.WithNumber("per_page", mcp.Description("Items per page (default 20, max 100)")), + ) + + DeleteIncomingWebhookTool = mcp.NewTool( + DeleteIncomingWebhookToolName, + mcp.WithDescription("Delete an incoming webhook"), + mcp.WithString("webhook_id", mcp.Required(), mcp.Description("Webhook ID to delete")), + ) +) + +func init() { + registerTools() +} + +func registerTools() { + tools := []server.ServerTool{ + {Tool: CreateIncomingWebhookTool, Handler: CreateIncomingWebhookFn}, + {Tool: ListIncomingWebhooksTool, Handler: ListIncomingWebhooksFn}, + {Tool: DeleteIncomingWebhookTool, Handler: DeleteIncomingWebhookFn}, + } + for _, t := range tools { + if t.Tool.Name == ListIncomingWebhooksToolName { + Tool.RegisterRead(t) + } else { + Tool.RegisterWrite(t) + } + } +} + +func CreateIncomingWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Webhook] Called CreateIncomingWebhookFn") + + args := req.GetArguments() + + channelID, err := params.GetString(args, "channel_id") + if err != nil { + return to.Error(fmt.Errorf("[channel_id] %v", err)), nil + } + + displayName, err := params.GetString(args, "display_name") + if err != nil { + return to.Error(fmt.Errorf("[display_name] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + hook, err := client.CreateIncomingWebhook(ctx, channelID, displayName) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to create: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "id": hook.Id, + "channel_id": hook.ChannelId, + "display_name": hook.DisplayName, + "message": "Webhook created successfully - retrieve URL from Mattermost UI", + }), nil +} + +func ListIncomingWebhooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Webhook] Called ListIncomingWebhooksFn") + + args := req.GetArguments() + + teamID, err := params.GetString(args, "team_id") + if err != nil { + return to.Error(fmt.Errorf("[team_id] %v", err)), nil + } + + page := params.GetOptionalInt(args, "page", 0) + perPage := params.GetOptionalInt(args, "per_page", 20) + if perPage > 100 { + perPage = 100 + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + hooks, err := client.ListIncomingWebhooks(ctx, teamID, int(page), int(perPage)) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to list: %v", err)), nil + } + + results := make([]map[string]interface{}, 0, len(hooks)) + for _, hook := range hooks { + results = append(results, map[string]interface{}{ + "id": hook.Id, + "channel_id": hook.ChannelId, + "display_name": hook.DisplayName, + "create_at": hook.CreateAt, + }) + } + + return to.Result(map[string]interface{}{ + "webhooks": results, + "count": len(results), + "page": page, + "per_page": perPage, + }), nil +} + +func DeleteIncomingWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("[Webhook] Called DeleteIncomingWebhookFn") + + args := req.GetArguments() + + webhookID, err := params.GetString(args, "webhook_id") + if err != nil { + return to.Error(fmt.Errorf("[webhook_id] %v", err)), nil + } + + client := mattermost.GetGlobalClient() + if client == nil { + return to.Error(fmt.Errorf("[internal] client not initialized")), nil + } + + err = client.DeleteIncomingWebhook(ctx, webhookID) + if err != nil { + return to.Error(fmt.Errorf("[webhook] failed to delete: %v", err)), nil + } + + return to.Result(map[string]interface{}{ + "success": true, + "webhook_id": webhookID, + "message": "Webhook deleted successfully", + }), nil +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000..0838dc8 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,360 @@ +package errors + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +type ErrorCategory string + +const ( + CategoryFile ErrorCategory = "file" + CategoryAuth ErrorCategory = "auth" + CategoryRepo ErrorCategory = "repo" + CategoryChannel ErrorCategory = "channel" + CategoryUser ErrorCategory = "user" + CategoryMessage ErrorCategory = "message" + CategoryNetwork ErrorCategory = "network" + CategoryUnknown ErrorCategory = "unknown" +) + +type EnhancedError struct { + Original error + Translated string + Category ErrorCategory + Operation string + Context map[string]string + Timestamp time.Time +} + +func (e *EnhancedError) Error() string { + if e.Translated != "" { + return e.Translated + } + if e.Original != nil { + return e.Original.Error() + } + return "unknown error" +} + +func (e *EnhancedError) Unwrap() error { + return e.Original +} + +func (e *EnhancedError) WithContext(key, value string) *EnhancedError { + if e.Context == nil { + e.Context = make(map[string]string) + } + e.Context[key] = value + return e +} + +func (e *EnhancedError) WithOperation(op string) *EnhancedError { + e.Operation = op + return e +} + +func (e *EnhancedError) WithParam(key, value string) *EnhancedError { + return e.WithContext(key, value) +} + +func (e *EnhancedError) FormatDetailed() string { + details := map[string]any{ + "error": e.Error(), + "category": e.Category, + "timestamp": e.Timestamp.Format(time.RFC3339), + } + + if e.Operation != "" { + details["operation"] = e.Operation + } + + if len(e.Context) > 0 { + details["context"] = e.Context + } + + if e.Original != nil && e.Original.Error() != e.Error() { + details["original"] = e.Original.Error() + } + + jsonBytes, err := json.MarshalIndent(details, "", " ") + if err != nil { + return e.Format() + } + + return string(jsonBytes) +} + +func (e *EnhancedError) Format() string { + var parts []string + + if e.Operation != "" { + parts = append(parts, fmt.Sprintf("Operation: %s", e.Operation)) + } + + parts = append(parts, fmt.Sprintf("Error: %s", e.Error())) + + if e.Category != "" && e.Category != CategoryUnknown { + parts = append(parts, fmt.Sprintf("Category: %s", e.Category)) + } + + if len(e.Context) > 0 { + var ctxParts []string + for k, v := range e.Context { + ctxParts = append(ctxParts, fmt.Sprintf("%s=%s", k, v)) + } + parts = append(parts, fmt.Sprintf("Context: %s", strings.Join(ctxParts, ", "))) + } + + if e.Original != nil && e.Original.Error() != e.Error() { + parts = append(parts, fmt.Sprintf("Original: %s", e.Original.Error())) + } + + return strings.Join(parts, " | ") +} + +func TranslateError(err error, context map[string]string) error { + if err == nil { + return nil + } + + var existing *EnhancedError + if errors.As(err, &existing) { + if context != nil { + for k, v := range context { + existing.WithContext(k, v) + } + } + return existing + } + + translated, category := translateErrorMessage(err) + + operation := "" + if context != nil { + operation = context["operation"] + } + + enhanced := &EnhancedError{ + Original: err, + Translated: translated, + Category: category, + Operation: operation, + Context: context, + Timestamp: time.Now().UTC(), + } + + return enhanced +} + +func translateErrorMessage(err error) (string, ErrorCategory) { + if err == nil { + return "", CategoryUnknown + } + + msg := err.Error() + lowerMsg := strings.ToLower(msg) + + if strings.Contains(msg, "404") { + return "Resource not found", CategoryUnknown + } + + if strings.Contains(msg, "401") { + return "Authentication failed - check your access token", CategoryAuth + } + + if strings.Contains(msg, "403") { + return "Permission denied - you don't have access to this resource", CategoryAuth + } + + translations := []struct { + pattern string + message string + category ErrorCategory + }{ + {"GetUser", "User not found", CategoryUser}, + {"GetChannel", "Channel not found", CategoryChannel}, + {"GetPost", "Message not found", CategoryMessage}, + {"CreatePost", "Failed to create message", CategoryMessage}, + {"UpdatePost", "Failed to update message", CategoryMessage}, + {"DeletePost", "Failed to delete message", CategoryMessage}, + {"CreateChannel", "Failed to create channel", CategoryChannel}, + } + + for _, t := range translations { + if strings.Contains(msg, t.pattern) { + return t.message, t.category + } + } + + if strings.Contains(lowerMsg, "timeout") || strings.Contains(lowerMsg, "deadline exceeded") { + return "Request timed out - the server took too long to respond", CategoryNetwork + } + + if strings.Contains(lowerMsg, "connection refused") || strings.Contains(lowerMsg, "no such host") { + return "Network error - cannot connect to server", CategoryNetwork + } + + return msg, CategoryUnknown +} + +func IsNotFound(err error) bool { + if err == nil { + return false + } + + var enhanced *EnhancedError + if errors.As(err, &enhanced) { + switch enhanced.Category { + case CategoryFile, CategoryRepo, CategoryChannel, CategoryUser, CategoryMessage: + return true + } + return strings.Contains(enhanced.Translated, "not found") + } + + var httpErr interface{ Error() string } + if errors.As(err, &httpErr) { + if strings.Contains(httpErr.Error(), "404") { + return true + } + } + + lowerMsg := strings.ToLower(err.Error()) + return strings.Contains(lowerMsg, "not found") || strings.Contains(lowerMsg, "404") +} + +func IsAuthError(err error) bool { + if err == nil { + return false + } + + var enhanced *EnhancedError + if errors.As(err, &enhanced) { + return enhanced.Category == CategoryAuth + } + + msg := err.Error() + if strings.Contains(msg, "401") || strings.Contains(msg, "403") { + return true + } + + lowerMsg := strings.ToLower(msg) + return strings.Contains(lowerMsg, "authentication") || + strings.Contains(lowerMsg, "unauthorized") || + strings.Contains(lowerMsg, "permission denied") || + strings.Contains(lowerMsg, "forbidden") +} + +func IsTimeout(err error) bool { + if err == nil { + return false + } + + var enhanced *EnhancedError + if errors.As(err, &enhanced) { + return enhanced.Category == CategoryNetwork || strings.Contains(enhanced.Translated, "timed out") + } + + lowerMsg := strings.ToLower(err.Error()) + return strings.Contains(lowerMsg, "timeout") || + strings.Contains(lowerMsg, "deadline exceeded") || + strings.Contains(lowerMsg, "context deadline") +} + +func IsNetworkError(err error) bool { + if err == nil { + return false + } + + var enhanced *EnhancedError + if errors.As(err, &enhanced) { + return enhanced.Category == CategoryNetwork + } + + lowerMsg := strings.ToLower(err.Error()) + return strings.Contains(lowerMsg, "connection") || + strings.Contains(lowerMsg, "network") || + strings.Contains(lowerMsg, "no such host") || + strings.Contains(lowerMsg, "dial tcp") +} + +func NewEnhancedError(original error, translated string, category ErrorCategory) *EnhancedError { + return &EnhancedError{ + Original: original, + Translated: translated, + Category: category, + Context: make(map[string]string), + Timestamp: time.Now().UTC(), + } +} + +func Wrap(err error, operation string) error { + if err == nil { + return nil + } + return TranslateError(err, map[string]string{"operation": operation}) +} + +type HTTPError interface { + error + Status() int +} + +type statusError struct { + status int + message string +} + +func (e *statusError) Error() string { return e.message } +func (e *statusError) Status() int { return e.status } + +func IsHTTPError(err error, statusCode int) bool { + if err == nil { + return false + } + + var httpErr HTTPError + if errors.As(err, &httpErr) { + return httpErr.Status() == statusCode + } + + msg := err.Error() + return strings.Contains(msg, fmt.Sprintf("status %d", statusCode)) || + strings.Contains(msg, fmt.Sprintf("%d", statusCode)) +} + +func IsUnauthorized(err error) bool { + return IsHTTPError(err, http.StatusUnauthorized) +} + +func IsForbidden(err error) bool { + return IsHTTPError(err, http.StatusForbidden) +} + +func IsNotFoundHTTP(err error) bool { + return IsHTTPError(err, http.StatusNotFound) +} + +func IsServerError(err error) bool { + if err == nil { + return false + } + + var httpErr HTTPError + if errors.As(err, &httpErr) { + return httpErr.Status() >= 500 && httpErr.Status() < 600 + } + + msg := err.Error() + for i := 500; i < 600; i++ { + if strings.Contains(msg, fmt.Sprintf("status %d", i)) || + strings.Contains(msg, fmt.Sprintf("%d", i)) { + return true + } + } + return false +} diff --git a/pkg/file/file.go b/pkg/file/file.go new file mode 100644 index 0000000..b6d57fb --- /dev/null +++ b/pkg/file/file.go @@ -0,0 +1,150 @@ +package file + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "strings" +) + +const MaxFileSize = 50 * 1024 * 1024 + +var AllowedMimeTypes = []string{ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "application/pdf", + "text/plain", + "text/markdown", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", +} + +var DangerousExtensions = []string{ + ".exe", ".sh", ".bat", ".cmd", ".com", ".scr", ".pif", ".vbs", + ".js", ".jse", ".wsf", ".wsh", ".ps1", ".ps2", ".msc", + ".dll", ".so", ".dylib", ".bin", +} + +func ValidatePath(base, target string) (string, error) { + cleanTarget := filepath.Clean(target) + if filepath.IsAbs(cleanTarget) { + return "", fmt.Errorf("absolute paths not allowed: %s", target) + } + + fullPath := filepath.Join(base, cleanTarget) + realBase, err := filepath.Abs(base) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for base: %w", err) + } + + realTarget, err := filepath.Abs(fullPath) + if err != nil { + return "", fmt.Errorf("failed to get absolute path for target: %w", err) + } + + if !filepath.HasPrefix(realTarget, realBase) { + return "", fmt.Errorf("path traversal detected: %s", target) + } + + return realTarget, nil +} + +func Exists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func IsValidPath(path string) bool { + cleanPath := filepath.Clean(path) + if strings.Contains(cleanPath, "..") { + return false + } + return true +} + +func SanitizePath(path string) (string, error) { + if !IsValidPath(path) { + return "", fmt.Errorf("invalid path: path traversal detected: %s", path) + } + return filepath.Clean(path), nil +} + +func ValidateFileSize(size int64) error { + if size > MaxFileSize { + return fmt.Errorf("file size %d bytes exceeds maximum allowed size of %d bytes (50MB)", size, MaxFileSize) + } + return nil +} + +func IsAllowedMimeType(mimeType string) bool { + baseMimeType := strings.Split(mimeType, ";")[0] + baseMimeType = strings.TrimSpace(baseMimeType) + for _, allowed := range AllowedMimeTypes { + if strings.EqualFold(baseMimeType, allowed) { + return true + } + } + if strings.HasPrefix(baseMimeType, "application/vnd.openxmlformats-officedocument") { + return true + } + return false +} + +func DetectMimeType(data []byte) string { + return http.DetectContentType(data) +} + +func ValidateMimeType(data []byte) (string, error) { + mimeType := DetectMimeType(data) + if !IsAllowedMimeType(mimeType) { + return "", fmt.Errorf("MIME type %q is not in the allowed whitelist", mimeType) + } + return mimeType, nil +} + +func IsDangerousExtension(filename string) bool { + ext := strings.ToLower(filepath.Ext(filename)) + for _, dangerous := range DangerousExtensions { + if ext == dangerous { + return true + } + } + return false +} + +func ValidateFilename(filename string) error { + if filename == "" { + return fmt.Errorf("filename cannot be empty") + } + if IsDangerousExtension(filename) { + return fmt.Errorf("file type %q is not allowed for security reasons", filepath.Ext(filename)) + } + if strings.Contains(filename, "\x00") { + return fmt.Errorf("filename contains invalid characters") + } + return nil +} + +func GetFileInfo(path string) (os.FileInfo, error) { + return os.Stat(path) +} + +func CheckDiskSpace(path string, requiredBytes int64) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + testFile := filepath.Join(dir, ".write_test_"+fmt.Sprintf("%d", os.Getpid())) + f, err := os.Create(testFile) + if err != nil { + return fmt.Errorf("insufficient permissions or disk space at %s: %w", dir, err) + } + f.Close() + os.Remove(testFile) + return nil +} diff --git a/pkg/flag/flag.go b/pkg/flag/flag.go new file mode 100644 index 0000000..9949a6a --- /dev/null +++ b/pkg/flag/flag.go @@ -0,0 +1,14 @@ +package flag + +var ( + Version = "dev" + ReadOnly = false + Debug = false + ShowVersion = false + Insecure = false + Token = "" + BotToken = "" + PAT = "" + Host = "" + Team = "" +) diff --git a/pkg/log/context.go b/pkg/log/context.go new file mode 100644 index 0000000..e3f98a4 --- /dev/null +++ b/pkg/log/context.go @@ -0,0 +1,85 @@ +package log + +import ( + "context" + "fmt" + "sync" + "time" +) + +type contextKey string + +const ( + correlationIDKey contextKey = "correlation_id" + operationKey contextKey = "operation" + startTimeKey contextKey = "start_time" +) + +var ( + correlationIDGenerator = &idGenerator{} +) + +type idGenerator struct { + mu sync.Mutex + seq uint64 +} + +func (g *idGenerator) Generate() string { + g.mu.Lock() + defer g.mu.Unlock() + g.seq++ + return time.Now().Format("20060102-150405") + "-" + fmt.Sprint(g.seq) +} + +func WithCorrelationID(ctx context.Context, id string) context.Context { + if id == "" { + id = correlationIDGenerator.Generate() + } + return context.WithValue(ctx, correlationIDKey, id) +} + +func WithOperation(ctx context.Context, operation string) context.Context { + return context.WithValue(ctx, operationKey, operation) +} + +func WithStartTime(ctx context.Context) context.Context { + return context.WithValue(ctx, startTimeKey, time.Now()) +} + +func GetCorrelationID(ctx context.Context) string { + if ctx == nil { + return "" + } + if id, ok := ctx.Value(correlationIDKey).(string); ok { + return id + } + return "" +} + +func GetOperation(ctx context.Context) string { + if ctx == nil { + return "" + } + if op, ok := ctx.Value(operationKey).(string); ok { + return op + } + return "" +} + +func GetStartTime(ctx context.Context) time.Time { + if ctx == nil { + return time.Time{} + } + if t, ok := ctx.Value(startTimeKey).(time.Time); ok { + return t + } + return time.Time{} +} + +func Duration(ctx context.Context) time.Duration { + start := GetStartTime(ctx) + if start.IsZero() { + return 0 + } + return time.Since(start) +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..e9cd2cd --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,139 @@ +package log + +import ( + "context" + "os" + "sync" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var ( + defaultLoggerOnce sync.Once + defaultLogger *zap.Logger +) + +func Default() *zap.Logger { + defaultLoggerOnce.Do(func() { + if defaultLogger != nil { + return + } + + ec := zap.NewProductionEncoderConfig() + ec.EncodeTime = zapcore.TimeEncoderOfLayout(time.DateTime) + ec.EncodeLevel = zapcore.CapitalLevelEncoder + + var ws zapcore.WriteSyncer + var wss []zapcore.WriteSyncer + + home, _ := os.UserHomeDir() + if home == "" { + home = os.TempDir() + } + + logDir := home + "/.mattermost-mcp" + if err := os.MkdirAll(logDir, 0o700); err != nil { + logDir = os.TempDir() + } + + wss = append(wss, zapcore.AddSync(&lumberjack.Logger{ + Filename: logDir + "/mattermost-mcp.log", + MaxSize: 10, + MaxBackups: 3, + MaxAge: 28, + })) + + ws = zapcore.NewMultiWriteSyncer(wss...) + + enc := zapcore.NewConsoleEncoder(ec) + core := zapcore.NewCore(enc, ws, zapcore.InfoLevel) + options := []zap.Option{ + zap.AddStacktrace(zapcore.DPanicLevel), + zap.AddCaller(), + zap.AddCallerSkip(1), + } + defaultLogger = zap.New(core, options...) + }) + + return defaultLogger +} + +func SetDefault(logger *zap.Logger) { + if logger != nil { + defaultLogger = logger + } +} + +type Logger struct { + *zap.Logger + ctx context.Context +} + +func New() *Logger { + return WithContext(context.Background()) +} + +func WithContext(ctx context.Context) *Logger { + return &Logger{ + Logger: Default(), + ctx: ctx, + } +} + +func Debug(msg string, fields ...zap.Field) { + Default().Debug(msg, fields...) +} + +func Info(msg string, fields ...zap.Field) { + Default().Info(msg, fields...) +} + +func Warn(msg string, fields ...zap.Field) { + Default().Warn(msg, fields...) +} + +func Error(msg string, fields ...zap.Field) { + Default().Error(msg, fields...) +} + +func Panic(msg string, fields ...zap.Field) { + Default().Panic(msg, fields...) +} + +func Debugf(format string, args ...any) { + Default().Sugar().Debugf(format, args...) +} + +func Infof(format string, args ...any) { + Default().Sugar().Infof(format, args...) +} + +func Warnf(format string, args ...any) { + Default().Sugar().Warnf(format, args...) +} + +func Errorf(format string, args ...any) { + Default().Sugar().Errorf(format, args...) +} + +func Fatalf(format string, args ...any) { + Default().Sugar().Fatalf(format, args...) +} + +func Initialize(level string) error { + // Logging is already initialized in Default() + return nil +} + +func Sync() { + Default().Sync() +} + +type Field = zap.Field + +func String(key string, value string) Field { + return zap.String(key, value) +} diff --git a/pkg/mattermost/client.go b/pkg/mattermost/client.go new file mode 100644 index 0000000..b144bc4 --- /dev/null +++ b/pkg/mattermost/client.go @@ -0,0 +1,889 @@ +// Package mattermost provides a wrapper around the Mattermost Go client +// with dual token support, timeout handling, retry logic, and error categorization. +package mattermost + +import ( + "context" + "fmt" + "math" + "net/http" + "strings" + "time" + + "github.com/karti-ai/mattermost-mcp-server/pkg/errors" + "github.com/karti-ai/mattermost-mcp-server/pkg/log" + "github.com/mattermost/mattermost-server/v6/model" + "go.uber.org/zap" +) + +const ( + // DefaultTimeout is the default timeout for all API requests + DefaultTimeout = 30 * time.Second + // MaxRetries is the maximum number of retries for 5xx errors + MaxRetries = 3 + // InitialBackoff is the initial backoff duration for retries + InitialBackoff = 500 * time.Millisecond +) + +// Client wraps the Mattermost Go client with enhanced functionality +type Client struct { + client *model.Client4 + botToken string + pat string + host string +} + +// Global client instance for use by operation handlers +var globalClient *Client + +// SetGlobalClient sets the global client instance +func SetGlobalClient(c *Client) { + globalClient = c +} + +// GetGlobalClient returns the global client instance +func GetGlobalClient() *Client { + return globalClient +} + +// NewClient creates a new Mattermost client with the given configuration +func NewClient(host, botToken, pat string) *Client { + c := model.NewAPIv4Client(host) + // Initialize HTTPHeader map to store custom headers + c.HTTPHeader = make(map[string]string) + return &Client{ + client: c, + botToken: botToken, + pat: pat, + host: host, + } +} + +// getToken returns the appropriate token based on operation type +// Uses BotToken for read operations (GET), PAT for write operations (POST/PUT/DELETE) +func (c *Client) getToken(isWrite bool) string { + if isWrite && c.pat != "" { + return c.pat + } + if c.botToken != "" { + return c.botToken + } + return c.pat +} + +// setToken sets the appropriate token on the client based on HTTP method +func (c *Client) setToken(method string) { + isWrite := method != http.MethodGet && method != http.MethodHead + token := c.getToken(isWrite) + if token != "" { + c.client.HTTPHeader["Authorization"] = "Bearer " + token + } +} + +// isRetryableError checks if an error warrants a retry +func isRetryableError(resp *model.Response, err error) bool { + if resp == nil { + return true // Network errors should be retried + } + return resp.StatusCode >= 500 && resp.StatusCode < 600 +} + +// calculateBackoff calculates the backoff duration for retry attempts +func calculateBackoff(attempt int) time.Duration { + // Exponential backoff: 500ms, 1s, 2s + backoff := InitialBackoff * time.Duration(math.Pow(2, float64(attempt))) + // Add jitter + return backoff + time.Duration(time.Now().UnixNano()%100)*time.Millisecond +} + +// logRequest logs API request details (NEVER logs tokens) +func (c *Client) logRequest(ctx context.Context, method, path string) { + logger := log.WithContext(ctx) + logger.Debug("sending API request", + zap.String("method", method), + zap.String("path", path), + zap.String("host", c.host), + ) +} + +// logResponse logs API response details +func (c *Client) logResponse(ctx context.Context, method, path string, statusCode int, duration time.Duration, err error) { + logger := log.WithContext(ctx) + if err != nil { + logger.Error("API request failed", + zap.String("method", method), + zap.String("path", path), + zap.Int("status_code", statusCode), + zap.Duration("duration", duration), + zap.Error(err), + ) + } else { + logger.Debug("API request completed", + zap.String("method", method), + zap.String("path", path), + zap.Int("status_code", statusCode), + zap.Duration("duration", duration), + ) + } +} + +// mapError converts Mattermost errors to categorized errors +func (c *Client) mapError(resp *model.Response, err error, operation string) error { + if err == nil { + return nil + } + + var category errors.ErrorCategory + var translated string + statusCode := 0 + + if resp != nil { + statusCode = resp.StatusCode + } + + switch statusCode { + case 401: + category = errors.CategoryAuth + translated = "Authentication failed - check your access token" + case 403: + category = errors.CategoryAuth + translated = "Permission denied - you don't have access to this resource" + case 404: + category = errors.CategoryUnknown + translated = "Resource not found" + case 429: + category = errors.CategoryNetwork + translated = "Rate limited - too many requests, please try again later" + case 500, 502, 503, 504: + category = errors.CategoryNetwork + translated = "Server error - the Mattermost server encountered a problem" + default: + if resp == nil { + category = errors.CategoryNetwork + translated = "Connection failed - unable to reach Mattermost server" + } else { + // Try to extract error message from Mattermost API error + if appErr, ok := err.(*model.AppError); ok && appErr.Message != "" { + translated = appErr.Message + category = c.categorizeAppError(appErr) + } else { + translated = err.Error() + category = errors.CategoryUnknown + } + } + } + + enhanced := errors.NewEnhancedError(err, translated, category) + enhanced.WithOperation(operation) + + return enhanced +} + +// categorizeAppError determines error category from Mattermost AppError +func (c *Client) categorizeAppError(appErr *model.AppError) errors.ErrorCategory { + id := strings.ToLower(appErr.Id) + message := strings.ToLower(appErr.Message) + + // Check error ID patterns + switch { + case strings.Contains(id, "authentication") || strings.Contains(id, "auth"): + return errors.CategoryAuth + case strings.Contains(id, "channel"): + return errors.CategoryChannel + case strings.Contains(id, "user"): + return errors.CategoryUser + case strings.Contains(id, "post") || strings.Contains(id, "message"): + return errors.CategoryMessage + case strings.Contains(id, "file"): + return errors.CategoryFile + } + + // Check message patterns + switch { + case strings.Contains(message, "channel") && strings.Contains(message, "not found"): + return errors.CategoryChannel + case strings.Contains(message, "user") && strings.Contains(message, "not found"): + return errors.CategoryUser + case strings.Contains(message, "post") && strings.Contains(message, "not found"): + return errors.CategoryMessage + case strings.Contains(message, "file") && strings.Contains(message, "not found"): + return errors.CategoryFile + case strings.Contains(message, "permission") || strings.Contains(message, "unauthorized"): + return errors.CategoryAuth + } + + return errors.CategoryUnknown +} + +// executeWithRetry executes an API call with retry logic for 5xx errors +func (c *Client) executeWithRetry( + ctx context.Context, + operation string, + method string, + path string, + fn func() (*model.Response, error), +) (*model.Response, error) { + ctx, cancel := context.WithTimeout(ctx, DefaultTimeout) + defer cancel() + + c.setToken(method) + c.logRequest(ctx, method, path) + + start := time.Now() + var lastErr error + var resp *model.Response + + for attempt := 0; attempt <= MaxRetries; attempt++ { + if attempt > 0 { + backoff := calculateBackoff(attempt - 1) + log.WithContext(ctx).Debug("retrying request", + zap.String("operation", operation), + zap.Int("attempt", attempt), + zap.Duration("backoff", backoff), + ) + time.Sleep(backoff) + } + + resp, lastErr = fn() + duration := time.Since(start) + + if lastErr == nil { + if resp != nil { + c.logResponse(ctx, method, path, resp.StatusCode, duration, nil) + } + return resp, nil + } + + if !isRetryableError(resp, lastErr) { + c.logResponse(ctx, method, path, 0, duration, lastErr) + return resp, c.mapError(resp, lastErr, operation) + } + + // Log retryable error + if resp != nil { + log.WithContext(ctx).Warn("retryable error occurred", + zap.String("operation", operation), + zap.Int("attempt", attempt), + zap.Int("status_code", resp.StatusCode), + zap.Error(lastErr), + ) + } + } + + // All retries exhausted + duration := time.Since(start) + c.logResponse(ctx, method, path, 0, duration, fmt.Errorf("max retries exceeded: %w", lastErr)) + return resp, c.mapError(resp, fmt.Errorf("max retries exceeded: %w", lastErr), operation) +} + +// ==================== User Operations ==================== + +// GetMe retrieves the current authenticated user +func (c *Client) GetMe(ctx context.Context) (*model.User, error) { + c.setToken(http.MethodGet) + user, resp, err := c.client.GetMe("") + if err != nil { + return nil, c.mapError(resp, err, "GetMe") + } + return user, nil +} + +// GetUser retrieves a user by ID +func (c *Client) GetUser(ctx context.Context, userID string) (*model.User, error) { + c.setToken(http.MethodGet) + user, resp, err := c.client.GetUser(userID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetUser") + } + return user, nil +} + +// GetUserByUsername retrieves a user by username +func (c *Client) GetUserByUsername(ctx context.Context, username string) (*model.User, error) { + c.setToken(http.MethodGet) + user, resp, err := c.client.GetUserByUsername(username, "") + if err != nil { + return nil, c.mapError(resp, err, "GetUserByUsername") + } + return user, nil +} + +// SearchUsers searches for users based on search criteria +func (c *Client) SearchUsers(ctx context.Context, search *model.UserSearch) ([]*model.User, error) { + c.setToken(http.MethodPost) + users, resp, err := c.client.SearchUsers(search) + if err != nil { + return nil, c.mapError(resp, err, "SearchUsers") + } + return users, nil +} + +// GetUserStatus retrieves the status of a user (online, away, offline, dnd) +func (c *Client) GetUserStatus(ctx context.Context, userID string) (*model.Status, error) { + c.setToken(http.MethodGet) + status, resp, err := c.client.GetUserStatus(userID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetUserStatus") + } + return status, nil +} + +// ==================== Channel Operations ==================== + +// GetChannel retrieves a channel by ID +func (c *Client) GetChannel(ctx context.Context, channelID string) (*model.Channel, error) { + c.setToken(http.MethodGet) + channel, resp, err := c.client.GetChannel(channelID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannel") + } + return channel, nil +} + +// GetChannelByName retrieves a channel by name in a team +func (c *Client) GetChannelByName(ctx context.Context, teamID, channelName string) (*model.Channel, error) { + c.setToken(http.MethodGet) + channel, resp, err := c.client.GetChannelByName(channelName, teamID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannelByName") + } + return channel, nil +} + +// GetChannelsForTeamForUser retrieves channels for a user in a team +func (c *Client) GetChannelsForTeamForUser(ctx context.Context, teamID, userID string, includeDeleted bool) ([]*model.Channel, error) { + c.setToken(http.MethodGet) + channels, resp, err := c.client.GetChannelsForTeamForUser(teamID, userID, includeDeleted, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannelsForTeamForUser") + } + return channels, nil +} + +// CreateDirectChannel creates a direct message channel between two users +func (c *Client) CreateDirectChannel(ctx context.Context, userID1, userID2 string) (*model.Channel, error) { + c.setToken(http.MethodPost) + channel, resp, err := c.client.CreateDirectChannel(userID1, userID2) + if err != nil { + return nil, c.mapError(resp, err, "CreateDirectChannel") + } + return channel, nil +} + +// GetTeamUnread retrieves unread counts for a team +func (c *Client) GetTeamUnread(ctx context.Context, userID, teamID string) (*model.TeamUnread, error) { + c.setToken(http.MethodGet) + unread, resp, err := c.client.GetTeamUnread(userID, teamID) + if err != nil { + return nil, c.mapError(resp, err, "GetTeamUnread") + } + return unread, nil +} + +// GetChannelMembersForUser retrieves channel members for a user in a team +func (c *Client) GetChannelMembersForUser(ctx context.Context, userID, teamID string) (model.ChannelMembers, error) { + c.setToken(http.MethodGet) + members, resp, err := c.client.GetChannelMembersForUser(userID, teamID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannelMembersForUser") + } + return members, nil +} + +// MarkChannelAsRead marks a channel as read for a user +func (c *Client) MarkChannelAsRead(ctx context.Context, channelID, userID string) (*model.ChannelViewResponse, error) { + c.setToken(http.MethodPut) + viewResp, resp, err := c.client.ViewChannel(userID, &model.ChannelView{ChannelId: channelID, PrevChannelId: ""}) + if err != nil { + return nil, c.mapError(resp, err, "MarkChannelAsRead") + } + return viewResp, nil +} + +// ==================== Post Operations ==================== + +// GetPostsForChannel retrieves posts for a channel +func (c *Client) GetPostsForChannel(ctx context.Context, channelID string, page, perPage int, collapsedThreads bool) (*model.PostList, error) { + c.setToken(http.MethodGet) + posts, resp, err := c.client.GetPostsForChannel(channelID, page, perPage, "", collapsedThreads) + if err != nil { + return nil, c.mapError(resp, err, "GetPostsForChannel") + } + return posts, nil +} + +// SearchPosts searches for posts using terms +func (c *Client) SearchPosts(ctx context.Context, teamID, terms string, isOrSearch bool) (*model.PostList, error) { + c.setToken(http.MethodPost) + results, resp, err := c.client.SearchPosts(teamID, terms, isOrSearch) + if err != nil { + return nil, c.mapError(resp, err, "SearchPosts") + } + return results, nil +} + +// CreatePost creates a new post in a channel +func (c *Client) CreatePost(ctx context.Context, post *model.Post) (*model.Post, error) { + c.setToken(http.MethodPost) + created, resp, err := c.client.CreatePost(post) + if err != nil { + return nil, c.mapError(resp, err, "CreatePost") + } + return created, nil +} + +// UpdatePost updates an existing post +func (c *Client) UpdatePost(ctx context.Context, postID string, post *model.Post) (*model.Post, error) { + c.setToken(http.MethodPut) + updated, resp, err := c.client.UpdatePost(postID, post) + if err != nil { + return nil, c.mapError(resp, err, "UpdatePost") + } + return updated, nil +} + +// DeletePost deletes a post by ID +func (c *Client) DeletePost(ctx context.Context, postID string) error { + _, err := c.executeWithRetry( + ctx, + "DeletePost", + http.MethodDelete, + fmt.Sprintf("/api/v4/posts/%s", postID), + func() (*model.Response, error) { + r, e := c.client.DeletePost(postID) + return r, e + }, + ) + return err +} + +// ==================== Reaction Operations ==================== + +// GetReactions retrieves all reactions for a post +func (c *Client) GetReactions(ctx context.Context, postID string) ([]*model.Reaction, error) { + c.setToken(http.MethodGet) + reactions, resp, err := c.client.GetReactions(postID) + if err != nil { + return nil, c.mapError(resp, err, "GetReactions") + } + return reactions, nil +} + +// SaveReaction adds a reaction to a post +func (c *Client) SaveReaction(ctx context.Context, reaction *model.Reaction) (*model.Reaction, error) { + c.setToken(http.MethodPost) + saved, resp, err := c.client.SaveReaction(reaction) + if err != nil { + return nil, c.mapError(resp, err, "SaveReaction") + } + return saved, nil +} + +// DeleteReaction removes a reaction from a post +func (c *Client) DeleteReaction(ctx context.Context, reaction *model.Reaction) error { + _, err := c.executeWithRetry( + ctx, + "DeleteReaction", + http.MethodDelete, + "/api/v4/reactions", + func() (*model.Response, error) { + r, e := c.client.DeleteReaction(reaction) + return r, e + }, + ) + return err +} + +// ==================== File Operations ==================== + +// UploadFile uploads a file to a channel +func (c *Client) UploadFile(ctx context.Context, data []byte, channelID, filename string) (*model.FileUploadResponse, error) { + c.setToken(http.MethodPost) + uploadResp, resp, err := c.client.UploadFile(data, channelID, filename) + if err != nil { + return nil, c.mapError(resp, err, "UploadFile") + } + return uploadResp, nil +} + +// GetFile retrieves a file by ID +func (c *Client) GetFile(ctx context.Context, fileID string) ([]byte, *model.Response, error) { + c.setToken(http.MethodGet) + data, resp, err := c.client.GetFile(fileID) + if err != nil { + return nil, resp, c.mapError(resp, err, "GetFile") + } + return data, resp, nil +} + +// ==================== Team Operations ==================== + +// GetTeamsForUser retrieves all teams a user is a member of +func (c *Client) GetTeamsForUser(ctx context.Context, userID string) ([]*model.Team, error) { + c.setToken(http.MethodGet) + teams, resp, err := c.client.GetTeamsForUser(userID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetTeamsForUser") + } + return teams, nil +} + +// GetChannelPosts retrieves posts for a channel with pagination support +func (c *Client) GetChannelPosts(ctx context.Context, channelID string, limit int, before, after string) (*model.PostList, error) { + c.setToken(http.MethodGet) + + var posts *model.PostList + var resp *model.Response + var err error + + if before != "" { + posts, resp, err = c.client.GetPostsBefore(channelID, before, 0, limit, "", false) + } else if after != "" { + posts, resp, err = c.client.GetPostsAfter(channelID, after, 0, limit, "", false) + } else { + posts, resp, err = c.client.GetPostsForChannel(channelID, 0, limit, "", false) + } + + if err != nil { + return nil, c.mapError(resp, err, "GetChannelPosts") + } + return posts, nil +} + +// GetPostThread retrieves all posts in a thread (parent + replies) +func (c *Client) GetPostThread(ctx context.Context, postID string) (*model.PostList, error) { + c.setToken(http.MethodGet) + posts, resp, err := c.client.GetPostThread(postID, "", false) + if err != nil { + return nil, c.mapError(resp, err, "GetPostThread") + } + return posts, nil +} + +// GetChannelMembers retrieves all members of a channel +func (c *Client) GetChannelMembers(ctx context.Context, channelID string, page, perPage int) (model.ChannelMembers, error) { + c.setToken(http.MethodGet) + members, resp, err := c.client.GetChannelMembers(channelID, page, perPage, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannelMembers") + } + return members, nil +} + +// CreateGroupChannel creates a group DM channel with multiple users +func (c *Client) CreateGroupChannel(ctx context.Context, userIDs []string) (*model.Channel, error) { + c.setToken(http.MethodPost) + channel, resp, err := c.client.CreateGroupChannel(userIDs) + if err != nil { + return nil, c.mapError(resp, err, "CreateGroupChannel") + } + return channel, nil +} + +// UpdateUserStatus updates the current user's status (online, away, dnd, offline) +func (c *Client) UpdateUserStatus(ctx context.Context, userID string, status string) (*model.Status, error) { + c.setToken(http.MethodPut) + userStatus := &model.Status{ + UserId: userID, + Status: status, + } + updated, resp, err := c.client.UpdateUserStatus(userID, userStatus) + if err != nil { + return nil, c.mapError(resp, err, "UpdateUserStatus") + } + return updated, nil +} + +// ==================== Admin Channel Operations ==================== + +// CreateChannel creates a new channel in a team +func (c *Client) CreateChannel(ctx context.Context, channel *model.Channel) (*model.Channel, error) { + c.setToken(http.MethodPost) + created, resp, err := c.client.CreateChannel(channel) + if err != nil { + return nil, c.mapError(resp, err, "CreateChannel") + } + return created, nil +} + +// AddChannelMember adds a user to a channel +func (c *Client) AddChannelMember(ctx context.Context, channelID, userID string) (*model.ChannelMember, error) { + c.setToken(http.MethodPost) + member, resp, err := c.client.AddChannelMember(channelID, userID) + if err != nil { + return nil, c.mapError(resp, err, "AddChannelMember") + } + return member, nil +} + +// DeleteChannel deletes/archival a channel (soft delete by default) +func (c *Client) DeleteChannel(ctx context.Context, channelID string) error { + _, err := c.executeWithRetry( + ctx, + "DeleteChannel", + http.MethodDelete, + fmt.Sprintf("/api/v4/channels/%s", channelID), + func() (*model.Response, error) { + r, e := c.client.DeleteChannel(channelID) + return r, e + }, + ) + return err +} + +// GetChannelStats retrieves statistics for a channel including member count +func (c *Client) GetChannelStats(ctx context.Context, channelID string) (*model.ChannelStats, error) { + c.setToken(http.MethodGet) + stats, resp, err := c.client.GetChannelStats(channelID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetChannelStats") + } + return stats, nil +} + +// RemoveChannelMember removes a user from a channel +func (c *Client) RemoveChannelMember(ctx context.Context, channelID, userID string) error { + _, err := c.executeWithRetry( + ctx, + "RemoveChannelMember", + http.MethodDelete, + fmt.Sprintf("/api/v4/channels/%s/members/%s", channelID, userID), + func() (*model.Response, error) { + r, e := c.client.RemoveUserFromChannel(channelID, userID) + return r, e + }, + ) + return err +} + +// ==================== Pin Operations ==================== + +// PinPost pins a post to a channel +func (c *Client) PinPost(ctx context.Context, postID string) error { + c.setToken(http.MethodPost) + _, err := c.client.PinPost(postID) + if err != nil { + return c.mapError(nil, err, "PinPost") + } + return nil +} + +// UnpinPost unpins a post from a channel +func (c *Client) UnpinPost(ctx context.Context, postID string) error { + c.setToken(http.MethodPost) + _, err := c.client.UnpinPost(postID) + if err != nil { + return c.mapError(nil, err, "UnpinPost") + } + return nil +} + +// GetPinnedPosts retrieves all pinned posts in a channel +func (c *Client) GetPinnedPosts(ctx context.Context, channelID string) (*model.PostList, error) { + c.setToken(http.MethodGet) + posts, resp, err := c.client.GetPinnedPosts(channelID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetPinnedPosts") + } + return posts, nil +} + +// ==================== Bulk Status Operations ==================== + +// GetUsersStatus retrieves status for multiple users by IDs +func (c *Client) GetUsersStatus(ctx context.Context, userIDs []string) ([]*model.Status, error) { + c.setToken(http.MethodPost) + statuses, resp, err := c.client.GetUsersStatusesByIds(userIDs) + if err != nil { + return nil, c.mapError(resp, err, "GetUsersStatus") + } + return statuses, nil +} + +// ==================== Webhook Operations ==================== + +// CreateIncomingWebhook creates an incoming webhook for a channel +func (c *Client) CreateIncomingWebhook(ctx context.Context, channelID string, displayName string) (*model.IncomingWebhook, error) { + c.setToken(http.MethodPost) + + // Get current user for webhook creation + me, err := c.GetMe(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get current user: %w", err) + } + + hook := &model.IncomingWebhook{ + ChannelId: channelID, + DisplayName: displayName, + UserId: me.Id, + } + + created, resp, err := c.client.CreateIncomingWebhook(hook) + if err != nil { + return nil, c.mapError(resp, err, "CreateIncomingWebhook") + } + return created, nil +} + +// ListIncomingWebhooks lists incoming webhooks for a team +func (c *Client) ListIncomingWebhooks(ctx context.Context, teamID string, page, perPage int) ([]*model.IncomingWebhook, error) { + c.setToken(http.MethodGet) + hooks, resp, err := c.client.GetIncomingWebhooksForTeam(teamID, page, perPage, "") + if err != nil { + return nil, c.mapError(resp, err, "ListIncomingWebhooks") + } + return hooks, nil +} + +// DeleteIncomingWebhook deletes an incoming webhook +func (c *Client) DeleteIncomingWebhook(ctx context.Context, hookID string) error { + _, err := c.executeWithRetry( + ctx, + "DeleteIncomingWebhook", + http.MethodDelete, + fmt.Sprintf("/api/v4/hooks/incoming/%s", hookID), + func() (*model.Response, error) { + r, e := c.client.DeleteIncomingWebhook(hookID) + return r, e + }, + ) + return err +} + +// ==================== Slash Commands ==================== + +// ExecuteSlashCommand runs a slash command in a channel +func (c *Client) ExecuteSlashCommand(ctx context.Context, channelID string, command string) (*model.CommandResponse, error) { + c.setToken(http.MethodPost) + resp, _, err := c.client.ExecuteCommand(channelID, command) + if err != nil { + return nil, c.mapError(nil, err, "ExecuteSlashCommand") + } + return resp, nil +} + +// ==================== Team Administration ==================== + +// InviteUserToTeam adds a user to a team +func (c *Client) InviteUserToTeam(ctx context.Context, teamID, userID string) (*model.TeamMember, error) { + c.setToken(http.MethodPost) + member, resp, err := c.client.AddTeamMember(teamID, userID) + if err != nil { + return nil, c.mapError(resp, err, "InviteUserToTeam") + } + return member, nil +} + +// RemoveUserFromTeam removes a user from a team +func (c *Client) RemoveUserFromTeam(ctx context.Context, teamID, userID string) error { + _, err := c.executeWithRetry( + ctx, + "RemoveUserFromTeam", + http.MethodDelete, + fmt.Sprintf("/api/v4/teams/%s/members/%s", teamID, userID), + func() (*model.Response, error) { + r, e := c.client.RemoveTeamMember(teamID, userID) + return r, e + }, + ) + return err +} + +// ListTeamMembers gets members of a team +func (c *Client) ListTeamMembers(ctx context.Context, teamID string, page, perPage int) ([]*model.TeamMember, error) { + c.setToken(http.MethodGet) + members, resp, err := c.client.GetTeamMembers(teamID, page, perPage, "") + if err != nil { + return nil, c.mapError(resp, err, "ListTeamMembers") + } + return members, nil +} + +// GetTeamStats gets statistics for a team +func (c *Client) GetTeamStats(ctx context.Context, teamID string) (*model.TeamStats, error) { + c.setToken(http.MethodGet) + stats, resp, err := c.client.GetTeamStats(teamID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetTeamStats") + } + return stats, nil +} + +// ==================== Post Operations ==================== + +// GetPost retrieves a single post by ID +func (c *Client) GetPost(ctx context.Context, postID string) (*model.Post, error) { + c.setToken(http.MethodGet) + post, resp, err := c.client.GetPost(postID, "") + if err != nil { + return nil, c.mapError(resp, err, "GetPost") + } + return post, nil +} + +// ==================== Outgoing Webhooks ==================== + +// CreateOutgoingWebhook creates an outgoing webhook +func (c *Client) CreateOutgoingWebhook(ctx context.Context, teamID string, displayName string, triggerWords []string, callbackURL string) (*model.OutgoingWebhook, error) { + c.setToken(http.MethodPost) + + hook := &model.OutgoingWebhook{ + TeamId: teamID, + DisplayName: displayName, + TriggerWords: triggerWords, + CallbackURLs: []string{callbackURL}, + } + + created, resp, err := c.client.CreateOutgoingWebhook(hook) + if err != nil { + return nil, c.mapError(resp, err, "CreateOutgoingWebhook") + } + return created, nil +} + +// ListOutgoingWebhooks lists outgoing webhooks for a team +func (c *Client) ListOutgoingWebhooks(ctx context.Context, teamID string, page, perPage int) ([]*model.OutgoingWebhook, error) { + c.setToken(http.MethodGet) + hooks, resp, err := c.client.GetOutgoingWebhooksForTeam(teamID, page, perPage, "") + if err != nil { + return nil, c.mapError(resp, err, "ListOutgoingWebhooks") + } + return hooks, nil +} + +// DeleteOutgoingWebhook deletes an outgoing webhook +func (c *Client) DeleteOutgoingWebhook(ctx context.Context, hookID string) error { + _, err := c.executeWithRetry( + ctx, + "DeleteOutgoingWebhook", + http.MethodDelete, + fmt.Sprintf("/api/v4/hooks/outgoing/%s", hookID), + func() (*model.Response, error) { + r, e := c.client.DeleteOutgoingWebhook(hookID) + return r, e + }, + ) + return err +} + +// ==================== System & Config ==================== + +// GetSystemLogs retrieves system logs (requires admin) +func (c *Client) GetSystemLogs(ctx context.Context, page, perPage int) ([]string, *model.Response, error) { + c.setToken(http.MethodGet) + logs, resp, err := c.client.GetLogs(page, perPage) + if err != nil { + return nil, resp, c.mapError(resp, err, "GetSystemLogs") + } + return logs, resp, nil +} + +// GetConfig retrieves server configuration (requires admin) +func (c *Client) GetConfig(ctx context.Context) (*model.Config, error) { + c.setToken(http.MethodGet) + config, resp, err := c.client.GetConfig() + if err != nil { + return nil, c.mapError(resp, err, "GetConfig") + } + return config, nil +} diff --git a/pkg/mattermost/client_test.go b/pkg/mattermost/client_test.go new file mode 100644 index 0000000..eca4b53 --- /dev/null +++ b/pkg/mattermost/client_test.go @@ -0,0 +1,39 @@ +package mattermost + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + client := NewClient("https://test.mattermost.com", "bot-token", "pat-token") + assert.NotNil(t, client) + assert.Equal(t, "https://test.mattermost.com", client.host) + assert.Equal(t, "bot-token", client.botToken) + assert.Equal(t, "pat-token", client.pat) +} + +func TestGetToken_ReadOperation(t *testing.T) { + client := NewClient("https://test.mattermost.com", "bot-token", "pat-token") + token := client.getToken(false) + assert.Equal(t, "bot-token", token) +} + +func TestGetToken_WriteOperation(t *testing.T) { + client := NewClient("https://test.mattermost.com", "bot-token", "pat-token") + token := client.getToken(true) + assert.Equal(t, "pat-token", token) +} + +func TestGetToken_FallbackToPAT(t *testing.T) { + client := NewClient("https://test.mattermost.com", "", "pat-token") + token := client.getToken(false) + assert.Equal(t, "pat-token", token) +} + +func TestSetGlobalClient(t *testing.T) { + client := NewClient("https://test.mattermost.com", "bot-token", "pat-token") + SetGlobalClient(client) + assert.Equal(t, client, GetGlobalClient()) +} diff --git a/pkg/params/params.go b/pkg/params/params.go new file mode 100644 index 0000000..9c0bb97 --- /dev/null +++ b/pkg/params/params.go @@ -0,0 +1,122 @@ +package params + +import ( + "fmt" + "strconv" +) + +func GetString(args map[string]any, key string) (string, error) { + val, ok := args[key].(string) + if !ok { + return "", fmt.Errorf("%s is required", key) + } + return val, nil +} + +func GetOptionalString(args map[string]any, key, defaultVal string) string { + if val, ok := args[key].(string); ok { + return val + } + return defaultVal +} + +func GetStringSlice(args map[string]any, key string) []string { + val, ok := args[key] + if !ok { + return nil + } + sliceVal, ok := val.([]any) + if !ok { + return nil + } + out := make([]string, 0, len(sliceVal)) + for _, item := range sliceVal { + if s, ok := item.(string); ok { + out = append(out, s) + } + } + return out +} + +func GetPagination(args map[string]any, defaultPageSize int64) (page, pageSize int) { + return int(GetOptionalInt(args, "page", 1)), int(GetOptionalInt(args, "perPage", defaultPageSize)) +} + +func ToInt64(val any) (int64, bool) { + switch v := val.(type) { + case float64: + return int64(v), true + case string: + i, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return 0, false + } + return i, true + default: + return 0, false + } +} + +func GetIndex(args map[string]any, key string) (int64, error) { + val, exists := args[key] + if !exists { + return 0, fmt.Errorf("%s is required", key) + } + + if i, ok := ToInt64(val); ok { + return i, nil + } + + if s, ok := val.(string); ok { + return 0, fmt.Errorf("%s must be a valid integer (got %q)", key, s) + } + + return 0, fmt.Errorf("%s must be a number or numeric string", key) +} + +func GetInt64Slice(args map[string]any, key string) ([]int64, error) { + raw, ok := args[key].([]any) + if !ok { + return nil, fmt.Errorf("%s (array of IDs) is required", key) + } + out := make([]int64, 0, len(raw)) + for _, v := range raw { + id, ok := ToInt64(v) + if !ok { + return nil, fmt.Errorf("invalid ID in %s array", key) + } + out = append(out, id) + } + return out, nil +} + +func GetOptionalInt(args map[string]any, key string, defaultVal int64) int64 { + val, exists := args[key] + if !exists { + return defaultVal + } + if i, ok := ToInt64(val); ok { + return i + } + return defaultVal +} + +func GetOptionalBool(args map[string]any, key string, defaultVal bool) bool { + val, exists := args[key] + if !exists { + return defaultVal + } + + switch v := val.(type) { + case bool: + return v + case float64: + return v != 0 + case string: + if b, err := strconv.ParseBool(v); err == nil { + return b + } + } + + return defaultVal +} diff --git a/pkg/to/to.go b/pkg/to/to.go new file mode 100644 index 0000000..cea50b6 --- /dev/null +++ b/pkg/to/to.go @@ -0,0 +1,46 @@ +package to + +import ( + "encoding/json" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func Result(data interface{}) *mcp.CallToolResult { + content, err := json.MarshalIndent(data, "", " ") + if err != nil { + return Error(fmt.Errorf("failed to marshal result: %w", err)) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(content), + }, + }, + } +} + +func Text(text string) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: text, + }, + }, + } +} + +func Error(err error) *mcp.CallToolResult { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: err.Error(), + }, + }, + IsError: true, + } +} diff --git a/pkg/tool/tool.go b/pkg/tool/tool.go new file mode 100644 index 0000000..5002140 --- /dev/null +++ b/pkg/tool/tool.go @@ -0,0 +1,45 @@ +package tool + +import ( + "github.com/karti-ai/mattermost-mcp-server/pkg/flag" + "github.com/mark3labs/mcp-go/server" +) + +type Tool struct { + write []server.ServerTool + read []server.ServerTool +} + +func New() *Tool { + return &Tool{ + write: make([]server.ServerTool, 0, 100), + read: make([]server.ServerTool, 0, 100), + } +} + +func (t *Tool) RegisterWrite(s server.ServerTool) { + t.write = append(t.write, s) +} + +func (t *Tool) RegisterRead(s server.ServerTool) { + t.read = append(t.read, s) +} + +func (t *Tool) Tools() []server.ServerTool { + tools := make([]server.ServerTool, 0, len(t.write)+len(t.read)) + if flag.ReadOnly { + tools = append(tools, t.read...) + return tools + } + tools = append(tools, t.write...) + tools = append(tools, t.read...) + return tools +} + +func (t *Tool) ReadTools() []server.ServerTool { + return t.read +} + +func (t *Tool) WriteTools() []server.ServerTool { + return t.write +}