mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
262 lines
9.5 KiB
Python
262 lines
9.5 KiB
Python
from types import MappingProxyType
|
|
|
|
from integrations.gitlab.gitlab_view import (
|
|
GitlabFactory,
|
|
GitlabInlineMRComment,
|
|
GitlabIssue,
|
|
GitlabIssueComment,
|
|
GitlabMRComment,
|
|
GitlabViewType,
|
|
)
|
|
from integrations.manager import Manager
|
|
from integrations.models import Message, SourceType
|
|
from integrations.types import ResolverViewInterface
|
|
from integrations.utils import (
|
|
CONVERSATION_URL,
|
|
HOST_URL,
|
|
OPENHANDS_RESOLVER_TEMPLATES_DIR,
|
|
)
|
|
from jinja2 import Environment, FileSystemLoader
|
|
from pydantic import SecretStr
|
|
from server.auth.token_manager import TokenManager
|
|
from server.utils.conversation_callback_utils import register_callback_processor
|
|
|
|
from openhands.core.logger import openhands_logger as logger
|
|
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
|
from openhands.integrations.provider import ProviderToken, ProviderType
|
|
from openhands.server.types import LLMAuthenticationError, MissingSettingsError
|
|
from openhands.storage.data_models.user_secrets import UserSecrets
|
|
|
|
|
|
class GitlabManager(Manager):
|
|
def __init__(self, token_manager: TokenManager, data_collector: None = None):
|
|
self.token_manager = token_manager
|
|
|
|
self.jinja_env = Environment(
|
|
loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR + 'gitlab')
|
|
)
|
|
|
|
def _confirm_incoming_source_type(self, message: Message):
|
|
if message.source != SourceType.GITLAB:
|
|
raise ValueError(f'Unexpected message source {message.source}')
|
|
|
|
async def _user_has_write_access_to_repo(
|
|
self, project_id: str, user_id: str
|
|
) -> bool:
|
|
"""
|
|
Check if the user has write access to the repository (can pull/push changes and open merge requests).
|
|
|
|
Args:
|
|
project_id: The ID of the GitLab project
|
|
username: The username of the user
|
|
user_id: The GitLab user ID
|
|
|
|
Returns:
|
|
bool: True if the user has write access to the repository, False otherwise
|
|
"""
|
|
|
|
keycloak_user_id = await self.token_manager.get_user_id_from_idp_user_id(
|
|
user_id, ProviderType.GITLAB
|
|
)
|
|
if keycloak_user_id is None:
|
|
logger.warning(f'Got invalid keyloak user id for GitLab User {user_id}')
|
|
return False
|
|
|
|
# Importing here prevents circular import
|
|
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
|
|
|
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
|
|
external_auth_id=keycloak_user_id
|
|
)
|
|
|
|
return await gitlab_service.user_has_write_access(project_id)
|
|
|
|
async def receive_message(self, message: Message):
|
|
self._confirm_incoming_source_type(message)
|
|
if await self.is_job_requested(message):
|
|
gitlab_view = await GitlabFactory.create_gitlab_view_from_payload(
|
|
message, self.token_manager
|
|
)
|
|
logger.info(
|
|
f'[GitLab] Creating job for {gitlab_view.user_info.username} in {gitlab_view.full_repo_name}#{gitlab_view.issue_number}'
|
|
)
|
|
|
|
await self.start_job(gitlab_view)
|
|
|
|
async def is_job_requested(self, message) -> bool:
|
|
self._confirm_incoming_source_type(message)
|
|
if not (
|
|
GitlabFactory.is_labeled_issue(message)
|
|
or GitlabFactory.is_issue_comment(message)
|
|
or GitlabFactory.is_mr_comment(message)
|
|
or GitlabFactory.is_mr_comment(message, inline=True)
|
|
):
|
|
return False
|
|
|
|
payload = message.message['payload']
|
|
|
|
repo_obj = payload['project']
|
|
project_id = repo_obj['id']
|
|
selected_project = repo_obj['path_with_namespace']
|
|
user = payload['user']
|
|
user_id = user['id']
|
|
username = user['username']
|
|
|
|
logger.info(
|
|
f'[GitLab] Checking permissions for {username} in {selected_project}'
|
|
)
|
|
|
|
has_write_access = await self._user_has_write_access_to_repo(
|
|
project_id=str(project_id), user_id=user_id
|
|
)
|
|
|
|
logger.info(
|
|
f'[GitLab]: {username} access in {selected_project}: {has_write_access}'
|
|
)
|
|
# Check if the user has write access to the repository
|
|
return has_write_access
|
|
|
|
async def send_message(self, message: Message, gitlab_view: ResolverViewInterface):
|
|
"""
|
|
Send a message to GitLab based on the view type.
|
|
|
|
Args:
|
|
message: The message to send
|
|
gitlab_view: The GitLab view object containing issue/PR/comment info
|
|
"""
|
|
keycloak_user_id = gitlab_view.user_info.keycloak_user_id
|
|
|
|
# Importing here prevents circular import
|
|
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
|
|
|
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
|
|
external_auth_id=keycloak_user_id
|
|
)
|
|
|
|
outgoing_message = message.message
|
|
|
|
if isinstance(gitlab_view, GitlabInlineMRComment) or isinstance(
|
|
gitlab_view, GitlabMRComment
|
|
):
|
|
await gitlab_service.reply_to_mr(
|
|
gitlab_view.project_id,
|
|
gitlab_view.issue_number,
|
|
gitlab_view.discussion_id,
|
|
message.message,
|
|
)
|
|
|
|
elif isinstance(gitlab_view, GitlabIssueComment):
|
|
await gitlab_service.reply_to_issue(
|
|
gitlab_view.project_id,
|
|
gitlab_view.issue_number,
|
|
gitlab_view.discussion_id,
|
|
outgoing_message,
|
|
)
|
|
elif isinstance(gitlab_view, GitlabIssue):
|
|
await gitlab_service.reply_to_issue(
|
|
gitlab_view.project_id,
|
|
gitlab_view.issue_number,
|
|
None, # no discussion id, issue is tagged
|
|
outgoing_message,
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f'[GitLab] Unsupported view type: {type(gitlab_view).__name__}'
|
|
)
|
|
|
|
async def start_job(self, gitlab_view: GitlabViewType):
|
|
"""
|
|
Start a job for the GitLab view.
|
|
|
|
Args:
|
|
gitlab_view: The GitLab view object containing issue/PR/comment info
|
|
"""
|
|
# Importing here prevents circular import
|
|
from server.conversation_callback_processor.gitlab_callback_processor import (
|
|
GitlabCallbackProcessor,
|
|
)
|
|
|
|
try:
|
|
try:
|
|
user_info = gitlab_view.user_info
|
|
|
|
logger.info(
|
|
f'[GitLab] Starting job for {user_info.username} in {gitlab_view.full_repo_name}#{gitlab_view.issue_number}'
|
|
)
|
|
|
|
user_token = await self.token_manager.get_idp_token_from_idp_user_id(
|
|
str(user_info.user_id), ProviderType.GITLAB
|
|
)
|
|
|
|
if not user_token:
|
|
logger.warning(
|
|
f'[GitLab] No token found for user {user_info.username} (id={user_info.user_id})'
|
|
)
|
|
raise MissingSettingsError('Missing settings')
|
|
|
|
logger.info(
|
|
f'[GitLab] Creating new conversation for user {user_info.username}'
|
|
)
|
|
|
|
secret_store = UserSecrets(
|
|
provider_tokens=MappingProxyType(
|
|
{
|
|
ProviderType.GITLAB: ProviderToken(
|
|
token=SecretStr(user_token),
|
|
user_id=str(user_info.user_id),
|
|
)
|
|
}
|
|
)
|
|
)
|
|
|
|
await gitlab_view.create_new_conversation(
|
|
self.jinja_env, secret_store.provider_tokens
|
|
)
|
|
|
|
conversation_id = gitlab_view.conversation_id
|
|
|
|
logger.info(
|
|
f'[GitLab] Created conversation {conversation_id} for user {user_info.username}'
|
|
)
|
|
|
|
# Create a GitlabCallbackProcessor for this conversation
|
|
processor = GitlabCallbackProcessor(
|
|
gitlab_view=gitlab_view,
|
|
send_summary_instruction=True,
|
|
)
|
|
|
|
# Register the callback processor
|
|
register_callback_processor(conversation_id, processor)
|
|
|
|
logger.info(
|
|
f'[GitLab] Created callback processor for conversation {conversation_id}'
|
|
)
|
|
|
|
conversation_link = CONVERSATION_URL.format(conversation_id)
|
|
msg_info = f"I'm on it! {user_info.username} can [track my progress at all-hands.dev]({conversation_link})"
|
|
|
|
except MissingSettingsError as e:
|
|
logger.warning(
|
|
f'[GitLab] Missing settings error for user {user_info.username}: {str(e)}'
|
|
)
|
|
|
|
msg_info = f'@{user_info.username} please re-login into [OpenHands Cloud]({HOST_URL}) before starting a job.'
|
|
|
|
except LLMAuthenticationError as e:
|
|
logger.warning(
|
|
f'[GitLab] LLM authentication error for user {user_info.username}: {str(e)}'
|
|
)
|
|
|
|
msg_info = f'@{user_info.username} please set a valid LLM API key in [OpenHands Cloud]({HOST_URL}) before starting a job.'
|
|
|
|
# Send the acknowledgment message
|
|
msg = self.create_outgoing_message(msg_info)
|
|
await self.send_message(msg, gitlab_view)
|
|
|
|
except Exception as e:
|
|
logger.exception(f'[GitLab] Error starting job: {str(e)}')
|
|
msg = self.create_outgoing_message(
|
|
msg='Uh oh! There was an unexpected error starting the job :('
|
|
)
|
|
await self.send_message(msg, gitlab_view)
|