Global code ovehaul - remove CAS and other, add 15 minute greet check
- Remove CAS, as it no longer works for us - Remove other checks - Implement 15 minute greet timeout check - Fix chat logos - Ditch thirdparty logger, use stdlib log facility wrapper for logging
This commit is contained in:
10
vendor/github.com/NicoNex/echotron/v3/.gitignore
generated
vendored
Normal file
10
vendor/github.com/NicoNex/echotron/v3/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/example/polling/polling
|
||||
/example/polling-fsm/polling-fsm
|
||||
/example/polling-fsm-lifecycle/polling-fsm-lifecycle
|
||||
/example/polling-fsm-persistence/polling-fsm-persistence
|
||||
/example/polling-inline/polling-inline
|
||||
/example/polling-keyboard/polling-keyboard
|
||||
/example/polling-ratelimit/polling-ratelimit
|
||||
/example/polling-simple/polling-simple
|
||||
/example/webhook/webhook
|
||||
/example/webhook-simple/webhook-simple
|
||||
629
vendor/github.com/NicoNex/echotron/v3/README.md
generated
vendored
629
vendor/github.com/NicoNex/echotron/v3/README.md
generated
vendored
@@ -1,339 +1,480 @@
|
||||
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://codecov.io/gh/NicoNex/echotron) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
|
||||
| :------: |
|
||||
<img src="assets/readme_banner.png" alt="Echotron" width="800">
|
||||
|
||||
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.
|
||||
<div align="center">
|
||||
|
||||
Fetch with
|
||||
[](https://golang.org/)
|
||||
[](https://pkg.go.dev/github.com/NicoNex/echotron/v3)
|
||||
[](https://goreportcard.com/report/github.com/NicoNex/echotron/v3)
|
||||
[](https://codecov.io/gh/NicoNex/echotron)
|
||||
[](https://github.com/NicoNex/echotron/blob/master/LICENSE)
|
||||
[](https://github.com/avelino/awesome-go)
|
||||
[](https://t.me/echotronnews)
|
||||
|
||||
**The idiomatic, concurrent Telegram Bot library for Go.**
|
||||
Zero boilerplate. Built-in rate limiting. One instance per chat, by design.
|
||||
|
||||
</div>
|
||||
|
||||
## Why Echotron?
|
||||
|
||||
Most Telegram bot libraries hand you a stream of updates and leave everything else to you. Echotron goes further: it ships a battle-tested concurrency model out of the box, so you can focus on bot logic instead of synchronisation primitives, rate limiters, and state machinery.
|
||||
|
||||
```bash
|
||||
go get github.com/NicoNex/echotron/v3
|
||||
```
|
||||
|
||||
## Example
|
||||
### Simplest implementations
|
||||
#### Long polling
|
||||
```golang
|
||||
## Five-line bot
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/NicoNex/echotron/v3"
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
|
||||
func main() {
|
||||
api := echotron.NewAPI(token)
|
||||
|
||||
for u := range echotron.PollingUpdates(token) {
|
||||
if u.Message.Text == "/start" {
|
||||
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||
}
|
||||
}
|
||||
api := echotron.NewAPI("MY_TOKEN")
|
||||
|
||||
for u := range echotron.PollingUpdates("MY_TOKEN") {
|
||||
if u.Message.Text == "/start" {
|
||||
api.SendMessage("Hello, world!", u.ChatID(), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
#### Webhook
|
||||
```golang
|
||||
package main
|
||||
|
||||
import "github.com/NicoNex/echotron/v3"
|
||||
No setup, no registration, no middleware stack. `PollingUpdates` returns a plain Go channel: range over it and you are done.
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
## The dispatcher pattern
|
||||
|
||||
func main() {
|
||||
api := echotron.NewAPI(token)
|
||||
For production bots, Echotron's `Dispatcher` automatically maintains one isolated bot instance per chat. Each instance gets its own goroutine; state is naturally scoped per conversation.
|
||||
|
||||
for u := range echotron.WebhookUpdates("https://example.com:443/my_token", token) {
|
||||
if u.Message.Text == "/start" {
|
||||
api.SendMessage("Hello world", u.ChatID(), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
For more scalable and recommended implementations see the other examples.
|
||||
|
||||
### Long Polling
|
||||
|
||||
```golang
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// Struct useful for managing internal states in your bot, but it could be of
|
||||
// any type such as `type bot int64` if you only need to store the chatID.
|
||||
// bot holds the per-chat state. Each conversation gets its own instance.
|
||||
// Embedding echotron.API promotes all Telegram methods onto the struct,
|
||||
// so b.SendMessage works directly without any explicit delegation.
|
||||
type bot struct {
|
||||
chatID int64
|
||||
echotron.API
|
||||
chatID int64
|
||||
echotron.API
|
||||
}
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
|
||||
// This function needs to be of type 'echotron.NewBotFn' and is called by
|
||||
// the echotron dispatcher upon any new message from a chatID that has never
|
||||
// interacted with the bot before.
|
||||
// This means that echotron keeps one instance of the echotron.Bot implementation
|
||||
// for each chat where the bot is used.
|
||||
// newBot is the factory function called by the Dispatcher the first time
|
||||
// a given chatID sends a message. It must return an echotron.Bot.
|
||||
func newBot(chatID int64) echotron.Bot {
|
||||
return &bot{
|
||||
chatID,
|
||||
echotron.NewAPI(token),
|
||||
}
|
||||
return &bot{chatID, echotron.NewAPI("MY_TOKEN")}
|
||||
}
|
||||
|
||||
// This method is needed to implement the echotron.Bot interface.
|
||||
// Update is the only method required by the echotron.Bot interface.
|
||||
// The Dispatcher calls it in a new goroutine for every incoming update,
|
||||
// so each chat is handled concurrently without blocking the others.
|
||||
func (b *bot) Update(update *echotron.Update) {
|
||||
if update.Message.Text == "/start" {
|
||||
b.SendMessage("Hello world", b.chatID, nil)
|
||||
}
|
||||
if update.Message.Text == "/start" {
|
||||
b.SendMessage("Hello!", b.chatID, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// This is the entry point of echotron library.
|
||||
dsp := echotron.NewDispatcher(token, newBot)
|
||||
for {
|
||||
log.Println(dsp.Poll())
|
||||
// In case of connection issues wait 5 seconds before trying to reconnect.
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
dsp := echotron.NewDispatcher("MY_TOKEN", newBot)
|
||||
for {
|
||||
// Poll blocks until a network error occurs, then returns it.
|
||||
// Sleeping before retrying avoids hammering the API on transient failures.
|
||||
log.Println(dsp.Poll())
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design
|
||||
| Concern | Handled by |
|
||||
|---|---|
|
||||
| Routing updates to the right chat | `Dispatcher` |
|
||||
| Creating state for first-time users | `newBot` factory |
|
||||
| Calling `Update` concurrently | `Dispatcher` (goroutine per update) |
|
||||
| Rate limiting API calls | built-in `lclient` |
|
||||
| Deduplication and offset tracking | `Dispatcher.Poll()` |
|
||||
|
||||
**Echotron** makes a new instance of the struct bot for each open chat with a Telegram user, channel or group.
|
||||
This allows to:
|
||||
- safely call the `Update(*echotron.Update)` method concurrently
|
||||
- give to the user a convenient way to manage the bot internal states across all the chats
|
||||
- make sure that, even if one instance of the bot is deadlocked, the other ones keep running just fine, making the bot work for other users without any issues and/or slowdowns.
|
||||
## A library, not a framework
|
||||
|
||||
Please note that the the aforementioned behaviour is dictated by the `echotron.Dispatcher` object whose usage is not mandatory and for special needs can be ignored and implemented in different ways still keeping all the methods in the `echotron.API` object.
|
||||
Echotron does not ask you to structure your application in any particular way. There is no command router to register, no middleware stack to assemble, no lifecycle hooks to implement, no configuration object to fill out before anything works.
|
||||
|
||||
**Echotron** is designed to be as similar to the official [Telegram API](https://core.telegram.org/bots/api) as possible, but there are some things to take into account before starting to work with this library.
|
||||
The `Dispatcher` is entirely optional. If you do not need per-chat state management, you can drive updates yourself with a plain channel:
|
||||
|
||||
- The methods have the exact same name, but with a capital first letter, since in Go methods have to start with a capital letter to be exported.
|
||||
_Example: `sendMessage` becomes `SendMessage`_
|
||||
- The order of the parameters in some methods is different than in the official Telegram API, so refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for the correct one.
|
||||
- The only `chat_id` (or, in this case, `chatID`) type supported is `int64`, instead of the "Integer or String" requirement of the official API. That's because numeric IDs can't change in any way, which isn't the case with text-based usernames.
|
||||
- In some methods, you might find a `InputFile` type parameter. [`InputFile`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#InputFile) is a struct with unexported fields, since only three combination of fields are valid, which can be obtained through the methods [`NewInputFileID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileID), [`NewInputFilePath`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFilePath) and [`NewInputFileBytes`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInputFileBytes).
|
||||
- In some methods, you might find a `MessageIDOptions` type parameter. [`MessageIDOptions`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#MessageIDOptions) is another struct with unexported fields, since only two combination of field are valid, which can be obtained through the methods [`NewMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewMessageID) and [`NewInlineMessageID`](https://pkg.go.dev/github.com/NicoNex/echotron/v3#NewInlineMessageID).
|
||||
- Optional parameters can be added by passing the correct struct to each method that might request optional parameters. If you don't want to pass any optional parameter, `nil` is more than enough. Refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) to check for each method's optional parameters struct: it's the type of the `opts` parameter.
|
||||
- Some parameters are hardcoded to avoid putting random stuff which isn't recognized by the Telegram API. Some notable examples are [`ParseMode`](https://github.com/NicoNex/echotron/blob/master/options.go#L21), [`ChatAction`](https://github.com/NicoNex/echotron/blob/master/options.go#L54) and [`InlineQueryType`](https://github.com/NicoNex/echotron/blob/master/inline.go#L27). For a full list of custom hardcoded parameters, refer to the [docs](https://pkg.go.dev/github.com/NicoNex/echotron/v3) for each custom type: by clicking on the type's name, you'll get the source which contains the possible values for that type.
|
||||
```go
|
||||
for u := range echotron.PollingUpdates("MY_TOKEN") {
|
||||
// your logic here, completely vanilla Go
|
||||
}
|
||||
```
|
||||
|
||||
## Additional examples
|
||||
### Functional approach to state management
|
||||
```golang
|
||||
package main
|
||||
If you want webhooks without the Dispatcher, that is a channel too:
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
```go
|
||||
for u := range echotron.WebhookUpdates("https://example.com:443/MY_TOKEN", "MY_TOKEN") {
|
||||
// handle u
|
||||
}
|
||||
```
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
If you need only a subset of the Telegram API for a quick script or a one-off tool, instantiate `NewAPI` directly and call whatever methods you need. Nothing forces you to go further.
|
||||
|
||||
// Recursive type definition of the bot state function.
|
||||
The `Bot` interface itself requires a single method:
|
||||
|
||||
```go
|
||||
type Bot interface {
|
||||
Update(*Update)
|
||||
}
|
||||
```
|
||||
|
||||
That one line is the entire contract. Every other piece of Echotron is additive: pick what fits your use case and ignore the rest.
|
||||
|
||||
## Feature highlights
|
||||
|
||||
### One instance per chat
|
||||
|
||||
The `Dispatcher` stores one `Bot` per `chatID` in a lock-free `sync.Map`. `Update()` is dispatched in a fresh goroutine for every incoming message:
|
||||
|
||||
- No chat ever blocks another: a deadlock in chat A has zero effect on chat B.
|
||||
- State lives in struct fields: no need for maps, mutexes, or context keys to correlate users.
|
||||
- Crashes are isolated: a panic in one goroutine does not bring down the whole bot.
|
||||
|
||||
### Built-in dual-level rate limiting
|
||||
|
||||
Echotron ships a transparent, dual-layer rate limiter that mirrors Telegram's own limits. It is always active with sensible defaults and requires no configuration to be correct from day one.
|
||||
|
||||
| Limiter | Default | How to change |
|
||||
|---|---|---|
|
||||
| Global (all chats) | 30 req/s, burst 30 | `api.SetGlobalRequestLimit(interval, burst)` |
|
||||
| Per-chat | 20 req/min, burst 20 | `api.SetChatRequestLimit(interval, burst)` |
|
||||
|
||||
One shared `http.Client` per bot token means connection pools are reused across all chat instances, keeping resource usage proportional to the number of bots, not the number of users.
|
||||
|
||||
### Functional state machines
|
||||
|
||||
Multi-step conversations can be modelled as a self-referential function type that returns the next state. No string enums, no external table, no switch-on-state:
|
||||
|
||||
```go
|
||||
// stateFn is a function that handles one update and returns the next state.
|
||||
// Because it references itself in its own return type, states form a chain:
|
||||
// each handler decides at runtime which function should run next.
|
||||
type stateFn func(*echotron.Update) stateFn
|
||||
|
||||
type bot struct {
|
||||
chatID int64
|
||||
state stateFn
|
||||
name string
|
||||
echotron.API
|
||||
chatID int64
|
||||
state stateFn // current state; replaced after every update
|
||||
name string
|
||||
echotron.API
|
||||
}
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
|
||||
func newBot(chatID int64) echotron.Bot {
|
||||
bot := &bot{
|
||||
chatID: chatID,
|
||||
API: echotron.NewAPI(token),
|
||||
}
|
||||
// We set the default state to the bot.handleMessage method.
|
||||
bot.state = bot.handleMessage
|
||||
return bot
|
||||
b := &bot{chatID: chatID, API: echotron.NewAPI("MY_TOKEN")}
|
||||
b.state = b.handleMessage // set the initial state
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bot) Update(update *echotron.Update) {
|
||||
// Here we execute the current state and set the next one.
|
||||
b.state = b.state(update)
|
||||
// Execute the current state and store whatever it returns as the next one.
|
||||
// A single assignment is all the state-machine machinery needed.
|
||||
b.state = b.state(update)
|
||||
}
|
||||
|
||||
func (b *bot) handleMessage(update *echotron.Update) stateFn {
|
||||
if strings.HasPrefix(update.Message.Text, "/set_name") {
|
||||
b.SendMessage("Send me my new name!", b.chatID, nil)
|
||||
// Here we return b.handleName since next time we receive a message it
|
||||
// will be the new name.
|
||||
return b.handleName
|
||||
}
|
||||
return b.handleMessage
|
||||
func (b *bot) handleMessage(u *echotron.Update) stateFn {
|
||||
if u.Message.Text == "/setname" {
|
||||
b.SendMessage("What should I call you?", b.chatID, nil)
|
||||
// The next update will be the user's reply, so transition to handleName.
|
||||
return b.handleName
|
||||
}
|
||||
// No relevant command: stay in the default state.
|
||||
return b.handleMessage
|
||||
}
|
||||
|
||||
func (b *bot) handleName(update *echotron.Update) stateFn {
|
||||
b.name = update.Message.Text
|
||||
b.SendMessage(fmt.Sprintf("My new name is %q", b.name), b.chatID, nil)
|
||||
// Here we return b.handleMessage since the next time we receive a message
|
||||
// it will be handled in the default way.
|
||||
return b.handleMessage
|
||||
}
|
||||
|
||||
func main() {
|
||||
dsp := echotron.NewDispatcher(token, newBot)
|
||||
log.Println(dsp.Poll())
|
||||
func (b *bot) handleName(u *echotron.Update) stateFn {
|
||||
b.name = u.Message.Text
|
||||
b.SendMessage("Got it, "+b.name+"!", b.chatID, nil)
|
||||
// Name has been recorded; go back to the default state.
|
||||
return b.handleMessage
|
||||
}
|
||||
```
|
||||
|
||||
### Self destruction for lower memory footprint
|
||||
```golang
|
||||
package main
|
||||
### Session self-destruction
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
type bot struct {
|
||||
chatID int64
|
||||
echotron.API
|
||||
}
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
|
||||
var dsp *echotron.Dispatcher
|
||||
Inactive sessions can remove themselves from the dispatcher, keeping memory usage proportional to currently active conversations rather than all users who ever interacted with the bot:
|
||||
|
||||
```go
|
||||
func newBot(chatID int64) echotron.Bot {
|
||||
bot := &bot{
|
||||
chatID,
|
||||
echotron.NewAPI(token),
|
||||
}
|
||||
go bot.selfDestruct(time.After(time.Hour))
|
||||
return bot
|
||||
b := &bot{chatID, echotron.NewAPI("MY_TOKEN")}
|
||||
// Launch the timer in a separate goroutine so newBot returns immediately.
|
||||
// The bot instance is fully functional while selfDestruct waits in the background.
|
||||
go b.selfDestruct(time.After(time.Hour))
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *bot) selfDestruct(timech <-chan time.Time) {
|
||||
<-timech
|
||||
b.SendMessage("goodbye", b.chatID, nil)
|
||||
dsp.DelSession(b.chatID)
|
||||
}
|
||||
|
||||
func (b *bot) Update(update *echotron.Update) {
|
||||
if update.Message.Text == "/start" {
|
||||
b.SendMessage("Hello world", b.chatID, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
dsp = echotron.NewDispatcher(token, newBot)
|
||||
log.Println(dsp.Poll())
|
||||
func (b *bot) selfDestruct(ch <-chan time.Time) {
|
||||
<-ch // block until the timer fires
|
||||
b.SendMessage("Goodbye!", b.chatID, nil)
|
||||
// Remove this instance from the dispatcher's session map.
|
||||
// After this call the struct will be garbage-collected once no other
|
||||
// references remain, freeing all per-chat state automatically.
|
||||
dsp.DelSession(b.chatID)
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook
|
||||
### Webhook support, with or without a custom server
|
||||
|
||||
```golang
|
||||
package main
|
||||
Minimal webhook:
|
||||
|
||||
import "github.com/NicoNex/echotron/v3"
|
||||
```go
|
||||
dsp := echotron.NewDispatcher("MY_TOKEN", newBot)
|
||||
dsp.ListenWebhook("https://example.com:443/MY_TOKEN")
|
||||
```
|
||||
|
||||
type bot struct {
|
||||
chatID int64
|
||||
echotron.API
|
||||
}
|
||||
Integrated into an existing HTTP server without displacing your own routes:
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
```go
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/login", loginHandler)
|
||||
|
||||
func newBot(chatID int64) echotron.Bot {
|
||||
return &bot{
|
||||
chatID,
|
||||
echotron.NewAPI(token),
|
||||
}
|
||||
}
|
||||
server := &http.Server{Addr: ":8080", Handler: mux}
|
||||
dsp.SetHTTPServer(server)
|
||||
dsp.ListenWebhook("https://example.com:8080/MY_TOKEN")
|
||||
```
|
||||
|
||||
func (b *bot) Update(update *echotron.Update) {
|
||||
if update.Message.Text == "/start" {
|
||||
b.SendMessage("Hello world", b.chatID, nil)
|
||||
}
|
||||
}
|
||||
Gzip-compressed payloads from Telegram are handled transparently.
|
||||
|
||||
func main() {
|
||||
dsp := echotron.NewDispatcher(token, newBot)
|
||||
dsp.ListenWebhook("https://example.com:443/my_bot_token")
|
||||
### Direct API parity
|
||||
|
||||
Echotron maps 1-to-1 to the [official Telegram Bot API](https://core.telegram.org/bots/api). Method names are identical, just capitalised as required by Go:
|
||||
|
||||
```
|
||||
sendMessage → SendMessage
|
||||
sendPhoto → SendPhoto
|
||||
answerCallbackQuery → AnswerCallbackQuery
|
||||
```
|
||||
|
||||
No invented abstractions sit between you and the Telegram docs.
|
||||
|
||||
### Type-safe options and enum constants
|
||||
|
||||
Optional parameters are typed structs, not variadic `interface{}` bags. Enum values are compile-time constants so the compiler catches typos:
|
||||
|
||||
```go
|
||||
b.SendMessage("*bold*", b.chatID, &echotron.MessageOptions{
|
||||
ParseMode: echotron.MarkdownV2,
|
||||
})
|
||||
|
||||
b.SendChatAction(b.chatID, echotron.Typing)
|
||||
```
|
||||
|
||||
Pass `nil` for optional parameters when you do not need them.
|
||||
|
||||
### Structured API errors
|
||||
|
||||
Errors from Telegram are typed `*APIError` values, not raw strings, so you can inspect the error code and description separately:
|
||||
|
||||
```go
|
||||
_, err := api.SendMessage("hello", chatID, nil)
|
||||
var apiErr *echotron.APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
fmt.Println(apiErr.ErrorCode(), apiErr.Description())
|
||||
}
|
||||
```
|
||||
|
||||
### Local Bot API server support
|
||||
|
||||
### Webhook with a custom http.Server
|
||||
Running a [Telegram Local Bot API](https://github.com/tdlib/telegram-bot-api) server for increased file size limits and upload throughput? One function call is all it takes:
|
||||
|
||||
This is an example for a custom http.Server which handles your own specified routes
|
||||
and also the webhook route which is specified by ListenWebhook.
|
||||
```go
|
||||
api := echotron.CustomAPI("http://localhost:8081/bot", "MY_TOKEN")
|
||||
```
|
||||
|
||||
```golang
|
||||
package main
|
||||
All methods route through your local server with no further changes.
|
||||
|
||||
import (
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
## Design gems
|
||||
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
The following are a few of the internal decisions that make Echotron pleasant to work with and correct under concurrent load.
|
||||
|
||||
type bot struct {
|
||||
chatID int64
|
||||
echotron.API
|
||||
}
|
||||
### A generic, type-safe sync.Map wrapper
|
||||
|
||||
const token = "MY TELEGRAM TOKEN"
|
||||
Session storage is backed by a generic type declared as:
|
||||
|
||||
func newBot(chatID int64) echotron.Bot {
|
||||
return &bot{
|
||||
chatID,
|
||||
echotron.NewAPI(token),
|
||||
}
|
||||
}
|
||||
```go
|
||||
type smap[K, V any] sync.Map
|
||||
```
|
||||
|
||||
func (b *bot) Update(update *echotron.Update) {
|
||||
if update.Message.Text == "/start" {
|
||||
b.SendMessage("Hello world", b.chatID, nil)
|
||||
}
|
||||
}
|
||||
This thin wrapper over `sync.Map` provides full type safety without any runtime overhead. There are no `interface{}` assertions scattered through the codebase and no risk of a type mismatch crashing the dispatcher at runtime.
|
||||
|
||||
func main() {
|
||||
termChan := make(chan os.Signal, 1) // Channel for terminating the app via os.Interrupt signal
|
||||
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
### Three levels of embedding, zero indirection
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle user login
|
||||
})
|
||||
mux.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle user logout
|
||||
})
|
||||
mux.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Tell something about your awesome telegram bot
|
||||
})
|
||||
`API` embeds `*lclient` as an anonymous field. `lclient` carries the rate-limiter methods `SetGlobalRequestLimit` and `SetChatRequestLimit`. Because the user's bot struct embeds `echotron.API`, those same methods are promoted all the way to the bot struct level. Calling `b.SetGlobalRequestLimit(...)` from inside your bot just works, with no explicit delegation code anywhere.
|
||||
|
||||
// Set custom http.Server
|
||||
server := &http.Server{Addr: ":8080", Handler: mux}
|
||||
### Atomic client initialisation without a mutex
|
||||
|
||||
go func() {
|
||||
<-termChan
|
||||
// Perform some cleanup..
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}()
|
||||
When a new bot token is first used, Echotron creates an `http.Client` and stores it in a package-level cache keyed by base URL. The initialisation uses `sync.Map.LoadOrStore` atomically: if two goroutines race to create the client at the same time, exactly one wins and both receive the same pointer. No mutex, no double-checked locking pattern, no `sync.Once` per token.
|
||||
|
||||
// Capture the interrupt signal for app termination handling
|
||||
dsp := echotron.NewDispatcher(token, newBot)
|
||||
dsp.SetHTTPServer(server)
|
||||
// Start your custom http.Server with a registered /my_bot_token handler.
|
||||
log.Println(dsp.ListenWebhook("https://example.com/my_bot_token"))
|
||||
### `Update.ChatID()` absorbs all update types
|
||||
|
||||
The `ChatID()` method on `Update` inspects every possible update variant, from plain messages and callback queries to business connections, story interactions, chat boosts, and more, and always returns the correct `int64`. The dispatcher calls this one method to route any update to the right bot instance, regardless of its type.
|
||||
|
||||
### `InputFile` is a sealed type
|
||||
|
||||
`InputFile` has only unexported fields. The only way to create one is through:
|
||||
|
||||
```go
|
||||
echotron.NewInputFileID("AgACAgI...") // existing Telegram file
|
||||
echotron.NewInputFilePath("photo.jpg") // local file on disk
|
||||
echotron.NewInputFileBytes("img.png", data) // in-memory bytes
|
||||
```
|
||||
|
||||
It is structurally impossible to create an `InputFile` in an invalid state from outside the package.
|
||||
|
||||
### A single dispatch choke point
|
||||
|
||||
Every API call, whether a plain GET, a form-encoded POST, or a multipart file upload, passes through the internal `lclient.dispatch()` function. Rate limiting, HTTP execution, JSON decoding, and error checking all happen exactly once, in one place. There is no duplicated error handling across the hundreds of API methods.
|
||||
|
||||
### The `stateFn` self-referential type
|
||||
|
||||
`type stateFn func(*Update) stateFn` is a function type that returns itself. This single line enables recursive, allocation-free state machines where the current state is simply the function that will handle the next update. No string-keyed state table, no `iota` enum, no external dependency.
|
||||
|
||||
## Echotron vs. other Go Telegram libraries
|
||||
|
||||
| Feature | **Echotron** | go-telegram-bot-api | telebot | gotgbot |
|
||||
|---|:---:|:---:|:---:|:---:|
|
||||
| Per-chat isolated state | ✅ built-in | ❌ manual | ❌ manual | ❌ manual |
|
||||
| Concurrency model | ✅ built-in | ❌ manual | ⚠️ partial | ❌ manual |
|
||||
| Built-in rate limiting | ✅ dual-level | ❌ | ❌ | ❌ |
|
||||
| Shared HTTP client pool | ✅ per-token | ❌ | ❌ | ❌ |
|
||||
| 1:1 Telegram API parity | ✅ | ✅ | ⚠️ | ✅ |
|
||||
| Type-safe enums | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
||||
| Typed API errors | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
||||
| Functional state machines | ✅ first-class | ❌ | ❌ | ❌ |
|
||||
| Session self-destruction | ✅ | ❌ | ❌ | ❌ |
|
||||
| Custom HTTP server | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
||||
| Local API server support | ✅ | ✅ | ✅ | ✅ |
|
||||
| No mandatory abstractions | ✅ | ✅ | ❌ | ✅ |
|
||||
| External dependencies | 1 | 0 | several | 0 |
|
||||
| License | LGPL-3.0 | MIT | MIT | MIT |
|
||||
|
||||
On the topic of dependencies: Echotron's single dependency is [`golang.org/x/time`](https://pkg.go.dev/golang.org/x/time), which lives in the `golang.org/x` namespace. That namespace is maintained by the Go team itself, under the same review standards and stability guarantees as the standard library. In practice, adding Echotron to your project means depending on the Go team's own code and nothing else.
|
||||
|
||||
**On the license:** LGPL-3.0 allows you to use Echotron in closed-source and commercial products without releasing your own code, provided you do not modify Echotron itself.
|
||||
|
||||
## More examples
|
||||
|
||||
### Sending files
|
||||
|
||||
```go
|
||||
// From a local path
|
||||
b.SendPhoto(b.chatID, echotron.NewInputFilePath("photo.jpg"), nil)
|
||||
|
||||
// By Telegram file_id, no re-upload
|
||||
b.SendPhoto(b.chatID, echotron.NewInputFileID("AgACAgI..."), nil)
|
||||
|
||||
// From raw bytes already in memory
|
||||
b.SendPhoto(b.chatID, echotron.NewInputFileBytes("photo.jpg", data), nil)
|
||||
```
|
||||
|
||||
### Media groups
|
||||
|
||||
```go
|
||||
b.SendMediaGroup(b.chatID, []echotron.GroupableInputMedia{
|
||||
echotron.InputMediaPhoto{Media: echotron.NewInputFilePath("a.jpg")},
|
||||
echotron.InputMediaPhoto{Media: echotron.NewInputFilePath("b.jpg")},
|
||||
echotron.InputMediaVideo{Media: echotron.NewInputFilePath("clip.mp4")},
|
||||
}, nil)
|
||||
```
|
||||
|
||||
### Inline keyboards
|
||||
|
||||
```go
|
||||
b.SendMessage("Choose an option:", b.chatID, &echotron.MessageOptions{
|
||||
ReplyMarkup: echotron.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]echotron.InlineKeyboardButton{
|
||||
{{Text: "Option A", CallbackData: "a"}},
|
||||
{{Text: "Option B", CallbackData: "b"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Handling callback queries
|
||||
|
||||
```go
|
||||
func (b *bot) Update(u *echotron.Update) {
|
||||
// Telegram guarantees that at most one field in an Update is non-nil,
|
||||
// so a switch on nil checks is the idiomatic way to route update types.
|
||||
switch {
|
||||
case u.CallbackQuery != nil:
|
||||
// AnswerCallbackQuery must be called to dismiss the loading indicator
|
||||
// shown by Telegram on the user's side after they tap a button.
|
||||
b.AnswerCallbackQuery(u.CallbackQuery.ID, nil)
|
||||
b.SendMessage("You chose: "+u.CallbackQuery.Data, b.chatID, nil)
|
||||
case u.Message != nil:
|
||||
b.SendMessage("Send me a button press!", b.chatID, nil)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom rate limits
|
||||
|
||||
```go
|
||||
api := echotron.NewAPI("MY_TOKEN")
|
||||
|
||||
api.SetGlobalRequestLimit(time.Second/50, 50) // 50 req/s globally
|
||||
api.SetChatRequestLimit(time.Second, 1) // 1 msg/s per chat
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/NicoNex/echotron/v3
|
||||
```
|
||||
|
||||
Go 1.21 or later is required.
|
||||
|
||||
## Starter templates
|
||||
|
||||
Every example in the [`example/`](./example/) directory is a self-contained Go
|
||||
module usable as a project skeleton with
|
||||
[`gonew`](https://pkg.go.dev/golang.org/x/tools/cmd/gonew).
|
||||
|
||||
```bash
|
||||
go install golang.org/x/tools/cmd/gonew@latest
|
||||
```
|
||||
|
||||
| Template | What it gives you |
|
||||
|---|---|
|
||||
| `polling-simple` | Minimal stateless bot, plain update channel, no `Dispatcher` |
|
||||
| `polling` | Per-chat stateful bot with `Dispatcher` and long-polling |
|
||||
| `polling-keyboard` | Inline keyboards and callback query handling |
|
||||
| `polling-inline` | Inline mode (`@botname <query>` from any chat) |
|
||||
| `polling-ratelimit` | Rate limiter configuration |
|
||||
| `polling-fsm` | Multi-step conversations via functional state machines |
|
||||
| `polling-fsm-lifecycle` | FSM + session self-destruction on idle timeout |
|
||||
| `polling-fsm-persistence` | FSM + disk persistence with [katalis](https://github.com/NicoNex/katalis) |
|
||||
| `webhook` | `Dispatcher` with webhook delivery |
|
||||
| `webhook-simple` | Minimal stateless bot on webhooks |
|
||||
|
||||
Clone any template and rename the module in one command:
|
||||
|
||||
```bash
|
||||
gonew github.com/NicoNex/echotron/v3/example/polling github.com/you/mybot
|
||||
cd mybot
|
||||
TELEGRAM_TOKEN=<your-token> go run .
|
||||
```
|
||||
|
||||
See [`example/README.md`](example/README.md) for the full list and a suggested reading order.
|
||||
|
||||
## Links
|
||||
|
||||
- [pkg.go.dev documentation](https://pkg.go.dev/github.com/NicoNex/echotron/v3)
|
||||
- [Telegram Bot API reference](https://core.telegram.org/bots/api)
|
||||
- [Echotron News on Telegram](https://t.me/echotronnews)
|
||||
- [Report an issue](https://github.com/NicoNex/echotron/issues)
|
||||
|
||||
## License
|
||||
|
||||
Echotron is free software released under the **GNU Lesser General Public License v3.0**.
|
||||
See [LICENSE](LICENSE) for details.
|
||||
|
||||
730
vendor/github.com/NicoNex/echotron/v3/api.go
generated
vendored
730
vendor/github.com/NicoNex/echotron/v3/api.go
generated
vendored
File diff suppressed because it is too large
Load Diff
37
vendor/github.com/NicoNex/echotron/v3/chatadminrights.go
generated
vendored
37
vendor/github.com/NicoNex/echotron/v3/chatadminrights.go
generated
vendored
@@ -20,21 +20,24 @@ package echotron
|
||||
|
||||
// ChatAdministratorRights represents the rights of an administrator in a chat.
|
||||
type ChatAdministratorRights struct {
|
||||
IsAnonymous bool `json:"is_anonymous"`
|
||||
CanManageChat bool `json:"can_manage_chat"`
|
||||
CanDeleteMessages bool `json:"can_delete_messages"`
|
||||
CanManageVideoChats bool `json:"can_manage_video_chats"`
|
||||
CanRestrictMembers bool `json:"can_restrict_members"`
|
||||
CanPromoteMembers bool `json:"can_promote_members"`
|
||||
CanChangeInfo bool `json:"can_change_info"`
|
||||
CanInviteUsers bool `json:"can_invite_users"`
|
||||
CanPostStories bool `json:"can_post_stories"`
|
||||
CanEditStories bool `json:"can_edit_stories"`
|
||||
CanDeleteStories bool `json:"can_delete_stories"`
|
||||
CanPostMessages bool `json:"can_post_messages,omitempty"`
|
||||
CanEditMessages bool `json:"can_edit_messages,omitempty"`
|
||||
CanPinMessages bool `json:"can_pin_messages,omitempty"`
|
||||
CanManageTopics bool `json:"can_manage_topics,omitempty"`
|
||||
IsAnonymous bool `json:"is_anonymous"`
|
||||
CanManageChat bool `json:"can_manage_chat"`
|
||||
CanDeleteMessages bool `json:"can_delete_messages"`
|
||||
CanManageVideoChats bool `json:"can_manage_video_chats"`
|
||||
CanRestrictMembers bool `json:"can_restrict_members"`
|
||||
CanPromoteMembers bool `json:"can_promote_members"`
|
||||
CanChangeInfo bool `json:"can_change_info"`
|
||||
CanInviteUsers bool `json:"can_invite_users"`
|
||||
CanPostStories bool `json:"can_post_stories"`
|
||||
CanEditStories bool `json:"can_edit_stories"`
|
||||
CanDeleteStories bool `json:"can_delete_stories"`
|
||||
CanPostMessages bool `json:"can_post_messages,omitempty"`
|
||||
CanEditMessages bool `json:"can_edit_messages,omitempty"`
|
||||
CanPinMessages bool `json:"can_pin_messages,omitempty"`
|
||||
CanManageTopics bool `json:"can_manage_topics,omitempty"`
|
||||
CanManageDirectMessages bool `json:"can_manage_direct_messages,omitempty"`
|
||||
CanManageTags bool `json:"can_manage_tags,omitempty"`
|
||||
CanSendWelcomeMessages bool `json:"can_send_welcome_messages,omitempty"`
|
||||
}
|
||||
|
||||
// SetMyDefaultAdministratorRightsOptions contains the optional parameters used by
|
||||
@@ -55,10 +58,10 @@ type GetMyDefaultAdministratorRightsOptions struct {
|
||||
// These rights will be suggested to users, but they are are free to modify the list
|
||||
// before adding the bot.
|
||||
func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
|
||||
return res, client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
return res, a.lclient.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
|
||||
func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
|
||||
return res, client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
return res, a.lclient.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
26
vendor/github.com/NicoNex/echotron/v3/dispatcher.go
generated
vendored
26
vendor/github.com/NicoNex/echotron/v3/dispatcher.go
generated
vendored
@@ -26,29 +26,8 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type smap struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
func (s *smap) load(id int64) (Bot, bool) {
|
||||
bot, ok := s.m.Load(id)
|
||||
if ok {
|
||||
return bot.(Bot), ok
|
||||
}
|
||||
return nil, ok
|
||||
}
|
||||
|
||||
func (s *smap) store(id int64, bot Bot) {
|
||||
s.m.Store(id, bot)
|
||||
}
|
||||
|
||||
func (s *smap) delete(id int64) {
|
||||
s.m.Delete(id)
|
||||
}
|
||||
|
||||
// Bot is the interface that must be implemented by your definition of
|
||||
// the struct thus it represent each open session with a user on Telegram.
|
||||
type Bot interface {
|
||||
@@ -64,12 +43,11 @@ type NewBotFn func(chatId int64) Bot
|
||||
// associated with each chatID. When a new chat ID is found, the provided function
|
||||
// of type NewBotFn will be called.
|
||||
type Dispatcher struct {
|
||||
// sessions map[int64]Bot
|
||||
sessions smap
|
||||
api API
|
||||
newBot NewBotFn
|
||||
updates chan *Update
|
||||
httpServer *http.Server
|
||||
api API
|
||||
sessions smap[int64, Bot]
|
||||
}
|
||||
|
||||
// NewDispatcher returns a new instance of the Dispatcher object.
|
||||
|
||||
6
vendor/github.com/NicoNex/echotron/v3/games.go
generated
vendored
6
vendor/github.com/NicoNex/echotron/v3/games.go
generated
vendored
@@ -52,7 +52,7 @@ func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (re
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("game_short_name", gameShortName)
|
||||
return res, client.get(a.base, "sendGame", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "sendGame", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SetGameScore is used to set the score of the specified user in a game.
|
||||
@@ -61,7 +61,7 @@ func (a API) SetGameScore(userID int64, score int, msgID MessageIDOptions, opts
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("score", itoa(int64(score)))
|
||||
return res, client.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
|
||||
return res, a.lclient.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
|
||||
}
|
||||
|
||||
// GetGameHighScores is used to get data for high score tables.
|
||||
@@ -69,5 +69,5 @@ func (a API) GetGameHighScores(userID int64, opts MessageIDOptions) (res APIResp
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return res, client.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
15
vendor/github.com/NicoNex/echotron/v3/go.work
generated
vendored
Normal file
15
vendor/github.com/NicoNex/echotron/v3/go.work
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
go 1.23
|
||||
|
||||
use (
|
||||
.
|
||||
./example/polling
|
||||
./example/polling-fsm
|
||||
./example/polling-fsm-lifecycle
|
||||
./example/polling-fsm-persistence
|
||||
./example/polling-inline
|
||||
./example/polling-keyboard
|
||||
./example/polling-ratelimit
|
||||
./example/polling-simple
|
||||
./example/webhook
|
||||
./example/webhook-simple
|
||||
)
|
||||
102
vendor/github.com/NicoNex/echotron/v3/helpers.go
generated
vendored
102
vendor/github.com/NicoNex/echotron/v3/helpers.go
generated
vendored
@@ -19,6 +19,7 @@
|
||||
package echotron
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -42,17 +43,8 @@ func check(r APIResponse) error {
|
||||
|
||||
func processMedia(media, thumbnail InputFile) (im mediaEnvelope, cnt []content, err error) {
|
||||
switch {
|
||||
case media.id != "":
|
||||
im = mediaEnvelope{
|
||||
media: media.id,
|
||||
thumbnail: "",
|
||||
}
|
||||
|
||||
case media.url != "":
|
||||
im = mediaEnvelope{
|
||||
media: media.url,
|
||||
thumbnail: "",
|
||||
}
|
||||
case media.ref() != "":
|
||||
im.media = media.ref()
|
||||
|
||||
case media.path != "" && len(media.content) == 0:
|
||||
if media.content, media.path, err = readFile(media); err != nil {
|
||||
@@ -62,13 +54,14 @@ func processMedia(media, thumbnail InputFile) (im mediaEnvelope, cnt []content,
|
||||
|
||||
case media.path != "" && len(media.content) > 0:
|
||||
cnt = append(cnt, content{media.path, media.path, media.content})
|
||||
im = mediaEnvelope{
|
||||
media: fmt.Sprintf("attach://%s", media.path),
|
||||
thumbnail: "",
|
||||
}
|
||||
im.media = fmt.Sprintf("attach://%s", media.path)
|
||||
}
|
||||
|
||||
switch {
|
||||
// Needed by live photos, whose static photo is passed as the thumbnail.
|
||||
case thumbnail.ref() != "":
|
||||
im.thumbnail = thumbnail.ref()
|
||||
|
||||
case thumbnail.path != "" && len(thumbnail.content) == 0:
|
||||
if thumbnail.content, thumbnail.path, err = readFile(thumbnail); err != nil {
|
||||
return
|
||||
@@ -126,6 +119,30 @@ func toContent(ftype string, f InputFile) (content, error) {
|
||||
return content{f.path, ftype, f.content}, nil
|
||||
}
|
||||
|
||||
// errNestedUpload is returned when a local file is used as media nested in another parameter.
|
||||
var errNestedUpload = errors.New("echotron: media nested in polls and rich messages must be a file ID or a URL")
|
||||
|
||||
// inlineMedia prepares a media to be serialised inside another JSON parameter.
|
||||
// It returns nil if m is nil, so that the media can be omitted.
|
||||
// Nested media only accept file IDs and URLs, uploading local files
|
||||
// would need multipart plumbing through every parameter that embeds media.
|
||||
func inlineMedia(m InputMedia) (*mediaEnvelope, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
media, thumbnail := m.media(), m.thumbnail()
|
||||
if (media.ref() == "" && media.path != "") || (thumbnail.ref() == "" && thumbnail.path != "") {
|
||||
return nil, errNestedUpload
|
||||
}
|
||||
|
||||
return &mediaEnvelope{
|
||||
InputMedia: m,
|
||||
media: media.ref(),
|
||||
thumbnail: thumbnail.ref(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
|
||||
ret = make([]InputMedia, len(media))
|
||||
|
||||
@@ -151,6 +168,61 @@ func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func processProfilePhoto(photo InputProfilePhoto) (env profilePhotoEnvelope, cnt []content, err error) {
|
||||
env.content = photo
|
||||
file := photo.profilePhotoFile()
|
||||
|
||||
switch {
|
||||
case file.id != "":
|
||||
env.ref = file.id
|
||||
|
||||
case file.url != "":
|
||||
env.ref = file.url
|
||||
|
||||
case file.path != "" && len(file.content) == 0:
|
||||
if file.content, file.path, err = readFile(file); err != nil {
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case file.path != "" && len(file.content) > 0:
|
||||
cnt = append(cnt, content{file.path, file.path, file.content})
|
||||
env.ref = fmt.Sprintf("attach://%s", file.path)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func processStoryContent(sc InputStoryContent) (env storyContentEnvelope, cnt []content, err error) {
|
||||
env.content = sc
|
||||
file := sc.storyContentFile()
|
||||
|
||||
switch {
|
||||
case file.id != "":
|
||||
env.ref = file.id
|
||||
|
||||
case file.url != "":
|
||||
env.ref = file.url
|
||||
|
||||
case file.path != "" && len(file.content) == 0:
|
||||
if file.content, file.path, err = readFile(file); err != nil {
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case file.path != "" && len(file.content) > 0:
|
||||
cnt = append(cnt, content{file.path, file.path, file.content})
|
||||
env.ref = fmt.Sprintf("attach://%s", file.path)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// withParam appends an escaped query parameter to a URL that already has a query string.
|
||||
func withParam(u, key, val string) string {
|
||||
return u + "&" + key + "=" + url.QueryEscape(val)
|
||||
}
|
||||
|
||||
func itoa(i int64) string {
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
|
||||
14
vendor/github.com/NicoNex/echotron/v3/inline.go
generated
vendored
14
vendor/github.com/NicoNex/echotron/v3/inline.go
generated
vendored
@@ -528,7 +528,7 @@ type InputInvoiceMessageContent struct {
|
||||
SuggestedTipAmounts *[]int `json:"suggested_tip_amounts,omitempty"`
|
||||
PhotoURL string `json:"photo_url,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Payload string `json:"string"`
|
||||
Payload string `json:"payload"`
|
||||
ProviderToken string `json:"provider_token,omitempty"`
|
||||
Currency string `json:"currency"`
|
||||
Title string `json:"title"`
|
||||
@@ -550,6 +550,14 @@ type InputInvoiceMessageContent struct {
|
||||
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||
func (i InputInvoiceMessageContent) ImplementsInputMessageContent() {}
|
||||
|
||||
// InputRichMessageContent represents the content of a rich message to be sent as the result of an inline query.
|
||||
type InputRichMessageContent struct {
|
||||
RichMessage InputRichMessage `json:"rich_message"`
|
||||
}
|
||||
|
||||
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||
func (i InputRichMessageContent) ImplementsInputMessageContent() {}
|
||||
|
||||
// InlineQueryResultsButton represents a button to be shown above inline query results.
|
||||
// You MUST use exactly one of the fields.
|
||||
type InlineQueryResultsButton struct {
|
||||
@@ -587,7 +595,7 @@ func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult
|
||||
jsn, _ := json.Marshal(results)
|
||||
vals.Set("inline_query_id", inlineQueryID)
|
||||
vals.Set("results", string(jsn))
|
||||
return res, client.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SavePreparedInlineMessage stores a message that can be sent by a user of a Mini App.
|
||||
@@ -597,5 +605,5 @@ func (a API) SavePreparedInlineMessage(userID int64, result InlineQueryResult, o
|
||||
jsn, _ := json.Marshal(result)
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("result", string(jsn))
|
||||
return res, client.get(a.base, "savePreparedInlineMessage", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "savePreparedInlineMessage", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
4
vendor/github.com/NicoNex/echotron/v3/menubutton.go
generated
vendored
4
vendor/github.com/NicoNex/echotron/v3/menubutton.go
generated
vendored
@@ -48,10 +48,10 @@ type GetChatMenuButtonOptions struct {
|
||||
|
||||
// SetChatMenuButton is used to change the bot's menu button in a private chat, or the default menu button.
|
||||
func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
|
||||
return res, client.get(a.base, "setChatMenuButton", urlValues(opts), &res)
|
||||
return res, a.lclient.get(a.base, "setChatMenuButton", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// GetChatMenuButton is used to get the current value of the bot's menu button in a private chat, or the default menu button.
|
||||
func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
|
||||
return res, client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
|
||||
return res, a.lclient.get(a.base, "getChatMenuButton", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
346
vendor/github.com/NicoNex/echotron/v3/network.go
generated
vendored
346
vendor/github.com/NicoNex/echotron/v3/network.go
generated
vendored
@@ -35,60 +35,85 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// lclient is an HTTP client with built-in dual-level rate limiting.
|
||||
// One instance is shared across all API objects for the same bot token.
|
||||
type lclient struct {
|
||||
*http.Client
|
||||
*sync.RWMutex
|
||||
cl map[string]*rate.Limiter // chat based limiter
|
||||
gl *rate.Limiter // global limiter
|
||||
cl map[string]*rate.Limiter
|
||||
gl *rate.Limiter
|
||||
climiter func() *rate.Limiter
|
||||
http *http.Client
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var client = &lclient{
|
||||
Client: new(http.Client),
|
||||
RWMutex: new(sync.RWMutex),
|
||||
cl: make(map[string]*rate.Limiter),
|
||||
gl: rate.NewLimiter(rate.Every(time.Second/30), 30),
|
||||
climiter: func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(time.Minute/20), 20)
|
||||
},
|
||||
// clients caches one lclient per base URL (i.e. one per bot token).
|
||||
var clients smap[string, *lclient]
|
||||
|
||||
func loadClient(url string) *lclient {
|
||||
// Fast path: client already exists for this base URL.
|
||||
if lc, ok := clients.load(url); ok {
|
||||
return lc
|
||||
}
|
||||
|
||||
// Slow path: first access, create a new client and store it atomically.
|
||||
// If two goroutines race here, loadOrStore guarantees only one wins and
|
||||
// both get back the same *lclient.
|
||||
lc, _ := clients.loadOrStore(
|
||||
url,
|
||||
&lclient{
|
||||
http: new(http.Client),
|
||||
cl: make(map[string]*rate.Limiter),
|
||||
gl: rate.NewLimiter(rate.Every(time.Second/30), 30),
|
||||
climiter: func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(time.Minute/20), 20)
|
||||
},
|
||||
},
|
||||
)
|
||||
return lc
|
||||
}
|
||||
|
||||
// SetGlobalRequestLimit sets the global rate limit for requests to the Telegram API.
|
||||
// An interval of 0 disables the rate limiter, allowing unlimited requests.
|
||||
// By default the interval of this limiter is set to time.Second/30 and the
|
||||
// burstSize is set to 30.
|
||||
func SetGlobalRequestLimit(interval time.Duration, burstSize int) {
|
||||
client.Lock()
|
||||
client.gl = rate.NewLimiter(rate.Every(interval), burstSize)
|
||||
client.Unlock()
|
||||
func (c *lclient) SetGlobalRequestLimit(interval time.Duration, burstSize int) {
|
||||
c.mu.Lock()
|
||||
c.gl = rate.NewLimiter(rate.Every(interval), burstSize)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetChatRequestLimit sets the per-chat rate limit for requests to the Telegram API.
|
||||
// An interval of 0 disables the rate limiter, allowing unlimited requests.
|
||||
// By default the interval of this limiter is set to time.Minute/20 and the
|
||||
// burstSize is set to 20.
|
||||
func SetChatRequestLimit(interval time.Duration, burstSize int) {
|
||||
client.Lock()
|
||||
client.cl = make(map[string]*rate.Limiter)
|
||||
client.climiter = func() *rate.Limiter {
|
||||
func (c *lclient) SetChatRequestLimit(interval time.Duration, burstSize int) {
|
||||
c.mu.Lock()
|
||||
// Reset the existing limiters so the new factory applies to all chats.
|
||||
c.cl = make(map[string]*rate.Limiter)
|
||||
c.climiter = func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(interval), burstSize)
|
||||
}
|
||||
client.Unlock()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c lclient) wait(chatID string) error {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
// wait blocks until both the per-chat and global rate limiters allow the request.
|
||||
func (c *lclient) wait(chatID string) error {
|
||||
ctx := context.Background()
|
||||
// If the chatID is empty, it's a general API call like GetUpdates, GetMe
|
||||
// and similar, so skip the per-chat request limit wait.
|
||||
if chatID != "" {
|
||||
// If no limiter exists for a chat, create one.
|
||||
c.mu.RLock()
|
||||
l, ok := c.cl[chatID]
|
||||
c.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
l = c.climiter()
|
||||
c.cl[chatID] = l
|
||||
c.mu.Lock()
|
||||
// Re-check after acquiring the write lock to avoid overwriting
|
||||
// a limiter created by another goroutine in the meantime.
|
||||
if l, ok = c.cl[chatID]; !ok {
|
||||
l = c.climiter()
|
||||
c.cl[chatID] = l
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Make sure to respect the single chat limit of requests.
|
||||
@@ -101,8 +126,24 @@ func (c lclient) wait(chatID string) error {
|
||||
return c.gl.Wait(ctx)
|
||||
}
|
||||
|
||||
func (c lclient) doGet(reqURL string) ([]byte, error) {
|
||||
resp, err := c.Get(reqURL)
|
||||
// dispatch is the common path for all API calls: rate-limit, send, decode, check.
|
||||
func (c *lclient) dispatch(chatID string, send func() ([]byte, error), v APIResponse) error {
|
||||
if err := c.wait(chatID); err != nil {
|
||||
return err
|
||||
}
|
||||
cnt, err := send()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
}
|
||||
|
||||
// doGet performs a raw HTTP GET and returns the response body.
|
||||
func (c *lclient) doGet(reqURL string) ([]byte, error) {
|
||||
resp, err := c.http.Get(reqURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,7 +156,8 @@ func (c lclient) doGet(reqURL string) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c lclient) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||
// doPost sends a multipart/form-data POST with the given files and returns the response body.
|
||||
func (c *lclient) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
w = multipart.NewWriter(buf)
|
||||
@@ -136,7 +178,7 @@ func (c lclient) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||
}
|
||||
req.Header.Add("Content-Type", w.FormDataContentType())
|
||||
|
||||
res, err := c.Do(req)
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,7 +186,8 @@ func (c lclient) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
func (c lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
|
||||
// doPostForm sends an application/x-www-form-urlencoded POST and returns the response body.
|
||||
func (c *lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
|
||||
var form = make(url.Values)
|
||||
|
||||
for k, v := range keyVals {
|
||||
@@ -155,10 +198,9 @@ func (c lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.PostForm = form
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := c.Do(req)
|
||||
res, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -166,23 +208,25 @@ func (c lclient) doPostForm(reqURL string, keyVals map[string]string) ([]byte, e
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
func (c lclient) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
|
||||
// sendFile sends a single file to Telegram.
|
||||
// If the file is identified by an ID or URL it is passed as a query parameter;
|
||||
// otherwise it is uploaded via multipart. The thumbnail, if present, is always uploaded.
|
||||
func (c *lclient) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
|
||||
var cnt []content
|
||||
|
||||
if file.id != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
|
||||
url = withParam(url, fileType, file.id)
|
||||
} else if file.url != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
|
||||
} else if c, e := toContent(fileType, file); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
url = withParam(url, fileType, file.url)
|
||||
} else if f, e := toContent(fileType, file); e == nil {
|
||||
cnt = append(cnt, f)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
if c, e := toContent("thumbnail", thumbnail); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
} else {
|
||||
err = e
|
||||
// Thumbnail is optional; conversion errors are silently ignored.
|
||||
if t, e := toContent("thumbnail", thumbnail); e == nil {
|
||||
cnt = append(cnt, t)
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
@@ -193,96 +237,63 @@ func (c lclient) sendFile(file, thumbnail InputFile, url, fileType string) (res
|
||||
return
|
||||
}
|
||||
|
||||
func (c lclient) get(base, endpoint string, vals url.Values, v APIResponse) error {
|
||||
url, err := url.JoinPath(base, endpoint)
|
||||
// get calls a Telegram API endpoint that requires no file upload.
|
||||
func (c *lclient) get(base, endpoint string, vals url.Values, v APIResponse) error {
|
||||
u, err := url.JoinPath(base, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vals != nil {
|
||||
if queries := vals.Encode(); queries != "" {
|
||||
url = fmt.Sprintf("%s?%s", url, queries)
|
||||
if q := vals.Encode(); q != "" {
|
||||
u = fmt.Sprintf("%s?%s", u, q)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.doGet(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.doGet(u)
|
||||
}, v)
|
||||
}
|
||||
|
||||
func (c lclient) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
// postFile calls a Telegram API endpoint that requires uploading a single file.
|
||||
func (c *lclient) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendFile(file, thumbnail, url, fileType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendFile(file, thumbnail, u, fileType)
|
||||
}, v)
|
||||
}
|
||||
|
||||
func (c lclient) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
// postMedia calls a Telegram API endpoint that sends a media group or edits a single media item.
|
||||
// editSingle serialises only the first element instead of the full array.
|
||||
func (c *lclient) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendMediaFiles(url, editSingle, files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendMediaFiles(u, editSingle, files...)
|
||||
}, v)
|
||||
}
|
||||
|
||||
func (c lclient) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
// postStickers calls a Telegram API endpoint that sends one or more stickers.
|
||||
func (c *lclient) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendStickers(url, stickers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendStickers(u, stickers...)
|
||||
}, v)
|
||||
}
|
||||
|
||||
func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
|
||||
// sendMediaFiles serialises the media group into JSON and uploads any local files via multipart.
|
||||
func (c *lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
|
||||
var (
|
||||
med []mediaEnvelope
|
||||
cnt []content
|
||||
@@ -307,6 +318,8 @@ func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
// editSingle is set when editing a single media message; Telegram expects
|
||||
// the object directly rather than wrapped in an array.
|
||||
if editSingle {
|
||||
jsn, err = json.Marshal(med[0])
|
||||
} else {
|
||||
@@ -317,7 +330,7 @@ func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia
|
||||
return
|
||||
}
|
||||
|
||||
url = fmt.Sprintf("%s&media=%s", url, jsn)
|
||||
url = withParam(url, "media", string(jsn))
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return c.doPost(url, cnt...)
|
||||
@@ -325,7 +338,119 @@ func (c lclient) sendMediaFiles(url string, editSingle bool, files ...InputMedia
|
||||
return c.doGet(url)
|
||||
}
|
||||
|
||||
func (c lclient) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
|
||||
// postProfilePhoto calls a Telegram API endpoint that sets a profile photo.
|
||||
func (c *lclient) postProfilePhoto(base, endpoint, param string, photo InputProfilePhoto, vals url.Values, v APIResponse) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendProfilePhotoFile(u, param, photo)
|
||||
}, v)
|
||||
}
|
||||
|
||||
// sendProfilePhotoFile serialises the profile photo and uploads it if it's a local file.
|
||||
func (c *lclient) sendProfilePhotoFile(u, param string, photo InputProfilePhoto) ([]byte, error) {
|
||||
env, cnt, err := processProfilePhoto(photo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsn, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u = withParam(u, param, string(jsn))
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return c.doPost(u, cnt...)
|
||||
}
|
||||
return c.doGet(u)
|
||||
}
|
||||
|
||||
// postStoryContent calls a Telegram API endpoint that posts or edits a story.
|
||||
func (c *lclient) postStoryContent(base, endpoint string, sc InputStoryContent, vals url.Values, v APIResponse) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendStoryContentFile(u, sc)
|
||||
}, v)
|
||||
}
|
||||
|
||||
// sendStoryContentFile serialises the story content and uploads it if it's a local file.
|
||||
func (c *lclient) sendStoryContentFile(u string, sc InputStoryContent) ([]byte, error) {
|
||||
env, cnt, err := processStoryContent(sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsn, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u = withParam(u, "content", string(jsn))
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return c.doPost(u, cnt...)
|
||||
}
|
||||
return c.doGet(u)
|
||||
}
|
||||
|
||||
// namedFile pairs an InputFile with its multipart field name.
|
||||
type namedFile struct {
|
||||
file InputFile
|
||||
typ string
|
||||
}
|
||||
|
||||
// sendNamedFiles sends one or more named files to Telegram. Each file is appended
|
||||
// as a query parameter if identified by ID or URL, or uploaded via multipart if local.
|
||||
func (c *lclient) sendNamedFiles(url string, files ...namedFile) (res []byte, err error) {
|
||||
var cnt []content
|
||||
|
||||
for _, nf := range files {
|
||||
switch {
|
||||
case nf.file.id != "":
|
||||
url = withParam(url, nf.typ, nf.file.id)
|
||||
case nf.file.url != "":
|
||||
url = withParam(url, nf.typ, nf.file.url)
|
||||
default:
|
||||
if f, e := toContent(nf.typ, nf.file); e == nil {
|
||||
cnt = append(cnt, f)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
res, err = c.doPost(url, cnt...)
|
||||
} else {
|
||||
res, err = c.doGet(url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// postNamedFiles calls a Telegram API endpoint that requires uploading one or more named files.
|
||||
func (c *lclient) postNamedFiles(base, endpoint string, vals url.Values, v APIResponse, files ...namedFile) error {
|
||||
u, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.dispatch(vals.Get("chat_id"), func() ([]byte, error) {
|
||||
return c.sendNamedFiles(u, files...)
|
||||
}, v)
|
||||
}
|
||||
|
||||
// sendStickers serialises the stickers and uploads any local files via multipart.
|
||||
// A single sticker uses the "sticker" parameter; multiple use "stickers".
|
||||
func (c *lclient) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
|
||||
var (
|
||||
sti []stickerEnvelope
|
||||
cnt []content
|
||||
@@ -347,12 +472,13 @@ func (c lclient) sendStickers(url string, stickers ...InputSticker) (res []byte,
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
// Telegram uses different parameter names for one vs many stickers.
|
||||
if len(sti) == 1 {
|
||||
jsn, _ = json.Marshal(sti[0])
|
||||
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
|
||||
url = withParam(url, "sticker", string(jsn))
|
||||
} else {
|
||||
jsn, _ = json.Marshal(sti)
|
||||
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
|
||||
url = withParam(url, "stickers", string(jsn))
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
|
||||
845
vendor/github.com/NicoNex/echotron/v3/options.go
generated
vendored
845
vendor/github.com/NicoNex/echotron/v3/options.go
generated
vendored
File diff suppressed because it is too large
Load Diff
6
vendor/github.com/NicoNex/echotron/v3/passport.go
generated
vendored
6
vendor/github.com/NicoNex/echotron/v3/passport.go
generated
vendored
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
// PassportData contains information about Telegram Passport data shared with the bot by the user.
|
||||
type PassportData struct {
|
||||
Credentials EncryptedCredentials `json:"encrypted_credentials"`
|
||||
Data []EncryptedPassportElement `json:"encrypted_passport_element"`
|
||||
Credentials EncryptedCredentials `json:"credentials"`
|
||||
Data []EncryptedPassportElement `json:"data"`
|
||||
}
|
||||
|
||||
// PassportFile represents a file uploaded to Telegram Passport.
|
||||
@@ -224,5 +224,5 @@ func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("errors", string(errorsArr))
|
||||
return res, client.get(a.base, "setPassportDataErrors", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setPassportDataErrors", vals, &res)
|
||||
}
|
||||
|
||||
41
vendor/github.com/NicoNex/echotron/v3/payments.go
generated
vendored
41
vendor/github.com/NicoNex/echotron/v3/payments.go
generated
vendored
@@ -183,14 +183,23 @@ func (t TransactionPartnerFragment) ImplementsTransactionPartner() {}
|
||||
// TransactionPartnerUser describes a transaction with a user.
|
||||
// Type MUST be "user".
|
||||
type TransactionPartnerUser struct {
|
||||
PaidMedia *[]PaidMedia `json:"paid_media,omitempty"`
|
||||
Type string `json:"type"`
|
||||
InvoicePayload string `json:"invoice_payload,omitempty"`
|
||||
PaidMediaPayload string `json:"paid_media_payload,omitempty"`
|
||||
User User `json:"user"`
|
||||
Affiliate *AffiliateInfo `json:"affiliate,omitempty"`
|
||||
Gift Gift `json:"gift,omitempty"`
|
||||
SubscriptionPeriod int `json:"subscription_period,omitempty"`
|
||||
Gift Gift `json:"gift,omitempty"`
|
||||
PaidMedia *[]PaidMedia `json:"paid_media,omitempty"`
|
||||
Affiliate *AffiliateInfo `json:"affiliate,omitempty"`
|
||||
Type string `json:"type"`
|
||||
InvoicePayload string `json:"invoice_payload,omitempty"`
|
||||
PaidMediaPayload string `json:"paid_media_payload,omitempty"`
|
||||
TransactionType string `json:"transaction_type"`
|
||||
User User `json:"user"`
|
||||
SubscriptionPeriod int `json:"subscription_period,omitempty"`
|
||||
PremiumSubscriptionDuration int `json:"premium_subscription_duration,omitempty"`
|
||||
}
|
||||
|
||||
// TransactionPartnerChat describes a transaction with a chat.
|
||||
type TransactionPartnerChat struct {
|
||||
Gift Gift `json:"gift,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Chat Chat `json:"chat"`
|
||||
}
|
||||
|
||||
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||
@@ -236,7 +245,7 @@ type StarTransaction struct {
|
||||
|
||||
// StarTransactions contains a list of Telegram Star transactions.
|
||||
type StarTransactions struct {
|
||||
Transaction []StarTransaction `json:"transaction"`
|
||||
Transactions []StarTransaction `json:"transactions"`
|
||||
}
|
||||
|
||||
// StarTransactionsOptions contains the optional parameters used by the GetStarTransactions method.
|
||||
@@ -260,7 +269,7 @@ func (a API) SendInvoice(chatID int64, title, description, payload, currency str
|
||||
vals.Set("payload", payload)
|
||||
vals.Set("currency", currency)
|
||||
vals.Set("prices", string(p))
|
||||
return res, client.get(a.base, "sendInvoice", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "sendInvoice", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CreateInvoiceLink creates a link for an invoice.
|
||||
@@ -277,7 +286,7 @@ func (a API) CreateInvoiceLink(title, description, payload, currency string, pri
|
||||
vals.Set("payload", payload)
|
||||
vals.Set("currency", currency)
|
||||
vals.Set("prices", string(p))
|
||||
return res, client.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// AnswerShippingQuery is used to reply to shipping queries.
|
||||
@@ -288,7 +297,7 @@ func (a API) AnswerShippingQuery(shippingQueryID string, ok bool, opts *Shipping
|
||||
|
||||
vals.Set("shipping_query_id", shippingQueryID)
|
||||
vals.Set("ok", btoa(ok))
|
||||
return res, client.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// AnswerPreCheckoutQuery is used to respond to such pre-checkout queries.
|
||||
@@ -300,12 +309,12 @@ func (a API) AnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool, opts *Pr
|
||||
|
||||
vals.Set("pre_checkout_query_id", preCheckoutQueryID)
|
||||
vals.Set("ok", btoa(ok))
|
||||
return res, client.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
|
||||
func (a API) GetStarTransactions(opts *StarTransactionsOptions) (res APIResponseStarTransactions, err error) {
|
||||
return res, client.get(a.base, "getStarTransactions", urlValues(opts), &res)
|
||||
return res, a.lclient.get(a.base, "getStarTransactions", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// RefundStarPayment refunds a successful payment in Telegram Stars.
|
||||
@@ -314,7 +323,7 @@ func (a API) RefundStarPayment(userID int64, telegramPaymentChargeID string) (re
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
|
||||
return res, client.get(a.base, "refundStarPayment", vals, &res)
|
||||
return res, a.lclient.get(a.base, "refundStarPayment", vals, &res)
|
||||
}
|
||||
|
||||
// EditUserStarSubscription allows the bot to cancel or re-enable extension of a subscription paid in Telegram Stars.
|
||||
@@ -324,5 +333,5 @@ func (a API) EditUserStarSubscription(userID int64, telegramPaymentChargeID stri
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
|
||||
vals.Set("is_canceled", btoa(isCanceled))
|
||||
return res, client.get(a.base, "editUserStarSubscription", vals, &res)
|
||||
return res, a.lclient.get(a.base, "editUserStarSubscription", vals, &res)
|
||||
}
|
||||
|
||||
8
vendor/github.com/NicoNex/echotron/v3/querybuilder.go
generated
vendored
8
vendor/github.com/NicoNex/echotron/v3/querybuilder.go
generated
vendored
@@ -39,8 +39,12 @@ func toString(v reflect.Value) string {
|
||||
case reflect.Bool:
|
||||
return strconv.FormatBool(v.Bool())
|
||||
|
||||
case reflect.Struct, reflect.Interface, reflect.Slice, reflect.Array:
|
||||
b, _ := json.Marshal(v.Interface())
|
||||
case reflect.Struct, reflect.Interface, reflect.Slice, reflect.Array, reflect.Pointer:
|
||||
var i any = v.Interface()
|
||||
if m, ok := i.(InputMedia); ok {
|
||||
i, _ = inlineMedia(m) // SendPoll reports the error before the query is built.
|
||||
}
|
||||
b, _ := json.Marshal(i)
|
||||
return string(b)
|
||||
|
||||
default:
|
||||
|
||||
26
vendor/github.com/NicoNex/echotron/v3/smap.go
generated
vendored
Normal file
26
vendor/github.com/NicoNex/echotron/v3/smap.go
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
package echotron
|
||||
|
||||
import "sync"
|
||||
|
||||
type smap[K comparable, V any] sync.Map
|
||||
|
||||
func (s *smap[K, V]) load(key K) (val V, ok bool) {
|
||||
v, ok := (*sync.Map)(s).Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
return v.(V), ok
|
||||
}
|
||||
|
||||
func (s *smap[K, V]) store(key K, val V) {
|
||||
(*sync.Map)(s).Store(key, val)
|
||||
}
|
||||
|
||||
func (s *smap[K, V]) loadOrStore(key K, val V) (actual V, loaded bool) {
|
||||
a, loaded := (*sync.Map)(s).LoadOrStore(key, val)
|
||||
return a.(V), loaded
|
||||
}
|
||||
|
||||
func (s *smap[K, V]) delete(key K) {
|
||||
(*sync.Map)(s).Delete(key)
|
||||
}
|
||||
34
vendor/github.com/NicoNex/echotron/v3/stickers.go
generated
vendored
34
vendor/github.com/NicoNex/echotron/v3/stickers.go
generated
vendored
@@ -116,7 +116,7 @@ func (a API) SendSticker(stickerID string, chatID int64, opts *StickerOptions) (
|
||||
|
||||
vals.Set("sticker", stickerID)
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return res, client.get(a.base, "sendSticker", addValues(vals, opts), &res)
|
||||
return res, a.lclient.get(a.base, "sendSticker", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetStickerSet is used to get a sticker set.
|
||||
@@ -124,7 +124,7 @@ func (a API) GetStickerSet(name string) (res APIResponseStickerSet, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("name", name)
|
||||
return res, client.get(a.base, "getStickerSet", vals, &res)
|
||||
return res, a.lclient.get(a.base, "getStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
|
||||
@@ -133,7 +133,7 @@ func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseSt
|
||||
|
||||
jsn, _ := json.Marshal(customEmojiIDs)
|
||||
vals.Set("custom_emoji_ids", string(jsn))
|
||||
return res, client.get(a.base, "getCustomEmojiStickers", vals, &res)
|
||||
return res, a.lclient.get(a.base, "getCustomEmojiStickers", vals, &res)
|
||||
}
|
||||
|
||||
// UploadStickerFile is used to upload a .PNG file with a sticker for later use in
|
||||
@@ -143,7 +143,7 @@ func (a API) UploadStickerFile(userID int64, sticker InputFile, format StickerFo
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("sticker_format", string(format))
|
||||
return res, client.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
|
||||
return res, a.lclient.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
|
||||
}
|
||||
|
||||
// CreateNewStickerSet is used to create a new sticker set owned by a user.
|
||||
@@ -153,7 +153,7 @@ func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []In
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
vals.Set("title", title)
|
||||
return res, client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
|
||||
return res, a.lclient.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
|
||||
}
|
||||
|
||||
// AddStickerToSet is used to add a new sticker to a set created by the bot.
|
||||
@@ -162,7 +162,7 @@ func (a API) AddStickerToSet(userID int64, name string, sticker InputSticker) (r
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
return res, client.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
|
||||
return res, a.lclient.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
|
||||
}
|
||||
|
||||
// SetStickerPositionInSet is used to move a sticker in a set created by the bot to a specific position.
|
||||
@@ -171,7 +171,7 @@ func (a API) SetStickerPositionInSet(sticker string, position int) (res APIRespo
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("position", itoa(int64(position)))
|
||||
return res, client.get(a.base, "setStickerPositionInSet", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setStickerPositionInSet", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteStickerFromSet is used to delete a sticker from a set created by the bot.
|
||||
@@ -179,7 +179,7 @@ func (a API) DeleteStickerFromSet(sticker string) (res APIResponseBase, err erro
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
return res, client.get(a.base, "deleteStickerFromSet", vals, &res)
|
||||
return res, a.lclient.get(a.base, "deleteStickerFromSet", vals, &res)
|
||||
}
|
||||
|
||||
// ReplaceStickerInSet is used to replace an existing sticker in a sticker set with a new one.
|
||||
@@ -190,7 +190,7 @@ func (a API) ReplaceStickerInSet(userID int64, name string, old_sticker string,
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
vals.Set("old_sticker", old_sticker)
|
||||
return res, client.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
|
||||
return res, a.lclient.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
|
||||
}
|
||||
|
||||
// SetStickerEmojiList is used to change the list of emoji assigned to a regular or custom emoji sticker.
|
||||
@@ -202,7 +202,7 @@ func (a API) SetStickerEmojiList(sticker string, emojis []string) (res APIRespon
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("emoji_list", string(jsn))
|
||||
return res, client.get(a.base, "setStickerEmojiList", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setStickerEmojiList", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerKeywords is used to change search keywords assigned to a regular or custom emoji sticker.
|
||||
@@ -214,7 +214,7 @@ func (a API) SetStickerKeywords(sticker string, keywords []string) (res APIRespo
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("keywords", string(jsn))
|
||||
return res, client.get(a.base, "setStickerKeywords", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setStickerKeywords", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerMaskPosition is used to change the mask position of a mask sticker.
|
||||
@@ -226,7 +226,7 @@ func (a API) SetStickerMaskPosition(sticker string, mask MaskPosition) (res APIR
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("mask_position", string(jsn))
|
||||
return res, client.get(a.base, "setStickerMaskPosition", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setStickerMaskPosition", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerSetTitle is used to set the title of a created sticker set.
|
||||
@@ -235,7 +235,7 @@ func (a API) SetStickerSetTitle(name, title string) (res APIResponseBool, err er
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("title", title)
|
||||
return res, client.get(a.base, "setStickerSetTitle", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setStickerSetTitle", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerSetThumbnail is used to set the thumbnail of a sticker set.
|
||||
@@ -245,7 +245,7 @@ func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFi
|
||||
vals.Set("name", name)
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("format", string(format))
|
||||
return res, client.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
|
||||
return res, a.lclient.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
|
||||
}
|
||||
|
||||
// SetCustomEmojiStickerSetThumbnail is used to set the thumbnail of a custom emoji sticker set.
|
||||
@@ -254,7 +254,7 @@ func (a API) SetCustomEmojiStickerSetThumbnail(name, emojiID string) (res APIRes
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("custom_emoji_id", emojiID)
|
||||
return res, client.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
|
||||
return res, a.lclient.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteStickerSet is used to delete a sticker set that was created by the bot.
|
||||
@@ -262,10 +262,10 @@ func (a API) DeleteStickerSet(name string) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("name", name)
|
||||
return res, client.get(a.base, "DeleteStickerSet", vals, &res)
|
||||
return res, a.lclient.get(a.base, "DeleteStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// GetForumTopicIconStickers is used to get custom emoji stickers, which can be used as a forum topic icon by any user.
|
||||
func (a API) GetForumTopicIconStickers() (res APIResponseStickers, err error) {
|
||||
return res, client.get(a.base, "getForumTopicIconStickers", nil, &res)
|
||||
return res, a.lclient.get(a.base, "getForumTopicIconStickers", nil, &res)
|
||||
}
|
||||
|
||||
1632
vendor/github.com/NicoNex/echotron/v3/types.go
generated
vendored
1632
vendor/github.com/NicoNex/echotron/v3/types.go
generated
vendored
File diff suppressed because it is too large
Load Diff
2
vendor/github.com/NicoNex/echotron/v3/webapp.go
generated
vendored
2
vendor/github.com/NicoNex/echotron/v3/webapp.go
generated
vendored
@@ -53,5 +53,5 @@ func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (
|
||||
|
||||
vals.Set("web_app_query_id", webAppQueryID)
|
||||
vals.Set("result", string(resultJson))
|
||||
return res, client.get(a.base, "answerWebAppQuery", vals, &res)
|
||||
return res, a.lclient.get(a.base, "answerWebAppQuery", vals, &res)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user