From a7f19759dfd5c90697a5000f8ae933a0ab22bd95 Mon Sep 17 00:00:00 2001 From: Peter Stuifzand Date: Sat, 15 Sep 2018 10:51:27 +0200 Subject: [PATCH 1/4] Add utf-8 to content-type --- cmd/eksterd/microsub.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/cmd/eksterd/microsub.go b/cmd/eksterd/microsub.go index 689cde3..71e49e3 100644 --- a/cmd/eksterd/microsub.go +++ b/cmd/eksterd/microsub.go @@ -69,6 +69,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + const OutputContentType = "application/json; charset=utf-8" if r.Method == http.MethodGet { w.Header().Add("Access-Control-Allow-Origin", "*") values := r.URL.Query() @@ -80,7 +81,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } jw := json.NewEncoder(w) - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(map[string][]microsub.Channel{ "channels": channels, }) @@ -95,7 +96,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } jw := json.NewEncoder(w) - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) jw.SetIndent("", " ") jw.SetEscapeHTML(false) err = jw.Encode(timeline) @@ -111,7 +112,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } jw := json.NewEncoder(w) jw.SetIndent("", " ") - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(timeline) if err != nil { http.Error(w, err.Error(), 500) @@ -125,7 +126,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } jw := json.NewEncoder(w) - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(map[string][]microsub.Feed{ "items": following, }) @@ -157,7 +158,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) fmt.Fprintln(w, "[]") h.Backend.(Debug).Debug() return @@ -170,7 +171,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(channel) if err != nil { http.Error(w, err.Error(), 500) @@ -182,7 +183,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(channel) if err != nil { http.Error(w, err.Error(), 500) @@ -199,7 +200,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) jw := json.NewEncoder(w) err = jw.Encode(feed) if err != nil { @@ -214,7 +215,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) fmt.Fprintln(w, "[]") } else if action == "search" { query := values.Get("query") @@ -224,7 +225,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } jw := json.NewEncoder(w) - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) err = jw.Encode(map[string][]microsub.Feed{ "results": feeds, }) @@ -264,7 +265,7 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("unknown method in timeline %s\n", method), 500) return } - w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Type", OutputContentType) fmt.Fprintln(w, "[]") } else { http.Error(w, fmt.Sprintf("unknown action %s\n", action), 500) From 46b8f2a3155b2c36ffb78d3020b1ffb3253393b2 Mon Sep 17 00:00:00 2001 From: Peter Stuifzand Date: Sat, 15 Sep 2018 15:52:52 +0200 Subject: [PATCH 2/4] Changes in the README --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 62a9db2..9cda931 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ # ekster -a microsub server - - -## Warning! - -Very alpha: no warranty. +a [Microsub](https://indieweb.org/Microsub) server ## Installing and running ekster @@ -19,7 +14,7 @@ you need a Go environment. Use these commands to install the programs. go get -u p83.nl/go/ekster/cmd/eksterd go get -u p83.nl/go/ekster/cmd/ek -`eksterd` uses [Redis](https://redis.io/) as the database, to temporarily save +`eksterd` uses [Redis](https://redis.io/) as the database to temporarily save the items and feeds. The more permanent information is saved in `backend.json`. #### Running eksterd @@ -162,3 +157,7 @@ Micropub client. `ekster` will check every 10 minutes, if the token is still valid. This could be retrieved automatically, but this doesn't happen at the moment. +## Other Microsub projects + +* +* Aperture: [code](https://github.com/aaronparecki/Aperture), [hosted](https://aperture.p3k.io) From 8607a2715069bb9bcc0ade0fa4794a64f74a6339 Mon Sep 17 00:00:00 2001 From: Peter Stuifzand Date: Sat, 15 Sep 2018 15:53:10 +0200 Subject: [PATCH 3/4] Move the OutputContentType outside --- cmd/eksterd/microsub.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/eksterd/microsub.go b/cmd/eksterd/microsub.go index 71e49e3..d51f5b4 100644 --- a/cmd/eksterd/microsub.go +++ b/cmd/eksterd/microsub.go @@ -29,6 +29,10 @@ import ( "github.com/gomodule/redigo/redis" ) +const ( + OutputContentType = "application/json; charset=utf-8" +) + type microsubHandler struct { Backend microsub.Microsub HubIncomingBackend HubBackend @@ -69,7 +73,6 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - const OutputContentType = "application/json; charset=utf-8" if r.Method == http.MethodGet { w.Header().Add("Access-Control-Allow-Origin", "*") values := r.URL.Query() From ec8da2692cecdc6a17bfae0f9038f84d5965675c Mon Sep 17 00:00:00 2001 From: Peter Stuifzand Date: Sat, 15 Sep 2018 15:55:27 +0200 Subject: [PATCH 4/4] Add missing files --- pkg/auth/types.go | 14 +++ pkg/jf2/tests/test0.json | 1 + pkg/jf2/tests/test1.json | 1 + pkg/jf2/tests/test2.json | 25 ++++ pkg/server/events.go | 51 ++++++++ pkg/server/microsub.go | 258 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 pkg/auth/types.go create mode 100644 pkg/jf2/tests/test0.json create mode 100644 pkg/jf2/tests/test1.json create mode 100644 pkg/jf2/tests/test2.json create mode 100644 pkg/server/events.go create mode 100644 pkg/server/microsub.go diff --git a/pkg/auth/types.go b/pkg/auth/types.go new file mode 100644 index 0000000..20aee69 --- /dev/null +++ b/pkg/auth/types.go @@ -0,0 +1,14 @@ +package auth + +type Auther interface { + AuthTokenAccepted(header string, r *TokenResponse) bool +} + +// TokenResponse is the information that we get back from the token endpoint of the user... +type TokenResponse struct { + Me string `json:"me"` + ClientID string `json:"client_id"` + Scope string `json:"scope"` + IssuedAt int64 `json:"issued_at"` + Nonce int64 `json:"nonce"` +} diff --git a/pkg/jf2/tests/test0.json b/pkg/jf2/tests/test0.json new file mode 100644 index 0000000..21bb27e --- /dev/null +++ b/pkg/jf2/tests/test0.json @@ -0,0 +1 @@ +{"type":["h-entry"],"properties":{"name":["name test"]}} \ No newline at end of file diff --git a/pkg/jf2/tests/test1.json b/pkg/jf2/tests/test1.json new file mode 100644 index 0000000..18d392d --- /dev/null +++ b/pkg/jf2/tests/test1.json @@ -0,0 +1 @@ +{"type":["h-entry"],"properties":{"name":["name test"],"author":[{"type":["h-card"],"properties":{"name":["Peter"]}}]}} \ No newline at end of file diff --git a/pkg/jf2/tests/test2.json b/pkg/jf2/tests/test2.json new file mode 100644 index 0000000..2e64d61 --- /dev/null +++ b/pkg/jf2/tests/test2.json @@ -0,0 +1,25 @@ +{ + "type": [ + "h-entry" + ], + "properties": { + "name": [ + "name test" + ], + "photo": [ + "https://peterstuifzand.nl/img/profile.jpg" + ], + "author": [ + { + "type": [ + "h-card" + ], + "properties": { + "name": [ + "Peter" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/server/events.go b/pkg/server/events.go new file mode 100644 index 0000000..ad846d4 --- /dev/null +++ b/pkg/server/events.go @@ -0,0 +1,51 @@ +package server + +import ( + "encoding/json" + "fmt" + "net" + "time" + + "p83.nl/go/ekster/pkg/microsub" +) + +type Consumer struct { + conn net.Conn + output chan microsub.Message +} + +func newConsumer(conn net.Conn) *Consumer { + cons := &Consumer{conn, make(chan microsub.Message)} + + fmt.Fprint(conn, "HTTP/1.0 200 OK\r\n") + fmt.Fprint(conn, "Content-Type: text/event-stream\r\n") + fmt.Fprint(conn, "Access-Control-Allow-Origin: *\r\n") + fmt.Fprint(conn, "\r\n") + + go func() { + ticker := time.NewTicker(10 * time.Second).C + for { + select { + case <-ticker: + fmt.Fprint(conn, `event: ping`) + fmt.Fprint(conn, "\r\n") + fmt.Fprint(conn, "\r\n") + + case msg := <-cons.output: + fmt.Fprint(conn, `event: message`) + fmt.Fprint(conn, "\r\n") + fmt.Fprint(conn, `data:`) + json.NewEncoder(conn).Encode(msg) + fmt.Fprint(conn, "\r\n") + fmt.Fprint(conn, "\r\n") + } + } + conn.Close() + }() + + return cons +} + +func (cons *Consumer) WriteMessage(evt microsub.Event) { + cons.output <- evt.Msg +} diff --git a/pkg/server/microsub.go b/pkg/server/microsub.go new file mode 100644 index 0000000..9a359fd --- /dev/null +++ b/pkg/server/microsub.go @@ -0,0 +1,258 @@ +/* + ekster - microsub server + Copyright (C) 2018 Peter Stuifzand + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +package server + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "regexp" + + "p83.nl/go/ekster/pkg/microsub" +) + +var ( + entryRegex = regexp.MustCompile("^entry\\[\\d+\\]$") +) + +type microsubHandler struct { + backend microsub.Microsub +} + +func NewMicrosubHandler(backend microsub.Microsub) http.Handler { + return µsubHandler{backend} +} + +func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + log.Printf("%s %s\n", r.Method, r.URL) + log.Println(r.URL.Query()) + log.Println(r.PostForm) + + if r.Method == http.MethodOptions { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST") + w.Header().Add("Access-Control-Allow-Headers", "Authorization, Cache-Control") + return + } + + const ContentType = "application/json" + if r.Method == http.MethodGet { + w.Header().Add("Access-Control-Allow-Origin", "*") + values := r.URL.Query() + action := values.Get("action") + if action == "channels" { + channels, err := h.backend.ChannelsGetList() + if err != nil { + http.Error(w, err.Error(), 500) + return + } + jw := json.NewEncoder(w) + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(map[string][]microsub.Channel{ + "channels": channels, + }) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "timeline" { + timeline, err := h.backend.TimelineGet(values.Get("before"), values.Get("after"), values.Get("channel")) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + jw := json.NewEncoder(w) + w.Header().Add("Content-Type", ContentType) + jw.SetIndent("", " ") + jw.SetEscapeHTML(false) + err = jw.Encode(timeline) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "preview" { + timeline, err := h.backend.PreviewURL(values.Get("url")) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + jw := json.NewEncoder(w) + jw.SetIndent("", " ") + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(timeline) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "follow" { + channel := values.Get("channel") + following, err := h.backend.FollowGetList(channel) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + jw := json.NewEncoder(w) + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(map[string][]microsub.Feed{ + "items": following, + }) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "events" { + conn, _, _ := w.(http.Hijacker).Hijack() + cons := newConsumer(conn) + h.backend.AddEventListener(cons) + } else { + http.Error(w, fmt.Sprintf("unknown action %s\n", action), 500) + return + } + return + } else if r.Method == http.MethodPost { + w.Header().Add("Access-Control-Allow-Origin", "*") + + values := r.URL.Query() + action := values.Get("action") + if action == "channels" { + name := values.Get("name") + method := values.Get("method") + uid := values.Get("channel") + if method == "delete" { + err := h.backend.ChannelsDelete(uid) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Add("Content-Type", ContentType) + fmt.Fprintln(w, "[]") + return + } + + jw := json.NewEncoder(w) + if uid == "" { + channel, err := h.backend.ChannelsCreate(name) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(channel) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else { + channel, err := h.backend.ChannelsUpdate(uid, name) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(channel) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } + } else if action == "follow" { + uid := values.Get("channel") + url := values.Get("url") + // h.HubIncomingBackend.CreateFeed(url, uid) + feed, err := h.backend.FollowURL(uid, url) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Add("Content-Type", ContentType) + jw := json.NewEncoder(w) + err = jw.Encode(feed) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "unfollow" { + uid := values.Get("channel") + url := values.Get("url") + err := h.backend.UnfollowURL(uid, url) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + w.Header().Add("Content-Type", ContentType) + fmt.Fprintln(w, "[]") + } else if action == "search" { + query := values.Get("query") + feeds, err := h.backend.Search(query) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + jw := json.NewEncoder(w) + w.Header().Add("Content-Type", ContentType) + err = jw.Encode(map[string][]microsub.Feed{ + "results": feeds, + }) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } else if action == "timeline" || r.PostForm.Get("action") == "timeline" { + method := values.Get("method") + + if method == "mark_read" || r.PostForm.Get("method") == "mark_read" { + values = r.Form + channel := values.Get("channel") + var markAsRead []string + if uids, e := values["entry"]; e { + markAsRead = uids + } else if uids, e := values["entry[]"]; e { + markAsRead = uids + } else { + uids := []string{} + for k, v := range values { + if entryRegex.MatchString(k) { + uids = append(uids, v...) + } + } + markAsRead = uids + } + + if len(markAsRead) > 0 { + err := h.backend.MarkRead(channel, markAsRead) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + } + } else { + http.Error(w, fmt.Sprintf("unknown method in timeline %s\n", method), 500) + return + } + w.Header().Add("Content-Type", ContentType) + fmt.Fprintln(w, "[]") + } else { + http.Error(w, fmt.Sprintf("unknown action %s\n", action), 500) + } + + return + } + return +}