Skip to content
This repository was archived by the owner on Oct 9, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Notable changes to Compa, newest first. Versions follow

- `compa-kernel mcp add`, `mcp remove` and `mcp edit` save their change when
a server has its own `call_timeout_seconds`; they refused every change.
- On Windows computers whose processor has AMX, such as recent Intel Xeon,
`compa-kernel` could crash a while after Telegram connected, with
`fatal error: found pointer to free object`.

## 3.0.0 - 2026-10-06

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.71.0 // indirect
github.com/valyala/fasthttp v1.71.0
github.com/valyala/fastjson v1.6.10 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/arch v0.24.0 // indirect
Expand Down
27 changes: 22 additions & 5 deletions pkg/channels/telegram/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
ta "github.com/mymmrac/telego/telegoapi"
th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil"
"github.com/valyala/fasthttp"

"github.com/xibodev/compa/v3/pkg/bus"
"github.com/xibodev/compa/v3/pkg/channels"
Expand Down Expand Up @@ -85,32 +86,48 @@ type telegramMessageParts struct {
mediaPaths []string
}

// errorFillingCaller gives every Telegram API response an Error. telego
// formats each response's Error for a debug log whatever the log level, and
// formatting a nil *Error makes fmt recover a nil dereference. On Windows hosts
// whose CPUs have AMX, recovering such a fault can corrupt the heap
// (https://go.dev/issue/81238), and the kernel crashes in a later GC.
type errorFillingCaller struct{ ta.Caller }

func (c errorFillingCaller) Call(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
resp, err := c.Caller.Call(ctx, url, data)
if resp != nil && resp.Error == nil {
resp.Error = &ta.Error{}
}
return resp, err
}
func NewTelegramChannel(
bc *config.Channel,
telegramCfg *config.TelegramSettings,
bus *bus.MessageBus,
) (*TelegramChannel, error) {
channelName := bc.Name()
var opts []telego.BotOption

// telego's own client, unless a proxy is set.
var caller ta.Caller = ta.FastHTTPCaller{Client: &fasthttp.Client{}}
if telegramCfg.Proxy != "" {
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
if parseErr != nil {
return nil, fmt.Errorf("invalid proxy URL %q: %w", telegramCfg.Proxy, parseErr)
}
opts = append(opts, telego.WithHTTPClient(&http.Client{
caller = ta.HTTPCaller{Client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}))
}}
} else if os.Getenv("HTTP_PROXY") != "" || os.Getenv("HTTPS_PROXY") != "" {
// Use environment proxy if configured
opts = append(opts, telego.WithHTTPClient(&http.Client{
caller = ta.HTTPCaller{Client: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}))
}}
}
opts := []telego.BotOption{telego.WithAPICaller(errorFillingCaller{caller})}

if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" {
opts = append(opts, telego.WithAPIServer(baseURL))
Expand Down
2 changes: 1 addition & 1 deletion pkg/channels/telegram/telegram_access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func newAccessTestChannel(t *testing.T, allowFrom ...string) (*TelegramChannel,
t.Helper()
getFiles := 0
bot, err := telego.NewBot("123456:"+strings.Repeat("a", 35),
telego.WithAPICaller(fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}),
telego.WithAPICaller(errorFillingCaller{fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}}),
telego.WithDiscardLogger(),
)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/channels/telegram/telegram_group_command_filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func newTestTelegramBot(t *testing.T, username string) *telego.Bot {

token := "123456:" + strings.Repeat("a", 35)
bot, err := telego.NewBot(token,
telego.WithAPICaller(getMeCaller{username: username}),
telego.WithAPICaller(errorFillingCaller{getMeCaller{username: username}}),
telego.WithDiscardLogger(),
)
if err != nil {
Expand Down Expand Up @@ -165,7 +165,7 @@ func TestHandleMessage_GroupIgnoredBeforeDownloadingMedia(t *testing.T) {
ch, messageBus := newGroupMentionOnlyChannel(t, "testbot")
getFiles := 0
bot, err := telego.NewBot("123456:"+strings.Repeat("a", 35),
telego.WithAPICaller(fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}),
telego.WithAPICaller(errorFillingCaller{fileCountingCaller{getMeCaller: getMeCaller{username: "testbot"}, getFiles: &getFiles}}),
telego.WithDiscardLogger(),
)
if err != nil {
Expand Down
33 changes: 32 additions & 1 deletion pkg/channels/telegram/telegram_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"path"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -144,7 +145,7 @@ func newTestChannelWithConstructor(
t.Helper()

bot, err := telego.NewBot(testToken,
telego.WithAPICaller(caller),
telego.WithAPICaller(errorFillingCaller{caller}),
telego.WithRequestConstructor(constructor),
telego.WithDiscardLogger(),
)
Expand All @@ -165,6 +166,36 @@ func newTestChannelWithConstructor(
}
}

// Every response has an Error, so telego's debug formatting of it never
// dereferences a nil *Error; a failed response keeps its own.
func TestErrorFillingCaller(t *testing.T) {
failure := &ta.Error{ErrorCode: 400, Description: "Bad Request"}
responses := map[string]*ta.Response{
"getMe": {Ok: true},
"sendMessage": {Error: failure},
}
caller := errorFillingCaller{&stubCaller{
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
if resp, ok := responses[path.Base(url)]; ok {
return resp, nil
}
return nil, errors.New("connection refused")
},
}}

resp, err := caller.Call(context.Background(), "https://api.telegram.org/bot/getMe", nil)
require.NoError(t, err)
require.NotNil(t, resp.Error)
assert.True(t, resp.Ok)

resp, err = caller.Call(context.Background(), "https://api.telegram.org/bot/sendMessage", nil)
require.NoError(t, err)
assert.Same(t, failure, resp.Error)

resp, err = caller.Call(context.Background(), "https://api.telegram.org/bot/getUpdates", nil)
assert.Nil(t, resp)
assert.EqualError(t, err, "connection refused")
}
func TestSendMedia_ImageFallbacksToDocumentOnInvalidDimensions(t *testing.T) {
constructor := &multipartRecordingConstructor{}
caller := &stubCaller{
Expand Down
Loading