Skip to content

Commit 5b1103d

Browse files
committed
attempt #2
1 parent 096d22a commit 5b1103d

File tree

1 file changed

+75
-290
lines changed

1 file changed

+75
-290
lines changed
Lines changed: 75 additions & 290 deletions
Original file line numberDiff line numberDiff line change
@@ -1,293 +1,78 @@
1-
#!python
2-
3-
# DEPENDENCIES
4-
# python3 -m pip install python-graphql-client
5-
6-
#Ref : Action in python https://www.python-engineer.com/posts/run-python-github-actions/
7-
8-
# source: https://github.com/sofa-framework/sofa/blob/master/scripts/comment-close-old-discussions.py
9-
10-
111
import os
12-
from datetime import datetime, timedelta, date
13-
from python_graphql_client import GraphqlClient
14-
from dateutil.relativedelta import relativedelta
15-
16-
17-
client = GraphqlClient(endpoint="https://api.github.com/graphql")
18-
github_token = os.environ['GITHUB_TOKEN']
19-
20-
21-
# List of the repository to scan
22-
repos=[['XRPLF', 'XRPL-Standards']]
23-
24-
25-
# Format the reference date (with which the last reply will be compared)
26-
# Today
27-
date_today = date.today()
28-
# warning delay = 2-month delay for warning
29-
delay_warning = relativedelta(days = 90)
30-
date_reference_warning = date_today - delay_warning
31-
# closing delay = 2+2.5-month delay for closing
32-
delay_closing = relativedelta(days = 90) + relativedelta(days = 30)
33-
date_reference_closing = date_today - delay_closing
34-
35-
# Check if the "createdAt" is older than the "date_reference"
36-
def isOlderThan(date_reference, createdAt):
37-
# Format date of creation YYYY-MM-DD
38-
creation_date = createdAt[:-10]
39-
creation_date = datetime.strptime(creation_date, '%Y-%m-%d')
40-
41-
if creation_date.date() > date_reference:
42-
return False
43-
else :
44-
return True
45-
46-
# Returns true of the date "createdAt" is more than the warning delay
47-
def isToBeWarned(createdAt):
48-
return isOlderThan(date_reference_warning, createdAt)
49-
50-
51-
# Returns true of the date "createdAt" is more than the closing delay
52-
def isToBeClosed(createdAt):
53-
return isOlderThan(date_reference_closing, createdAt)
54-
55-
56-
def computeListOfDiscussionToProcess():
57-
for repo in repos:
58-
59-
owner = repo[0]
60-
name = repo[1]
61-
62-
has_next_page = True
63-
after_cursor = None
64-
65-
to_be_warned_discussion_number = []
66-
to_be_warned_discussion_id = []
67-
to_be_warned_discussion_author = []
68-
69-
to_be_closed_discussion_number = []
70-
to_be_closed_discussion_id = []
71-
to_be_closed_discussion_author = []
72-
73-
while has_next_page:
74-
# Trigger the query on discussions
75-
data = client.execute(
76-
query = make_query_discussions(owner, name, after_cursor),
77-
headers = {"Authorization": "Bearer {}".format(github_token)},
78-
)
79-
80-
# Process each discussion
81-
for discussion in data["data"]["repository"]["discussions"]["nodes"]:
82-
83-
# Save original author of the discussion
84-
discussionAuthor = discussion["author"]["login"]
85-
86-
# Detect the last comment
87-
lastCommentId = len(discussion["comments"]["nodes"]) - 1
88-
89-
# Pass to the next discussion item if :
90-
# no comment in the discussion OR discussion is answered OR closed
91-
if(lastCommentId < 0 or discussion["closed"] == True or discussion["isAnswered"] == True ):
2+
import requests
3+
from datetime import datetime, timedelta
4+
5+
GITHUB_TOKEN = os.environ['GITHUB_TOKEN']
6+
REPO = os.environ['GITHUB_REPOSITORY']
7+
API_URL = f"https://api.github.com/repos/{REPO}/discussions"
8+
HEADERS = {
9+
"Authorization": f"token {GITHUB_TOKEN}",
10+
"Accept": "application/vnd.github.v3+json"
11+
}
12+
13+
STALE_LABEL = "Stale"
14+
STALE_COMMENT = "This discussion has been marked as stale due to inactivity for 90 days. If there is no further activity, it will be closed in 14 days."
15+
16+
def get_discussions():
17+
discussions = []
18+
page = 1
19+
while True:
20+
resp = requests.get(f"{API_URL}?per_page=100&page={page}", headers=HEADERS)
21+
if resp.status_code != 200:
22+
break
23+
data = resp.json()
24+
if not data:
25+
break
26+
discussions.extend(data)
27+
page += 1
28+
return discussions
29+
30+
def get_comments(discussion_number):
31+
url = f"{API_URL}/{discussion_number}/comments"
32+
resp = requests.get(url, headers=HEADERS)
33+
if resp.status_code != 200:
34+
return []
35+
return resp.json()
36+
37+
def add_label(discussion_number, label):
38+
print(f"Adding label '{label}' to discussion #{discussion_number}")
39+
# url = f"{API_URL}/{discussion_number}/labels"
40+
# requests.post(url, headers=HEADERS, json={"labels": [label]})
41+
42+
def post_comment(discussion_number, body):
43+
print(f"Posting comment to discussion #{discussion_number}")
44+
# url = f"{API_URL}/{discussion_number}/comments"
45+
# requests.post(url, headers=HEADERS, json={"body": body})
46+
47+
def close_and_lock(discussion_number):
48+
print(f"Closing and locking discussion #{discussion_number}")
49+
# url = f"{API_URL}/{discussion_number}"
50+
# requests.patch(url, headers=HEADERS, json={"state": "closed", "locked": True})
51+
52+
def main():
53+
now = datetime.utcnow()
54+
discussions = get_discussions()
55+
for d in discussions:
56+
number = d['number']
57+
labels = [l['name'] for l in d.get('labels', [])]
58+
last_updated = datetime.strptime(d['updated_at'], "%Y-%m-%dT%H:%M:%SZ")
59+
comments = get_comments(number)
60+
stale_comment = next((c for c in comments if STALE_COMMENT in c['body']), None)
61+
62+
# Mark as stale after 90 days
63+
if (now - last_updated).days >= 90 and STALE_LABEL not in labels:
64+
add_label(number, STALE_LABEL)
65+
post_comment(number, STALE_COMMENT)
9266
continue
9367

