Files
LocalAI/addons/telegram/transport/runtime.go
T
Ettore Di Giacinto c5f636d1b9 refactor(addons): move connector file bytes by handle only
A connector add-on no longer carries file bytes in the connector
envelope. The ops media and send_file need a transfer handle, and the
gateway refuses a call to them without one, or for a kind that declares
no files.

- The manifest field files.transfer, the inline file of a request and
  of an answer (ConnectorFile) and the 1 MiB cap
  (MaxConnectorFileBytes) are removed. The 2 MiB limit of a connector
  call between hosts drops to 1 MiB, because it holds text and no file.
- A kind that declares no files moves no file: the host answers a sender
  who attaches one with a plain sentence and does not offer attach_file.
  A direction left out of a declared files block still gets the host
  ceiling.
- The inline layer is gone from the file limit, from the API
  (inline_bytes) and from the Files section of the connector page.
- The Telegram and Slack add-ons keep only the streaming paths. The
  tests moved to the handle path, and the studio test rig gives every
  connector add-on a table of handles.

Assisted-by: Claude:claude-sonnet-5-5 go test node
2026-11-03 16:57:04 +00:00

176 lines
5.6 KiB
Go

package transport
import (
"context"
"io"
"strconv"
"github.com/mudler/LocalAI/addons/internal/connector"
"github.com/mudler/LocalAI/addons/internal/idle"
"github.com/mudler/LocalAI/pkg/addons/contract"
)
// MaxMessageRunes is the longest text Telegram takes in one message.
const MaxMessageRunes = 4096
// Kind is the one kind of binding this add-on serves.
const Kind = "telegram"
// Runtime is the add-on runtime: the connector route over the Bot API. It
// declares no tool, so every other operation is refused as in any add-on that
// has only a UI.
type Runtime struct {
idle.Runtime
API *API
// Host is the host operation channel, with which a file moves by handle.
Host connector.Host
}
var _ contract.ConnectorRuntime = Runtime{}
// Connector serves one call of the host through the shared route of the
// connector add-ons.
func (r Runtime) Connector(ctx context.Context, req contract.ConnectorRequest) (contract.ConnectorResponse, error) {
api := r.API
if api == nil {
api = &API{}
}
return connector.ServeWith(ctx, platform{api}, r.Host, req)
}
// platform is the Bot API as a connector.Platform. Telegram names chats and
// messages with numbers; the contract carries them as text.
type platform struct{ api *API }
func (platform) Kind() string { return Kind }
func (platform) MaxRunes() int { return MaxMessageRunes }
func (p platform) Poll(ctx context.Context, token string, offset int64, wait int) ([]contract.ConnectorMessage, error) {
return p.api.Updates(ctx, token, offset, wait)
}
var (
_ connector.RichSender = platform{}
_ connector.Drafter = platform{}
_ connector.Splitter = platform{}
)
// Split cuts an answer, which is Markdown, without breaking a code block. A
// part is at most MaxMessageRunes long as Markdown, and Telegram counts the
// text after it parsed the markup, which is never longer.
func (platform) Split(text string) []string { return connector.SplitFenced(text, MaxMessageRunes) }
// where reads the chat and the topic of a request. Telegram names them with
// numbers; a request without a topic has thread 0.
func where(req contract.ConnectorRequest) (chat, thread int64, err error) {
if chat, err = strconv.ParseInt(req.Chat, 10, 64); err != nil {
return 0, 0, err
}
if req.Thread != "" {
if thread, err = strconv.ParseInt(req.Thread, 10, 64); err != nil {
return 0, 0, err
}
}
return chat, thread, nil
}
func (p platform) Send(ctx context.Context, req contract.ConnectorRequest) (string, error) {
chat, thread, err := where(req)
if err != nil {
return "", err
}
id, err := p.api.Send(ctx, req.Token, chat, thread, req.Text)
return messageRef(id), err
}
// messageRef is the ID of a message as the contract carries it: "" when
// Telegram named none, so the host never edits a message that is not there.
func messageRef(id int64) string {
if id == 0 {
return ""
}
return strconv.FormatInt(id, 10)
}
// MediaStream streams a file of a message to the host by handle.
func (p platform) MediaStream(ctx context.Context, token, ref string, max int64) (connector.MediaStream, error) {
return p.api.MediaStream(ctx, token, ref, max)
}
// SendStream sends a file that the host hands over by handle.
func (p platform) SendStream(ctx context.Context, req contract.ConnectorRequest, body io.Reader) (string, error) {
chat, thread, err := where(req)
if err != nil {
return "", err
}
id, err := p.api.SendFileStream(ctx, req.Token, chat, thread, req.Transfer.Name, req.Transfer.MIME, req.Transfer.Size, body)
return messageRef(id), err
}
// SendRich sends one part of an answer as MarkdownV2, or as plain text when
// Telegram refuses the markup.
func (p platform) SendRich(ctx context.Context, req contract.ConnectorRequest) (string, error) {
chat, thread, err := where(req)
if err != nil {
return "", err
}
id, err := p.api.SendMarkdown(ctx, req.Token, chat, thread, req.Text)
return messageRef(id), err
}
// edited reads the chat and the message of an edit.
func edited(req contract.ConnectorRequest) (chat, msg int64, err error) {
if chat, err = strconv.ParseInt(req.Chat, 10, 64); err != nil {
return 0, 0, err
}
if msg, err = strconv.ParseInt(req.MessageID, 10, 64); err != nil {
return 0, 0, err
}
return chat, msg, nil
}
// Edit shows a preview: plain text, since the answer is not whole yet and
// its markup may be open.
func (p platform) Edit(ctx context.Context, req contract.ConnectorRequest) error {
chat, msg, err := edited(req)
if err != nil {
return err
}
return p.api.Edit(ctx, req.Token, chat, msg, req.Text)
}
// EditRich replaces the working message with the first part of an answer.
func (p platform) EditRich(ctx context.Context, req contract.ConnectorRequest) error {
chat, msg, err := edited(req)
if err != nil {
return err
}
return p.api.EditMarkdown(ctx, req.Token, chat, msg, req.Text)
}
// Draft shows a preview as a draft, which only a private chat has; Telegram
// refuses it elsewhere, and the host then edits a working message instead.
func (p platform) Draft(ctx context.Context, req contract.ConnectorRequest) error {
chat, thread, err := where(req)
if err != nil {
return err
}
return p.api.Draft(ctx, req.Token, chat, thread, req.DraftID, req.Text)
}
func (p platform) Typing(ctx context.Context, req contract.ConnectorRequest) error {
chat, thread, err := where(req)
if err != nil {
return err
}
return p.api.Typing(ctx, req.Token, chat, thread)
}
func (p platform) Me(ctx context.Context, token string) (connector.Bot, error) {
bot, err := p.api.Me(ctx, token)
if err != nil {
return connector.Bot{}, err
}
return connector.Bot{ID: strconv.FormatInt(bot.ID, 10), Username: bot.Username}, nil
}