Skip to content

Commit 16890e7

Browse files
Refactor mention decorator from gatekeeper to enrichment pattern
1 parent bf80ebc commit 16890e7

File tree

5 files changed

+402
-301
lines changed

5 files changed

+402
-301
lines changed

src/django_github_app/mentions.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

33
import re
4+
from dataclasses import dataclass
45
from enum import Enum
56
from typing import NamedTuple
67

78
from gidgethub import sansio
89

10+
from .permissions import Permission
11+
912

1013
class EventAction(NamedTuple):
1114
event: str
@@ -43,6 +46,13 @@ def all_events(cls) -> list[EventAction]:
4346
)
4447

4548

49+
@dataclass
50+
class MentionContext:
51+
commands: list[str]
52+
user_permission: Permission | None
53+
scope: MentionScope | None
54+
55+
4656
class MentionMatch(NamedTuple):
4757
mention: str
4858
command: str | None
@@ -53,7 +63,9 @@ class MentionMatch(NamedTuple):
5363
QUOTE_PATTERN = re.compile(r"^\s*>.*$", re.MULTILINE)
5464

5565

56-
def parse_mentions(text: str, username: str) -> list[MentionMatch]:
66+
def parse_mentions(event: sansio.Event, username: str) -> list[MentionMatch]:
67+
text = event.data.get("comment", {}).get("body", "")
68+
5769
if not text:
5870
return []
5971

@@ -77,11 +89,15 @@ def parse_mentions(text: str, username: str) -> list[MentionMatch]:
7789
return mentions
7890

7991

92+
def get_commands(event: sansio.Event, username: str) -> list[str]:
93+
mentions = parse_mentions(event, username)
94+
return [m.command for m in mentions if m.command]
95+
96+
8097
def check_event_for_mention(
8198
event: sansio.Event, command: str | None, username: str
8299
) -> bool:
83-
comment = event.data.get("comment", {}).get("body", "")
84-
mentions = parse_mentions(comment, username)
100+
mentions = parse_mentions(event, username)
85101

