mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 20:43:27 -05:00
Two enabled connections can hold different token pairs of the same upstream account, such as one Slack app registered twice. Both then receive every message and both answer it. The op "me" may now return accountId, a stable name of the upstream account. Slack returns the workspace and bot user, Telegram the bot ID and Discord the application ID. The host stores it on the binding when a token is saved and, for a binding that has none, when its add-on runs. An add-on that does not return it has no identity and is never compared. When two enabled bindings of one owner and kind share an identity: - the binding carries a duplicate marker with the code duplicate_connection, with a sentence in all eight languages of the connection texts; - saving one of them is a 409 of that type until the request carries confirm_duplicate, and nothing is saved before; - only the oldest enabled binding answers a message that has a platform message ID. The other logs one line and stays silent. The choice reads the stored bindings only, so every frontend makes the same one. Assisted-by: Claude:claude-sonnet-5-5
115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
package studio
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// msgNotJSON is the answer to a body that does not parse.
|
|
const msgNotJSON = "the body is not valid JSON"
|
|
|
|
// Error types of the chats API. The UI and clients match on them.
|
|
const (
|
|
typeInvalid = "invalid_request_error"
|
|
typeNotFound = "not_found"
|
|
typeConflict = "conflict"
|
|
typeServerError = "server_error"
|
|
typeUpstream = "upstream_error"
|
|
typeAuthz = "authorization_error"
|
|
typeFilesOff = "files_off"
|
|
typeAgentsOff = "agents_off"
|
|
typeNotifyOff = "notifications_off"
|
|
typeTriggersOff = "triggers_off"
|
|
typeConnectorOff = "connectors_off"
|
|
// typeDuplicateConnection: the connection to save is the same bot as
|
|
// another enabled one; send confirm_duplicate to save it anyway.
|
|
typeDuplicateConnection = "duplicate_connection"
|
|
// typePageNotAllowed: a shared page called its bridge without your consent
|
|
// to the version it runs (or after you revoked it).
|
|
typePageNotAllowed = "page_not_allowed"
|
|
// typeSharedPagesOff: an administrator switched the shared pages off.
|
|
typeSharedPagesOff = "shared_pages_off"
|
|
)
|
|
|
|
// errRule is one row of an error table: the errors that match (errors.Is), the
|
|
// status and type of the answer, and its text. The text is msg when it is set;
|
|
// else the text of the error, less the prefix trim, or less the sentinel strip
|
|
// and a following ": " when strip is set.
|
|
type errRule struct {
|
|
is []error
|
|
status int
|
|
typ string
|
|
msg string
|
|
trim string
|
|
strip error
|
|
}
|
|
|
|
// fixed answers with msg whatever the text of the error is, so that the answer
|
|
// never holds a detail of the failure.
|
|
func fixed(status int, typ, msg string, is ...error) errRule {
|
|
return errRule{is: is, status: status, typ: typ, msg: msg}
|
|
}
|
|
|
|
// verbatim answers with the text of the error, less the prefix trim.
|
|
func verbatim(status int, typ, trim string, is ...error) errRule {
|
|
return errRule{is: is, status: status, typ: typ, trim: trim}
|
|
}
|
|
|
|
// sentinelText answers with the text of the error after its sentinel. The
|
|
// text is written for the person who reads it.
|
|
func sentinelText(status int, typ string, sentinel error) errRule {
|
|
return errRule{is: []error{sentinel}, status: status, typ: typ, strip: sentinel}
|
|
}
|
|
|
|
func (r errRule) text(err error) string {
|
|
switch {
|
|
case r.msg != "":
|
|
return r.msg
|
|
case r.strip != nil:
|
|
return strings.TrimPrefix(strings.TrimPrefix(err.Error(), r.strip.Error()), ": ")
|
|
}
|
|
return strings.TrimPrefix(err.Error(), r.trim)
|
|
}
|
|
|
|
func (r errRule) matches(err error) bool {
|
|
for _, target := range r.is {
|
|
if errors.Is(err, target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// mapErr answers with the first rule that matches err. When none does it calls
|
|
// next, the mapper of the layer below.
|
|
func mapErr(c echo.Context, err error, rules []errRule, next func(echo.Context, error) error) error {
|
|
for _, r := range rules {
|
|
if r.matches(err) {
|
|
return fail(c, r.status, r.typ, r.text(err))
|
|
}
|
|
}
|
|
return next(c, err)
|
|
}
|
|
|
|
// bind reads the JSON body of the request into a T. When it cannot, it writes
|
|
// the answer and returns false: 413 for a body over the limit that the handler
|
|
// set with http.MaxBytesReader, else 400 with msg.
|
|
func bind[T any](c echo.Context, msg string) (T, bool) {
|
|
var req T
|
|
err := c.Bind(&req)
|
|
if err == nil {
|
|
return req, true
|
|
}
|
|
var tooBig *http.MaxBytesError
|
|
var httpErr *echo.HTTPError
|
|
if errors.As(err, &tooBig) || (errors.As(err, &httpErr) && httpErr.Code == http.StatusRequestEntityTooLarge) {
|
|
_ = fail(c, http.StatusRequestEntityTooLarge, typeInvalid, "the body is too large")
|
|
} else {
|
|
_ = fail(c, http.StatusBadRequest, typeInvalid, msg)
|
|
}
|
|
return req, false
|
|
}
|