package main import ( "context" "github.com/fsnotify/fsnotify" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/urfave/cli/v3" "log" "net/http" "os" "path/filepath" "regexp" "strings" ) var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }} var previousLine = make(map[string]int) func main() { cmd := &cli.Command{ Name: "edas", Usage: "an api for reading data from elite dangerous", Flags: []cli.Flag{ &cli.StringFlag{Name: "path", Value: "%USERPROFILE%\\Saved Games\\Frontier Developments\\Elite Dangerous"}, }, Action: func(ctx context.Context, cmd *cli.Command) error { r := gin.Default() /* These are all the routes that just serve json data straight from the files. */ r.GET("/backpack", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Backpack.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/cargo", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Cargo.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/fcmaterials", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "FCMaterials.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/market", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Market.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/modulesinfo", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "ModulesInfo.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/navroute", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "NavRoute.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/outfitting", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Outfitting.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/shiplocker", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "ShipLocker.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/shipyard", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Shipyard.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) r.GET("/status", func(c *gin.Context) { b, err := os.ReadFile(ExpandPath(filepath.Join(cmd.String("path"), "Status.json"))) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } c.Data(200, "application/json; charset=utf-8", b) }) /* Shared variables for the watcher and websocket. */ hub := HubNew() /* Read in all existing Journal files so we know what to ignore. */ files, err := filepath.Glob(ExpandPath(filepath.Join(cmd.String("path"), "Journal.*.log"))) if err != nil { return err } for _, f := range files { data, err := os.ReadFile(f) if err != nil { return err } lines := strings.Split(string(data), "\n") previousLine[filepath.Base(f)] = len(lines) } /* This is the watcher that waits for changes to the journal files. */ watcher, err := fsnotify.NewWatcher() if err != nil { return err } defer watcher.Close() go func() { for { select { case event, ok := <-watcher.Events: if !ok { return } if event.Op.Has(fsnotify.Write) { data, err := processChangedFile(event.Name) if err != nil { log.Println(err) continue } hub.sendClient(data) } } } }() err = watcher.Add(ExpandPath(cmd.String("path"))) if err != nil { return err } /* This is the endpoint for the websocket that publishes events. */ r.GET("/journal", func(c *gin.Context) { w, r := c.Writer, c.Request con, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Print(err) return } client := &Client{hub: hub, conn: con} client.hub.registerClient(client) go client.keepAlive() }) return r.Run(":3000") }, } if err := cmd.Run(context.Background(), os.Args); err != nil { log.Fatal(err) } } func ExpandPath(path string) string { re := regexp.MustCompile(`(?m)%([^%]+)%`) return os.ExpandEnv(re.ReplaceAllString(path, "${$1}")) } func processChangedFile(path string) ([]byte, error) { switch filepath.Base(path) { case "Backpack.json", "Cargo.json", "FCMaterials.json", "Market.json", "ModulesInfo.json", "NavRoute.json", "Outfitting.json", "ShipLocker.json", "Shipyard.json", "Status.json": return os.ReadFile(path) default: if strings.HasPrefix(filepath.Base(path), "Journal") { data, err := os.ReadFile(path) if err != nil { return nil, err } offset, ok := previousLine[filepath.Base(path)] if !ok { offset = 0 } lines := strings.Split(string(data), "\n") nextLine := lines[offset-1] previousLine[filepath.Base(path)]++ return []byte(nextLine), nil } } return nil, nil }