Compare commits

...

No commits in common. "main" and "rewrite-go" have entirely different histories.

50 changed files with 3049 additions and 2234 deletions

View file

@ -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,
},
};

3
.gitignore vendored
View file

@ -1 +1,2 @@
node_modules/
comcord
comcord.exe

View file

@ -1,5 +0,0 @@
{
"semi": true,
"bracketSpacing": false,
"endOfLine": "lf"
}

21
LICENSE
View file

@ -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.

View file

@ -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 <token>`
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

9
commands/clear.go Normal file
View file

@ -0,0 +1,9 @@
package commands
import (
"fmt"
)
func ClearCommand() {
fmt.Print("\n\r\033[H\033[2J")
}

45
commands/emote.go Normal file
View file

@ -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("<not in a channel>\n\r")
return
}
prompt := ":emote> "
lib.MakePrompt(prompt, true, func(input string, interrupt bool) {
if input == "" {
if interrupt {
fmt.Print("^C<no message sent>\n\r")
} else {
fmt.Print(prompt, "<no message sent>\n\r")
}
} else {
fmt.Print(prompt, input, "\n\r")
client := state.GetClient()
snowflake, err := discord.ParseSnowflake(channelId)
if err != nil {
fmt.Print("<failed to parse channel id: ", err.Error(), ">\n\r")
return
}
_, err = client.SendMessage(discord.ChannelID(snowflake), "*" + input + "*")
if err != nil {
fmt.Print("<failed to send message: ", err.Error(), ">\n\r")
}
// TODO: update afk state
}
})
}

658
commands/guild.go Normal file
View file

@ -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("<failed to get guilds: ", err.Error(), ">\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("<failed to get self: ", err.Error(), ">\n\r")
return
}
currentGuild := state.GetCurrentGuild()
if currentGuild == "" {
fmt.Print("<not in a guild>\n\r")
return
}
guildSnowflake, err := discord.ParseSnowflake(currentGuild)
if err != nil {
fmt.Print("<failed to parse current guild id: ", err.Error(), ">\n\r")
return
}
parsedGuildId := discord.GuildID(guildSnowflake)
guild, err := client.GuildStore.Guild(parsedGuildId)
if err != nil {
fmt.Print("<failed to get current guild: ", err.Error(), ">\n\r")
return
}
selfMember, err := client.MemberStore.Member(parsedGuildId, self.ID)
if err != nil {
fmt.Print("<failed to get self member: ", err.Error(), ">\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("<not in a guild>\n\r")
return
}
if currentChannel == "" {
fmt.Print("<not in a channel>\n\r")
return
}
parsedGuildId, err := discord.ParseSnowflake(currentGuild)
if err != nil {
fmt.Print("<failed to parse guild id: ", err.Error(), ">\n\r")
return
}
parsedChannelId, err := discord.ParseSnowflake(currentChannel)
if err != nil {
fmt.Print("<failed to parse channel id: ", err.Error(), ">\n\r")
return
}
guild, err := client.GuildStore.Guild(discord.GuildID(parsedGuildId))
if err != nil {
fmt.Print("<failed to get guild: ", err.Error(), ">\n\r")
return
}
channel, err := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId))
if err != nil {
fmt.Print("<failed to get channel: ", err.Error(), ">\n\r")
return
}
longest := 0
sortedMembers := make([]ListedMember, 0)
presences, err := client.Presences(guild.ID)
if err != nil {
fmt.Print("<failed to get presences: ", err.Error(), ">\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("<failed to get guilds: ", err.Error(), ">\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("<guild not found>\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("<not in a guild>\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("<channel not found>\n\r")
} else {
state.SetCurrentChannel(target)
state.SetLastChannel(currentGuild, target)
ListUsersCommand()
lib.UpdatePresence()
}
}
})
}

83
commands/help.go Normal file
View file

@ -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")
}

157
commands/history.go Normal file
View file

@ -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("<failed to parse channel id: ", err.Error(), ">\n\r")
return
}
messages, err := client.Messages(discord.ChannelID(parsedChannelId), 100)
if err != nil {
fmt.Print("<failed to get messages: ", err.Error(), ">\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("<not in a channel>\n\r")
return
}
GetHistory(20, currentChannel)
}
func ExtendedHistoryCommand() {
currentChannel := state.GetCurrentChannel()
if currentChannel == "" {
fmt.Print("<not in a channel>\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("<not a number>\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("<channel not found>\n\r")
} else {
GetHistory(20, target)
}
}
func PeekCommand() {
currentGuild := state.GetCurrentGuild()
if currentGuild == "" {
fmt.Print("<not in a guild>\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("<failed to get guilds: ", err.Error(), ">\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("<guild not found>\n\r")
} else {
lib.MakePrompt(":peek> ", false, func(input string, interrupt bool) {
fmt.Print("\r")
if input != "" {
PeekHistory(targetGuild, input)
}
})
}
}
})
}

91
commands/main.go Normal file
View file

@ -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
}

16
commands/quit.go Normal file
View file

@ -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)
}

129
commands/send.go Normal file
View file

@ -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("<not in a channel>\n\r")
return
}
parsedChannelId, err := discord.ParseSnowflake(channelId)
if err != nil {
fmt.Print("<failed to parse channel id: ", err.Error(), ">\n\r")
return
}
channel, err := client.ChannelStore.Channel(discord.ChannelID(parsedChannelId))
if err != nil {
fmt.Print("<error getting channel: ", err.Error(), ">\n\r")
return
}
guild, err := client.GuildStore.Guild(channel.GuildID)
if err != nil {
fmt.Print("<failed to get current guild: ", err.Error(), ">\n\r")
return
}
self, err := client.MeStore.Me()
if err != nil {
fmt.Print("<failed to get self: ", err.Error(), ">\n\r")
return
}
selfMember, err := client.MemberStore.Member(guild.ID, self.ID)
if err != nil {
fmt.Print("<failed to get self as member: ", err.Error(), ">\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("<you do not have permission to send messages here>\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<no message sent>\n\r")
} else {
fmt.Print(prompt, "<no message sent>\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("<failed to send message: ", err, ">\n\r")
}
// TODO: update afk state
}
})
}

12
commands/time.go Normal file
View file

@ -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]"))
}

42
events/clock.go Normal file
View file

@ -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
}
}
}
}
}()
}

13
events/main.go Normal file
View file

@ -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()
}

88
events/messages.go Normal file
View file

@ -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())
}
}

56
events/reactions.go Normal file
View file

@ -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)
}
}
}

52
events/ready.go Normal file
View file

@ -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.")
}
}
}

27
go.mod Normal file
View file

@ -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
)

350
go.sum Normal file
View file

@ -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=

547
lib/messages.go Normal file
View file

@ -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(`</([^\s]+?):(\d+)>`)
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(`<t:(-?\d{1,17})(?::(t|T|d|D|f|F|R))?>`)
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: %s >", 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: \"%s\" https://cdn.discordapp.com/stickers/%s.png >", 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()
}

121
lib/presence.go Normal file
View file

@ -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)
}

32
lib/prompt.go Normal file
View file

@ -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()
}

79
lib/util.go Normal file
View file

@ -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
}

185
main.go Normal file
View file

@ -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()*/
}

View file

@ -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"
}
}

View file

@ -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

47
rcfile/main.go Normal file
View file

@ -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)
}
}

View file

@ -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("<you have returned>");
} else {
comcord.state.afk = true;
comcord.client.editStatus("idle");
comcord.client.editAFK(true);
console.log("<you go AFK>");
}
updatePresence();
});

View file

@ -1,5 +0,0 @@
const {addCommand} = require("../lib/command");
addCommand("c", "clear", function () {
console.clear();
});

View file

@ -1,25 +0,0 @@
const {addCommand} = require("../lib/command");
const {startPrompt} = require("../lib/prompt");
addCommand("e", "emote", function () {
if (!comcord.state.currentChannel) {
console.log("<not in a channel>");
return;
}
startPrompt(":emote> ", async function (input) {
if (input == "") {
console.log("<no message sent>");
} 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(`<failed to send message: ${err.message}>`);
}
}
});
});

View file

@ -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");
});

View file

@ -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("<not in a channel>");
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("<not a number>");
return;
}
try {
await getHistory(input);
} catch (err) {
console.log(`<failed to get history: ${err.message}>`);
}
}
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("<not in a guild>");
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("<channel not found>");
} 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("<guild not found>");
} 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("<channel not found>");
} else {
await getHistory(20, target);
}
});
}
});
});

View file

@ -1,55 +0,0 @@
const {addCommand} = require("../lib/command");
function listChannels() {
if (!comcord.state.currentGuild) {
console.log("<not in a guild>");
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,
};

View file

@ -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,
};

View file

@ -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("<not in a guild>");
return;
}
if (!comcord.state.currentChannel) {
console.log("<not in a channel>");
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,
};

View file

@ -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<message not sent to ${channel.recipent?.username ?? "group DM"}>`
);
} else {
try {
await channel.createMessage({content: input});
console.log(
chalk.bold.green(
`\n<message sent to ${channel.recipient?.username ?? "group DM"}>`
)
);
} catch (err) {
console.log(`\n<failed to send message: ${err.message}>`);
}
}
});
}
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(
`<answering ${comcord.state.lastDM.recipient?.username ?? "group DM"}>`
)
);
startDM(comcord.state.lastDM);
} else {
// FIXME: figure out the actual message in com
console.log("<no one to answer>");
}
});

