Samyak000 commited on
Commit
efc5fb2
·
verified ·
1 Parent(s): 3cf7d6e

Update webhook.py

Browse files
Files changed (1) hide show
  1. webhook.py +343 -0
webhook.py CHANGED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GitHub Webhooks Handler
3
+ Processes real-time GitHub events and updates Supabase automatically
4
+ """
5
+
6
+ import hmac
7
+ import hashlib
8
+ import logging
9
+ from typing import Optional, Dict, Any
10
+ from datetime import datetime
11
+ from supabase import Client
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class GitHubWebhookHandler:
17
+ """Handle GitHub webhook events and update Supabase"""
18
+
19
+ def __init__(self, supabase: Client, webhook_secret: str):
20
+ """
21
+ Initialize webhook handler
22
+
23
+ Args:
24
+ supabase: Supabase client instance
25
+ webhook_secret: GitHub webhook secret for signature verification
26
+ """
27
+ self.supabase = supabase
28
+ self.webhook_secret = webhook_secret
29
+
30
+ def verify_signature(self, payload: bytes, signature: str) -> bool:
31
+ """
32
+ Verify GitHub webhook signature
33
+
34
+ Args:
35
+ payload: Raw webhook payload bytes
36
+ signature: X-Hub-Signature-256 header value
37
+
38
+ Returns:
39
+ True if signature is valid, False otherwise
40
+ """
41
+ expected_signature = "sha256=" + hmac.new(
42
+ self.webhook_secret.encode(),
43
+ payload,
44
+ hashlib.sha256
45
+ ).hexdigest()
46
+
47
+ return hmac.compare_digest(signature, expected_signature)
48
+
49
+ def get_firebase_id_from_installation(self, installation_id: int) -> Optional[str]:
50
+ """Get firebase_id associated with installation_id"""
51
+ try:
52
+ result = self.supabase.table("Users").select("firebase_id").eq("installation_id", installation_id).limit(1).execute()
53
+ if result.data:
54
+ return result.data[0]["firebase_id"]
55
+ except Exception as e:
56
+ logger.error(f"Failed to get firebase_id for installation {installation_id}: {str(e)}")
57
+
58
+ return None
59
+
60
+ def get_github_id(self, firebase_id: str, full_name: str) -> Optional[int]:
61
+ """Get github table id for a repo"""
62
+ try:
63
+ result = self.supabase.table("github").select("id").eq("firebase_id", firebase_id).eq("full_name", full_name).limit(1).execute()
64
+ if result.data:
65
+ return result.data[0]["id"]
66
+ except Exception as e:
67
+ logger.error(f"Failed to get github_id for {full_name}: {str(e)}")
68
+
69
+ return None
70
+
71
+ # ========================================================================
72
+ # PUSH EVENTS (Commits)
73
+ # ========================================================================
74
+
75
+ def handle_push(self, payload: Dict[str, Any]) -> bool:
76
+ """
77
+ Handle push event - new commits pushed to repo
78
+
79
+ Args:
80
+ payload: GitHub webhook payload
81
+
82
+ Returns:
83
+ True if handled successfully, False otherwise
84
+ """
85
+ try:
86
+ installation_id = payload.get("installation", {}).get("id")
87
+ repo_full_name = payload.get("repository", {}).get("full_name")
88
+ commits = payload.get("commits", [])
89
+
90
+ if not installation_id or not repo_full_name or not commits:
91
+ logger.warning("Incomplete push payload data")
92
+ return False
93
+
94
+ firebase_id = self.get_firebase_id_from_installation(installation_id)
95
+ if not firebase_id:
96
+ logger.warning(f"No firebase_id found for installation {installation_id}")
97
+ return False
98
+
99
+ github_id = self.get_github_id(firebase_id, repo_full_name)
100
+ if not github_id:
101
+ logger.warning(f"No github_id found for {repo_full_name}")
102
+ return False
103
+
104
+ # Process each commit
105
+ commit_rows = []
106
+ for commit in commits:
107
+ commit_rows.append({
108
+ "github_id": github_id,
109
+ "firebase_id": firebase_id,
110
+ "sha": commit.get("id"),
111
+ "message": commit.get("message"),
112
+ "author": commit.get("author", {}).get("name"),
113
+ "author_email": commit.get("author", {}).get("email"),
114
+ "committed_date": commit.get("timestamp"),
115
+ })
116
+
117
+ if commit_rows:
118
+ self.supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
119
+ logger.info(f"✅ Stored {len(commit_rows)} commits from push event")
120
+ return True
121
+
122
+ except Exception as e:
123
+ logger.error(f"Failed to handle push event: {str(e)}", exc_info=True)
124
+
125
+ return False
126
+
127
+ # ========================================================================
128
+ # PULL REQUEST EVENTS
129
+ # ========================================================================
130
+
131
+ def handle_pull_request(self, payload: Dict[str, Any]) -> bool:
132
+ """
133
+ Handle pull request events (opened, synchronize, closed, merged)
134
+
135
+ Args:
136
+ payload: GitHub webhook payload
137
+
138
+ Returns:
139
+ True if handled successfully, False otherwise
140
+ """
141
+ try:
142
+ action = payload.get("action") # opened, synchronize, closed, reopened, edited
143
+ installation_id = payload.get("installation", {}).get("id")
144
+ repo_full_name = payload.get("repository", {}).get("full_name")
145
+ pr = payload.get("pull_request", {})
146
+
147
+ if not installation_id or not repo_full_name or not pr:
148
+ logger.warning("Incomplete pull_request payload data")
149
+ return False
150
+
151
+ firebase_id = self.get_firebase_id_from_installation(installation_id)
152
+ if not firebase_id:
153
+ logger.warning(f"No firebase_id found for installation {installation_id}")
154
+ return False
155
+
156
+ github_id = self.get_github_id(firebase_id, repo_full_name)
157
+ if not github_id:
158
+ logger.warning(f"No github_id found for {repo_full_name}")
159
+ return False
160
+
161
+ pr_data = {
162
+ "github_id": github_id,
163
+ "firebase_id": firebase_id,
164
+ "pr_id": pr.get("id"),
165
+ "pr_number": pr.get("number"),
166
+ "title": pr.get("title"),
167
+ "body": pr.get("body"),
168
+ "state": pr.get("state"),
169
+ "author": pr.get("user", {}).get("login"),
170
+ "created_at": pr.get("created_at"),
171
+ "updated_at": pr.get("updated_at"),
172
+ "merged_at": pr.get("merged_at"),
173
+ "closed_at": pr.get("closed_at"),
174
+ "url": pr.get("html_url"),
175
+ "head_branch": pr.get("head", {}).get("ref"),
176
+ "base_branch": pr.get("base", {}).get("ref"),
177
+ "draft": pr.get("draft", False),
178
+ "mergeable": pr.get("mergeable"),
179
+ "comments": pr.get("comments", 0),
180
+ "commits": pr.get("commits", 0),
181
+ "additions": pr.get("additions", 0),
182
+ "deletions": pr.get("deletions", 0),
183
+ "changed_files": pr.get("changed_files", 0),
184
+ }
185
+
186
+ self.supabase.table("pull_requests").upsert([pr_data], on_conflict="firebase_id,pr_id").execute()
187
+ logger.info(f"✅ Updated PR #{pr.get('number')} ({action}) in {repo_full_name}")
188
+ return True
189
+
190
+ except Exception as e:
191
+ logger.error(f"Failed to handle pull_request event: {str(e)}", exc_info=True)
192
+
193
+ return False
194
+
195
+ # ========================================================================
196
+ # ISSUES EVENTS
197
+ # ========================================================================
198
+
199
+ def handle_issues(self, payload: Dict[str, Any]) -> bool:
200
+ """
201
+ Handle issues events (opened, edited, closed, reopened, etc.)
202
+
203
+ Args:
204
+ payload: GitHub webhook payload
205
+
206
+ Returns:
207
+ True if handled successfully, False otherwise
208
+ """
209
+ try:
210
+ action = payload.get("action") # opened, edited, closed, reopened
211
+ installation_id = payload.get("installation", {}).get("id")
212
+ repo_full_name = payload.get("repository", {}).get("full_name")
213
+ issue = payload.get("issue", {})
214
+
215
+ if not installation_id or not repo_full_name or not issue:
216
+ logger.warning("Incomplete issues payload data")
217
+ return False
218
+
219
+ # Skip if this is actually a pull request (pulled_request key exists)
220
+ if "pull_request" in issue:
221
+ logger.debug("Skipping issue event - this is actually a PR")
222
+ return False
223
+
224
+ firebase_id = self.get_firebase_id_from_installation(installation_id)
225
+ if not firebase_id:
226
+ logger.warning(f"No firebase_id found for installation {installation_id}")
227
+ return False
228
+
229
+ github_id = self.get_github_id(firebase_id, repo_full_name)
230
+ if not github_id:
231
+ logger.warning(f"No github_id found for {repo_full_name}")
232
+ return False
233
+
234
+ issue_data = {
235
+ "github_id": github_id,
236
+ "firebase_id": firebase_id,
237
+ "issue_id": issue.get("id"),
238
+ "issue_number": issue.get("number"),
239
+ "title": issue.get("title"),
240
+ "body": issue.get("body"),
241
+ "state": issue.get("state"),
242
+ "author": issue.get("user", {}).get("login"),
243
+ "created_at": issue.get("created_at"),
244
+ "updated_at": issue.get("updated_at"),
245
+ "url": issue.get("html_url"),
246
+ "labels": [label.get("name") for label in issue.get("labels", [])],
247
+ "comments": issue.get("comments", 0),
248
+ "reactions": issue.get("reactions", {}).get("total_count", 0) if issue.get("reactions") else 0,
249
+ }
250
+
251
+ self.supabase.table("issues").upsert([issue_data], on_conflict="firebase_id,issue_id").execute()
252
+ logger.info(f"✅ Updated issue #{issue.get('number')} ({action}) in {repo_full_name}")
253
+ return True
254
+
255
+ except Exception as e:
256
+ logger.error(f"Failed to handle issues event: {str(e)}", exc_info=True)
257
+
258
+ return False
259
+
260
+ # ========================================================================
261
+ # REPOSITORY EVENTS
262
+ # ========================================================================
263
+
264
+ def handle_repository(self, payload: Dict[str, Any]) -> bool:
265
+ """
266
+ Handle repository events (created, deleted, renamed, etc.)
267
+
268
+ Args:
269
+ payload: GitHub webhook payload
270
+
271
+ Returns:
272
+ True if handled successfully, False otherwise
273
+ """
274
+ try:
275
+ action = payload.get("action")
276
+ installation_id = payload.get("installation", {}).get("id")
277
+ repo = payload.get("repository", {})
278
+
279
+ if not installation_id or not repo:
280
+ logger.warning("Incomplete repository payload data")
281
+ return False
282
+
283
+ firebase_id = self.get_firebase_id_from_installation(installation_id)
284
+ if not firebase_id:
285
+ logger.warning(f"No firebase_id found for installation {installation_id}")
286
+ return False
287
+
288
+ logger.info(f"✅ Repository event ({action}): {repo.get('full_name')}")
289
+ return True
290
+
291
+ except Exception as e:
292
+ logger.error(f"Failed to handle repository event: {str(e)}", exc_info=True)
293
+
294
+ return False
295
+
296
+ # ========================================================================
297
+ # MAIN DISPATCHER
298
+ # ========================================================================
299
+
300
+ def handle_webhook(self, event_type: str, payload: Dict[str, Any]) -> Dict[str, Any]:
301
+ """
302
+ Main webhook dispatcher - routes to appropriate handler
303
+
304
+ Args:
305
+ event_type: GitHub event type (from X-GitHub-Event header)
306
+ payload: GitHub webhook payload
307
+
308
+ Returns:
309
+ Response dict with status and details
310
+ """
311
+ logger.info(f"Processing webhook event: {event_type}")
312
+
313
+ handlers = {
314
+ "push": self.handle_push,
315
+ "pull_request": self.handle_pull_request,
316
+ "issues": self.handle_issues,
317
+ "repository": self.handle_repository,
318
+ }
319
+
320
+ handler = handlers.get(event_type)
321
+
322
+ if not handler:
323
+ logger.warning(f"No handler for event type: {event_type}")
324
+ return {
325
+ "success": False,
326
+ "message": f"No handler for event type: {event_type}",
327
+ "event": event_type
328
+ }
329
+
330
+ try:
331
+ success = handler(payload)
332
+ return {
333
+ "success": success,
334
+ "message": f"Event {event_type} processed",
335
+ "event": event_type
336
+ }
337
+ except Exception as e:
338
+ logger.error(f"Error handling {event_type}: {str(e)}", exc_info=True)
339
+ return {
340
+ "success": False,
341
+ "message": f"Error processing {event_type}: {str(e)}",
342
+ "event": event_type
343
+ }