ProjectCacophony/Gateway

View on GitHub
pkg/handler/handler.go

Summary

Maintainability
C
1 day
Test Coverage
package handler

import (
    "context"
    "time"

    "github.com/bwmarrin/discordgo"
    raven "github.com/getsentry/raven-go"
    "github.com/go-redis/redis"
    "gitlab.com/Cacophony/Gateway/pkg/whitelist"
    "gitlab.com/Cacophony/go-kit/events"
    "gitlab.com/Cacophony/go-kit/state"
    "go.opentelemetry.io/contrib/propagators/b3"
    "go.opentelemetry.io/otel/api/global"
    "go.uber.org/zap"
)

var b3Prop = b3.B3{}

// EventHandler handles discord events and puts them into rabbitMQ
type EventHandler struct {
    logger                   *zap.Logger
    redisClient              *redis.Client
    publisher                *events.Publisher
    checker                  *whitelist.Checker
    state                    *state.State
    requestGuildMembersDelay time.Duration
    deduplicate              bool
}

// NewEventHandler creates a new EventHandler
func NewEventHandler(
    logger *zap.Logger,
    redisClient *redis.Client,
    publisher *events.Publisher,
    checker *whitelist.Checker,
    state *state.State,
    deduplicate bool,
    requestGuildMembersDelay time.Duration,
) *EventHandler {
    return &EventHandler{
        logger:                   logger,
        redisClient:              redisClient,
        publisher:                publisher,
        checker:                  checker,
        state:                    state,
        deduplicate:              deduplicate,
        requestGuildMembersDelay: requestGuildMembersDelay,
    }
}

