|
1 | 1 | import os
|
2 | 2 | import signal
|
| 3 | +from collections.abc import Generator |
3 | 4 | from pathlib import Path
|
4 | 5 | from typing import Any
|
5 |
| -from urllib.parse import urlparse |
| 6 | +from urllib.parse import urlencode, urlparse |
6 | 7 |
|
7 | 8 | from buildbot.changes.base import ChangeSource
|
8 | 9 | from buildbot.config.builder import BuilderConfig
|
9 | 10 | from buildbot.plugins import util
|
10 | 11 | from buildbot.reporters.base import ReporterBase
|
11 | 12 | from buildbot.reporters.gitlab import GitLabStatusPush
|
| 13 | +from buildbot.util import httpclientservice |
| 14 | +from buildbot.www import resource |
12 | 15 | from buildbot.www.auth import AuthBase
|
13 | 16 | from buildbot.www.avatar import AvatarBase
|
14 | 17 | from pydantic import BaseModel
|
| 18 | +from twisted.internet import defer |
15 | 19 | from twisted.logger import Logger
|
16 | 20 | from twisted.python import log
|
17 | 21 |
|
@@ -186,7 +190,7 @@ def create_auth(self) -> AuthBase:
|
186 | 190 | raise NotImplementedError
|
187 | 191 |
|
188 | 192 | def create_avatar_method(self) -> AvatarBase | None:
|
189 |
| - return None |
| 193 | + return AvatarGitlab(config=self.config) |
190 | 194 |
|
191 | 195 | @property
|
192 | 196 | def reload_builder_name(self) -> str:
|
@@ -260,6 +264,99 @@ def run_post(self) -> Any:
|
260 | 264 | return util.SUCCESS
|
261 | 265 |
|
262 | 266 |
|
| 267 | +class AvatarGitlab(AvatarBase): |
| 268 | + name = "gitlab" |
| 269 | + |
| 270 | + config: GitlabConfig |
| 271 | + |
| 272 | + def __init__( |
| 273 | + self, |
| 274 | + config: GitlabConfig, |
| 275 | + debug: bool = False, |
| 276 | + verify: bool = True, |
| 277 | + ) -> None: |
| 278 | + self.config = config |
| 279 | + self.debug = debug |
| 280 | + self.verify = verify |
| 281 | + |
| 282 | + self.master = None |
| 283 | + self.client: httpclientservice.HTTPSession | None = None |
| 284 | + |
| 285 | + def _get_http_client( |
| 286 | + self, |
| 287 | + ) -> httpclientservice.HTTPSession: |
| 288 | + if self.client is not None: |
| 289 | + return self.client |
| 290 | + |
| 291 | + headers = { |
| 292 | + "User-Agent": "Buildbot", |
| 293 | + "Authorization": f"Bearer {self.config.token}", |
| 294 | + } |
| 295 | + |
| 296 | + self.client = httpclientservice.HTTPSession( |
| 297 | + self.master.httpservice, # type: ignore[attr-defined] |
| 298 | + self.config.instance_url, |
| 299 | + headers=headers, |
| 300 | + debug=self.debug, |
| 301 | + verify=self.verify, |
| 302 | + ) |
| 303 | + |
| 304 | + return self.client |
| 305 | + |
| 306 | + @defer.inlineCallbacks |
| 307 | + def getUserAvatar( # noqa: N802 |
| 308 | + self, |
| 309 | + email: str, |
| 310 | + username: str | None, |
| 311 | + size: str | int, |
| 312 | + defaultAvatarUrl: str, # noqa: N803 |
| 313 | + ) -> Generator[defer.Deferred, str | None, None]: |
| 314 | + if isinstance(size, int): |
| 315 | + size = str(size) |
| 316 | + avatar_url = None |
| 317 | + if username is not None: |
| 318 | + avatar_url = yield self._get_avatar_by_username(username) |
| 319 | + if avatar_url is None: |
| 320 | + avatar_url = yield self._get_avatar_by_email(email, size) |
| 321 | + if not avatar_url: |
| 322 | + avatar_url = defaultAvatarUrl |
| 323 | + raise resource.Redirect(avatar_url) |
| 324 | + |
| 325 | + @defer.inlineCallbacks |
| 326 | + def _get_avatar_by_username( |
| 327 | + self, username: str |
| 328 | + ) -> Generator[defer.Deferred, Any, str | None]: |
| 329 | + qs = urlencode(dict(username=username)) |
| 330 | + http = self._get_http_client() |
| 331 | + users = yield http.get(f"/api/v4/users?{qs}") |
| 332 | + users = yield users.json() |
| 333 | + if len(users) == 1: |
| 334 | + return users[0]["avatar_url"] |
| 335 | + if len(users) > 1: |
| 336 | + # TODO: log warning |
| 337 | + ... |
| 338 | + return None |
| 339 | + |
| 340 | + @defer.inlineCallbacks |
| 341 | + def _get_avatar_by_email( |
| 342 | + self, email: str, size: str | None |
| 343 | + ) -> Generator[defer.Deferred, Any, str | None]: |
| 344 | + http = self._get_http_client() |
| 345 | + q = dict(email=email) |
| 346 | + if size is not None: |
| 347 | + q["size"] = size |
| 348 | + qs = urlencode(q) |
| 349 | + res = yield http.get(f"/api/v4/avatar?{qs}") |
| 350 | + data = yield res.json() |
| 351 | + # N.B: Gitlab's public avatar endpoint returns a gravatar url if there isn't an |
| 352 | + # account with a matching public email - so it should always return *something*. |
| 353 | + # See: https://docs.gitlab.com/api/avatar/#get-details-on-an-account-avatar |
| 354 | + if "avatar_url" in data: |
| 355 | + return data["avatar_url"] |
| 356 | + else: |
| 357 | + return None |
| 358 | + |
| 359 | + |
263 | 360 | def refresh_projects(config: GitlabConfig, cache_file: Path) -> list[RepoData]:
|
264 | 361 | # access level 40 == Maintainer. See https://docs.gitlab.com/api/members/#roles
|
265 | 362 | return [
|
|
0 commit comments