From ba287dd2e80afe9c9dc58f4f7a3cf2c5ff2d2174 Mon Sep 17 00:00:00 2001 From: min Date: Thu, 26 Feb 2026 08:23:30 -0500 Subject: [PATCH] Initial implementation --- main.go | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 193 insertions(+), 17 deletions(-) diff --git a/main.go b/main.go index c4e22cd..b7bcdaa 100644 --- a/main.go +++ b/main.go @@ -2,39 +2,146 @@ package main import ( "context" + "encoding/json" + "fmt" "log" "net/http" "os" + "strconv" + "strings" "sync" + "time" + "unicode" + "unicode/utf8" + "github.com/diamondburned/arikawa/v3/api" + "github.com/diamondburned/arikawa/v3/discord" "github.com/diamondburned/arikawa/v3/gateway" "github.com/diamondburned/arikawa/v3/session" "github.com/gorilla/websocket" "github.com/joho/godotenv" ) +const ( + writeWait = 10 * time.Second + pingPeriod = 50 * time.Second +) + var upgrader = websocket.Upgrader{} +type mtag string + +const ( + mtagOnDiscordChat mtag = "on_discord_chat" + mtagOnMinecraftChat mtag = "on_minecraft_chat" + mtagNothing mtag = "nothing" +) + +type message struct { + Tag mtag + OnDiscordChat *onDiscordChatMessage + OnMinecraftChat *onMinecraftChatMessage +} + +type onDiscordChatMessage struct { + SenderNickname string + SenderUsername string + Content string +} + +type onMinecraftChatMessage struct { + Sender string + Content string +} + +type skyBase struct { + mu sync.RWMutex + luaOutboxes []chan message + goInbox chan message +} + +func (sb *skyBase) luaDispatch(m message) { + sb.mu.RLock() + defer sb.mu.RUnlock() + for _, ch := range sb.luaOutboxes { + ch <- m + } +} + +var base *skyBase + func handleIndex(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello from skybase")) } func handleChat(w http.ResponseWriter, r *http.Request) { - c, err := upgrader.Upgrade(w, r, nil) + ws, err := upgrader.Upgrade(w, r, nil) if err != nil { return } - defer c.Close() + + outbox := make(chan message, 10) + base.mu.Lock() + base.luaOutboxes = append(base.luaOutboxes, outbox) + base.mu.Unlock() + + defer func() { + ws.Close() + base.mu.Lock() + for i, ch := range base.luaOutboxes { + if ch != outbox { + continue + } + base.luaOutboxes[i] = base.luaOutboxes[len(base.luaOutboxes)-1] + base.luaOutboxes = base.luaOutboxes[:len(base.luaOutboxes)-1] + close(ch) + } + base.mu.Unlock() + }() + + pingTicker := time.NewTicker(pingPeriod) + defer pingTicker.Stop() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + defer cancel() + for { + _, data, err := ws.ReadMessage() + if err != nil { + return + } + + var msg message + if err = json.Unmarshal(data, &msg); err != nil { + return + } + + base.goInbox <- msg + } + }() for { - mt, message, err := c.ReadMessage() - if err != nil { - break - } + select { + case <-pingTicker.C: + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err = ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + case msg := <-outbox: + data, err := json.Marshal(msg) + if err != nil { + return + } - err = c.WriteMessage(mt, message) - if err != nil { - break + log.Println("sending", string(data)) + + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err = ws.WriteMessage(websocket.TextMessage, data); err != nil { + return + } + case <-ctx.Done(): + return } } } @@ -47,25 +154,95 @@ func main() { if token == "" { log.Fatal("missing bot token") } + channelID, err := strconv.ParseUint(os.Getenv("CHANNEL_ID"), 10, 64) + if err != nil { + log.Fatal("missing channel ID") + } httpListen := os.Getenv("HTTP_LISTEN") if httpListen == "" { log.Fatal("missing http listen address") } + // init base + base = &skyBase{ + luaOutboxes: []chan message{}, + goInbox: make(chan message, 10), + } + // init session - s := session.New("Bot " + token) + id := gateway.DefaultIdentifier("Bot " + token) + id.Presence = &gateway.UpdatePresenceCommand{ + Status: discord.DoNotDisturbStatus, + Activities: []discord.Activity{{ + Type: discord.CustomActivity, + Name: "whatever", + State: "PQJDfhkjldalsljkfhasd", + }}, + } + s := session.NewWithIdentifier(id) s.AddHandler(func(c *gateway.ReadyEvent) { u, err := s.Me() if err != nil { log.Fatal(err) } log.Println("started as", u.Username) + + channelID := discord.ChannelID(channelID) + + for msg := range base.goInbox { + switch msg.Tag { + case mtagOnMinecraftChat: + p := msg.OnMinecraftChat + if p == nil { + continue + } + + if p.Content == "" { + continue + } + + filtered := strings.ReplaceAll(p.Content, "`", "\\`") + filtered = strings.ReplaceAll(filtered, "|", "\\|") + filtered = strings.ReplaceAll(filtered, "*", "\\*") + filtered = strings.ReplaceAll(filtered, "_", "\\_") + filtered = strings.ReplaceAll(filtered, "#", "\\#") + filtered = strings.ReplaceAll(filtered, "[", "\\[") + filtered = strings.ReplaceAll(filtered, "-", "\\-") + filtered = strings.ReplaceAll(filtered, ".", "\\.") + filtered = strings.ReplaceAll(filtered, ">", "\\>") + log.Printf("<%s> %s\n", p.Sender, p.Content) + + r, _ := utf8.DecodeRuneInString(p.Content) + isAlphanumeric := unicode.IsLetter(r) || unicode.IsNumber(r) + if !isAlphanumeric { + continue + } + + s.SendMessageComplex(channelID, api.SendMessageData{ + Content: fmt.Sprintf("<%s> %s", p.Sender, filtered), + AllowedMentions: &api.AllowedMentions{}, + Flags: discord.SuppressEmbeds | discord.SuppressNotifications, + }) + case mtagNothing: + default: + return + } + } }) s.AddHandler(func(c *gateway.MessageCreateEvent) { if c.Author.Bot { return } - log.Println(c.Author.Username, "sent", c.Content) + + log.Printf("[@%s] %s\n", c.Author.DisplayName, c.Content) + base.luaDispatch(message{ + Tag: mtagOnDiscordChat, + OnDiscordChat: &onDiscordChatMessage{ + SenderUsername: c.Author.Username, + SenderNickname: c.Author.DisplayName, + Content: c.Content, + }, + }) }) s.AddIntents(gateway.IntentGuildMessages) @@ -74,17 +251,16 @@ func main() { http.HandleFunc("/chat", handleChat) // begin - var wg sync.WaitGroup - wg.Go(func() { + go func() { if err := s.Connect(context.Background()); err != nil { log.Fatal(err) } defer s.Close() - }) - wg.Go(func() { + }() + go func() { http.ListenAndServe(httpListen, nil) - }) + }() // wait - wg.Wait() + select {} }