@@ -145,6 +145,138 @@ def getRepoData(repo, token):
145145 return githubListReq .json ()
146146
147147
148+ def queryGithubDiscussions (repo , token , until , cursor = None ):
149+ """
150+ Query GitHub discussions using GraphQL API with pagination support.
151+ Returns discussions created or updated after the 'until' date.
152+ """
153+ url = "https://api.github.com/graphql"
154+ headers = {
155+ "Authorization" : f"Bearer { token } " ,
156+ "Content-Type" : "application/json"
157+ }
158+
159+ owner , name = repo .split ("/" )
160+ after_clause = f', after: "{ cursor } "' if cursor else ""
161+
162+ query = f"""
163+ query {{
164+ repository(owner: "{ owner } ", name: "{ name } ") {{
165+ hasDiscussionsEnabled
166+ discussions(first: 100{ after_clause } , orderBy: {{field: UPDATED_AT, direction: DESC}}) {{
167+ pageInfo {{
168+ hasNextPage
169+ endCursor
170+ }}
171+ nodes {{
172+ id
173+ title
174+ url
175+ author {{
176+ login
177+ }}
178+ createdAt
179+ updatedAt
180+ comments(first: 10) {{
181+ totalCount
182+ nodes {{
183+ author {{
184+ login
185+ }}
186+ bodyText
187+ createdAt
188+ updatedAt
189+ }}
190+ }}
191+ answerChosenAt
192+ category {{
193+ name
194+ }}
195+ labels(first: 10) {{
196+ nodes {{
197+ name
198+ color
199+ }}
200+ }}
201+ }}
202+ }}
203+ }}
204+ }}
205+ """
206+
207+ response = requests .post (url , headers = headers , json = {"query" : query })
208+
209+ if response .status_code != 200 :
210+ return {"error" : f"GraphQL request failed with status { response .status_code } " }
211+
212+ data = response .json ()
213+ if "errors" in data :
214+ return {"error" : f"GraphQL errors: { data ['errors' ]} " }
215+
216+ return data
217+
218+
219+ def listGithubDiscussions (repo , token , until ):
220+ """
221+ Fetch GitHub discussions with pagination, filtering by date.
222+ """
223+ discussions = []
224+ cursor = None
225+ until_iso = until .strftime ("%Y-%m-%dT%H:%M:%SZ" )
226+
227+ while True :
228+ result = queryGithubDiscussions (repo , token , until , cursor )
229+
230+ if "error" in result :
231+ return {"discussions" : [], "error" : result ["error" ]}
232+
233+ repo_data = result .get ("data" , {}).get ("repository" , {})
234+
235+ # Check if discussions are enabled for this repository
236+ if not repo_data .get ("hasDiscussionsEnabled" , False ):
237+ return {"discussions" : [], "hasDiscussionsEnabled" : False }
238+
239+ discussions_data = repo_data .get ("discussions" , {})
240+ nodes = discussions_data .get ("nodes" , [])
241+
242+ # Filter discussions by date - include if created or updated after 'until'
243+ for discussion in nodes :
244+ created_at = discussion .get ("createdAt" , "" )
245+ updated_at = discussion .get ("updatedAt" , "" )
246+
247+ if created_at >= until_iso or updated_at >= until_iso :
248+ # Add text colors for labels (similar to existing label handling)
249+ for label in discussion .get ("labels" , {}).get ("nodes" , []):
250+ bg_color = label .get ("color" , "000000" )
251+ bg_rgb = int (bg_color , 16 )
252+ bg_r = (bg_rgb >> 16 ) & 0xFF
253+ bg_g = (bg_rgb >> 8 ) & 0xFF
254+ bg_b = (bg_rgb >> 0 ) & 0xFF
255+ luma = 0.2126 * bg_r + 0.7152 * bg_g + 0.0722 * bg_b # ITU-R BT.709
256+ if luma < 128 :
257+ label ["text_color" ] = "ffffff"
258+ else :
259+ label ["text_color" ] = "000000"
260+
261+ discussions .append (discussion )
262+ elif updated_at < until_iso :
263+ # Since discussions are ordered by updated_at DESC, we can stop here
264+ return {"discussions" : discussions , "hasDiscussionsEnabled" : True }
265+
266+ # Check if we need to fetch more pages
267+ page_info = discussions_data .get ("pageInfo" , {})
268+ if not page_info .get ("hasNextPage" , False ):
269+ break
270+
271+ cursor = page_info .get ("endCursor" )
272+
273+ # Safety check to prevent infinite loops
274+ if not cursor :
275+ break
276+
277+ return {"discussions" : discussions , "hasDiscussionsEnabled" : True }
278+
279+
148280def navigateGithubList (url , token , until , cumul = []):
149281 headers = {}
150282 headers ["Authorization" ] = "token %s" % token
@@ -186,7 +318,7 @@ def andify(l):
186318 return [{"name" : x , "last" : i == len (l ) - 1 } for i , x in enumerate (sorted (l ))]
187319
188320
189- def extractDigestInfo (events , eventFilter = None ):
321+ def extractDigestInfo (events , eventFilter = None , discussions = None ):
190322 def listify (l ):
191323 return {"count" : len (l ), "list" : l }
192324
@@ -235,6 +367,37 @@ def listify(l):
235367 commentedissues [number ]["commentors" ] = andify (
236368 commentedissues [number ]["commentors" ]
237369 )
370+
371+ # Process discussions data
372+ discussion_list = []
373+ discussion_comments = []
374+ if discussions and discussions .get ("discussions" ):
375+ for discussion in discussions ["discussions" ]:
376+ # Extract basic discussion info
377+ disc_data = {
378+ "id" : discussion .get ("id" ),
379+ "title" : discussion .get ("title" ),
380+ "url" : discussion .get ("url" ),
381+ "author" : discussion .get ("author" , {}).get ("login" , "unknown" ),
382+ "createdAt" : discussion .get ("createdAt" ),
383+ "updatedAt" : discussion .get ("updatedAt" ),
384+ "category" : discussion .get ("category" , {}).get ("name" , "" ),
385+ "labels" : discussion .get ("labels" , {}).get ("nodes" , []),
386+ "answerChosenAt" : discussion .get ("answerChosenAt" ),
387+ "isAnswered" : bool (discussion .get ("answerChosenAt" ))
388+ }
389+ discussion_list .append (disc_data )
390+
391+ # Process discussion comments
392+ comments = discussion .get ("comments" , {}).get ("nodes" , [])
393+ if comments :
394+ disc_comments = {
395+ "discussion" : disc_data ,
396+ "comments" : comments ,
397+ "commentscount" : discussion .get ("comments" , {}).get ("totalCount" , len (comments )),
398+ "commentors" : andify (list (set ([c .get ("author" , {}).get ("login" , "unknown" ) for c in comments if c .get ("author" )])))
399+ }
400+ discussion_comments .append (disc_comments )
238401 data ["errors" ] = listify (errors )
239402 data ["newissues" ] = listify (newissues )
240403 data ["closedissues" ] = listify (closedissues )
@@ -264,6 +427,13 @@ def listify(l):
264427 data ["activepr" ] = (
265428 data ["prcommentscount" ] > 0 or len (newpr ) > 0 or len (mergedpr ) > 0
266429 )
430+
431+ # Add discussions data
432+ data ["discussions" ] = listify (discussion_list )
433+ data ["discussioncomments" ] = listify (discussion_comments )
434+ data ["discussioncommentscount" ] = sum (d ["commentscount" ] for d in discussion_comments )
435+ data ["activediscussion" ] = len (discussion_list ) > 0 or data ["discussioncommentscount" ] > 0
436+
267437 return data
268438
269439# this preserves order which list(set()) wouldn't
@@ -337,6 +507,7 @@ def sendDigest(config, period="daily"):
337507
338508 events ["activeissuerepos" ] = []
339509 events ["activeprrepos" ] = []
510+ events ["activediscussionrepos" ] = []
340511 events ["repostatus" ] = []
341512 events ["period" ] = duration .capitalize ()
342513
@@ -370,8 +541,13 @@ def sendDigest(config, period="daily"):
370541 for r in s ["repos" ]:
371542 eventFilters [r ] = s .get ("eventFilter" , None )
372543 for repo in repos :
544+ # Fetch discussions data if available
545+ discussions_data = None
546+ if token :
547+ discussions_data = listGithubDiscussions (repo , token , until )
548+
373549 data = extractDigestInfo (
374- listGithubEvents (repo , token , until ), eventFilters [repo ]
550+ listGithubEvents (repo , token , until ), eventFilters [repo ], discussions_data
375551 )
376552 data ["repo" ] = getRepoData (repo , token )
377553 data ["name" ] = repo
@@ -384,6 +560,8 @@ def sendDigest(config, period="daily"):
384560 events ["activeissuerepos" ].append (data )
385561 if data ["activepr" ]:
386562 events ["activeprrepos" ].append (data )
563+ if data ["activediscussion" ]:
564+ events ["activediscussionrepos" ].append (data )
387565 events ["filtered" ] = d .get ("eventFilter" , None )
388566 events ["filteredlabels" ] = (
389567 len (d .get ("eventFilter" , {}).get ("label" , [])) > 0
@@ -398,10 +576,12 @@ def sendDigest(config, period="daily"):
398576 events ["topic" ] = d .get ("topic" , None )
399577 events ["activeissues" ] = len (events ["activeissuerepos" ])
400578 events ["activeprs" ] = len (events ["activeprrepos" ])
579+ events ["activediscussions" ] = len (events ["activediscussionrepos" ])
401580 events ["summary" ] = len (events ["repostatus" ])
402581 if (
403582 events ["activeissues" ] > 0
404583 or events ["activeprs" ] > 0
584+ or events ["activediscussions" ] > 0
405585 or events ["summary" ] > 0
406586 ):
407587 templates , error = loadTemplates (
0 commit comments