summaryrefslogtreecommitdiff
path: root/srv/src/http/chat.go
diff options
context:
space:
mode:
Diffstat (limited to 'srv/src/http/chat.go')
-rw-r--r--srv/src/http/chat.go211
1 files changed, 211 insertions, 0 deletions
diff --git a/srv/src/http/chat.go b/srv/src/http/chat.go
new file mode 100644
index 0000000..f76e4ad
--- /dev/null
+++ b/srv/src/http/chat.go
@@ -0,0 +1,211 @@
+package http
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "unicode"
+
+ "github.com/gorilla/websocket"
+ "github.com/mediocregopher/blog.mediocregopher.com/srv/chat"
+ "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil"
+)
+
+type chatHandler struct {
+ *http.ServeMux
+
+ room chat.Room
+ userIDCalc *chat.UserIDCalculator
+
+ wsUpgrader websocket.Upgrader
+}
+
+func newChatHandler(
+ room chat.Room, userIDCalc *chat.UserIDCalculator,
+ requirePowMiddleware func(http.Handler) http.Handler,
+) http.Handler {
+ c := &chatHandler{
+ ServeMux: http.NewServeMux(),
+ room: room,
+ userIDCalc: userIDCalc,
+
+ wsUpgrader: websocket.Upgrader{},
+ }
+
+ c.Handle("/history", c.historyHandler())
+ c.Handle("/user-id", requirePowMiddleware(c.userIDHandler()))
+ c.Handle("/append", requirePowMiddleware(c.appendHandler()))
+ c.Handle("/listen", c.listenHandler())
+
+ return c
+}
+
+func (c *chatHandler) historyHandler() http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ limit, err := apiutil.StrToInt(r.PostFormValue("limit"), 0)
+ if err != nil {
+ apiutil.BadRequest(rw, r, fmt.Errorf("invalid limit parameter: %w", err))
+ return
+ }
+
+ cursor := r.PostFormValue("cursor")
+
+ cursor, msgs, err := c.room.History(r.Context(), chat.HistoryOpts{
+ Limit: limit,
+ Cursor: cursor,
+ })
+
+ if argErr := (chat.ErrInvalidArg{}); errors.As(err, &argErr) {
+ apiutil.BadRequest(rw, r, argErr.Err)
+ return
+ } else if err != nil {
+ apiutil.InternalServerError(rw, r, err)
+ }
+
+ apiutil.JSONResult(rw, r, struct {
+ Cursor string `json:"cursor"`
+ Messages []chat.Message `json:"messages"`
+ }{
+ Cursor: cursor,
+ Messages: msgs,
+ })
+ })
+}
+
+func (c *chatHandler) userID(r *http.Request) (chat.UserID, error) {
+ name := r.PostFormValue("name")
+ if l := len(name); l == 0 {
+ return chat.UserID{}, errors.New("name is required")
+ } else if l > 16 {
+ return chat.UserID{}, errors.New("name too long")
+ }
+
+ nameClean := strings.Map(func(r rune) rune {
+ if !unicode.IsPrint(r) {
+ return -1
+ }
+ return r
+ }, name)
+
+ if nameClean != name {
+ return chat.UserID{}, errors.New("name contains invalid characters")
+ }
+
+ password := r.PostFormValue("password")
+ if l := len(password); l == 0 {
+ return chat.UserID{}, errors.New("password is required")
+ } else if l > 128 {
+ return chat.UserID{}, errors.New("password too long")
+ }
+
+ return c.userIDCalc.Calculate(name, password), nil
+}
+
+func (c *chatHandler) userIDHandler() http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ userID, err := c.userID(r)
+ if err != nil {
+ apiutil.BadRequest(rw, r, err)
+ return
+ }
+
+ apiutil.JSONResult(rw, r, struct {
+ UserID chat.UserID `json:"userID"`
+ }{
+ UserID: userID,
+ })
+ })
+}
+
+func (c *chatHandler) appendHandler() http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ userID, err := c.userID(r)
+ if err != nil {
+ apiutil.BadRequest(rw, r, err)
+ return
+ }
+
+ body := r.PostFormValue("body")
+
+ if l := len(body); l == 0 {
+ apiutil.BadRequest(rw, r, errors.New("body is required"))
+ return
+
+ } else if l > 300 {
+ apiutil.BadRequest(rw, r, errors.New("body too long"))
+ return
+ }
+
+ msg, err := c.room.Append(r.Context(), chat.Message{
+ UserID: userID,
+ Body: body,
+ })
+
+ if err != nil {
+ apiutil.InternalServerError(rw, r, err)
+ return
+ }
+
+ apiutil.JSONResult(rw, r, struct {
+ MessageID string `json:"messageID"`
+ }{
+ MessageID: msg.ID,
+ })
+ })
+}
+
+func (c *chatHandler) listenHandler() http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+
+ ctx := r.Context()
+ sinceID := r.FormValue("sinceID")
+
+ conn, err := c.wsUpgrader.Upgrade(rw, r, nil)
+ if err != nil {
+ apiutil.BadRequest(rw, r, err)
+ return
+ }
+ defer conn.Close()
+
+ it, err := c.room.Listen(ctx, sinceID)
+
+ if errors.As(err, new(chat.ErrInvalidArg)) {
+ apiutil.BadRequest(rw, r, err)
+ return
+
+ } else if errors.Is(err, context.Canceled) {
+ return
+
+ } else if err != nil {
+ apiutil.InternalServerError(rw, r, err)
+ return
+ }
+
+ defer it.Close()
+
+ for {
+
+ msg, err := it.Next(ctx)
+ if errors.Is(err, context.Canceled) {
+ return
+
+ } else if err != nil {
+ apiutil.InternalServerError(rw, r, err)
+ return
+ }
+
+ err = conn.WriteJSON(struct {
+ Message chat.Message `json:"message"`
+ }{
+ Message: msg,
+ })
+
+ if err != nil {
+ apiutil.GetRequestLogger(r).Error(ctx, "couldn't write message", err)
+ return
+ }
+ }
+ })
+}