Compare commits

..

No commits in common. "15d7c69c30168d671d318957d78dde11a59c8105" and "1e00d32aedb8c41cd5141eaa6ec6122f99dedbe5" have entirely different histories.

9 changed files with 71 additions and 170 deletions

View File

@ -4,11 +4,3 @@
- Increase ease of use for people who want to try Ekster - Increase ease of use for people who want to try Ekster
- Hosted version?? - Hosted version??
- Per user backends - Per user backends
#### Code
- Remove dependency between hubIncomingHandler and memoryBackend
- Improve error handling

View File

@ -1,4 +1,20 @@
// Ek is a microsub client. /*
ek - microsub client
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 <http://www.gnu.org/licenses/>.
*/
package main package main
import ( import (
@ -33,10 +49,8 @@ type Export struct {
Feeds map[string][]ExportFeed `json:"feeds,omitempty"` Feeds map[string][]ExportFeed `json:"feeds,omitempty"`
} }
// ExportFeed is a feed.
type ExportFeed string type ExportFeed string
// ExportChannel contains the channel information for exports.
type ExportChannel struct { type ExportChannel struct {
UID string `json:"uid,omitempty"` UID string `json:"uid,omitempty"`
Name string `json:"channel,omitempty"` Name string `json:"channel,omitempty"`
@ -345,9 +359,9 @@ func performCommands(sub microsub.Microsub, commands []string) {
filetype := commands[1] filetype := commands[1]
if filetype == "opml" { if filetype == "opml" {
exportOPMLFromMicrosub(sub) exportOpmlFromMicrosub(sub)
} else if filetype == "json" { } else if filetype == "json" {
exportJSONFromMicrosub(sub) exportJsonFromMicrosub(sub)
} else { } else {
log.Fatalf("unsupported filetype %q", filetype) log.Fatalf("unsupported filetype %q", filetype)
} }
@ -358,9 +372,9 @@ func performCommands(sub microsub.Microsub, commands []string) {
filename := commands[2] filename := commands[2]
if filetype == "opml" { if filetype == "opml" {
importOPMLIntoMicrosub(sub, filename) importOpmlIntoMicrosub(sub, filename)
} else if filetype == "json" { } else if filetype == "json" {
importJSONIntoMicrosub(sub, filename) importJsonIntoMicrosub(sub, filename)
} else { } else {
log.Fatalf("unsupported filetype %q", filetype) log.Fatalf("unsupported filetype %q", filetype)
} }
@ -369,19 +383,9 @@ func performCommands(sub microsub.Microsub, commands []string) {
if len(commands) == 1 && commands[0] == "version" { if len(commands) == 1 && commands[0] == "version" {
fmt.Printf("ek %s\n", Version) fmt.Printf("ek %s\n", Version)
} }
if len(commands) == 1 && commands[0] == "events" {
c, err := sub.Events()
if err != nil {
log.Fatalf("could not start event listener: %+v", err)
}
for msg := range c {
log.Printf("%s: %s", msg.Event, msg.Data)
}
}
} }
func exportOPMLFromMicrosub(sub microsub.Microsub) { func exportOpmlFromMicrosub(sub microsub.Microsub) {
output := opml.OPML{} output := opml.OPML{}
output.Head.Title = "Microsub channels and feeds" output.Head.Title = "Microsub channels and feeds"
output.Head.DateCreated = time.Now().Format(time.RFC3339) output.Head.DateCreated = time.Now().Format(time.RFC3339)
@ -420,7 +424,7 @@ func exportOPMLFromMicrosub(sub microsub.Microsub) {
os.Stdout.WriteString(xml) os.Stdout.WriteString(xml)
} }
func exportJSONFromMicrosub(sub microsub.Microsub) { func exportJsonFromMicrosub(sub microsub.Microsub) {
contents := Export{Version: "1.0", Generator: "ek version " + Version} contents := Export{Version: "1.0", Generator: "ek version " + Version}
channels, err := sub.ChannelsGetList() channels, err := sub.ChannelsGetList()
if err != nil { if err != nil {
@ -445,7 +449,7 @@ func exportJSONFromMicrosub(sub microsub.Microsub) {
} }
} }
func importJSONIntoMicrosub(sub microsub.Microsub, filename string) { func importJsonIntoMicrosub(sub microsub.Microsub, filename string) {
var export Export var export Export
f, err := os.Open(filename) f, err := os.Open(filename)
if err != nil { if err != nil {
@ -505,7 +509,7 @@ func importJSONIntoMicrosub(sub microsub.Microsub, filename string) {
} }
} }
func importOPMLIntoMicrosub(sub microsub.Microsub, filename string) { func importOpmlIntoMicrosub(sub microsub.Microsub, filename string) {
channelMap := make(map[string]microsub.Channel) channelMap := make(map[string]microsub.Channel)
channels, err := sub.ChannelsGetList() channels, err := sub.ChannelsGetList()
if err != nil { if err != nil {

View File

@ -260,7 +260,9 @@ func getAppInfo(clientID string) (app, error) {
} }
func (h *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
conn := h.pool.Get() pool := h.pool
conn := pool.Get()
defer conn.Close() defer conn.Close()
err := r.ParseForm() err := r.ParseForm()

View File

@ -515,10 +515,6 @@ func (b *memoryBackend) MarkRead(channel string, uids []string) error {
return nil return nil
} }
func (b *memoryBackend) Events() (chan sse.Message, error) {
return sse.StartConnection(b.broker)
}
func (b *memoryBackend) ProcessContent(channel, fetchURL, contentType string, body io.Reader) error { func (b *memoryBackend) ProcessContent(channel, fetchURL, contentType string, body io.Reader) error {
cachingFetch := WithCaching(b.pool, Fetch2) cachingFetch := WithCaching(b.pool, Fetch2)

View File

@ -10,9 +10,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/pkg/errors"
"p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/microsub"
"p83.nl/go/ekster/pkg/sse"
) )
// Client is a HTTP client for Microsub // Client is a HTTP client for Microsub
@ -203,7 +201,6 @@ func (c *Client) PreviewURL(url string) (microsub.Timeline, error) {
return timeline, nil return timeline, nil
} }
// FollowGetList gets the list of followed feeds.
func (c *Client) FollowGetList(channel string) ([]microsub.Feed, error) { func (c *Client) FollowGetList(channel string) ([]microsub.Feed, error) {
args := make(map[string]string) args := make(map[string]string)
args["channel"] = channel args["channel"] = channel
@ -231,7 +228,6 @@ func (c *Client) FollowGetList(channel string) ([]microsub.Feed, error) {
return response.Items, nil return response.Items, nil
} }
// ChannelsCreate creates and new channel on a microsub server.
func (c *Client) ChannelsCreate(name string) (microsub.Channel, error) { func (c *Client) ChannelsCreate(name string) (microsub.Channel, error) {
args := make(map[string]string) args := make(map[string]string)
args["name"] = name args["name"] = name
@ -249,7 +245,6 @@ func (c *Client) ChannelsCreate(name string) (microsub.Channel, error) {
return channel, nil return channel, nil
} }
// ChannelsUpdate updates a channel.
func (c *Client) ChannelsUpdate(uid, name string) (microsub.Channel, error) { func (c *Client) ChannelsUpdate(uid, name string) (microsub.Channel, error) {
args := make(map[string]string) args := make(map[string]string)
args["name"] = name args["name"] = name
@ -268,7 +263,6 @@ func (c *Client) ChannelsUpdate(uid, name string) (microsub.Channel, error) {
return channel, nil return channel, nil
} }
// ChannelsDelete deletes a channel.
func (c *Client) ChannelsDelete(uid string) error { func (c *Client) ChannelsDelete(uid string) error {
args := make(map[string]string) args := make(map[string]string)
args["channel"] = uid args["channel"] = uid
@ -281,7 +275,6 @@ func (c *Client) ChannelsDelete(uid string) error {
return nil return nil
} }
// FollowURL follows a url.
func (c *Client) FollowURL(channel, url string) (microsub.Feed, error) { func (c *Client) FollowURL(channel, url string) (microsub.Feed, error) {
args := make(map[string]string) args := make(map[string]string)
args["channel"] = channel args["channel"] = channel
@ -300,7 +293,6 @@ func (c *Client) FollowURL(channel, url string) (microsub.Feed, error) {
return feed, nil return feed, nil
} }
// UnfollowURL unfollows a url in a channel.
func (c *Client) UnfollowURL(channel, url string) error { func (c *Client) UnfollowURL(channel, url string) error {
args := make(map[string]string) args := make(map[string]string)
args["channel"] = channel args["channel"] = channel
@ -313,7 +305,6 @@ func (c *Client) UnfollowURL(channel, url string) error {
return nil return nil
} }
// Search asks the server to search for the query.
func (c *Client) Search(query string) ([]microsub.Feed, error) { func (c *Client) Search(query string) ([]microsub.Feed, error) {
args := make(map[string]string) args := make(map[string]string)
args["query"] = query args["query"] = query
@ -334,7 +325,6 @@ func (c *Client) Search(query string) ([]microsub.Feed, error) {
return response.Results, nil return response.Results, nil
} }
// MarkRead marks an item read on the server.
func (c *Client) MarkRead(channel string, uids []string) error { func (c *Client) MarkRead(channel string, uids []string) error {
args := make(map[string]string) args := make(map[string]string)
args["channel"] = channel args["channel"] = channel
@ -352,17 +342,3 @@ func (c *Client) MarkRead(channel string, uids []string) error {
res.Body.Close() res.Body.Close()
return nil return nil
} }
// Events open an event channel to the server.
func (c *Client) Events() (chan sse.Message, error) {
res, err := c.microsubGetRequest("events", nil)
if err != nil {
return nil, err
}
ch, err := sse.Reader(res.Body)
if err != nil {
return nil, errors.Wrap(err, "could not create reader")
}
return ch, nil
}

View File

@ -4,8 +4,6 @@ package microsub
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"p83.nl/go/ekster/pkg/sse"
) )
/* /*
@ -39,7 +37,6 @@ type Channel struct {
Unread Unread `json:"unread,omitempty"` Unread Unread `json:"unread,omitempty"`
} }
// Card contains the fields of an author or location.
type Card struct { type Card struct {
// Filled bool `json:"filled,omitempty"` // Filled bool `json:"filled,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
@ -53,7 +50,6 @@ type Card struct {
Latitude string `json:"latitude,omitempty" mf2:"latitude"` Latitude string `json:"latitude,omitempty" mf2:"latitude"`
} }
// Content contains the Text or HTML content of an Item.
type Content struct { type Content struct {
Text string `json:"text,omitempty" mf2:"value"` Text string `json:"text,omitempty" mf2:"value"`
HTML string `json:"html,omitempty" mf2:"html"` HTML string `json:"html,omitempty" mf2:"html"`
@ -96,7 +92,6 @@ type Timeline struct {
Paging Pagination `json:"paging"` Paging Pagination `json:"paging"`
} }
// Feed is one microsub feed.
type Feed struct { type Feed struct {
Type string `json:"type"` Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
@ -106,6 +101,16 @@ type Feed struct {
Author Card `json:"author,omitempty"` Author Card `json:"author,omitempty"`
} }
type Message string
type Event struct {
Msg Message
}
type EventListener interface {
WriteMessage(evt Event)
}
// Microsub is the main protocol that should be implemented by a backend // Microsub is the main protocol that should be implemented by a backend
type Microsub interface { type Microsub interface {
ChannelsGetList() ([]Channel, error) ChannelsGetList() ([]Channel, error)
@ -124,11 +129,8 @@ type Microsub interface {
Search(query string) ([]Feed, error) Search(query string) ([]Feed, error)
PreviewURL(url string) (Timeline, error) PreviewURL(url string) (Timeline, error)
Events() (chan sse.Message, error)
} }
// MarshalJSON encodes an Unread value as JSON
func (unread Unread) MarshalJSON() ([]byte, error) { func (unread Unread) MarshalJSON() ([]byte, error) {
switch unread.Type { switch unread.Type {
case UnreadBool: case UnreadBool:
@ -139,7 +141,6 @@ func (unread Unread) MarshalJSON() ([]byte, error) {
return json.Marshal(nil) return json.Marshal(nil)
} }
// UnmarshalJSON decodes an Unread value from JSON
func (unread *Unread) UnmarshalJSON(bytes []byte) error { func (unread *Unread) UnmarshalJSON(bytes []byte) error {
var b bool var b bool
err := json.Unmarshal(bytes, &b) err := json.Unmarshal(bytes, &b)
@ -160,7 +161,6 @@ func (unread *Unread) UnmarshalJSON(bytes []byte) error {
return fmt.Errorf("can't unmarshal as bool or int") return fmt.Errorf("can't unmarshal as bool or int")
} }
// String returns a string of the unread value
func (unread Unread) String() string { func (unread Unread) String() string {
switch unread.Type { switch unread.Type {
case UnreadBool: case UnreadBool:
@ -171,7 +171,6 @@ func (unread Unread) String() string {
return "" return ""
} }
// HasUnread return true of there are unread items.
func (unread *Unread) HasUnread() bool { func (unread *Unread) HasUnread() bool {
switch unread.Type { switch unread.Type {
case UnreadBool: case UnreadBool:

View File

@ -7,7 +7,6 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"regexp" "regexp"
@ -100,30 +99,10 @@ func (h *microsubHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"items": following, "items": following,
}) })
} else if action == "events" { } else if action == "events" {
events, err := h.backend.Events() err := sse.StartConnection(h.Broker, w)
if err != nil { if err != nil {
http.Error(w, "could not start sse connection", 500) http.Error(w, "could not start sse connection", 500)
} }
// Remove this client from the map of connected clients
// when this handler exits.
defer func() {
h.Broker.CloseClient(events)
}()
// Listen to connection close and un-register messageChan
notify := w.(http.CloseNotifier).CloseNotify()
go func() {
<-notify
h.Broker.CloseClient(events)
}()
err = sse.WriteMessages(w, events)
if err != nil {
log.Println(err)
http.Error(w, "internal server error", 500)
}
} else { } else {
http.Error(w, fmt.Sprintf("unknown action %s\n", action), 400) http.Error(w, fmt.Sprintf("unknown action %s\n", action), 400)
return return

View File

@ -2,7 +2,6 @@ package server
import ( import (
"p83.nl/go/ekster/pkg/microsub" "p83.nl/go/ekster/pkg/microsub"
"p83.nl/go/ekster/pkg/sse"
) )
// NullBackend is the simplest possible backend // NullBackend is the simplest possible backend
@ -82,10 +81,3 @@ func (b *NullBackend) PreviewURL(url string) (microsub.Timeline, error) {
func (b *NullBackend) MarkRead(channel string, uids []string) error { func (b *NullBackend) MarkRead(channel string, uids []string) error {
return nil return nil
} }
// Events returns a closed channel.
func (b *NullBackend) Events() (chan sse.Message, error) {
ch := make(chan sse.Message)
close(ch)
return ch, nil
}

View File

@ -1,15 +1,10 @@
package sse package sse
import ( import (
"bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"strings"
"github.com/pkg/errors"
) )
// A MessageChan is a channel of channels // A MessageChan is a channel of channels
@ -21,7 +16,6 @@ type MessageChan chan Message
// Message is a message. // Message is a message.
type Message struct { type Message struct {
Event string Event string
Data string
Object interface{} Object interface{}
} }
@ -83,24 +77,8 @@ func NewBroker() (broker *Broker) {
return return
} }
// CloseClient closes the client channel
func (broker *Broker) CloseClient(ch MessageChan) {
broker.closingClients <- ch
}
// StartConnection starts a SSE connection, based on an existing HTTP connection. // StartConnection starts a SSE connection, based on an existing HTTP connection.
func StartConnection(broker *Broker) (MessageChan, error) { func StartConnection(broker *Broker, w http.ResponseWriter) error {
// Each connection registers its own message channel with the Broker's connections registry
messageChan := make(MessageChan)
// Signal the broker that we have a new connection
broker.newClients <- messageChan
return messageChan, nil
}
// WriteMessages writes SSE formatted messages to the writer
func WriteMessages(w http.ResponseWriter, messageChan chan Message) error {
// Make sure that the writer supports flushing. // Make sure that the writer supports flushing.
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
@ -124,56 +102,39 @@ func WriteMessages(w http.ResponseWriter, messageChan chan Message) error {
flusher.Flush() flusher.Flush()
// block waiting or messages broadcast on this connection's messageChan // Each connection registers its own message channel with the Broker's connections registry
for message := range messageChan { messageChan := make(MessageChan)
// Write to the ResponseWriter, Server Sent Events compatible
output, err := json.Marshal(message.Object)
if err != nil {
return errors.Wrap(err, "could not marshal message data")
}
_, err = fmt.Fprintf(w, "event: %s\r\n", message.Event) // Signal the broker that we have a new connection
if err != nil { broker.newClients <- messageChan
return errors.Wrap(err, "could not write message header")
}
_, err = fmt.Fprintf(w, "data: %s\r\n\r\n", output) // Remove this client from the map of connected clients
if err != nil { // when this handler exits.
return errors.Wrap(err, "could not write message data") defer func() {
} broker.closingClients <- messageChan
flusher.Flush()
}
return nil
}
// Reader returns a channel that contains parsed SSE messages.
func Reader(body io.ReadCloser) (MessageChan, error) {
ch := make(MessageChan)
r := bufio.NewScanner(body)
var msg Message
go func() {
for r.Scan() {
line := r.Text()
if line == "" {
ch <- msg
msg = Message{}
continue
}
if strings.HasPrefix(line, "event: ") {
line = line[len("event: "):]
msg.Event = line
}
if strings.HasPrefix(line, "data: ") {
line = line[len("data: "):]
msg.Data = line
}
}
if err := r.Err(); err != nil {
log.Printf("could not scanner lines from sse events: %+v", err)
}
}() }()
return ch, nil // Listen to connection close and un-register messageChan
notify := w.(http.CloseNotifier).CloseNotify()
go func() {
<-notify
broker.closingClients <- messageChan
}()
// block waiting or messages broadcast on this connection's messageChan
for {
// Write to the ResponseWriter, Server Sent Events compatible
message := <-messageChan
output, err := json.Marshal(message.Object)
if err != nil {
log.Println(err)
continue
}
fmt.Fprintf(w, "event: %s\n", message.Event)
fmt.Fprintf(w, "data: %s\n\n", output)
// Flush the data immediately instead of buffering it for later.
flusher.Flush()
}
} }