package learning import ( "context" "embed" "io" "log" "net/http" "os" "strings" "sync" "time" "codeberg.org/arimelody/ari-stream-tools/broadcast" "github.com/gin-gonic/gin" ) //go:embed public var publicFS embed.FS //go:embed pages var pagesFS embed.FS type ( ServiceConfig struct { TitleFilePath string } Service struct { cfg ServiceConfig titleFileMutex sync.Mutex titleLastModTime time.Time titleText string titleUpdated chan string titleUpdatedBroadcast broadcast.BroadcastChannel[string] } ) func New(ctx context.Context, cfg ServiceConfig) *Service { titleText := []byte{} titleUpdated := make(chan string) if _, err := os.Stat(cfg.TitleFilePath); os.IsNotExist(err) { log.Printf("Learning title file [%s] does not exist, creating one...", cfg.TitleFilePath) os.WriteFile(cfg.TitleFilePath, []byte("Untitled"), 0644) titleText = []byte("Untitled") } else if err != nil { log.Fatalf("Failed to stat title file: %v", err) } else { titleText, err = os.ReadFile(cfg.TitleFilePath) if err != nil { log.Fatalf("Failed to read title file: %v", err) } } srv := &Service{ cfg: cfg, titleFileMutex: sync.Mutex{}, titleLastModTime: time.Unix(0, 0), titleText: string(titleText), titleUpdated: titleUpdated, titleUpdatedBroadcast: broadcast.NewBroadcastChannel(ctx, titleUpdated), } return srv } func (srv *Service) BindRoutes(group *gin.RouterGroup) { group.GET("/public/*path", func(ctx *gin.Context) { path := strings.TrimPrefix(ctx.Request.URL.Path, "/learning/") http.ServeFileFS(ctx.Writer, ctx.Request, publicFS, path) }) group.GET("/sse", func(ctx *gin.Context) { ctx.Header("connection", "keep-alive") ctx.SSEvent("title-update", srv.titleText) titleUpdated := srv.titleUpdatedBroadcast.Subscribe() ticker := time.NewTicker(10 * time.Millisecond) ctx.Stream(func(w io.Writer) bool { select { case titleText := <-titleUpdated: ctx.SSEvent("title-update", titleText) case <-ticker.C: } return true }) srv.titleUpdatedBroadcast.Cancel(titleUpdated) }) group.POST("/title", func(ctx *gin.Context) { data, err := io.ReadAll(ctx.Request.Body) if err != nil { log.Printf("Failed to read request body: %v", err) ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) return } srv.titleFileMutex.Lock() defer srv.titleFileMutex.Unlock() file, err := os.OpenFile(srv.cfg.TitleFilePath, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0) if err != nil { log.Printf("Failed to open title file: %v", err) ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) return } if _, err := file.Write(data); err != nil { log.Printf("Failed to write to title file: %v", err) ctx.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) return } srv.titleUpdated<-string(data) ctx.String(http.StatusOK, http.StatusText(http.StatusOK)) }) group.GET("/hello", func(ctx *gin.Context) { http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/index.html") }) group.GET("/brb", func(ctx *gin.Context) { http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/brb.html") }) group.GET("/bye", func(ctx *gin.Context) { http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/bye.html") }) }