86102
if not mentions:
87103
return False
@@ -92,21 +108,15 @@ def check_event_for_mention(
92108
return any(mention.command == command.lower() for mention in mentions)
93109

94110

95-
def check_event_scope(event: sansio.Event, scope: MentionScope | None) -> bool:
96-
if scope is None:
97-
return True
98-
99-
# For issue_comment events, we need to distinguish between issues and PRs
111+
def get_event_scope(event: sansio.Event) -> MentionScope | None:
100112
if event.event == "issue_comment":
101113
issue = event.data.get("issue", {})
102114
is_pull_request = "pull_request" in issue and issue["pull_request"] is not None
115+
return MentionScope.PR if is_pull_request else MentionScope.ISSUE
103116

104-
# If scope is ISSUE, we only want actual issues (not PRs)
105-
if scope == MentionScope.ISSUE:
106-
return not is_pull_request
107-
# If scope is PR, we only want pull requests
108-
elif scope == MentionScope.PR:
109-
return is_pull_request
117+
for scope in MentionScope:
118+
scope_events = scope.get_events()
119+
if any(event_action.event == event.event for event_action in scope_events):
120+
return scope
110121

111-
scope_events = scope.get_events()
112-
return any(event_action.event == event.event for event_action in scope_events)
122+
return None

src/django_github_app/permissions.py

Lines changed: 23 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -133,81 +133,47 @@ def from_event(cls, event: sansio.Event) -> EventInfo:
133133

134134
class PermissionCheck(NamedTuple):
135135
has_permission: bool
136-
error_message: str | None
137136

138137

139-
PERMISSION_CHECK_ERROR_MESSAGE = """
140-
❌ **Permission Denied**
138+
async def aget_user_permission_from_event(
139+
event: sansio.Event, gh: AsyncGitHubAPI
140+
) -> Permission | None:
141+
comment_author, owner, repo = EventInfo.from_event(event)
141142

142-
@{comment_author}, you need at least **{required_permission}** permission to use this command.
143+
if not (comment_author and owner and repo):
144+
return None
143145

144-
Your current permission level: **{user_permission}**
145-
"""
146+
return await aget_user_permission(gh, owner, repo, comment_author)
146147

147148

148149
async def acheck_mention_permission(
149150
event: sansio.Event, gh: AsyncGitHubAPI, required_permission: Permission
150151
) -> PermissionCheck:
151-
comment_author, owner, repo = EventInfo.from_event(event)
152-
153-
if not (comment_author and owner and repo):
154-
return PermissionCheck(has_permission=False, error_message=None)
155-
156-
user_permission = await aget_user_permission(gh, owner, repo, comment_author)
152+
user_permission = await aget_user_permission_from_event(event, gh)
157153

158-
if user_permission >= required_permission:
159-
return PermissionCheck(has_permission=True, error_message=None)
154+
if user_permission is None:
155+
return PermissionCheck(has_permission=False)
160156

161-
return PermissionCheck(
162-
has_permission=False,
163-
error_message=PERMISSION_CHECK_ERROR_MESSAGE.format(
164-
comment_author=comment_author,
165-
required_permission=required_permission.name.lower(),
166-
user_permission=user_permission.name.lower(),
167-
),
168-
)
157+
return PermissionCheck(has_permission=user_permission >= required_permission)
169158

170159

171-
def check_mention_permission(
172-
event: sansio.Event, gh: SyncGitHubAPI, required_permission: Permission
173-
) -> PermissionCheck:
160+
def get_user_permission_from_event(
161+
event: sansio.Event, gh: SyncGitHubAPI
162+
) -> Permission | None:
174163
comment_author, owner, repo = EventInfo.from_event(event)
175164

176165
if not (comment_author and owner and repo):
177-
return PermissionCheck(has_permission=False, error_message=None)
178-
179-
user_permission = get_user_permission(gh, owner, repo, comment_author)
180-
181-
if user_permission >= required_permission:
182-
return PermissionCheck(has_permission=True, error_message=None)
183-
184-
return PermissionCheck(
185-
has_permission=False,
186-
error_message=PERMISSION_CHECK_ERROR_MESSAGE.format(
187-
comment_author=comment_author,
188-
required_permission=required_permission.name.lower(),
189-
user_permission=user_permission.name.lower(),
190-
),
191-
)
166+
return None
192167

168+
return get_user_permission(gh, owner, repo, comment_author)
193169

194-
def get_comment_post_url(event: sansio.Event) -> str | None:
195-
if event.data.get("action") != "created":
196-
return None
197170

198-
_, owner, repo = EventInfo.from_event(event)
171+
def check_mention_permission(
172+
event: sansio.Event, gh: SyncGitHubAPI, required_permission: Permission
173+
) -> PermissionCheck:
174+
user_permission = get_user_permission_from_event(event, gh)
199175

200-
if not (owner and repo):
201-
return None
176+
if user_permission is None:
177+
return PermissionCheck(has_permission=False)
202178

203-
if "issue" in event.data:
204-
issue_number = event.data["issue"]["number"]
205-
return f"/repos/{owner}/{repo}/issues/{issue_number}/comments"
206-
elif "pull_request" in event.data:
207-
pr_number = event.data["pull_request"]["number"]
208-
return f"/repos/{owner}/{repo}/issues/{pr_number}/comments"
209-
elif "commit_sha" in event.data:
210-
commit_sha = event.data["commit_sha"]
211-
return f"/repos/{owner}/{repo}/commits/{commit_sha}/comments"
212-
213-
return None
179+
return PermissionCheck(has_permission=user_permission >= required_permission)

src/django_github_app/routing.py

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
from ._typing import override
1717
from .github import AsyncGitHubAPI
1818
from .github import SyncGitHubAPI
19+
from .mentions import MentionContext
1920
from .mentions import MentionScope
2021
from .mentions import check_event_for_mention
21-
from .mentions import check_event_scope
22-
from .permissions import Permission
23-
from .permissions import acheck_mention_permission
24-
from .permissions import check_mention_permission
25-
from .permissions import get_comment_post_url
22+
from .mentions import get_commands
23+
from .mentions import get_event_scope
24+
from .permissions import aget_user_permission_from_event
25+
from .permissions import get_user_permission_from_event
2626

2727
AsyncCallback = Callable[..., Awaitable[None]]
2828
SyncCallback = Callable[..., None]
@@ -90,26 +90,15 @@ async def async_wrapper(
9090
if not check_event_for_mention(event, command, username):
9191
return
9292

93-
if not check_event_scope(event, scope):
93+
event_scope = get_event_scope(event)
94+
if scope is not None and event_scope != scope:
9495
return
9596

96-
# Check permissions if required
97-
if permission is not None:
98-
required_perm = Permission.from_string(permission)
99-
permission_check = await acheck_mention_permission(
100-
event, gh, required_perm
101-
)
102-
103-
if not permission_check.has_permission:
104-
# Post error comment if we have an error message
105-
if permission_check.error_message:
106-
comment_url = get_comment_post_url(event)
107-
if comment_url:
108-
await gh.post(
109-
comment_url,
110-
data={"body": permission_check.error_message},
111-
)
112-
return
97+
kwargs["mention"] = MentionContext(
98+
commands=get_commands(event, username),
99+
user_permission=await aget_user_permission_from_event(event, gh),
100+
scope=event_scope,
101+
)
113102

114103
await func(event, gh, *args, **kwargs) # type: ignore[func-returns-value]
115104

@@ -123,26 +112,15 @@ def sync_wrapper(
123112
if not check_event_for_mention(event, command, username):
124113
return
125114

126-
if not check_event_scope(event, scope):
115+
event_scope = get_event_scope(event)
116+
if scope is not None and event_scope != scope:
127117
return
128118

129-
# Check permissions if required
130-
if permission is not None:
131-
required_perm = Permission.from_string(permission)
132-
permission_check = check_mention_permission(
133-
event, gh, required_perm
134-
)
135-
136-
if not permission_check.has_permission:
137-
# Post error comment if we have an error message
138-
if permission_check.error_message:
139-
comment_url = get_comment_post_url(event)
140-
if comment_url:
141-
gh.post( # type: ignore[unused-coroutine]
142-
comment_url,
143-
data={"body": permission_check.error_message},
144-
)
145-
return
119+
kwargs["mention"] = MentionContext(
120+
commands=get_commands(event, username),
121+
user_permission=get_user_permission_from_event(event, gh),
122+
scope=event_scope,
123+
)
146124

147125
func(event, gh, *args, **kwargs)
148126

0 commit comments

Comments
 (0)