commit 52356a003621e9c4fc721d2d21c2013e0a0d6e40 Author: Neale Pickett Date: Sat Nov 23 16:46:29 2019 -0800 It works. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d365ad3 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +μChat +===== + +It's an itty-bitty web chat, +for people who aren't trying to screw each other over. + +It doesn't do a lot of checks, announcements, etc. +because it presumes nobody will be using it who is trying to make life hard on anyone else. + +I wrote this at Cyber Fire Foundry 15 +because we needed a minimal chat client, +but the only machine we had to run it on was our router. diff --git a/microchat.go b/microchat.go new file mode 100644 index 0000000..01405f2 --- /dev/null +++ b/microchat.go @@ -0,0 +1,105 @@ +package main + +import ( + "net/http" + "log" + "time" + "strconv" + "encoding/json" +) + +type Message struct { + When int64 + Who string + Text string +} + +type Forum struct { + Name string + Log []Message +} + +var foraByName map[string]*Forum + +func newForum(name string) *Forum { + f := &Forum{ + Name: name, + } + foraByName[name] = f + return f +} + +func sayHandler(w http.ResponseWriter, r *http.Request) { + message := Message{ + When: time.Now().Unix(), + Who: r.FormValue("who"), + Text: r.FormValue("text"), + } + log.Println(message) + forumName := r.FormValue("forum") + forum, ok := foraByName[forumName] + if !ok { + forum = newForum(forumName) + } + forum.Log = append(forum.Log, message) + + w.WriteHeader(http.StatusOK) +} + +func readHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + forumName := r.FormValue("forum") + entriesStr := r.FormValue("entries") + + if entriesStr == "" { + entriesStr = "0" + } + entries, err := strconv.Atoi(entriesStr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if entries <= 0 { + entries = 20 + } else if entries > 500 { + entries = 500 + } + + forum, ok := foraByName[forumName] + if !ok { + http.NotFound(w, r) + return + } + + pos := len(forum.Log) - entries + if pos < 0 { + pos = 0 + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + e := json.NewEncoder(w) + e.Encode(forum.Log[pos:]) +} + +func main() { + http.HandleFunc("/say", sayHandler) + http.HandleFunc("/read", readHandler) + http.Handle("/", http.FileServer(http.Dir("static/"))) + + foraByName = map[string]*Forum{} + f := newForum("") + f.Log = []Message{ + { + When: time.Now().Unix(), + Who: "(system)", + Text: "Welcome to μChat", + }, + } + + bind := ":8080" + log.Printf("Listening on %s", bind) + log.Fatal(http.ListenAndServe(bind, nil)) +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..d1f4cd7 --- /dev/null +++ b/static/index.html @@ -0,0 +1,19 @@ + + + + μChat + + + + +

μChat

+
+
+ + + + +
+
+ + \ No newline at end of file diff --git a/static/microchat.css b/static/microchat.css new file mode 100644 index 0000000..dbd811e --- /dev/null +++ b/static/microchat.css @@ -0,0 +1,27 @@ +body { + font-family: Roboto, sans-serif; +} + +#chatlog { + height: 15em; + max-width: 80em; + overflow: scroll; +} + +input[name="who"] { + width: 6em; +} + +.when { + color: #888; + min-width: 10em; + font-size: small; +} + +.who { + margin: auto 0.8em; + color: blue; + min-width: 5em; + display: inline-block; + text-align: right; +} diff --git a/static/microchat.js b/static/microchat.js new file mode 100644 index 0000000..e3c7609 --- /dev/null +++ b/static/microchat.js @@ -0,0 +1,106 @@ +// jshint asi:true + +function μchatInit(initEvent) { + let updateInterval + let lastMsg + + function toast(message, timeout=5000) { + let p = document.createElement("p") + + p.innerText = message + document.querySelector("#chattoast").appendChild(p) + setTimeout(e => { p.remove() }, timeout) + } + + function input(event) { + event.preventDefault() + + let form = event.target + let inp = form.elements.text + let body = new FormData(form) + console.log(form, body) + fetch("say", { + method: "POST", + body: body, + }) + .then(resp => { + if (resp.ok) { + // Yay it was okay, reset input + inp.value = "" + } else { + toast("ERROR: DOES NOT COMPUTE") + console.log(resp) + } + }) + .catch(err => { + toast("ERROR: DOES NOT COMPUTE") + console.log(err) + }) + + window.localStorage["who"] = form.elements.who.value + } + + function updateLog(log) { + let lastLogMsg = log[log.length - 1] + if ( + lastMsg && + (lastLogMsg.When == lastMsg.When) && + (lastLogMsg.Who == lastMsg.Who) && + (lastLogMsg.Text == lastMsg.Text) + ) { + return + } + lastMsg = lastLogMsg + + let chatlog = document.querySelector("#chatlog") + while (chatlog.firstChild) { + chatlog.firstChild.remove() + } + for (let ll of log) { + let line = chatlog.appendChild(document.createElement("div")) + line.classList.add("line") + + let when = line.appendChild(document.createElement("span")) + when.classList.add("when") + when.innerText = (new Date(ll.When * 1000)).toISOString() + + let who = line.appendChild(document.createElement("span")) + who.classList.add("who") + who.innerText = ll.Who + + let text = line.appendChild(document.createElement("span")) + text.classList.add("text") + text.innerText = ll.Text + } + chatlog.lastChild.scrollIntoView() + + // trigger a fetch of chat log so the user gets some feedback + update() + } + + function update(event) { + fetch("read") + .then(resp => { + if (resp.ok) { + resp.json() + .then(updateLog) + } + }) + } + + for (let f of document.forms) { + let who = window.localStorage["who"] + f.addEventListener("submit", input) + if (who) { + f.elements.who.value = who + } + } + updateInterval = setInterval(update, 3000) + update() +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", μchatInit) +} else { + μchatInit() +}