diff options
Diffstat (limited to 'srv/src/http')
-rw-r--r-- | srv/src/http/api.go | 17 | ||||
-rw-r--r-- | srv/src/http/chat.go | 211 | ||||
-rw-r--r-- | srv/src/http/tpl/chat.html | 251 |
3 files changed, 3 insertions, 476 deletions
diff --git a/srv/src/http/api.go b/srv/src/http/api.go index 44b9170..01cad50 100644 --- a/srv/src/http/api.go +++ b/srv/src/http/api.go @@ -17,7 +17,6 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/mediocregopher/blog.mediocregopher.com/srv/cfg" - "github.com/mediocregopher/blog.mediocregopher.com/srv/chat" "github.com/mediocregopher/blog.mediocregopher.com/srv/http/apiutil" "github.com/mediocregopher/blog.mediocregopher.com/srv/mailinglist" "github.com/mediocregopher/blog.mediocregopher.com/srv/post" @@ -41,9 +40,6 @@ type Params struct { MailingList mailinglist.MailingList - GlobalRoom chat.Room - UserIDCalculator *chat.UserIDCalculator - // PublicURL is the base URL which site visitors can navigate to. PublicURL *url.URL @@ -176,16 +172,9 @@ func (a *api) apiHandler() http.Handler { mux.Handle("/mailinglist/finalize", a.mailingListFinalizeHandler()) mux.Handle("/mailinglist/unsubscribe", a.mailingListUnsubscribeHandler()) - mux.Handle("/chat/global/", http.StripPrefix("/chat/global", newChatHandler( - a.params.GlobalRoom, - a.params.UserIDCalculator, - a.requirePowMiddleware, - ))) - - // disallowGetMiddleware is used rather than a MethodMux because it has an - // exception for websockets, which is needed for chat. - return disallowGetMiddleware(mux) - + return apiutil.MethodMux(map[string]http.Handler{ + "POST": mux, + }) } func (a *api) blogHandler() http.Handler { diff --git a/srv/src/http/chat.go b/srv/src/http/chat.go deleted file mode 100644 index f76e4ad..0000000 --- a/srv/src/http/chat.go +++ /dev/null @@ -1,211 +0,0 @@ -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 - } - } - }) -} diff --git a/srv/src/http/tpl/chat.html b/srv/src/http/tpl/chat.html deleted file mode 100644 index b2038e2..0000000 --- a/srv/src/http/tpl/chat.html +++ /dev/null @@ -1,251 +0,0 @@ -{{ define "body" }} - -<script async type="module" src="/assets/api.js"></script> - -<style> - #messages { - max-height: 65vh; - overflow: auto; - padding-right: 2rem; - } - - #messages .message { - border: 1px solid #AAA; - border-radius: 10px; - margin-bottom: 1rem; - padding: 2rem; - overflow: auto; - } - - #messages .message .title { - font-weight: bold; - font-size: 120%; - } - - #messages .message .secondaryTitle { - font-family: monospace; - color: #CCC; - } - - #messages .message p { - font-family: monospace; - margin: 1rem 0 0 0; - } - -</style> - -<div id="messages"></div> - -<span id="fail" style="color: red;"></span> - -<script> - -const messagesEl = document.getElementById("messages"); - -let messagesScrolledToBottom = true; -messagesEl.onscroll = () => { - const el = messagesEl; - messagesScrolledToBottom = el.scrollHeight == el.scrollTop + el.clientHeight; -}; - -function renderMessages(msgs) { - - messagesEl.innerHTML = ''; - - msgs.forEach((msg) => { - const el = document.createElement("div"); - el.className = "row message" - - const elWithTextContents = (tag, body) => { - const el = document.createElement(tag); - el.appendChild(document.createTextNode(body)); - return el; - }; - - const titleEl = document.createElement("div"); - titleEl.className = "title"; - el.appendChild(titleEl); - - const userNameEl = elWithTextContents("span", msg.userID.name); - titleEl.appendChild(userNameEl); - - const secondaryTitleEl = document.createElement("div"); - secondaryTitleEl.className = "secondaryTitle"; - el.appendChild(secondaryTitleEl); - - const dt = new Date(msg.createdAt*1000); - const dtStr - = `${dt.getFullYear()}-${dt.getMonth()+1}-${dt.getDate()}` - + ` ${dt.getHours()}:${dt.getMinutes()}:${dt.getSeconds()}`; - - const userIDEl = elWithTextContents("span", `userID:${msg.userID.id} @ ${dtStr}`); - secondaryTitleEl.appendChild(userIDEl); - - const bodyEl = document.createElement("p"); - - const bodyParts = msg.body.split("\n"); - for (const i in bodyParts) { - if (i > 0) bodyEl.appendChild(document.createElement("br")); - bodyEl.appendChild(document.createTextNode(bodyParts[i])); - } - - el.appendChild(bodyEl); - - messagesEl.appendChild(el); - }); -} - - -(async () => { - - const failEl = document.getElementById("fail"); - setErr = (msg) => failEl.innerHTML = `${msg} (please refresh the page to retry)`; - - try { - - const api = await import("/assets/api.js"); - - const history = await api.call("/api/chat/global/history"); - const msgs = history.messages; - - // history returns msgs in time descending, but we display them in time - // ascending. - msgs.reverse() - - const sinceID = (msgs.length > 0) ? msgs[msgs.length-1].id : ""; - - const ws = await api.ws("/api/chat/global/listen", { - params: { sinceID }, - }); - - while (true) { - renderMessages(msgs); - - // If the user was previously scrolled to the bottom then keep them - // there. - if (messagesScrolledToBottom) { - messagesEl.scrollTop = messagesEl.scrollHeight; - } - - const msg = await ws.next(); - msgs.push(msg.message); - renderMessages(msgs); - } - - - } catch (e) { - e = `Failed to fetch message history: ${e}` - setErr(e); - console.error(e); - return; - } - -})() - -</script> - -<style> -#append { - border: 1px dashed #AAA; - border-radius: 10px; - padding: 2rem; -} - -#append #appendBody { - font-family: monospace; -} - -#append #appendStatus { - color: red; -} - -</style> - -<form id="append"> - <h5>New Message</h5> - <div class="row"> - <div class="columns four"> - <input class="u-full-width" placeholder="Name" id="appendName" type="text" /> - <input class="u-full-width" placeholder="Secret" id="appendSecret" type="password" /> - </div> - <div class="columns eight"> - <p> - Your name is displayed alongside your message. - - Your name+secret is used to generate your userID, which is also - displayed alongside your message. - - Other users can validate two messages are from the same person - by comparing the messages' userID. - </p> - </div> - </div> - <div class="row"> - <div class="columns twelve"> - <textarea - style="font-family: monospace" - id="appendBody" - class="u-full-width" - placeholder="Well thought out statement goes here..." - ></textarea> - </div> - </div> - <div class="row"> - <div class="columns four"> - <input class="u-full-width button-primary" id="appendSubmit" type="button" value="Submit" /> - </div> - </div> - <span id="appendStatus"></span> -</form> - -<script> - -const append = document.getElementById("append"); -const appendName = document.getElementById("appendName"); -const appendSecret = document.getElementById("appendSecret"); -const appendBody = document.getElementById("appendBody"); -const appendSubmit = document.getElementById("appendSubmit"); -const appendStatus = document.getElementById("appendStatus"); - -appendSubmit.onclick = async () => { - - const appendSubmitOrigValue = appendSubmit.value; - - appendSubmit.disabled = true; - appendSubmit.className = ""; - appendSubmit.value = "Please hold..."; - - appendStatus.innerHTML = ''; - - try { - - const api = await import("/assets/api.js"); - - await api.call('/api/chat/global/append', { - body: { - name: appendName.value, - password: appendSecret.value, - body: appendBody.value, - }, - requiresPow: true, - }); - - appendBody.value = ''; - - } catch (e) { - - appendStatus.innerHTML = e; - - } finally { - appendSubmit.disabled = false; - appendSubmit.className = "button-primary"; - appendSubmit.value = appendSubmitOrigValue; - } -}; - -</script> - -{{ end }} - -{{ template "base.html" . }} - |