nmdc-webfrontend/main.go

284 lines
6.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"code.ivysaur.me/libnmdc"
"github.com/googollee/go-socket.io"
)
const (
VERSION = `nmdc-webfrontend/1.1.2`
)
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+"<br>")
so.Emit("sys", "Enter a name to connect as (or name:pass for a registered nick)")
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()
}