diff --git a/Chat.go b/Chat.go index 5bed563..ff33888 100644 --- a/Chat.go +++ b/Chat.go @@ -15,6 +15,11 @@ import ( func ChatPageHandler(c *fiber.Ctx) error { authCookie := c.Cookies("jade-edgedb-auth-token", "") + go func() { + fmt.Println("Sending test event") + sseChanel.SendEvent("test", "Hello from server") + }() + if authCookie != "" && !checkIfLogin() { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}) } @@ -400,7 +405,14 @@ func LoadKeysHandler(c *fiber.Ctx) error { } openaiExists, anthropicExists, mistralExists, groqExists := getExistingKeys() - out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-keys.html")).Execute(pongo2.Context{"IsLogin": checkIfLogin(), "OpenaiExists": openaiExists, "AnthropicExists": anthropicExists, "MistralExists": mistralExists, "GroqExists": groqExists}) + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-keys.html")).Execute(pongo2.Context{ + "IsLogin": checkIfLogin(), + "OpenaiExists": openaiExists, + "AnthropicExists": anthropicExists, + "MistralExists": mistralExists, + "GroqExists": groqExists, + "AnyExists": openaiExists || anthropicExists || mistralExists || groqExists, + }) if err != nil { c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Error rendering template", diff --git a/login.go b/login.go index 07d73ee..4083d1d 100644 --- a/login.go +++ b/login.go @@ -32,8 +32,6 @@ func generatePKCE() (string, string) { func handleUiSignIn(c *fiber.Ctx) error { verifier, challenge := generatePKCE() - fmt.Println("Challenge: ", challenge) - c.Cookie(&fiber.Cookie{ Name: "jade-edgedb-pkce-verifier", Value: verifier, @@ -49,11 +47,11 @@ func handleUiSignIn(c *fiber.Ctx) error { func handleCallbackSignup(c *fiber.Ctx) error { code := c.Query("code") if code == "" { - error := c.Query("error") - return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("OAuth callback is missing 'code'. OAuth provider responded with error: %s", error)) + err := c.Query("error") + return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("OAuth callback is missing 'code'. OAuth provider responded with error: %s", err)) } - verifier := c.Cookies("jade-edgedb-pkce-verifier") + verifier := c.Cookies("jade-edgedb-pkce-verifier", "") if verifier == "" { return c.Status(fiber.StatusBadRequest).SendString("Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?") } @@ -111,17 +109,17 @@ func handleCallbackSignup(c *fiber.Ctx) error { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}) - return c.Redirect("/", fiber.StatusTemporaryRedirect) + return c.Redirect("/", fiber.StatusPermanentRedirect) } func handleCallback(c *fiber.Ctx) error { code := c.Query("code") if code == "" { - error := c.Query("error") - return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("OAuth callback is missing 'code'. OAuth provider responded with error: %s", error)) + err := c.Query("error") + return c.Status(fiber.StatusBadRequest).SendString(fmt.Sprintf("OAuth callback is missing 'code'. OAuth provider responded with error: %s", err)) } - verifier := c.Cookies("jade-edgedb-pkce-verifier") + verifier := c.Cookies("jade-edgedb-pkce-verifier", "") if verifier == "" { fmt.Println("Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?") return c.Status(fiber.StatusBadRequest).SendString("Could not find 'verifier' in the cookie store. Is this the same user agent/browser that started the authorization flow?") @@ -158,7 +156,7 @@ func handleCallback(c *fiber.Ctx) error { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}) - return c.Redirect("/", fiber.StatusTemporaryRedirect) + return c.Redirect("/", fiber.StatusPermanentRedirect) } func handleSignOut(c *fiber.Ctx) error { @@ -175,5 +173,5 @@ func handleSignOut(c *fiber.Ctx) error { edgeCtx = ctx edgeClient = client - return c.Redirect("/", fiber.StatusTemporaryRedirect) + return c.Redirect("/", fiber.StatusPermanentRedirect) } diff --git a/main.go b/main.go index 7022165..d815a40 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/MrBounty/JADE2.0/ssefiber" "github.com/flosch/pongo2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" @@ -11,6 +12,7 @@ import ( var userTmpl *pongo2.Template var botTmpl *pongo2.Template +var sseChanel *ssefiber.FiberSSEChannel func main() { botTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-bot.html")) @@ -25,6 +27,8 @@ func main() { AppName: "JADE 2.0", EnablePrintRoutes: true, }) + sse := ssefiber.New(app, "/sse") + sseChanel = sse.CreateChannel("sse", "/sse") // Add default logger app.Use(logger.New()) diff --git a/ssefiber/sse.go b/ssefiber/sse.go new file mode 100644 index 0000000..6c214c0 --- /dev/null +++ b/ssefiber/sse.go @@ -0,0 +1,250 @@ +package ssefiber + +import ( + "bufio" + "fmt" + "time" + + "github.com/gofiber/fiber/v2" +) + +// SSEEvent is a struct that represents an SSE event +type FiberSSEEvent struct { + Timestamp time.Time `json:"timestamp"` + ID string `json:"id"` + Event string `json:"event"` + Data string `json:"data"` + Retry string `json:"retry"` + OnChannel *FiberSSEChannel +} + +// # TypeDef +// +// Handler for channel events +type FiberSSEEventHandler func(ctx *fiber.Ctx, sseChannel *FiberSSEChannel) + +// # TypeDef +// +// Handler for specific events on a channel +type FiberSSEOnEventHandler func(ctx *fiber.Ctx, sseChannel *FiberSSEChannel, sseEvent *FiberSSEEvent) +type FiberSSEEvents interface { + OnConnect(handlers ...FiberSSEEventHandler) + OnDisconnect(handlers ...FiberSSEEventHandler) + OnEvent(eventName string, handlers ...FiberSSEOnEventHandler) + FireOnEventHandlers(fiberCtx *fiber.Ctx, event string) +} + +/* +A channel with a name, and a sub-base-path +*/ +type FiberSSEChannel struct { + FiberSSEEvents + Name string + Base string + Events chan *FiberSSEEvent + ParentSSEApp *FiberSSEApp + Handlers map[string]([]FiberSSEEventHandler) + EventHandlers map[string]([]FiberSSEOnEventHandler) +} +type FiberSSEHandler func(c *fiber.Ctx, w *bufio.Writer) error + +/* +The SSE Information Structure includes a list of channels and the fiber application +*/ +type FiberSSEApp struct { + IFiberSSEApp + Base string + Router *fiber.Router + Channels map[string]*FiberSSEChannel + FiberApp *fiber.App +} + +// FiberSSEApp Interface +type IFiberSSEApp interface { + ServeHTTP(ctx *fiber.Ctx) error + CreateChannel(name, base string) *FiberSSEChannel + ListChannels() map[string]*FiberSSEChannel + GetChannel(name string) *FiberSSEChannel +} + +/* +New initializes a base SSE route group at `base`. + +The base route is the base path for all channels. + +The channels parameter is a list of channels that will be created. +Each channel has a name, a base route, and a channel for sending events. + + // Create a new SSE app + app := fiber.New() + // Create a new SSE app on the fiber app + sseApp := ssefiber.New(app, "/sse") + // Add a channel to the SSE app + testChan := sseApp.CreateChannel("test", "/test") // Channel at /sse/test + // Events Channel + eventsChan := testChan.Events +*/ +func New(app *fiber.App, base string) *FiberSSEApp { + // Add the base route + fiberRouter := app.Group(base, func(c *fiber.Ctx) error { + // Set the headers for SSE + c.Set("Cache-Control", "no-cache") + c.Set("Content-Type", "text/event-stream") + c.Set("Connection", "keep-alive") + c.Set("Access-Control-Allow-Origin", "*") + return c.Next() + }) + + // Create a new SSE App + newFSSEApp := &FiberSSEApp{ + Base: base, + Router: &fiberRouter, + FiberApp: app, + Channels: make(map[string]*FiberSSEChannel), + } + return newFSSEApp +} + +/* +CreateChannel creates a new channel with the given name and base path. +Functions as a shortcut for making a new chan each time + +Example: + + app := fiber.New() + sseApp := ssefiber.New(app, "/sse") + chanOne := sseApp.CreateChannel("Channel One", "/one") + chanTwo := sseApp.CreateChannel("Channel Two", "/two") +*/ +func (app *FiberSSEApp) CreateChannel(name, base string) *FiberSSEChannel { + newChannel := &FiberSSEChannel{ + Name: name, + Base: base, + Events: make(chan *FiberSSEEvent), + ParentSSEApp: app, + Handlers: make(map[string][]FiberSSEEventHandler), + EventHandlers: make(map[string][]FiberSSEOnEventHandler), + } + app.Channels[name] = newChannel + // Add the sub-route for the channel + (*app.Router).Get(newChannel.Base, newChannel.ServeHTTP) + return newChannel +} + +// ListChannels returns a list of all the channels and prints them to the console +func (app *FiberSSEApp) ListChannels() map[string]*FiberSSEChannel { + fmt.Println("Listing Channels...") + for _, channel := range app.Channels { + channel.Print() + } + return app.Channels +} + +/* +Create an event and send it to the channel. +*/ +func (channel *FiberSSEChannel) SendEvent(event, data string) { + sseEvent := &FiberSSEEvent{ + Timestamp: time.Now(), + Event: event, + Data: data, + OnChannel: channel, + } + channel.Events <- sseEvent +} + +// Flush the event to the writer `w` - formats according to SSE standard +func (e *FiberSSEEvent) Flush(w *bufio.Writer) error { + fmt.Fprintf(w, "event: %s\ndata: %s\n\n", e.Event, e.Data) + return w.Flush() +} + +// Prints the channel information to the console +func (c *FiberSSEChannel) Print() { + fmt.Printf("==CHANNEL CREATED==\nName: %s\nRoute Endpoint: %s\n===================", c.Name, c.ParentSSEApp.Base+c.Base) +} + +// # Internal Method +// +// ServeHTTP returns a fiber.Handler for the channel. +// +// Use `sseApp.CreateChannel` to create a new channel. +func (fChan *FiberSSEChannel) ServeHTTP(c *fiber.Ctx) error { + + c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + // Fire OnConnect Event Handlers + + go fChan.FireHandlers(c, "connect") + + for { + event, more := <-fChan.Events + // fmt.Fprintf(w, "event: %s\ndata: %s\n\n", string(event.Event), string(event.Data)) + // w.Flush() + go event.FireEventHandlers(c) + if err := event.Flush(w); err != nil { + go fChan.FireHandlers(c, "disconnect") + return + } + if !more { + // Fire OnDisconnect Event Handlers + go fChan.FireHandlers(c, "disconnect") + return + } + } + }) + + return nil + +} + +// Cleanup removes all of the channels from the app. Should be used as a defer +func (sseApp *FiberSSEApp) Cleanup() { + for _, channel := range sseApp.Channels { + close(channel.Events) + } + fmt.Println("All Channels Closed - Cleanup Successful") +} + +// Fire the handlers for a given channel event (connect, disconnect) +func (channel *FiberSSEChannel) FireHandlers(fiberCtx *fiber.Ctx, event string) { + for _, handler := range channel.Handlers[event] { + handler(fiberCtx, channel) + } +} + +// Fire the handlers for this event +func (e *FiberSSEEvent) FireEventHandlers(fiberCtx *fiber.Ctx) { + channel := e.OnChannel + for _, handler := range channel.EventHandlers[e.Event] { + handler(fiberCtx, channel, e) + } +} + +// Adds the handlers to the channel for the connect method +func (channel *FiberSSEChannel) OnConnect(handlers ...FiberSSEEventHandler) { + channel.Handlers["connect"] = []FiberSSEEventHandler{} + channel.Handlers["connect"] = append(channel.Handlers["connect"], handlers...) +} + +// Adds the handlers to the channel for the disconnect method +func (channel *FiberSSEChannel) OnDisconnect(handlers ...FiberSSEEventHandler) { + channel.Handlers["disconnect"] = []FiberSSEEventHandler{} + channel.Handlers["disconnect"] = append(channel.Handlers["disconnect"], handlers...) +} + +// Add handlers for the any given event +// +// Example: +// +// channelOne.OnEvent("test", ...) // Fires anytime the event "test" is fired +func (channel *FiberSSEChannel) OnEvent(eventName string, handlers ...FiberSSEOnEventHandler) { + channel.EventHandlers[eventName] = []FiberSSEOnEventHandler{} + channel.EventHandlers[eventName] = append(channel.EventHandlers[eventName], handlers...) + +} + +// Returns a channel by name +func (app *FiberSSEApp) GetChannel(name string) *FiberSSEChannel { + findChan := app.Channels[name] + return findChan +} diff --git a/static/icons/bouvai2.png b/static/icons/bouvai2.png index cad16aa..aee9877 100644 Binary files a/static/icons/bouvai2.png and b/static/icons/bouvai2.png differ diff --git a/static/style.css b/static/style.css index 2f83bc1..b3ce253 100644 --- a/static/style.css +++ b/static/style.css @@ -47,6 +47,10 @@ html { --bulma-primary: #126d0f; } +.button.is-primary { + background-color: #1ebc19; +} + /* Chat input stuff */ .chat-input-container { display: flex; diff --git a/views/layouts/main.html b/views/layouts/main.html index e2469e8..87a65b8 100644 --- a/views/layouts/main.html +++ b/views/layouts/main.html @@ -11,12 +11,11 @@ - -
+ {{embed}} diff --git a/views/partials/popover-keys.html b/views/partials/popover-keys.html index 10dfd70..e439391 100644 --- a/views/partials/popover-keys.html +++ b/views/partials/popover-keys.html @@ -1,6 +1,7 @@