mirror of https://github.com/Bios-Marcel/cordless
353 lines
8.9 KiB
Go
353 lines
8.9 KiB
Go
package readstate
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Bios-Marcel/discordgo"
|
|
)
|
|
|
|
var (
|
|
data = make(map[string]uint64)
|
|
mentions = make(map[string]bool)
|
|
readStateMutex = &sync.Mutex{}
|
|
timerMutex = &sync.Mutex{}
|
|
ackTimers = make(map[string]*time.Timer)
|
|
state *discordgo.State
|
|
)
|
|
|
|
// Load loads the locally saved readmarkers returning an error if this failed.
|
|
func Load(sessionState *discordgo.State) {
|
|
state = sessionState
|
|
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
for _, channelState := range sessionState.ReadState {
|
|
lastMessageID := channelState.GetLastMessageID()
|
|
if lastMessageID == "" {
|
|
continue
|
|
}
|
|
|
|
parsed, parseError := strconv.ParseUint(lastMessageID, 10, 64)
|
|
if parseError != nil {
|
|
continue
|
|
}
|
|
|
|
data[channelState.ID] = parsed
|
|
|
|
if channelState.MentionCount > 0 {
|
|
mentions[channelState.ID] = true
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// ClearReadStateFor clears all entries for the given Channel.
|
|
func ClearReadStateFor(channelID string) {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
timerMutex.Lock()
|
|
defer timerMutex.Unlock()
|
|
|
|
delete(data, channelID)
|
|
delete(ackTimers, channelID)
|
|
}
|
|
|
|
// UpdateReadLocal can be used to locally update the data without sending
|
|
// anything to the Discord API. The update will only be applied if the new
|
|
// message ID is greater than the old one.
|
|
func UpdateReadLocal(channelID string, lastMessageID string) bool {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
delete(mentions, channelID)
|
|
|
|
parsed, parseError := strconv.ParseUint(lastMessageID, 10, 64)
|
|
if parseError != nil {
|
|
return false
|
|
}
|
|
|
|
old, isPresent := data[channelID]
|
|
if !isPresent || old < parsed {
|
|
data[channelID] = parsed
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// UpdateRead tells the discord server that a channel has been read. If the
|
|
// channel has already been read and this method was called needlessly, then
|
|
// this will be a No-OP.
|
|
func UpdateRead(session *discordgo.Session, channel *discordgo.Channel, lastMessageID string) error {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
delete(mentions, channel.ID)
|
|
|
|
// Avoid unnecessary traffic
|
|
if hasBeenReadWithoutLocking(channel, lastMessageID) {
|
|
return nil
|
|
}
|
|
|
|
parsed, parseError := strconv.ParseUint(lastMessageID, 10, 64)
|
|
if parseError != nil {
|
|
return parseError
|
|
}
|
|
|
|
data[channel.ID] = parsed
|
|
|
|
_, ackError := session.ChannelMessageAck(channel.ID, lastMessageID, "")
|
|
return ackError
|
|
}
|
|
|
|
// UpdateReadBuffered triggers an acknowledgement after a certain amount of
|
|
// seconds. If this message is called again during that time, the timer will
|
|
// be reset. This avoid unnecessarily many calls to the Discord servers.
|
|
func UpdateReadBuffered(session *discordgo.Session, channel *discordgo.Channel, lastMessageID string) {
|
|
timerMutex.Lock()
|
|
timerMutex.Unlock()
|
|
|
|
ackTimer := ackTimers[channel.ID]
|
|
if ackTimer == nil {
|
|
newTimer := time.NewTimer(4 * time.Second)
|
|
ackTimers[channel.ID] = newTimer
|
|
go func() {
|
|
<-newTimer.C
|
|
ackTimers[channel.ID] = nil
|
|
UpdateRead(session, channel, lastMessageID)
|
|
}()
|
|
} else {
|
|
ackTimer.Reset(4 * time.Second)
|
|
}
|
|
}
|
|
|
|
// IsGuildMuted returns whether the user muted the given guild.
|
|
func IsGuildMuted(guildID string) bool {
|
|
for _, settings := range state.UserGuildSettings {
|
|
if settings.GuildID == guildID {
|
|
if settings.Muted && isStillMuted(settings.MuteConfig) {
|
|
return true
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// HasGuildBeenRead returns true if the guild has no unread messages or is
|
|
// muted.
|
|
func HasGuildBeenRead(guildID string) bool {
|
|
if IsGuildMuted(guildID) {
|
|
return true
|
|
}
|
|
|
|
realGuild, cacheError := state.Guild(guildID)
|
|
if cacheError == nil {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
for _, channel := range realGuild.Channels {
|
|
if !hasReadMessagesPermission(channel.ID, state) {
|
|
continue
|
|
}
|
|
|
|
if !hasBeenReadWithoutLocking(channel, channel.LastMessageID) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
//HACK Had to copy this from discordutil/channel.go due to import cycle.
|
|
func hasReadMessagesPermission(channelID string, state *discordgo.State) bool {
|
|
userPermissions, err := state.UserChannelPermissions(state.User.ID, channelID)
|
|
if err != nil {
|
|
// Unable to access channel permissions.
|
|
return false
|
|
}
|
|
return (userPermissions & discordgo.PermissionViewChannel) > 0
|
|
}
|
|
|
|
// HasGuildBeenMentioned checks whether any channel in the guild mentioned
|
|
// the currently logged in user.
|
|
func HasGuildBeenMentioned(guildID string) bool {
|
|
if IsGuildMuted(guildID) {
|
|
return false
|
|
}
|
|
|
|
realGuild, cacheError := state.Guild(guildID)
|
|
if cacheError == nil {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
for _, channel := range realGuild.Channels {
|
|
if hasBeenMentionedWithoutLocking(channel.ID) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isStillMuted(config *discordgo.MuteConfig) bool {
|
|
if config == nil || config.EndTime == "" {
|
|
//This means permanently muted; I think!
|
|
//We make the assumption that this function is only
|
|
//called if "Muted" is set to "true". Therefore no timeframe means
|
|
//we must be permanently muted.
|
|
return true
|
|
}
|
|
|
|
muteEndTime, parseError := config.EndTime.Parse()
|
|
if parseError != nil {
|
|
panic(parseError)
|
|
}
|
|
|
|
return time.Now().UTC().Before(muteEndTime)
|
|
}
|
|
|
|
func isChannelMuted(channel *discordgo.Channel) bool {
|
|
//optimization for the case of guild channels, as the handling for
|
|
//private channels will be unnecessarily slower.
|
|
if channel.GuildID == "" {
|
|
return IsPrivateChannelMuted(channel)
|
|
}
|
|
|
|
return IsGuildChannelMuted(channel)
|
|
}
|
|
|
|
// IsGuildChannelMuted checks whether a guild channel has been set to silent.
|
|
func IsGuildChannelMuted(channel *discordgo.Channel) bool {
|
|
if isGuildChannelMuted(channel.GuildID, channel.ID) {
|
|
return true
|
|
}
|
|
|
|
//Check if Parent (CATEGORY) is muted
|
|
if channel.ParentID != "" && isGuildChannelMuted(channel.GuildID, channel.ParentID) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func isGuildChannelMuted(guildID, channelID string) bool {
|
|
for _, settings := range state.UserGuildSettings {
|
|
if settings.GetGuildID() == guildID {
|
|
for _, override := range settings.ChannelOverrides {
|
|
if override.ChannelID == channelID {
|
|
if override.Muted && isStillMuted(override.MuteConfig) {
|
|
return true
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsPrivateChannelMuted checks whether a private channel has been set to
|
|
// silent.
|
|
func IsPrivateChannelMuted(channel *discordgo.Channel) bool {
|
|
for _, settings := range state.UserGuildSettings {
|
|
//Discord holds the mute settings for private channels in the user-guildsettings
|
|
//but for an empty Guild ID. Doesn't really make sense, but ¯\_(ツ)_/¯
|
|
if settings.GetGuildID() == "" {
|
|
for _, override := range settings.ChannelOverrides {
|
|
if override.ChannelID == channel.ID {
|
|
if override.Muted && isStillMuted(override.MuteConfig) {
|
|
return true
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
//No break here, since it can happen that there are multiple
|
|
//instances of UserGuildSettings for non guilds ... don't ask
|
|
//me why ...
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// HasBeenRead checks whether the passed channel has an unread Message or not.
|
|
func HasBeenRead(channel *discordgo.Channel, lastMessageID string) bool {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
return hasBeenReadWithoutLocking(channel, lastMessageID)
|
|
}
|
|
|
|
// HasBeenMentioned checks whether the currently logged in user has been
|
|
// mentioned in this channel.
|
|
func HasBeenMentioned(channelID string) bool {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
return hasBeenMentionedWithoutLocking(channelID)
|
|
}
|
|
|
|
func hasBeenMentionedWithoutLocking(channelID string) bool {
|
|
mentioned, ok := mentions[channelID]
|
|
return ok && mentioned
|
|
}
|
|
|
|
// MarkAsMentioned sets the given channel ID to mentioned.
|
|
func MarkAsMentioned(channelID string) {
|
|
readStateMutex.Lock()
|
|
defer readStateMutex.Unlock()
|
|
|
|
mentions[channelID] = true
|
|
}
|
|
|
|
// hasBeenReadWithoutLocking checks whether the passed channel has an unread Message or not.
|
|
// The difference to HasBeenRead is, that no locking happens. This is inteded to be used
|
|
// for recursive calls to this method and avoiding lock overhead and deadlocks.
|
|
func hasBeenReadWithoutLocking(channel *discordgo.Channel, lastMessageID string) bool {
|
|
if lastMessageID == "" {
|
|
return true
|
|
}
|
|
|
|
if isChannelMuted(channel) {
|
|
return true
|
|
}
|
|
|
|
// If there was no message, lastMessageID would've been empty, therefore
|
|
// this check only makes sense if the cache is filled already.
|
|
if len(channel.Messages) > 0 {
|
|
lastMessage := channel.Messages[len(channel.Messages)-1]
|
|
//I once had a crash here running into a nil-dereference, so I assume the author must've been null.
|
|
if lastMessage != nil && lastMessage.Author != nil && lastMessage.Author.ID == state.User.ID {
|
|
return true
|
|
}
|
|
}
|
|
|
|
data, present := data[channel.ID]
|
|
if !present {
|
|
//We return true as there are too many false-positive otherwise and damn, that shit is annoying.
|
|
return true
|
|
}
|
|
|
|
parsed, parseError := strconv.ParseUint(lastMessageID, 10, 64)
|
|
if parseError != nil {
|
|
return true
|
|
}
|
|
|
|
return data >= parsed
|
|
}
|