-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclient.go
More file actions
170 lines (153 loc) · 5.49 KB
/
Copy pathclient.go
File metadata and controls
170 lines (153 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// SPDX-License-Identifier: Apache-2.0
package tg
import (
"context"
"log/slog"
"net"
"net/http"
"strings"
"time"
)
// DefaultAPIBase is Telegram's own server. A self-hosted one is set with
// [WithAPIBase].
const DefaultAPIBase = "https://api.telegram.org"
// Client talks to one bot's Bot API endpoint. It is safe for concurrent use.
type Client struct {
token string
apiBase string
http *http.Client
timeout time.Duration
sleep func(context.Context, time.Duration) error
log *slog.Logger
observe Observer
filesRoot string
allowed []string
}
// Event describes one HTTP attempt against the Bot API. It carries no bot
// token and no response body, so it is safe to log or turn into metrics.
type Event struct {
// Method is the Bot API method name, e.g. "sendMessage".
Method string
// Status is the HTTP status, or 0 when the request never got a response.
Status int
// Duration covers the request and reading the response body.
Duration time.Duration
// Attempt counts from 1.
Attempt int
// Retryable reports whether this failure is of a kind this package retries
// -- a transport failure, a 429, a 5xx. It does not promise another attempt
// followed: the retry budget may already be spent, and a POST is replayed
// only when Telegram asked for it.
Retryable bool
// Err is the attempt's error, already redacted of the bot token.
Err error
// Probe marks a capability check rather than work the bot asked for. Those
// deliberately call a method with an empty body and are answered with a
// parameter error, so counting them as failures adds one phantom incident
// per probed method to every start.
Probe bool
}
// Observer is called once per HTTP attempt. It must not block: it runs on the
// calling goroutine, inside the request path.
type Observer func(Event)
// Option configures a Client.
type Option func(*Client)
// WithAPIBase points the client at a self-hosted Bot API server. An empty
// value keeps [DefaultAPIBase].
func WithAPIBase(base string) Option {
return func(c *Client) {
if base != "" {
c.apiBase = strings.TrimRight(base, "/")
}
}
}
// WithHTTPClient replaces the transport. The client must not set a global
// Timeout that is shorter than a long poll: every call carries its own context
// deadline instead.
func WithHTTPClient(h *http.Client) Option {
return func(c *Client) {
if h != nil {
c.http = h
}
}
}
// WithTimeout sets the per-attempt deadline for ordinary calls. getUpdates and
// file downloads derive their own, longer deadlines.
func WithTimeout(d time.Duration) Option {
return func(c *Client) {
if d > 0 {
c.timeout = d
}
}
}
// WithLogger attaches a logger. The client logs nothing above debug level on
// its own: errors are returned, not logged, so the caller decides.
func WithLogger(log *slog.Logger) Option {
return func(c *Client) {
if log != nil {
c.log = log
}
}
}
// WithAllowedUpdates restricts long polling to the update kinds the bot
// actually handles. Telegram's own default silently omits some kinds
// (my_chat_member among them), so a bot that needs one must say so. Passing
// nothing leaves the parameter out and keeps Telegram's default.
func WithAllowedUpdates(kinds ...string) Option {
return func(c *Client) { c.allowed = kinds }
}
// WithObserver registers a per-attempt callback for metrics.
func WithObserver(o Observer) Option {
return func(c *Client) { c.observe = o }
}
// WithLocalFiles enables reading media from the data directory of a Bot API
// server started with --local. root is the server's working directory as it is
// visible to this process, e.g. "/var/lib/telegram-bot-api"; the client only
// ever reads under root/<token>, which is the one subdirectory that belongs to
// this bot. Mounting the whole directory of a shared server would expose every
// other bot's token, because that is what its subdirectories are named after.
//
// Without this option an absolute file_path is a hard error: a --local server
// does not serve files over HTTP at all, so there is nothing to fall back to.
func WithLocalFiles(root string) Option {
return func(c *Client) { c.filesRoot = strings.TrimRight(root, "/") }
}
// New returns a client for token. It performs no I/O; call [Client.Preflight]
// at startup to find out whether the server on the other end can serve it.
func New(token string, opts ...Option) *Client {
c := &Client{
token: token,
apiBase: DefaultAPIBase,
http: defaultHTTPClient(),
timeout: 30 * time.Second,
sleep: sleep,
log: slog.New(slog.DiscardHandler),
}
for _, opt := range opts {
opt(c)
}
return c
}
// APIBase reports the server this client talks to.
func (c *Client) APIBase() string { return c.apiBase }
// IsLocalServer reports whether the client is pointed at something other than
// Telegram's own endpoint.
func (c *Client) IsLocalServer() bool { return c.apiBase != DefaultAPIBase }
// defaultHTTPClient has no global timeout — every attempt carries a context
// deadline — and raises MaxIdleConnsPerHost, whose default of 2 would
// serialize a pool of update workers against a single host.
func defaultHTTPClient() *http.Client {
return &http.Client{Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 64,
MaxIdleConnsPerHost: 16,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}}
}