connector: implement user cache

* Fixes the totally broken UserInfo resolution in guilds.
* Adds support for USER_UPDATE from the gateway.

Design considerations behind the user cache:

* Explicitly handle deleted user IDs by short circuiting the lookup
  logic and returning a singleton.
* The cache map is protected during HTTP requests to the Discord API.
* The nonexistence of a user is cached. This is to prevent excessive
  requests (a user can't suddenly begin existing at a given ID).

The user cache is upserted on READY, incoming messages, backfill, etc.
This commit is contained in:
Skip R
2026-02-06 13:24:26 -08:00
parent c611e8f116
commit d8ca44ecd9
6 changed files with 203 additions and 20 deletions

View File

@@ -54,18 +54,19 @@ func (d *DiscordClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost)
return nil, nil
}
// FIXME(skip): This won't work for users in guilds.
user, ok := d.usersFromReady[discordid.ParseUserID(ghost.ID)]
if !ok {
log.Error().Str("ghost_id", discordid.ParseUserID(ghost.ID)).Msg("Couldn't find corresponding user from READY for ghost")
discordUserID := discordid.ParseUserID(ghost.ID)
discordUser := d.userCache.Resolve(ctx, discordUserID)
if discordUser == nil {
log.Error().Str("discord_user_id", discordUserID).
Msg("Failed to resolve user")
return nil, nil
}
return &bridgev2.UserInfo{
Identifiers: []string{fmt.Sprintf("discord:%s", user.ID)},
Name: ptr.Ptr(user.DisplayName()),
Avatar: d.makeUserAvatar(user),
IsBot: &user.Bot,
// FIXME clear this for webhooks (stash in ghost metadata)
Identifiers: []string{fmt.Sprintf("discord:%s", discordUser.ID)},
Name: ptr.Ptr(discordUser.DisplayName()),
Avatar: d.makeUserAvatar(discordUser),
IsBot: &discordUser.Bot,
}, nil
}