|
5 | 5 | generated schema consistent across similar viewsets. |
6 | 6 | """ |
7 | 7 |
|
| 8 | +import logging |
8 | 9 | from typing import Any |
9 | 10 |
|
10 | 11 | from drf_spectacular.utils import ( |
|
21 | 22 | from rest_framework.response import Response |
22 | 23 |
|
23 | 24 | from core.models import ( |
| 25 | + BlueskyCredentials, |
24 | 26 | Content, |
25 | 27 | Entity, |
26 | 28 | IngestionRun, |
|
48 | 50 | SUMMARIZATION_SKILL_NAME = "summarization" |
49 | 51 | RELATED_CONTENT_SKILL_NAME = "find_related" |
50 | 52 |
|
| 53 | +logger = logging.getLogger(__name__) |
| 54 | + |
51 | 55 | PROJECT_ID_PARAMETER = OpenApiParameter( |
52 | 56 | name="project_id", |
53 | 57 | type=int, |
|
115 | 119 | request_only=True, |
116 | 120 | ) |
117 | 121 |
|
| 122 | +SOURCE_CONFIG_BLUESKY_REQUEST_EXAMPLE = OpenApiExample( |
| 123 | + "Create Bluesky Source Request", |
| 124 | + value={ |
| 125 | + "plugin_name": "bluesky", |
| 126 | + "config": { |
| 127 | + "author_handle": "alice.bsky.social", |
| 128 | + "include_replies": False, |
| 129 | + "max_posts_per_fetch": 100, |
| 130 | + }, |
| 131 | + "is_active": True, |
| 132 | + }, |
| 133 | + request_only=True, |
| 134 | +) |
| 135 | + |
118 | 136 | SOURCE_CONFIG_RESPONSE_EXAMPLE = OpenApiExample( |
119 | 137 | "Source Configuration Response", |
120 | 138 | value={ |
|
228 | 246 | examples=[AUTHENTICATION_REQUIRED_EXAMPLE], |
229 | 247 | ) |
230 | 248 |
|
| 249 | +BLUESKY_CREDENTIALS_VERIFY_RESPONSE = inline_serializer( |
| 250 | + name="BlueskyCredentialsVerifyResponse", |
| 251 | + fields={ |
| 252 | + "status": serializers.CharField(), |
| 253 | + "handle": serializers.CharField(), |
| 254 | + "last_verified_at": serializers.DateTimeField(allow_null=True), |
| 255 | + "last_error": serializers.CharField(allow_blank=True), |
| 256 | + }, |
| 257 | +) |
| 258 | + |
231 | 259 |
|
232 | 260 | def build_success_response( |
233 | 261 | response, description: str, examples: list[OpenApiExample] | None = None |
@@ -554,6 +582,80 @@ def get_queryset(self): |
554 | 582 |
|
555 | 583 | return self.queryset.filter(group__user=self.request.user).distinct() |
556 | 584 |
|
| 585 | + @extend_schema( |
| 586 | + summary="Verify Bluesky credentials", |
| 587 | + description=( |
| 588 | + "Verify the selected project's stored Bluesky credentials by authenticating " |
| 589 | + "the account and checking the current session." |
| 590 | + ), |
| 591 | + tags=["Ingestion"], |
| 592 | + request=None, |
| 593 | + responses={ |
| 594 | + 200: build_success_response( |
| 595 | + BLUESKY_CREDENTIALS_VERIFY_RESPONSE, |
| 596 | + "The project's Bluesky credentials were verified successfully.", |
| 597 | + ), |
| 598 | + 400: OpenApiResponse( |
| 599 | + response=inline_serializer( |
| 600 | + name="BlueskyCredentialsVerifyErrorResponse", |
| 601 | + fields={ |
| 602 | + "type": serializers.CharField(), |
| 603 | + "errors": inline_serializer( |
| 604 | + name="BlueskyCredentialsVerifyError", |
| 605 | + fields={ |
| 606 | + "code": serializers.CharField(), |
| 607 | + "detail": serializers.CharField(), |
| 608 | + "attr": serializers.CharField(allow_null=True), |
| 609 | + }, |
| 610 | + many=True, |
| 611 | + ), |
| 612 | + }, |
| 613 | + ), |
| 614 | + description="The project is missing Bluesky credentials or verification failed.", |
| 615 | + ), |
| 616 | + 403: AUTHENTICATION_REQUIRED_RESPONSE, |
| 617 | + }, |
| 618 | + ) |
| 619 | + @action(detail=True, methods=["post"], url_path="verify-bluesky-credentials") |
| 620 | + def verify_bluesky_credentials(self, request, *args, **kwargs): |
| 621 | + """Verify the Bluesky credentials stored for the selected project.""" |
| 622 | + |
| 623 | + from core.plugins.bluesky import BlueskySourcePlugin |
| 624 | + |
| 625 | + project = self.get_object() |
| 626 | + try: |
| 627 | + credentials = project.bluesky_credentials |
| 628 | + except BlueskyCredentials.DoesNotExist as exc: |
| 629 | + raise serializers.ValidationError( |
| 630 | + {"bluesky_credentials": "No Bluesky credentials are configured for this project."} |
| 631 | + ) from exc |
| 632 | + |
| 633 | + try: |
| 634 | + BlueskySourcePlugin.verify_credentials(credentials) |
| 635 | + except Exception as exc: |
| 636 | + logger.exception( |
| 637 | + "Bluesky credential verification failed for project id=%s", |
| 638 | + project.id, |
| 639 | + ) |
| 640 | + raise serializers.ValidationError( |
| 641 | + { |
| 642 | + "bluesky_credentials": ( |
| 643 | + "Credential verification failed. Please re-check the credentials " |
| 644 | + "and try again." |
| 645 | + ) |
| 646 | + } |
| 647 | + ) from exc |
| 648 | + |
| 649 | + credentials.refresh_from_db() |
| 650 | + return Response( |
| 651 | + { |
| 652 | + "status": "verified", |
| 653 | + "handle": credentials.handle, |
| 654 | + "last_verified_at": credentials.last_verified_at, |
| 655 | + "last_error": "", |
| 656 | + } |
| 657 | + ) |
| 658 | + |
557 | 659 |
|
558 | 660 | @document_project_owned_viewset( |
559 | 661 | resource_plural="project configurations", |
@@ -741,6 +843,7 @@ class IngestionRunViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): |
741 | 843 | create_examples=[ |
742 | 844 | SOURCE_CONFIG_CREATE_REQUEST_EXAMPLE, |
743 | 845 | SOURCE_CONFIG_REDDIT_REQUEST_EXAMPLE, |
| 846 | + SOURCE_CONFIG_BLUESKY_REQUEST_EXAMPLE, |
744 | 847 | SOURCE_CONFIG_RESPONSE_EXAMPLE, |
745 | 848 | ], |
746 | 849 | create_response_examples=[SOURCE_CONFIG_RESPONSE_EXAMPLE], |
|
0 commit comments