mirror of
https://github.com/crazy-max/diun.git
synced 2024-12-22 19:38:28 +00:00
64f77319b2
Bumps [github.com/PaulSonOfLars/gotgbot/v2](https://github.com/PaulSonOfLars/gotgbot) from 2.0.0-rc.27 to 2.0.0-rc.30. - [Release notes](https://github.com/PaulSonOfLars/gotgbot/releases) - [Commits](https://github.com/PaulSonOfLars/gotgbot/compare/v2.0.0-rc.27...v2.0.0-rc.30) --- updated-dependencies: - dependency-name: github.com/PaulSonOfLars/gotgbot/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
254 lines
7.9 KiB
Go
254 lines
7.9 KiB
Go
package gotgbot
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// DefaultAPIURL is the default telegram API URL.
|
|
DefaultAPIURL = "https://api.telegram.org"
|
|
// DefaultTimeout is the default timeout to be set for all requests.
|
|
DefaultTimeout = time.Second * 5
|
|
)
|
|
|
|
type BotClient interface {
|
|
// RequestWithContext submits a POST HTTP request a bot API instance.
|
|
RequestWithContext(ctx context.Context, token string, method string, params map[string]string, data map[string]FileReader, opts *RequestOpts) (json.RawMessage, error)
|
|
// GetAPIURL gets the URL of the API either in use by the bot or defined in the request opts.
|
|
GetAPIURL(opts *RequestOpts) string
|
|
// FileURL gets the URL of a file at the API address that the bot is interacting with.
|
|
FileURL(token string, tgFilePath string, opts *RequestOpts) string
|
|
}
|
|
|
|
var _ BotClient = &BaseBotClient{}
|
|
|
|
type BaseBotClient struct {
|
|
// Client is the HTTP Client used for all HTTP requests made for this bot.
|
|
Client http.Client
|
|
// UseTestEnvironment defines whether this bot was created to run on telegram's test environment.
|
|
// Enabling this uses a slightly different API path.
|
|
// See https://core.telegram.org/bots/webapps#using-bots-in-the-test-environment for more details.
|
|
UseTestEnvironment bool
|
|
// Default opts to use for all requests, when no other request opts are specified.
|
|
DefaultRequestOpts *RequestOpts
|
|
}
|
|
|
|
type Response struct {
|
|
// Ok: if true, request was successful, and result can be found in the Result field.
|
|
// If false, error can be explained in the Description.
|
|
Ok bool `json:"ok"`
|
|
// Result: result of requests (if Ok)
|
|
Result json.RawMessage `json:"result"`
|
|
// ErrorCode: Integer error code of request. Subject to change in the future.
|
|
ErrorCode int `json:"error_code"`
|
|
// Description: contains a human readable description of the error result.
|
|
Description string `json:"description"`
|
|
// Parameters: Optional extra data which can help automatically handle the error.
|
|
Parameters *ResponseParameters `json:"parameters"`
|
|
}
|
|
|
|
type TelegramError struct {
|
|
// The telegram method which raised the error.
|
|
Method string
|
|
// The HTTP parameters which raised the error.
|
|
Params map[string]string
|
|
// The error code returned by telegram.
|
|
Code int
|
|
// The error description returned by telegram.
|
|
Description string
|
|
// The additional parameters returned by telegram
|
|
ResponseParams *ResponseParameters
|
|
}
|
|
|
|
func (t *TelegramError) Error() string {
|
|
return fmt.Sprintf("unable to %s: %s", t.Method, t.Description)
|
|
}
|
|
|
|
// RequestOpts defines any request-specific options used to interact with the telegram API.
|
|
type RequestOpts struct {
|
|
// Timeout for the HTTP request to the telegram API.
|
|
Timeout time.Duration
|
|
// Custom API URL to use for requests.
|
|
APIURL string
|
|
}
|
|
|
|
// getTimeoutContext returns the appropriate context for the current settings.
|
|
func (bot *BaseBotClient) getTimeoutContext(parentCtx context.Context, opts *RequestOpts) (context.Context, context.CancelFunc) {
|
|
if parentCtx == nil {
|
|
parentCtx = context.Background()
|
|
}
|
|
|
|
if opts != nil {
|
|
ctx, cancelFunc := timeoutFromOpts(parentCtx, opts)
|
|
if ctx != nil {
|
|
return ctx, cancelFunc
|
|
}
|
|
}
|
|
|
|
if bot.DefaultRequestOpts != nil {
|
|
ctx, cancelFunc := timeoutFromOpts(parentCtx, bot.DefaultRequestOpts)
|
|
if ctx != nil {
|
|
return ctx, cancelFunc
|
|
}
|
|
}
|
|
|
|
return context.WithTimeout(parentCtx, DefaultTimeout)
|
|
}
|
|
|
|
func timeoutFromOpts(parentCtx context.Context, opts *RequestOpts) (context.Context, context.CancelFunc) {
|
|
// nothing? no timeout.
|
|
if opts == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
if parentCtx == nil {
|
|
parentCtx = context.Background()
|
|
}
|
|
|
|
if opts.Timeout > 0 {
|
|
return context.WithTimeout(parentCtx, opts.Timeout)
|
|
|
|
} else if opts.Timeout < 0 {
|
|
// < 0 no timeout; infinite.
|
|
return parentCtx, func() {}
|
|
}
|
|
// 0 == nothing defined, use defaults.
|
|
return nil, nil
|
|
}
|
|
|
|
// RequestWithContext allows sending a POST request to the telegram bot API with an existing context.
|
|
// - ctx: the timeout contexts to be used.
|
|
// - method: the telegram API method to call.
|
|
// - params: map of parameters to be sending to the telegram API. eg: chat_id, user_id, etc.
|
|
// - data: map of any files to be sending to the telegram API.
|
|
// - opts: request opts to use.
|
|
func (bot *BaseBotClient) RequestWithContext(parentCtx context.Context, token string, method string, params map[string]string, data map[string]FileReader, opts *RequestOpts) (json.RawMessage, error) {
|
|
ctx, cancel := bot.getTimeoutContext(parentCtx, opts)
|
|
defer cancel()
|
|
|
|
var requestBody io.Reader
|
|
|
|
var contentType string
|
|
// Check if there are any files to upload. If yes, use multipart; else, use JSON.
|
|
if len(data) > 0 {
|
|
pr, pw := io.Pipe()
|
|
defer pr.Close() // avoid writer goroutine leak
|
|
mw := multipart.NewWriter(pw)
|
|
contentType = mw.FormDataContentType()
|
|
requestBody = pr
|
|
// Write the request data asynchronously from another goroutine
|
|
// to the multipart.Writer which will be piped into the pipe reader
|
|
// which is tied to the request to be sent
|
|
go func() {
|
|
writerError := fillBuffer(mw, params, data)
|
|
// Close the writer with error of multipart writer.
|
|
// If the error is nil, this will act just like pw.Close()
|
|
_ = pw.CloseWithError(writerError)
|
|
}()
|
|
} else {
|
|
contentType = "application/json"
|
|
bodyBytes, err := json.Marshal(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to encode parameters as JSON: %w", err)
|
|
}
|
|
requestBody = bytes.NewReader(bodyBytes)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, bot.methodEndpoint(token, method, opts), requestBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build POST request to %s: %w", method, err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", contentType)
|
|
|
|
resp, err := bot.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute POST request to %s: %w", method, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var r Response
|
|
if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
|
|
return nil, fmt.Errorf("failed to decode POST request to %s: %w", method, err)
|
|
}
|
|
|
|
if !r.Ok {
|
|
return nil, &TelegramError{
|
|
Method: method,
|
|
Params: params,
|
|
Code: r.ErrorCode,
|
|
Description: r.Description,
|
|
ResponseParams: r.Parameters,
|
|
}
|
|
}
|
|
|
|
return r.Result, nil
|
|
}
|
|
|
|
// Fill the buffer of multipart.Writer with data which is going to be sent.
|
|
func fillBuffer(w *multipart.Writer, params map[string]string, data map[string]FileReader) error {
|
|
for k, v := range params {
|
|
err := w.WriteField(k, v)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write multipart field %s with value %s: %w", k, v, err)
|
|
}
|
|
}
|
|
|
|
for field, file := range data {
|
|
fileName := file.Name
|
|
if fileName == "" {
|
|
fileName = field
|
|
}
|
|
|
|
part, err := w.CreateFormFile(field, fileName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create form file for field %s and fileName %s: %w", field, fileName, err)
|
|
}
|
|
|
|
_, err = io.Copy(part, file.Data)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy file contents of field %s to form: %w", field, err)
|
|
}
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return fmt.Errorf("failed to close multipart form writer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAPIURL returns the currently used API endpoint.
|
|
func (bot *BaseBotClient) GetAPIURL(opts *RequestOpts) string {
|
|
if opts != nil && opts.APIURL != "" {
|
|
return strings.TrimSuffix(opts.APIURL, "/")
|
|
}
|
|
if bot.DefaultRequestOpts != nil && bot.DefaultRequestOpts.APIURL != "" {
|
|
return strings.TrimSuffix(bot.DefaultRequestOpts.APIURL, "/")
|
|
}
|
|
return DefaultAPIURL
|
|
}
|
|
|
|
func (bot *BaseBotClient) FileURL(token string, tgFilePath string, opts *RequestOpts) string {
|
|
return fmt.Sprintf("%s/file/%s/%s", bot.GetAPIURL(opts), bot.getEnvAuth(token), tgFilePath)
|
|
}
|
|
|
|
func (bot *BaseBotClient) getEnvAuth(token string) string {
|
|
if bot.UseTestEnvironment {
|
|
return "bot" + token + "/test"
|
|
}
|
|
return "bot" + token
|
|
}
|
|
|
|
func (bot *BaseBotClient) methodEndpoint(token string, method string, opts *RequestOpts) string {
|
|
return fmt.Sprintf("%s/%s/%s", bot.GetAPIURL(opts), bot.getEnvAuth(token), method)
|
|
}
|