// OnDiscordEvent receives discord events
func (eh *EventHandler) OnDiscordEvent(session *discordgo.Session, eventItem interface{}) {
    var err error

    if session == nil || session.State == nil || session.State.User == nil {
        return
    }

    ready, ok := eventItem.(*discordgo.Ready)
    if ok {
        go eh.requestGuildMembers(session, ready)
    }

    event, expiration, err := events.GenerateEventFromDiscordgoEvent(
        session.State.User.ID,
        eventItem,
    )
    if err != nil {
        raven.CaptureError(err, nil)
        eh.logger.Error("unable to generate event",
            zap.Error(err),
            zap.Any("event", eventItem),
        )

        err = eh.state.SharedStateEventHandler(session, eventItem)
        if err != nil {
            raven.CaptureError(err, nil)
            eh.logger.Error("state client failed to handle event", zap.Error(err))
        }
        return
    }

    if event == nil {
        err = eh.state.SharedStateEventHandler(session, eventItem)
        if err != nil {
            raven.CaptureError(err, nil)
            eh.logger.Error("state client failed to handle event", zap.Error(err))
        }
        return
    }

    ctx, span := global.Tracer("cacophony.dev/gateway").Start(context.Background(), "Recv.Discord")
    defer span.End()

    b3Prop.Inject(ctx, &event.SpanContext)
    span.SetAttributes(
        events.SpanLabelEventingType.String(string(event.Type)),
        events.SpanLabelDiscordBotUserID.String(event.BotUserID),
        events.SpanLabelDiscordGuildID.String(event.GuildID),
        events.SpanLabelDiscordChannelID.String(event.ChannelID),
        events.SpanLabelDiscordUserID.String(event.UserID),
        events.SpanLabelDiscordMessageID.String(event.MessageID),
    )

    l := eh.logger.With(
        zap.String("event_id", event.ID),
        zap.String("event_type", string(event.Type)),
        zap.String("event_guild_id", event.GuildID),
        zap.String("event_user_id", event.UserID),
        zap.String("event_bot_user_id", event.BotUserID),
    )

    if event.GuildID != "" && eh.checker.IsBlacklisted(event.GuildID) {
        return
    }

    var oldGuild *discordgo.Guild
    var oldMember *discordgo.Member
    var oldChannel *discordgo.Channel
    var oldRole *discordgo.Role
    var oldEmoji []*discordgo.Emoji
    var oldWebhooks []*discordgo.Webhook
    // var oldInvites []*discordgo.Invite
    switch event.Type {
    case events.GuildUpdateType:
        oldGuild, _ = eh.state.Guild(event.GuildID)
    case events.GuildMemberUpdateType:
        oldMember, _ = eh.state.Member(event.GuildID, event.GuildMemberUpdate.Member.User.ID)
    case events.ChannelUpdateType:
        oldChannel, _ = eh.state.Channel(event.ChannelUpdate.ID)
    case events.ChannelDeleteType:
        oldChannel, _ = eh.state.Channel(event.ChannelDelete.ID)
    case events.GuildRoleUpdateType:
        oldRole, _ = eh.state.Role(event.GuildID, event.GuildRoleUpdate.Role.ID)
    case events.GuildRoleDeleteType:
        oldRole, _ = eh.state.Role(event.GuildID, event.GuildRoleDelete.RoleID)
    case events.GuildEmojisUpdateType:
        guild, _ := eh.state.Guild(event.GuildID)
        if guild != nil {
            oldEmoji = guild.Emojis
        }
    case events.WebhooksUpdateType:
        oldWebhooks, _ = eh.state.GuildWebhooks(event.GuildID)
    case events.GuildMemberAddType:
        // oldInvites, _ = eh.state.GuildInvites(event.GuildID)
    }

    if eh.deduplicate {
        duplicate, err := eh.IsDuplicate(event.CacheKey, expiration)
        if err != nil {
            raven.CaptureError(err, nil)
            l.Debug("unable to deduplicate event",
                zap.Error(err),
                zap.Any("event", eventItem),
            )

            err = eh.state.SharedStateEventHandler(session, eventItem)
            if err != nil {
                raven.CaptureError(err, nil)
                l.Error("state client failed to handle event", zap.Error(err))
            }
            return
        }
        if duplicate {
            l.Debug("skipping event, as it is a duplicate", zap.String("cache_key", event.CacheKey))
            return
        }
    }

    err = eh.state.SharedStateEventHandler(session, eventItem)
    if err != nil {
        raven.CaptureError(err, nil)
        l.Error("state client failed to handle event", zap.Error(err))
    }

    if event.GuildID != "" && !eh.checker.IsWhitelisted(event.GuildID) {
        l.Debug("skipping publishing event because guild is not whitelisted")
        return
    }

    var diffEvent *events.Event
    switch event.Type {
    case events.GuildUpdateType:
        newGuild, _ := eh.state.Guild(event.GuildID)
        diffEvent, err = guildDiff(oldGuild, newGuild)
    case events.GuildMemberUpdateType:
        newMember, _ := eh.state.Member(event.GuildID, event.GuildMemberUpdate.Member.User.ID)
        diffEvent, err = memberDiff(oldMember, newMember)
    case events.ChannelUpdateType:
        newChannel, _ := eh.state.Channel(event.ChannelUpdate.ID)
        diffEvent, err = channelDiff(oldChannel, newChannel)
    case events.ChannelDeleteType:
        newChannel, _ := eh.state.Channel(event.ChannelDelete.ID)
        diffEvent, err = channelDiff(oldChannel, newChannel)
    case events.GuildRoleUpdateType:
        newRole, _ := eh.state.Role(event.GuildID, event.GuildRoleUpdate.Role.ID)
        diffEvent, err = roleDiff(event.GuildID, oldRole, newRole)
    case events.GuildRoleDeleteType:
        newRole, _ := eh.state.Role(event.GuildID, event.GuildRoleDelete.RoleID)
        diffEvent, err = roleDiff(event.GuildID, oldRole, newRole)
    case events.GuildEmojisUpdateType:
        guild, _ := eh.state.Guild(event.GuildID)
        if guild != nil {
            newEmoji := guild.Emojis
            diffEvent, err = emojiDiff(event.GuildID, oldEmoji, newEmoji)
        }
    case events.WebhooksUpdateType:
        newWebhooks, _ := eh.state.GuildWebhooks(event.WebhooksUpdate.GuildID)
        diffEvent, err = webhooksDiff(event.GuildID, oldWebhooks, newWebhooks)
    case events.GuildMemberAddType:
        // disables this, calling invites endpoint every time someone joins a guild is not sustainable
        // go func(guildID string, oldInvites []*discordgo.Invite, memberAdd *discordgo.GuildMemberAdd) {
        //     newInvites, err := eh.state.GuildInvitesWithRefresh(guildID, session)
        //     if err != nil {
        //         raven.CaptureError(err, nil)
        //         l.Error("failure getting new invites", zap.Error(err))
        //     }
        //
        //     extraEvent, err := events.New(events.CacophonyGuildMemberAddExtra)
        //     if err != nil {
        //         raven.CaptureError(err, nil)
        //         l.Error("failure generating member add extra event", zap.Error(err))
        //         return
        //     }
        //     extraEvent.GuildID = guildID
        //     extraEvent.GuildMemberAddExtra = &events.GuildMemberAddExtra{
        //         GuildMemberAdd: memberAdd,
        //     }
        //
        //     var diffEvent *events.Event
        //     if newInvites != nil {
        //         diffEvent, err = invitesDiff(guildID, oldInvites, newInvites)
        //         if err != nil {
        //             raven.CaptureError(err, nil)
        //             l.Error("failure generating invites diff", zap.Error(err))
        //             return
        //         }
        //     }
        //
        //     new, updated, removed := compareInvitesDiff(diffEvent.DiffInvites)
        //     if len(new) <= 0 && len(updated) <= 0 && len(removed) <= 0 {
        //         usedInvite := inviteDiffFindUsed(diffEvent.DiffInvites)
        //         if usedInvite != nil {
        //             extraEvent.GuildMemberAddExtra.UsedInvite = usedInvite
        //         }
        //
        //         // no change except possibly uses, do not send invites diff
        //         diffEvent = nil
        //     }
        //
        //     if extraEvent != nil {
        //         err, recoverable := eh.publisher.Publish(
        //             context.TODO(),
        //             extraEvent,
        //         )
        //         if err != nil {
        //             raven.CaptureError(err, nil)
        //             if !recoverable {
        //                 l.Fatal("unrecoverable publishing error, shutting down",
        //                     zap.Error(err),
        //                 )
        //             }
        //             l.Error("unable to publish event",
        //                 zap.Error(err),
        //             )
        //             return
        //         }
        //
        //         l.Debug("published member add extra event")
        //     }
        //
        //     if diffEvent != nil {
        //         err, recoverable := eh.publisher.Publish(
        //             context.TODO(),
        //             diffEvent,
        //         )
        //         if err != nil {
        //             raven.CaptureError(err, nil)
        //             if !recoverable {
        //                 l.Fatal("unrecoverable publishing error, shutting down",
        //                     zap.Error(err),
        //                 )
        //             }
        //             l.Error("unable to publish event",
        //                 zap.Error(err),
        //             )
        //             return
        //         }
        //
        //         l.Debug("published invite diff event")
        //     }
        //
        // }(event.GuildID, oldInvites, event.GuildMemberAdd)
    }
    if err != nil {
        raven.CaptureError(err, nil)
    }

    err, recoverable := eh.publisher.Publish(
        ctx,
        event,
    )
    if err != nil {
        raven.CaptureError(err, nil)
        if !recoverable {
            l.Fatal("unrecoverable publishing error, shutting down",
                zap.Error(err),
            )
        }
        l.Error("unable to publish event",
            zap.Error(err),
        )
        return
    }

    l.Debug("published event")

    if diffEvent != nil {
        err, recoverable = eh.publisher.Publish(
            context.TODO(),
            diffEvent,
        )
        if err != nil {
            raven.CaptureError(err, nil)
            if !recoverable {
                l.Fatal("unrecoverable publishing error, shutting down",
                    zap.Error(err),
                )
            }
            l.Error("unable to publish event",
                zap.Error(err),
            )
            return
        }

        l.Debug("published diff event")
    }
}