Documentation
¶
Overview ¶
Package bridge serves Muster's Bridge protocol, so that a bridge between Muster and a messenger that Muster does not support natively is only the calls to that messenger.
A bridge is a small HTTP service. Muster calls it to post, edit and delete messages, reply in their Threads, check channels, look up people, send direct messages and answer people; the bridge reports what people do in the messenger — button presses, messages in Threads, deleted messages, Account link tokens — as events that Muster polls or that the bridge pushes. The protocol is api/bridge.yaml of Muster's repository; this package embeds a byte-for-byte copy of it, generates its Go types from it (Info, Capabilities, Limits, MessageInput, MessageContent, ThreadReplyInput, Person, Problem, the events and the rest) and validates every request and answer against it.
The library does the protocol: the bots' tokens, request validation and the bridge's limits, Idempotency-Keys, problem answers, the event queue, long polling and signed pushes. The bridge author writes a Messenger and reports events:
b, err := bridge.New(bridge.Config{
Name: "my-bridge", Version: "1.0.0", MessengerName: "My messenger",
Bots: []bridge.Bot{{Name: "main", Token: token}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{
MaxPerMessage: 5, MaxPerRow: 5, DataMaxBytes: 64,
Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary, bridge.ButtonStyleDanger}}},
}, myMessenger)
if err != nil {
return err
}
defer b.Close(ctx)
http.Handle("/", b.Handler())
// When a person presses a button in the messenger:
err = b.Press(ctx, "main", bridge.Press{UpdateID: callbackID, MessageID: messageID, User: person, Data: data})
examples/minimal in the repository is a complete bridge whose messenger lives in memory.
Operations ¶
Bridge.Handler serves every operation under /v1/; mount it under a path prefix with net/http.StripPrefix. Serve it with an net/http.Server that has no WriteTimeout, or one above 75 seconds: pollEvents holds a request for up to 60 seconds (Muster asks for 25 and waits 15 more), and a shorter WriteTimeout cuts such a poll off without an answer, so that its events reach Muster only on a later poll. ReadHeaderTimeout and IdleTimeout are safe to set. Each request carries one bot's token, compared in constant time with every bot's, and is that bot's: the Messenger is called with the bot's name, and its Idempotency-Keys and events are its own. A missing or wrong token is 401.
- getInfo answers the info that New built from the Config.
- postMessage, editMessage, replyInThread, checkChannel and answerEvent call the Messenger.
- findUsers calls UserFinder, served when Config.UserLookup is not InfoUserLookupNone.
- sendDirectMessage calls DirectMessenger, served with Capabilities.DirectMessages.
- deleteMessage calls Deleter, served with Capabilities.Delete. A message that is gone is deleted: 204.
- pollEvents answers the queued events, served when Config.EventDelivery has poll (the default).
An operation the bridge does not offer answers 404 invalid. New refuses a configuration whose features need an interface the Messenger lacks.
Before it calls the Messenger, the library refuses as 400 invalid a request that does not match the specification, a request body over 1 MiB, a body longer than Limits.MaxBodyLength characters, buttons sent to a bridge without buttons, more buttons than MaxPerMessage and button data longer than DataMaxBytes. It checks every answer of the Messenger against the specification too; one that does not match is logged and answered as transient.
Idempotency ¶
postMessage, replyInThread, sendDirectMessage and answerEvent carry an Idempotency-Key. The first successful answer of a key is kept for 24 hours per bot and answered again, with replayed true, without calling the Messenger, whatever the repeated body. A request whose key is still running is transient; a failed request keeps nothing, so its retry runs again. A request holds its key for two minutes; a Messenger method should return when its ctx ends. If one runs longer, a retry may claim the key and run as well, but the late request neither frees nor overwrites the retry's claim. IdempotencyKeyFrom gives the key to a Messenger whose messenger has idempotency of its own.
Errors ¶
A Messenger method returns an *Error to answer a class of the protocol: TransientError, RateLimitedError (with the wait, sent as Retry-After, and the scope), GoneError (optionally with ReasonEditWindowClosed), ForbiddenError and InvalidError, each with a detail for the Admin. Classes are answered with 500, 429, 404, 403 and 400. A class the operation does not declare is mapped to one it does: gone from deleteMessage is a success; gone from postMessage, checkChannel and sendDirectMessage, and invalid from checkChannel, are forbidden; any other is transient, logged as a mistake of the bridge. Any other error, and a panic, is transient with the detail "The bridge could not reach its messenger.", and logged at level error. The library never logs a token, a push secret or an Account link token, and replaces the bots' tokens and push secrets in every detail and log line.
Events ¶
Bridge.Press, Bridge.ThreadMessage, Bridge.MessageDeleted and Bridge.DirectMessage queue an event for the bot and return at once, so that a bridge confirms an update to its messenger within its deadline independently of Muster. An event's id is derived from the bot, the event type and the messenger's own id of the update when the bridge gives one, so that an update the messenger delivers twice is queued once; it is random otherwise. A direct message becomes an account_link event only when its whole text, trimmed and after an optional "/start ", is 43 characters of base64url; any other is ignored. Events are kept at least 24 hours. pollEvents answers the events after the cursor at once, or waits up to wait seconds (25 by default, at most 60) for one; a cursor the bridge did not give is invalid. answerEvent is gone for an event the bridge does not hold, and otherwise gives the Messenger the ReportedEvent it answers, with the Chat the bridge reported.
Push ¶
With a PushTarget for a bot, in Config.Bots or later with Bridge.SetPush, its events are posted to the callback URL one at a time and in order, signed as Standard Webhooks specifies with the push secret: webhook-id (the event's id), webhook-timestamp and webhook-signature. A 2xx answer acknowledges an event and 400 drops it; any other answer, a timeout of 10 seconds or a network error is retried with exponential back-off from 1 second up to 5 minutes under the same webhook-id, for an hour, after which the event stays queued for polling only. Redirects are not followed. Bridge.Close stops the pushers.
Store ¶
A Store keeps the Idempotency-Keys and the queued events; NewMemoryStore, the default, keeps them in memory, so a restart forgets them and Muster polls again without a cursor. A bridge that must keep them across restarts implements Store over its own database. Long polls and pushers are woken by the process that queued the event: with several processes over one shared Store, run each bot in one process only.
Example ¶
A bridge is a Config, a Messenger and the handler that serves them to Muster.
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
bridge "github.com/muster-io/muster-bridge-go"
)
// notebook is a messenger that keeps the bot's messages in memory: a whole Messenger in a few lines.
type notebook struct {
mu sync.Mutex
messages []bridge.MessageInput
}
func (n *notebook) PostMessage(_ context.Context, _ string, in bridge.MessageInput) (bridge.MessagePosted, error) {
if in.ChannelId != "alerts" {
return bridge.MessagePosted{}, bridge.ForbiddenError("The bot has not joined this channel.")
}
n.mu.Lock()
defer n.mu.Unlock()
n.messages = append(n.messages, in)
return bridge.MessagePosted{MessageId: "n" + strconv.Itoa(len(n.messages))}, nil
}
func (n *notebook) EditMessage(_ context.Context, _, id string, in bridge.MessageContent) (bridge.MessageEdited,
error) {
n.mu.Lock()
defer n.mu.Unlock()
i, err := strconv.Atoi(strings.TrimPrefix(id, "n"))
if err != nil || i < 1 || i > len(n.messages) {
return bridge.MessageEdited{}, bridge.GoneError("That message is gone.")
}
n.messages[i-1].Title, n.messages[i-1].Body = in.Title, in.Body
return bridge.MessageEdited{}, nil
}
func (n *notebook) ReplyInThread(_ context.Context, _, id string, _ bridge.ThreadReplyInput) (bridge.ThreadReplyPosted,
error) {
return bridge.ThreadReplyPosted{MessageId: id + "-reply", ThreadRef: id}, nil
}
func (n *notebook) CheckChannel(_ context.Context, _, channelID string) (string, error) {
if channelID != "alerts" {
return "", bridge.ForbiddenError("The bot has not joined this channel.")
}
return "Alerts", nil
}
func (n *notebook) AnswerEvent(context.Context, string, bridge.ReportedEvent, string) (bridge.AnswerShownShownAs,
error) {
return bridge.AnswerShownShownAsThread, nil
}
// newNotebookBridge is a bridge with one bot, main, whose token is example-token.
func newNotebookBridge(m bridge.Messenger) *bridge.Bridge {
b, err := bridge.New(bridge.Config{
Name: "notebook-bridge",
Version: "1.0.0",
MessengerName: "Notebook",
Bots: []bridge.Bot{{Name: "main", Token: "example-token"}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{MaxPerMessage: 5, MaxPerRow: 5,
DataMaxBytes: 64, Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary}}},
Logger: slog.New(slog.DiscardHandler),
}, m)
if err != nil {
panic(err)
}
return b
}
// get is the answer of the handler to a GET of path with the token, as Muster sends it.
func get(h http.Handler, path, token string) *http.Response {
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, path, nil)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Result()
}
func main() {
b := newNotebookBridge(¬ebook{})
defer func() { _ = b.Close(context.Background()) }()
res := get(b.Handler(), "/v1/info", "example-token")
defer res.Body.Close()
var info bridge.Info
if err := json.NewDecoder(res.Body).Decode(&info); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, info.ProtocolVersion, info.UserLookup, info.EventDelivery)
}
Output: 200 1 none [poll push]
Index ¶
- Constants
- Variables
- func IdempotencyKeyFrom(ctx context.Context) string
- func ReplaceMentions(body string, mentions []Mention, render func(Mention) string) string
- type AccountLinkEvent
- type AnswerEventJSONRequestBody
- type AnswerEventParams
- type AnswerInput
- type AnswerShown
- type AnswerShownShownAs
- type Bot
- type Bridge
- func (b *Bridge) Close(ctx context.Context) error
- func (b *Bridge) DirectMessage(ctx context.Context, bot string, m DirectMessage) error
- func (b *Bridge) Handler() http.Handler
- func (b *Bridge) MessageDeleted(ctx context.Context, bot string, m MessageDeleted) error
- func (b *Bridge) Press(ctx context.Context, bot string, p Press) error
- func (b *Bridge) SetPush(bot string, target *PushTarget) error
- func (b *Bridge) ThreadMessage(ctx context.Context, bot string, m ThreadMessage) error
- type BridgeEventJSONRequestBody
- type BridgeEventParams
- type Button
- type ButtonStyle
- type Capabilities
- type CapabilitiesPrivateAnswers
- type CapabilitiesThreadMessages
- type Capabilities_Buttons
- type Capabilities_Delete
- type Capabilities_DirectMessages
- type ChannelCheck
- type ChannelId
- type Class
- type Config
- type Deleter
- type DirectMessage
- type DirectMessageInput
- type DirectMessageSent
- type DirectMessenger
- type EditMessageJSONRequestBody
- type Error
- type ErrorClass
- type Event
- func (t Event) AsAccountLinkEvent() (AccountLinkEvent, error)
- func (t Event) AsMessageDeletedEvent() (MessageDeletedEvent, error)
- func (t Event) AsPressEvent() (PressEvent, error)
- func (t Event) AsThreadMessageEvent() (ThreadMessageEvent, error)
- func (t Event) Discriminator() (string, error)
- func (t *Event) FromAccountLinkEvent(v AccountLinkEvent) error
- func (t *Event) FromMessageDeletedEvent(v MessageDeletedEvent) error
- func (t *Event) FromPressEvent(v PressEvent) error
- func (t *Event) FromThreadMessageEvent(v ThreadMessageEvent) error
- func (t Event) MarshalJSON() ([]byte, error)
- func (t *Event) MergeAccountLinkEvent(v AccountLinkEvent) error
- func (t *Event) MergeMessageDeletedEvent(v MessageDeletedEvent) error
- func (t *Event) MergePressEvent(v PressEvent) error
- func (t *Event) MergeThreadMessageEvent(v ThreadMessageEvent) error
- func (t *Event) UnmarshalJSON(b []byte) error
- func (t Event) ValueByDiscriminator() (interface{}, error)
- type EventBase
- type EventBatch
- type EventType
- type FindUsersParams
- type Forbidden
- type Gone
- type IdempotencyKey
- type Info
- type InfoEventDelivery
- type InfoProtocolVersion
- type InfoUserLookup
- type Info_Bridge
- type Info_Messenger
- type Invalid
- type KeyClaim
- type KeyState
- type Limits
- type Link
- type Mention
- type MessageContent
- type MessageDeleted
- type MessageDeletedEvent
- type MessageEdited
- type MessageId
- type MessageInput
- type MessageKind
- type MessagePosted
- type Messenger
- type Person
- type PersonList
- type PollEventsParams
- type PostMessageJSONRequestBody
- type PostMessageParams
- type Press
- type PressEvent
- type Problem
- type ProblemReason
- type ProblemScope
- type PushTarget
- type RateLimited
- type ReplyInThreadJSONRequestBody
- type ReplyInThreadParams
- type ReportedEvent
- type SendDirectMessageJSONRequestBody
- type SendDirectMessageParams
- type Status
- type Store
- type StoredAnswer
- type StoredEvent
- type ThreadMessage
- type ThreadMessageEvent
- type ThreadReplyInput
- type ThreadReplyPosted
- type Transient
- type Unauthorized
- type UserFinder
Examples ¶
Constants ¶
const ( // ClassTransient is a temporary failure, such as a messenger that is down or a timeout; Muster retries. ClassTransient = Class(ErrorClassTransient) // ClassRateLimited means the messenger or the bridge throttles; Muster waits RetryAfter. ClassRateLimited = Class(ErrorClassRateLimited) // ClassGone means the message, its Thread or the event no longer exists. ClassGone = Class(ErrorClassGone) // ClassForbidden means the channel does not exist, the bot lacks the rights there, or a person blocked the bot. ClassForbidden = Class(ErrorClassForbidden) // ClassInvalid means the request cannot succeed as sent. ClassInvalid = Class(ErrorClassInvalid) )
The classes of the protocol, the values of ErrorClass in api/bridge.yaml.
const ReasonEditWindowClosed = ProblemReasonEditWindowClosed
ReasonEditWindowClosed tells Muster that an edit failed as gone because the messenger allows no edits to a message that old, while the message itself still exists; Muster then posts a new Root message.
Variables ¶
var ( // ErrUnknownBot is the error of a report or of SetPush for a bot that is not in Config.Bots. ErrUnknownBot = errors.New("bridge: no bot of that name") // ErrClosed is the error of SetPush after Close. ErrClosed = errors.New("bridge: closed") )
var ( // ErrNotOffered is the error of a report whose event needs a feature the bridge does not offer. ErrNotOffered = errors.New("bridge: the bridge does not offer the feature this event needs") // ErrInvalidEvent is the error of a report whose event does not match api/bridge.yaml. ErrInvalidEvent = errors.New("bridge: the event does not match api/bridge.yaml") )
var ErrUnknownCursor = errors.New("bridge: unknown cursor")
ErrUnknownCursor is the error of Store.EventsAfter for a cursor the store did not give for the bot.
Functions ¶
func IdempotencyKeyFrom ¶
IdempotencyKeyFrom is the Idempotency-Key of the request that a Messenger call serves, "" for an operation without one, so that a messenger with an idempotency mechanism of its own can be given it. Keys are per bot.
func ReplaceMentions ¶
ReplaceMentions replaces each Mention marker [@name](muster-mention:ref) of body whose ref names an entry of mentions by what render makes of that entry: the messenger's own mention for a bridge with native_mentions, or the plain name. A marker whose ref names no entry, or every marker when render is nil, becomes its link text, the @name as the body escapes it. The rest of body is unchanged.
Example ¶
package main
import (
"fmt"
bridge "github.com/muster-io/muster-bridge-go"
)
func main() {
body := `[@Alice Smith](muster-mention:a1) acknowledged; [@Bob \[ops\]](muster-mention:b2) is next.`
mentions := []bridge.Mention{{Ref: "a1", ExternalId: "U1", DisplayName: "Alice Smith"}}
// A bridge with native_mentions writes the messenger's own mention.
fmt.Println(bridge.ReplaceMentions(body, mentions, func(m bridge.Mention) string { return "@" + m.ExternalId }))
}
Output: @U1 acknowledged; @Bob \[ops\] is next.
Types ¶
type AccountLinkEvent ¶
type AccountLinkEvent struct {
// Id Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the `webhook-id` header.
Id string `json:"id"`
// OccurredAt When it happened in the messenger, or when the bridge learned of it.
OccurredAt time.Time `json:"occurred_at"`
Token string `json:"token"`
Type interface{} `json:"type"`
User Person `json:"user"`
}
AccountLinkEvent defines model for AccountLinkEvent.
type AnswerEventJSONRequestBody ¶
type AnswerEventJSONRequestBody = AnswerInput
AnswerEventJSONRequestBody defines body for AnswerEvent for application/json ContentType.
type AnswerEventParams ¶
type AnswerEventParams struct {
// IdempotencyKey Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token.
IdempotencyKey IdempotencyKey `json:"Idempotency-Key"`
}
AnswerEventParams defines parameters for AnswerEvent.
type AnswerInput ¶
type AnswerInput struct {
EventId string `json:"event_id"`
// Text Plain text; a pop-up may shorten it.
Text string `json:"text"`
}
AnswerInput defines model for AnswerInput.
type AnswerShown ¶
type AnswerShown struct {
Replayed bool `json:"replayed"`
// ShownAs Where the person sees the answer.
ShownAs AnswerShownShownAs `json:"shown_as"`
}
AnswerShown defines model for AnswerShown.
type AnswerShownShownAs ¶
type AnswerShownShownAs string
AnswerShownShownAs Where the person sees the answer.
const ( AnswerShownShownAsDirect AnswerShownShownAs = "direct" AnswerShownShownAsEphemeral AnswerShownShownAs = "ephemeral" AnswerShownShownAsPopup AnswerShownShownAs = "popup" AnswerShownShownAsThread AnswerShownShownAs = "thread" )
Defines values for AnswerShownShownAs.
func (AnswerShownShownAs) Valid ¶
func (e AnswerShownShownAs) Valid() bool
Valid indicates whether the value is a known member of the AnswerShownShownAs enum.
type Bot ¶
type Bot struct {
// Name names the bot in the Messenger calls and the reports, such as "main".
Name string
// Token is the token Muster sends for this bot, as saved on its Bridge Connection.
Token string
// Push is where the bot's events are pushed; nil for none.
Push *PushTarget
}
Bot is one bot of the bridge.
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge serves the Bridge protocol for the bots of its Config and calls the Messenger.
func New ¶
New checks cfg and returns the bridge that serves it with m. It names every problem it finds: a bot without a name or a token of its own, a feature whose interface m lacks, a push target that is not valid, and the info that cfg makes when it does not match api/bridge.yaml. Bots with a push target start pushing at once.
func (*Bridge) Close ¶
Close stops the pushers and waits for them until ctx ends. The handler and the reports keep working; no event is pushed any more.
func (*Bridge) DirectMessage ¶
DirectMessage reports a direct message to the bot. A message whose whole text, trimmed and after an optional "/start ", is 43 characters of base64url is an Account link token and becomes an account_link event; any other is ignored, and DirectMessage returns nil. Every bridge takes Account link tokens.
Example ¶
A direct message is an Account link token when its whole text is one, after an optional /start; any other is ignored.
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
bridge "github.com/muster-io/muster-bridge-go"
)
// notebook is a messenger that keeps the bot's messages in memory: a whole Messenger in a few lines.
type notebook struct {
mu sync.Mutex
messages []bridge.MessageInput
}
func (n *notebook) PostMessage(_ context.Context, _ string, in bridge.MessageInput) (bridge.MessagePosted, error) {
if in.ChannelId != "alerts" {
return bridge.MessagePosted{}, bridge.ForbiddenError("The bot has not joined this channel.")
}
n.mu.Lock()
defer n.mu.Unlock()
n.messages = append(n.messages, in)
return bridge.MessagePosted{MessageId: "n" + strconv.Itoa(len(n.messages))}, nil
}
func (n *notebook) EditMessage(_ context.Context, _, id string, in bridge.MessageContent) (bridge.MessageEdited,
error) {
n.mu.Lock()
defer n.mu.Unlock()
i, err := strconv.Atoi(strings.TrimPrefix(id, "n"))
if err != nil || i < 1 || i > len(n.messages) {
return bridge.MessageEdited{}, bridge.GoneError("That message is gone.")
}
n.messages[i-1].Title, n.messages[i-1].Body = in.Title, in.Body
return bridge.MessageEdited{}, nil
}
func (n *notebook) ReplyInThread(_ context.Context, _, id string, _ bridge.ThreadReplyInput) (bridge.ThreadReplyPosted,
error) {
return bridge.ThreadReplyPosted{MessageId: id + "-reply", ThreadRef: id}, nil
}
func (n *notebook) CheckChannel(_ context.Context, _, channelID string) (string, error) {
if channelID != "alerts" {
return "", bridge.ForbiddenError("The bot has not joined this channel.")
}
return "Alerts", nil
}
func (n *notebook) AnswerEvent(context.Context, string, bridge.ReportedEvent, string) (bridge.AnswerShownShownAs,
error) {
return bridge.AnswerShownShownAsThread, nil
}
// newNotebookBridge is a bridge with one bot, main, whose token is example-token.
func newNotebookBridge(m bridge.Messenger) *bridge.Bridge {
b, err := bridge.New(bridge.Config{
Name: "notebook-bridge",
Version: "1.0.0",
MessengerName: "Notebook",
Bots: []bridge.Bot{{Name: "main", Token: "example-token"}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{MaxPerMessage: 5, MaxPerRow: 5,
DataMaxBytes: 64, Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary}}},
Logger: slog.New(slog.DiscardHandler),
}, m)
if err != nil {
panic(err)
}
return b
}
// get is the answer of the handler to a GET of path with the token, as Muster sends it.
func get(h http.Handler, path, token string) *http.Response {
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, path, nil)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Result()
}
// pollTypes polls the events of the bot main and returns their types.
func pollTypes(b *bridge.Bridge) []string {
res := get(b.Handler(), "/v1/events?wait=0", "example-token")
defer res.Body.Close()
var batch struct {
Events []struct {
Type string `json:"type"`
MessageID string `json:"message_id"`
} `json:"events"`
}
if err := json.NewDecoder(res.Body).Decode(&batch); err != nil {
panic(err)
}
types := []string{}
for _, ev := range batch.Events {
types = append(types, strings.TrimSuffix(ev.Type+" "+ev.MessageID, " "))
}
return types
}
func main() {
b := newNotebookBridge(¬ebook{})
defer func() { _ = b.Close(context.Background()) }()
alice := bridge.Person{ExternalId: "U1", DisplayName: "Alice Smith"}
for _, text := range []string{"/start " + strings.Repeat("A", 43), "hello"} {
if err := b.DirectMessage(context.Background(), "main", bridge.DirectMessage{User: alice, Text: text}); err != nil {
panic(err)
}
}
fmt.Println(pollTypes(b))
}
Output: [account_link]
func (*Bridge) Handler ¶
Handler serves the protocol: every operation under /v1/. Mount it at the bridge's base URL, under a path prefix with http.StripPrefix.
Example ¶
The handler mounted under a path prefix: Muster's Bridge Connection then has the base URL https://bridge.example.org/bridge.
package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
bridge "github.com/muster-io/muster-bridge-go"
)
// notebook is a messenger that keeps the bot's messages in memory: a whole Messenger in a few lines.
type notebook struct {
mu sync.Mutex
messages []bridge.MessageInput
}
func (n *notebook) PostMessage(_ context.Context, _ string, in bridge.MessageInput) (bridge.MessagePosted, error) {
if in.ChannelId != "alerts" {
return bridge.MessagePosted{}, bridge.ForbiddenError("The bot has not joined this channel.")
}
n.mu.Lock()
defer n.mu.Unlock()
n.messages = append(n.messages, in)
return bridge.MessagePosted{MessageId: "n" + strconv.Itoa(len(n.messages))}, nil
}
func (n *notebook) EditMessage(_ context.Context, _, id string, in bridge.MessageContent) (bridge.MessageEdited,
error) {
n.mu.Lock()
defer n.mu.Unlock()
i, err := strconv.Atoi(strings.TrimPrefix(id, "n"))
if err != nil || i < 1 || i > len(n.messages) {
return bridge.MessageEdited{}, bridge.GoneError("That message is gone.")
}
n.messages[i-1].Title, n.messages[i-1].Body = in.Title, in.Body
return bridge.MessageEdited{}, nil
}
func (n *notebook) ReplyInThread(_ context.Context, _, id string, _ bridge.ThreadReplyInput) (bridge.ThreadReplyPosted,
error) {
return bridge.ThreadReplyPosted{MessageId: id + "-reply", ThreadRef: id}, nil
}
func (n *notebook) CheckChannel(_ context.Context, _, channelID string) (string, error) {
if channelID != "alerts" {
return "", bridge.ForbiddenError("The bot has not joined this channel.")
}
return "Alerts", nil
}
func (n *notebook) AnswerEvent(context.Context, string, bridge.ReportedEvent, string) (bridge.AnswerShownShownAs,
error) {
return bridge.AnswerShownShownAsThread, nil
}
// newNotebookBridge is a bridge with one bot, main, whose token is example-token.
func newNotebookBridge(m bridge.Messenger) *bridge.Bridge {
b, err := bridge.New(bridge.Config{
Name: "notebook-bridge",
Version: "1.0.0",
MessengerName: "Notebook",
Bots: []bridge.Bot{{Name: "main", Token: "example-token"}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{MaxPerMessage: 5, MaxPerRow: 5,
DataMaxBytes: 64, Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary}}},
Logger: slog.New(slog.DiscardHandler),
}, m)
if err != nil {
panic(err)
}
return b
}
// get is the answer of the handler to a GET of path with the token, as Muster sends it.
func get(h http.Handler, path, token string) *http.Response {
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, path, nil)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Result()
}
func main() {
b := newNotebookBridge(¬ebook{})
defer func() { _ = b.Close(context.Background()) }()
mux := http.NewServeMux()
mux.Handle("/bridge/", http.StripPrefix("/bridge", b.Handler()))
for _, token := range []string{"example-token", "wrong-token"} {
res := get(mux, "/bridge/v1/info", token)
_ = res.Body.Close()
fmt.Println(token, res.StatusCode)
}
}
Output: example-token 200 wrong-token 401
func (*Bridge) MessageDeleted ¶
MessageDeleted reports the deletion of a message of the bot. The bridge must offer deletion_events.
func (*Bridge) Press ¶
Press reports a press. It returns once the event is queued; pushes happen in the background, so a bridge confirms the press to its messenger within its deadline independently of Muster. The bridge must offer buttons.
Example ¶
A press is reported once the messenger has told the bridge about it; Muster then polls it, or the bridge pushes it.
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
bridge "github.com/muster-io/muster-bridge-go"
)
// notebook is a messenger that keeps the bot's messages in memory: a whole Messenger in a few lines.
type notebook struct {
mu sync.Mutex
messages []bridge.MessageInput
}
func (n *notebook) PostMessage(_ context.Context, _ string, in bridge.MessageInput) (bridge.MessagePosted, error) {
if in.ChannelId != "alerts" {
return bridge.MessagePosted{}, bridge.ForbiddenError("The bot has not joined this channel.")
}
n.mu.Lock()
defer n.mu.Unlock()
n.messages = append(n.messages, in)
return bridge.MessagePosted{MessageId: "n" + strconv.Itoa(len(n.messages))}, nil
}
func (n *notebook) EditMessage(_ context.Context, _, id string, in bridge.MessageContent) (bridge.MessageEdited,
error) {
n.mu.Lock()
defer n.mu.Unlock()
i, err := strconv.Atoi(strings.TrimPrefix(id, "n"))
if err != nil || i < 1 || i > len(n.messages) {
return bridge.MessageEdited{}, bridge.GoneError("That message is gone.")
}
n.messages[i-1].Title, n.messages[i-1].Body = in.Title, in.Body
return bridge.MessageEdited{}, nil
}
func (n *notebook) ReplyInThread(_ context.Context, _, id string, _ bridge.ThreadReplyInput) (bridge.ThreadReplyPosted,
error) {
return bridge.ThreadReplyPosted{MessageId: id + "-reply", ThreadRef: id}, nil
}
func (n *notebook) CheckChannel(_ context.Context, _, channelID string) (string, error) {
if channelID != "alerts" {
return "", bridge.ForbiddenError("The bot has not joined this channel.")
}
return "Alerts", nil
}
func (n *notebook) AnswerEvent(context.Context, string, bridge.ReportedEvent, string) (bridge.AnswerShownShownAs,
error) {
return bridge.AnswerShownShownAsThread, nil
}
// newNotebookBridge is a bridge with one bot, main, whose token is example-token.
func newNotebookBridge(m bridge.Messenger) *bridge.Bridge {
b, err := bridge.New(bridge.Config{
Name: "notebook-bridge",
Version: "1.0.0",
MessengerName: "Notebook",
Bots: []bridge.Bot{{Name: "main", Token: "example-token"}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{MaxPerMessage: 5, MaxPerRow: 5,
DataMaxBytes: 64, Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary}}},
Logger: slog.New(slog.DiscardHandler),
}, m)
if err != nil {
panic(err)
}
return b
}
// get is the answer of the handler to a GET of path with the token, as Muster sends it.
func get(h http.Handler, path, token string) *http.Response {
r := httptest.NewRequestWithContext(context.Background(), http.MethodGet, path, nil)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
h.ServeHTTP(w, r)
return w.Result()
}
// pollTypes polls the events of the bot main and returns their types.
func pollTypes(b *bridge.Bridge) []string {
res := get(b.Handler(), "/v1/events?wait=0", "example-token")
defer res.Body.Close()
var batch struct {
Events []struct {
Type string `json:"type"`
MessageID string `json:"message_id"`
} `json:"events"`
}
if err := json.NewDecoder(res.Body).Decode(&batch); err != nil {
panic(err)
}
types := []string{}
for _, ev := range batch.Events {
types = append(types, strings.TrimSuffix(ev.Type+" "+ev.MessageID, " "))
}
return types
}
func main() {
b := newNotebookBridge(¬ebook{})
defer func() { _ = b.Close(context.Background()) }()
alice := bridge.Person{ExternalId: "U1", DisplayName: "Alice Smith"}
press := bridge.Press{UpdateID: "callback-17", MessageID: "n1", User: alice, Data: "ack"}
for range 2 {
// The messenger delivered the update twice; it is queued once.
if err := b.Press(context.Background(), "main", press); err != nil {
panic(err)
}
}
fmt.Println(pollTypes(b))
}
Output: [press n1]
func (*Bridge) SetPush ¶
func (b *Bridge) SetPush(bot string, target *PushTarget) error
SetPush sets where the events of the bot are pushed; nil stops pushing. A pusher that this call starts begins with the oldest event kept; a pusher that already runs continues where it was, now towards the new target.
func (*Bridge) ThreadMessage ¶
ThreadMessage reports a person's message in a Thread. The bridge must offer thread_messages.
type BridgeEventJSONRequestBody ¶
type BridgeEventJSONRequestBody = Event
BridgeEventJSONRequestBody defines body for BridgeEvent for application/json ContentType.
type BridgeEventParams ¶
type BridgeEventParams struct {
// WebhookId The event's `id`, kept across retries.
WebhookId string `json:"webhook-id"`
// WebhookTimestamp Unix time of this attempt, in seconds.
WebhookTimestamp string `json:"webhook-timestamp"`
// WebhookSignature `v1,<base64 HMAC-SHA256>`; several signatures separated by spaces are allowed while a secret is rotated.
WebhookSignature string `json:"webhook-signature"`
}
BridgeEventParams defines parameters for BridgeEvent.
type Button ¶
type Button struct {
// Data Opaque, signed by Muster; return it unchanged with the press.
Data string `json:"data"`
Label string `json:"label"`
// Style A hint; the bridge maps it to the closest style its messenger has, or shows a plain button.
Style ButtonStyle `json:"style"`
}
Button defines model for Button.
type ButtonStyle ¶
type ButtonStyle string
ButtonStyle A hint; the bridge maps it to the closest style its messenger has, or shows a plain button.
const ( ButtonStyleDanger ButtonStyle = "danger" ButtonStyleNeutral ButtonStyle = "neutral" ButtonStylePrimary ButtonStyle = "primary" ButtonStyleSuccess ButtonStyle = "success" )
Defines values for ButtonStyle.
func (ButtonStyle) Valid ¶
func (e ButtonStyle) Valid() bool
Valid indicates whether the value is a known member of the ButtonStyle enum.
type Capabilities ¶
type Capabilities struct {
// Buttons The bridge shows buttons and reports presses on them.
Buttons *Capabilities_Buttons `json:"buttons,omitempty"`
// Delete The bot can delete its own messages (unused by Muster L1).
Delete *Capabilities_Delete `json:"delete,omitempty"`
// DeletionEvents The bridge reports `message_deleted` when a message of the bot is deleted in the messenger.
DeletionEvents *bool `json:"deletion_events,omitempty"`
// DirectMessages The bot can send direct messages, which Account links by code need.
DirectMessages *Capabilities_DirectMessages `json:"direct_messages,omitempty"`
// NativeMentions Mention markers become the messenger's own mentions, which notify the person.
NativeMentions *bool `json:"native_mentions,omitempty"`
// PrivateAnswers How the bridge can show an answer to one person only: an ephemeral message, a pop-up (short plain text), or both.
PrivateAnswers *[]CapabilitiesPrivateAnswers `json:"private_answers,omitempty"`
// QuietMessages The messenger can post a new message without notifying anyone (`notify` false).
QuietMessages *bool `json:"quiet_messages,omitempty"`
// StatusColor The messenger can colour a message by `color`; otherwise the bridge shows `emoji`.
StatusColor *bool `json:"status_color,omitempty"`
// ThreadMessages The bridge reports people's messages in Threads of the bot's messages — every one, or only those that mention the bot. Text commands need it.
ThreadMessages *CapabilitiesThreadMessages `json:"thread_messages,omitempty"`
}
Capabilities The optional features of the bridge. A missing member means the feature is not offered.
type CapabilitiesPrivateAnswers ¶
type CapabilitiesPrivateAnswers string
CapabilitiesPrivateAnswers defines model for Capabilities.PrivateAnswers.
const ( CapabilitiesPrivateAnswersEphemeral CapabilitiesPrivateAnswers = "ephemeral" CapabilitiesPrivateAnswersPopup CapabilitiesPrivateAnswers = "popup" )
Defines values for CapabilitiesPrivateAnswers.
func (CapabilitiesPrivateAnswers) Valid ¶
func (e CapabilitiesPrivateAnswers) Valid() bool
Valid indicates whether the value is a known member of the CapabilitiesPrivateAnswers enum.
type CapabilitiesThreadMessages ¶
type CapabilitiesThreadMessages string
CapabilitiesThreadMessages The bridge reports people's messages in Threads of the bot's messages — every one, or only those that mention the bot. Text commands need it.
const ( CapabilitiesThreadMessagesAll CapabilitiesThreadMessages = "all" CapabilitiesThreadMessagesMentionsOnly CapabilitiesThreadMessages = "mentions_only" )
Defines values for CapabilitiesThreadMessages.
func (CapabilitiesThreadMessages) Valid ¶
func (e CapabilitiesThreadMessages) Valid() bool
Valid indicates whether the value is a known member of the CapabilitiesThreadMessages enum.
type Capabilities_Buttons ¶
type Capabilities_Buttons struct {
DataMaxBytes int `json:"data_max_bytes"`
// MaxPerMessage Muster sends at most this many, dropping Snooze buttons first: the Snooze presets, then the durations from the longest.
MaxPerMessage int `json:"max_per_message"`
MaxPerRow int `json:"max_per_row"`
// Styles The style hints the messenger can show as such; the bridge maps the others to the closest one, or shows a plain button.
Styles []ButtonStyle `json:"styles"`
}
Capabilities_Buttons The bridge shows buttons and reports presses on them.
type Capabilities_Delete ¶
type Capabilities_Delete struct {
MaxAgeSeconds *int `json:"max_age_seconds"`
}
Capabilities_Delete The bot can delete its own messages (unused by Muster L1).
type Capabilities_DirectMessages ¶
type Capabilities_DirectMessages struct {
// BotCanStart Whether the bot can write to a person who never wrote to it.
BotCanStart bool `json:"bot_can_start"`
}
Capabilities_DirectMessages The bot can send direct messages, which Account links by code need.
type ChannelCheck ¶
type ChannelCheck struct {
ChannelId string `json:"channel_id"`
// Name The channel's name as people see it; Muster shows it on the Destination.
Name string `json:"name"`
}
ChannelCheck defines model for ChannelCheck.
type Class ¶
type Class string
Class is the class of an error, which tells Muster what to do about it.
type Config ¶
type Config struct {
// Name and Version are the bridge's, as info shows them: 1 to 100 characters each.
Name string
Version string
// MessengerName is the messenger's name as people know it: 1 to 100 characters.
MessengerName string
// Bots are the bots the bridge serves, at least one; each is a Bridge Connection in Muster.
Bots []Bot
// Capabilities are the optional features; a nil member is a feature the bridge does not offer.
Capabilities Capabilities
// Limits are the bridge's limits; MaxBodyLength from 500 to 100000.
Limits Limits
// UserLookup is what findUsers searches by; "" is InfoUserLookupNone.
UserLookup InfoUserLookup
// EventDelivery are the event delivery modes the bridge offers; nil is both, poll and push.
EventDelivery []InfoEventDelivery
// LinkURLTemplate is an http or https link with {token}, which opens the bot with an Account link token; "" for none.
LinkURLTemplate string
// Store keeps the Idempotency-Keys and the events; nil is NewMemoryStore().
Store Store
// Logger receives the library's logs; nil is slog.Default().
Logger *slog.Logger
// contains filtered or unexported fields
}
Config is a bridge: what it tells Muster in info, its bots and where it keeps its state.
type Deleter ¶
type Deleter interface {
// DeleteMessage deletes a message of the bot. A message that no longer exists is a success, also when this
// returns a gone *Error.
DeleteMessage(ctx context.Context, bot, messageID string) error
}
Deleter is the Messenger's deletion of the bot's messages, which Capabilities.Delete needs.
type DirectMessage ¶
type DirectMessage struct {
// UpdateID is the messenger's own id of the update, "" when it has none.
UpdateID string
// User is the person who wrote.
User Person
// Text is the whole text the person sent.
Text string
// At is when it happened; zero is now.
At time.Time
// Chat is the direct chat, so that AnswerEvent answers there.
Chat string
}
DirectMessage is a private message from a person to the bot.
type DirectMessageInput ¶
type DirectMessageInput struct {
// Body CommonMark.
Body string `json:"body"`
UserExternalId string `json:"user_external_id"`
}
DirectMessageInput defines model for DirectMessageInput.
type DirectMessageSent ¶
type DirectMessageSent struct {
MessageId string `json:"message_id"`
Replayed bool `json:"replayed"`
}
DirectMessageSent defines model for DirectMessageSent.
type DirectMessenger ¶
type DirectMessenger interface {
// SendDirectMessage sends a direct message from the bot, opening the direct chat if needed.
SendDirectMessage(ctx context.Context, bot string, in DirectMessageInput) (DirectMessageSent, error)
}
DirectMessenger is the Messenger's direct messages, which Capabilities.DirectMessages needs.
type EditMessageJSONRequestBody ¶
type EditMessageJSONRequestBody = MessageContent
EditMessageJSONRequestBody defines body for EditMessage for application/json ContentType.
type Error ¶
type Error struct {
// Class is the class Muster acts on.
Class Class
// Detail is shown to the Admin as the reason, as untrusted text. It must not carry a secret; the library replaces
// the bridge's tokens and push secrets in it anyway, and cuts it to 1000 characters.
Detail string
// RetryAfter is how long Muster waits, for ClassRateLimited; it is sent in whole seconds, rounded up.
RetryAfter time.Duration
// Scope is what the wait of ClassRateLimited holds: ProblemScopeChannel (the default) or ProblemScopeBot.
Scope ProblemScope
// Reason is ReasonEditWindowClosed on a ClassGone answer to an edit when the message is too old to edit; empty
// otherwise. The library sends no other reason, and logs one as a mistake of the bridge.
Reason ProblemReason
}
Error is an answer of the messenger that the bridge classifies for Muster. A Messenger method returns one to answer its class; any other error is answered as transient, with a generic detail.
func ForbiddenError ¶
ForbiddenError says that the channel does not exist, the bot lacks the rights there, or the person blocked the bot. A channel that does not exist is forbidden, never gone.
func GoneError ¶
func GoneError(detail string, reason ...ProblemReason) *Error
GoneError says that the message or its Thread no longer exists. With ReasonEditWindowClosed it says instead that the message is past the messenger's edit window. Only the first reason is used.
Example ¶
A Messenger answers a class of the protocol with an *Error; Muster then posts a new Root message instead of editing.
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
bridge "github.com/muster-io/muster-bridge-go"
)
// notebook is a messenger that keeps the bot's messages in memory: a whole Messenger in a few lines.
type notebook struct {
mu sync.Mutex
messages []bridge.MessageInput
}
func (n *notebook) PostMessage(_ context.Context, _ string, in bridge.MessageInput) (bridge.MessagePosted, error) {
if in.ChannelId != "alerts" {
return bridge.MessagePosted{}, bridge.ForbiddenError("The bot has not joined this channel.")
}
n.mu.Lock()
defer n.mu.Unlock()
n.messages = append(n.messages, in)
return bridge.MessagePosted{MessageId: "n" + strconv.Itoa(len(n.messages))}, nil
}
func (n *notebook) EditMessage(_ context.Context, _, id string, in bridge.MessageContent) (bridge.MessageEdited,
error) {
n.mu.Lock()
defer n.mu.Unlock()
i, err := strconv.Atoi(strings.TrimPrefix(id, "n"))
if err != nil || i < 1 || i > len(n.messages) {
return bridge.MessageEdited{}, bridge.GoneError("That message is gone.")
}
n.messages[i-1].Title, n.messages[i-1].Body = in.Title, in.Body
return bridge.MessageEdited{}, nil
}
func (n *notebook) ReplyInThread(_ context.Context, _, id string, _ bridge.ThreadReplyInput) (bridge.ThreadReplyPosted,
error) {
return bridge.ThreadReplyPosted{MessageId: id + "-reply", ThreadRef: id}, nil
}
func (n *notebook) CheckChannel(_ context.Context, _, channelID string) (string, error) {
if channelID != "alerts" {
return "", bridge.ForbiddenError("The bot has not joined this channel.")
}
return "Alerts", nil
}
func (n *notebook) AnswerEvent(context.Context, string, bridge.ReportedEvent, string) (bridge.AnswerShownShownAs,
error) {
return bridge.AnswerShownShownAsThread, nil
}
// newNotebookBridge is a bridge with one bot, main, whose token is example-token.
func newNotebookBridge(m bridge.Messenger) *bridge.Bridge {
b, err := bridge.New(bridge.Config{
Name: "notebook-bridge",
Version: "1.0.0",
MessengerName: "Notebook",
Bots: []bridge.Bot{{Name: "main", Token: "example-token"}},
Limits: bridge.Limits{MaxBodyLength: 4000},
Capabilities: bridge.Capabilities{Buttons: &bridge.Capabilities_Buttons{MaxPerMessage: 5, MaxPerRow: 5,
DataMaxBytes: 64, Styles: []bridge.ButtonStyle{bridge.ButtonStylePrimary}}},
Logger: slog.New(slog.DiscardHandler),
}, m)
if err != nil {
panic(err)
}
return b
}
// inkNotebook refuses every edit, as a messenger does for messages older than its edit window.
type inkNotebook struct{ *notebook }
func (inkNotebook) EditMessage(context.Context, string, string, bridge.MessageContent) (bridge.MessageEdited, error) {
return bridge.MessageEdited{}, bridge.GoneError("Messages older than 48 hours cannot be edited.",
bridge.ReasonEditWindowClosed)
}
func main() {
b := newNotebookBridge(inkNotebook{¬ebook{}})
defer func() { _ = b.Close(context.Background()) }()
body := `{"status":"acknowledged","color":"#f9a825","emoji":"👀","title":"Disk full","body":"",` +
`"link":{"url":"https://muster.example.org/g/1","label":"Muster"},"buttons":[]}`
r := httptest.NewRequestWithContext(context.Background(), http.MethodPatch, "/v1/messages/n1",
strings.NewReader(body))
r.Header.Set("Authorization", "Bearer example-token")
r.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.Handler().ServeHTTP(w, r)
var p bridge.Problem
if err := json.NewDecoder(w.Body).Decode(&p); err != nil {
panic(err)
}
fmt.Println(w.Code, *p.Class, *p.Reason, *p.Detail)
}
Output: 404 gone edit_window_closed Messages older than 48 hours cannot be edited.
func InvalidError ¶
InvalidError says that the request cannot succeed as sent: too long, malformed, an unknown id.
func RateLimitedError ¶
func RateLimitedError(wait time.Duration, scope ProblemScope, detail string) *Error
RateLimitedError asks Muster to wait before the next request, for the channel or, with ProblemScopeBot, for every channel of the bot. An empty scope is ProblemScopeChannel.
func TransientError ¶
TransientError is a temporary failure: the messenger is down, a timeout. Muster retries.
type ErrorClass ¶
type ErrorClass string
ErrorClass defines model for ErrorClass.
const ( ErrorClassForbidden ErrorClass = "forbidden" ErrorClassGone ErrorClass = "gone" ErrorClassInvalid ErrorClass = "invalid" ErrorClassRateLimited ErrorClass = "rate_limited" ErrorClassTransient ErrorClass = "transient" )
Defines values for ErrorClass.
func (ErrorClass) Valid ¶
func (e ErrorClass) Valid() bool
Valid indicates whether the value is a known member of the ErrorClass enum.
type Event ¶
type Event struct {
// contains filtered or unexported fields
}
Event One event; `type` selects the variant. `account_link` events are always sent; the others only with the feature they belong to (`buttons`, `thread_messages`, `deletion_events`).
func (Event) AsAccountLinkEvent ¶
func (t Event) AsAccountLinkEvent() (AccountLinkEvent, error)
AsAccountLinkEvent returns the union data inside the Event as a AccountLinkEvent
func (Event) AsMessageDeletedEvent ¶
func (t Event) AsMessageDeletedEvent() (MessageDeletedEvent, error)
AsMessageDeletedEvent returns the union data inside the Event as a MessageDeletedEvent
func (Event) AsPressEvent ¶
func (t Event) AsPressEvent() (PressEvent, error)
AsPressEvent returns the union data inside the Event as a PressEvent
func (Event) AsThreadMessageEvent ¶
func (t Event) AsThreadMessageEvent() (ThreadMessageEvent, error)
AsThreadMessageEvent returns the union data inside the Event as a ThreadMessageEvent
func (Event) Discriminator ¶
func (*Event) FromAccountLinkEvent ¶
func (t *Event) FromAccountLinkEvent(v AccountLinkEvent) error
FromAccountLinkEvent overwrites any union data inside the Event as the provided AccountLinkEvent
func (*Event) FromMessageDeletedEvent ¶
func (t *Event) FromMessageDeletedEvent(v MessageDeletedEvent) error
FromMessageDeletedEvent overwrites any union data inside the Event as the provided MessageDeletedEvent
func (*Event) FromPressEvent ¶
func (t *Event) FromPressEvent(v PressEvent) error
FromPressEvent overwrites any union data inside the Event as the provided PressEvent
func (*Event) FromThreadMessageEvent ¶
func (t *Event) FromThreadMessageEvent(v ThreadMessageEvent) error
FromThreadMessageEvent overwrites any union data inside the Event as the provided ThreadMessageEvent
func (Event) MarshalJSON ¶
func (*Event) MergeAccountLinkEvent ¶
func (t *Event) MergeAccountLinkEvent(v AccountLinkEvent) error
MergeAccountLinkEvent performs a merge with any union data inside the Event, using the provided AccountLinkEvent
func (*Event) MergeMessageDeletedEvent ¶
func (t *Event) MergeMessageDeletedEvent(v MessageDeletedEvent) error
MergeMessageDeletedEvent performs a merge with any union data inside the Event, using the provided MessageDeletedEvent
func (*Event) MergePressEvent ¶
func (t *Event) MergePressEvent(v PressEvent) error
MergePressEvent performs a merge with any union data inside the Event, using the provided PressEvent
func (*Event) MergeThreadMessageEvent ¶
func (t *Event) MergeThreadMessageEvent(v ThreadMessageEvent) error
MergeThreadMessageEvent performs a merge with any union data inside the Event, using the provided ThreadMessageEvent
func (*Event) UnmarshalJSON ¶
func (Event) ValueByDiscriminator ¶
type EventBase ¶
type EventBase struct {
// Id Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the `webhook-id` header.
Id string `json:"id"`
// OccurredAt When it happened in the messenger, or when the bridge learned of it.
OccurredAt time.Time `json:"occurred_at"`
Type string `json:"type"`
}
EventBase defines model for EventBase.
type EventBatch ¶
type EventBatch struct {
Events []Event `json:"events"`
// NextCursor Send it as `cursor` with the next call; opaque to Muster.
NextCursor string `json:"next_cursor"`
}
EventBatch defines model for EventBatch.
type FindUsersParams ¶
type FindUsersParams struct {
// Handle The handle as the person entered it, in the kind `user_lookup` declares.
Handle string `form:"handle" json:"handle"`
}
FindUsersParams defines parameters for FindUsers.
type Forbidden ¶
type Forbidden = Problem
Forbidden RFC 9457 problem details with the Bridge protocol's members.
type Info ¶
type Info struct {
Bridge Info_Bridge `json:"bridge"`
// Capabilities The optional features of the bridge. A missing member means the feature is not offered.
Capabilities Capabilities `json:"capabilities"`
// EventDelivery The event delivery modes the bridge offers; each Connection chooses one.
EventDelivery []InfoEventDelivery `json:"event_delivery"`
Limits Limits `json:"limits"`
// LinkUrlTemplate An `http` or `https` link that opens a chat with the bot and hands it the Account link token, with `{token}` where the token goes, such as `https://chat.example.org/bots/muster?start={token}`. Muster shows the link next to the token in the profile; without one it shows only the token. Another scheme makes `info` fail the Connection check.
LinkUrlTemplate *string `json:"link_url_template,omitempty"`
Messenger Info_Messenger `json:"messenger"`
// ProtocolVersion The major version of this protocol the bridge speaks.
ProtocolVersion InfoProtocolVersion `json:"protocol_version"`
// UserLookup What a person enters to be found for an Account link by code, and what `findUsers` searches by. `none` offers only the token sent to the bot.
UserLookup InfoUserLookup `json:"user_lookup"`
}
Info defines model for Info.
type InfoEventDelivery ¶
type InfoEventDelivery string
InfoEventDelivery defines model for Info.EventDelivery.
const ( InfoEventDeliveryPoll InfoEventDelivery = "poll" InfoEventDeliveryPush InfoEventDelivery = "push" )
Defines values for InfoEventDelivery.
func (InfoEventDelivery) Valid ¶
func (e InfoEventDelivery) Valid() bool
Valid indicates whether the value is a known member of the InfoEventDelivery enum.
type InfoProtocolVersion ¶
type InfoProtocolVersion string
InfoProtocolVersion The major version of this protocol the bridge speaks.
const (
InfoProtocolVersionN1 InfoProtocolVersion = "1"
)
Defines values for InfoProtocolVersion.
func (InfoProtocolVersion) Valid ¶
func (e InfoProtocolVersion) Valid() bool
Valid indicates whether the value is a known member of the InfoProtocolVersion enum.
type InfoUserLookup ¶
type InfoUserLookup string
InfoUserLookup What a person enters to be found for an Account link by code, and what `findUsers` searches by. `none` offers only the token sent to the bot.
const ( InfoUserLookupEmail InfoUserLookup = "email" InfoUserLookupLogin InfoUserLookup = "login" InfoUserLookupNone InfoUserLookup = "none" InfoUserLookupUserId InfoUserLookup = "user_id" InfoUserLookupUsername InfoUserLookup = "username" )
Defines values for InfoUserLookup.
func (InfoUserLookup) Valid ¶
func (e InfoUserLookup) Valid() bool
Valid indicates whether the value is a known member of the InfoUserLookup enum.
type Info_Bridge ¶
Info_Bridge defines model for Info.Bridge.
type Info_Messenger ¶
type Info_Messenger struct {
// Name The messenger's name as people know it; Muster shows it on the Connection and the Destinations.
Name string `json:"name"`
}
Info_Messenger defines model for Info.Messenger.
type Invalid ¶
type Invalid = Problem
Invalid RFC 9457 problem details with the Bridge protocol's members.
type KeyClaim ¶
type KeyClaim struct {
State KeyState
// Answer is the stored answer, with KeyAnswered.
Answer StoredAnswer
// ID names the claim, with KeyClaimed; the library gives it back to SaveKey and ReleaseKey.
ID string
}
KeyClaim is the result of Store.ClaimKey.
type Limits ¶
type Limits struct {
// EditMaxAgeSeconds How long after posting the bot can still edit a message; `null` when edits never expire. Muster stops editing a minute earlier.
EditMaxAgeSeconds *int `json:"edit_max_age_seconds"`
// MaxBodyLength The longest `body`, in Unicode code points of the CommonMark Muster sends, that the bridge always accepts for a post and for an edit alike, after its own rendering.
MaxBodyLength int `json:"max_body_length"`
// SendPerSecond A hint for the Connection limiter, over all channels of the bot.
SendPerSecond *float32 `json:"send_per_second,omitempty"`
// SendPerSecondPerChannel A hint for the Destination limiter; `rate_limited` stays the truth.
SendPerSecondPerChannel *float32 `json:"send_per_second_per_channel,omitempty"`
}
Limits defines model for Limits.
type Mention ¶
type Mention struct {
DisplayName string `json:"display_name"`
// ExternalId The account's id in the messenger, as the bridge reported it when the account was linked.
ExternalId string `json:"external_id"`
// Ref Names the marker `muster-mention:<ref>` in the body.
Ref string `json:"ref"`
}
Mention defines model for Mention.
type MessageContent ¶
type MessageContent struct {
// Body CommonMark, as the description of this protocol says; at most `limits.max_body_length` code points.
Body string `json:"body"`
// Buttons In order. Empty when the message has none, always empty without `capabilities.buttons`.
Buttons []Button `json:"buttons"`
// Color The colour of the status, for messengers that colour messages.
Color string `json:"color"`
// Emoji The status as an emoji (🔴 🟠 ⚪ 🟢 ⛈), for messengers without colours.
Emoji string `json:"emoji"`
Link Link `json:"link"`
// Status The status of the Alert Group the message shows; `storm` for a Storm summary.
Status Status `json:"status"`
// Title `#N` and the Alert Group's title, plain text; shown as the heading and linked to `link`.
Title string `json:"title"`
}
MessageContent defines model for MessageContent.
type MessageDeleted ¶
type MessageDeleted struct {
// UpdateID is the messenger's own id of the update, "" when it has none.
UpdateID string
// MessageID is the deleted message.
MessageID string
// At is when it happened; zero is now.
At time.Time
}
MessageDeleted is the deletion of a message of the bot in the messenger.
type MessageDeletedEvent ¶
type MessageDeletedEvent struct {
// Id Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the `webhook-id` header.
Id string `json:"id"`
MessageId string `json:"message_id"`
// OccurredAt When it happened in the messenger, or when the bridge learned of it.
OccurredAt time.Time `json:"occurred_at"`
Type interface{} `json:"type"`
}
MessageDeletedEvent defines model for MessageDeletedEvent.
type MessageEdited ¶
type MessageEdited struct {
Truncated bool `json:"truncated"`
}
MessageEdited defines model for MessageEdited.
type MessageInput ¶
type MessageInput struct {
// Body CommonMark, as the description of this protocol says; at most `limits.max_body_length` code points.
Body string `json:"body"`
// Buttons In order. Empty when the message has none, always empty without `capabilities.buttons`.
Buttons []Button `json:"buttons"`
ChannelId string `json:"channel_id"`
// Color The colour of the status, for messengers that colour messages.
Color string `json:"color"`
// Emoji The status as an emoji (🔴 🟠 ⚪ 🟢 ⛈), for messengers without colours.
Emoji string `json:"emoji"`
// Kind A Root message of an Alert Group, a Storm summary, or a test message sent from the Destination page.
Kind MessageKind `json:"kind"`
Link Link `json:"link"`
Mentions []Mention `json:"mentions"`
// Notify Loud (`true`) or Quiet (`false`).
Notify bool `json:"notify"`
// Status The status of the Alert Group the message shows; `storm` for a Storm summary.
Status Status `json:"status"`
// Title `#N` and the Alert Group's title, plain text; shown as the heading and linked to `link`.
Title string `json:"title"`
}
MessageInput defines model for MessageInput.
type MessageKind ¶
type MessageKind string
MessageKind A Root message of an Alert Group, a Storm summary, or a test message sent from the Destination page.
const ( MessageKindRoot MessageKind = "root" MessageKindStormSummary MessageKind = "storm_summary" MessageKindTest MessageKind = "test" )
Defines values for MessageKind.
func (MessageKind) Valid ¶
func (e MessageKind) Valid() bool
Valid indicates whether the value is a known member of the MessageKind enum.
type MessagePosted ¶
type MessagePosted struct {
// MessageId Opaque to Muster; it encodes whatever the bridge needs to edit the message and reply under it.
MessageId string `json:"message_id"`
// Replayed The `Idempotency-Key` was seen before, and this is its first result.
Replayed bool `json:"replayed"`
// ThreadRef The message's Thread, when known at once; otherwise the first `replyInThread` returns it.
ThreadRef *string `json:"thread_ref,omitempty"`
// Truncated The bridge shortened the body to fit its messenger.
Truncated bool `json:"truncated"`
// Url A link that opens the message in the messenger, if it has one; Muster shows it on the Alert Group page and links to it from the Thread of an older Root message.
Url *string `json:"url,omitempty"`
}
MessagePosted defines model for MessagePosted.
type Messenger ¶
type Messenger interface {
// PostMessage posts a message to in.ChannelId: a Root message, a Storm summary or a test message.
PostMessage(ctx context.Context, bot string, in MessageInput) (MessagePosted, error)
// EditMessage replaces the whole message with in; editing to the state the message already shows is a success.
EditMessage(ctx context.Context, bot, messageID string, in MessageContent) (MessageEdited, error)
// ReplyInThread posts a reply in the Thread of the message. in.ThreadRef is the thread_ref Muster sent, nil for
// none; the answer's ThreadRef is the one Muster keeps and sends with the next reply.
ReplyInThread(ctx context.Context, bot, messageID string, in ThreadReplyInput) (ThreadReplyPosted, error)
// CheckChannel posts nothing and returns the channel's name as people see it, or a forbidden *Error when the bot
// cannot post, edit and reply there.
CheckChannel(ctx context.Context, bot, channelID string) (string, error)
// AnswerEvent shows text to whoever caused the event, which the bridge reported, and returns where it was shown.
AnswerEvent(ctx context.Context, bot string, event ReportedEvent, text string) (AnswerShownShownAs, error)
}
Messenger is what every bridge implements: the calls to its messenger. Each method is given the bot whose token Muster sent, by its name in Config.Bots, and returns the protocol's answer, or an error: an *Error to answer its class, any other error to answer transient with a generic detail. The library validates every request before it calls a method and every answer before it sends it.
type Person ¶
type Person struct {
DisplayName string `json:"display_name"`
// ExternalId The account's stable id in the messenger.
ExternalId string `json:"external_id"`
// Handle The account's handle of the kind `user_lookup` declares, when the bridge knows it.
Handle *string `json:"handle,omitempty"`
}
Person defines model for Person.
type PersonList ¶
type PersonList struct {
Items []Person `json:"items"`
}
PersonList defines model for PersonList.
type PollEventsParams ¶
type PollEventsParams struct {
// Cursor The `next_cursor` of the previous answer; opaque to Muster.
Cursor *string `form:"cursor,omitempty" json:"cursor,omitempty"`
// Wait How long to hold the request open when there are no events, in seconds.
Wait *int `form:"wait,omitempty" json:"wait,omitempty"`
}
PollEventsParams defines parameters for PollEvents.
type PostMessageJSONRequestBody ¶
type PostMessageJSONRequestBody = MessageInput
PostMessageJSONRequestBody defines body for PostMessage for application/json ContentType.
type PostMessageParams ¶
type PostMessageParams struct {
// IdempotencyKey Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token.
IdempotencyKey IdempotencyKey `json:"Idempotency-Key"`
}
PostMessageParams defines parameters for PostMessage.
type Press ¶
type Press struct {
// UpdateID is the messenger's own id of the update, "" when it has none; an update reported twice with the same
// id is queued once.
UpdateID string
// MessageID is the message the button belongs to: a Root message or a Thread reply.
MessageID string
// User is the person who pressed.
User Person
// Data is the button's data, unchanged.
Data string
// At is when it happened; zero is now.
At time.Time
// Chat is anything the bridge needs to answer the person, such as a chat or callback id; the library keeps it with
// the event, gives it back in ReportedEvent.Chat and never sends it to Muster.
Chat string
}
Press is a press of a button of a message of the bot, already confirmed to the messenger.
type PressEvent ¶
type PressEvent struct {
// Data The button's `data`, unchanged.
Data string `json:"data"`
// Id Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the `webhook-id` header.
Id string `json:"id"`
// MessageId The message the button belongs to, a Root message or a Thread reply.
MessageId string `json:"message_id"`
// OccurredAt When it happened in the messenger, or when the bridge learned of it.
OccurredAt time.Time `json:"occurred_at"`
Type interface{} `json:"type"`
User Person `json:"user"`
}
PressEvent defines model for PressEvent.
type Problem ¶
type Problem struct {
Class *ErrorClass `json:"class,omitempty"`
// Detail Shown to the Admin as the reason, as untrusted text; never put a secret in it.
Detail *string `json:"detail,omitempty"`
Instance *string `json:"instance,omitempty"`
// Reason For `gone`, `edit_window_closed` when the messenger refuses the edit only because of the message's age.
Reason *ProblemReason `json:"reason,omitempty"`
// Scope For `rate_limited`, whether the wait holds one channel or every channel of the bot.
Scope *ProblemScope `json:"scope,omitempty"`
Status *int `json:"status,omitempty"`
Title *string `json:"title,omitempty"`
Type *string `json:"type,omitempty"`
}
Problem RFC 9457 problem details with the Bridge protocol's members.
type ProblemReason ¶
type ProblemReason string
ProblemReason For `gone`, `edit_window_closed` when the messenger refuses the edit only because of the message's age.
const (
ProblemReasonEditWindowClosed ProblemReason = "edit_window_closed"
)
Defines values for ProblemReason.
func (ProblemReason) Valid ¶
func (e ProblemReason) Valid() bool
Valid indicates whether the value is a known member of the ProblemReason enum.
type ProblemScope ¶
type ProblemScope string
ProblemScope For `rate_limited`, whether the wait holds one channel or every channel of the bot.
const ( ProblemScopeBot ProblemScope = "bot" ProblemScopeChannel ProblemScope = "channel" )
Defines values for ProblemScope.
func (ProblemScope) Valid ¶
func (e ProblemScope) Valid() bool
Valid indicates whether the value is a known member of the ProblemScope enum.
type PushTarget ¶
type PushTarget struct {
// URL is the callback address of the Bridge Connection, with the http or https scheme and a host.
URL string
// Secret is the push secret as the Bridge Connection shows it: whsec_ and the base64 of the signing key.
Secret string
}
PushTarget is where the events of a bot are pushed, as the Bridge Connection in Muster shows it.
type RateLimited ¶
type RateLimited = Problem
RateLimited RFC 9457 problem details with the Bridge protocol's members.
type ReplyInThreadJSONRequestBody ¶
type ReplyInThreadJSONRequestBody = ThreadReplyInput
ReplyInThreadJSONRequestBody defines body for ReplyInThread for application/json ContentType.
type ReplyInThreadParams ¶
type ReplyInThreadParams struct {
// IdempotencyKey Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token.
IdempotencyKey IdempotencyKey `json:"Idempotency-Key"`
}
ReplyInThreadParams defines parameters for ReplyInThread.
type ReportedEvent ¶
type ReportedEvent struct {
ID string
Type EventType
// MessageID is the message of a press or a Thread message.
MessageID string
// ThreadRef is the thread_ref of a Thread message, "" for none.
ThreadRef string
User Person
Chat string
OccurredAt time.Time
}
ReportedEvent is an event the bridge reported, as AnswerEvent is given it.
type SendDirectMessageJSONRequestBody ¶
type SendDirectMessageJSONRequestBody = DirectMessageInput
SendDirectMessageJSONRequestBody defines body for SendDirectMessage for application/json ContentType.
type SendDirectMessageParams ¶
type SendDirectMessageParams struct {
// IdempotencyKey Identifies the request across retries; a repeated key with the same token returns the first result without creating anything again. Keys are scoped per token.
IdempotencyKey IdempotencyKey `json:"Idempotency-Key"`
}
SendDirectMessageParams defines parameters for SendDirectMessage.
type Status ¶
type Status string
Status The status of the Alert Group the message shows; `storm` for a Storm summary.
type Store ¶
type Store interface {
// ClaimKey returns KeyAnswered with the answer when one is stored for (bot, key) until after now; else KeyRunning
// when another claim holds the key until after now; else it claims the key until `until` and returns KeyClaimed
// with an ID that tells this claim from any later claim of the same key.
ClaimKey(ctx context.Context, bot, key string, now, until time.Time) (KeyClaim, error)
// SaveKey stores the answer of (bot, key), kept until `until`, in place of the claim claimID. When nothing holds the
// key, because the claim expired and was forgotten, it stores the answer all the same. When another claim or an
// answer holds the key, SaveKey leaves it as it is: the first answer stays, and so does a newer request's claim.
SaveKey(ctx context.Context, bot, key, claimID string, answer StoredAnswer, until time.Time) error
// ReleaseKey removes the claim claimID of (bot, key), so that a retry with the key runs again. It removes nothing
// else: not an answer, and not the claim of a request that took the key over after claimID expired.
ReleaseKey(ctx context.Context, bot, key, claimID string) error
// AppendEvent queues the event for the bot unless an event with its ID is kept, and reports whether it queued it.
AppendEvent(ctx context.Context, bot string, event StoredEvent, now time.Time) (bool, error)
// EventsAfter returns up to limit of the bot's kept events, oldest first, that follow cursor ("" starts before the
// oldest), with the cursor that follows the last one returned. Returning none, it gives cursor back unchanged, or,
// for "", the cursor at the end of the queue. A cursor that this store did not issue for the bot is
// ErrUnknownCursor.
EventsAfter(ctx context.Context, bot, cursor string, limit int, now time.Time) ([]StoredEvent, string, error)
// FindEvent returns the kept event of the bot with the id, and whether there is one.
FindEvent(ctx context.Context, bot, id string, now time.Time) (StoredEvent, bool, error)
}
Store keeps, per bot, the first answers of Idempotency-Keys and the queued events. Every method is given the time of the library's clock and must be safe for concurrent use. A bridge that must keep its keys and events across restarts implements it over its own database; NewMemoryStore keeps them in memory.
func NewMemoryStore ¶
func NewMemoryStore() Store
NewMemoryStore returns a Store that keeps keys and events in memory, for one process: a restart forgets them, and Muster then polls again without a cursor.
Its memory is bounded by time, not by size: it holds every event of the last 24 hours, whatever Muster has already polled or acknowledged (a Thread message carries up to 4000 characters), and the answers of the Idempotency-Keys of the last 24 hours. A bridge whose day of traffic does not fit in memory implements Store over its database, which may also drop the events before the cursor that Muster last sent.
Example ¶
A Store implementation follows the memory store: a key is claimed while its request runs, then holds its answer. Only the holder of a claim, named by its ID, saves or releases it.
package main
import (
"context"
"fmt"
"net/http"
"time"
bridge "github.com/muster-io/muster-bridge-go"
)
func main() {
ctx := context.Background()
store := bridge.NewMemoryStore()
now := time.Date(2026, 10, 18, 9, 0, 0, 0, time.UTC)
claim, _ := store.ClaimKey(ctx, "main", "key-1", now, now.Add(2*time.Minute))
fmt.Println(claim.State == bridge.KeyClaimed)
running, _ := store.ClaimKey(ctx, "main", "key-1", now, now.Add(2*time.Minute))
fmt.Println(running.State == bridge.KeyRunning)
answer := bridge.StoredAnswer{Status: http.StatusCreated, Body: []byte(`{"message_id":"n1","replayed":false}`)}
_ = store.SaveKey(ctx, "main", "key-1", claim.ID, answer, now.Add(24*time.Hour))
later, _ := store.ClaimKey(ctx, "main", "key-1", now.Add(time.Hour), now.Add(time.Hour+2*time.Minute))
fmt.Println(later.State == bridge.KeyAnswered, later.Answer.Status)
}
Output: true true true 201
type StoredAnswer ¶
type StoredAnswer struct {
Status int
// Body is the JSON of the first answer, with replayed false.
Body []byte
}
StoredAnswer is the first successful answer of an Idempotency-Key.
type StoredEvent ¶
type StoredEvent struct {
ID string
Type EventType
// Body is the event exactly as pollEvents and the push send it.
Body []byte
// Chat is what the bridge gave with the report; it is never sent to Muster.
Chat string
// Until is the time until which the event is kept, at least.
Until time.Time
}
StoredEvent is an event queued for a bot.
type ThreadMessage ¶
type ThreadMessage struct {
// UpdateID is the messenger's own id of the update, "" when it has none.
UpdateID string
// MessageID is the bot's message that owns the Thread, as replyInThread names it; never the id of a reply.
MessageID string
// ThreadRef is the Thread's thread_ref, "" for none.
ThreadRef string
// User is the person who wrote.
User Person
// Text is the message as plain text, mentions of the bot removed; it is cut to 4000 characters.
Text string
// At is when it happened; zero is now.
At time.Time
// Chat is kept with the event and given back in ReportedEvent.Chat.
Chat string
}
ThreadMessage is what a person wrote under a message of the bot, in its Thread; the bot's own replies are not reported.
type ThreadMessageEvent ¶
type ThreadMessageEvent struct {
// Id Stable across retries and polls; Muster handles each id once. Printable ASCII without spaces, so that it can travel as the `webhook-id` header.
Id string `json:"id"`
// MessageId The message whose Thread it is in — the `message_id` in the path of `replyInThread`, usually a Root message, never the id of a reply.
MessageId string `json:"message_id"`
// OccurredAt When it happened in the messenger, or when the bridge learned of it.
OccurredAt time.Time `json:"occurred_at"`
// Text The message as plain text, mentions of the bot removed.
Text string `json:"text"`
ThreadRef *string `json:"thread_ref"`
Type interface{} `json:"type"`
User Person `json:"user"`
}
ThreadMessageEvent defines model for ThreadMessageEvent.
type ThreadReplyInput ¶
type ThreadReplyInput struct {
// Body CommonMark, at most `limits.max_body_length` code points.
Body string `json:"body"`
Buttons []Button `json:"buttons"`
Mentions []Mention `json:"mentions"`
// Notify Loud (`true`) or Quiet (`false`).
Notify bool `json:"notify"`
// ThreadRef The last `thread_ref` Muster received for the message — from `postMessage` or an earlier reply — or `null` when it has none.
ThreadRef *string `json:"thread_ref"`
}
ThreadReplyInput defines model for ThreadReplyInput.
type ThreadReplyPosted ¶
type ThreadReplyPosted struct {
// MessageId The reply; presses on its buttons name it.
MessageId string `json:"message_id"`
Replayed bool `json:"replayed"`
ThreadRef string `json:"thread_ref"`
Truncated bool `json:"truncated"`
}
ThreadReplyPosted defines model for ThreadReplyPosted.
type Transient ¶
type Transient = Problem
Transient RFC 9457 problem details with the Bridge protocol's members.
type Unauthorized ¶
type Unauthorized = Problem
Unauthorized RFC 9457 problem details with the Bridge protocol's members.
type UserFinder ¶
type UserFinder interface {
// FindUser returns the one account with the handle, of the kind Config.UserLookup declares, or nil for nobody.
FindUser(ctx context.Context, bot, handle string) (*Person, error)
}
UserFinder is the Messenger's lookup of people, which a UserLookup other than InfoUserLookupNone needs.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
chat
command
Command chat is the example bridge: a small chat with its own web page — channels, Threads, buttons, direct chats between a person and the bot, a picker of who the viewer is, and an event log — served on one listener beside the Bridge protocol, so that Muster can be tried, and tested, with a messenger that runs anywhere.
|
Command chat is the example bridge: a small chat with its own web page — channels, Threads, buttons, direct chats between a person and the bot, a picker of who the viewer is, and an event log — served on one listener beside the Bridge protocol, so that Muster can be tried, and tested, with a messenger that runs anywhere. |
|
minimal
command
Command minimal is a bridge whose messenger lives in memory and writes everything the bot posts to its log, so that Muster, muster bridge check or a person with curl can try the Bridge protocol without a real messenger.
|
Command minimal is a bridge whose messenger lives in memory and writes everything the bot posts to its log, so that Muster, muster bridge check or a person with curl can try the Bridge protocol without a real messenger. |