main.go 7.3 KB

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