mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
fix(backend): resolve missing email and display name for user identity tracking (#12719)
This commit is contained in:
@@ -4,6 +4,7 @@ from typing import Any
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.integrations.service_types import (
|
||||
BaseGitService,
|
||||
@@ -24,6 +25,18 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
|
||||
|
||||
BASE_URL = 'https://api.bitbucket.org/2.0'
|
||||
|
||||
@staticmethod
|
||||
def _resolve_primary_email(emails: list[dict]) -> str | None:
|
||||
"""Find the primary confirmed email from a list of Bitbucket email objects.
|
||||
|
||||
Bitbucket's /user/emails endpoint returns objects with
|
||||
'email', 'is_primary', and 'is_confirmed' keys.
|
||||
"""
|
||||
for entry in emails:
|
||||
if entry.get('is_primary') and entry.get('is_confirmed'):
|
||||
return entry.get('email')
|
||||
return None
|
||||
|
||||
def _extract_owner_and_repo(self, repository: str) -> tuple[str, str]:
|
||||
"""Extract owner and repo from repository string.
|
||||
|
||||
@@ -137,6 +150,17 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
|
||||
|
||||
return all_items[:max_items]
|
||||
|
||||
async def get_user_emails(self) -> list[dict]:
|
||||
"""Fetch the authenticated user's email addresses from Bitbucket.
|
||||
|
||||
Calls GET /user/emails which returns a paginated response with a
|
||||
'values' list of email objects containing 'email', 'is_primary',
|
||||
and 'is_confirmed' fields.
|
||||
"""
|
||||
url = f'{self.BASE_URL}/user/emails'
|
||||
response, _ = await self._make_request(url)
|
||||
return response.get('values', [])
|
||||
|
||||
async def get_user(self) -> User:
|
||||
"""Get the authenticated user's information."""
|
||||
url = f'{self.BASE_URL}/user'
|
||||
@@ -144,12 +168,22 @@ class BitBucketMixinBase(BaseGitService, HTTPClient):
|
||||
|
||||
account_id = data.get('account_id', '')
|
||||
|
||||
email = None
|
||||
try:
|
||||
emails = await self.get_user_emails()
|
||||
email = self._resolve_primary_email(emails)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
'bitbucket:get_user:email_fallback_failed',
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return User(
|
||||
id=account_id,
|
||||
login=data.get('username', ''),
|
||||
avatar_url=data.get('links', {}).get('avatar', {}).get('href', ''),
|
||||
name=data.get('display_name'),
|
||||
email=None, # Bitbucket API doesn't return email in this endpoint
|
||||
email=email,
|
||||
)
|
||||
|
||||
def _parse_repository(
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any, cast
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.integrations.service_types import (
|
||||
BaseGitService,
|
||||
@@ -22,6 +23,19 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
|
||||
BASE_URL: str
|
||||
GRAPHQL_URL: str
|
||||
|
||||
@staticmethod
|
||||
def _resolve_primary_email(emails: list[dict]) -> str | None:
|
||||
"""Find the primary verified email from a list of GitHub email objects.
|
||||
|
||||
GitHub's /user/emails endpoint returns a list of dicts, each with
|
||||
'email', 'primary', and 'verified' keys. This selects the one marked
|
||||
as both primary and verified — the email the user considers canonical.
|
||||
"""
|
||||
for entry in emails:
|
||||
if entry.get('primary') and entry.get('verified'):
|
||||
return entry.get('email')
|
||||
return None
|
||||
|
||||
async def _get_headers(self) -> dict:
|
||||
"""Retrieve the GH Token from settings store to construct the headers."""
|
||||
if not self.token:
|
||||
@@ -107,6 +121,17 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
|
||||
except httpx.HTTPError as e:
|
||||
raise self.handle_http_error(e)
|
||||
|
||||
async def get_user_emails(self) -> list[dict]:
|
||||
"""Fetch the authenticated user's email addresses from GitHub.
|
||||
|
||||
Calls GET /user/emails which returns a list of email objects, each
|
||||
containing 'email', 'primary', 'verified', and 'visibility' fields.
|
||||
Requires the user:email OAuth scope.
|
||||
"""
|
||||
url = f'{self.BASE_URL}/user/emails'
|
||||
response, _ = await self._make_request(url)
|
||||
return response
|
||||
|
||||
async def verify_access(self) -> bool:
|
||||
url = f'{self.BASE_URL}'
|
||||
await self._make_request(url)
|
||||
@@ -116,11 +141,22 @@ class GitHubMixinBase(BaseGitService, HTTPClient):
|
||||
url = f'{self.BASE_URL}/user'
|
||||
response, _ = await self._make_request(url)
|
||||
|
||||
email = response.get('email')
|
||||
if email is None:
|
||||
try:
|
||||
emails = await self.get_user_emails()
|
||||
email = self._resolve_primary_email(emails)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
'github:get_user:email_fallback_failed',
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
return User(
|
||||
id=str(response.get('id', '')),
|
||||
login=cast(str, response.get('login') or ''),
|
||||
avatar_url=cast(str, response.get('avatar_url') or ''),
|
||||
company=response.get('company'),
|
||||
name=response.get('name'),
|
||||
email=response.get('email'),
|
||||
email=email,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user