View file

@ -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);
});

View file

@ -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("<not in a channel>");
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("<no message sent>");
} 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("<you have returned>");
updatePresence();
}
} catch (err) {
console.log("<failed to send message: " + err.message + ">");
}
}
}
);
}
module.exports = {sendMode};

View file

@ -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("<channel not found>");
} 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("<not in a guild>");
return;
}
startPrompt(":channel> ", switchChannel);
});

View file

@ -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("<guild not found>");
} 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};

View file

@ -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);

View file

@ -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 = '<script src="(.+?)" integrity=".+?">';
const REGEX_SCRIPT = new RegExp(MATCH_SCRIPT);
const REGEX_SCRIPT_GLOBAL = new RegExp(MATCH_SCRIPT, "g");
async function extractScripts() {
const mainPage = await fetchMainPage();
return mainPage
.match(REGEX_SCRIPT_GLOBAL)
.map((script) => script.match(REGEX_SCRIPT)[1]);
}
const REGEX_BUILD_NUMBER = /Build Number: (\d+), Version Hash:/;
const REGEX_BUILD_NUMBER_SWC = /Build Number: "\).concat\("(\d+)"/;
async function getBuildNumber() {
if (comcord.state.cachedBuildNumber) {
return comcord.state.cachedBuildNumber;
}
const scripts = await extractScripts();
const chunkWithBuildInfoAsset = scripts[3];
const chunkWithBuildInfo = await fetchAsset(chunkWithBuildInfoAsset);
const buildNumber =
chunkWithBuildInfo.match(REGEX_BUILD_NUMBER_SWC)?.[1] ??
chunkWithBuildInfo.match(REGEX_BUILD_NUMBER)?.[1];
comcord.state.cachedBuildNumber = buildNumber;
return buildNumber;
}
/*async function getClientVersion() {
if (comcord.state.cachedClientVersion) {
return comcord.state.cachedClientVersion;
}
const data = await fetch(
"https://updates.discord.com/distributions/app/manifests/latest?channel=stable&platform=win&arch=x86"
).then((res) => res.json());
const clientVersion = data.full.host_version.join(".");
comcord.state.cachedClientVersion = clientVersion;
return clientVersion;
}*/
async function getBrowserInfo() {
let targetOS;
switch (process.platform) {
case "win32":
default:
targetOS = "windows";
break;
case "darwin":
targetOS = "mac os";
break;
case "linux":
targetOS = "linux";
break;
}
const data = await fetch(
`https://cdn.jsdelivr.net/gh/ray-lothian/UserAgent-Switcher/v2/firefox/data/popup/browsers/firefox-${encodeURIComponent(
targetOS
)}.json`
).then((res) => res.json());
data.sort((a, b) => Number(b.browser.major) - Number(a.browser.major));
const target = data[0];
return {ua: target.ua, version: target.browser.version};
}
async function getSuperProperties() {
const buildNumber = await getBuildNumber();
// const clientVersion = await getClientVersion();
const browserInfo = await getBrowserInfo();
let _os;
switch (process.platform) {
case "win32":
default:
_os = "Windows";
break;
case "darwin":
_os = "Mac OS X";
break;
case "linux":
_os = "Linux";
break;
}
const props = {
browser: "Firefox",
browser_user_agent: browserInfo.ua,
browser_version: browserInfo.version,
client_build_number: buildNumber,
client_event_source: null,
device: "",
os: _os,
os_version: os.release(),
//os_arch: os.arch(),
referrer: "",
referrer_current: "",
referring_domain: "",
referring_domain_current: "",
release_channel: "stable",
system_locale: "en-US",
};
return props;
}
module.exports = {getSuperProperties};

