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 +}