nmdc-webfrontend/main.go

273 lines
6.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"mime"
"net/http"
"path/filepath"
"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
url := this.cfg.Hub.Address
if this.cfg.Hub.Port == 0 {
url = fmt.Sprintf("%s:%d", this.cfg.Hub.Address, this.cfg.Hub.Port)
}
hco := libnmdc.HubConnectionOptions{
Address: libnmdc.HubAddress(url),
Self: selfUser,
NickPassword: Pass,
}
hubEvents := make(chan libnmdc.HubEvent, 10)
hub := libnmdc.ConnectAsync(&hco, hubEvents)
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 := <-hubEvents:
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+"<br>")
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
}
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(fileName)))
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
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()
}