1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 09:37:02 +00:00

Create V2 API for monitor+deletes, revert V1 API (#347)

* Revert socketv1 controller API to maintain V1 contract, introduce
V2 controller for Inbucket UI.

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Introduce MessageID for deletes, instead of recycling header

Signed-off-by: James Hillyerd <james@hillyerd.com>

* Update UI for monitor V2 API

Signed-off-by: James Hillyerd <james@hillyerd.com>

---------

Signed-off-by: James Hillyerd <james@hillyerd.com>
This commit is contained in:
James Hillyerd
2023-02-17 12:37:17 -08:00
committed by GitHub
parent b554c7db83
commit 82ddf2141c
7 changed files with 283 additions and 64 deletions

View File

@@ -47,10 +47,3 @@ type JSONMessageBodyV1 struct {
Text string `json:"text"` Text string `json:"text"`
HTML string `json:"html"` HTML string `json:"html"`
} }
// JSONMonitorEventV1 contains events for the Inbucket mailbox and monitor tabs.
type JSONMonitorEventV1 struct {
// Event variant: `message-deleted`, `message-stored`.
Variant string `json:"variant"`
Header *JSONMessageHeaderV1 `json:"header"`
}

View File

@@ -0,0 +1,15 @@
package model
// JSONMessageIDV2 uniquely identifies a message.
type JSONMessageIDV2 struct {
Mailbox string `json:"mailbox"`
ID string `json:"id"`
}
// JSONMonitorEventV2 contains events for the Inbucket mailbox and monitor tabs.
type JSONMonitorEventV2 struct {
// Event variant: `message-deleted`, `message-stored`.
Variant string `json:"variant"`
Identifier *JSONMessageIDV2 `json:"identifier"`
Header *JSONMessageHeaderV1 `json:"header"`
}

View File

@@ -22,4 +22,10 @@ func SetupRoutes(r *mux.Router) {
web.Handler(MonitorAllMessagesV1)).Name("MonitorAllMessagesV1").Methods("GET") web.Handler(MonitorAllMessagesV1)).Name("MonitorAllMessagesV1").Methods("GET")
r.Path("/v1/monitor/messages/{name}").Handler( r.Path("/v1/monitor/messages/{name}").Handler(
web.Handler(MonitorMailboxMessagesV1)).Name("MonitorMailboxMessagesV1").Methods("GET") web.Handler(MonitorMailboxMessagesV1)).Name("MonitorMailboxMessagesV1").Methods("GET")
// API v2
r.Path("/v2/monitor/messages").Handler(
web.Handler(MonitorAllMessagesV2)).Name("MonitorAllMessagesV2").Methods("GET")
r.Path("/v2/monitor/messages/{name}").Handler(
web.Handler(MonitorMailboxMessagesV2)).Name("MonitorMailboxMessagesV2").Methods("GET")
} }

View File

