diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index e78c339..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,31 +0,0 @@ -const OFF = 0; -// const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: ["eslint:recommended"], - parserOptions: { - ecmaVersion: 2020, - }, - env: { - es6: true, - node: true, - }, - rules: { - indent: OFF, - semi: ERROR, - quotes: [ERROR, "double", {avoidEscape: true, allowTemplateLiterals: true}], - "no-empty": ERROR, - "array-callback-return": ERROR, - "consistent-return": ERROR, - eqeqeq: OFF, - "prefer-const": ERROR, - "no-unused-vars": [ERROR, {args: "none", varsIgnorePattern: "^_"}], - "no-console": OFF, - "no-debugger": OFF, - "require-atomic-updates": OFF, - }, - globals: { - comcord: true, - }, -}; diff --git a/.gitignore b/.gitignore index c2658d7..6b037e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules/ +comcord +comcord.exe diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 15520fe..0000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "semi": true, - "bracketSpacing": false, - "endOfLine": "lf" -} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index bc127de..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Cynthia Foxwell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index dbc8d73..e163695 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# comcord +# comcord (`rewrite-go`) A CLI-based client for Discord inspired by [SDF](https://sdf.org)'s [commode](https://sdf.org/?tutorials/comnotirc). ## Why? @@ -6,30 +6,13 @@ A CLI-based client for Discord inspired by [SDF](https://sdf.org)'s [commode](ht 2. I've been spending more time in commode on SDF and have been accustomed to the experience. ## Usage -1. `pnpm i` -2. `node src/index.js ` +TODO -Your token will be then stored in `.comcordrc` after the first launch. - -### User Accounts -User accounts are _partially_ supported via `allowUserAccounts=true` in your `.comcordrc`. -This is use at your own risk, despite spoofing the official client. I am not responsible for any banned accounts. - -#### Guild members not populating -This is due to most libraries not implementing Lazy Guilds, as bots do not need lazy guilds to function. - -If you are willing to implement Lazy Guilds based off of [unofficial documentation](https://luna.gitlab.io/discord-unofficial-docs/lazy_guilds.html) -and my already existing horrible hacks to make user accounts work in the first place, feel free to send a PR (on GitLab, GitHub repo is a read only mirror). - -### Bot Accounts (prefered) -You **MUST** grant your bot all Privileged Gateway Intents. - -## Design Decisions -- Node.js was chosen currently due to familiarity. -- Dysnomia was chosen due to familiarity and the nature of everything not being abstracted out to 200 different classes unlike discord.js. -- "Jank" by design. While I don't expect anyone to actually use comcord on serial terminals or teletypes other than for meme factor, the option is still there. +## Rewrite Design Decisions +Go is more portable than Node.js ## TODO +- [x] Send mode - [x] Commands - [x] Quit (q) - [x] Switch guilds (G) @@ -38,41 +21,42 @@ You **MUST** grant your bot all Privileged Gateway Intents. - [x] Emote (e) - Just sends message surrounded in `*`'s - [ ] Finger (f) - - [ ] Shows presence data if available - - [ ] Creation date, join date, ID, etc + - Shows presence data if available + - Creation date, join date, ID, etc - [x] Room history (r) - [x] Extended room history (R) + - [x] Peek (p) + - [x] Cross-guild peek (P) - [x] List channels (l) - [x] List guilds (L) - [x] Clear (c) - [ ] Surf channels forwards (>) - [ ] Surf channels backwards (<) - - [x] AFK toggle (A) - - [x] Send DM (s) - - [x] Answer DM (a) - - [x] Peek (p) + - [ ] AFK toggle (A) + - [ ] Send DM (s) + - [ ] Answer DM (a) + - [x] Current time (+) + - [ ] DM history (TBD) + - [ ] Reply to message (TBD) + - [ ] Toggle color (z) - [x] Message Receiving - - [x] Markdown styling - - [x] Common markdown (bold, italic, etc) - - [x] Figure out how spoilers would work - - [x] Emotes????? + - Markdown styling + - [x] Emotes - [x] Timestamp parsing - [x] Mentions parsing - - [ ] Embeds in the style of commode's posted links + - [ ] Embeds + - [ ] Plain links with title = commode's posted links - [x] Messages wrapped in `*`'s or `_`'s parsed as emotes - [x] Inline DMs to replicate commode's private messages - [x] Replies + - [ ] Group DMs + - [ ] Only works with user accounts, might not even be worth doing - [x] Message sending - [x] Puts incoming messages into queue whilst in send mode - - [ ] Mentions - - [ ] Replies + - [x] Send typing + - [ ] Mentioning - [x] Configuration + - [x] Write token from argv into rc file if rc file doesn't exist - [x] Default guild/channel - - No way to set in client (yet?), `defaultChannel=` and `defaultGuild=` in your `.comcordrc`. -- [ ] Threads -- [x] Not have the token just be in argv -- [x] Not have everything in one file - -## Repository -If you're viewing this on GitHub or GitLab, you are viewing a read only mirror. -The main repository is located on [Gitdab](https://gitdab.com/Cynosphere/comcord) and is push mirrored to the other two. +- [ ] Threads/Forums +- [ ] External rich presence when using bot accounts diff --git a/commands/clear.go b/commands/clear.go new file mode 100644 index 0000000..256c2d9 --- /dev/null +++ b/commands/clear.go @@ -0,0 +1,9 @@ +package commands + +import ( + "fmt" +) + +func ClearCommand() { + fmt.Print("\n\r\033[H\033[2J") +} diff --git a/commands/emote.go b/commands/emote.go new file mode 100644 index 0000000..e5c94f8 --- /dev/null +++ b/commands/emote.go @@ -0,0 +1,45 @@ +package commands + +import ( + "fmt" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" +) + +func EmoteCommand() { + channelId := state.GetCurrentChannel() + if channelId == "" { + fmt.Print("\n\r") + return + } + + prompt := ":emote> " + lib.MakePrompt(prompt, true, func(input string, interrupt bool) { + if input == "" { + if interrupt { + fmt.Print("^C\n\r") + } else { + fmt.Print(prompt, "\n\r") + } + } else { + fmt.Print(prompt, input, "\n\r") + client := state.GetClient() + + snowflake, err := discord.ParseSnowflake(channelId) + if err != nil { + fmt.Print("\n\r") + return + } + + _, err = client.SendMessage(discord.ChannelID(snowflake), "*" + input + "*") + + if err != nil { + fmt.Print("\n\r") + } + + // TODO: update afk state + } + }) +} diff --git a/commands/guild.go b/commands/guild.go new file mode 100644 index 0000000..fdf1e91 --- /dev/null +++ b/commands/guild.go @@ -0,0 +1,658 @@ +package commands + +import ( + "fmt" + "math" + "regexp" + "sort" + "strconv" + "strings" + "unicode/utf8" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + tsize "github.com/kopoli/go-terminal-size" + "github.com/mgutz/ansi" +) + +var REGEX_EMOTE = regexp.MustCompile(`<(?:\x{200b}|&)?a?:(\w+):(\d+)>`) + +type GuildListing struct { + Name string + Members int + Online int +} + +func ListGuildsCommand() { + client := state.GetClient() + + longest := 0 + guilds := make([]GuildListing, 0) + + clientGuilds, err := client.Guilds() + if err != nil { + fmt.Print("\n\r") + return + } + + for _, guild := range clientGuilds { + length := utf8.RuneCountInString(guild.Name) + if length > longest { + longest = length + } + + withCount, err := client.GuildWithCount(guild.ID) + if err == nil { + guilds = append(guilds, GuildListing{ + Name: guild.Name, + Members: int(withCount.ApproximateMembers), + Online: int(withCount.ApproximatePresences), + }) + } else { + guilds = append(guilds, GuildListing{ + Name: guild.Name, + Members: int(guild.ApproximateMembers), + Online: int(guild.ApproximatePresences), + }) + } + } + + fmt.Print("\n\r") + fmt.Printf(" %*s online total\n\r", longest, "guild-name") + fmt.Print(strings.Repeat("-", 80) + "\n\r") + for _, guild := range guilds { + fmt.Printf(" %*s %6d %5d\n\r", longest, guild.Name, guild.Online, guild.Members) + } + fmt.Print(strings.Repeat("-", 80) + "\n\r") + fmt.Print("\n\r") +} + +func GetSortedChannels(guildId string, withCategories bool, withPrivate bool) []discord.Channel { + client := state.GetClient() + channels := make([]discord.Channel, 0) + + guildSnowflake, err := discord.ParseSnowflake(guildId) + if err != nil { + return channels + } + + parsedGuildId := discord.GuildID(guildSnowflake) + + guild, err := client.GuildStore.Guild(parsedGuildId) + if err != nil { + return channels + } + + guildChannels, err := client.ChannelStore.Channels(guild.ID) + if err != nil { + return channels + } + + self, err := client.MeStore.Me() + if err != nil { + return channels + } + + selfMember, err := client.MemberStore.Member(guild.ID, self.ID) + if err != nil { + return channels + } + + if withCategories { + categories := make(map[string][]discord.Channel) + + for _, channel := range guildChannels { + if channel.Type != discord.GuildText && channel.Type != discord.GuildAnnouncement { + continue + } + + private := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + } + + if private { + perms := lib.ChannelPermissionsOf(*guild, channel, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + + if private && !withPrivate { + continue + } + + categoryID := "0" + if channel.ParentID.IsValid() { + categoryID = channel.ParentID.String() + } + + _, has := categories[categoryID] + if !has { + categories[categoryID] = make([]discord.Channel, 0) + } + + categories[categoryID] = append(categories[categoryID], channel) + } + + for id, category := range categories { + // sort channels by position + sort.Slice(category, func(i, j int) bool { + return category[i].Position < category[j].Position + }) + categoryChannels := make([]discord.Channel, 0) + + // append category channel to top + if id != "0" { + parsedCategoryId, err := discord.ParseSnowflake(id) + if err != nil { + continue + } + + for _, channel := range guildChannels { + if channel.ID == discord.ChannelID(parsedCategoryId) { + categoryChannels = append(categoryChannels, channel) + break + } + } + } + + // append channels + for _, channel := range category { + categoryChannels = append(categoryChannels, channel) + } + categories[id] = categoryChannels + } + + keys := make([]string, 0) + for id := range categories { + if id == "0" { + continue + } + keys = append(keys, id) + } + sort.Slice(keys, func(i, j int) bool { + pa, _ := discord.ParseSnowflake(keys[i]) + pb, _ := discord.ParseSnowflake(keys[j]) + + ca, _ := client.ChannelStore.Channel(discord.ChannelID(pa)) + cb, _ := client.ChannelStore.Channel(discord.ChannelID(pb)) + + return ca.Position < cb.Position + }) + sortedCategories := make(map[string][]discord.Channel) + sortedCategories["0"] = categories["0"] + + for _, id := range keys { + sortedCategories[id] = categories[id] + } + + for _, categoryChannels := range sortedCategories { + for _, channel := range categoryChannels { + channels = append(channels, channel) + } + } + } else { + for _, channel := range guildChannels { + if channel.Type != discord.GuildText && channel.Type != discord.GuildAnnouncement { + continue + } + + private := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + } + + if private { + perms := lib.ChannelPermissionsOf(*guild, channel, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + + if private && !withPrivate { + continue + } + + channels = append(channels, channel) + } + + sort.Slice(channels, func(i, j int) bool { + return channels[i].Position < channels[j].Position + }) + } + + return channels +} + +func ListChannelsCommand() { + client := state.GetClient() + self, err := client.MeStore.Me() + if err != nil { + fmt.Print("\n\r") + return + } + + currentGuild := state.GetCurrentGuild() + if currentGuild == "" { + fmt.Print("\n\r") + return + } + + guildSnowflake, err := discord.ParseSnowflake(currentGuild) + if err != nil { + fmt.Print("\n\r") + return + } + + parsedGuildId := discord.GuildID(guildSnowflake) + guild, err := client.GuildStore.Guild(parsedGuildId) + if err != nil { + fmt.Print("\n\r") + return + } + + selfMember, err := client.MemberStore.Member(parsedGuildId, self.ID) + if err != nil { + fmt.Print("\n\r") + return + } + + longest := 0 + channels := GetSortedChannels(currentGuild, true, false) + + for _, channel := range channels { + private := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + } + + if private { + perms := lib.ChannelPermissionsOf(*guild, channel, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + + category := channel.Type == discord.GuildCategory + + catLen := 0 + if category { + catLen = 6 + } + + privLen := 0 + if private { + privLen = 1 + } + length := utf8.RuneCountInString(channel.Name) + privLen + catLen + + if length > longest { + longest = int(math.Min(25, float64(length))) + } + } + + fmt.Print("\n\r") + fmt.Printf(" %*s created topic\n\r", longest, "channel-name") + fmt.Print(strings.Repeat("-", 80) + "\n\r") + for _, channel := range channels { + private := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + } + + if private { + perms := lib.ChannelPermissionsOf(*guild, channel, *selfMember) + private = !perms.Has(discord.PermissionViewChannel) + } + + category := channel.Type == discord.GuildCategory + topic := REGEX_EMOTE.ReplaceAllString(channel.Topic, ":$1:") + topic = strings.ReplaceAll(topic, "\n", " ") + name := channel.Name + if category { + name = "-- " + name + " --" + } + if private { + name = "*" + name + } + + nameLength := utf8.RuneCountInString(name) + if nameLength > 25 { + name = name[:24] + "\u2026" + } + + topicLength := utf8.RuneCountInString(topic) + longestTopic := 80 - (longest + 5) - 11 + if topicLength > longestTopic { + topic = topic[:(longestTopic - 1)] + "\u2026" + } + + created := "??-???-??" + timestamp := channel.CreatedAt() + created = timestamp.UTC().Format("02-Jan-06") + + fmt.Printf(" %*s %s %s\n\r", longest, name, created, topic) + } + fmt.Print(strings.Repeat("-", 80) + "\n\r") + fmt.Print("\n\r") +} + +type ListedMember struct { + Name string + Bot bool + Status discord.Status + Position int +} + +func ListUsersCommand() { + client := state.GetClient() + + currentGuild := state.GetCurrentGuild() + currentChannel := state.GetCurrentChannel() + + if currentGuild == "" { + fmt.Print("\n\r") + return + } + if currentChannel == "" { + fmt.Print("\n\r") + return + } + + parsedGuildId, err := discord.ParseSnowflake(currentGuild) + if err != nil { + fmt.Print("\n\r") + return + } + parsedChannelId, err := discord.ParseSnowflake(currentChannel) + if err != nil { + fmt.Print("\n\r") + return + } + + guild, err := client.GuildStore.Guild(discord.GuildID(parsedGuildId)) + if err != nil { + fmt.Print("\n\r") + return + } + channel, err := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId)) + if err != nil { + fmt.Print("\n\r") + return + } + + longest := 0 + + sortedMembers := make([]ListedMember, 0) + + presences, err := client.Presences(guild.ID) + if err != nil { + fmt.Print("\n\r") + return + } + + for _, presence := range presences { + if presence.Status == discord.OfflineStatus { + continue + } + member, err := client.MemberStore.Member(guild.ID, presence.User.ID) + if err != nil { + continue + } + + private := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *member) + private = !perms.Has(discord.PermissionViewChannel) + } + } + + if private { + perms := lib.ChannelPermissionsOf(*guild, *channel, *member) + private = !perms.Has(discord.PermissionViewChannel) + } + + length := utf8.RuneCountInString(member.User.Username) + 3 + if length > longest { + longest = length + } + + position := 0 + for _, id := range member.RoleIDs { + role, err := client.RoleStore.Role(guild.ID, id) + if err != nil { + continue + } + + if role.Hoist && role.Position > position { + position = role.Position + } + } + + sortedMembers = append(sortedMembers, ListedMember{ + Name: member.User.Username, + Bot: member.User.Bot, + Status: presence.Status, + Position: position, + }) + } + + fmt.Print("\n\r") + fmt.Printf("[you are in '%s' in '#%s' among %d]\n\r", guild.Name, channel.Name, len(sortedMembers)) + fmt.Print("\n\r") + + membersByPosition := make(map[int][]ListedMember) + for _, member := range sortedMembers { + _, has := membersByPosition[member.Position] + if !has { + membersByPosition[member.Position] = make([]ListedMember, 0) + } + + membersByPosition[member.Position] = append(membersByPosition[member.Position], member) + } + for _, members := range membersByPosition { + sort.Slice(members, func(i, j int) bool { + return members[i].Name < members[j].Name + }) + } + + positions := make([]int, 0, len(membersByPosition)) + for k := range membersByPosition { + positions = append(positions, k) + } + sort.Slice(positions, func(i, j int) bool { + return positions[i] > positions[j] + }) + + size, err := tsize.GetSize() + if err != nil { + return + } + columns := int(math.Floor(float64(size.Width) / float64(longest))) + + index := 0 + for _, position := range positions { + members := membersByPosition[position] + if len(members) > 150 { + str := "[hiding " + strconv.Itoa(len(members)) + " members]" + length := utf8.RuneCountInString(str) + + index++ + + pad := 0 + if index % columns != 0 { + pad = longest - length + } + if pad < 0 { + pad = 0 + } + + fmt.Printf(str + strings.Repeat(" ", pad)) + + if index % columns == 0 { + fmt.Print("\n\r") + } + + continue + } + for _, member := range members { + + statusColor := "reset" + if member.Status == discord.OnlineStatus { + statusColor = "green+b" + } else if member.Status == discord.IdleStatus { + statusColor = "yellow+b" + } else if member.Status == discord.DoNotDisturbStatus { + statusColor = "red+b" + } + + nameColor := "reset" + if member.Bot { + nameColor = "yellow" + } + + nameAndStatus := ansi.Color(" \u2022 ", statusColor) + ansi.Color(member.Name, nameColor) + nameLength := utf8.RuneCountInString(member.Name) + 3 + + index++ + + pad := 0 + if index % columns != 0 { + pad = longest - nameLength + } + if pad < 0 { + pad = 0 + } + + fmt.Printf(nameAndStatus + strings.Repeat(" ", pad)) + + if index % columns == 0 { + fmt.Print("\n\r") + } + } + } + if index % columns != 0 { + fmt.Print("\n\r") + } + fmt.Print("\n\r") + + if channel.Topic != "" { + fmt.Print("--Topic" + strings.Repeat("-", 73) + "\n\r") + for _, line := range strings.Split(channel.Topic, "\n") { + fmt.Print(line + "\n\r") + } + fmt.Print(strings.Repeat("-", 80) + "\n\r") + fmt.Print("\n\r") + } +} + +func SwitchGuild(input string) { + client := state.GetClient() + + if input == "" { + ListChannelsCommand() + ListUsersCommand() + } else { + target := "" + + guilds, err := client.GuildStore.Guilds() + if err != nil { + fmt.Print("\n\r") + return + } + + for _, guild := range guilds { + if strings.Index(strings.ToLower(guild.Name), strings.ToLower(input)) > -1 { + target = guild.ID.String() + break; + } + } + + if target == "" { + fmt.Print("\n\r") + } else { + state.SetCurrentGuild(target) + last := state.GetLastChannel(target) + if last == "" { + channels := GetSortedChannels(target, false, false) + if len(channels) > 0 { + topChannel := channels[0] + + state.SetCurrentChannel(topChannel.ID.String()) + state.SetLastChannel(target, topChannel.ID.String()) + } + } else { + state.SetCurrentChannel(last) + } + + ListChannelsCommand() + ListUsersCommand() + + lib.UpdatePresence() + } + } +} + +func SwitchGuildsCommand() { + lib.MakePrompt(":guild> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + SwitchGuild(input) + }) +} + +func SwitchChannelsCommand() { + currentGuild := state.GetCurrentGuild() + + if currentGuild == "" { + fmt.Print("\n\r") + return + } + + lib.MakePrompt(":channel> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + if input == "" { + ListUsersCommand() + } else { + target := "" + + channels := GetSortedChannels(currentGuild, false, false) + + for _, channel := range channels { + if strings.Index(strings.ToLower(channel.Name), strings.ToLower(input)) > -1 { + target = channel.ID.String() + break + } + } + + if target == "" { + fmt.Print("\n\r") + } else { + state.SetCurrentChannel(target) + state.SetLastChannel(currentGuild, target) + + ListUsersCommand() + + lib.UpdatePresence() + } + } + }) +} diff --git a/commands/help.go b/commands/help.go new file mode 100644 index 0000000..48b47f6 --- /dev/null +++ b/commands/help.go @@ -0,0 +1,83 @@ +package commands + +import ( + "fmt" + "sort" + "strings" + "unicode" + "unicode/utf8" + + "github.com/Cynosphere/comcord/state" + "github.com/mgutz/ansi" +) + +const format string = " %s - %s%s" + +func lessLower(sa, sb string) bool { + for { + rb, nb := utf8.DecodeRuneInString(sb) + if nb == 0 { + // The number of runes in sa is greater than or + // equal to the number of runes in sb. It follows + // that sa is not less than sb. + return false + } + + ra, na := utf8.DecodeRuneInString(sa) + if na == 0 { + // The number of runes in sa is less than the + // number of runes in sb. It follows that sa + // is less than sb. + return true + } + + rbl := unicode.ToLower(rb) + ral := unicode.ToLower(ra) + + if ral != rbl { + return ral < rbl + } else { + return ra > rb + } + } +} + +func HelpCommand() { + noColor := state.HasNoColor() + + fmt.Println("\r\nCOMcord (c)left 2023\n\r") + + commands := GetAllCommands() + keys := make([]string, 0, len(commands)) + for key := range commands { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return lessLower(keys[i], keys[j]) + }) + + index := 0 + for _, key := range keys { + cmd := commands[key] + str := fmt.Sprintf(format, key, cmd.Description, "") + length := len(str) + padding := strings.Repeat(" ", 25 - length) + + if noColor { + fmt.Printf(format, key, cmd.Description, padding) + } else { + coloredKey := ansi.Color(key, "yellow+b") + fmt.Printf(format, coloredKey, cmd.Description, padding) + } + + index++ + if index % 3 == 0 { + fmt.Print("\n\r") + } + } + if index % 3 != 0 { + fmt.Print("\n\r") + } + + fmt.Println("\r\nTo begin TALK MODE, press [SPACE]\n\r") +} diff --git a/commands/history.go b/commands/history.go new file mode 100644 index 0000000..3a83e81 --- /dev/null +++ b/commands/history.go @@ -0,0 +1,157 @@ +package commands + +import ( + "fmt" + "strconv" + "strings" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" +) + +func GetHistory(limit int, channel string) { + client := state.GetClient() + + parsedChannelId, err := discord.ParseSnowflake(channel) + if err != nil { + fmt.Print("\n\r") + return + } + + messages, err := client.Messages(discord.ChannelID(parsedChannelId), 100) + if err != nil { + fmt.Print("\n\r") + return + } + + for i, j := 0, len(messages) - 1; i < j; i, j = i + 1, j - 1 { + messages[i], messages[j] = messages[j], messages[i] + } + + state.SetInPrompt(true) + + fmt.Print("--Beginning-Review", strings.Repeat("-", 62), "\n\r") + + lines := make([]string, 0) + for _, msg := range messages { + msgLines := lib.ProcessMessage(msg, lib.MessageOptions{NoColor: true, InHistory: true}) + for _, line := range msgLines { + lines = append(lines, line) + } + } + + length := len(lines) + startIndex := length - limit + if startIndex < 0 { + startIndex = 0 + } + for i := startIndex; i < length; i++ { + fmt.Print(lines[i]) + } + + fmt.Print("--Review-Complete", strings.Repeat("-", 63), "\n\r") + + state.SetInPrompt(false) +} + +func HistoryCommand() { + currentChannel := state.GetCurrentChannel() + if currentChannel == "" { + fmt.Print("\n\r") + return + } + + GetHistory(20, currentChannel) +} + +func ExtendedHistoryCommand() { + currentChannel := state.GetCurrentChannel() + if currentChannel == "" { + fmt.Print("\n\r") + return + } + + lib.MakePrompt(":lines> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + limit, err := strconv.Atoi(input) + + if err != nil { + fmt.Print("\n\r") + } else { + GetHistory(limit, currentChannel) + } + }) +} + +func PeekHistory(guild, channel string) { + target := "" + + channels := GetSortedChannels(guild, false, false) + + for _, c := range channels { + if strings.Index(strings.ToLower(c.Name), strings.ToLower(channel)) > -1 { + target = c.ID.String() + break + } + } + + if target == "" { + fmt.Print("\n\r") + } else { + GetHistory(20, target) + } +} + +func PeekCommand() { + currentGuild := state.GetCurrentGuild() + if currentGuild == "" { + fmt.Print("\n\r") + return + } + + lib.MakePrompt(":peek> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + + if input != "" { + PeekHistory(currentGuild, input) + } + }) +} + +func CrossPeekCommand() { + client := state.GetClient() + + lib.MakePrompt(":guild> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + + if input != "" { + targetGuild := "" + + guilds, err := client.GuildStore.Guilds() + if err != nil { + fmt.Print("\n\r") + return + } + + for _, guild := range guilds { + if strings.Index(strings.ToLower(guild.Name), strings.ToLower(input)) > -1 { + targetGuild = guild.ID.String() + break; + } + } + + if targetGuild == "" { + fmt.Print("\n\r") + } else { + lib.MakePrompt(":peek> ", false, func(input string, interrupt bool) { + fmt.Print("\r") + + if input != "" { + PeekHistory(targetGuild, input) + } + }) + } + } + }) +} diff --git a/commands/main.go b/commands/main.go new file mode 100644 index 0000000..dc870fd --- /dev/null +++ b/commands/main.go @@ -0,0 +1,91 @@ +package commands + +var commandMap map[string]Command + +type Command struct { + Run func() + Description string +} + +func Setup() { + commandMap = make(map[string]Command) + + commandMap["q"] = Command{ + Run: QuitCommand, + Description: "quit comcord", + } + + commandMap["h"] = Command{ + Run: HelpCommand, + Description: "command help", + } + + commandMap["c"] = Command{ + Run: ClearCommand, + Description: "clear", + } + + commandMap["e"] = Command{ + Run: EmoteCommand, + Description: "emote", + } + + commandMap["L"] = Command{ + Run: ListGuildsCommand, + Description: "list guilds", + } + + commandMap["l"] = Command{ + Run: ListChannelsCommand, + Description: "list channels", + } + + commandMap["G"] = Command{ + Run: SwitchGuildsCommand, + Description: "goto guild", + } + + commandMap["g"] = Command{ + Run: SwitchChannelsCommand, + Description: "goto channel", + } + + commandMap["w"] = Command{ + Run: ListUsersCommand, + Description: "who is in channel", + } + + commandMap["r"] = Command{ + Run: HistoryCommand, + Description: "channel history", + } + + commandMap["R"] = Command{ + Run: ExtendedHistoryCommand, + Description: "extended history", + } + + commandMap["p"] = Command{ + Run: PeekCommand, + Description: "peek at channel", + } + + commandMap["P"] = Command{ + Run: CrossPeekCommand, + Description: "cross-guild peek", + } + + commandMap["+"] = Command{ + Run: TimeCommand, + Description: "current time", + } +} + +func GetCommand(key string) (Command, bool) { + command, has := commandMap[key] + return command, has +} + +func GetAllCommands() map[string]Command { + return commandMap +} diff --git a/commands/quit.go b/commands/quit.go new file mode 100644 index 0000000..ade4769 --- /dev/null +++ b/commands/quit.go @@ -0,0 +1,16 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/Cynosphere/comcord/state" +) + +func QuitCommand() { + client := state.GetClient() + + fmt.Print("Unlinking TTY...\n\r") + client.Close() + os.Exit(0) +} diff --git a/commands/send.go b/commands/send.go new file mode 100644 index 0000000..2e5fe5c --- /dev/null +++ b/commands/send.go @@ -0,0 +1,129 @@ +package commands + +import ( + "fmt" + "regexp" + "strings" + "unicode/utf8" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/mgutz/ansi" +) + +var REGEX_MENTION = regexp.MustCompile("@([a-z0-9._]{1,32})") + +func SendMode() { + client := state.GetClient() + + currentGuild := state.GetCurrentGuild() + + channelId := state.GetCurrentChannel() + if channelId == "" { + fmt.Print("\n\r") + return + } + parsedChannelId, err := discord.ParseSnowflake(channelId) + if err != nil { + fmt.Print("\n\r") + return + } + + channel, err := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId)) + if err != nil { + fmt.Print("\n\r") + return + } + + guild, err := client.GuildStore.Guild(channel.GuildID) + if err != nil { + fmt.Print("\n\r") + return + } + + self, err := client.MeStore.Me() + if err != nil { + fmt.Print("\n\r") + return + } + + selfMember, err := client.MemberStore.Member(guild.ID, self.ID) + if err != nil { + fmt.Print("\n\r") + return + } + + cannotSend := false + + if channel.ParentID.IsValid() { + category, err := client.ChannelStore.Channel(channel.ParentID) + if err == nil { + perms := lib.ChannelPermissionsOf(*guild, *category, *selfMember) + cannotSend = !perms.Has(discord.PermissionSendMessages) + } + } + + if cannotSend { + perms := lib.ChannelPermissionsOf(*guild, *channel, *selfMember) + cannotSend = !perms.Has(discord.PermissionSendMessages) + } + + if cannotSend { + fmt.Print("\n\r") + return + } + + length := utf8.RuneCountInString(self.Username) + 2 + curLength := state.GetNameLength() + + prompt := fmt.Sprintf("[%s]%s", self.Username, strings.Repeat(" ", (curLength - length) + 1)) + if !state.HasNoColor() { + prompt = ansi.Color(prompt, "cyan+b") + } + + client.Typing(channel.ID) + + lib.MakePrompt(prompt, true, func(input string, interrupt bool) { + if input == "" { + if interrupt { + fmt.Print("^C\n\r") + } else { + fmt.Print(prompt, "\n\r") + } + } else { + fmt.Print(prompt, input, "\n\r") + + input := REGEX_MENTION.ReplaceAllStringFunc(input, func(match string) string { + matches := REGEX_MENTION.FindStringSubmatch(match) + username := matches[1] + + parsedGuildId, err := discord.ParseSnowflake(currentGuild) + if err != nil { + return match + } + + members, err := client.MemberStore.Members(discord.GuildID(parsedGuildId)) + if err != nil { + return match + } + + for _, member := range members { + if member.User.Username == username { + return member.User.ID.Mention() + } + } + + return match + }) + + _, err := client.SendMessage(channel.ID, input) + + if err != nil { + fmt.Print("\n\r") + } + + // TODO: update afk state + } + }) +} diff --git a/commands/time.go b/commands/time.go new file mode 100644 index 0000000..c445fb1 --- /dev/null +++ b/commands/time.go @@ -0,0 +1,12 @@ +package commands + +import ( + "fmt" + "time" +) + +func TimeCommand() { + now := time.Now().UTC() + + fmt.Printf("%s\n\r", now.Format("[Mon 02-Jan-06 15:04:05]")) +} diff --git a/events/clock.go b/events/clock.go new file mode 100644 index 0000000..01ad0f3 --- /dev/null +++ b/events/clock.go @@ -0,0 +1,42 @@ +package events + +import ( + "fmt" + "time" + "unicode/utf8" + + "github.com/Cynosphere/comcord/state" +) + +var sentTime bool = false + +func SetupClock() { + clock := time.NewTicker(500 * time.Millisecond) + go func() { + for { + select { + case <- clock.C: { + now := time.Now().UTC() + if now.Minute() % 15 == 0 && now.Second() < 2 && !sentTime { + if state.IsInPrompt() { + // TODO + } else { + fmt.Printf("%s\n\r", now.Format("[Mon 02-Jan-06 15:04:05]")) + } + + client := state.GetClient() + self, err := client.MeStore.Me() + if err != nil { + return + } + + state.SetNameLength(utf8.RuneCountInString(self.Username) + 2) + sentTime = true + } else if now.Second() > 2 && sentTime { + sentTime = false + } + } + } + } + }() +} diff --git a/events/main.go b/events/main.go new file mode 100644 index 0000000..a006c57 --- /dev/null +++ b/events/main.go @@ -0,0 +1,13 @@ +package events + +import ( + "github.com/diamondburned/ningen/v3" +) + +func Setup(session *ningen.State) { + session.PreHandler.AddHandler(Ready) + session.PreHandler.AddHandler(MessageCreate) + session.PreHandler.AddHandler(MessageUpdate) + session.PreHandler.AddHandler(ReactionAdd) + SetupClock() +} diff --git a/events/messages.go b/events/messages.go new file mode 100644 index 0000000..c2f7228 --- /dev/null +++ b/events/messages.go @@ -0,0 +1,88 @@ +package events + +import ( + "fmt" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/discord" +) + +func MessageCreate(msg *gateway.MessageCreateEvent) { + client := state.GetClient() + self, err := client.MeStore.Me() + if err != nil { + return + } + + if msg.Author.ID == self.ID { + return + } + + channel, err := client.ChannelStore.Channel(msg.ChannelID) + if err != nil { + return + } + + isDM := channel.Type == discord.DirectMessage || channel.Type == discord.GroupDM + + if state.IsInPrompt() { + state.AddMessageToQueue(msg.Message) + } else { + lines := lib.ProcessMessage(msg.Message, lib.MessageOptions{NoColor: state.HasNoColor()}) + for _, line := range lines { + fmt.Print(line) + } + } + + if isDM { + state.SetLastDM(msg.ChannelID.String()) + } +} + +func MessageUpdate(msg *gateway.MessageUpdateEvent) { + client := state.GetClient() + self, err := client.MeStore.Me() + if err != nil { + return + } + + if msg.Author.ID == self.ID { + return + } + + /*old, err := client.MessageStore.Message(msg.ChannelID, msg.ID) + if err != nil { + return + } + + if msg.Content == old.Content { + return + }*/ + + // dont process embed updates as messages + if !msg.EditedTimestamp.IsValid() { + return + } + + channel, err := client.ChannelStore.Channel(msg.ChannelID) + if err != nil { + return + } + + isDM := channel.Type == discord.DirectMessage || channel.Type == discord.GroupDM + + if state.IsInPrompt() { + state.AddMessageToQueue(msg.Message) + } else { + lines := lib.ProcessMessage(msg.Message, lib.MessageOptions{NoColor: state.HasNoColor()}) + for _, line := range lines { + fmt.Print(line) + } + } + + if isDM { + state.SetLastDM(msg.ChannelID.String()) + } +} diff --git a/events/reactions.go b/events/reactions.go new file mode 100644 index 0000000..4624973 --- /dev/null +++ b/events/reactions.go @@ -0,0 +1,56 @@ +package events + +import ( + "fmt" + "time" + + "github.com/Cynosphere/comcord/lib" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" +) + +func ReactionAdd(event *gateway.MessageReactionAddEvent) { + client := state.GetClient() + currentChannel := state.GetCurrentChannel() + + if event.ChannelID.String() != currentChannel { + return + } + + emote := event.Emoji.Name + if event.Emoji.IsCustom() { + emote = ":" + emote + ":" + } + + now := time.Now() + nowSnowflake := discord.NewSnowflake(now) + + message, err := client.MessageStore.Message(event.ChannelID, event.MessageID) + if err != nil { + message, err = client.Message(event.ChannelID, event.MessageID) + if err != nil { + return + } + } + + msg := discord.Message{ + Content: fmt.Sprintf("*reacted with %s*", emote), + Author: event.Member.User, + ChannelID: event.ChannelID, + GuildID: event.GuildID, + ID: discord.MessageID(nowSnowflake), + ReferencedMessage: message, + Type: discord.InlinedReplyMessage, + Timestamp: discord.Timestamp(now), + } + + if state.IsInPrompt() { + state.AddMessageToQueue(msg) + } else { + lines := lib.ProcessMessage(msg, lib.MessageOptions{NoColor: state.HasNoColor()}) + for _, line := range lines { + fmt.Print(line) + } + } +} diff --git a/events/ready.go b/events/ready.go new file mode 100644 index 0000000..a996151 --- /dev/null +++ b/events/ready.go @@ -0,0 +1,52 @@ +package events + +import ( + "fmt" + "unicode/utf8" + + "github.com/Cynosphere/comcord/commands" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/mgutz/ansi" +) + +func Ready(event *gateway.ReadyEvent) { + client := state.GetClient() + self, err := client.Me() + if err != nil { + fmt.Print("\r% Failed to get self: ", err.Error(), "\n\r") + return + } + + fmt.Printf("\rLogged in as: %s\n\r", ansi.Color(fmt.Sprintf("%s (%s)", self.Username, self.ID), "yellow")) + + state.SetNameLength(utf8.RuneCountInString(self.Username) + 2) + + commands.ListGuildsCommand() + + defaultGuild := state.GetConfigValue("defaultGuild") + defaultChannel := state.GetConfigValue("defaultChannel") + if defaultGuild != "" { + parsedGuildId, err := discord.ParseSnowflake(defaultGuild) + if err != nil { + fmt.Print("\r% Failed to parse guild ID: ", err.Error(), "\n\r") + return + } + + guild, err := client.Guild(discord.GuildID(parsedGuildId)) + if err == nil { + if defaultChannel != "" { + state.SetCurrentChannel(defaultChannel) + state.SetLastChannel(defaultGuild, defaultChannel) + } + commands.SwitchGuild(guild.Name) + } else { + fmt.Println("\r% This account is not in the defined default guild.") + } + } else { + if defaultChannel != "" { + fmt.Println("\r% Default channel defined without defining default guild.") + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..338a935 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module github.com/Cynosphere/comcord + +go 1.20 + +require ( + atomicgo.dev/keyboard v0.2.9 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/diamondburned/arikawa/v3 v3.3.1 // indirect + github.com/diamondburned/ningen/v3 v3.0.0 // indirect + github.com/ergochat/readline v0.0.5 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mergestat/timediff v0.0.3 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/twmb/murmur3 v1.1.3 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6036f3 --- /dev/null +++ b/go.sum @@ -0,0 +1,350 @@ +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= +github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/diamondburned/arikawa v1.3.2 h1:ftWgP95IJGXNvCvtO5x0QBYsnFSnIBY0SvDdGoC3ILA= +github.com/diamondburned/arikawa v1.3.2/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= +github.com/diamondburned/arikawa/v3 v3.1.1-0.20221103093025-87c479a2dcd4/go.mod h1:5jBSNnp82Z/EhsKa6Wk9FsOqSxfVkNZDTDBPOj47LpY= +github.com/diamondburned/arikawa/v3 v3.3.1 h1:puqs7mog383RJnUfRSiug5K+z/pC0Cbuq4yGJMQrQWg= +github.com/diamondburned/arikawa/v3 v3.3.1/go.mod h1:+ifmDonP/JdBiUOzZmVReEjPTHDUSkyqqRRmjSf9NE8= +github.com/diamondburned/ningen v1.0.0 h1:fr+7oDWA0Db73CuVeLY8SScWdW6ft/aWwkULXD0flKw= +github.com/diamondburned/ningen v1.0.0/go.mod h1:TcvJV0bK4bp7t+7m29/Tz9dCqgA0sJBKM/Igt0WkvT4= +github.com/diamondburned/ningen/v3 v3.0.0 h1:S7DF+AwOt/zuFsBMAu00mtE8MfuYqaTtDii6iJPX758= +github.com/diamondburned/ningen/v3 v3.0.0/go.mod h1:wMe9WZQiFgkH5Slr5xK8XBBqMJxWTfRDZU82wPney4Y= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ergochat/readline v0.0.5 h1:PlmCLW9HUTnVfhFburg65pQUDKb0LB43G8hS+ygEkp8= +github.com/ergochat/readline v0.0.5/go.mod h1:8RNv74chpO0eTm6rdD1H6WZGihL5rJ+RfSlhv4fIfjg= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 h1:0SMHxjkLKNawqUjjnMlCtEdj6uWZjv0+qDZ3F6GOADI= +github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54/go.mod h1:bm7MVZZvHQBfqHG5X59jrRE/3ak6HvK+/Zb6aZhLR2s= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= +github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/twmb/murmur3 v1.1.3 h1:D83U0XYKcHRYwYIpBKf3Pks91Z0Byda/9SJ8B6EMRcA= +github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +github.com/wader/readline v0.0.0-20230307172220-bcb7158e7448 h1:AzpBtmgdXa3uznrb3esNeEoaLqtNEwckRmaUH0qWD6w= +github.com/wader/readline v0.0.0-20230307172220-bcb7158e7448/go.mod h1:Zgz8IJWvJoe7NK23CCPpC109XMCqJCpUhpHcnnA4XaM= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.2/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211001092434-39dca1131b70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/lib/messages.go b/lib/messages.go new file mode 100644 index 0000000..b93f576 --- /dev/null +++ b/lib/messages.go @@ -0,0 +1,547 @@ +package lib + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/mergestat/timediff" + "github.com/mgutz/ansi" +) + +var REGEX_CODEBLOCK = regexp.MustCompile(`(?i)\x60\x60\x60(?:([a-z0-9_+\-\.]+?)\n)?\n*([^\n](?:.|\n)*?)\n*\x60\x60\x60`) +var REGEX_MENTION = regexp.MustCompile(`<@!?(\d+)>`) +var REGEX_ROLE_MENTION = regexp.MustCompile(`<@&(\d+)>`) +var REGEX_CHANNEL = regexp.MustCompile(`<#(\d+)>`) +var REGEX_EMOTE = regexp.MustCompile(`<(?:\x{200b}|&)?a?:(\w+):(\d+)>`) +var REGEX_COMMAND = regexp.MustCompile(``) +var REGEX_BLOCKQUOTE = regexp.MustCompile(`^ *>>?>? +`) +var REGEX_GREENTEXT = regexp.MustCompile(`^(>.+?)(?:\n|$)`) +var REGEX_SPOILER = regexp.MustCompile(`\|\|(.+?)\|\|`) +var REGEX_BOLD = regexp.MustCompile(`\*\*(.+?)\*\*`) +var REGEX_UNDERLINE = regexp.MustCompile(`__(.+?)__`) +var REGEX_ITALIC_1 = regexp.MustCompile(`\*(.+?)\*`) +var REGEX_ITALIC_2 = regexp.MustCompile(`_(.+?)_`) +var REGEX_STRIKE = regexp.MustCompile(`~~(.+?)~~`) +var REGEX_3Y3 = regexp.MustCompile(`[\x{e0020}-\x{e007e}]{1,}`) +var REGEX_TIMESTAMP = regexp.MustCompile(``) + +type MessageOptions struct { + Content string + Name string + Channel discord.ChannelID + Bot bool + Webhook bool + Attachments []discord.Attachment + Stickers []discord.StickerItem + Reply *discord.Message + Timestamp time.Time + IsMention bool + IsDM bool + IsJoin bool + IsPin bool + IsDump bool + NoColor bool + InHistory bool +} + +func Parse3y3(content string) string { + out := []rune{} + for i, w := 0, 0; i < len(content); i += w { + runeValue, width := utf8.DecodeRuneInString(content[i:]) + w = width + + out = append(out, rune(int(runeValue) - 0xe0000)) + } + + return string(out) +} + +func ReplaceStyledMarkdown(content string) string { + content = REGEX_BLOCKQUOTE.ReplaceAllString(content, ansi.Color("\u258e", "black+h")) + content = REGEX_GREENTEXT.ReplaceAllStringFunc(content, func(match string) string { + return ansi.Color(match, "green") + }) + + if state.GetConfigValue("enable3y3") == "true" { + parsed := REGEX_3Y3.FindString(content) + parsed = Parse3y3(parsed) + parsed = "\033[3m" + parsed + "\033[23m" + content = REGEX_3Y3.ReplaceAllString(content, ansi.Color(parsed, "magenta")) + } + + content = REGEX_SPOILER.ReplaceAllString(content, "\033[30m\033[40m$1\033[39m\033[49m") + content = REGEX_STRIKE.ReplaceAllString(content, "\033[9m$1\033[29m") + content = REGEX_BOLD.ReplaceAllString(content, "\033[1m$1\033[22m") + content = REGEX_UNDERLINE.ReplaceAllString(content, "\033[4m$1\033[24m") + content = REGEX_ITALIC_1.ReplaceAllString(content, "\033[3m$1\033[23m") + content = REGEX_ITALIC_2.ReplaceAllString(content, "\033[3m$1\033[23m") + + return content +} + +func replaceAllWithCallback(re regexp.Regexp, content string, callback func(matches []string) string) string { + return re.ReplaceAllStringFunc(content, func(match string) string { + matches := re.FindStringSubmatch(match) + return callback(matches) + }) +} + +func ReplaceMarkdown(content string, noColor bool) string { + client := state.GetClient() + + content = replaceAllWithCallback(*REGEX_MENTION, content, func(matches []string) string { + id := matches[1] + parsedId, err := discord.ParseSnowflake(id) + if err != nil { + return "@Unknown User" + } + + currentGuild := state.GetCurrentGuild() + if currentGuild == "" { + user, err := client.User(discord.UserID(parsedId)) + if err != nil { + return "@Unknown User" + } + + return "@" + user.Username + } else { + parsedGuildId, err := discord.ParseSnowflake(currentGuild) + if err != nil { + return "@Unknown User" + } + + member, err := client.MemberStore.Member(discord.GuildID(parsedGuildId), discord.UserID(parsedId)) + if err != nil { + return "@Unknown User" + } + + return "@" + member.User.Username + } + }) + + content = replaceAllWithCallback(*REGEX_ROLE_MENTION, content, func(matches []string) string { + id := matches[1] + parsedId, err := discord.ParseSnowflake(id) + if err != nil { + return "[@Unknown Role]" + } + + currentGuild := state.GetCurrentGuild() + if currentGuild == "" { + return "[@Unknown Role]" + } + parsedGuildId, err := discord.ParseSnowflake(currentGuild) + if err != nil { + return "[@Unknown Role]" + } + + role, err := client.RoleStore.Role(discord.GuildID(parsedGuildId), discord.RoleID(parsedId)) + if err != nil { + return "[@Unknown Role]" + } + + return fmt.Sprintf("[@%s]", role.Name) + }) + + content = replaceAllWithCallback(*REGEX_CHANNEL, content, func(matches []string) string { + id := matches[1] + parsedId, err := discord.ParseSnowflake(id) + if err != nil { + return "#Unknown" + } + + channel, err := client.ChannelStore.Channel(discord.ChannelID(parsedId)) + if err != nil { + return "#Unknown" + } + + return "#" + channel.Name + }) + + content = REGEX_EMOTE.ReplaceAllString(content, ":$1:") + content = REGEX_COMMAND.ReplaceAllString(content, "/$1") + + content = replaceAllWithCallback(*REGEX_TIMESTAMP, content, func (matches []string) string { + timestamp, err := strconv.Atoi(matches[1]) + if err != nil { + return "Invalid Date" + } + timeObj := time.Unix(int64(timestamp), 0).UTC() + + format := matches[2] + + switch format { + case "t": + return timeObj.Format("15:04") + case "T": + return timeObj.Format("15:04:05") + case "d": + return timeObj.Format("2006/01/02") + case "D": + return timeObj.Format("2 January 2006") + case "f": + default: + return timeObj.Format("2 January 2006 15:04") + case "F": + return timeObj.Format("Monday, 2 January 2006 15:04") + case "R": + return timediff.TimeDiff(timeObj) + } + + return "Invalid Date" + }) + + if !noColor { + content = ReplaceStyledMarkdown(content) + } else { + if state.GetConfigValue("enable3y3") == "true" { + parsed := REGEX_3Y3.FindString(content) + parsed = Parse3y3(parsed) + content = REGEX_3Y3.ReplaceAllString(content, "<3y3:" + parsed + ">") + } + } + + return content +} + +func FormatMessage(options MessageOptions) []string { + client := state.GetClient() + + lines := make([]string, 0) + + timestamp := options.Timestamp.UTC().Format("[15:04:05]") + + nameLength := utf8.RuneCountInString(options.Name) + 2 + stateNameLength := state.GetNameLength() + if nameLength > stateNameLength { + state.SetNameLength(nameLength) + stateNameLength = nameLength + } + + if options.Reply != nil { + nameColor := "cyan+b" + if options.Reply.Author.Bot { + nameColor = "yellow+b" + } + + headerLength := 6 + utf8.RuneCountInString(options.Reply.Author.Username) + + content := options.Reply.Content + replyContent := strings.ReplaceAll(content, "\n", " ") + + replyContent = ReplaceMarkdown(replyContent, options.NoColor) + + 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.Stickers) + 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 + } + + lines = append(lines, replySymbol, name, replyContent, "\n\r") + } + + if options.IsDump { + if options.InHistory { + headerLength := 80 - (utf8.RuneCountInString(options.Name) + 5) + dumpHeader := fmt.Sprintf("--- %s %s\n\r", options.Name, strings.Repeat("-", headerLength)) + + contentLines := strings.Split(options.Content, "\n") + + lines = append(lines, dumpHeader) + for _, line := range contentLines { + lines = append(lines, line + "\n\r") + } + lines = append(lines, dumpHeader) + } else { + wordCount := len(strings.Split(options.Content, " ")) + lineCount := len(strings.Split(options.Content, "\n")) + wordsPlural := "" + linesPlural := "" + + if wordCount > 1 { + wordsPlural = "s" + } + if lineCount > 1 { + linesPlural = "s" + } + + 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 { + str = ansi.Color(str, "yellow+b") + } + + lines = append(lines, str + "\n\r") + } + } else { + content := options.Content + + if options.IsDM { + name := fmt.Sprintf("*%s*", options.Name) + if !options.NoColor { + name = ansi.Color(name, "red+b") + } + + content = ReplaceMarkdown(content, options.NoColor) + + lines = append(lines, fmt.Sprintf("%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 { + str = ansi.Color(str, "green+b") + } + + lines = append(lines, str + "\n\r") + } else if options.IsJoin { + channel, err := client.ChannelStore.Channel(options.Channel) + if err != nil { + return lines + } + guild, err := client.GuildStore.Guild(channel.GuildID) + if err != nil { + return lines + } + + str := fmt.Sprintf("%s %s has joined %s", timestamp, options.Name, guild.Name) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + lines = append(lines, str + "\n\r") + } else if options.IsPin { + str := fmt.Sprintf("%s %s pinned a message to this channel", timestamp, options.Name) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + lines = append(lines, str + "\n\r") + } else { + nameColor := "cyan+b" + if options.IsMention { + nameColor = "red+b" + } else if options.Webhook { + nameColor = "magenta+b" + } else if options.Bot { + nameColor = "yellow+b" + } + + content = ReplaceMarkdown(content, options.NoColor) + + name := fmt.Sprintf("[%s]", options.Name) + if !options.NoColor { + name = ansi.Color(name, nameColor) + } + + padding := strings.Repeat(" ", int(math.Abs(float64(stateNameLength) - float64(nameLength))) + 1) + str := name + padding + content + if options.IsMention { + str = str + "\x07" + } + lines = append(lines, str + "\n\r") + } + } + + if len(options.Attachments) > 0 { + for _, attachment := range options.Attachments { + str := fmt.Sprintf("", attachment.URL) + if !options.NoColor { + str = ansi.Color(str, "yellow+b") + } + + lines = append(lines, 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") + } + + lines = append(lines, str + "\n\r") + } + } + + // TODO: links + + // TODO: embeds + + // TODO: lines output for history + return lines +} + +func ProcessMessage(msg discord.Message, options MessageOptions) []string { + client := state.GetClient() + lines := make([]string, 0) + + channel, err := client.ChannelStore.Channel(msg.ChannelID) + if err != nil { + return lines + } + + guild, err := client.GuildStore.Guild(channel.GuildID) + if err != nil { + return lines + } + + self, err := client.MeStore.Me() + if err != nil { + return lines + } + + selfMember, err := client.MemberStore.Member(guild.ID, self.ID) + if err != nil { + return lines + } + + hasMentionedRole := false + for _, role := range msg.MentionRoleIDs { + for _, selfRole := range selfMember.RoleIDs { + if role == selfRole { + hasMentionedRole = true + break; + } + } + } + + isDirectlyMentioned := false + for _, user := range msg.Mentions { + if user.ID == self.ID { + isDirectlyMentioned = true + break; + } + } + + isPing := msg.MentionEveryone || hasMentionedRole || isDirectlyMentioned + isDM := channel.Type == discord.DirectMessage || channel.Type == discord.GroupDM + isEdit := msg.EditedTimestamp.IsValid() + + currentChannel := state.GetCurrentChannel() + isCurrentChannel := currentChannel == msg.ChannelID.String() + + if !isCurrentChannel && !isDM && !isPing && !options.InHistory { + return lines + } + + if isPing && !isCurrentChannel && !isDM && !options.InHistory { + str := fmt.Sprintf("**mentioned by %s in #%s in %s**", msg.Author.Username, channel.Name, guild.Name) + if !options.NoColor { + str = ansi.Color(str, "red+b") + } + str = str + "\x07\n\r" + lines = append(lines, str) + } else { + content := msg.Content + if isEdit { + content = content + " (edited)" + } + + isDump := REGEX_CODEBLOCK.MatchString(content) + + if strings.Index(content, "\n") > -1 && !isDump { + for i, line := range strings.Split(content, "\n") { + options.Content = line + options.Name = msg.Author.Username + options.Channel = msg.ChannelID + options.Bot = msg.Author.Bot + options.Webhook = msg.WebhookID.IsValid() + options.Attachments = msg.Attachments + options.Stickers = msg.Stickers + if i == 0 { + options.Reply = msg.ReferencedMessage + } else { + options.Reply = nil + } + options.Timestamp = time.Time(msg.Timestamp) + options.IsMention = isPing + options.IsDM = isDM + options.IsJoin = msg.Type == discord.GuildMemberJoinMessage + options.IsPin = msg.Type == discord.ChannelPinnedMessage + options.IsDump = false + + msgLines := FormatMessage(options) + for _, line := range msgLines { + lines = append(lines, line) + } + } + } else { + options.Content = content + options.Name = msg.Author.Username + options.Channel = msg.ChannelID + options.Bot = msg.Author.Bot + options.Webhook = msg.WebhookID.IsValid() + options.Attachments = msg.Attachments + options.Stickers = msg.Stickers + options.Reply = msg.ReferencedMessage + options.Timestamp = time.Time(msg.Timestamp) + options.IsMention = isPing + options.IsDM = isDM + options.IsJoin = msg.Type == discord.GuildMemberJoinMessage + options.IsPin = msg.Type == discord.ChannelPinnedMessage + options.IsDump = isDump + + lines = FormatMessage(options) + } + } + + return lines +} + +func ProcessQueue() { + queue := state.GetMessageQueue() + + for _, msg := range queue { + lines := ProcessMessage(msg, MessageOptions{NoColor: state.HasNoColor()}) + for _, line := range lines { + fmt.Print(line) + } + } + + state.EmptyMessageQueue() +} diff --git a/lib/presence.go b/lib/presence.go new file mode 100644 index 0000000..4c74d51 --- /dev/null +++ b/lib/presence.go @@ -0,0 +1,121 @@ +package lib + +import ( + "context" + "fmt" + + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" +) + +func UpdatePresence() { + client := state.GetClient() + + self, err := client.MeStore.Me() + if err != nil { + return + } + + afk := state.IsAFK() + presence := gateway.UpdatePresenceCommand{ + Since: 0, + Activities: make([]discord.Activity, 0), + AFK: false, + } + + currentGuild := state.GetCurrentGuild() + currentChannel := state.GetCurrentChannel() + + parsedGuildId, err := discord.ParseSnowflake(currentGuild) + if err != nil { + return + } + parsedChannelId, err := discord.ParseSnowflake(currentChannel) + if err != nil { + return + } + + var activity discord.Activity + + startTime := state.GetStartTime() + + if self.Bot { + activity = discord.Activity{ + Type: discord.GameActivity, + Name: "comcord", + } + + if currentGuild != "" && currentChannel != "" { + guild, guildErr := client.GuildStore.Guild(discord.GuildID(parsedGuildId)) + channel, channelErr := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId)) + + if guildErr == nil && channelErr == nil { + activity.Type = discord.CustomActivity + activity.Name = "comcord" + activity.State = fmt.Sprintf("#%s - %s | comcord", channel.Name, guild.Name) + } + } + + if afk { + activity.State = activity.State + " [AFK]" + } + } else { + parsedAppId, err := discord.ParseSnowflake("1026163285877325874") + if err != nil { + return + } + + activity = discord.Activity{ + Type: 0, + AppID: discord.AppID(parsedAppId), + Name: "comcord", + Timestamps: &discord.ActivityTimestamps{ + Start: discord.UnixMsTimestamp(startTime.Unix()), + }, + /*Buttons: make([]string, 0), + Metadata: ActivityMetadata{ + ButtonURLs: make([]string, 0), + },*/ + } + + //activity.Buttons = append(activity.Buttons, "comcord Repo") + //activity.Metadata.ButtonURLs = append(activity.Metadata.ButtonURLs, "https://gitdab.com/Cynosphere/comcord") + + if currentGuild != "" && currentChannel != "" { + guild, guildErr := client.GuildStore.Guild(discord.GuildID(parsedGuildId)) + channel, channelErr := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId)) + + if guildErr == nil && channelErr == nil { + activity.Details = fmt.Sprintf("#%s - %s", channel.Name, guild.Name) + + activity.Assets = &discord.ActivityAssets{} + activity.Assets.LargeText = guild.Name + if guild.Icon != "" { + activity.Assets.LargeImage = fmt.Sprintf("mp:icons/%s/%s.png?size=1024", guild.ID, guild.Icon) + } + } + } + + if afk { + activity.State = "AFK" + } + } + + activity.CreatedAt = discord.UnixTimestamp(startTime.Unix()) + + presence.Activities = append(presence.Activities, activity) + + defaultStatus := state.GetConfigValue("defaultStatus") + if defaultStatus != "" { + presence.Status = discord.Status(defaultStatus) + } else { + if afk { + presence.Status = discord.IdleStatus + } else { + presence.Status = discord.OnlineStatus + } + } + + client.Gateway().Send(context.Background(), &presence) +} diff --git a/lib/prompt.go b/lib/prompt.go new file mode 100644 index 0000000..f287b8c --- /dev/null +++ b/lib/prompt.go @@ -0,0 +1,32 @@ +package lib + +import ( + "strings" + + "github.com/Cynosphere/comcord/state" + "github.com/ergochat/readline" +) + +func MakePrompt(prompt string, uniqueLine bool, callback func(input string, interrupt bool)) { + state.SetInPrompt(true) + state.SetPromptText(prompt) + + rl, _ := readline.NewFromConfig(&readline.Config{ + Prompt: prompt, + UniqueEditLine: uniqueLine, + }) + defer rl.Close() + + input, err := rl.Readline() + input = strings.TrimSpace(input) + rl.Close() + + interrupt := err == readline.ErrInterrupt + + callback(input, interrupt) + + state.SetInPrompt(false) + state.SetPromptText("") + + ProcessQueue() +} diff --git a/lib/util.go b/lib/util.go new file mode 100644 index 0000000..1f15385 --- /dev/null +++ b/lib/util.go @@ -0,0 +1,79 @@ +package lib + +import "github.com/diamondburned/arikawa/v3/discord" + +func GuildPermissionsOf(guild discord.Guild, member discord.Member) discord.Permissions { + if guild.OwnerID == member.User.ID { + return discord.PermissionAll + } + + var perm discord.Permissions + + for _, role := range guild.Roles { + if role.ID == discord.RoleID(guild.ID) { + perm |= role.Permissions + break + } + } + + if perm.Has(discord.PermissionAdministrator) { + return discord.PermissionAll + } + + for _, role := range guild.Roles { + for _, id := range member.RoleIDs { + if id == role.ID { + perm |= role.Permissions + } + } + } + + if perm.Has(discord.PermissionAdministrator) { + return discord.PermissionAll + } + + return perm +} + +func ChannelPermissionsOf(guild discord.Guild, channel discord.Channel, member discord.Member) discord.Permissions { + perm := GuildPermissionsOf(guild, member) + + if perm.Has(discord.PermissionAdministrator) { + return discord.PermissionAll + } + + for _, overwrite := range channel.Overwrites { + if discord.GuildID(overwrite.ID) == guild.ID { + perm &= ^overwrite.Deny + perm |= overwrite.Allow + break + } + } + + var deny, allow discord.Permissions + + for _, overwrite := range channel.Overwrites { + for _, id := range member.RoleIDs { + if id == discord.RoleID(overwrite.ID) && overwrite.Type == discord.OverwriteRole { + deny |= overwrite.Deny + allow |= overwrite.Allow + } + } + } + + perm &= ^deny + perm |= allow + + for _, overwrite := range channel.Overwrites { + if discord.UserID(overwrite.ID) == member.User.ID && overwrite.Type == discord.OverwriteMember { + perm &= ^overwrite.Deny + perm |= overwrite.Allow + } + } + + if perm.Has(discord.PermissionAdministrator) { + return discord.PermissionAll + } + + return perm +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2342b2f --- /dev/null +++ b/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "context" + "fmt" + "os" + "runtime" + "strings" + + "atomicgo.dev/keyboard" + "atomicgo.dev/keyboard/keys" + "github.com/Cynosphere/comcord/commands" + "github.com/Cynosphere/comcord/events" + "github.com/Cynosphere/comcord/rcfile" + "github.com/Cynosphere/comcord/state" + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/gateway" + "github.com/diamondburned/arikawa/v3/session" + arikawa_state "github.com/diamondburned/arikawa/v3/state" + "github.com/diamondburned/arikawa/v3/state/store/defaultstore" + "github.com/diamondburned/arikawa/v3/utils/handler" + "github.com/diamondburned/ningen/v3" + "golang.org/x/term" +) + +func main() { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer term.Restore(int(os.Stdin.Fd()), oldState) + + var config map[string]string = make(map[string]string) + var token string + + homeDir, homeErr := os.UserHomeDir() + if homeErr != nil { + panic(homeErr) + } + RCPATH := rcfile.GetPath() + + _, rcErr := os.Stat(RCPATH) + if !os.IsNotExist(rcErr) { + fmt.Printf("%% Reading %s ...\n", strings.Replace(RCPATH, homeDir, "~", 1)) + config = rcfile.Load() + } + + if len(os.Args) > 1 { + token = os.Args[1] + if os.IsNotExist(rcErr) { + fmt.Println("% Writing token to ~/.comcordrc") + config["token"] = token + rcfile.Save(config) + } + } else { + configToken, tokenInConfig := config["token"] + if tokenInConfig { + token = configToken + } else { + fmt.Println("No token provided.") + os.Exit(1) + return + } + } + + fmt.Println("\rCOMcord (c)left 2023") + fmt.Println("\rType 'h' for Commands") + fmt.Print("\r") + + commands.Setup() + + allowUserAccounts := config["allowUserAccounts"] == "true" + tokenPrefix := "Bot " + if allowUserAccounts { + tokenPrefix = "" + } + + fullToken := tokenPrefix + token + + props := gateway.IdentifyProperties{ + OS: runtime.GOOS, + } + statusType := config["statusType"] + if statusType == "mobile" { + props.Browser = "Discord Android" + } else if statusType == "embedded" { + props.Browser = "Discord Embedded" + } else if statusType == "desktop" { + props.Browser = "Discord Client" + } else { + props.Browser = "comcord" + } + + ident := gateway.IdentifyCommand{ + Token: fullToken, + Properties: props, + + Compress: true, + LargeThreshold: 50, + } + + status := "online" + defaultStatus := config["defaultStatus"] + if defaultStatus != "" { + status = defaultStatus + } + startTime := state.GetStartTime() + + activity := discord.Activity{ + Name: "comcord", + Type: discord.GameActivity, + CreatedAt: discord.UnixTimestamp(startTime.Unix()), + Timestamps: &discord.ActivityTimestamps{ + Start: discord.UnixMsTimestamp(startTime.Unix()), + }, + } + + presence := gateway.UpdatePresenceCommand{ + Since: 0, + Activities: make([]discord.Activity, 0), + Status: discord.Status(status), + AFK: false, + } + presence.Activities = append(presence.Activities, activity) + ident.Presence = &presence + + gwURL, err := gateway.URL(context.Background()) + if err != nil { + fmt.Print("% Failed to get gateway URL: ", err, "\n\r") + os.Exit(1) + } + gw := gateway.NewCustomWithIdentifier(gateway.AddGatewayParams(gwURL), gateway.NewIdentifier(ident), nil) + ses := session.NewWithGateway(gw, handler.New()) + st := arikawa_state.NewFromSession(ses, defaultstore.New()) + client := ningen.FromState(st) + client.PreHandler = handler.New() + + client.AddIntents(gateway.IntentGuilds) + client.AddIntents(gateway.IntentGuildPresences) + client.AddIntents(gateway.IntentGuildMembers) + client.AddIntents(gateway.IntentGuildMessages) + client.AddIntents(gateway.IntentGuildMessageReactions) + client.AddIntents(gateway.IntentDirectMessages) + client.AddIntents(gateway.IntentDirectMessageReactions) + client.AddIntents(gateway.IntentMessageContent) + + state.Setup(config, client) + events.Setup(client) + + err = client.Open(context.Background()) + if err != nil { + fmt.Print("% Failed to connect to Discord: ", err, "\n\r") + os.Exit(1) + return + } + defer client.Close() + + keyboard.Listen(func(key keys.Key) (stop bool, err error) { + if !state.IsInPrompt() { + if key.Code == keys.CtrlC { + term.Restore(int(os.Stdin.Fd()), oldState) + commands.QuitCommand() + return true, nil + } else { + command, has := commands.GetCommand(key.String()) + if has { + if key.String() == "q" { + term.Restore(int(os.Stdin.Fd()), oldState) + } + command.Run() + } else { + commands.SendMode() + } + } + } + + return false, nil + }) + + /*sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + <-sc + + client.Close()*/ +} diff --git a/package.json b/package.json deleted file mode 100644 index a0a794e..0000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "comcord", - "version": "1.0.0", - "description": "", - "main": "index.js", - "keywords": [], - "author": "Cynosphere", - "license": "MIT", - "dependencies": { - "@projectdysnomia/dysnomia": "^0.1.2", - "chalk": "4.1.2", - "discord-rpc": "^4.0.1" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 22c8dc5..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,191 +0,0 @@ -lockfileVersion: '6.0' - -dependencies: - '@projectdysnomia/dysnomia': - specifier: ^0.1.2 - version: 0.1.2 - chalk: - specifier: 4.1.2 - version: 4.1.2 - discord-rpc: - specifier: ^4.0.1 - version: 4.0.1 - -packages: - - /@projectdysnomia/dysnomia@0.1.2: - resolution: {integrity: sha512-F64G64JwFWn/QUFqkhsyBvXJ0Du3E6Y0yu8tSrcukAnSeW8qV+reKqeQnMmHcQWYopwuYM8Q6OF/VX6VKggtOA==} - engines: {node: '>=10.4.0'} - peerDependencies: - '@discordjs/opus': ^0.9.0 - erlpack: github:discord/erlpack || github:abalabahaha/erlpack - eventemitter3: ^5.0.0 - pako: ^2.1.0 - sodium-native: ^4.0.1 - zlib-sync: ^0.1.8 - peerDependenciesMeta: - '@discordjs/opus': - optional: true - erlpack: - optional: true - eventemitter3: - optional: true - pako: - optional: true - sodium-native: - optional: true - zlib-sync: - optional: true - dependencies: - ws: 8.13.0 - optionalDependencies: - opusscript: 0.0.8 - tweetnacl: 1.0.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - optional: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /discord-rpc@4.0.1: - resolution: {integrity: sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==} - dependencies: - node-fetch: 2.6.7 - ws: 7.5.9 - optionalDependencies: - register-scheme: github.com/devsnek/node-register-scheme/e7cc9a63a1f512565da44cb57316d9fb10750e17 - transitivePeerDependencies: - - bufferutil - - encoding - - utf-8-validate - dev: false - - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - optional: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: false - - /node-addon-api@1.7.2: - resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - dev: false - optional: true - - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - - /opusscript@0.0.8: - resolution: {integrity: sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==} - requiresBuild: true - dev: false - optional: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: false - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /tweetnacl@1.0.3: - resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} - requiresBuild: true - dev: false - optional: true - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - - /ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - github.com/devsnek/node-register-scheme/e7cc9a63a1f512565da44cb57316d9fb10750e17: - resolution: {tarball: https://codeload.github.com/devsnek/node-register-scheme/tar.gz/e7cc9a63a1f512565da44cb57316d9fb10750e17} - name: register-scheme - version: 0.0.2 - requiresBuild: true - dependencies: - bindings: 1.5.0 - node-addon-api: 1.7.2 - dev: false - optional: true diff --git a/rcfile/main.go b/rcfile/main.go new file mode 100644 index 0000000..f2873b1 --- /dev/null +++ b/rcfile/main.go @@ -0,0 +1,47 @@ +package rcfile + +import ( + "os" + "path/filepath" + "strings" +) + +func GetPath() string { + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + return filepath.Join(homeDir, ".comcordrc") +} + +func Load() map[string]string { + config := make(map[string]string) + file, err := os.ReadFile(GetPath()) + if err != nil { + panic(err) + } + + lines := strings.Split(string(file), "\n") + for _, line := range lines { + kvs := strings.Split(line, "=") + if len(kvs) == 2 { + config[kvs[0]] = kvs[1] + } + } + + return config +} + +func Save(config map[string]string) { + out := "" + + for key, value := range config { + out = out + key + "=" + value + "\n" + } + + err := os.WriteFile(GetPath(), []byte(out), 0644) + if err != nil { + panic(err) + } +} diff --git a/src/commands/afk.js b/src/commands/afk.js deleted file mode 100644 index 88931d0..0000000 --- a/src/commands/afk.js +++ /dev/null @@ -1,18 +0,0 @@ -const {addCommand} = require("../lib/command"); -const {updatePresence} = require("../lib/presence"); - -addCommand("A", "toggles AFK mode", function () { - if (comcord.state.afk == true) { - comcord.state.afk = false; - comcord.client.editStatus("online"); - comcord.client.editAFK(false); - console.log(""); - } else { - comcord.state.afk = true; - comcord.client.editStatus("idle"); - comcord.client.editAFK(true); - console.log(""); - } - - updatePresence(); -}); diff --git a/src/commands/clear.js b/src/commands/clear.js deleted file mode 100644 index db973c5..0000000 --- a/src/commands/clear.js +++ /dev/null @@ -1,5 +0,0 @@ -const {addCommand} = require("../lib/command"); - -addCommand("c", "clear", function () { - console.clear(); -}); diff --git a/src/commands/emote.js b/src/commands/emote.js deleted file mode 100644 index 153a4a9..0000000 --- a/src/commands/emote.js +++ /dev/null @@ -1,25 +0,0 @@ -const {addCommand} = require("../lib/command"); -const {startPrompt} = require("../lib/prompt"); - -addCommand("e", "emote", function () { - if (!comcord.state.currentChannel) { - console.log(""); - return; - } - - startPrompt(":emote> ", async function (input) { - if (input == "") { - console.log(""); - } else { - try { - process.stdout.write("\n"); - await comcord.client.createMessage(comcord.state.currentChannel, { - content: `*${input}*`, - }); - console.log(`<${comcord.client.user.username} ${input}>`); - } catch (err) { - console.log(``); - } - } - }); -}); diff --git a/src/commands/help.js b/src/commands/help.js deleted file mode 100644 index 27047cf..0000000 --- a/src/commands/help.js +++ /dev/null @@ -1,29 +0,0 @@ -const chalk = require("chalk"); - -const {addCommand} = require("../lib/command"); - -addCommand("h", "command help", function () { - console.log("\nCOMcord (c)left 2022\n"); - - const keys = Object.keys(comcord.commands); - keys.sort((a, b) => a.localeCompare(b)); - - let index = 0; - for (const key of keys) { - const desc = comcord.commands[key].name; - const length = ` ${key} - ${desc}`.length; - - process.stdout.write( - " " + - chalk.bold.yellow(key) + - chalk.reset(" - " + desc) + - " ".repeat(Math.abs(25 - length)) - ); - - index++; - if (index % 3 == 0) process.stdout.write("\n"); - } - if (index % 3 != 0) process.stdout.write("\n"); - - console.log("\nTo begin TALK MODE, press [SPACE]\n"); -}); diff --git a/src/commands/history.js b/src/commands/history.js deleted file mode 100644 index 987856a..0000000 --- a/src/commands/history.js +++ /dev/null @@ -1,126 +0,0 @@ -const {addCommand} = require("../lib/command"); -const {startPrompt} = require("../lib/prompt"); -const {processMessage} = require("../lib/messages"); -const {listChannels} = require("./listChannels"); - -async function getHistory(limit = 20, channel = null) { - if (!channel && !comcord.state.currentChannel) { - console.log(""); - return; - } - - const messages = await comcord.client.getMessages( - channel ?? comcord.state.currentChannel - ); - messages.reverse(); - - console.log("--Beginning-Review".padEnd(72, "-")); - - const lines = []; - for (const msg of messages) { - const processedLines = processMessage(msg, {noColor: true, history: true}); - if (processedLines) lines.push(...processedLines); - } - console.log(lines.slice(-limit).join("\n")); - - console.log("--Review-Complete".padEnd(73, "-")); -} - -async function getExtendedHistory(input) { - input = parseInt(input); - if (isNaN(input)) { - console.log(""); - return; - } - - try { - await getHistory(input); - } catch (err) { - console.log(``); - } -} - -addCommand("r", "channel history", getHistory); -addCommand("R", "extended history", function () { - startPrompt(":lines> ", async function (input) { - process.stdout.write("\n"); - await getExtendedHistory(input); - }); -}); -addCommand("p", "peek at channel", function () { - if (!comcord.state.currentGuild) { - console.log(""); - return; - } - - startPrompt(":peek> ", async function (input) { - console.log(""); - if (input == "") { - return; - } - let target; - - const guild = comcord.client.guilds.get(comcord.state.currentGuild); - const channels = [...guild.channels.values()].filter((c) => c.type == 0); - channels.sort((a, b) => a.position - b.position); - - for (const channel of channels) { - if (channel.name.toLowerCase().indexOf(input.toLowerCase()) > -1) { - target = channel.id; - break; - } - } - - if (target == null) { - console.log(""); - } else { - await getHistory(20, target); - } - }); -}); -addCommand("P", "cross-guild peek", function () { - startPrompt(":guild> ", async function (input) { - console.log(""); - if (input == "") { - return; - } - let targetGuild; - for (const guild of comcord.client.guilds.values()) { - if (guild.name.toLowerCase().indexOf(input.toLowerCase()) > -1) { - targetGuild = guild.id; - break; - } - } - - if (targetGuild == null) { - console.log(""); - } else { - startPrompt(":peek> ", async function (input) { - console.log(""); - if (input == "") { - return; - } - let target; - - const guild = comcord.client.guilds.get(targetGuild); - const channels = [...guild.channels.values()].filter( - (c) => c.type == 0 - ); - channels.sort((a, b) => a.position - b.position); - - for (const channel of channels) { - if (channel.name.toLowerCase().indexOf(input.toLowerCase()) > -1) { - target = channel.id; - break; - } - } - - if (target == null) { - console.log(""); - } else { - await getHistory(20, target); - } - }); - } - }); -}); diff --git a/src/commands/listChannels.js b/src/commands/listChannels.js deleted file mode 100644 index 14863f5..0000000 --- a/src/commands/listChannels.js +++ /dev/null @@ -1,55 +0,0 @@ -const {addCommand} = require("../lib/command"); - -function listChannels() { - if (!comcord.state.currentGuild) { - console.log(""); - return; - } - - let longest = 0; - const guild = comcord.client.guilds.get(comcord.state.currentGuild); - const channels = Array.from(guild.channels.values()).filter( - (c) => c.type == 0 || c.type == 5 - ); - channels.sort((a, b) => a.position - b.position); - - for (const channel of channels) { - const perms = channel.permissionsOf(comcord.client.user.id); - const private = !perms.has("readMessageHistory"); - - if (channel.name.length + (private ? 1 : 0) > longest) - longest = Math.min(25, channel.name.length + (private ? 1 : 0)); - } - - console.log(""); - console.log(" " + "channel-name".padStart(longest, " ") + " " + "topic"); - console.log("-".repeat(80)); - for (const channel of channels) { - const topic = - channel.topic != null ? channel.topic.replace(/\n/g, " ") : ""; - const perms = channel.permissionsOf(comcord.client.user.id); - const private = !perms.has("viewChannel"); - - const name = (private ? "*" : "") + channel.name; - - console.log( - " " + - (name.length > 24 ? name.substring(0, 24) + "\u2026" : name).padStart( - longest, - " " - ) + - " " + - (topic.length > 80 - (longest + 5) - ? topic.substring(0, 79 - (longest + 5)) + "\u2026" - : topic) - ); - } - console.log("-".repeat(80)); - console.log(""); -} - -addCommand("l", "list channels", listChannels); - -module.exports = { - listChannels, -}; diff --git a/src/commands/listGuilds.js b/src/commands/listGuilds.js deleted file mode 100644 index e194b89..0000000 --- a/src/commands/listGuilds.js +++ /dev/null @@ -1,37 +0,0 @@ -const {addCommand} = require("../lib/command"); - -function listGuilds() { - let longest = 0; - const guilds = []; - - for (const guild of comcord.client.guilds.values()) { - if (guild.name.length > longest) longest = guild.name.length; - - const online = Array.from(guild.members.values()).filter( - (m) => m.status - ).length; - guilds.push({name: guild.name, members: guild.memberCount, online}); - } - - console.log(""); - console.log(" " + "guild-name".padStart(longest, " ") + " online total"); - console.log("-".repeat(80)); - for (const guild of guilds) { - console.log( - " " + - guild.name.padStart(longest, " ") + - " " + - guild.online.toString().padStart(6, " ") + - " " + - guild.members.toString().padStart(5, " ") - ); - } - console.log("-".repeat(80)); - console.log(""); -} - -addCommand("L", "list guilds", listGuilds); - -module.exports = { - listGuilds, -}; diff --git a/src/commands/listUsers.js b/src/commands/listUsers.js deleted file mode 100644 index 4b99c04..0000000 --- a/src/commands/listUsers.js +++ /dev/null @@ -1,88 +0,0 @@ -const chalk = require("chalk"); - -const {addCommand} = require("../lib/command"); - -function getStatus(status) { - let color; - switch (status) { - case "online": - color = chalk.bold.green; - break; - case "idle": - color = chalk.bold.yellow; - break; - case "dnd": - color = chalk.bold.red; - break; - default: - color = chalk.bold; - break; - } - - return color(" \u2022 "); -} - -function listUsers() { - if (!comcord.state.currentGuild) { - console.log(""); - return; - } - if (!comcord.state.currentChannel) { - console.log(""); - return; - } - - const guild = comcord.client.guilds.get(comcord.state.currentGuild); - const channel = guild.channels.get(comcord.state.currentChannel); - - console.log( - `\n[you are in '${guild.name}' in '${channel.name}' among ${guild.memberCount}]\n` - ); - - const online = Array.from(guild.members.values()).filter((m) => m.status); - online.sort((a, b) => a.username.localeCompare(b.username)); - - let longest = 0; - for (const member of online) { - // FIXME: remove discrim stuff after username migration finished - const name = member.username; - if (name.length + 3 > longest) longest = name.length + 3; - } - - const columns = Math.floor(process.stdout.columns / longest); - - let index = 0; - for (const member of online) { - const name = member.username; - const status = getStatus(member.status); - const nameAndStatus = - (member.user.bot ? chalk.yellow(name) : chalk.reset(name)) + status; - - index++; - process.stdout.write( - nameAndStatus + - " ".repeat( - index % columns == 0 ? 0 : Math.abs(longest - (name.length + 3)) - ) - ); - - if (index % columns == 0) process.stdout.write("\n"); - } - if (index % columns != 0) process.stdout.write("\n"); - console.log(""); - - if (channel.topic != null) { - console.log("--Topic".padEnd(80, "-")); - console.log(channel.topic); - console.log("-".repeat(80)); - console.log(""); - } -} - -if (!comcord.commands.w) { - addCommand("w", "who is in guild", listUsers); -} - -module.exports = { - listUsers, -}; diff --git a/src/commands/privateMessages.js b/src/commands/privateMessages.js deleted file mode 100644 index eed1aff..0000000 --- a/src/commands/privateMessages.js +++ /dev/null @@ -1,60 +0,0 @@ -const chalk = require("chalk"); - -const {addCommand} = require("../lib/command"); -const {startPrompt} = require("../lib/prompt"); -const {listUsers} = require("./listUsers"); - -function startDM(channel) { - startPrompt(":msg> ", async function (input) { - if (input == "") { - console.log( - `\n` - ); - } else { - try { - await channel.createMessage({content: input}); - console.log( - chalk.bold.green( - `\n` - ) - ); - } catch (err) { - console.log(`\n`); - } - } - }); -} - -addCommand("s", "send private", function () { - console.log("Provide a RECIPIENT"); - startPrompt(":to> ", function (who) { - let target; - for (const user of comcord.client.users.values()) { - if (user.username == who) { - target = user; - break; - } - } - - if (target) { - console.log(""); - startDM(target); - } else { - listUsers(); - } - }); -}); - -addCommand("a", "answer a send", function () { - if (comcord.state.lastDM) { - console.log( - chalk.bold.green( - `` - ) - ); - startDM(comcord.state.lastDM); - } else { - // FIXME: figure out the actual message in com - console.log(""); - } -}); diff --git a/src/commands/quit.js b/src/commands/quit.js deleted file mode 100644 index 3adb706..0000000 --- a/src/commands/quit.js +++ /dev/null @@ -1,7 +0,0 @@ -const {addCommand} = require("../lib/command"); - -addCommand("q", "quit comcord", function () { - comcord.state.quitting = true; - comcord.client.disconnect({reconnect: false}); - process.exit(0); -}); diff --git a/src/commands/send.js b/src/commands/send.js deleted file mode 100644 index bc83806..0000000 --- a/src/commands/send.js +++ /dev/null @@ -1,44 +0,0 @@ -const chalk = require("chalk"); - -const {startPrompt} = require("../lib/prompt"); -const {updatePresence} = require("../lib/presence"); - -function sendMode() { - if (!comcord.state.currentChannel) { - console.log(""); - return; - } - - startPrompt( - chalk.bold.cyan(`[${comcord.client.user.username}]`) + - " ".repeat( - comcord.state.nameLength - (comcord.client.user.username.length + 2) - ) + - chalk.reset(" "), - async function (input) { - if (input == "") { - console.log(""); - } else { - try { - process.stdout.write("\n"); - await comcord.client.createMessage(comcord.state.currentChannel, { - content: input, - }); - - if (comcord.state.afk == true) { - comcord.state.afk = false; - comcord.client.editStatus("online"); - comcord.client.editAFK(false); - console.log(""); - - updatePresence(); - } - } catch (err) { - console.log(""); - } - } - } - ); -} - -module.exports = {sendMode}; diff --git a/src/commands/switchChannel.js b/src/commands/switchChannel.js deleted file mode 100644 index b59f1ab..0000000 --- a/src/commands/switchChannel.js +++ /dev/null @@ -1,47 +0,0 @@ -const {addCommand} = require("../lib/command"); -const {startPrompt} = require("../lib/prompt"); -const {updatePresence} = require("../lib/presence"); - -const {listUsers} = require("./listUsers"); - -function switchChannel(input) { - if (input == "") { - listUsers(); - return; - } - let target; - - const guild = comcord.client.guilds.get(comcord.state.currentGuild); - const channels = [...guild.channels.values()].filter((c) => c.type == 0); - channels.sort((a, b) => a.position - b.position); - - for (const channel of channels) { - if (channel.name.toLowerCase().indexOf(input.toLowerCase()) > -1) { - target = channel.id; - break; - } - } - - if (target == null) { - console.log(""); - } else { - comcord.state.currentChannel = target; - comcord.state.lastChannel.set(comcord.state.currentGuild, target); - - listUsers(); - - const channel = guild.channels.get(comcord.state.currentChannel); - - process.title = `${guild.name} - ${channel.name} - comcord`; - - updatePresence(); - } -} - -addCommand("g", "goto channel", function () { - if (!comcord.state.currentGuild) { - console.log(""); - return; - } - startPrompt(":channel> ", switchChannel); -}); diff --git a/src/commands/switchGuild.js b/src/commands/switchGuild.js deleted file mode 100644 index c072a63..0000000 --- a/src/commands/switchGuild.js +++ /dev/null @@ -1,60 +0,0 @@ -const {addCommand} = require("../lib/command"); -const {startPrompt} = require("../lib/prompt"); -const {updatePresence} = require("../lib/presence"); - -const {listChannels} = require("./listChannels"); -const {listUsers} = require("./listUsers"); - -function findTopChannel(guildId) { - const guild = comcord.client.guilds.get(guildId); - const channels = [...guild.channels.values()].filter((c) => c.type == 0); - channels.sort((a, b) => a.position - b.position); - - return channels[0]; -} - -function switchGuild(input) { - if (input == "") { - listChannels(); - listUsers(); - return; - } - - let target; - - for (const guild of comcord.client.guilds.values()) { - if (guild.name.toLowerCase().indexOf(input.toLowerCase()) > -1) { - target = guild.id; - break; - } - } - - if (target == null) { - console.log(""); - } else { - comcord.state.currentGuild = target; - if (!comcord.state.lastChannel.has(target)) { - const topChannel = findTopChannel(target); - comcord.state.currentChannel = topChannel.id; - comcord.state.lastChannel.set(target, topChannel.id); - } else { - comcord.state.currentChannel = comcord.state.lastChannel.get(target); - } - - listChannels(); - listUsers(); - - const guild = comcord.client.guilds.get(comcord.state.currentGuild); - const channel = guild.channels.get(comcord.state.currentChannel); - - process.title = `${guild.name} - ${channel.name} - comcord`; - - updatePresence(); - } -} - -addCommand("G", "goto guild", function () { - startPrompt(":guild> ", switchGuild); -}); - -module.exports = {switchGuild}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 86aa96f..0000000 --- a/src/index.js +++ /dev/null @@ -1,470 +0,0 @@ -const {Client, Constants, Channel} = require("@projectdysnomia/dysnomia"); -const DiscordRPC = require("discord-rpc"); -const chalk = require("chalk"); -const fs = require("fs"); -const os = require("os"); - -const rcfile = require("./lib/rcfile"); -const config = {}; - -if (fs.existsSync(rcfile.path)) { - console.log(`% Reading ${rcfile.path.replace(os.homedir(), "~")} ...`); - rcfile.readFile(config); -} - -const CLIENT_ID = "1026163285877325874"; - -const token = process.argv[2]; -if (!config.token && token) { - console.log("% Writing token to .comcordrc"); - config.token = token; - rcfile.writeFile(config); -} - -if (!config.token && !token) { - console.log("No token provided."); - process.exit(1); -} - -process.title = "comcord"; - -global.comcord = { - config, - state: { - connected: true, - rpcConnected: false, - startTime: Date.now(), - currentGuild: null, - currentChannel: null, - nameLength: 2, - inPrompt: false, - messageQueue: [], - lastChannel: new Map(), - afk: false, - }, - commands: {}, -}; -const client = new Client( - (config.allowUserAccounts == "true" ? "" : "Bot ") + (token ?? config.token), - { - defaultImageFormat: "png", - defaultImageSize: 1024, - gateway: { - intents: Object.values(Constants.Intents), - }, - allowedMentions: { - everyone: false, - }, - } -); -comcord.client = client; -const rpc = new DiscordRPC.Client({transport: "ipc"}); -comcord.rpc = rpc; - -const {finalizePrompt} = require("./lib/prompt"); -const {processMessage, processQueue} = require("./lib/messages"); -const {updatePresence} = require("./lib/presence"); - -require("./commands/quit"); -require("./commands/clear"); -require("./commands/help"); -const {sendMode} = require("./commands/send"); -require("./commands/emote"); -const {listGuilds} = require("./commands/listGuilds"); -const {switchGuild} = require("./commands/switchGuild"); // loads listChannels and listUsers -require("./commands/switchChannel"); //loads listUsers -require("./commands/history"); // includes extended history -require("./commands/afk"); -require("./commands/privateMessages"); - -process.stdin.setRawMode(true); -process.stdin.resume(); -process.stdin.setEncoding("utf8"); - -client.once("ready", function () { - console.log( - "Logged in as: " + - chalk.yellow(`${client.user?.username} (${client.user?.id})`) - ); - comcord.state.nameLength = (client.user?.username?.length ?? 0) + 2; - - listGuilds(); - - if (config.defaultGuild) { - const guild = client.guilds.get(config.defaultGuild); - if (guild != null) { - if (config.defaultChannel) { - comcord.state.currentChannel = config.defaultChannel; - comcord.state.lastChannel.set( - config.defaultGuild, - config.defaultChannel - ); - } - switchGuild(guild.name); - } else { - console.log("% This account is not in the defined default guild."); - } - } else { - if (config.defaultChannel) { - console.log("% Default channel defined without defining default guild."); - } - } - - if (client.user.bot && !config.disableRPC) { - rpc - .login({ - clientId: CLIENT_ID, - }) - .catch(function () {}); - } -}); -client.on("error", function () {}); -client.on("ready", function () { - if (comcord.state.connected === false) { - console.log("% Reconnected"); - } -}); -client.on("disconnect", function () { - if (!comcord.state.quitting) { - comcord.state.connected = false; - console.log("% Disconnected, retrying..."); - } -}); - -rpc.on("connected", function () { - comcord.state.rpcConnected = true; - updatePresence(); -}); -let retryingRPC = false; -rpc.once("ready", function () { - rpc.transport.on("error", function () {}); - rpc.transport.on("close", function () { - comcord.state.rpcConnected = false; - if (!retryingRPC) { - retryingRPC = true; - setTimeout(function () { - rpc.transport - .connect() - .then(() => { - retryingRPC = false; - }) - .catch((err) => { - retryingRPC = false; - rpc.transport.emit("close"); - }); - }, 5000); - } - }); -}); -rpc.on("error", function () {}); - -client.on("messageCreate", async function (msg) { - if ( - (msg.mentions.find((user) => user.id == client.user.id) || - msg.mentionEveryone) && - msg.channel.id != comcord.state.currentChannel && - msg.channel.type !== Constants.ChannelTypes.DM && - msg.channel.type !== Constants.ChannelTypes.GROUP_DM - ) { - const data = {ping: true, channel: msg.channel, author: msg.author}; - if (comcord.state.inPrompt) { - comcord.state.messageQueue.push(data); - } else { - processMessage(data); - } - } - - if (!msg.author) return; - if (msg.author.id === client.user.id) return; - - if ( - !(msg.channel instanceof Channel) && - msg.author.id != client.user.id && - !msg.guildID - ) { - if (msg.channel.type === Constants.ChannelTypes.DM) { - const newChannel = await client.getDMChannel(msg.author.id); - if (msg.channel.id == newChannel.id) msg.channel = newChannel; - } else if (msg.channel.type === Constants.ChannelTypes.GROUP_DM) { - // TODO - } - } - - if (!(msg.channel instanceof Channel)) return; - - if ( - msg.channel.id == comcord.state.currentChannel || - msg.channel.type === Constants.ChannelTypes.DM || - msg.channel.type === Constants.ChannelTypes.GROUP_DM - ) { - if (comcord.state.inPrompt) { - comcord.state.messageQueue.push(msg); - } else { - processMessage(msg); - } - } - - if ( - msg.channel.type === Constants.ChannelTypes.DM || - msg.channel.type === Constants.ChannelTypes.GROUP_DM - ) { - comcord.state.lastDM = msg.channel; - } -}); -client.on("messageUpdate", async function (msg, old) { - if (!msg.author) return; - if (msg.author.id === client.user.id) return; - - if ( - !(msg.channel instanceof Channel) && - msg.author.id != client.user.id && - !msg.guildID - ) { - if (msg.channel.type === Constants.ChannelTypes.DM) { - const newChannel = await client.getDMChannel(msg.author.id); - if (msg.channel.id == newChannel.id) msg.channel = newChannel; - } else if (msg.channel.type === Constants.ChannelTypes.GROUP_DM) { - // TODO - } - } - - if (!(msg.channel instanceof Channel)) return; - - if ( - msg.channel.id == comcord.state.currentChannel || - msg.channel.type === Constants.ChannelTypes.DM || - msg.channel.type === Constants.ChannelTypes.GROUP_DM - ) { - if (old && msg.content == old.content) return; - - if (comcord.state.inPrompt) { - comcord.state.messageQueue.push(msg); - } else { - processMessage(msg); - } - } - - if ( - msg.channel.type === Constants.ChannelTypes.DM || - msg.channel.type === Constants.ChannelTypes.GROUP_DM - ) { - comcord.state.lastDM = msg.channel; - } -}); -client.on("messageReactionAdd", async function (msg, emoji, reactor) { - if (msg.channel.id != comcord.state.currentChannel) return; - const reply = - msg.channel.messages.get(msg.id) ?? - (await msg.channel - .getMessages({ - limit: 1, - around: msg.id, - }) - .then((msgs) => msgs[0])); - - const data = { - channel: msg.channel, - referencedMessage: reply, - author: reactor?.user ?? client.users.get(reactor.id), - timestamp: Date.now(), - mentions: [], - content: `*reacted with ${emoji.id ? `:${emoji.name}:` : emoji.name}*`, - }; - - if (comcord.state.inPrompt) { - comcord.state.messageQueue.push(data); - } else { - processMessage(data); - } -}); - -process.stdin.on("data", async function (key) { - if (comcord.state.inPrompt) { - if (key === "\r") { - await finalizePrompt(); - processQueue(); - } else { - if (key === "\b" || key === "\u007f") { - if (comcord.state.promptInput.length > 0) { - process.stdout.moveCursor(-1); - process.stdout.write(" "); - process.stdout.moveCursor(-1); - comcord.state.promptInput = comcord.state.promptInput.substring( - 0, - comcord.state.promptInput.length - 1 - ); - } - } else { - key = key.replace("\u001b", ""); - process.stdout.write(key); - comcord.state.promptInput += key; - } - } - } else { - if (comcord.commands[key]) { - comcord.commands[key].callback(); - } else { - sendMode(); - } - } -}); - -if ( - config.allowUserAccounts == "true" && - !(token ?? config.token).startsWith("Bot ") -) { - if (fetch == null) { - console.log("Node v18+ needed for user account support."); - process.exit(1); - } - - (async function () { - comcord.clientSpoof = require("./lib/clientSpoof"); - const superProperties = await comcord.clientSpoof.getSuperProperties(); - comcord.clientSpoof.superProperties = superProperties; - comcord.clientSpoof.superPropertiesBase64 = Buffer.from( - JSON.stringify(superProperties) - ).toString("base64"); - - // FIXME: is there a way we can string patch functions without having to - // dump locals into global - global.MultipartData = require("@projectdysnomia/dysnomia/lib/util/MultipartData.js"); - global.SequentialBucket = require("@projectdysnomia/dysnomia/lib/util/SequentialBucket.js"); - global.DiscordHTTPError = require("@projectdysnomia/dysnomia/lib/errors/DiscordHTTPError.js"); - global.DiscordRESTError = require("@projectdysnomia/dysnomia/lib/errors/DiscordRESTError.js"); - global.Zlib = require("node:zlib"); - global.HTTPS = require("node:https"); - global.HTTP = require("node:http"); - global.GatewayOPCodes = Constants.GatewayOPCodes; - global.GATEWAY_VERSION = Constants.GATEWAY_VERSION; - - client.getGateway = async function getGateway() { - return {url: "wss://gateway.discord.gg"}; - }; - - console.log("% Injecting headers into request handler"); - client.requestHandler.userAgent = superProperties.browser_user_agent; - const requestFunction = client.requestHandler.request.toString(); - const newRequest = requestFunction - .replace( - "this.userAgent,", - 'this.userAgent,\n"X-Super-Properties":comcord.clientSpoof.superPropertiesBase64,' - ) - .replace("._token", '._token.replace("Bot ","")'); - if (requestFunction === newRequest) - throw new Error("Failed to patch request"); - client.requestHandler.request = new Function( - "method", - "url", - "auth", - "body", - "file", - "_route", - "short", - `return (function ${newRequest}).apply(this,arguments)` - ).bind(client.requestHandler); - - console.log("% Injecting shard spawning"); - client.shards._spawn = client.shards.spawn.bind(client.shards); - client.shards.spawn = function (id) { - const res = this._spawn.apply(this, [id]); - const shard = this.get(id); - if (shard) { - const identifyFunction = shard.identify.toString(); - const newIdentify = identifyFunction - .replace( - /properties: {\n\s+.+?\n\s+.+?\n\s+.+?\n\s+}\n/, - "properties: comcord.clientSpoof.superProperties\n" - ) - .replace(/\s+intents: this.client.shards.options.intents,/, ""); - if (identifyFunction === newIdentify) - throw new Error("Failed to patch identify"); - shard.identify = new Function( - `(function ${newIdentify}).apply(this, arguments)` - ); - shard._wsEvent = shard.wsEvent; - shard.wsEvent = function (packet) { - if (packet.t == "READY") { - packet.d.application = {id: CLIENT_ID, flags: 565248}; - } - - const ret = this._wsEvent.apply(this, [packet]); - - if (packet.t == "READY") { - for (const guild of packet.d.guilds) { - this._wsEvent.apply(this, [ - { - t: "GUILD_CREATE", - d: guild, - }, - ]); - } - } - - return ret; - }; - } - - return res; - }; - - console.log("% Connecting to gateway now"); - await client.connect(); - })(); -} else { - client.connect(); -} - -console.log("COMcord (c)left 2022"); -console.log("Type 'h' for Commands"); - -const dateObj = new Date(); -let sentTime = false; - -const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -const months = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", -]; - -setInterval(function () { - dateObj.setTime(Date.now()); - - const hour = dateObj.getUTCHours(), - minutes = dateObj.getUTCMinutes(), - seconds = dateObj.getUTCSeconds(), - day = dateObj.getUTCDate(), - month = dateObj.getUTCMonth(), - year = dateObj.getUTCFullYear(), - weekDay = dateObj.getUTCDay(); - - const timeString = `[${weekdays[weekDay]} ${day - .toString() - .padStart(2, "0")}-${months[month]}-${year - .toString() - .substring(2, 4)} ${hour.toString().padStart(2, "0")}:${minutes - .toString() - .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}]`; - - if (minutes % 15 == 0 && seconds < 2 && !sentTime) { - if (comcord.state.inPrompt == true) { - comcord.state.messageQueue.push({time: true, content: timeString}); - } else { - console.log(timeString); - } - comcord.state.nameLength = (client.user?.username?.length ?? 0) + 2; - sentTime = true; - } else if (seconds > 2 && sentTime) { - sentTime = false; - } -}, 500); diff --git a/src/lib/clientSpoof.js b/src/lib/clientSpoof.js deleted file mode 100644 index 6b78a75..0000000 --- a/src/lib/clientSpoof.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * This single file is **EXCLUDED** from the project license. - * - * (c) 2022 Cynthia Foxwell, all rights reserved. - * Permission is hereby granted to redistribute this file ONLY with copies of comcord. - * You may not reverse engineer, modify, copy, or redistribute this file for any other uses outside of comcord. - */ - -const os = require("os"); - -async function fetchMainPage() { - const res = await fetch("https://discord.com/channels/@me"); - return await res.text(); -} - -async function fetchAsset(assetPath) { - return await fetch("https://discord.com/" + assetPath).then((res) => - res.text() - ); -} - -const MATCH_SCRIPT = '