/** * The signed-in user and people lookup. */ import type { TeamsConnection } from "../config/index.ts"; import type { PersonRef, SignedInUser } from "../types.ts"; import { graphList, graphRequest } from "./client.ts"; import { mapPerson, mapUser } from "./mappers.ts"; const USER_SELECT = "id,displayName,userPrincipalName,mail,jobTitle,department,officeLocation"; /** Who pi is currently acting as. */ export async function getMe(conn: TeamsConnection, signal?: AbortSignal): Promise { const raw = await graphRequest>(conn, "GET", "/me", { query: { $select: USER_SELECT }, signal, }); const user = mapUser(raw as Record | undefined); if (!user) throw new Error("Could not read the signed-in user from Microsoft Graph."); return { ...user, tenantId: conn.tenantId }; } /** * UPN and mail of one user, by object id. * * A chat message's sender carries only id and display name, so a listen rule * like `*@contoso.com` has nothing to match against without this lookup. */ export async function getUserAddresses( conn: TeamsConnection, userId: string, signal?: AbortSignal, ): Promise<{ upn?: string; mail?: string }> { const raw = await graphRequest>( conn, "GET", `/users/${encodeURIComponent(userId)}`, { query: { $select: "userPrincipalName,mail" }, signal }, ); return { upn: raw?.userPrincipalName ?? undefined, mail: raw?.mail ?? undefined }; } /** * Find people by name, UPN or e-mail. * * Graph's `$filter startsWith` is the only broadly available option here: * `$search` on /users needs ConsistencyLevel=eventual and is not granted in * every tenant, so the query is escaped and matched against the three fields * people actually type. */ export async function findUsers( conn: TeamsConnection, query: string, max = 15, signal?: AbortSignal, ): Promise { const escaped = query.replace(/'/g, "''"); const filter = `startswith(displayName,'${escaped}') or ` + `startswith(userPrincipalName,'${escaped}') or ` + `startswith(mail,'${escaped}') or ` + `startswith(givenName,'${escaped}') or ` + `startswith(surname,'${escaped}')`; const raw = await graphList>(conn, "/users", { query: { $filter: filter, $select: USER_SELECT, $top: Math.min(max, 50) }, max, signal, }); return raw .map((entry) => mapPerson(entry as Record)) .filter((p): p is PersonRef => !!p); } /** Resolve a person reference (UPN, e-mail, display name or id) to a user id. */ export async function resolveUserId( conn: TeamsConnection, reference: string, signal?: AbortSignal, ): Promise { // A UPN or object id can be fetched directly — one call instead of a search. if (reference.includes("@") || /^[0-9a-f-]{36}$/i.test(reference)) { try { const raw = await graphRequest>( conn, "GET", `/users/${encodeURIComponent(reference)}`, { query: { $select: USER_SELECT }, signal }, ); const person = mapPerson(raw as Record | undefined); if (person) return person; } catch { /* fall through to search */ } } const matches = await findUsers(conn, reference, 5, signal); return matches[0]; }