View file

@ -1,17 +0,0 @@
function addCommand(key, name, callback) {
if (comcord.commands[key]) {
console.error(
`Registering duplicate key for "${key}": "${name}" wants to overwrite "${comcord.commands[key].name}"!`
);
return;
}
comcord.commands[key] = {
name,
callback,
};
}
module.exports = {
addCommand,
};

View file

@ -1,566 +0,0 @@
const {Constants} = require("@projectdysnomia/dysnomia");
const chalk = require("chalk");
const REGEX_CODEBLOCK = /```(?:([a-z0-9_+\-.]+?)\n)?\n*([^\n][^]*?)\n*```/i;
const REGEX_CODEBLOCK_GLOBAL =
/```(?:[a-z0-9_+\-.]+?\n)?\n*([^\n][^]*?)\n*```/gi;
const REGEX_MENTION = /<@!?(\d+)>/g;
const REGEX_ROLE_MENTION = /<@&?(\d+)>/g;
const REGEX_CHANNEL = /<#(\d+)>/g;
const REGEX_EMOTE = /<(?:\u200b|&)?a?:(\w+):(\d+)>/g;
const REGEX_COMMAND = /<\/([^\s]+?):(\d+)>/g;
const REGEX_BLOCKQUOTE = /^ *>>?>? +/;
const REGEX_GREENTEXT = /^(>.+?)(?:\n|$)/;
const REGEX_SPOILER = /\|\|(.+?)\|\|/;
const REGEX_BOLD = /\*\*(.+?)\*\*/g;
const REGEX_UNDERLINE = /__(.+?)__/g;
const REGEX_ITALIC_1 = /\*(.+?)\*/g;
const REGEX_ITALIC_2 = /_(.+?)_/g;
const REGEX_STRIKE = /~~(.+?)~~/g;
const REGEX_3Y3 = /[\u{e0020}-\u{e007e}]{1,}/gu;
function readableTime(time) {
const seconds = time / 1000;
const minutes = seconds / 60;
const hours = minutes / 60;
const days = hours / 24;
const weeks = days / 7;
const months = days / 30;
const years = days / 365.25;
if (years >= 1) {
return `${years.toFixed(0)} year${years > 1 ? "s" : ""}`;
} else if (weeks > 5 && months < 13) {
return `${months.toFixed(0)} month${months > 1 ? "s" : ""}`;
} else if (days > 7 && weeks < 5) {
return `${weeks.toFixed(0)} week${weeks > 1 ? "s" : ""}`;
} else if (hours > 24 && days < 7) {
return `${days.toFixed(0)} day${days > 1 ? "s" : ""}`;
} else if (minutes > 60 && hours < 24) {
return `${hours.toFixed(0)} hour${hours > 1 ? "s" : ""}`;
} else if (seconds > 60 && minutes < 60) {
return `${minutes.toFixed(0)} minute${minutes > 1 ? "s" : ""}`;
} else {
return `${seconds.toFixed(0)} second${seconds > 1 ? "s" : ""}`;
}
}
const MONTH_NAMES = [
"January",
"Feburary",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const DAY_NAMES = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const TIME_FORMATS = {
t: function (time) {
const timeObj = new Date(time);
return timeObj.getUTCHours() + 1 + ":" + timeObj.getUTCMinutes();
},
T: function (time) {
const timeObj = new Date(time);
return TIME_FORMATS.t(time) + ":" + timeObj.getUTCSeconds();
},
d: function (time) {
const timeObj = new Date(time);
return (
timeObj.getUTCFullYear() +
"/" +
(timeObj.getUTCMonth() + 1).toString().padStart(2, "0") +
"/" +
timeObj.getUTCDate().toString().padStart(2, "0")
);
},
D: function (time) {
const timeObj = new Date(time);
return (
timeObj.getUTCDate() +
" " +
MONTH_NAMES[timeObj.getUTCMonth()] +
" " +
timeObj.getUTCFullYear()
);
},
f: function (time) {
return TIME_FORMATS.D(time) + " " + TIME_FORMATS.t(time);
},
F: function (time) {
const timeObj = new Date(time);
return DAY_NAMES[timeObj.getUTCDay()] + ", " + TIME_FORMATS.f(time);
},
R: function (time) {
const now = Date.now();
if (time > now) {
const delta = time - now;
return "in " + readableTime(delta);
} else {
const delta = now - time;
return readableTime(delta) + " ago";
}
},
};
const REGEX_TIMESTAMP = new RegExp(
`<t:(-?\\d{1,17})(?::(${Object.keys(TIME_FORMATS).join("|")}))?>`,
"g"
);
function replaceMentions(_, id) {
const user = comcord.client.users.get(id);
if (user) {
return `@${user.username}`;
} else {
return "@Unknown User";
}
}
function replaceRoles(_, id) {
const role = comcord.client.guilds
.get(comcord.state.currentGuild)
.roles.get(id);
if (role) {
return `[@${role.name}]`;
} else {
return "[@Unknown Role]";
}
}
function replaceChannels(_, id) {
const guildForChannel = comcord.client.channelGuildMap[id];
if (guildForChannel) {
const channel = comcord.client.guilds.get(guildForChannel).channels.get(id);
if (channel) {
return `#${channel.name}`;
} else {
return "#unknown-channel";
}
} else {
return "#unknown-channel";
}
}
function replaceEmotes(_, name, id) {
return `:${name}:`;
}
function replaceCommands(_, name, id) {
return `/${name}`;
}
function replaceTimestamps(_, time, format = "f") {
return TIME_FORMATS[format](time * 1000);
}
function replaceStyledMarkdown(content) {
content = content.replace(REGEX_BLOCKQUOTE, chalk.blackBright("\u258e"));
content = content.replace(REGEX_GREENTEXT, (orig) => chalk.green(orig));
if (comcord.config.enable3y3) {
content = content.replace(REGEX_3Y3, (text) =>
chalk.italic.magenta(
[...text]
.map((char) => String.fromCodePoint(char.codePointAt(0) - 0xe0000))
.join("")
)
);
}
content = content.replace(REGEX_SPOILER, (_, text) =>
chalk.bgBlack.black(text)
);
content = content.replace(REGEX_STRIKE, (_, text) =>
chalk.strikethrough(text)
);
content = content.replace(REGEX_BOLD, (_, text) => chalk.bold(text));
content = content.replace(REGEX_UNDERLINE, (_, text) =>
chalk.underline(text)
);
content = content
.replace(REGEX_ITALIC_1, (_, text) => chalk.italic(text))
.replace(REGEX_ITALIC_2, (_, text) => chalk.italic(text));
return content;
}
function formatMessage({
channel,
name,
content,
bot,
attachments,
stickers,
reply,
timestamp,
mention = false,
noColor = false,
dump = false,
history = false,
dm = false,
join = false,
pin = false,
}) {
const dateObj = new Date(timestamp);
const hour = dateObj.getUTCHours().toString().padStart(2, "0"),
minutes = dateObj.getUTCMinutes().toString().padStart(2, "0"),
seconds = dateObj.getUTCSeconds().toString().padStart(2, "0");
let console = global.console;
const lines = [];
if (history) {
console = {
log: function (...args) {
lines.push(...args.join(" ").split("\n"));
},
};
}
if (name.length + 2 > comcord.state.nameLength)
comcord.state.nameLength = name.length + 2;
if (reply) {
const nameColor = reply.author.bot ? chalk.bold.yellow : chalk.bold.cyan;
const headerLength = 5 + reply.author.username.length;
let replyContent = reply.content.replace(/\n/g, " ");
replyContent = replyContent
.replace(REGEX_MENTION, replaceMentions)
.replace(REGEX_ROLE_MENTION, replaceRoles)
.replace(REGEX_CHANNEL, replaceChannels)
.replace(REGEX_EMOTE, replaceEmotes)
.replace(REGEX_COMMAND, replaceCommands)
.replace(REGEX_TIMESTAMP, replaceTimestamps);
if (!noColor) {
replyContent = replaceStyledMarkdown(replyContent);
} else {
if (comcord.config.enable3y3) {
replyContent = replyContent.replace(
REGEX_3Y3,
(text) =>
`<3y3:${[...text]
.map((char) =>
String.fromCodePoint(char.codePointAt(0) - 0xe0000)
)
.join("")}>`
);
}
}
if (reply.attachments.size > 0) {
replyContent += ` <${reply.attachments.size} attachment${
reply.attachments.size > 1 ? "s" : ""
}>`;
replyContent = replyContent.trim();
}
const length = headerLength + replyContent.length;
if (noColor) {
console.log(
` \u250d [${reply.author.username}] ${
length > 79
? replyContent.substring(0, 79 - headerLength) + "\u2026"
: replyContent
}`
);
} else {
console.log(
chalk.bold.white(" \u250d ") +
nameColor(`[${reply.author.username}] `) +
`${
length > 79
? replyContent.substring(0, 79 - headerLength) +
chalk.reset("\u2026")
: replyContent
}`
);
}
}
if (dump) {
if (history) {
const headerLength = 80 - (name.length + 5);
console.log(`--- ${name} ${"-".repeat(headerLength)}`);
console.log(content);
console.log(`--- ${name} ${"-".repeat(headerLength)}`);
} else {
const wordCount = content.split(" ").length;
const lineCount = content.split("\n").length;
if (noColor) {
console.log(
`<${name} DUMPs in ${content.length} characters of ${wordCount} word${
wordCount > 1 ? "s" : ""
} in ${lineCount} line${lineCount > 1 ? "s" : ""}>`
);
} else {
console.log(
chalk.bold.yellow(
`<${name} DUMPs in ${
content.length
} characters of ${wordCount} word${
wordCount > 1 ? "s" : ""
} in ${lineCount} line${lineCount > 1 ? "s" : ""}>`
)
);
}
}
} else {
content = content
.replace(REGEX_MENTION, replaceMentions)
.replace(REGEX_ROLE_MENTION, replaceRoles)
.replace(REGEX_CHANNEL, replaceChannels)
.replace(REGEX_EMOTE, replaceEmotes)
.replace(REGEX_COMMAND, replaceCommands)
.replace(REGEX_TIMESTAMP, replaceTimestamps);
if (dm) {
if (noColor) {
if (comcord.config.enable3y3) {
content = content.replace(
REGEX_3Y3,
(text) =>
`<3y3:${[...text]
.map((char) =>
String.fromCodePoint(char.codePointAt(0) - 0xe0000)
)
.join("")}>`
);
}
console.log(`*${name}* ${content}\x07`);
} else {
content = replaceStyledMarkdown(content);
console.log(`${chalk.bold.red(`*${name}*`)} ${content}\x07`);
}
} else if (
content.length > 1 &&
((content.startsWith("*") &&
content.endsWith("*") &&
!content.startsWith("**") &&
!content.endsWith("**")) ||
(content.startsWith("_") &&
content.endsWith("_") &&
!content.startsWith("__") &&
!content.endsWith("__")))
) {
if (comcord.config.enable3y3) {
content = content.replace(
REGEX_3Y3,
(text) =>
`<3y3:${[...text]
.map((char) =>
String.fromCodePoint(char.codePointAt(0) - 0xe0000)
)
.join("")}>`
);
}
const str = `<${name} ${content.substring(1, content.length - 1)}>`;
if (noColor) {
console.log(str);
} else {
console.log(chalk.bold.green(str));
}
} else if (join) {
const str = `[${hour}:${minutes}:${seconds}] ${name} has joined ${channel.guild.name}`;
if (noColor) {
console.log(str);
} else {
console.log(chalk.bold.yellow(str));
}
} else if (pin) {
const str = `[${hour}:${minutes}:${seconds}] ${name} pinned a message to this channel`;
if (noColor) {
console.log(str);
} else {
console.log(chalk.bold.yellow(str));
}
} else {
if (noColor) {
if (comcord.config.enable3y3) {
content = content.replace(
REGEX_3Y3,
(text) =>
`<3y3:${[...text]
.map((char) =>
String.fromCodePoint(char.codePointAt(0) - 0xe0000)
)
.join("")}>`
);
}
console.log(
`[${name}]${" ".repeat(
Math.abs(comcord.state.nameLength - (name.length + 2))
)} ${content}`
);
} else {
const nameColor = mention
? chalk.bold.red
: bot
? chalk.bold.yellow
: chalk.bold.cyan;
content = replaceStyledMarkdown(content);
console.log(
`${nameColor(`[${name}]`)}${" ".repeat(
Math.abs(comcord.state.nameLength - (name.length + 2))
)} ${content}${mention ? "\x07" : ""}`
);
}
}
}
if (attachments) {
for (const attachment of attachments.values()) {
if (noColor) {
console.log(`<attachment: ${attachment.url} >`);
} else {
console.log(chalk.bold.yellow(`<attachment: ${attachment.url} >`));
}
}
}
if (stickers) {
for (const sticker of stickers) {
if (noColor) {
console.log(
`<sticker: "${sticker.name}" https://media.discordapp.net/stickers/${sticker.id}.png >`
);
} else {
console.log(
chalk.bold.yellow(
`<sticker: "${sticker.name}" https://media.discordapp.net/stickers/${sticker.id}.png >`
)
);
}
}
}
if (history) {
return lines;
}
return null;
}
function processMessage(msg, options = {}) {
if (
msg.channel?.type === Constants.ChannelTypes.DM ||
msg.channel?.type === Constants.ChannelTypes.GROUP_DM
) {
options.dm = true;
}
if (msg.type === Constants.MessageTypes.USER_JOIN) {
options.join = true;
} else if (msg.type === Constants.MessageTypes.CHANNEL_PINNED_MESSAGE) {
options.pin = true;
}
if (msg.time) {
console.log(msg.content);
return null;
} else if (msg.ping) {
console.log(
chalk.bold.red(
`**mentioned by ${msg.author?.username ?? "<unknown>"} in #${
msg.channel?.name ?? "<unknown>"
} in ${msg.channel.guild?.name ?? "<unknown>"}**\x07`
)
);
return null;
} else if (msg.content && msg.content.indexOf("\n") > -1) {
if (msg.content.match(REGEX_CODEBLOCK)) {
return formatMessage({
channel: msg.channel,
name: msg.author.username,
bot: msg.author.bot,
content: msg.content.replace(
REGEX_CODEBLOCK_GLOBAL,
(_, content) => content
),
attachments: msg.attachments,
stickers: msg.stickerItems,
reply: msg.referencedMessage,
timestamp: msg.timestamp,
mention:
msg.mentionsEveryone ||
msg.mentions.find((user) => user.id == comcord.client.user.id),
dump: true,
...options,
});
} else {
const lines = msg.content.split("\n");
const outLines = [];
for (const index in lines) {
const line = lines[index];
outLines.push(
formatMessage({
channel: msg.channel,
name: msg.author.username,
bot: msg.author.bot,
content:
line +
(msg.editedTimestamp != null && index == lines.length - 1
? " (edited)"
: ""),
attachments: index == lines.length - 1 ? msg.attachments : [],
stickers: index == lines.length - 1 ? msg.stickerItems : [],
reply: index == 0 ? msg.referencedMessage : null,
timestamp: msg.timestamp,
mention:
index == 0 &&
(msg.mentionsEveryone ||
msg.mentions.find((user) => user.id == comcord.client.user.id)),
...options,
})
);
}
return outLines;
}
} else {
return formatMessage({
channel: msg.channel,
name: msg.author.username,
bot: msg.author.bot,
content: msg.content + (msg.editedTimestamp != null ? " (edited)" : ""),
attachments: msg.attachments,
stickers: msg.stickerItems,
reply: msg.referencedMessage,
timestamp: msg.timestamp,
mention:
msg.mentionsEveryone ||
msg.mentions.find((user) => user.id == comcord.client.user.id),
...options,
});
}
}
function processQueue() {
for (const msg of comcord.state.messageQueue) {
processMessage(msg);
}
comcord.state.messageQueue.splice(0, comcord.state.messageQueue.length);
}
module.exports = {
processMessage,
processQueue,
formatMessage,
};

View file

@ -1,89 +0,0 @@
const CLIENT_ID = "1026163285877325874";
function updatePresence() {
let guild, channel;
if (comcord.state.currentGuild != null) {
guild = comcord.client.guilds.get(comcord.state.currentGuild);
}
if (comcord.state.currentChannel != null && guild != null) {
channel = guild.channels.get(comcord.state.currentChannel);
}
if (comcord.client.user.bot) {
if (comcord.state.rpcConnected) {
try {
const activity = {
startTimestamp: comcord.state.startTime,
smallImageKey: `https://cdn.discordapp.com/avatars/${comcord.client.user.id}/${comcord.client.user.avatar}.png?size=1024`,
smallImageText: comcord.client.user.username,
buttons: [
{
label: "comcord Repo",
url: "https://github.com/Cynosphere/comcord",
},
],
};
if (guild != null) {
activity.largeImageKey = `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png?size=1024`;
activity.largeImageText = guild.name;
if (channel != null) {
activity.details = `#${channel.name} - ${guild.name}`;
}
}
if (comcord.state.afk == true) {
activity.state = "AFK";
}
comcord.rpc.setActivity(activity);
} catch (err) {
//
}
}
comcord.client.editStatus(
comcord.state.afk ? "idle" : comcord.config.defaultStatus ?? "online",
[
{
name: "comcord",
type: 0,
application_id: CLIENT_ID,
timestamps: {
start: comcord.state.startTime,
},
},
]
);
} else {
const activity = {
application_id: CLIENT_ID,
name: "comcord",
timestamps: {
start: comcord.state.startTime,
},
assets: {},
buttons: ["comcord Repo"],
metadata: {
button_urls: ["https://github.com/Cynosphere/comcord"],
},
type: 0,
};
if (guild != null) {
activity.assets.large_image = `mp:icons/${guild.id}/${guild.icon}.png?size=1024`;
activity.assets.large_text = guild.name;
if (channel != null) {
activity.details = `#${channel.name} - ${guild.name}`;
}
}
if (comcord.state.afk == true) {
activity.state = "AFK";
}
comcord.client.editStatus(
comcord.state.afk ? "idle" : comcord.config.defaultStatus ?? "online",
[activity]
);
}
}
module.exports = {updatePresence};

View file

@ -1,22 +0,0 @@
function startPrompt(display, callback) {
comcord.state.inPrompt = true;
comcord.state.promptText = display;
comcord.state.promptInput = "";
comcord.state.promptCallback = callback;
process.stdout.write(display);
}
async function finalizePrompt() {
comcord.state.inPrompt = false;
comcord.state.promptText = null;
const input = comcord.state.promptInput.trim();
await comcord.state.promptCallback(input);
}
module.exports = {
startPrompt,
finalizePrompt,
};

View file

@ -1,30 +0,0 @@
const fs = require("fs");
const path = require("path");
const os = require("os");
const RCPATH = path.resolve(os.homedir(), ".comcordrc");
function readFile(config) {
const rc = fs.readFileSync(RCPATH, "utf8");
const lines = rc.split("\n");
for (const line of lines) {
const [key, value] = line.split("=");
config[key] = value;
}
}
function writeFile(config) {
if (fs.existsSync(RCPATH)) {
readFile(config);
}
const newrc = [];
for (const key in config) {
const value = config[key];
newrc.push(`${key}=${value}`);
}
fs.writeFileSync(RCPATH, newrc.join("\n"));
}
module.exports = {readFile, writeFile, path: RCPATH};

181
state/main.go Normal file
View file

@ -0,0 +1,181 @@
package state
import (
"time"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/ningen/v3"
)
type ComcordState struct {
Client *ningen.State
Config map[string]string
Readied bool
Connected bool
RPCConnected bool
StartTime time.Time
CurrentGuild string
CurrentChannel string
NameLength int
InPrompt bool
PromptText string
AFK bool
MessageQueue []discord.Message
LastChannel map[string]string
LastDM string
NoColor bool
}
var state ComcordState
func Setup(config map[string]string, client *ningen.State) {
state = ComcordState{}
state.Client = client
state.Config = config
state.Readied = false
state.Connected = true
state.RPCConnected = false
state.StartTime = time.Now()
state.CurrentGuild = ""
state.CurrentChannel = ""
state.NameLength = 2
state.InPrompt = false
state.PromptText = ""
state.AFK = false
state.MessageQueue = make([]discord.Message, 0)
state.LastChannel = make(map[string]string)
state.LastDM = ""
state.NoColor = false
}
func GetClient() *ningen.State {
return state.Client
}
func HasReadied() bool {
return state.Readied
}
func SetReadied(value bool) {
state.Readied = value
}
func IsConnected() bool {
return state.Connected
}
func SetConnected(value bool) {
state.Connected = value
}
func IsRPCConnected() bool {
return state.RPCConnected
}
func SetRPCConnected(value bool) {
state.RPCConnected = value
}
func GetStartTime() time.Time {
return state.StartTime
}
func GetCurrentGuild() string {
return state.CurrentGuild
}
func SetCurrentGuild(value string) {
state.CurrentGuild = value
}
func GetCurrentChannel() string {
return state.CurrentChannel
}
func SetCurrentChannel(value string) {
state.CurrentChannel = value
}
func GetNameLength() int {
return state.NameLength
}
func SetNameLength(value int) {
state.NameLength = value
}
func IsInPrompt() bool {
return state.InPrompt
}
func SetInPrompt(value bool) {
state.InPrompt = value
}
func GetPromptText() string {
return state.PromptText
}
func SetPromptText(value string) {
state.PromptText = value
}
func IsAFK() bool {
return state.AFK
}
func SetAFK(value bool) {
state.AFK = value
}
func GetMessageQueue() []discord.Message {
return state.MessageQueue
}
func AddMessageToQueue(msg discord.Message) {
state.MessageQueue = append(state.MessageQueue, msg)
}
func EmptyMessageQueue() {
state.MessageQueue = make([]discord.Message, 0)
}
func SetLastChannel(guild string, channel string) {
state.LastChannel[guild] = channel
}
func GetLastChannel(guild string) string {
channel, has := state.LastChannel[guild]
if has {
return channel
} else {
return ""
}
}
func GetLastDM() string {
return state.LastDM
}
func SetLastDM(value string) {
state.LastDM = value
}
func GetConfigValue(key string) string {
value, has := state.Config[key]
if has {
return value
} else {
return ""
}
}
func SetNoColor(value bool) {
state.NoColor = value
}
func HasNoColor() bool {
return state.NoColor
}