A web interface to an NMDC hub https://code.ivysaur.me/nmdc-webfrontend/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.go 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "log"
  7. "mime"
  8. "net/http"
  9. "path/filepath"
  10. "code.ivysaur.me/libnmdc"
  11. "github.com/googollee/go-socket.io"
  12. )
  13. var VERSION string = `nmdc-webfrontend/devel-unreleased`
  14. type App struct {
  15. cfg *Config
  16. }
  17. func NewApp(ConfigFilePath string) (*App, error) {
  18. cfgData, err := ioutil.ReadFile(ConfigFilePath)
  19. if err != nil {
  20. return nil, err
  21. }
  22. cfg := Config{}
  23. err = json.Unmarshal(cfgData, &cfg)
  24. if err != nil {
  25. return nil, err
  26. }
  27. return &App{cfg: &cfg}, nil
  28. }
  29. type UserMessageStruct struct {
  30. User string `json:"user"`
  31. Message string `json:"message,omitempty"`
  32. }
  33. func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan struct{}) {
  34. log.Printf("[%s] Connecting to hub\n", so.Id())
  35. selfUser := libnmdc.NewUserInfo(Nick)
  36. selfUser.ClientTag = this.cfg.Hub.Tag
  37. selfUser.ClientVersion = libnmdc.DEFAULT_CLIENT_VERSION
  38. url := this.cfg.Hub.Address
  39. if this.cfg.Hub.Port == 0 {
  40. url = fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)
  41. }
  42. hco := libnmdc.HubConnectionOptions{
  43. Address: libnmdc.HubAddress(url),
  44. Self: selfUser,
  45. NickPassword: Pass,
  46. }
  47. hubEvents := make(chan libnmdc.HubEvent, 10)
  48. hub := libnmdc.ConnectAsync(&hco, hubEvents)
  49. defer func() {
  50. hub.Disconnect()
  51. so.Emit("disconnect") // necessary? https://github.com/googollee/go-socket.io/issues/117
  52. log.Printf("[%s] Leaving worker\n", so.Id())
  53. }()
  54. // Register after-connected SIO handlers
  55. so.On("pub", func(data map[string]string) {
  56. hub.SayPublic(data["message"])
  57. })
  58. so.On("priv", func(data map[string]string) {
  59. hub.SayPrivate(data["user"], data["message"])
  60. })
  61. so.On("raw", func(data map[string]string) {
  62. hub.SayRaw(data["message"])
  63. })
  64. // Loop hub connection
  65. serveUserInfo := func(nick string) {
  66. props := ""
  67. hub.Users(func(users *map[string]libnmdc.UserInfo) error {
  68. uinfo, ok := (*users)[nick]
  69. if !ok {
  70. return nil // just skip
  71. }
  72. bProps, err := json.Marshal(uinfo)
  73. if err != nil {
  74. return nil // just skip
  75. }
  76. props = string(bProps)
  77. return nil
  78. })
  79. // 'Message' is a json-encoded param with user properties
  80. if len(props) > 0 {
  81. so.Emit("info", UserMessageStruct{User: nick, Message: props})
  82. }
  83. }
  84. for {
  85. select {
  86. case hev, ok := <-hubEvents:
  87. if !ok {
  88. log.Printf("[%s] hub chan closed\n", so.Id())
  89. return // abandon
  90. }
  91. switch hev.EventType {
  92. case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB:
  93. so.Emit("sys", hev.Message)
  94. case libnmdc.EVENT_PUBLIC:
  95. so.Emit("pub", UserMessageStruct{User: hev.Nick, Message: hev.Message})
  96. case libnmdc.EVENT_PRIVATE:
  97. so.Emit("priv", UserMessageStruct{User: hev.Nick, Message: hev.Message})
  98. case libnmdc.EVENT_USER_UPDATED_INFO:
  99. serveUserInfo(hev.Nick)
  100. case libnmdc.EVENT_USER_JOINED:
  101. so.Emit("join", UserMessageStruct{User: hev.Nick})
  102. serveUserInfo(hev.Nick)
  103. case libnmdc.EVENT_USER_PART:
  104. so.Emit("part", UserMessageStruct{User: hev.Nick})
  105. case libnmdc.EVENT_CONNECTION_STATE_CHANGED:
  106. if hev.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED {
  107. log.Printf("[%s] Connected to hub\n", so.Id())
  108. so.Emit("hello")
  109. } else if hev.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED {
  110. so.Emit("close")
  111. } else {
  112. so.Emit("sys", hev.StateChange.String())
  113. }
  114. case libnmdc.EVENT_HUBNAME_CHANGED:
  115. so.Emit("hubname", hev.Nick)
  116. case libnmdc.EVENT_USERCOMMAND:
  117. so.Emit("usercommand", map[string]interface{}{
  118. "type": hev.UserCommand.Type,
  119. "context": hev.UserCommand.Context,
  120. "title": hev.UserCommand.Message,
  121. "raw": hev.UserCommand.Command,
  122. })
  123. }
  124. case <-done:
  125. log.Printf("[%s] done chan closed\n", so.Id())
  126. return // abandon
  127. }
  128. }
  129. }
  130. func (this *App) SocketIOServer(so socketio.Socket) {
  131. log.Printf("[%s] Client connected", so.Id())
  132. so.Emit("cls")
  133. so.Emit("hubname", this.cfg.Web.Title)
  134. so.Emit("raw", this.cfg.App.MotdHTML+"<br>")
  135. so.Emit("sys", "Enter a name to connect as (or name:pass for a registered nick)")
  136. if len(this.cfg.App.ContentedServer) > 0 {
  137. so.Emit("contented", this.cfg.App.ContentedServer)
  138. }
  139. doneChan := make(chan struct{}, 0)
  140. so.On("hello", func(data map[string]string) {
  141. log.Printf("[%s] Connection request", so.Id())
  142. go this.HubWorker(data["nick"], data["pass"], so, doneChan)
  143. })
  144. so.On("disconnection", func() { // n.b. not 'disconnect' (??)
  145. log.Printf("[%s] Client dropped", so.Id())
  146. close(doneChan)
  147. })
  148. }
  149. func (this *App) customFaviconHandler(w http.ResponseWriter, r *http.Request) {
  150. http.ServeFile(w, r, "favicon.ico")
  151. }
  152. func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) {
  153. fileName := r.URL.Path[1:]
  154. if fileName == "" {
  155. fileName = "index.htm"
  156. }
  157. data, err := Asset(fileName)
  158. if err != nil {
  159. w.WriteHeader(404)
  160. return
  161. }
  162. w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(fileName)))
  163. w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
  164. w.Write(data)
  165. }
  166. func (this *App) RunServer() {
  167. // Inner mux {{
  168. innerMux := http.NewServeMux()
  169. // Socket.io handler
  170. server, err := socketio.NewServer(nil)
  171. if err != nil {
  172. log.Fatal(err)
  173. }
  174. server.On("connection", this.SocketIOServer)
  175. server.On("error", func(so socketio.Socket, err error) {
  176. log.Println("error:", err)
  177. })
  178. innerMux.Handle("/socket.io/", server)
  179. // Custom favicon handler
  180. if this.cfg.Web.CustomFavicon {
  181. innerMux.HandleFunc("/favicon.ico", this.customFaviconHandler)
  182. }
  183. // Asset handler
  184. if this.cfg.Web.ExternalWebroot {
  185. innerMux.Handle("/", http.FileServer(http.Dir("client")))
  186. } else {
  187. innerMux.HandleFunc("/", this.StaticRequestHandler)
  188. }
  189. // }}
  190. // Wrapper mux {{
  191. outerMux := http.NewServeMux()
  192. outerMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  193. w.Header().Set("Server", VERSION)
  194. innerMux.ServeHTTP(w, r)
  195. })
  196. // }}
  197. // Listen and serve
  198. bindAddr := fmt.Sprintf("%s:%d", this.cfg.Web.BindTo, this.cfg.Web.Port)
  199. log.Printf("Serving at %s...", bindAddr)
  200. log.Fatal(http.ListenAndServe(bindAddr, outerMux))
  201. }
  202. func main() {
  203. log.Println(VERSION)
  204. a, err := NewApp("nmdc-webfrontend.conf")
  205. if err != nil {
  206. log.Fatal(err.Error())
  207. return
  208. }
  209. a.RunServer()
  210. }