diff options
author | Brian Picciano <mediocregopher@gmail.com> | 2022-05-20 11:17:31 -0600 |
---|---|---|
committer | Brian Picciano <mediocregopher@gmail.com> | 2022-05-20 11:17:31 -0600 |
commit | 09acb111a2b22f5794541fac175b024dd0f9100e (patch) | |
tree | 11d4578a42ad4aea968b42a2689f64c799f9176e /srv/src/http/chat.go | |
parent | f69ed83de73bbfc4b7af0931de6ced8cf12dea61 (diff) |
Rename api package to http
Diffstat (limited to 'srv/src/http/chat.go')
-rw-r--r-- | srv/src/http/chat.go | 211 |
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 + } + } + }) +} |