diff --git a/index.py b/index.py
index 8523d62..c05c020 100755
--- a/index.py
+++ b/index.py
@@ -145,6 +145,133 @@ def getRepoData(repo, token):
return githubListReq.json()
+def queryGithubDiscussions(repo, token, until, cursor=None):
+ """
+ Query GitHub discussions using GraphQL API with pagination support.
+ Returns discussions created or updated after the 'until' date.
+ """
+ url = "https://api.github.com/graphql"
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+
+ owner, name = repo.split("/")
+ after_clause = f', after: "{cursor}"' if cursor else ""
+
+ query = f"""
+ query {{
+ repository(owner: "{owner}", name: "{name}") {{
+ hasDiscussionsEnabled
+ discussions(first: 100{after_clause}, orderBy: {{field: UPDATED_AT, direction: DESC}}) {{
+ pageInfo {{
+ hasNextPage
+ endCursor
+ }}
+ nodes {{
+ id
+ title
+ url
+ author {{
+ login
+ }}
+ createdAt
+ updatedAt
+ comments(first: 10) {{
+ totalCount
+ nodes {{
+ author {{
+ login
+ }}
+ createdAt
+ updatedAt
+ }}
+ }}
+ labels(first: 10) {{
+ nodes {{
+ name
+ color
+ }}
+ }}
+ }}
+ }}
+ }}
+ }}
+ """
+
+ response = requests.post(url, headers=headers, json={"query": query})
+
+ if response.status_code != 200:
+ return {"error": f"GraphQL request failed with status {response.status_code}"}
+
+ data = response.json()
+ if "errors" in data:
+ return {"error": f"GraphQL errors: {data['errors']}"}
+
+ return data
+
+
+def listGithubDiscussions(repo, token, until):
+ """
+ Fetch GitHub discussions with pagination, filtering by date.
+ """
+ discussions = []
+ cursor = None
+ until_iso = until.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ while True:
+ result = queryGithubDiscussions(repo, token, until, cursor)
+
+ if "error" in result:
+ return {"discussions": [], "error": result["error"]}
+
+ repo_data = result.get("data", {}).get("repository", {})
+
+ # Check if discussions are enabled for this repository
+ if not repo_data.get("hasDiscussionsEnabled", False):
+ return {"discussions": [], "hasDiscussionsEnabled": False}
+
+ discussions_data = repo_data.get("discussions", {})
+ nodes = discussions_data.get("nodes", [])
+
+ # Filter discussions by date - include if created or updated after 'until'
+ for discussion in nodes:
+ created_at = discussion.get("createdAt", "")
+ updated_at = discussion.get("updatedAt", "")
+
+ if created_at >= until_iso or updated_at >= until_iso:
+ # Add text colors for labels (similar to existing label handling)
+ for label in discussion.get("labels", {}).get("nodes", []):
+ bg_color = label.get("color", "000000")
+ bg_rgb = int(bg_color, 16)
+ bg_r = (bg_rgb >> 16) & 0xFF
+ bg_g = (bg_rgb >> 8) & 0xFF
+ bg_b = (bg_rgb >> 0) & 0xFF
+ luma = 0.2126 * bg_r + 0.7152 * bg_g + 0.0722 * bg_b # ITU-R BT.709
+ if luma < 128:
+ label["text_color"] = "ffffff"
+ else:
+ label["text_color"] = "000000"
+
+ discussions.append(discussion)
+ elif updated_at < until_iso:
+ # Since discussions are ordered by updated_at DESC, we can stop here
+ return {"discussions": discussions, "hasDiscussionsEnabled": True}
+
+ # Check if we need to fetch more pages
+ page_info = discussions_data.get("pageInfo", {})
+ if not page_info.get("hasNextPage", False):
+ break
+
+ cursor = page_info.get("endCursor")
+
+ # Safety check to prevent infinite loops
+ if not cursor:
+ break
+
+ return {"discussions": discussions, "hasDiscussionsEnabled": True}
+
+
def navigateGithubList(url, token, until, cumul=[]):
headers = {}
headers["Authorization"] = "token %s" % token
@@ -186,7 +313,7 @@ def andify(l):
return [{"name": x, "last": i == len(l) - 1} for i, x in enumerate(sorted(l))]
-def extractDigestInfo(events, eventFilter=None):
+def extractDigestInfo(events, eventFilter=None, discussions=None):
def listify(l):
return {"count": len(l), "list": l}
@@ -235,6 +362,34 @@ def listify(l):
commentedissues[number]["commentors"] = andify(
commentedissues[number]["commentors"]
)
+
+ # Process discussions data
+ discussion_list = []
+ discussion_comments = []
+ if discussions and discussions.get("discussions"):
+ for discussion in discussions["discussions"]:
+ # Extract basic discussion info
+ disc_data = {
+ "id": discussion.get("id"),
+ "title": discussion.get("title"),
+ "url": discussion.get("url"),
+ "author": discussion.get("author", {}).get("login", "unknown"),
+ "createdAt": discussion.get("createdAt"),
+ "updatedAt": discussion.get("updatedAt"),
+ "labels": discussion.get("labels", {}).get("nodes", []),
+ }
+ discussion_list.append(disc_data)
+
+ # Process discussion comments
+ comments = discussion.get("comments", {}).get("nodes", [])
+ if comments:
+ disc_comments = {
+ "discussion": disc_data,
+ "comments": comments,
+ "commentscount": discussion.get("comments", {}).get("totalCount", len(comments)),
+ "commentors": andify(list(set([c.get("author", {}).get("login", "unknown") for c in comments if c.get("author")])))
+ }
+ discussion_comments.append(disc_comments)
data["errors"] = listify(errors)
data["newissues"] = listify(newissues)
data["closedissues"] = listify(closedissues)
@@ -264,6 +419,13 @@ def listify(l):
data["activepr"] = (
data["prcommentscount"] > 0 or len(newpr) > 0 or len(mergedpr) > 0
)
+
+ # Add discussions data
+ data["discussions"] = listify(discussion_list)
+ data["discussioncomments"] = listify(discussion_comments)
+ data["discussioncommentscount"] = sum(d["commentscount"] for d in discussion_comments)
+ data["activediscussion"] = len(discussion_list) > 0 or data["discussioncommentscount"] > 0
+
return data
# this preserves order which list(set()) wouldn't
@@ -337,6 +499,7 @@ def sendDigest(config, period="daily"):
events["activeissuerepos"] = []
events["activeprrepos"] = []
+ events["activediscussionrepos"] = []
events["repostatus"] = []
events["period"] = duration.capitalize()
@@ -370,8 +533,13 @@ def sendDigest(config, period="daily"):
for r in s["repos"]:
eventFilters[r] = s.get("eventFilter", None)
for repo in repos:
+ # Fetch discussions data if available
+ discussions_data = None
+ if token:
+ discussions_data = listGithubDiscussions(repo, token, until)
+
data = extractDigestInfo(
- listGithubEvents(repo, token, until), eventFilters[repo]
+ listGithubEvents(repo, token, until), eventFilters[repo], discussions_data
)
data["repo"] = getRepoData(repo, token)
data["name"] = repo
@@ -384,6 +552,8 @@ def sendDigest(config, period="daily"):
events["activeissuerepos"].append(data)
if data["activepr"]:
events["activeprrepos"].append(data)
+ if data["activediscussion"]:
+ events["activediscussionrepos"].append(data)
events["filtered"] = d.get("eventFilter", None)
events["filteredlabels"] = (
len(d.get("eventFilter", {}).get("label", [])) > 0
@@ -398,10 +568,12 @@ def sendDigest(config, period="daily"):
events["topic"] = d.get("topic", None)
events["activeissues"] = len(events["activeissuerepos"])
events["activeprs"] = len(events["activeprrepos"])
+ events["activediscussions"] = len(events["activediscussionrepos"])
events["summary"] = len(events["repostatus"])
if (
events["activeissues"] > 0
or events["activeprs"] > 0
+ or events["activediscussions"] > 0
or events["summary"] > 0
):
templates, error = loadTemplates(
diff --git a/templates/generic/digest b/templates/generic/digest
index 8598f63..5949fad 100644
--- a/templates/generic/digest
+++ b/templates/generic/digest
@@ -67,6 +67,27 @@
{{/activeprrepos}}
{{/activeprs}}
+{{#activediscussions}}Discussions
+-----------
+{{#activediscussionrepos}}
+* {{name}} ({{discussions.count}} discussions, {{discussioncommentscount}} comments)
+{{#discussions.count}} {{.}} discussions active:
+{{#discussions.list}}
+ - {{{title}}} (by {{author}})
+ {{{url}}} {{#labels}}[{{name}}] {{/labels}}
+{{/discussions.list}}
+
+{{/discussions.count}}
+{{#discussioncomments.count}} Discussions with comments:
+{{#discussioncomments.list}}
+ - {{{discussion.title}}} ({{commentscount}} by {{#commentors}}{{name}}{{^last}}, {{/last}}{{/commentors}})
+ {{{discussion.url}}}
+{{/discussioncomments.list}}
+
+{{/discussioncomments.count}}
+{{/activediscussionrepos}}
+{{/activediscussions}}
+
Repositories tracked by this digest:
-----------------------------------
{{#repos}}
diff --git a/templates/generic/digest.html b/templates/generic/digest.html
index f2cce80..6b311cb 100644
--- a/templates/generic/digest.html
+++ b/templates/generic/digest.html
@@ -99,6 +99,17 @@
{{name}} (+{{newpr.count}}/-{{mergedpr.count}}/💬{{prcommentscount}})
{{/activeprrepos}}
{{/activeprs}}
+{{#activediscussions}}Discussions
+{{#activediscussionrepos}}
+{{name}} (+{{newdiscussions.count}})
+{{#newdiscussions.count}} {{.}} new discussions:
+ {{#newdiscussions.list}}
+ - {{title}} (by {{author.login}})
+ {{/newdiscussions.list}}
+{{/newdiscussions.count}}
+{{/activediscussionrepos}}
+{{/activediscussions}}
+
Repositories tracked by this digest:
diff --git a/test_digest.py b/test_digest.py
index 3d65f73..5846b42 100644
--- a/test_digest.py
+++ b/test_digest.py
@@ -82,6 +82,23 @@ def setUp(self):
body=self.read_file("tests/rec-repos.json"),
content_type="application/json",
)
+ # Add GraphQL discussions endpoint mocks
+ responses.add(
+ responses.POST,
+ "https://api.github.com/graphql",
+ json={
+ "data": {
+ "repository": {
+ "hasDiscussionsEnabled": True,
+ "discussions": {
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
+ "nodes": []
+ }
+ }
+ }
+ },
+ content_type="application/json",
+ )
def parseReferenceMessage():
return headers, body
@@ -105,7 +122,7 @@ def test_weekly_digest(self, mock_smtp):
],
mock_smtp.return_value.__enter__.return_value.sendmail,
)
- self.assertEqual(len(responses.calls), 6)
+ self.assertEqual(len(responses.calls), 13)
@responses.activate
@patch("smtplib.SMTP", autospec=True)
@@ -113,12 +130,39 @@ def test_quarterly_summary(self, mock_smtp):
self.do_digest(
"quarterly", [{"dom@localhost": "tests/summary-quarterly.msg"}], mock_smtp.return_value.__enter__.return_value.sendmail
)
- self.assertEqual(len(responses.calls), 2)
+ self.assertEqual(len(responses.calls), 3)
+
+ @responses.activate
+ @patch("smtplib.SMTP", autospec=True)
+ def test_weekly_digest_with_discussions(self, mock_smtp):
+ # Override GraphQL response with discussions data
+ responses.replace(
+ responses.POST,
+ "https://api.github.com/graphql",
+ body=self.read_file("tests/repo1-discussions-comprehensive.json"),
+ content_type="application/json",
+ )
+
+ # Use the discussions-specific MLS config
+ config_with_discussions = dict(config)
+ config_with_discussions["mls"] = "tests/mls-discussions.json"
+
+ self.do_digest(
+ "Wednesday",
+ [{"dom@localhost": "tests/digest-weekly-with-discussions.msg"}],
+ mock_smtp.return_value.__enter__.return_value.sendmail,
+ config_with_discussions
+ )
+
+ # Check that GraphQL was called for discussions
+ graphql_calls = [call for call in responses.calls if call.request.url == "https://api.github.com/graphql"]
+ self.assertTrue(len(graphql_calls) > 0)
- def do_digest(self, period, refs, mock_smtp):
+ def do_digest(self, period, refs, mock_smtp, config_override=None):
import email
- sendDigest(config, period)
+ digest_config = config_override if config_override else config
+ sendDigest(digest_config, period)
self.assertEqual(mock_smtp.call_count, len(refs))
counter = 0
import pprint
diff --git a/tests/digest-weekly-allrepos.msg b/tests/digest-weekly-allrepos.msg
index d51027e..7c9187b 100644
--- a/tests/digest-weekly-allrepos.msg
+++ b/tests/digest-weekly-allrepos.msg
@@ -31,6 +31,7 @@ Issues
+
Repositories tracked by this digest:
-----------------------------------
* https://github.com/w3c/webrtc-pc
diff --git a/tests/digest-weekly-filtered.msg b/tests/digest-weekly-filtered.msg
index 7426382..b494474 100644
--- a/tests/digest-weekly-filtered.msg
+++ b/tests/digest-weekly-filtered.msg
@@ -29,6 +29,8 @@ Issues
+
+
Repositories tracked by this digest:
-----------------------------------
* https://github.com/w3c/webrtc-pc
diff --git a/tests/digest-weekly-filtered.msg.html b/tests/digest-weekly-filtered.msg.html
index e7c3144..c4ad555 100644
--- a/tests/digest-weekly-filtered.msg.html
+++ b/tests/digest-weekly-filtered.msg.html
@@ -70,6 +70,7 @@ w3c/webrtc-pc (+0/-1/💬5)
+
Repositories tracked by this digest:
diff --git a/tests/digest-weekly-repofiltered.msg b/tests/digest-weekly-repofiltered.msg
index 4e0316a..412a125 100644
--- a/tests/digest-weekly-repofiltered.msg
+++ b/tests/digest-weekly-repofiltered.msg
@@ -95,6 +95,8 @@ Pull requests
+
+
Repositories tracked by this digest:
-----------------------------------
* https://github.com/w3c/webcrypto
diff --git a/tests/digest-weekly-with-discussions.msg b/tests/digest-weekly-with-discussions.msg
new file mode 100644
index 0000000..2513c33
--- /dev/null
+++ b/tests/digest-weekly-with-discussions.msg
@@ -0,0 +1,121 @@
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="utf-8"; format="flowed"
+
+
+
+
+
+
+Issues
+------
+* w3c/webrtc-pc (+5/-2/💬12)
+ 5 issues created:
+ - replaceTrack(null): Allowed? (by aboba)
+ https://github.com/w3c/webrtc-pc/issues/947
+ - sender.setParameters(): Changing simulcast parameters? (by aboba)
+ https://github.com/w3c/webrtc-pc/issues/945
+ - Effect of sender.replaceTrack(null) (by aboba)
+ https://github.com/w3c/webrtc-pc/issues/943
+ - Meta: auto-publish changes to the spec (by foolip)
+ https://github.com/w3c/webrtc-pc/issues/942 [editorial]
+ - STUN/TURN Auto Discovery handling (by misi)
+ https://github.com/w3c/webrtc-pc/issues/941
+
+
+ 5 issues received 12 new comments:
+ - #942 Meta: auto-publish changes to the spec (1 by henbos)
+ https://github.com/w3c/webrtc-pc/issues/942
+ - #927 What happens when setDirection() is called (1 by taylor-b)
+ https://github.com/w3c/webrtc-pc/issues/927 [November interim topic]
+ - #888 Section 6.2: Issue 3 - buffered data at transport close (5 by alvestrand, feross, jesup, taylor-b)
+ https://github.com/w3c/webrtc-pc/issues/888 [PR exists]
+ - #803 Rules for negotiation-needed flag need to be updated for transceivers. (2 by aboba, taylor-b)
+ https://github.com/w3c/webrtc-pc/issues/803 [November interim topic]
+ - #714 STUN/TURN OAuth token auth parameter passing (3 by juberti, misi)
+ https://github.com/w3c/webrtc-pc/issues/714 [External feedback] [July interim topic] [List discussion needed] [November interim topic] [PR exists]
+
+
+ 2 issues closed:
+ - Rules for negotiation-needed flag need to be updated for transceivers. https://github.com/w3c/webrtc-pc/issues/803 [November interim topic]
+ - Section 6.2: Issue 3 - buffered data at transport close https://github.com/w3c/webrtc-pc/issues/888 [PR exists]
+
+
+
+
+
+
+Pull requests
+-------------
+* w3c/webrtc-pc (+8/-1/💬10)
+ 8 pull requests submitted:
+ - Revert "Respec now uses yarn instead of npm" (by vivienlacourba)
+ https://github.com/w3c/webrtc-pc/pull/948
+ - Splitting transceiver direction into "direction" and "currentDirection". (by taylor-b)
+ https://github.com/w3c/webrtc-pc/pull/946
+ - Effect of setting sender.track to null (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/944
+ - Use of "stopped" in insertDTMF and replaceTrack (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/940
+ - Removed "stopped" from removeTrack (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/939
+ - What happens when setDirection() is called (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/938
+ - What happens when transceiver.stop() is called (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/937
+ - Remove Issue 1: Current and Pending SDP (by aboba)
+ https://github.com/w3c/webrtc-pc/pull/936
+
+
+ 7 pull requests received 10 new comments:
+ - #946 Splitting transceiver direction into "direction" and "currentDirection". (1 by aboba)
+ https://github.com/w3c/webrtc-pc/pull/946
+ - #944 Effect of setting sender.track to null (4 by aboba, pthatcherg)
+ https://github.com/w3c/webrtc-pc/pull/944
+ - #940 Use of "stopped" in insertDTMF and replaceTrack (1 by aboba)
+ https://github.com/w3c/webrtc-pc/pull/940
+ - #939 Removed "stopped" from removeTrack (1 by aboba)
+ https://github.com/w3c/webrtc-pc/pull/939
+ - #929 Have RTCSessionDescription's sdp member default to "" (1 by jan-ivar)
+ https://github.com/w3c/webrtc-pc/pull/929
+ - #920 Revise text dealing with with the ICE Agent/User Agent interactions. (1 by aboba)
+ https://github.com/w3c/webrtc-pc/pull/920 [November interim topic]
+ - #919 Removing incorrect statement related to IP leaking issue. (1 by vivienlacourba)
+ https://github.com/w3c/webrtc-pc/pull/919
+
+
+ 1 pull requests merged:
+ - Respec now uses yarn instead of npm
+ https://github.com/w3c/webrtc-pc/pull/935
+
+
+
+
+Discussions
+-----------
+* w3c/webrtc-pc (2 discussions, 4 comments)
+ 2 discussions active:
+ - WebRTC Protocol Discussion (by alice-developer)
+ https://github.com/w3c/webrtc-pc/discussions/2850 [enhancement] [needs-review]
+ - Question about ICE candidate gathering (by david-student)
+ https://github.com/w3c/webrtc-pc/discussions/2851 [question]
+
+
+ Discussions with comments:
+ - WebRTC Protocol Discussion (3 by alice-developer, bob-reviewer, charlie-contributor)
+ https://github.com/w3c/webrtc-pc/discussions/2850
+ - Question about ICE candidate gathering (1 by expert-maintainer)
+ https://github.com/w3c/webrtc-pc/discussions/2851
+
+
+
+
+Repositories tracked by this digest:
+-----------------------------------
+* https://github.com/w3c/webrtc-pc
+
+
+
+
+--
+Sent with ♥
diff --git a/tests/digest-weekly.msg b/tests/digest-weekly.msg
index ec37aac..01b52e5 100644
--- a/tests/digest-weekly.msg
+++ b/tests/digest-weekly.msg
@@ -92,6 +92,9 @@ Pull requests
+
+
+
Repositories tracked by this digest:
-----------------------------------
* https://github.com/w3c/webrtc-pc
diff --git a/tests/mls-discussions.json b/tests/mls-discussions.json
new file mode 100644
index 0000000..440397e
--- /dev/null
+++ b/tests/mls-discussions.json
@@ -0,0 +1,11 @@
+{
+ "dom@localhost": {
+ "digest:wednesday": [
+ {
+ "repos": [
+ "w3c/webrtc-pc"
+ ]
+ }
+ ]
+ }
+}
diff --git a/tests/repo1-discussions-comprehensive.json b/tests/repo1-discussions-comprehensive.json
new file mode 100644
index 0000000..456e96a
--- /dev/null
+++ b/tests/repo1-discussions-comprehensive.json
@@ -0,0 +1,105 @@
+{
+ "data": {
+ "repository": {
+ "hasDiscussionsEnabled": true,
+ "discussions": {
+ "pageInfo": {
+ "hasNextPage": false,
+ "endCursor": "Y3Vyc29yOnYyOpHOEj4HSw=="
+ },
+ "nodes": [
+ {
+ "id": "D_kwDOGvjz5M4ANMbH",
+ "title": "WebRTC Protocol Discussion",
+ "url": "https://github.com/w3c/webrtc-pc/discussions/2850",
+ "author": {
+ "login": "alice-developer"
+ },
+ "createdAt": "2016-11-15T12:30:00Z",
+ "updatedAt": "2016-11-15T14:45:00Z",
+ "comments": {
+ "totalCount": 3,
+ "nodes": [
+ {
+ "author": {
+ "login": "bob-reviewer"
+ },
+ "bodyText": "This is a very interesting proposal. Have you considered the backwards compatibility implications?",
+ "createdAt": "2016-11-15T13:15:00Z",
+ "updatedAt": "2016-11-15T13:15:00Z"
+ },
+ {
+ "author": {
+ "login": "charlie-contributor"
+ },
+ "bodyText": "I agree with Bob. Also, what about the security considerations mentioned in RFC 8831?",
+ "createdAt": "2016-11-15T14:00:00Z",
+ "updatedAt": "2016-11-15T14:00:00Z"
+ },
+ {
+ "author": {
+ "login": "alice-developer"
+ },
+ "bodyText": "Great points! I'll address both concerns in the next iteration.",
+ "createdAt": "2016-11-15T14:45:00Z",
+ "updatedAt": "2016-11-15T14:45:00Z"
+ }
+ ]
+ },
+ "answerChosenAt": null,
+ "category": {
+ "name": "Ideas"
+ },
+ "labels": {
+ "nodes": [
+ {
+ "name": "enhancement",
+ "color": "a2eeef"
+ },
+ {
+ "name": "needs-review",
+ "color": "fbca04"
+ }
+ ]
+ }
+ },
+ {
+ "id": "D_kwDOGvjz5M4ANMbI",
+ "title": "Question about ICE candidate gathering",
+ "url": "https://github.com/w3c/webrtc-pc/discussions/2851",
+ "author": {
+ "login": "david-student"
+ },
+ "createdAt": "2016-11-14T09:20:00Z",
+ "updatedAt": "2016-11-15T16:30:00Z",
+ "comments": {
+ "totalCount": 1,
+ "nodes": [
+ {
+ "author": {
+ "login": "expert-maintainer"
+ },
+ "bodyText": "Good question! The ICE candidate gathering process is defined in section 4.1.3.1 of the spec. Here's how it works...",
+ "createdAt": "2016-11-15T16:30:00Z",
+ "updatedAt": "2016-11-15T16:30:00Z"
+ }
+ ]
+ },
+ "answerChosenAt": "2016-11-15T16:30:00Z",
+ "category": {
+ "name": "Q&A"
+ },
+ "labels": {
+ "nodes": [
+ {
+ "name": "question",
+ "color": "d876e3"
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/summary-quarterly.msg b/tests/summary-quarterly.msg
index d309183..0cd40f8 100644
--- a/tests/summary-quarterly.msg
+++ b/tests/summary-quarterly.msg
@@ -21,6 +21,8 @@ Repositories
+
+
Repositories tracked by this digest:
-----------------------------------
* https://github.com/w3c/webcrypto