94-
lastReplyOnLastComment = len(discussion["comments"]["nodes"][lastCommentId]["replies"]["nodes"]) - 1
95-
96-
# No replies on the last comment
97-
if(lastReplyOnLastComment < 0):
98-
author = discussion["comments"]["nodes"][lastCommentId]["author"]["login"]
99-
dateLastMessage = discussion["comments"]["nodes"][lastCommentId]["createdAt"]
100-
# Select the last reply of the last comment
101-
else:
102-
author = discussion["comments"]["nodes"][lastCommentId]["replies"]["nodes"][lastReplyOnLastComment]["author"]["login"]
103-
dateLastMessage = discussion["comments"]["nodes"][lastCommentId]["replies"]["nodes"][lastReplyOnLastComment]["createdAt"]
104-
105-
authorAsList = [author]
106-
107-
if isToBeClosed(dateLastMessage) == True:
108-
to_be_closed_discussion_number.append(discussion["number"])
109-
to_be_closed_discussion_id.append(discussion["id"])
110-
to_be_closed_discussion_author.append(discussionAuthor)
111-
elif isToBeWarned(dateLastMessage) == True and author != "github-actions":
112-
to_be_warned_discussion_number.append(discussion["number"])
113-
to_be_warned_discussion_id.append(discussion["id"])
114-
to_be_warned_discussion_author.append(discussionAuthor)
115-
116-
117-
# save if request has another page to browse and its cursor pointers
118-
has_next_page = data["data"]["repository"]["discussions"]["pageInfo"]["hasNextPage"]
119-
after_cursor = data["data"]["repository"]["discussions"]["pageInfo"]["endCursor"]
120-
return [to_be_warned_discussion_number,to_be_warned_discussion_id,to_be_warned_discussion_author,to_be_closed_discussion_number,to_be_closed_discussion_id,to_be_closed_discussion_author]
121-
122-
123-
# Query to access all discussions
124-
def make_query_discussions(owner, name, after_cursor=None):
125-
query = """
126-
query {
127-
repository(owner: "%s" name: "%s") {
128-
discussions(answered: false, first: 10, after:AFTER) {
129-
totalCount
130-
pageInfo {
131-
hasNextPage
132-
endCursor
133-
}
134-
nodes {
135-
id
136-
number
137-
isAnswered
138-
closed
139-
author {
140-
login
141-
}
142-
comments (first: 100) {
143-
nodes {
144-
createdAt
145-
author {
146-
login
147-
}
148-
replies (first: 100) {
149-
nodes {
150-
createdAt
151-
author {
152-
login
153-
}
154-
}
155-
}
156-
}
157-
}
158-
}
159-
}
160-
}
161-
}""" % (owner, name)
162-
return query.replace("AFTER", '"{}"'.format(after_cursor) if after_cursor else "null")
163-
164-
165-
166-
167-
def make_github_warning_comment(discussion_id, discussion_author):
168-
message = ":warning: :warning: :warning:<br>@"+str(discussion_author)+"<br>Feedback has been given to you by the project reviewers, however we have not received a response from you. Without further news in the coming weeks, this discussion will be automatically closed in order to keep this forum clean and fresh :seedling: Thank you for your understanding"
169-
170-
query = """
171-
mutation {
172-
addDiscussionComment(input: {body: "%s", discussionId: "%s"}) {
173-
comment {
174-
id
175-
}
176-
}
177-
}
178-
""" % (message, discussion_id)
179-
return query
180-
181-
182-
183-
def make_github_closing_comment(discussion_id, discussion_author):
184-
message = ":warning: :warning: :warning:<br>@"+str(discussion_author)+"<br>In accordance with our forum management policy, the last reply is more than 4 months old and is therefore closed. Our objective is to keep the forum up to date and offer the best support experience.<br><br>Please feel free to reopen it if the topic is still active by providing us with an update. Please feel free to open a new thread at any time - we'll be happy to help where and when we can."
185-
186-
query = """
187-
mutation {
188-
addDiscussionComment(input: {body: "%s", discussionId: "%s"}) {
189-
comment {
190-
id
191-
}
192-
}
193-
}
194-
""" % (message, discussion_id)
195-
return query
196-
197-
198-
199-
def close_github_discussion(discussion_id):
200-
201-
query = """
202-
mutation {
203-
closeDiscussion(input: {discussionId: "%s"}) {
204-
discussion {
205-
id
206-
}
207-
}
208-
}
209-
""" % discussion_id
210-
return query
211-
212-
213-
214-
215-
216-
217-
#==========================================================
218-
# STEPS computed by the script
219-
#==========================================================
220-
# 1 - get the discussion to be warned and closed
221-
result = computeListOfDiscussionToProcess()
222-
to_be_warned_discussion_number = result[0]
223-
to_be_warned_discussion_id = result[1]
224-
to_be_warned_discussion_author = result[2]
225-
to_be_closed_discussion_number = result[3]
226-
to_be_closed_discussion_id = result[4]
227-
to_be_closed_discussion_author = result[5]
228-
#==========================================================
229-
# 2- do it using github API
230-
if(len(to_be_warned_discussion_id)!=len(to_be_warned_discussion_author)):
231-
print('Error: size of both vectors number/author for discussions to be warned is different')
232-
exit(1)
233-
if(len(to_be_closed_discussion_id)!=len(to_be_closed_discussion_author)):
234-
print('Error: size of both vectors number/author for discussions to be closed is different')
235-
exit(1)
236-
237-
print("** Output lists **")
238-
print("******************")
239-
print("Nb discussions to be WARNED = "+str(len(to_be_warned_discussion_number)))
240-
print("Nb discussions to be CLOSED = "+str(len(to_be_closed_discussion_number)))
241-
print("******************")
242-
print("to_be_warned_discussion_number = "+str(to_be_warned_discussion_number))
243-
print("to_be_warned_discussion_id = "+str(to_be_warned_discussion_id))
244-
print("to_be_warned_discussion_author = "+str(to_be_warned_discussion_author))
245-
print("******************")
246-
print("to_be_closed_discussion_number = "+str(to_be_closed_discussion_number))
247-
print("to_be_closed_discussion_id = "+str(to_be_closed_discussion_id))
248-
print("to_be_closed_discussion_author = "+str(to_be_closed_discussion_author))
249-
print("******************")
250-
print("******************")
251-
252-
#==========================================================
253-
# WARNING step
254-
print("** WARNING step **")
255-
256-
for index, discussion_id in enumerate(to_be_warned_discussion_id):
257-
print("to_be_warned_discussion_number[index] = "+str(to_be_warned_discussion_number[index]))
258-
print("to_be_warned_discussion_author[index] = "+str(to_be_warned_discussion_author[index]))
259-
print("discussion_id = "+str(discussion_id))
260-
# Warning comment
261-
data = client.execute(
262-
query = make_github_warning_comment( discussion_id, to_be_warned_discussion_author[index] ),
263-
headers = {"Authorization": "Bearer {}".format(github_token)},
264-
)
265-
print(data)
266-
print("******************")
267-
print("******************")
268-
269-
#==========================================================
270-
# CLOSING step
271-
print("** CLOSING step **")
272-
273-
for index, discussion_id in enumerate(to_be_closed_discussion_id):
274-
print("to_be_closed_discussion_number[index] = "+str(to_be_closed_discussion_number[index]))
275-
print("to_be_closed_discussion_author[index] = "+str(to_be_closed_discussion_author[index]))
276-
print("discussion_id = "+str(discussion_id))
277-
278-
continue # dummy
279-
# Closing comment
280-
data = client.execute(
281-
query = make_github_closing_comment( discussion_id, to_be_closed_discussion_author[index] ),
282-
headers = {"Authorization": "Bearer {}".format(github_token)},
283-
)
284-
print(data)
285-
286-
# Close discussion
287-
data = client.execute(
288-
query = close_github_discussion( discussion_id ),
289-
headers = {"Authorization": "Bearer {}".format(github_token)},
290-
)
291-
print(data)
292-
293-
#==========================================================
68+
# Close and lock after 14 days of being stale
69+
if STALE_LABEL in labels and stale_comment:
70+
stale_time = datetime.strptime(stale_comment['created_at'], "%Y-%m-%dT%H:%M:%SZ")
71+
if (now - stale_time).days >= 14:
72+
# Check for any comments after stale comment
73+
recent_comments = [c for c in comments if datetime.strptime(c['created_at'], "%Y-%m-%dT%H:%M:%SZ") > stale_time]
74+
if not recent_comments:
75+
close_and_lock(number)
76+
77+
if __name__ == "__main__":
78+
main()

0 commit comments

Comments
 (0)