refactor(trigger): remove resubscribe functionality and update subscription handling

- Removed the resubscribe_trigger method and its associated request entity.
- Updated subscription parameters to streamline the subscribe and unsubscribe processes.
- Enhanced the trigger provider interface to utilize the new subscription schema.
- Adjusted GitHub provider implementation to align with the updated subscription structure.
This commit is contained in:
Harry
2025-08-29 15:07:26 +08:00
parent a35dcd74f7
commit 3a66fc061b
9 changed files with 209 additions and 391 deletions
@@ -1,6 +1,5 @@
import hashlib
import hmac
import json
import secrets
import time
import urllib.parse
@@ -12,7 +11,6 @@ from werkzeug import Request, Response
from dify_plugin.interfaces.trigger import TriggerProvider
from dify_plugin.entities.trigger import TriggerEventDispatch, Subscription, Unsubscription
from dify_plugin.entities.oauth import ToolOAuthCredentials
from dify_plugin.errors.tool import ToolProviderCredentialValidationError, ToolProviderOAuthError
from dify_plugin.errors.trigger import SubscriptionError, WebhookValidationError, TriggerDispatchError
@@ -53,7 +51,6 @@ class GithubProvider(TriggerProvider):
access_token = response_json.get("access_token")
if not access_token:
raise ToolProviderOAuthError(f"Error in GitHub OAuth: {response_json}")
return {"access_tokens": access_token}
def _validate_credentials(self, credentials: dict) -> None:
@@ -70,12 +67,12 @@ class GithubProvider(TriggerProvider):
except Exception as e:
raise ToolProviderCredentialValidationError(str(e)) from e
def _dispatch_event(self, settings: Mapping[str, Any], request: Request) -> TriggerEventDispatch:
def _dispatch_event(self, subscription: Subscription, request: Request) -> TriggerEventDispatch:
"""
Dispatch GitHub webhook events - focusing on issue comment events
"""
# Verify webhook signature if secret is provided
webhook_secret = settings.get("webhook_secret")
webhook_secret = subscription.configuration.get("webhook_secret")
if webhook_secret:
signature = request.headers.get("X-Hub-Signature-256")
if not signature:
@@ -111,50 +108,46 @@ class GithubProvider(TriggerProvider):
else:
# For other events, pass them through with prefix
return TriggerEventDispatch(event=f"github.{event_type}", response=response)
def _subscribe(self, credentials: Mapping[str, Any], subscription_params: Mapping[str, Any]) -> Subscription:
"""
Create a GitHub webhook subscription for issue comment events
"""
# Extract parameters
callback_url = subscription_params.get("callback_url")
endpoint = subscription_params.get("endpoint")
webhook_secret = subscription_params.get("webhook_secret")
repository = subscription_params.get("repository") # format: "owner/repo"
events = subscription_params.get("events", ["issue_comment", "issues"])
if not callback_url:
raise ValueError("callback_url is required for webhook subscription")
if not endpoint:
raise ValueError("endpoint is required for webhook subscription")
if not repository:
raise ValueError("repository is required (format: owner/repo)")
# Parse repository owner and name
try:
owner, repo = repository.split("/")
except ValueError:
raise ValueError("repository must be in format 'owner/repo'")
# Create webhook using GitHub API
url = f"https://api.github.com/repos/{owner}/{repo}/hooks"
headers = {
"Authorization": f"Bearer {credentials.get('access_tokens')}",
"Accept": "application/vnd.github+json",
}
webhook_data = {
"name": "web",
"active": True,
"events": events,
"config": {
"url": callback_url,
"content_type": "json",
"insecure_ssl": "0"
}
"config": {"url": endpoint, "content_type": "json", "insecure_ssl": "0"},
}
# Add secret if provided
if webhook_secret:
webhook_data["config"]["secret"] = webhook_secret
try:
response = requests.post(url, json=webhook_data, headers=headers, timeout=10)
if response.status_code == 201:
@@ -162,83 +155,78 @@ class GithubProvider(TriggerProvider):
# Return subscription with webhook details
return Subscription(
expire_at=int(time.time()) + 30 * 24 * 60 * 60, # 30 days expiration
metadata={
endpoint=endpoint,
configuration={
"external_id": str(webhook["id"]),
"webhook_url": webhook["url"],
"callback_url": callback_url,
"repository": repository,
"events": events,
"active": webhook["active"]
}
"webhook_secret": webhook_secret,
"active": webhook["active"],
},
)
else:
error_msg = response.json().get("message", "Unknown error")
raise SubscriptionError(f"Failed to create GitHub webhook: {error_msg}",
error_code="WEBHOOK_CREATION_FAILED",
external_response=response.json())
raise SubscriptionError(
f"Failed to create GitHub webhook: {error_msg}",
error_code="WEBHOOK_CREATION_FAILED",
external_response=response.json(),
)
except requests.RequestException as e:
raise SubscriptionError(f"Network error while creating webhook: {e}",
error_code="NETWORK_ERROR")
def _unsubscribe(self, subscription: Subscription, credentials: Mapping[str, Any], settings: Mapping[str, Any]) -> Unsubscription:
raise SubscriptionError(f"Network error while creating webhook: {e}", error_code="NETWORK_ERROR")
def _unsubscribe(self, subscription: Subscription, credentials: Mapping[str, Any]) -> Unsubscription:
"""
Remove a GitHub webhook subscription
"""
# Extract webhook details from metadata
external_id = subscription.metadata.get("external_id")
repository = subscription.metadata.get("repository")
# Extract webhook details from data
external_id = subscription.configuration.get("external_id")
repository = subscription.configuration.get("repository")
if not external_id or not repository:
return Unsubscription(
success=False,
message="Missing webhook ID or repository information",
error_code="MISSING_METADATA"
success=False, message="Missing webhook ID or repository information", error_code="MISSING_DATA"
)
# Parse repository
try:
owner, repo = repository.split("/")
except ValueError:
return Unsubscription(
success=False,
message="Invalid repository format in metadata",
error_code="INVALID_REPOSITORY"
success=False, message="Invalid repository format in data", error_code="INVALID_REPOSITORY"
)
# Delete webhook using GitHub API
url = f"https://api.github.com/repos/{owner}/{repo}/hooks/{external_id}"
headers = {
"Authorization": f"Bearer {credentials.get('access_tokens')}",
"Accept": "application/vnd.github+json",
}
try:
response = requests.delete(url, headers=headers, timeout=10)
if response.status_code == 204:
return Unsubscription(
success=True,
message=f"Successfully removed webhook {external_id} from {repository}"
success=True, message=f"Successfully removed webhook {external_id} from {repository}"
)
elif response.status_code == 404:
return Unsubscription(
success=False,
message=f"Webhook {external_id} not found in repository {repository}",
error_code="WEBHOOK_NOT_FOUND"
error_code="WEBHOOK_NOT_FOUND",
)
else:
return Unsubscription(
success=False,
message=f"Failed to delete webhook: {response.json().get('message', 'Unknown error')}",
error_code="API_ERROR",
external_response=response.json()
external_response=response.json(),
)
except requests.RequestException as e:
return Unsubscription(
success=False,
message=f"Network error while deleting webhook: {e}",
error_code="NETWORK_ERROR"
success=False, message=f"Network error while deleting webhook: {e}", error_code="NETWORK_ERROR"
)
def _refresh(self, subscription: Subscription, credentials: Mapping[str, Any]) -> Subscription:
"""
Refresh a GitHub webhook subscription (extend expiration)
@@ -248,31 +236,6 @@ class GithubProvider(TriggerProvider):
# GitHub webhooks don't have built-in expiration
return Subscription(
expire_at=int(time.time()) + 30 * 24 * 60 * 60, # Extend by 30 days
metadata=subscription.metadata # Keep the same metadata
endpoint=subscription.endpoint, # Keep the same endpoint
configuration=subscription.configuration, # Keep the same data
)
def _resubscribe(self, subscription: Subscription, credentials: Mapping[str, Any], settings: Mapping[str, Any]) -> Subscription:
"""
Update a GitHub webhook subscription with new settings
"""
# First unsubscribe the old webhook
unsubscribe_result = self._unsubscribe(subscription, credentials, settings)
if not unsubscribe_result.success:
# If we can't delete the old one, still try to create a new one
# This handles cases where the old webhook was already deleted
pass
# Extract new subscription parameters from settings
repository = subscription.metadata.get("repository")
callback_url = subscription.metadata.get("callback_url")
# Create new subscription params with updated settings
new_subscription_params = {
"callback_url": callback_url,
"repository": repository,
"webhook_secret": settings.get("webhook_secret"),
"events": settings.get("events", ["issue_comment", "issues"])
}
# Create new webhook with updated settings
return self._subscribe(credentials, new_subscription_params)
@@ -56,57 +56,59 @@ oauth_schema:
en_US: "Access Token"
subscription_schema:
- name: "repository"
type: "text-input"
required: true
label:
zh_Hans: "仓库"
en_US: "Repository"
placeholder:
zh_Hans: "owner/repo"
en_US: "owner/repo"
help:
en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)"
pt_BR: "Repositório do GitHub no formato owner/repo (ex: microsoft/vscode)"
zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode"
- name: "events"
type: "select"
required: true
default: ["issue_comment", "issues"]
options:
- value: "issue_comment"
label:
en_US: "Issue Comments"
zh_Hans: "Issue 评论"
- value: "issues"
label:
en_US: "Issues"
zh_Hans: "Issues"
- value: "pull_request"
label:
en_US: "Pull Requests"
zh_Hans: "Pull Requests"
- value: "push"
label:
en_US: "Push"
zh_Hans: "推送"
label:
zh_Hans: "监听事件"
en_US: "Events to Listen"
help:
en_US: "Select which GitHub events to subscribe to"
pt_BR: "Selecione quais eventos do GitHub inscrever-se"
zh_Hans: "选择要订阅的 GitHub 事件"
- name: "webhook_secret"
type: "secret-input"
required: false
label:
zh_Hans: "Webhook Secret"
en_US: "Webhook Secret"
help:
en_US: "Optional webhook secret for validating GitHub webhook requests"
pt_BR: "Segredo opcional do webhook para validar solicitações do webhook do GitHub"
zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥"
parameters_schema:
- name: "repository"
type: "text-input"
required: true
label:
zh_Hans: "仓库"
en_US: "Repository"
placeholder:
zh_Hans: "owner/repo"
en_US: "owner/repo"
help:
en_US: "GitHub repository in format owner/repo (e.g., microsoft/vscode)"
pt_BR: "Repositório do GitHub no formato owner/repo (ex: microsoft/vscode)"
zh_Hans: "GitHub 仓库,格式为 owner/repo(例如:microsoft/vscode"
- name: "events"
type: "select"
required: true
default: ["issue_comment", "issues"]
options:
- value: "issue_comment"
label:
en_US: "Issue Comments"
zh_Hans: "Issue 评论"
- value: "issues"
label:
en_US: "Issues"
zh_Hans: "Issues"
- value: "pull_request"
label:
en_US: "Pull Requests"
zh_Hans: "Pull Requests"
- value: "push"
label:
en_US: "Push"
zh_Hans: "推送"
label:
zh_Hans: "监听事件"
en_US: "Events to Listen"
help:
en_US: "Select which GitHub events to subscribe to"
pt_BR: "Selecione quais eventos do GitHub inscrever-se"
zh_Hans: "选择要订阅的 GitHub 事件"
properties_schema:
- name: "webhook_secret"
type: "secret-input"
required: false
label:
zh_Hans: "Webhook Secret"
en_US: "Webhook Secret"
help:
en_US: "Optional webhook secret for validating GitHub webhook requests"
pt_BR: "Segredo opcional do webhook para validar solicitações do webhook do GitHub"
zh_Hans: "可选的用于验证 GitHub webhook 请求的 webhook 密钥"
triggers:
- triggers/issue_comment.yaml