diff --git a/Taskfile.Development.yaml b/Taskfile.Development.yaml index a0baf8a..bd9a020 100644 --- a/Taskfile.Development.yaml +++ b/Taskfile.Development.yaml @@ -25,6 +25,11 @@ tasks: - package.json - package-lock.json + irc: + desc: Start IRC Server for Development + cmds: + - docker run --name ergo -d -p 6667:6667 -p 6697:6697 ghcr.io/ergochat/ergo:stable + mock: desc: Start Mock Server for Development dir: cmd/mock diff --git a/go.mod b/go.mod index f54695c..a5c4c6e 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/go-chi/chi/v5 v5.0.7 github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.0 // indirect github.com/mholt/archiver/v3 v3.5.1 github.com/nwaples/rardecode v1.1.3 // indirect github.com/rs/cors v1.8.2 diff --git a/irc/irc.go b/irc/irc.go index 1bb9a43..b78a288 100644 --- a/irc/irc.go +++ b/irc/irc.go @@ -55,6 +55,7 @@ func (i *Conn) Disconnect() { } i.Write([]byte("QUIT :Goodbye\r\n")) i.Conn.Close() + i.Conn = nil } // SendMessage sends the given message string to the connected IRC server diff --git a/server/client.go b/server/client.go deleted file mode 100644 index d4a3595..0000000 --- a/server/client.go +++ /dev/null @@ -1,111 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/evan-buss/openbooks/core" - "github.com/evan-buss/openbooks/irc" - "github.com/evan-buss/openbooks/util" - "github.com/google/uuid" - "github.com/r3labs/sse/v2" -) - -// Client is a middleman between the websocket connection and the hub. -type Client struct { - // Unique ID for the client - uuid uuid.UUID - - sse *sse.Server - - // Signal to indicate the connection should be terminated. - // disconnect chan struct{} - - // Message to send to the client ws connection - send chan interface{} - - // Individual IRC connection per connected client. - irc *irc.Conn - - log *log.Logger - - // Context is used to signal when this client should close. - ctx context.Context -} - -// TODO: we don't really need clients anymore as SSE handles it for us and we aren't allowing -// multiple IRC connections simulatenously. -// writePump pumps messages from the hub to the websocket connection. -// -// A goroutine running writePump is started for each connection. The -// application ensures that there is at most one writer to a connection by -// executing all writes from this goroutine. -func (server *server) writePump(c *Client) { - for { - select { - case message, ok := <-c.send: - if !ok { - continue - } - - byteMessage, err := json.Marshal(message) - if err != nil { - c.log.Printf("Error marshalling message to JSON: %s\n", err) - return - } - c.sse.Publish(c.uuid.String(), &sse.Event{ - Data: byteMessage, - }) - case <-c.ctx.Done(): - return - } - } -} - -func (server *server) connectIRC(client *Client) { - // The IRC connection could be re-used if it is already connected - if !client.irc.IsConnected() { - err := core.Join(client.irc, server.config.Server, server.config.EnableTLS) - if err != nil { - client.log.Println(err) - client.send <- newErrorResponse("Unable to connect to IRC server.") - return - } - - handler := server.NewIrcEventHandler(client) - - if server.config.Log { - logger, _, err := util.CreateLogFile(client.irc.Username, server.config.DownloadDir) - if err != nil { - server.log.Println(err) - } - handler[core.Message] = func(text string) { logger.Println(text) } - } - - go core.StartReader(client.ctx, client.irc, handler) - - client.send <- ConnectionResponse{ - StatusResponse: StatusResponse{ - MessageType: CONNECT, - NotificationType: SUCCESS, - Title: "Welcome, connection established.", - Detail: fmt.Sprintf("IRC username %s", client.irc.Username), - }, - Name: client.irc.Username, - } - - return - } - - client.send <- ConnectionResponse{ - StatusResponse: StatusResponse{ - MessageType: CONNECT, - NotificationType: NOTIFY, - Title: "Welcome back, re-using open IRC connection.", - Detail: fmt.Sprintf("IRC username %s", client.irc.Username), - }, - Name: client.irc.Username, - } -} diff --git a/server/irc_events.go b/server/irc_events.go index 9ffc574..a91fd6b 100644 --- a/server/irc_events.go +++ b/server/irc_events.go @@ -8,107 +8,107 @@ import ( "github.com/evan-buss/openbooks/core" ) -func (server *server) NewIrcEventHandler(client *Client) core.EventHandler { +func (s *server) NewIrcEventHandler() core.EventHandler { handler := core.EventHandler{} - handler[core.SearchResult] = client.searchResultHandler(server.config.DownloadDir) - handler[core.BookResult] = client.bookResultHandler(server.config.DownloadDir, server.config.DisableBrowserDownloads) - handler[core.NoResults] = client.noResultsHandler - handler[core.BadServer] = client.badServerHandler - handler[core.SearchAccepted] = client.searchAcceptedHandler - handler[core.MatchesFound] = client.matchesFoundHandler - handler[core.Ping] = client.pingHandler - handler[core.ServerList] = client.userListHandler(server.repository) - handler[core.Version] = client.versionHandler(server.config.UserAgent) + handler[core.SearchResult] = s.searchResultHandler(s.config.DownloadDir) + handler[core.BookResult] = s.bookResultHandler(s.config.DownloadDir, s.config.DisableBrowserDownloads) + handler[core.NoResults] = s.noResultsHandler + handler[core.BadServer] = s.badServerHandler + handler[core.SearchAccepted] = s.searchAcceptedHandler + handler[core.MatchesFound] = s.matchesFoundHandler + handler[core.Ping] = s.pingHandler + handler[core.ServerList] = s.userListHandler(s.repository) + handler[core.Version] = s.versionHandler(s.config.UserAgent) return handler } // searchResultHandler downloads from DCC server, parses data, and sends data to client -func (c *Client) searchResultHandler(downloadDir string) core.HandlerFunc { +func (s *server) searchResultHandler(downloadDir string) core.HandlerFunc { return func(text string) { extractedPath, err := core.DownloadExtractDCCString(filepath.Join(downloadDir, "books"), text, nil) if err != nil { - c.log.Println(err) - c.send <- newErrorResponse("Error when downloading search results.") + s.log.Println(err) + s.send <- newErrorResponse("Error when downloading search results.") return } bookResults, parseErrors, err := core.ParseSearchFile(extractedPath) if err != nil { - c.log.Println(err) - c.send <- newErrorResponse("Error when parsing search results.") + s.log.Println(err) + s.send <- newErrorResponse("Error when parsing search results.") return } if len(bookResults) == 0 && len(parseErrors) == 0 { - c.noResultsHandler(text) + s.noResultsHandler(text) return } // Output all errors so parser can be improved over time if len(parseErrors) > 0 { - c.log.Printf("%d Search Result Parsing Errors\n", len(parseErrors)) + s.log.Printf("%d Search Result Parsing Errors\n", len(parseErrors)) for _, err := range parseErrors { - c.log.Println(err) + s.log.Println(err) } } - c.log.Printf("Sending %d search results.\n", len(bookResults)) - c.send <- newSearchResponse(bookResults, parseErrors) + s.log.Printf("Sending %d search results.\n", len(bookResults)) + s.send <- newSearchResponse(bookResults, parseErrors) err = os.Remove(extractedPath) if err != nil { - c.log.Printf("Error deleting search results file: %v", err) + s.log.Printf("Error deleting search results file: %v", err) } } } // bookResultHandler downloads the book file and sends it over the websocket -func (c *Client) bookResultHandler(downloadDir string, disableBrowserDownloads bool) core.HandlerFunc { +func (s *server) bookResultHandler(downloadDir string, disableBrowserDownloads bool) core.HandlerFunc { return func(text string) { extractedPath, err := core.DownloadExtractDCCString(filepath.Join(downloadDir, "books"), text, nil) if err != nil { - c.log.Println(err) - c.send <- newErrorResponse("Error when downloading book.") + s.log.Println(err) + s.send <- newErrorResponse("Error when downloading book.") return } - c.log.Printf("Sending book entitled '%s'.\n", filepath.Base(extractedPath)) - c.send <- newDownloadResponse(extractedPath, disableBrowserDownloads) + s.log.Printf("Sending book entitled '%s'.\n", filepath.Base(extractedPath)) + s.send <- newDownloadResponse(extractedPath, disableBrowserDownloads) } } // NoResults is called when the server returns that nothing was found for the query -func (c *Client) noResultsHandler(_ string) { - c.send <- newErrorResponse("No results found for the query.") +func (s *server) noResultsHandler(_ string) { + s.send <- newErrorResponse("No results found for the query.") } // BadServer is called when the requested download fails because the server is not available -func (c *Client) badServerHandler(_ string) { - c.send <- newErrorResponse("Server is not available. Try another one.") +func (s *server) badServerHandler(_ string) { + s.send <- newErrorResponse("Server is not available. Try another one.") } // SearchAccepted is called when the user's query is accepted into the search queue -func (c *Client) searchAcceptedHandler(_ string) { - c.send <- newStatusResponse(NOTIFY, "Search accepted into the queue.") +func (s *server) searchAcceptedHandler(_ string) { + s.send <- newStatusResponse(NOTIFY, "Search accepted into the queue.") } // MatchesFound is called when the server finds matches for the user's query -func (c *Client) matchesFoundHandler(num string) { - c.send <- newStatusResponse(NOTIFY, fmt.Sprintf("Found %s results for your query.", num)) +func (s *server) matchesFoundHandler(num string) { + s.send <- newStatusResponse(NOTIFY, fmt.Sprintf("Found %s results for your query.", num)) } -func (c *Client) pingHandler(serverUrl string) { - c.irc.Pong(serverUrl) +func (s *server) pingHandler(serverUrl string) { + s.irc.Pong(serverUrl) } -func (c *Client) versionHandler(version string) core.HandlerFunc { +func (s *server) versionHandler(version string) core.HandlerFunc { return func(line string) { - c.log.Printf("Sending CTCP version response: %s", line) - core.SendVersionInfo(c.irc, line, version) + s.log.Printf("Sending CTCP version response: %s", line) + core.SendVersionInfo(s.irc, line, version) } } -func (c *Client) userListHandler(repo *Repository) core.HandlerFunc { +func (s *server) userListHandler(repo *Repository) core.HandlerFunc { return func(text string) { repo.servers = core.ParseServers(text) } diff --git a/server/middlewares.go b/server/middlewares.go deleted file mode 100644 index 89db080..0000000 --- a/server/middlewares.go +++ /dev/null @@ -1,60 +0,0 @@ -package server - -import ( - "context" - "log" - "net/http" - - "github.com/google/uuid" -) - -type userCtxKeyType string -type uuidCtxKeyType string - -const ( - userCtxKey userCtxKeyType = "user-client" - uuidCtxKey uuidCtxKeyType = "user-uuid" -) - -func (server *server) requireUser(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("OpenBooks") - if err != nil { - server.log.Println(err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - userUUID, err := uuid.Parse(cookie.Value) - if err != nil { - server.log.Println(err) - w.WriteHeader(http.StatusUnauthorized) - return - } - - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), uuidCtxKey, userUUID))) - }) -} - -// getClient should only be called when requireUser is in the middleware chain. -func (server *server) getClient(ctx context.Context) *Client { - - user := getUUID(ctx) - if user == uuid.Nil { - return nil - } - - if client, ok := server.clients[user]; ok { - return client - } - - return nil -} - -func getUUID(ctx context.Context) uuid.UUID { - uid, ok := ctx.Value(uuidCtxKey).(uuid.UUID) - if !ok { - log.Println("Unable to find user.") - } - return uid -} diff --git a/server/routes.go b/server/routes.go index 080f9b1..04e7ea6 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,13 +1,9 @@ package server import ( - "context" "embed" "encoding/json" - "errors" - "fmt" "io/fs" - "log" "net/http" "net/url" "os" @@ -17,10 +13,8 @@ import ( "time" "github.com/evan-buss/openbooks/core" - "github.com/evan-buss/openbooks/irc" "github.com/go-chi/chi/v5" - "github.com/google/uuid" ) //go:embed app/dist @@ -30,11 +24,10 @@ func (server *server) registerRoutes() *chi.Mux { router := chi.NewRouter() router.Handle("/*", server.staticFilesHandler("app/dist")) router.HandleFunc("/events", server.eventsHandler()) - router.Get("/stats", server.statsHandler()) + // router.Get("/stats", server.statsHandler()) router.Get("/servers", server.serverListHandler()) router.Group(func(r chi.Router) { - r.Use(server.requireUser) r.Post("/search", server.searchHandler()) r.Post("/download", server.downloadHandler()) @@ -48,58 +41,23 @@ func (server *server) registerRoutes() *chi.Mux { func (server *server) eventsHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("OpenBooks") - if errors.Is(err, http.ErrNoCookie) { - cookie = &http.Cookie{ - Name: "OpenBooks", - Value: uuid.New().String(), - Secure: false, - HttpOnly: true, - Expires: time.Now().Add(time.Hour * 24 * 7), - SameSite: http.SameSiteStrictMode, - } - w.Header().Add("Set-Cookie", cookie.String()) - } - - userId, err := uuid.Parse(cookie.Value) - if err != nil { - http.Error(w, "Invalid user ID.", http.StatusBadRequest) - return - } - - _, alreadyConnected := server.clients[userId] - if alreadyConnected { - http.Error(w, "Already connected.", http.StatusBadRequest) - return - } - - if len(server.clients) > 0 { - http.Error(w, "Multiple connections not allowed.", http.StatusBadRequest) + if server.connectedClients.Load() > 0 { + http.Error(w, "Multiple connections not allowed.", http.StatusServiceUnavailable) return } - client := &Client{ - sse: server.sse, - send: make(chan interface{}, 128), - uuid: userId, - irc: irc.New(server.config.UserName, server.config.UserAgent), - log: log.New(os.Stdout, fmt.Sprintf("CLIENT (%s): ", server.config.UserName), log.LstdFlags|log.Lmsgprefix), - ctx: context.Background(), - } - server.log.Printf("Client connected from %s\n", r.RemoteAddr) // The SSE library expects a "stream" query parameter to be set newQuery := r.URL.Query() - newQuery.Set("stream", client.uuid.String()) + newQuery.Set("stream", "events") r.URL.RawQuery = newQuery.Encode() go func() { <-r.Context().Done() - server.unregister <- client + server.log.Println("Client disconnected from", r.RemoteAddr) }() - server.register <- client server.sse.ServeHTTP(w, r) } } @@ -115,27 +73,27 @@ func (server *server) staticFilesHandler(assetPath string) http.Handler { return http.StripPrefix(server.config.Basepath, http.FileServer(http.FS(app))) } -func (server *server) statsHandler() http.HandlerFunc { - type statsReponse struct { - UUID string `json:"uuid"` - Name string `json:"name"` - } +// func (server *server) statsHandler() http.HandlerFunc { +// type statsReponse struct { +// UUID string `json:"uuid"` +// Name string `json:"name"` +// } - return func(w http.ResponseWriter, r *http.Request) { - result := make([]statsReponse, 0, len(server.clients)) +// return func(w http.ResponseWriter, r *http.Request) { +// result := make([]statsReponse, 0, len(server.clients)) - for _, client := range server.clients { - details := statsReponse{ - UUID: client.uuid.String(), - Name: client.irc.Username, - } +// for _, client := range server.clients { +// details := statsReponse{ +// UUID: client.uuid.String(), +// Name: client.irc.Username, +// } - result = append(result, details) - } +// result = append(result, details) +// } - json.NewEncoder(w).Encode(result) - } -} +// json.NewEncoder(w).Encode(result) +// } +// } func (server *server) serverListHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -221,12 +179,6 @@ func (server *server) deleteBooksHandler() http.HandlerFunc { func (server *server) searchHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - client := server.getClient(r.Context()) - if client == nil { - http.Error(w, "Unable to find client.", http.StatusBadRequest) - return - } - server.lastSearchMutex.Lock() defer server.lastSearchMutex.Unlock() @@ -236,44 +188,38 @@ func (server *server) searchHandler() http.HandlerFunc { remainingSeconds := time.Until(nextAvailableSearch).Seconds() // TODO: Show HTTP errors on client instead of sending SSE message http.Error(w, "Rate limited.", http.StatusTooManyRequests) - client.send <- newRateLimitResponse(remainingSeconds) + server.send <- newRateLimitResponse(remainingSeconds) return } query := r.URL.Query().Get("query") if query == "" { http.Error(w, "No search query provided.", http.StatusBadRequest) - client.send <- newErrorResponse("No search query provided.") + server.send <- newErrorResponse("No search query provided.") return } - core.SearchBook(client.irc, server.config.SearchBot, query) + core.SearchBook(server.irc, server.config.SearchBot, query) server.lastSearch = time.Now() w.WriteHeader(http.StatusAccepted) - client.send <- newStatusResponse(NOTIFY, "Search request sent.") + server.send <- newStatusResponse(NOTIFY, "Search request sent.") } } func (server *server) downloadHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - client := server.getClient(r.Context()) - if client == nil { - http.Error(w, "Unable to find client.", http.StatusBadRequest) - return - } - book := r.URL.Query().Get("book") if book == "" { http.Error(w, "No book provided.", http.StatusBadRequest) - client.send <- newErrorResponse("No book provided.") + server.send <- newErrorResponse("No book provided.") return } - core.DownloadBook(client.irc, book) + core.DownloadBook(server.irc, book) w.WriteHeader(http.StatusAccepted) - client.send <- newStatusResponse(NOTIFY, "Download request received.") + server.send <- newStatusResponse(NOTIFY, "Download request received.") } } diff --git a/server/server.go b/server/server.go index aec0d10..7250925 100644 --- a/server/server.go +++ b/server/server.go @@ -2,18 +2,23 @@ package server import ( "context" + "encoding/json" + "fmt" "log" "net/http" "os" "os/signal" "path/filepath" "sync" + "sync/atomic" "syscall" "time" + "github.com/evan-buss/openbooks/core" + "github.com/evan-buss/openbooks/irc" + "github.com/evan-buss/openbooks/util" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/google/uuid" "github.com/r3labs/sse/v2" "github.com/rs/cors" ) @@ -25,16 +30,17 @@ type server struct { // Shared data repository *Repository - // Registered clients. - clients map[uuid.UUID]*Client - sse *sse.Server - // Register requests from the clients. - register chan *Client + // Send messages to connected clients + send chan interface{} + + // Keep track of connected clients + connectedClients atomic.Int32 + status chan int - // Unregister requests from clients. - unregister chan *Client + // Single IRC connection shared among all clients + irc *irc.Conn log *log.Logger @@ -62,19 +68,31 @@ type Config struct { } func New(config Config) *server { + server := &server{ + config: &config, + repository: NewRepository(), + send: make(chan interface{}, 128), + connectedClients: atomic.Int32{}, + status: make(chan int), + irc: irc.New(config.UserName, config.UserAgent), + log: log.New(os.Stdout, "SERVER: ", log.LstdFlags|log.Lmsgprefix), + lastSearchMutex: sync.Mutex{}, + lastSearch: time.Time{}, + } + sseServer := sse.New() sseServer.AutoReplay = false sseServer.AutoStream = true - - return &server{ - sse: sseServer, - repository: NewRepository(), - config: &config, - register: make(chan *Client), - unregister: make(chan *Client), - clients: make(map[uuid.UUID]*Client), - log: log.New(os.Stdout, "SERVER: ", log.LstdFlags|log.Lmsgprefix), + sseServer.OnSubscribe = func(streamID string, sub *sse.Subscriber) { + server.status <- 1 } + sseServer.OnUnsubscribe = func(streamID string, sub *sse.Subscriber) { + server.status <- -1 + } + + server.sse = sseServer + + return server } // Start instantiates the web server and opens the browser @@ -97,7 +115,7 @@ func Start(config Config) { routes := server.registerRoutes() ctx, cancel := context.WithCancel(context.Background()) - go server.startClientHub(ctx) + go server.startEventForwarder(ctx) server.registerGracefulShutdown(cancel) router.Mount(config.Basepath, routes) @@ -108,55 +126,47 @@ func Start(config Config) { server.log.Fatal(http.ListenAndServe(":"+config.Port, router)) } -// The client hub is to be run in a goroutine and handles management of -// websocket client registrations. -func (server *server) startClientHub(ctx context.Context) { - type selfDestructor struct { - timer *time.Timer - client *Client - } - - selfDestructors := make(map[uuid.UUID]selfDestructor) +func (server *server) startEventForwarder(ctx context.Context) { + var destructTimer *time.Timer for { select { - case client := <-server.register: - if destructor, ok := selfDestructors[client.uuid]; ok { - destructor.timer.Stop() - server.log.Printf("Client %s reconnected\n", client.uuid.String()) - - client.irc = destructor.client.irc + case message, ok := <-server.send: + if !ok { + continue + } - delete(selfDestructors, client.uuid) + byteMessage, err := json.Marshal(message) + if err != nil { + server.log.Printf("Error marshalling message to JSON: %s\n", err) + return } + server.sse.Publish("events", &sse.Event{ + Data: byteMessage, + }) - server.clients[client.uuid] = client - server.connectIRC(client) - go server.writePump(client) - - case client := <-server.unregister: - // Keep the client and IRC connection alive for 3 minutes in case the client reconnects - timer := time.AfterFunc(time.Minute*3, func() { - if _, ok := selfDestructors[client.uuid]; ok { - client.irc.Disconnect() - _, cancel := context.WithCancel(client.ctx) - close(client.send) - cancel() - delete(selfDestructors, client.uuid) - server.log.Printf("Client %s self-destructed\n", client.uuid.String()) + case delta := <-server.status: + server.connectedClients.Add(int32(delta)) + + if server.connectedClients.Load() == 0 { + // Keep the client and IRC connection alive for 3 minutes in case another client connects + server.log.Println("No clients connected. Waiting 3 minutes before closing connection.") + destructTimer = time.AfterFunc(time.Second*30, func() { + if server.connectedClients.Load() == 0 { + server.irc.Disconnect() + server.log.Println("IRC connection closed.") + } + }) + } else { + if destructTimer != nil && destructTimer.Stop() { + server.log.Println("IRC connection kept alive.") } - }) - selfDestructors[client.uuid] = selfDestructor{timer, client} - delete(server.clients, client.uuid) - server.log.Printf("Client %s disconnected\n", client.uuid.String()) - case <-ctx.Done(): - for _, client := range server.clients { - _, cancel := context.WithCancel(client.ctx) - close(client.send) - cancel() - delete(server.clients, client.uuid) + server.connectIRC(ctx) } + + case <-ctx.Done(): + server.irc.Disconnect() return } } @@ -181,3 +191,51 @@ func createBooksDirectory(config Config) { panic(err) } } + +func (server *server) connectIRC(ctx context.Context) { + // The IRC connection is re-used if already connected + if server.irc.IsConnected() { + server.send <- ConnectionResponse{ + StatusResponse: StatusResponse{ + MessageType: CONNECT, + NotificationType: NOTIFY, + Title: "Welcome back, re-using open IRC connection.", + Detail: fmt.Sprintf("IRC username %s", server.irc.Username), + }, + Name: server.irc.Username, + } + + return + } + + server.log.Println("Connecting to IRC server.") + + err := core.Join(server.irc, server.config.Server, server.config.EnableTLS) + if err != nil { + server.log.Println(err) + server.send <- newErrorResponse("Unable to connect to IRC server.") + return + } + + handler := server.NewIrcEventHandler() + + if server.config.Log { + logger, _, err := util.CreateLogFile(server.irc.Username, server.config.DownloadDir) + if err != nil { + server.log.Println(err) + } + handler[core.Message] = func(text string) { logger.Println(text) } + } + + go core.StartReader(ctx, server.irc, handler) + + server.send <- ConnectionResponse{ + StatusResponse: StatusResponse{ + MessageType: CONNECT, + NotificationType: SUCCESS, + Title: "Welcome, connection established.", + Detail: fmt.Sprintf("IRC username %s", server.irc.Username), + }, + Name: server.irc.Username, + } +} diff --git a/todo b/todo index e34af92..538c923 100644 --- a/todo +++ b/todo @@ -2,9 +2,13 @@ Future: - Show IRC logs in the browser - Handle IRC name collisions -- Allow reconnecting to same IRC connection if websocket disconnects. - - Currently we immediately terminate the IRC connection and remove the client - - Possible use a timer that keeps the Client around for a set period of time - after websocket disconnect. -- Send download progress updates to the browser like we show for the CLI -- Responsive layout +- OpenAPI docs +- OPDS server to search / downoad on KOReader directly + +Ideas: +- Connection pooling to IRC to allow multi users at the same time. + - I no longer think it's possible to multi-plex a single connection and route the responses to the correct user. + There just isn't enough information to correlate the responses to the correct user. I've thought of using invisible characters in the messages, + but even then the responses wouldn't have it and it's not guarantted that the requests won't be rejected.. + - Instead, we could have a pool of connections and assign a connection to a user when they connect. If none are available, we deny the connection. + - The config could have a set of usernames that would be the limit of connections for that server.