Skip to content

Commit 351813d

Browse files
committed
Add functions to fetch user details from Keycloak and update DM invitation logic
1 parent ec435a4 commit 351813d

2 files changed

Lines changed: 83 additions & 26 deletions

File tree

arthur/apis/directory/keycloak.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@ async def force_password_reset(username: str, password: str) -> None:
3131

3232
await client.a_set_user_password(user_id, password, temporary=True)
3333

34+
async def get_user(user_id: str) -> dict:
35+
"""Fetch a user from Keycloak using their user ID."""
36+
client = create_client()
37+
38+
return await client.a_get_user(user_id)
39+
40+
41+
async def get_discord_id(user_id: str) -> int | None:
42+
"""Fetch a user's Discord ID from Keycloak attributes."""
43+
user = await get_user(user_id)
44+
discord_ids = user.get("attributes", {}).get("discordId")
45+
if not isinstance(discord_ids, list) or not discord_ids:
46+
return None
47+
48+
try:
49+
return int(discord_ids[0])
50+
except (TypeError, ValueError):
51+
return None
52+
53+
async def get_user_id(username: str) -> str | None:
54+
"""Fetch a user's Keycloak ID using their username."""
55+
client = create_client()
56+
57+
return await client.a_get_user_id(username)
3458

3559
async def get_user_github_id(username: str) -> str | None:
3660
"""Fetch a users GitHub ID from Keycloak."""

arthur/exts/github/management.py

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from discord.ext.commands import Cog, group
99

1010
from arthur.apis.directory import ldap
11-
from arthur.apis.directory.keycloak import all_github_identities
11+
from arthur.apis.directory.keycloak import all_github_identities, get_discord_id, get_user_id
1212
from arthur.apis.github import (
1313
GitHubError,
1414
add_member_to_team,
@@ -389,30 +389,67 @@ async def _notify_failed_invites(self, skipped_failed: list[str]) -> None:
389389
f"`{username}` because a failed invitation record already exists."
390390
)
391391

392-
async def _try_dm_user_invite(self, github_username: str, keycloak_user: dict | None) -> None:
393-
"""DM a user using their Discord ID from Keycloak."""
394-
if not keycloak_user:
395-
logger.debug(f"No Keycloak user data for GitHub username {github_username}")
392+
def _get_keycloak_username_for_github_username(
393+
self,
394+
github_username: str,
395+
keycloak_identities: dict[str, dict[str, str]],
396+
resolved_logins_by_user_id: dict[str, str],
397+
) -> str | None:
398+
"""Find the Keycloak username mapped to a GitHub username."""
399+
normalised_target = self._normalise_login(github_username)
400+
401+
for keycloak_username, identity in keycloak_identities.items():
402+
user_id = identity.get("user_id", "").strip()
403+
if not user_id:
404+
continue
405+
406+
resolved_login = resolved_logins_by_user_id.get(user_id)
407+
if resolved_login and self._normalise_login(resolved_login) == normalised_target:
408+
return keycloak_username
409+
410+
return None
411+
412+
async def _try_dm_user_invite(
413+
self,
414+
github_username: str,
415+
keycloak_username: str | None,
416+
) -> None:
417+
"""DM a user using their Discord ID from Keycloak user attributes."""
418+
if not keycloak_username:
419+
logger.debug(f"No Keycloak username for GitHub username {github_username}")
396420
return
397421

398-
# Extract Discord ID from Keycloak attributes
399-
discord_ids = keycloak_user.get("attributes", {}).get("discordId")
400-
if not discord_ids or not isinstance(discord_ids, list) or not discord_ids:
422+
keycloak_user_id = await get_user_id(keycloak_username)
423+
if not keycloak_user_id:
424+
logger.debug(f"No Keycloak user ID found for {keycloak_username}")
425+
return
426+
427+
discord_id = await get_discord_id(keycloak_user_id)
428+
if discord_id is None:
401429
logger.debug(
402-
f"No Discord ID found in Keycloak for {keycloak_user.get('username', 'unknown')} (GitHub: {github_username})"
430+
f"No valid Discord ID found in Keycloak for {keycloak_username} (GitHub: {github_username})"
403431
)
404432
return
405433

406-
try:
407-
discord_id = int(discord_ids[0])
408-
member = self.bot.get_user(discord_id) or await self.bot.fetch_user(discord_id)
434+
guild = self.bot.get_guild(CONFIG.guild_id)
435+
if guild is None:
436+
guild = await self.bot.fetch_guild(CONFIG.guild_id)
409437

438+
member = guild.get_member(discord_id)
439+
if member is None:
440+
try:
441+
member = await guild.fetch_member(discord_id)
442+
except discord.NotFound:
443+
logger.debug(
444+
f"User {discord_id} is not a member of guild {CONFIG.guild_id}; skipping GitHub invite DM"
445+
)
446+
return
447+
448+
try:
410449
await member.send(
411-
f"You've been invited to join the {CONFIG.github_org} GitHub organization!\n\n"
412-
f"Accept your invitation here: https://github.com/orgs/{CONFIG.github_org}/invitation"
450+
"You've been invited to join the python-discord GitHub organization!\n\n"
451+
"Accept your invitation here: https://github.com/orgs/python-discord/invitation"
413452
)
414-
except ValueError:
415-
logger.debug(f"Invalid Discord ID in Keycloak: {discord_ids[0]}")
416453
except discord.Forbidden:
417454
logger.debug(f"Could not DM user {discord_id} - DMs disabled")
418455
except discord.HTTPException as e:
@@ -427,21 +464,17 @@ async def _apply_org_additions(
427464
resolved_logins_by_user_id: dict[str, str],
428465
) -> list[str]:
429466
"""Apply organisation additions and return successfully added logins."""
430-
# Build reverse map: GitHub login -> Keycloak user data
431-
github_login_to_keycloak = {}
432-
for identity in keycloak_identities.values():
433-
user_id = identity.get("user_id", "").strip()
434-
if user_id in resolved_logins_by_user_id:
435-
github_login = resolved_logins_by_user_id[user_id]
436-
github_login_to_keycloak[github_login] = identity
437-
438467
added = []
439468
for username in usernames:
440469
try:
441470
await add_org_member(username)
442471
added.append(username)
443-
keycloak_user = github_login_to_keycloak.get(username)
444-
await self._try_dm_user_invite(username, keycloak_user)
472+
keycloak_username = self._get_keycloak_username_for_github_username(
473+
username,
474+
keycloak_identities,
475+
resolved_logins_by_user_id,
476+
)
477+
await self._try_dm_user_invite(username, keycloak_username)
445478
except GitHubError as e:
446479
logger.opt(exception=e).error(f"GitHub: Failed to add {username} to org")
447480

0 commit comments

Comments
 (0)