package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "strings" "code.ivysaur.me/libnmdc" "github.com/googollee/go-socket.io" ) var VERSION string = `nmdc-webfrontend/devel-unreleased` type App struct { cfg *Config } func NewApp(ConfigFilePath string) (*App, error) { cfgData, err := ioutil.ReadFile(ConfigFilePath) if err != nil { return nil, err } cfg := Config{} err = json.Unmarshal(cfgData, &cfg) if err != nil { return nil, err } return &App{cfg: &cfg}, nil } type UserMessageStruct struct { User string `json:"user"` Message string `json:"message,omitempty"` } func (this *App) HubWorker(Nick, Pass string, so socketio.Socket, done chan struct{}) { log.Printf("[%s] Connecting to hub\n", so.Id()) selfUser := libnmdc.NewUserInfo(Nick) selfUser.ClientTag = this.cfg.Hub.Tag selfUser.ClientVersion = libnmdc.DEFAULT_CLIENT_VERSION hco := libnmdc.HubConnectionOptions{ Address: libnmdc.HubAddress(fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)), Self: *selfUser, NickPassword: Pass, NumEventsToBuffer: 0, } hub := hco.Connect() defer func() { hub.Disconnect() so.Emit("disconnect") // necessary? https://github.com/googollee/go-socket.io/issues/117 log.Printf("[%s] Leaving worker\n", so.Id()) }() // Register after-connected SIO handlers so.On("pub", func(data map[string]string) { hub.SayPublic(data["message"]) }) so.On("priv", func(data map[string]string) { hub.SayPrivate(data["user"], data["message"]) }) so.On("raw", func(data map[string]string) { hub.SayRaw(data["message"]) }) // Loop hub connection serveUserInfo := func(nick string) { props := "" hub.Users(func(users *map[string]libnmdc.UserInfo) error { uinfo, ok := (*users)[nick] if !ok { return nil // just skip } bProps, err := json.Marshal(uinfo) if err != nil { return nil // just skip } props = string(bProps) return nil }) // 'Message' is a json-encoded param with user properties if len(props) > 0 { so.Emit("info", UserMessageStruct{User: nick, Message: props}) } } for { select { case hev, ok := <-hub.OnEvent: if !ok { log.Printf("[%s] hub chan closed\n", so.Id()) return // abandon } switch hev.EventType { case libnmdc.EVENT_SYSTEM_MESSAGE_FROM_CONN, libnmdc.EVENT_SYSTEM_MESSAGE_FROM_HUB: so.Emit("sys", hev.Message) case libnmdc.EVENT_PUBLIC: so.Emit("pub", UserMessageStruct{User: hev.Nick, Message: hev.Message}) case libnmdc.EVENT_PRIVATE: so.Emit("priv", UserMessageStruct{User: hev.Nick, Message: hev.Message}) case libnmdc.EVENT_USER_UPDATED_INFO: serveUserInfo(hev.Nick) case libnmdc.EVENT_USER_JOINED: so.Emit("join", UserMessageStruct{User: hev.Nick}) serveUserInfo(hev.Nick) case libnmdc.EVENT_USER_PART: so.Emit("part", UserMessageStruct{User: hev.Nick}) case libnmdc.EVENT_CONNECTION_STATE_CHANGED: if hev.StateChange == libnmdc.CONNECTIONSTATE_CONNECTED { log.Printf("[%s] Connected to hub\n", so.Id()) so.Emit("hello") } else if hev.StateChange == libnmdc.CONNECTIONSTATE_DISCONNECTED { so.Emit("close") } else { so.Emit("sys", hev.StateChange.String()) } case libnmdc.EVENT_HUBNAME_CHANGED: so.Emit("hubname", hev.Nick) case libnmdc.EVENT_USERCOMMAND: so.Emit("usercommand", map[string]interface{}{ "type": hev.UserCommand.Type, "context": hev.UserCommand.Context, "title": hev.UserCommand.Message, "raw": hev.UserCommand.Command, }) } case <-done: log.Printf("[%s] done chan closed\n", so.Id()) return // abandon } } } func (this *App) SocketIOServer(so socketio.Socket) { log.Printf("[%s] Client connected", so.Id()) so.Emit("cls") so.Emit("hubname", this.cfg.Web.Title) so.Emit("raw", this.cfg.App.MotdHTML+"
") so.Emit("sys", "Enter a name to connect as (or name:pass for a registered nick)") if len(this.cfg.App.ContentedServer) > 0 { so.Emit("contented", this.cfg.App.ContentedServer) } doneChan := make(chan struct{}, 0) so.On("hello", func(data map[string]string) { log.Printf("[%s] Connection request", so.Id()) go this.HubWorker(data["nick"], data["pass"], so, doneChan) }) so.On("disconnection", func() { // n.b. not 'disconnect' (??) log.Printf("[%s] Client dropped", so.Id()) close(doneChan) }) } func (this *App) customFaviconHandler(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "favicon.ico") } func (this *App) StaticRequestHandler(w http.ResponseWriter, r *http.Request) { fileName := r.URL.Path[1:] if fileName == "" { fileName = "index.htm" } data, err := Asset(fileName) if err != nil { w.WriteHeader(404) return } knownContentTypes := map[string]string{ ".htm": "text/html", ".png": "image/png", ".ico": "image/x-icon", // No CSS/JS since they're embedded in the HTML } foundMime := false for ext, mimeType := range knownContentTypes { if strings.HasSuffix(fileName, ext) { w.Header().Set("Content-Type", mimeType) foundMime = true break } } if !foundMime { w.Header().Set("Content-Type", "application/x-octet-stream") } dataInfo, _ := AssetInfo(fileName) w.Header().Set("Content-Length", fmt.Sprintf("%d", dataInfo.Size())) w.Write(data) } func (this *App) RunServer() { // Inner mux {{ innerMux := http.NewServeMux() // Socket.io handler server, err := socketio.NewServer(nil) if err != nil { log.Fatal(err) } server.On("connection", this.SocketIOServer) server.On("error", func(so socketio.Socket, err error) { log.Println("error:", err) }) innerMux.Handle("/socket.io/", server) // Custom favicon handler if this.cfg.Web.CustomFavicon { innerMux.HandleFunc("/favicon.ico", this.customFaviconHandler) } // Asset handler if this.cfg.Web.ExternalWebroot { innerMux.Handle("/", http.FileServer(http.Dir("client"))) } else { innerMux.HandleFunc("/", this.StaticRequestHandler) } // }} // Wrapper mux {{ outerMux := http.NewServeMux() outerMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Server", VERSION) innerMux.ServeHTTP(w, r) }) // }} // Listen and serve bindAddr := fmt.Sprintf("%s:%d", this.cfg.Web.BindTo, this.cfg.Web.Port) log.Printf("Serving at %s...", bindAddr) log.Fatal(http.ListenAndServe(bindAddr, outerMux)) } func main() { log.Println(VERSION) a, err := NewApp("nmdc-webfrontend.conf") if err != nil { log.Fatal(err.Error()) return } a.RunServer() }