@@ -15,37 +15,37 @@ import (
const ( const (
// Time allowed to write a message to the peer. // Time allowed to write a message to the peer.
writeWait = 10 * time.Second writeWaitV1 = 10 * time.Second
// Send pings to peer with this period. Must be less than pongWait. // Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10 pingPeriodV1 = (pongWaitV1 * 9) / 10
// Time allowed to read the next pong message from the peer. // Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second pongWaitV1 = 60 * time.Second
// Maximum message size allowed from peer. // Maximum message size allowed from peer.
maxMessageSize = 512 maxMessageSizeV1 = 512
) )
// options for gorilla connection upgrader // options for gorilla connection upgrader
var upgrader = websocket.Upgrader{ var upgraderV1 = websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
} }
// msgListener handles messages from the msghub // msgListenerV1 handles messages from the msghub
type msgListener struct { type msgListenerV1 struct {
hub *msghub.Hub // Global message hub. hub *msghub.Hub // Global message hub
c chan *model.JSONMonitorEventV1 // Queue of incoming events. c chan event.MessageMetadata // Queue of messages from Receive()
mailbox string // Name of mailbox to monitor, "" == all mailboxes. mailbox string // Name of mailbox to monitor, "" == all mailboxes
} }
// newMsgListener creates a listener and registers it. Optional mailbox parameter will restrict // newMsgListenerV1 creates a listener and registers it. Optional mailbox parameter will restrict
// messages sent to WebSocket to that mailbox only. // messages sent to WebSocket to that mailbox only.
func newMsgListener(hub *msghub.Hub, mailbox string) *msgListener { func newMsgListenerV1(hub *msghub.Hub, mailbox string) *msgListenerV1 {
ml := &msgListener{ ml := &msgListenerV1{
hub: hub, hub: hub,
c: make(chan *model.JSONMonitorEventV1, 100), c: make(chan event.MessageMetadata, 100),
mailbox: mailbox, mailbox: mailbox,
} }
hub.AddListener(ml) hub.AddListener(ml)
@@ -53,50 +53,31 @@ func newMsgListener(hub *msghub.Hub, mailbox string) *msgListener {
} }
// Receive handles an incoming message. // Receive handles an incoming message.
func (ml *msgListener) Receive(msg event.MessageMetadata) error { func (ml *msgListenerV1) Receive(msg event.MessageMetadata) error {
if ml.mailbox != "" && ml.mailbox != msg.Mailbox { if ml.mailbox != "" && ml.mailbox != msg.Mailbox {
// Did not match the watched mailbox name. // Did not match the watched mailbox name.
return nil return nil
} }
ml.c <- msg
// Enqueue for websocket.
ml.c <- &model.JSONMonitorEventV1{
Variant: "message-stored",
Header: metadataToHeader(&msg),
}
return nil return nil
} }
// Delete handles a deleted message. // Delete handles a deleted message.
func (ml *msgListener) Delete(mailbox string, id string) error { func (ml *msgListenerV1) Delete(mailbox string, id string) error {
if ml.mailbox != "" && ml.mailbox != mailbox { // Deletes are ignored in socketv1 API.
// Did not match watched mailbox name.
return nil
}
// Enqueue for websocket.
ml.c <- &model.JSONMonitorEventV1{
Variant: "message-deleted",
Header: &model.JSONMessageHeaderV1{
Mailbox: mailbox,
ID: id,
},
}
return nil return nil
} }
// WSReader makes sure the websocket client is still connected, discards any messages from client // WSReader makes sure the websocket client is still connected, discards any messages from client
func (ml *msgListener) WSReader(conn *websocket.Conn) { func (ml *msgListenerV1) WSReader(conn *websocket.Conn) {
slog := log.With().Str("module", "rest").Str("proto", "WebSocket"). slog := log.With().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Logger() Str("remote", conn.RemoteAddr().String()).Logger()
defer ml.Close() defer ml.Close()
conn.SetReadLimit(maxMessageSize) conn.SetReadLimit(maxMessageSizeV1)
conn.SetReadDeadline(time.Now().Add(pongWait)) conn.SetReadDeadline(time.Now().Add(pongWaitV1))
conn.SetPongHandler(func(string) error { conn.SetPongHandler(func(string) error {
slog.Debug().Msg("Got pong") slog.Debug().Msg("Got pong")
conn.SetReadDeadline(time.Now().Add(pongWait)) conn.SetReadDeadline(time.Now().Add(pongWaitV1))
return nil return nil
}) })
@@ -119,8 +100,8 @@ func (ml *msgListener) WSReader(conn *websocket.Conn) {
} }
// WSWriter makes sure the websocket client is still connected // WSWriter makes sure the websocket client is still connected
func (ml *msgListener) WSWriter(conn *websocket.Conn) { func (ml *msgListenerV1) WSWriter(conn *websocket.Conn) {
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriodV1)
defer func() { defer func() {
ticker.Stop() ticker.Stop()
ml.Close() ml.Close()
@@ -129,20 +110,20 @@ func (ml *msgListener) WSWriter(conn *websocket.Conn) {
// Handle messages from hub until msgListener is closed // Handle messages from hub until msgListener is closed
for { for {
select { select {
case event, ok := <-ml.c: case msg, ok := <-ml.c:
conn.SetWriteDeadline(time.Now().Add(writeWait)) conn.SetWriteDeadline(time.Now().Add(writeWaitV1))
if !ok { if !ok {
// msgListener closed, exit // msgListener closed, exit
conn.WriteMessage(websocket.CloseMessage, []byte{}) conn.WriteMessage(websocket.CloseMessage, []byte{})
return return
} }
if conn.WriteJSON(event) != nil { if conn.WriteJSON(metadataToHeader(&msg)) != nil {
// Write failed // Write failed
return return
} }
case <-ticker.C: case <-ticker.C:
// Send ping // Send ping
conn.SetWriteDeadline(time.Now().Add(writeWait)) conn.SetWriteDeadline(time.Now().Add(writeWaitV1))
if conn.WriteMessage(websocket.PingMessage, []byte{}) != nil { if conn.WriteMessage(websocket.PingMessage, []byte{}) != nil {
// Write error // Write error
return return
@@ -154,7 +135,7 @@ func (ml *msgListener) WSWriter(conn *websocket.Conn) {
} }
// Close removes the listener registration // Close removes the listener registration
func (ml *msgListener) Close() { func (ml *msgListenerV1) Close() {
select { select {
case <-ml.c: case <-ml.c:
// Already closed // Already closed
@@ -169,7 +150,7 @@ func (ml *msgListener) Close() {
func MonitorAllMessagesV1( func MonitorAllMessagesV1(
w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) { w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Upgrade to Websocket. // Upgrade to Websocket.
conn, err := upgrader.Upgrade(w, req, nil) conn, err := upgraderV1.Upgrade(w, req, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -181,7 +162,7 @@ func MonitorAllMessagesV1(
log.Debug().Str("module", "rest").Str("proto", "WebSocket"). log.Debug().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket") Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket")
// Create, register listener; then interact with conn. // Create, register listener; then interact with conn.
ml := newMsgListener(ctx.MsgHub, "") ml := newMsgListenerV1(ctx.MsgHub, "")
go ml.WSWriter(conn) go ml.WSWriter(conn)
ml.WSReader(conn) ml.WSReader(conn)
return nil return nil
@@ -196,7 +177,7 @@ func MonitorMailboxMessagesV1(
return err return err
} }
// Upgrade to Websocket. // Upgrade to Websocket.
conn, err := upgrader.Upgrade(w, req, nil) conn, err := upgraderV1.Upgrade(w, req, nil)
if err != nil { if err != nil {
return err return err
} }
@@ -208,7 +189,7 @@ func MonitorMailboxMessagesV1(
log.Debug().Str("module", "rest").Str("proto", "WebSocket"). log.Debug().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket") Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket")
// Create, register listener; then interact with conn. // Create, register listener; then interact with conn.
ml := newMsgListener(ctx.MsgHub, name) ml := newMsgListenerV1(ctx.MsgHub, name)
go ml.WSWriter(conn) go ml.WSWriter(conn)
ml.WSReader(conn) ml.WSReader(conn)
return nil return nil

View File

@@ -0,0 +1,214 @@
package rest
import (
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/inbucket/inbucket/pkg/extension/event"
"github.com/inbucket/inbucket/pkg/msghub"
"github.com/inbucket/inbucket/pkg/rest/model"
"github.com/inbucket/inbucket/pkg/server/web"
"github.com/rs/zerolog/log"
)
const (
// Time allowed to write a message to the peer.
writeWaitV2 = 10 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriodV2 = (pongWaitV2 * 9) / 10
// Time allowed to read the next pong message from the peer.
pongWaitV2 = 60 * time.Second
// Maximum message size allowed from peer.
maxMessageSizeV2 = 512
)
// options for gorilla connection upgrader
var upgraderV2 = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// msgListenerV2 handles messages from the msghub
type msgListenerV2 struct {
hub *msghub.Hub // Global message hub.
c chan *model.JSONMonitorEventV2 // Queue of incoming events.
mailbox string // Name of mailbox to monitor, "" == all mailboxes.
}
// newMsgListenerV2 creates a listener and registers it. Optional mailbox parameter will restrict
// messages sent to WebSocket to that mailbox only.
func newMsgListenerV2(hub *msghub.Hub, mailbox string) *msgListenerV2 {
ml := &msgListenerV2{
hub: hub,
c: make(chan *model.JSONMonitorEventV2, 100),
mailbox: mailbox,
}
hub.AddListener(ml)
return ml
}
// Receive handles an incoming message.
func (ml *msgListenerV2) Receive(msg event.MessageMetadata) error {
if ml.mailbox != "" && ml.mailbox != msg.Mailbox {
// Did not match the watched mailbox name.
return nil
}
// Enqueue for websocket.
ml.c <- &model.JSONMonitorEventV2{
Variant: "message-stored",
Header: metadataToHeader(&msg),
}
return nil
}
// Delete handles a deleted message.
func (ml *msgListenerV2) Delete(mailbox string, id string) error {
if ml.mailbox != "" && ml.mailbox != mailbox {
// Did not match watched mailbox name.
return nil
}
// Enqueue for websocket.
ml.c <- &model.JSONMonitorEventV2{
Variant: "message-deleted",
Identifier: &model.JSONMessageIDV2{
Mailbox: mailbox,
ID: id,
},
}
return nil
}
// WSReader makes sure the websocket client is still connected, discards any messages from client
func (ml *msgListenerV2) WSReader(conn *websocket.Conn) {
slog := log.With().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Logger()
defer ml.Close()
conn.SetReadLimit(maxMessageSizeV2)
conn.SetReadDeadline(time.Now().Add(pongWaitV2))
conn.SetPongHandler(func(string) error {
slog.Debug().Msg("Got pong")
conn.SetReadDeadline(time.Now().Add(pongWaitV2))
return nil
})
for {
if _, _, err := conn.ReadMessage(); err != nil {
if websocket.IsUnexpectedCloseError(
err,
websocket.CloseNormalClosure,
websocket.CloseGoingAway,
websocket.CloseNoStatusReceived,
) {
// Unexpected close code
slog.Warn().Err(err).Msg("Socket error")
} else {
slog.Debug().Msg("Closing socket")
}
break
}
}
}
// WSWriter makes sure the websocket client is still connected
func (ml *msgListenerV2) WSWriter(conn *websocket.Conn) {
ticker := time.NewTicker(pingPeriodV2)
defer func() {
ticker.Stop()
ml.Close()
}()
// Handle messages from hub until msgListener is closed
for {
select {
case event, ok := <-ml.c:
conn.SetWriteDeadline(time.Now().Add(writeWaitV2))
if !ok {
// msgListener closed, exit
conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if conn.WriteJSON(event) != nil {
// Write failed
return
}
case <-ticker.C:
// Send ping
conn.SetWriteDeadline(time.Now().Add(writeWaitV2))
if conn.WriteMessage(websocket.PingMessage, []byte{}) != nil {
// Write error
return
}
log.Debug().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Msg("Sent ping")
}
}
}
// Close removes the listener registration
func (ml *msgListenerV2) Close() {
select {
case <-ml.c:
// Already closed
default:
ml.hub.RemoveListener(ml)
close(ml.c)
}
}
// MonitorAllMessagesV2 is a web handler which upgrades the connection to a websocket and notifies
// the client of all messages received.
func MonitorAllMessagesV2(
w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Upgrade to Websocket.
conn, err := upgraderV2.Upgrade(w, req, nil)
if err != nil {
return err
}
web.ExpWebSocketConnectsCurrent.Add(1)
defer func() {
_ = conn.Close()
web.ExpWebSocketConnectsCurrent.Add(-1)
}()
log.Debug().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket")
// Create, register listener; then interact with conn.
ml := newMsgListenerV2(ctx.MsgHub, "")
go ml.WSWriter(conn)
ml.WSReader(conn)
return nil
}
// MonitorMailboxMessagesV2 is a web handler which upgrades the connection to a websocket and
// notifies the client of messages received by a particular mailbox.
func MonitorMailboxMessagesV2(
w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
if err != nil {
return err
}
// Upgrade to Websocket.
conn, err := upgraderV2.Upgrade(w, req, nil)
if err != nil {
return err
}
web.ExpWebSocketConnectsCurrent.Add(1)
defer func() {
_ = conn.Close()
web.ExpWebSocketConnectsCurrent.Add(-1)
}()
log.Debug().Str("module", "rest").Str("proto", "WebSocket").
Str("remote", conn.RemoteAddr().String()).Msg("Upgraded to WebSocket")
// Create, register listener; then interact with conn.
ml := newMsgListenerV2(ctx.MsgHub, name)
go ml.WSWriter(conn)
ml.WSReader(conn)
return nil
}

View File

@@ -127,7 +127,7 @@ markMessageSeen session msg mailboxName id =
monitorUri : Session -> String monitorUri : Session -> String
monitorUri session = monitorUri session =
apiV1Url session [ "monitor", "messages" ] apiV2Url session [ "monitor", "messages" ]
purgeMailbox : Session -> HttpResult msg -> String -> Cmd msg purgeMailbox : Session -> HttpResult msg -> String -> Cmd msg
@@ -135,14 +135,24 @@ purgeMailbox session msg mailboxName =
HttpUtil.delete msg (apiV1Url session [ "mailbox", mailboxName ]) HttpUtil.delete msg (apiV1Url session [ "mailbox", mailboxName ])
apiV1Url : Session -> List String -> String
apiV1Url =
apiUrl "v1"
apiV2Url : Session -> List String -> String
apiV2Url =
apiUrl "v2"
{-| Builds a public REST API URL (see wiki). {-| Builds a public REST API URL (see wiki).
-} -}
apiV1Url : Session -> List String -> String apiUrl : String -> Session -> List String -> String
apiV1Url session elements = apiUrl version session elements =
Url.Builder.absolute Url.Builder.absolute
(List.concat (List.concat
[ splitBasePath session.config.basePath [ splitBasePath session.config.basePath
, [ "api", "v1" ] , [ "api", version ]
, elements , elements
] ]
) )

View File

@@ -27,7 +27,7 @@ variantDecoder variant =
case variant of case variant of
"message-deleted" -> "message-deleted" ->
succeed MessageDeleted succeed MessageDeleted
|> required "header" messageIdDecoder |> required "identifier" messageIdDecoder
"message-stored" -> "message-stored" ->
succeed MessageStored succeed MessageStored