main.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package main
  2. import (
  3. "embed"
  4. _ "embed"
  5. "excalidraw-complete/core"
  6. "excalidraw-complete/handlers/api/documents"
  7. "excalidraw-complete/handlers/api/firebase"
  8. "excalidraw-complete/stores"
  9. "flag"
  10. "fmt"
  11. "io"
  12. "io/fs"
  13. "net/http"
  14. "os"
  15. "os/signal"
  16. "strings"
  17. "syscall"
  18. "github.com/sirupsen/logrus"
  19. "github.com/go-chi/chi/v5"
  20. "github.com/go-chi/chi/v5/middleware"
  21. "github.com/go-chi/cors"
  22. "github.com/zishang520/engine.io/v2/types"
  23. "github.com/zishang520/engine.io/v2/utils"
  24. socketio "github.com/zishang520/socket.io/v2/socket"
  25. )
  26. type (
  27. UserToFollow struct {
  28. SocketId string `json:"socketId"`
  29. Username string `json:"username"`
  30. }
  31. OnUserFollowedPayload struct {
  32. UserToFollow UserToFollow `json:"userToFollow"`
  33. Action string `json:"action"` // "FOLLOW" | "UNFOLLOW"
  34. }
  35. )
  36. //go:embed all:frontend
  37. var assets embed.FS
  38. func handleUI() http.Handler {
  39. sub, err := fs.Sub(assets, "frontend")
  40. if err != nil {
  41. panic(err)
  42. }
  43. // Let's hot-patch all calls to firebase DB
  44. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  45. originalPath := r.URL.Path
  46. originalPath = strings.TrimPrefix(originalPath, "/")
  47. // Redirect "/" to "index.html"
  48. if originalPath == "" {
  49. originalPath = "index.html"
  50. }
  51. file, err := sub.Open(originalPath)
  52. if err != nil {
  53. http.Error(w, "File not found", http.StatusNotFound)
  54. return
  55. }
  56. defer file.Close()
  57. fileContent, err := io.ReadAll(file)
  58. if err != nil {
  59. http.Error(w, "Error reading file", http.StatusInternalServerError)
  60. return
  61. }
  62. modifiedContent := strings.ReplaceAll(string(fileContent), "firestore.googleapis.com", "localhost:3002")
  63. modifiedContent = strings.ReplaceAll(modifiedContent, "ssl=!0", "ssl=0")
  64. modifiedContent = strings.ReplaceAll(modifiedContent, "ssl:!0", "ssl:0")
  65. // Set the correct Content-Type based on the file extension
  66. contentType := http.DetectContentType([]byte(modifiedContent))
  67. switch {
  68. case strings.HasSuffix(originalPath, ".js"):
  69. contentType = "application/javascript"
  70. case strings.HasSuffix(originalPath, ".html"):
  71. contentType = "text/html"
  72. case strings.HasSuffix(originalPath, ".css"):
  73. contentType = "text/css"
  74. case strings.HasSuffix(originalPath, ".wasm"):
  75. contentType = "application/wasm"
  76. case strings.HasSuffix(originalPath, ".tsx"):
  77. contentType = "text/typescript"
  78. case strings.HasSuffix(originalPath, ".png"):
  79. contentType = "image/png"
  80. case strings.HasSuffix(originalPath, ".woff2"):
  81. contentType = "font/woff2"
  82. }
  83. // Serve the modified content
  84. w.Header().Set("Content-Type", contentType)
  85. _, err = w.Write([]byte(modifiedContent))
  86. if err != nil {
  87. http.Error(w, "Error serving file", http.StatusInternalServerError)
  88. return
  89. }
  90. return
  91. })
  92. }
  93. func setupRouter(documentStore core.DocumentStore) *chi.Mux {
  94. r := chi.NewRouter()
  95. r.Use(middleware.Logger)
  96. r.Use(cors.Handler(cors.Options{
  97. AllowedOrigins: []string{"https://*", "http://*"},
  98. AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
  99. AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Content-Length", "X-CSRF-Token", "Token", "session", "Origin", "Host", "Connection", "Accept-Encoding", "Accept-Language", "X-Requested-With"},
  100. AllowCredentials: true,
  101. MaxAge: 300, // Maximum value not ignored by any of major browsers
  102. }))
  103. r.Route("/v1/projects/{project_id}/databases/{database_id}", func(r chi.Router) {
  104. r.Post("/documents:commit", firebase.HandleBatchCommit())
  105. r.Post("/documents:batchGet", firebase.HandleBatchGet())
  106. })
  107. r.Route("/api/v2", func(r chi.Router) {
  108. r.Post("/post/", documents.HandleCreate(documentStore))
  109. r.Route("/{id}", func(r chi.Router) {
  110. r.Get("/", documents.HandleGet(documentStore))
  111. })
  112. })
  113. return r
  114. }
  115. func setupSocketIO() *socketio.Server {
  116. opts := socketio.DefaultServerOptions()
  117. opts.SetMaxHttpBufferSize(5000000)
  118. opts.SetPath("/socket.io")
  119. opts.SetAllowEIO3(true)
  120. opts.SetCors(&types.Cors{
  121. Origin: "*",
  122. Credentials: true,
  123. })
  124. ioo := socketio.NewServer(nil, opts)
  125. ioo.On("connection", func(clients ...any) {
  126. socket := clients[0].(*socketio.Socket)
  127. me := socket.Id()
  128. myRoom := socketio.Room(me)
  129. ioo.To(myRoom).Emit("init-room")
  130. utils.Log().Println("init room ", myRoom)
  131. socket.On("join-room", func(datas ...any) {
  132. room := socketio.Room(datas[0].(string))
  133. utils.Log().Printf("Socket %v has joined %v\n", me, room)
  134. socket.Join(room)
  135. ioo.In(room).FetchSockets()(func(usersInRoom []*socketio.RemoteSocket, _ error) {
  136. if len(usersInRoom) <= 1 {
  137. ioo.To(myRoom).Emit("first-in-room")
  138. } else {
  139. utils.Log().Printf("emit new user %v in room %v\n", me, room)
  140. socket.Broadcast().To(room).Emit("new-user", me)
  141. }
  142. // Inform all clients by new users.
  143. newRoomUsers := []socketio.SocketId{}
  144. for _, user := range usersInRoom {
  145. newRoomUsers = append(newRoomUsers, user.Id())
  146. }
  147. utils.Log().Println(" room ", room, " has users ", newRoomUsers)
  148. ioo.In(room).Emit(
  149. "room-user-change",
  150. newRoomUsers,
  151. )
  152. })
  153. })
  154. socket.On("server-broadcast", func(datas ...any) {
  155. roomID := datas[0].(string)
  156. utils.Log().Printf(" user %v sends update to room %v\n", me, roomID)
  157. socket.Broadcast().To(socketio.Room(roomID)).Emit("client-broadcast", datas[1], datas[2])
  158. })
  159. socket.On("server-volatile-broadcast", func(datas ...any) {
  160. roomID := datas[0].(string)
  161. utils.Log().Printf(" user %v sends volatile update to room %v\n", me, roomID)
  162. socket.Volatile().Broadcast().To(socketio.Room(roomID)).Emit("client-broadcast", datas[1], datas[2])
  163. })
  164. socket.On("user-follow", func(datas ...any) {
  165. // TODO()
  166. })
  167. socket.On("disconnecting", func(datas ...any) {
  168. for _, currentRoom := range socket.Rooms().Keys() {
  169. ioo.In(currentRoom).FetchSockets()(func(usersInRoom []*socketio.RemoteSocket, _ error) {
  170. otherClients := []socketio.SocketId{}
  171. utils.Log().Printf("disconnecting %v from room %v\n", me, currentRoom)
  172. for _, userInRoom := range usersInRoom {
  173. if userInRoom.Id() != me {
  174. otherClients = append(otherClients, userInRoom.Id())
  175. }
  176. }
  177. if len(otherClients) > 0 {
  178. utils.Log().Printf("leaving user, room %v has users %v\n", currentRoom, otherClients)
  179. ioo.In(currentRoom).Emit(
  180. "room-user-change",
  181. otherClients,
  182. )
  183. }
  184. })
  185. }
  186. })
  187. socket.On("disconnect", func(datas ...any) {
  188. socket.RemoveAllListeners("")
  189. socket.Disconnect(true)
  190. })
  191. })
  192. return ioo
  193. }
  194. func waitForShutdown(ioo *socketio.Server) {
  195. exit := make(chan struct{})
  196. SignalC := make(chan os.Signal)
  197. signal.Notify(SignalC, os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
  198. go func() {
  199. for s := range SignalC {
  200. switch s {
  201. case os.Interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
  202. close(exit)
  203. return
  204. }
  205. }
  206. }()
  207. <-exit
  208. ioo.Close(nil)
  209. os.Exit(0)
  210. fmt.Println("Shutting down...")
  211. // TODO(patwie): Close other resources
  212. os.Exit(0)
  213. }
  214. func main() {
  215. // Define a log level flag
  216. logLevel := flag.String("loglevel", "info", "Set the logging level: debug, info, warn, error, fatal, panic")
  217. flag.Parse()
  218. // Set the log level
  219. level, err := logrus.ParseLevel(*logLevel)
  220. if err != nil {
  221. fmt.Fprintf(os.Stderr, "Invalid log level: %v\n", err)
  222. os.Exit(1)
  223. }
  224. logrus.SetLevel(level)
  225. documentStore := stores.GetStore() // Make sure this is well-defined in your "stores" package
  226. r := setupRouter(documentStore)
  227. ioo := setupSocketIO()
  228. r.Handle("/socket.io/", ioo.ServeHandler(nil))
  229. r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
  230. _, err := w.Write([]byte("pong"))
  231. if err != nil {
  232. panic(err)
  233. }
  234. })
  235. r.Mount("/", handleUI())
  236. addr := ":3002"
  237. logrus.WithField("addr", addr).Info("starting server")
  238. go func() {
  239. if err := http.ListenAndServe(addr, r); err != nil {
  240. logrus.WithField("event", "start server").Fatal(err)
  241. }
  242. }()
  243. logrus.Debug("Server is running in the background")
  244. waitForShutdown(ioo)
  245. }