| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- package main
- import (
- "embed"
- _ "embed"
- "excalidraw-complete/core"
- "excalidraw-complete/handlers/api/documents"
- "excalidraw-complete/handlers/api/firebase"
- "excalidraw-complete/stores"
- "flag"
- "fmt"
- "io"
- "io/fs"
- "net/http"
- "os"
- "os/signal"
- "strings"
- "syscall"
- "github.com/sirupsen/logrus"
- "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"
- )
- type (
- UserToFollow struct {
- SocketId string `json:"socketId"`
- Username string `json:"username"`
- }
- OnUserFollowedPayload struct {
- UserToFollow UserToFollow `json:"userToFollow"`
- Action string `json:"action"` // "FOLLOW" | "UNFOLLOW"
- }
- )
- //go:embed all:frontend
- var assets embed.FS
- func handleUI() http.Handler {
- sub, err := fs.Sub(assets, "frontend")
- if err != nil {
- panic(err)
- }
- // 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, "/")
- // Redirect "/" to "index.html"
- if originalPath == "" {
- originalPath = "index.html"
- }
- 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")
- // 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 {
- r := chi.NewRouter()
- r.Use(middleware.Logger)
- r.Use(cors.Handler(cors.Options{
- AllowedOrigins: []string{"https://*", "http://*"},
- AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
- AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Content-Length", "X-CSRF-Token", "Token", "session", "Origin", "Host", "Connection", "Accept-Encoding", "Accept-Language", "X-Requested-With"},
- AllowCredentials: true,
- 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) {
- r.Get("/", documents.HandleGet(documentStore))
- })
- })
- return r
- }
- func setupSocketIO() *socketio.Server {
- opts := socketio.DefaultServerOptions()
- opts.SetMaxHttpBufferSize(5000000)
- opts.SetPath("/socket.io")
- opts.SetAllowEIO3(true)
- opts.SetCors(&types.Cors{
- Origin: "*",
- Credentials: true,
- })
- ioo := socketio.NewServer(nil, opts)
- ioo.On("connection", func(clients ...any) {
- socket := clients[0].(*socketio.Socket)
- 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))
- 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(myRoom).Emit("first-in-room")
- } else {
- utils.Log().Printf("emit new user %v in room %v\n", me, room)
- socket.Broadcast().To(room).Emit("new-user", me)
- }
- // Inform all clients by new users.
- newRoomUsers := []socketio.SocketId{}
- for _, user := range usersInRoom {
- newRoomUsers = append(newRoomUsers, user.Id())
- }
- utils.Log().Println(" room ", room, " has users ", newRoomUsers)
- ioo.In(room).Emit(
- "room-user-change",
- newRoomUsers,
- )
- })
- })
- socket.On("server-broadcast", func(datas ...any) {
- roomID := datas[0].(string)
- 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)
- 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])
- })
- socket.On("user-follow", func(datas ...any) {
- // TODO()
- })
- socket.On("disconnecting", func(datas ...any) {
- for _, currentRoom := range socket.Rooms().Keys() {
- ioo.In(currentRoom).FetchSockets()(func(usersInRoom []*socketio.RemoteSocket, _ error) {
- otherClients := []socketio.SocketId{}
- utils.Log().Printf("disconnecting %v from room %v\n", me, currentRoom)
- for _, userInRoom := range usersInRoom {
- if userInRoom.Id() != me {
- otherClients = append(otherClients, userInRoom.Id())
- }
- }
- if len(otherClients) > 0 {
- utils.Log().Printf("leaving user, room %v has users %v\n", currentRoom, otherClients)
- ioo.In(currentRoom).Emit(
- "room-user-change",
- otherClients,
- )
- }
- })
- }
- })
- socket.On("disconnect", func(datas ...any) {
- socket.RemoveAllListeners("")
- socket.Disconnect(true)
- })
- })
- return ioo
- }
- func waitForShutdown(ioo *socketio.Server) {
- exit := make(chan struct{})
- SignalC := make(chan os.Signal)
- signal.Notify(SignalC, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
- go func() {
- for s := range SignalC {
- switch s {
- case os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
- close(exit)
- return
- }
- }
- }()
- <-exit
- ioo.Close(nil)
- os.Exit(0)
- fmt.Println("Shutting down...")
- // TODO(patwie): Close other resources
- os.Exit(0)
- }
- func main() {
- // Define a log level flag
- logLevel := flag.String("loglevel", "info", "Set the logging level: debug, info, warn, error, fatal, panic")
- flag.Parse()
- // Set the log level
- level, err := logrus.ParseLevel(*logLevel)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Invalid log level: %v\n", err)
- os.Exit(1)
- }
- logrus.SetLevel(level)
- documentStore := stores.GetStore() // Make sure this is well-defined in your "stores" package
- r := setupRouter(documentStore)
- ioo := setupSocketIO()
- r.Handle("/socket.io/", ioo.ServeHandler(nil))
- r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
- _, err := w.Write([]byte("pong"))
- if err != nil {
- panic(err)
- }
- })
- r.Mount("/", handleUI())
- addr := ":3002"
- logrus.WithField("addr", addr).Info("starting server")
- go func() {
- if err := http.ListenAndServe(addr, r); err != nil {
- logrus.WithField("event", "start server").Fatal(err)
- }
- }()
- logrus.Debug("Server is running in the background")
- waitForShutdown(ioo)
- }
|