package main import ( "bytes" "database/sql" "encoding/base64" "encoding/json" "flag" "fmt" "log" "net/http" "os" "slices" "strconv" "strings" "sync" "time" "github.com/gorilla/websocket" _ "github.com/mattn/go-sqlite3" ) const CONFIGFILE = "./config.json" type Client struct { Conn *websocket.Conn Name string Mu sync.Mutex } type Server struct { Mu sync.Mutex Clients []*Client Db *sql.DB Cef *CEF } type CEF struct { Conn *websocket.Conn FromServer chan string ToServer chan string Kill chan error } type Message struct { Type string `json:"type"` User string `json:"user"` Content string `json:"content"` } type Config struct { Listen string `json:"listen"` Webhook string `json:"webhook"` Cef struct { Channel string `json:"channel"` Username string `json:"username"` Password string `json:"password"` } `json:"cef"` Users []string `json:"users"` } var config Config var server = Server{ Clients: make([]*Client, 0), } func (s *Server) KillClient(client *Client) { s.Mu.Lock() defer s.Mu.Unlock() for i, _ := range s.Clients { if client == s.Clients[i] { s.Clients = append(s.Clients[:i], s.Clients[i+1:]...) return } } } func (c *Client) Send(struc any) { c.Mu.Lock() defer c.Mu.Unlock() err := c.Conn.WriteJSON(struc) if err != nil { server.KillClient(c) } } func Webhook(message Message) { jsonValue, _ := json.Marshal(struct { Content string `json:"content"` Username string `json:"username"` }{ Content: message.Content, Username: message.User, }) _, err := http.Post(config.Webhook, "application/json", bytes.NewBuffer(jsonValue)) if err != nil { log.Println("webhook", err) } } func (s *Server) BroadcastMessage(from string, content string) { message := Message{ Type: "message", User: from, Content: content, } for _, c := range s.Clients { c.Send(message) } tx, err := s.Db.Begin() stmt, err := tx.Prepare("insert into messages(`type`, `user`, `content`) values(?, ?, ?)") if err != nil { log.Fatal(err) return } _, err = stmt.Exec("message", from, content) if err != nil { log.Println(stmt) return } tx.Commit() go Webhook(message) go s.CEFSend(message) } func (s *Server) CEFSend(message Message) { msg := fmt.Sprintf("NPC %s %s :%s", config.Cef.Channel, message.User, message.Content) select { case s.Cef.ToServer <- msg: default: } } func (s *Server) SendHistory(client *Client) { // Blast rows, err := s.Db.Query("SELECT `user`, `content` FROM messages ORDER BY rowid ASC") if err != nil { log.Println(err) return } defer rows.Close() var user, content string for rows.Next() { err = rows.Scan(&user, &content) if err != nil { log.Panicln(err) } client.Send(Message{ Type: "history", User: user, Content: content, }) } client.Send(Message{ Type: "ready", User: strconv.Itoa(len(server.Clients)), Content: "Welcome back, " + client.Name, }) } func loadConfig() { data, err := os.ReadFile(CONFIGFILE) if err != nil { log.Panicln("Could not read config, ", err) } err = json.Unmarshal(data, &config) if err != nil { log.Panicln("Could not load config, ", err) } } func watchConfig() { initialStat, _ := os.Stat(CONFIGFILE) for { stat, _ := os.Stat(CONFIGFILE) if stat.Size() != initialStat.Size() || stat.ModTime() != initialStat.ModTime() { loadConfig() } time.Sleep(1 * time.Second) } } func (c *CEF) Send(m string) { log.Println("[CEF-O]", m) err := c.Conn.WriteMessage(websocket.TextMessage, []byte(m)) if err != nil { log.Println("[CEF-S]", err) c.Kill <- err return } } func (c *CEF) Loop() { for { _, message, err := c.Conn.ReadMessage() log.Println("[CEF]", string(message), err) if err != nil { c.Kill <- err return } c.FromServer <- string(message) } } func BasicCef() { c, _, err := websocket.DefaultDialer.Dial("wss://cef.icu/chat", nil) defer c.Close() if err != nil { return } cef := &CEF{ Conn: c, FromServer: make(chan string), ToServer: make(chan string), Kill: make(chan error), } defer close(cef.FromServer) defer close(cef.ToServer) defer close(cef.Kill) server.Cef = cef cef.Send("CAP REQ :account-notify account-tag away-notify batch chghost cef/extended-names draft/chathistory draft/multiline draft/event-playback draft/relaymsg echo-message extended-join invite-notify labeled-response message-tags multi-prefix sasl server-time setname userhost-in-names") cef.Send("NICK " + config.Cef.Username) cef.Send("USER " + config.Cef.Username + " . . :cool dude") cef.Send("AUTHENTICATE PLAIN") auth := fmt.Sprintf("%s\000%s\000%s", config.Cef.Username, config.Cef.Username, config.Cef.Password) cef.Send("AUTHENTICATE " + base64.StdEncoding.EncodeToString([]byte(auth))) cef.Send("CAP END") // cef.Send("JOIN " + config.Cef.Channel) go cef.Loop() for { select { case msg := <-cef.FromServer: split := strings.Split(msg, " ") if len(split) > 2 { if split[1] == "PING" { cef.Send("PONG " + split[2]) } } case msg := <-cef.ToServer: cef.Send(msg) case killError := <-cef.Kill: log.Println("[CEF]", killError) break } } } var upgrader = websocket.Upgrader{} // use default options func comm(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Print("upgrade:", err) return } defer c.Close() var client = &Client{ Conn: c, Name: "", } setup := false var inbound struct { Msg string `json:"msg"` } for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) break } err = json.Unmarshal(message, &inbound) if err != nil { log.Println("unmarshal", err) return } log.Printf("recv: %s", message) if !setup { if slices.Contains(config.Users, inbound.Msg) { server.Mu.Lock() server.Clients = append(server.Clients, client) server.Mu.Unlock() defer server.KillClient(client) setup = true client.Name = inbound.Msg server.SendHistory(client) } else { err := c.WriteJSON(Message{ Type: "error", User: "", Content: "LANCER NOT FOUND", }) if err != nil { log.Println("writejson", err) return } } } else { server.BroadcastMessage(client.Name, inbound.Msg) } } } func CefDaemon() { for { BasicCef() time.Sleep(60) } } func main() { loadConfig() go watchConfig() var addr = flag.String("addr", config.Listen, "http service address") db, err := sql.Open("sqlite3", "./messages.db") if err != nil { log.Fatal(err) } server.Db = db db.Exec("CREATE TABLE messages(`type` text, `user` text, `content` text)") defer db.Close() go CefDaemon() flag.Parse() log.SetFlags(0) fs := http.FileServer(http.Dir("./assets")) http.Handle("/", fs) http.HandleFunc("/comm", comm) log.Fatal(http.ListenAndServe(*addr, nil)) }