22from datetime import datetime , timedelta , timezone
33from pathlib import Path
44from typing import Dict , List , Optional , Set
5+ from typing_extensions import Literal
56
67from github import Github
8+ from github .PaginatedList import PaginatedList
9+ from github .IssueComment import IssueComment
710from github .Issue import Issue
811from github .IssueEvent import IssueEvent
912from pydantic import BaseModel , SecretStr , validator
1013from pydantic_settings import BaseSettings
1114
15+ REMINDER_MARKER = "<!-- reminder -->"
16+
17+
18+ class Reminder (BaseModel ):
19+ message : str = (
20+ "This will be closed automatically soon if there's no further activity."
21+ )
22+ before : timedelta = timedelta (days = 1 )
23+
1224
1325class KeywordMeta (BaseModel ):
1426 delay : timedelta = timedelta (days = 10 )
@@ -17,6 +29,7 @@ class KeywordMeta(BaseModel):
1729 )
1830 remove_label_on_comment : bool = True
1931 remove_label_on_close : bool = False
32+ reminder : Optional [Reminder ] = None
2033
2134
2235class Settings (BaseSettings ):
@@ -42,9 +55,26 @@ class PartialGitHubEvent(BaseModel):
4255 pull_request : Optional [PartialGitHubEventIssue ] = None
4356
4457
58+ def filter_comments (
59+ comments : PaginatedList [IssueComment ], include : Literal ["regular" , "reminder" ]
60+ ) -> list [IssueComment ]:
61+ if include == "regular" :
62+ return [
63+ comment
64+ for comment in comments
65+ if not comment .body .startswith (REMINDER_MARKER )
66+ ]
67+ elif include == "reminder" :
68+ return [
69+ comment for comment in comments if comment .body .startswith (REMINDER_MARKER )
70+ ]
71+ else :
72+ raise ValueError (f"Unsupported value of include ({ include } )" )
73+
74+
4575def get_last_interaction_date (issue : Issue ) -> Optional [datetime ]:
4676 last_date : Optional [datetime ] = None
47- comments = list (issue .get_comments ())
77+ comments = filter_comments (issue .get_comments (), include = "regular" )
4878 if issue .pull_request :
4979 pr = issue .as_pull_request ()
5080 commits = list (pr .get_commits ())
@@ -87,6 +117,19 @@ def get_last_event_for_label(
87117 return last_event
88118
89119
120+ def get_last_reminder_date (issue : Issue ) -> Optional [datetime ]:
121+ """Get date of last reminder message was sent"""
122+ last_date : Optional [datetime ] = None
123+ comments = filter_comments (issue .get_comments (), include = "reminder" )
124+ comment_dates = [comment .created_at for comment in comments ]
125+ for item_date in comment_dates :
126+ if not last_date :
127+ last_date = item_date
128+ elif item_date > last_date :
129+ last_date = item_date
130+ return last_date
131+
132+
90133def close_issue (
91134 * , issue : Issue , keyword_meta : KeywordMeta , keyword : str , label_strs : Set [str ]
92135) -> None :
@@ -106,6 +149,7 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
106149 events = list (issue .get_events ())
107150 labeled_events = get_labeled_events (events )
108151 last_date = get_last_interaction_date (issue )
152+ last_reminder_date = get_last_reminder_date (issue )
109153 now = datetime .now (timezone .utc )
110154 for keyword , keyword_meta in settings .input_config .items ():
111155 # Check closable delay, if enough time passed and the issue could be closed
@@ -116,6 +160,19 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
116160 keyword_event = get_last_event_for_label (
117161 labeled_events = labeled_events , label = keyword
118162 )
163+ # Check if we need to send a reminder
164+ need_send_reminder = False
165+ if keyword_meta .reminder and keyword_event :
166+ scheduled_close_date = keyword_event .created_at + keyword_meta .delay
167+ remind_time = ( # Time point after which we should send reminder
168+ scheduled_close_date - keyword_meta .reminder .before
169+ )
170+ need_send_reminder = (
171+ (now > remind_time ) # It's time to send reminder
172+ and ( # .. and it hasn't been sent yet
173+ not last_reminder_date or (last_reminder_date < remind_time )
174+ )
175+ )
119176 if last_date and keyword_event and last_date > keyword_event .created_at :
120177 logging .info (
121178 f"Not closing as the last comment was written after adding the "
@@ -124,6 +181,11 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
124181 if keyword_meta .remove_label_on_comment :
125182 logging .info (f'Removing label: "{ keyword } "' )
126183 issue .remove_from_labels (keyword )
184+ elif need_send_reminder and keyword_meta .reminder :
185+ message = keyword_meta .reminder .message
186+ logging .info (f"Sending reminder: #{ issue .number } with message: { message } " )
187+ issue .create_comment (f"{ REMINDER_MARKER } \n { message } " )
188+ break
127189 elif closable_delay :
128190 close_issue (
129191 issue = issue ,
0 commit comments