From 2ba9b1405f441e0e9b4b55782133a0bda4cace25 Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Sun, 9 Jul 2023 14:00:49 -0600 Subject: [PATCH 1/2] more work on message parsing --- commands/clear.go | 11 +++ commands/main.go | 5 ++ commands/send.go | 3 +- events/main.go | 9 +++ events/messages.go | 19 +++++ events/ready.go | 3 +- lib/messages.go | 198 ++++++++++++++++++++++++++++++++++++--------- main.go | 19 ++++- 8 files changed, 226 insertions(+), 41 deletions(-) create mode 100644 commands/clear.go create mode 100644 events/main.go diff --git a/commands/clear.go b/commands/clear.go new file mode 100644 index 0000000..6f5877b --- /dev/null +++ b/commands/clear.go @@ -0,0 +1,11 @@ +package commands + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func ClearCommand(session *discordgo.Session) { + fmt.Print("\n\r\033[H\033[2J") +} diff --git a/commands/main.go b/commands/main.go index 8665df0..916ff01 100644 --- a/commands/main.go +++ b/commands/main.go @@ -21,6 +21,11 @@ func Setup() { Run: HelpCommand, Description: "command help", } + + commandMap["c"] = Command{ + Run: ClearCommand, + Description: "clear", + } } func GetCommand(key string) (Command, bool) { diff --git a/commands/send.go b/commands/send.go index 2244678..fba8a53 100644 --- a/commands/send.go +++ b/commands/send.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "strings" + "unicode/utf8" "github.com/Cynosphere/comcord/lib" "github.com/Cynosphere/comcord/state" @@ -20,7 +21,7 @@ func SendMode(session *discordgo.Session) { state.SetInPrompt(true) - length := len(session.State.User.Username) + 2 + length := utf8.RuneCountInString(session.State.User.Username) + 2 curLength := state.GetNameLength() prompt := fmt.Sprintf("[%s]%s", session.State.User.Username, strings.Repeat(" ", (curLength - length) + 1)) diff --git a/events/main.go b/events/main.go new file mode 100644 index 0000000..9b6c63b --- /dev/null +++ b/events/main.go @@ -0,0 +1,9 @@ +package events + +import "github.com/bwmarrin/discordgo" + +func Setup(session *discordgo.Session) { + session.AddHandlerOnce(Ready) + session.AddHandler(MessageCreate) + session.AddHandler(MessageUpdate) +} diff --git a/events/messages.go b/events/messages.go index afbabb7..41912e7 100644 --- a/events/messages.go +++ b/events/messages.go @@ -30,5 +30,24 @@ func MessageCreate(session *discordgo.Session, msg *discordgo.MessageCreate) { } func MessageUpdate(session *discordgo.Session, msg *discordgo.MessageUpdate) { + if msg.Author.ID == session.State.User.ID { + return + } + channel, err := session.State.Channel(msg.ChannelID) + if err != nil { + return + } + + isDM := channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM + + if state.IsInPrompt() { + state.AddMessageToQueue(msg.Message) + } else { + lib.ProcessMessage(session, msg.Message, lib.MessageOptions{NoColor: state.HasNoColor()}) + } + + if isDM { + state.SetLastDM(msg.ChannelID) + } } diff --git a/events/ready.go b/events/ready.go index 91e2cc9..911c8d9 100644 --- a/events/ready.go +++ b/events/ready.go @@ -2,6 +2,7 @@ package events import ( "fmt" + "unicode/utf8" "github.com/Cynosphere/comcord/state" "github.com/bwmarrin/discordgo" @@ -11,7 +12,7 @@ import ( func Ready(session *discordgo.Session, event *discordgo.Ready) { fmt.Printf("\rLogged in as: %s\n\r", ansi.Color(fmt.Sprintf("%s (%s)", session.State.User.Username, session.State.User.ID), "yellow")) - state.SetNameLength(len(session.State.User.Username) + 2) + state.SetNameLength(utf8.RuneCountInString(session.State.User.Username) + 2) defaultGuild := state.GetConfigValue("defaultGuild") defaultChannel := state.GetConfigValue("defaultChannel") diff --git a/lib/messages.go b/lib/messages.go index 8913581..0febfc7 100644 --- a/lib/messages.go +++ b/lib/messages.go @@ -6,13 +6,15 @@ import ( "regexp" "strings" "time" + "unicode/utf8" "github.com/Cynosphere/comcord/state" "github.com/bwmarrin/discordgo" "github.com/mgutz/ansi" ) -var /*const*/ REGEX_CODEBLOCK = regexp.MustCompile(`(?i)\x60\x60\x60(?:([a-z0-9_+\-\.]+?)\n)?\n*([^\n].*?)\n*\x60\x60\x60`) +var REGEX_CODEBLOCK = regexp.MustCompile(`(?i)\x60\x60\x60(?:([a-z0-9_+\-\.]+?)\n)?\n*([^\n].*?)\n*\x60\x60\x60`) +var REGEX_EMOTE = regexp.MustCompile(`<(?:\x{200b}|&)?a?:(\w+):(\d+)>`) type MessageOptions struct { Content string @@ -33,18 +35,74 @@ type MessageOptions struct { } func FormatMessage(session *discordgo.Session, options MessageOptions) { - - // TODO: timestamps for pin and join + timestamp := options.Timestamp.Format("[15:04:05]") // TODO: history lines - nameLength := len(options.Name) + 2 + nameLength := utf8.RuneCountInString(options.Name) + 2 stateNameLength := state.GetNameLength() if nameLength > stateNameLength { state.SetNameLength(nameLength) + stateNameLength = nameLength } - // TODO: replies + if options.Reply != nil { + nameColor := "cyan+b" + if options.Bot { + nameColor = "yellow+b" + } + + headerLength := 6 + utf8.RuneCountInString(options.Reply.Author.Username) + + content, _ := options.Reply.ContentWithMoreMentionsReplaced(session) + replyContent := strings.ReplaceAll(content, "\n", " ") + + // TODO: markdown + replyContent = REGEX_EMOTE.ReplaceAllString(replyContent, ":$1:") + + attachmentCount := len(options.Reply.Attachments) + if attachmentCount > 0 { + attachmentPlural := "" + if attachmentCount > 1 { + attachmentPlural = "s" + } + + replyContent = strings.TrimSpace(replyContent + fmt.Sprintf(" <%d attachment%s>", attachmentCount, attachmentPlural)) + } + + stickerCount := len(options.Reply.StickerItems) + if stickerCount > 0 { + stickerPlural := "" + if stickerCount > 0 { + stickerPlural = "s" + } + + replyContent = strings.TrimSpace(replyContent + fmt.Sprintf(" <%d sticker%s>", stickerCount, stickerPlural)) + } + + length := headerLength + utf8.RuneCountInString(replyContent) + + replySymbol := " \u00bb " + if !options.NoColor { + replySymbol = ansi.Color(replySymbol, "white+b") + } + + name := fmt.Sprintf("[%s] ", options.Reply.Author.Username) + if !options.NoColor { + name = ansi.Color(name, nameColor) + } + + moreContent := "\u2026" + if !options.NoColor { + moreContent = ansi.Color(moreContent, "reset") + } + + if length > 79 { + replyContent = replyContent[:79 - headerLength] + moreContent + } + + fmt.Print(replySymbol, name, replyContent, "\n\r") + } if options.IsDump { if options.InHistory { @@ -55,7 +113,7 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) { wordsPlural := "" linesPlural := "" - if wordCount > 1 { + if wordCount > 1 { wordsPlural = "s" } if lineCount > 1 { @@ -64,12 +122,16 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) { str := fmt.Sprintf("<%s DUMPs in %d characters of %d word%s in %d line%s>", options.Name, len(options.Content), wordCount, wordsPlural, lineCount, linesPlural) - if options.NoColor { - fmt.Print(str) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") } + + fmt.Print(str + "\n\r") } } else { // TODO: markdown + content := options.Content + content = REGEX_EMOTE.ReplaceAllString(content, ":$1:") if options.IsDM { name := fmt.Sprintf("*%s*", options.Name) @@ -77,21 +139,40 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) { name = ansi.Color(name, "red+b") } - fmt.Printf("%s %s\x07\n\r", name, options.Content) - } else if len(options.Content) > 1 && - (strings.HasPrefix(options.Content, "*") && strings.HasSuffix(options.Content, "*") && !strings.HasPrefix(options.Content, "**") && !strings.HasSuffix(options.Content, "**")) || - (strings.HasPrefix(options.Content, "_") && strings.HasSuffix(options.Content, "_") && !strings.HasPrefix(options.Content, "__") && !strings.HasSuffix(options.Content, "__")) { - str := fmt.Sprintf("<%s %s>", options.Name, options.Content[1:len(options.Content)-1]) + fmt.Printf("%s %s\x07\n\r", name, content) + } else if utf8.RuneCountInString(content) > 1 && + (strings.HasPrefix(content, "*") && strings.HasSuffix(content, "*") && !strings.HasPrefix(content, "**") && !strings.HasSuffix(content, "**")) || + (strings.HasPrefix(content, "_") && strings.HasSuffix(content, "_") && !strings.HasPrefix(content, "__") && !strings.HasSuffix(content, "__")) { + str := fmt.Sprintf("<%s %s>", options.Name, content[1:len(content)-1]) - if options.NoColor { - fmt.Print(str + "\n\r") - } else { - fmt.Print(ansi.Color(str, "green+b") + "\n\r") + if !options.NoColor { + str = ansi.Color(str, "green+b") } + + fmt.Print(str + "\n\r") } else if options.IsJoin { - // TODO + channel, err := session.State.Channel(options.Channel) + if err != nil { + return + } + guild, err := session.State.Guild(channel.GuildID) + if err != nil { + return + } + + str := fmt.Sprintf("%s %s has joined %s", timestamp, options.Name, guild.Name) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + fmt.Print(str + "\n\r") } else if options.IsPin { - // TODO + str := fmt.Sprintf("%s %s pinned a message to this channel", timestamp, options.Name) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + fmt.Print(str + "\n\r") } else { nameColor := "cyan+b" if options.IsMention { @@ -105,9 +186,8 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) { name = ansi.Color(name, nameColor) } - // FIXME: where is this off by 4 actually from - padding := strings.Repeat(" ", int(math.Abs(float64(stateNameLength) - float64(nameLength) - 4))) - str := fmt.Sprintf("%s%s %s", name, padding, options.Content) + padding := strings.Repeat(" ", int(math.Abs(float64(stateNameLength) - float64(nameLength))) + 1) + str := name + padding + content if options.IsMention { str = str + "\x07" } @@ -115,9 +195,27 @@ func FormatMessage(session *discordgo.Session, options MessageOptions) { } } - // TODO: attachments + if len(options.Attachments) > 0 { + for _, attachment := range options.Attachments { + str := fmt.Sprintf("", attachment.URL) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } - // TODO: stickers + fmt.Print(str + "\n\r") + } + } + + if len(options.Stickers) > 0 { + for _, sticker := range options.Stickers { + str := fmt.Sprintf("", sticker.Name, sticker.ID) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + fmt.Print(str + "\n\r") + } + } // TODO: links @@ -162,6 +260,7 @@ func ProcessMessage(session *discordgo.Session, msg *discordgo.Message, options isPing := msg.MentionEveryone || hasMentionedRole || isDirectlyMentioned isDM := channel.Type == discordgo.ChannelTypeDM || channel.Type == discordgo.ChannelTypeGroupDM + isEdit := msg.EditedTimestamp != nil currentChannel := state.GetCurrentChannel() isCurrentChannel := currentChannel == msg.ChannelID @@ -182,20 +281,45 @@ func ProcessMessage(session *discordgo.Session, msg *discordgo.Message, options } content, _ := msg.ContentWithMoreMentionsReplaced(session) - options.Content = content - options.Name = msg.Author.Username - options.Channel = msg.ChannelID - options.Bot = msg.Author.Bot - options.Attachments = msg.Attachments - options.Stickers = msg.StickerItems - options.Reply = msg.ReferencedMessage - options.IsMention = isPing - options.IsDM = isDM - options.IsJoin = msg.Type == discordgo.MessageTypeGuildMemberJoin - options.IsPin = msg.Type == discordgo.MessageTypeChannelPinnedMessage - options.IsDump = REGEX_CODEBLOCK.MatchString(content) + if isEdit { + content = content + " (edited)" + } - FormatMessage(session, options) + isDump := REGEX_CODEBLOCK.MatchString(content) + + if strings.Index(content, "\n") > -1 && !isDump { + for _, line := range strings.Split(content, "\n") { + options.Content = line + options.Name = msg.Author.Username + options.Channel = msg.ChannelID + options.Bot = msg.Author.Bot + options.Attachments = msg.Attachments + options.Stickers = msg.StickerItems + options.Reply = msg.ReferencedMessage + options.IsMention = isPing + options.IsDM = isDM + options.IsJoin = msg.Type == discordgo.MessageTypeGuildMemberJoin + options.IsPin = msg.Type == discordgo.MessageTypeChannelPinnedMessage + options.IsDump = false + + FormatMessage(session, options) + } + } else { + options.Content = content + options.Name = msg.Author.Username + options.Channel = msg.ChannelID + options.Bot = msg.Author.Bot + options.Attachments = msg.Attachments + options.Stickers = msg.StickerItems + options.Reply = msg.ReferencedMessage + options.IsMention = isPing + options.IsDM = isDM + options.IsJoin = msg.Type == discordgo.MessageTypeGuildMemberJoin + options.IsPin = msg.Type == discordgo.MessageTypeChannelPinnedMessage + options.IsDump = isDump + + FormatMessage(session, options) + } } func ProcessQueue(session *discordgo.Session) { diff --git a/main.go b/main.go index 7fcae95..75b4082 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "runtime" "strings" "atomicgo.dev/keyboard" @@ -73,8 +74,22 @@ func main() { // TODO: dont set for user accounts(? never really tested if it matters) client.Identify.Intents = discordgo.IntentsAll - client.AddHandlerOnce(events.Ready) - client.AddHandler(events.MessageCreate) + if config["useMobile"] == "true" { + client.Identify.Properties = discordgo.IdentifyProperties{ + OS: "Android", + Browser: "Discord Android", + Device: "Pixel, raven", + } + } else { + // TODO: user account support + client.Identify.Properties = discordgo.IdentifyProperties{ + OS: runtime.GOOS, + Browser: "comcord", + Device: "comcord", + } + } + + events.Setup(client) err = client.Open() if err != nil { From f21c414f1b12d299e275f69e473aef1ae801d91f Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Mon, 10 Jul 2023 13:04:40 -0600 Subject: [PATCH 2/2] update readme --- README.md | 28 +++++++++++++++------------- commands/quit.go | 2 ++ main.go | 9 +++++---- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0315ce4..07d8591 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ TODO Go is more portable than Node.js ## TODO -- [ ] Send mode +- [x] Send mode - [ ] Commands - - [ ] Quit (q) + - [x] Quit (q) - [ ] Switch guilds (G) - [ ] Switch channels (g) - [ ] List online users in guild (w) @@ -25,6 +25,8 @@ Go is more portable than Node.js - Creation date, join date, ID, etc - [ ] Room history (r) - [ ] Extended room history (R) + - [ ] Peek (p) + - [ ] Cross-guild peek (P) - [ ] List channels (l) - [ ] List guilds (L) - [ ] Clear (c) @@ -33,20 +35,20 @@ Go is more portable than Node.js - [ ] AFK toggle (A) - [ ] Send DM (s) - [ ] Answer DM (a) -- [ ] Message Receiving +- [x] Message Receiving - Markdown styling - - [ ] Emotes + - [x] Emotes - [ ] Timestamp parsing - - [ ] Mentions parsing + - [x] Mentions parsing - [ ] Embeds in the style of commode's posted links - - [ ] Messages wrapped in `*`'s or `_`'s parsed as emotes - - [ ] Inline DMs to replicate commode's private messages - - [ ] Replies + - [x] Messages wrapped in `*`'s or `_`'s parsed as emotes + - [x] Inline DMs to replicate commode's private messages + - [x] Replies - [ ] Group DMs -- [ ] Message sending - - [ ] Puts incoming messages into queue whilst in send mode -- [ ] Configuration - - [ ] Write token from argv into rc file if rc file doesn't exist - - [ ] Default guild/channel +- [x] Message sending + - [x] Puts incoming messages into queue whilst in send mode +- [x] Configuration + - [x] Write token from argv into rc file if rc file doesn't exist + - [x] Default guild/channel - [ ] Threads - [ ] External rich presence when using bot accounts diff --git a/commands/quit.go b/commands/quit.go index eea1472..8c81036 100644 --- a/commands/quit.go +++ b/commands/quit.go @@ -1,12 +1,14 @@ package commands import ( + "fmt" "os" "github.com/bwmarrin/discordgo" ) func QuitCommand(session *discordgo.Session) { + fmt.Print("Unlinking TTY...\n\r") session.Close() os.Exit(0) } diff --git a/main.go b/main.go index 75b4082..0b8bdde 100644 --- a/main.go +++ b/main.go @@ -66,7 +66,8 @@ func main() { // TODO: user account support client, err := discordgo.New("Bot " + token) if err != nil { - fmt.Println("\r% Failed to create client:", err) + fmt.Println("% Failed to create client:", err) + fmt.Print("\r") os.Exit(1) return } @@ -93,7 +94,8 @@ func main() { err = client.Open() if err != nil { - fmt.Println("\r% Failed to connect to Discord:", err) + fmt.Println("% Failed to connect to Discord:", err) + fmt.Print("\r") os.Exit(1) return } @@ -101,8 +103,7 @@ func main() { keyboard.Listen(func(key keys.Key) (stop bool, err error) { if !state.IsInPrompt() { if key.Code == keys.CtrlC { - client.Close() - os.Exit(0) + commands.QuitCommand(client) return true, nil } else { command, has := commands.GetCommand(key.String())