diff --git a/handlers/api/documents.go b/handlers/api/documents/documents.go similarity index 100% rename from handlers/api/documents.go rename to handlers/api/documents/documents.go diff --git a/handlers/api/firebase/firebase.go b/handlers/api/firebase/firebase.go new file mode 100644 index 0000000..407e0b5 --- /dev/null +++ b/handlers/api/firebase/firebase.go @@ -0,0 +1,131 @@ +package firebase + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +type ( + BatchGetRequest struct { + Documents []string `json:"documents"` + } + BatchGetEmptyResponse struct { + Missing string `json:"missing"` + ReadTime string `json:"readTime"` + } + + FoundInfoResponse struct { + Name string `json:"name"` + Fields interface{} `json:"fields"` + CreateTime string `json:"createTime"` + UpdateTime string `json:"updateTime"` + } + BatchGetExistsResponse struct { + Found FoundInfoResponse `json:"found"` + ReadTime string `json:"readTime"` + } + + UpdateRequest struct { + Name string `json:"name"` + Fields interface{} `json:"fields"` + } + WriteRequest struct { + Update UpdateRequest `json:"update"` + } + BatchCommitRequest struct { + Writes []WriteRequest `json:"writes"` + } + + WriteResult struct { + UpdateTime string `json:"updateTime"` + } + BatchCommitResponse struct { + WriteResults []WriteResult `json:"writeResults"` + CommitTime string `json:"commitTime"` + } +) + +var savedItems = make(map[string]interface{}) + +func (body *BatchGetRequest) Bind(r *http.Request) (err error) { + return nil +} +func (body *BatchCommitRequest) Bind(r *http.Request) (err error) { + return nil +} +func HandleBatchCommit() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + projectId := chi.URLParam(r, "project_id") + databaseId := chi.URLParam(r, "database_id") + _ = projectId + _ = databaseId + + data := &BatchCommitRequest{} + // Seems like requests is text/plain but content is json ... + if err := render.DecodeJSON(r.Body, data); err != nil { + fmt.Println(err) + render.Status(r, http.StatusBadRequest) + return + } + + savedItems[data.Writes[0].Update.Name] = data.Writes[0].Update.Fields + + render.JSON(w, r, BatchCommitResponse{ + CommitTime: time.Now().Format(time.RFC3339), + WriteResults: []WriteResult{ + WriteResult{UpdateTime: time.Now().Format(time.RFC3339)}, + }, + }) + render.Status(r, http.StatusOK) + return + + } +} + +func HandleBatchGet() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + projectId := chi.URLParam(r, "project_id") + databaseId := chi.URLParam(r, "database_id") + fmt.Printf("Got %v and %v\n", projectId, databaseId) + data := &BatchGetRequest{} + + // Seems like requests is text/plain but content is json ... + if err := render.DecodeJSON(r.Body, data); err != nil { + fmt.Println(err) + render.Status(r, http.StatusBadRequest) + return + } + key := data.Documents[0] + fmt.Printf("Got key %v \n", key) + + fields, ok := savedItems[key] + + if !ok { + fmt.Println("missing key") + render.JSON(w, r, []BatchGetEmptyResponse{BatchGetEmptyResponse{ + Missing: key, + ReadTime: time.Now().Format(time.RFC3339), + }}) + render.Status(r, http.StatusOK) + return + } + fmt.Println("existing key") + render.JSON(w, r, []BatchGetExistsResponse{BatchGetExistsResponse{ + Found: FoundInfoResponse{ + Name: key, + Fields: fields, + CreateTime: time.Now().Format(time.RFC3339), + UpdateTime: time.Now().Format(time.RFC3339), + }, + ReadTime: time.Now().Format(time.RFC3339), + }}) + render.Status(r, http.StatusOK) + return + + } +} diff --git a/main.go b/main.go index 88c6bc1..b4ea1e6 100644 --- a/main.go +++ b/main.go @@ -4,19 +4,23 @@ import ( "embed" _ "embed" "excalidraw-complete/core" - documents "excalidraw-complete/handlers/api" + "excalidraw-complete/handlers/api/documents" + "excalidraw-complete/handlers/api/firebase" "excalidraw-complete/stores" "fmt" + "io" "io/fs" "net/http" "os" "os/signal" + "strings" "syscall" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/zishang520/engine.io/v2/types" + "github.com/zishang520/engine.io/v2/utils" socketio "github.com/zishang520/socket.io/v2/socket" ) @@ -40,7 +44,58 @@ func handleUI() http.Handler { if err != nil { panic(err) } - return http.FileServer(http.FS(sub)) + // Let's hot-patch all calls to firebase DB + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + originalPath := r.URL.Path + originalPath = strings.TrimPrefix(originalPath, "/") + fmt.Println(originalPath) + file, err := sub.Open(originalPath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + defer file.Close() + + fileContent, err := io.ReadAll(file) + if err != nil { + http.Error(w, "Error reading file", http.StatusInternalServerError) + return + } + modifiedContent := strings.ReplaceAll(string(fileContent), "firestore.googleapis.com", "localhost:3002") + modifiedContent = strings.ReplaceAll(modifiedContent, "ssl=!0", "ssl=0") + modifiedContent = strings.ReplaceAll(modifiedContent, "ssl:!0", "ssl:0") + if modifiedContent != string(fileContent) { + fmt.Println("has replace") + } + + // Set the correct Content-Type based on the file extension + contentType := http.DetectContentType([]byte(modifiedContent)) + switch { + case strings.HasSuffix(originalPath, ".js"): + contentType = "application/javascript" + case strings.HasSuffix(originalPath, ".html"): + contentType = "text/html" + case strings.HasSuffix(originalPath, ".css"): + contentType = "text/css" + case strings.HasSuffix(originalPath, ".wasm"): + contentType = "application/wasm" + case strings.HasSuffix(originalPath, ".tsx"): + contentType = "text/typescript" + case strings.HasSuffix(originalPath, ".png"): + contentType = "image/png" + case strings.HasSuffix(originalPath, ".woff2"): + contentType = "font/woff2" + } + + // Serve the modified content + w.Header().Set("Content-Type", contentType) + _, err = w.Write([]byte(modifiedContent)) + if err != nil { + http.Error(w, "Error serving file", http.StatusInternalServerError) + return + } + return + }) } func setupRouter(documentStore core.DocumentStore) *chi.Mux { @@ -55,6 +110,11 @@ func setupRouter(documentStore core.DocumentStore) *chi.Mux { MaxAge: 300, // Maximum value not ignored by any of major browsers })) + r.Route("/v1/projects/{project_id}/databases/{database_id}", func(r chi.Router) { + r.Post("/documents:commit", firebase.HandleBatchCommit()) + r.Post("/documents:batchGet", firebase.HandleBatchGet()) + }) + r.Route("/api/v2", func(r chi.Router) { r.Post("/post/", documents.HandleCreate(documentStore)) r.Route("/{id}", func(r chi.Router) { @@ -72,21 +132,24 @@ func setupSocketIO() *socketio.Server { Origin: "*", Credentials: true, }) + opts.SetTransports(types.NewSet("polling", "webtransport")) ioo := socketio.NewServer(nil, opts) ioo.On("connection", func(clients ...any) { socket := clients[0].(*socketio.Socket) - ioo.To(socketio.Room(socket.Id())).Emit("init-room") me := socket.Id() + myRoom := socketio.Room(me) + ioo.To(myRoom).Emit("init-room") + utils.Log().Println("init room ", myRoom) socket.On("join-room", func(datas ...any) { room := socketio.Room(datas[0].(string)) - fmt.Printf("Socket %v has joined %v\n", me, room) + utils.Log().Printf("Socket %v has joined %v\n", me, room) socket.Join(room) ioo.In(room).FetchSockets()(func(usersInRoom []*socketio.RemoteSocket, _ error) { if len(usersInRoom) <= 1 { - ioo.To(socketio.Room(me)).Emit("first-in-room") + ioo.To(myRoom).Emit("first-in-room") } else { - fmt.Printf("emit new user %v in room %v\n", me, room) + utils.Log().Printf("emit new user %v in room %v\n", me, room) socket.Broadcast().To(room).Emit("new-user", me) } @@ -95,7 +158,7 @@ func setupSocketIO() *socketio.Server { for _, user := range usersInRoom { newRoomUsers = append(newRoomUsers, user.Id()) } - fmt.Printf(" room %v has users %v\n", room, newRoomUsers) + utils.Log().Println(" room ", room, " has users ", newRoomUsers) ioo.In(room).Emit( "room-user-change", newRoomUsers, @@ -105,12 +168,12 @@ func setupSocketIO() *socketio.Server { }) socket.On("server-broadcast", func(datas ...any) { roomID := datas[0].(string) - fmt.Printf(" user %v sends update to room %v\n", me, roomID) + utils.Log().Printf(" user %v sends update to room %v\n", me, roomID) socket.Broadcast().To(socketio.Room(roomID)).Emit("client-broadcast", datas[1], datas[2]) }) socket.On("server-volatile-broadcast", func(datas ...any) { roomID := datas[0].(string) - fmt.Printf(" user %v sends volatile update to room %v\n", me, roomID) + utils.Log().Printf(" user %v sends volatile update to room %v\n", me, roomID) socket.Volatile().Broadcast().To(socketio.Room(roomID)).Emit("client-broadcast", datas[1], datas[2]) }) @@ -121,20 +184,18 @@ func setupSocketIO() *socketio.Server { socket.On("disconnecting", func(datas ...any) { for _, currentRoom := range socket.Rooms().Keys() { ioo.In(currentRoom).FetchSockets()(func(usersInRoom []*socketio.RemoteSocket, _ error) { - allUsers := []socketio.SocketId{} - remainingUsers := []socketio.SocketId{} - fmt.Printf("disconnecting %v from room %v\n", me, currentRoom) + otherClients := []socketio.SocketId{} + utils.Log().Printf("disconnecting %v from room %v\n", me, currentRoom) for _, userInRoom := range usersInRoom { - allUsers = append(allUsers, userInRoom.Id()) if userInRoom.Id() != me { - remainingUsers = append(remainingUsers, userInRoom.Id()) + otherClients = append(otherClients, userInRoom.Id()) } } - if len(remainingUsers) > 0 { - fmt.Printf("leaving user, room %v has users %v -> %v\n", currentRoom, allUsers, remainingUsers) + if len(otherClients) > 0 { + utils.Log().Printf("leaving user, room %v has users %v\n", currentRoom, otherClients) ioo.In(currentRoom).Emit( "room-user-change", - remainingUsers, + otherClients, ) } @@ -149,6 +210,7 @@ func setupSocketIO() *socketio.Server { socket.Disconnect(true) }) }) + utils.Log().Println("%v", ioo) return ioo }