diff --git a/commands/guild.go b/commands/guild.go index 75b132b..97c182c 100644 --- a/commands/guild.go +++ b/commands/guild.go @@ -442,7 +442,7 @@ func SwitchGuild(session *discordgo.Session, input string) { ListChannelsCommand(session) ListUsersCommand(session) - // TODO: update presence + lib.UpdatePresence(session) } } } @@ -486,7 +486,7 @@ func SwitchChannelsCommand(session *discordgo.Session) { ListUsersCommand(session) - // TODO: update presence + lib.UpdatePresence(session) } } }) diff --git a/lib/presence.go b/lib/presence.go new file mode 100644 index 0000000..e057411 --- /dev/null +++ b/lib/presence.go @@ -0,0 +1,159 @@ +package lib + +import ( + "fmt" + "reflect" + "sync" + "time" + "unsafe" + + "github.com/Cynosphere/comcord/state" + "github.com/bwmarrin/discordgo" + "github.com/gorilla/websocket" +) + +type ActivityMetadata struct { + ButtonURLs []string `json:"button_urls,omitempty"` +} + +type Activity struct { + Name string `json:"name"` + Type discordgo.ActivityType `json:"type"` + //URL string `json:"url,omitempty"` + CreatedAt time.Time `json:"created_at"` + ApplicationID string `json:"application_id,omitempty"` + State string `json:"state,omitempty"` + Details string `json:"details,omitempty"` + Timestamps discordgo.TimeStamps `json:"timestamps,omitempty"` + //Emoji discordgo.Emoji `json:"emoji,omitempty"` + //Party discordgo.Party `json:"party,omitempty"` + Assets discordgo.Assets `json:"assets,omitempty"` + //Secrets discordgo.Secrets `json:"secrets,omitempty"` + //Instance bool `json:"instance,omitempty"` + //Flags int `json:"flags,omitempty"` + Buttons []string `json:"buttons,omitempty"` + Metadata ActivityMetadata `json:"metadata,omitempty"` +} + +type GatewayPresenceUpdate struct { + Since int `json:"since"` + Activities []Activity `json:"activities,omitempty"` + Status string `json:"status"` + AFK bool `json:"afk"` + Broadcast string `json:"broadcast,omitempty"` +} + +type presenceOp struct { + Op int `json:"op"` + Data GatewayPresenceUpdate `json:"d"` +} + +func getUnexportedField(field reflect.Value) interface{} { + return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() +} + +func UpdatePresence(session *discordgo.Session) { + // there is a way to send presence without reflecting to grab the websocket + // connection, but theres an issue with the serialization that because a value + // isn't being considered "null" that its trying to apply and failing because + // the default doesn't make sense in this context, even if omitempty is set + // + // this doesnt happen with bot accounts because they have certain fields + // stripped + values := reflect.ValueOf(session) + fieldWsConn := reflect.Indirect(values).FieldByName("wsConn") + fieldWsMutex := reflect.Indirect(values).FieldByName("wsMutex") + + wsConn := getUnexportedField(fieldWsConn).(*websocket.Conn) + wsMutex := getUnexportedField(fieldWsMutex).(sync.Mutex) + + afk := state.IsAFK() + presence := GatewayPresenceUpdate{ + Since: 0, + AFK: afk, + Activities: make([]Activity, 0), + } + + currentGuild := state.GetCurrentGuild() + currentChannel := state.GetCurrentChannel() + + var activity Activity + + startTime := state.GetStartTime() + + if session.State.User.Bot { + activity = Activity{ + Type: 0, + Name: "comcord", + } + + if currentGuild != "" && currentChannel != "" { + guild, guildErr := session.State.Guild(currentGuild) + channel, channelErr := session.State.Channel(currentChannel) + + if guildErr == nil && channelErr == nil { + activity.Type = 3 + activity.Name = fmt.Sprintf("#%s in %s | comcord", channel.Name, guild.Name) + } + } + + if afk { + activity.Name = activity.Name + " [AFK]" + } + } else { + activity = Activity{ + Type: 0, + ApplicationID: "1026163285877325874", + Name: "comcord", + Timestamps: discordgo.TimeStamps{ + StartTimestamp: 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 := session.State.Guild(currentGuild) + channel, channelErr := session.State.Channel(currentChannel) + + if guildErr == nil && channelErr == nil { + activity.Details = fmt.Sprintf("#%s - %s", channel.Name, guild.Name) + + activity.Assets = discordgo.Assets{} + activity.Assets.LargeText = guild.Name + if guild.Icon != "" { + activity.Assets.LargeImageID = fmt.Sprintf("mp:icons/%s/%s.png?size=1024", guild.ID, guild.Icon) + } + } + } + + if afk { + activity.State = "AFK" + } + } + + activity.CreatedAt = startTime + + presence.Activities = append(presence.Activities, activity) + + defaultStatus := state.GetConfigValue("defaultStatus") + if defaultStatus != "" { + presence.Status = defaultStatus + } else { + if afk { + presence.Status = "idle" + } else { + presence.Status = "online" + } + } + + op := presenceOp{3, presence} + wsMutex.Lock() + wsConn.WriteJSON(op) + wsMutex.Unlock() +} diff --git a/main.go b/main.go index 0b8bdde..8a98643 100644 --- a/main.go +++ b/main.go @@ -63,8 +63,15 @@ func main() { state.Setup(config) commands.Setup() - // TODO: user account support - client, err := discordgo.New("Bot " + token) + allowUserAccounts := config["allowUserAccounts"] == "true" + tokenPrefix := "Bot " + if allowUserAccounts { + tokenPrefix = "" + } + + fullToken := tokenPrefix + token + + client, err := discordgo.New(fullToken) if err != nil { fmt.Println("% Failed to create client:", err) fmt.Print("\r") @@ -72,22 +79,41 @@ func main() { return } - // TODO: dont set for user accounts(? never really tested if it matters) + //client.LogLevel = discordgo.LogDebug + client.Identify.Intents = discordgo.IntentsAll - if config["useMobile"] == "true" { - client.Identify.Properties = discordgo.IdentifyProperties{ - OS: "Android", - Browser: "Discord Android", - Device: "Pixel, raven", - } + client.Identify.Properties = discordgo.IdentifyProperties{ + OS: runtime.GOOS, + } + statusType := config["statusType"] + if statusType == "mobile" { + client.Identify.Properties.Browser = "Discord Android" + } else if statusType == "embedded" { + client.Identify.Properties.Browser = "Discord Embedded" + } else if statusType == "desktop" { + client.Identify.Properties.Browser = "Discord Client" } else { - // TODO: user account support - client.Identify.Properties = discordgo.IdentifyProperties{ - OS: runtime.GOOS, - Browser: "comcord", - Device: "comcord", - } + client.Identify.Properties.Browser = "comcord" + } + + status := "online" + defaultStatus := config["defaultStatus"] + if defaultStatus != "" { + status = defaultStatus + } + startTime := state.GetStartTime() + + client.Identify.Presence = discordgo.GatewayStatusUpdate{ + Since: 0, + Status: status, + AFK: false, + Game: discordgo.Activity{ + Type: 0, + Name: "comcord", + ApplicationID: "1026163285877325874", + CreatedAt: startTime, + }, } events.Setup(client) diff --git a/state/main.go b/state/main.go index cd63ec8..2949db8 100644 --- a/state/main.go +++ b/state/main.go @@ -1,16 +1,16 @@ package state import ( - "time" + "time" - "github.com/bwmarrin/discordgo" + "github.com/bwmarrin/discordgo" ) type ComcordState struct { Config map[string]string Connected bool RPCConnected bool - StartTime int64 + StartTime time.Time CurrentGuild string CurrentChannel string NameLength int @@ -30,7 +30,7 @@ func Setup(config map[string]string) { state.Config = config state.Connected = true state.RPCConnected = false - state.StartTime = time.Now().Unix() + state.StartTime = time.Now() state.CurrentGuild = "" state.CurrentChannel = "" state.NameLength = 2 @@ -59,7 +59,7 @@ func SetRPCConnected(value bool) { state.RPCConnected = value } -func GetStartTime() int64 { +func GetStartTime() time.Time { return state.StartTime }