Skip to content

Commit 400c9fa

Browse files
authored
Post updates about releases to a different room (#3)
1 parent efc8019 commit 400c9fa

File tree

6 files changed

+64
-14
lines changed

6 files changed

+64
-14
lines changed

base-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ linear:
1111
# IDs of Linear organizations that are allowed to use the bot.
1212
# Empty list disables the whitelist.
1313
allowed_organizations: []
14+
# Label IDs for issues that are releases and should be shared to the release room
15+
release_label_ids: []
1416

1517
# Settings for reading data from GitLab for migration purposes
1618
gitlab:

linearbot/api/client.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from yarl import URL
66

7-
from .types import Issue, User, IssueMeta, IssueSummary, IssueCreateResponse, Label
8-
from .queries import (get_user_details, get_user, get_issue, get_issue_details, get_labels,
7+
from .types import Issue, User, IssueMeta, IssueSummary, IssueLabels, IssueCreateResponse, Label
8+
from .queries import (get_user_details, get_user, get_issue, get_issue_details, get_issue_labels, get_labels,
99
create_issue, create_comment, create_reaction, create_label, update_label)
1010

1111
if TYPE_CHECKING:
@@ -158,6 +158,11 @@ async def get_issue_details(self, issue_identifier: str) -> IssueSummary:
158158
issue = IssueSummary.deserialize(resp["issue"])
159159
return issue
160160

161+
async def get_issue_labels(self, issue_id: UUID) -> List[UUID]:
162+
resp = await self.request(get_issue_labels, variables={"issueID": str(issue_id)})
163+
issue = IssueLabels.deserialize(resp["issue"])
164+
return issue.label_ids
165+
161166
async def get_all_labels(self) -> Dict[UUID, Dict[str, Label]]:
162167
teams = {}
163168
has_next_page = True
@@ -166,7 +171,11 @@ async def get_all_labels(self) -> Dict[UUID, Dict[str, Label]]:
166171
resp = await self.request(get_labels, variables={"cursor": cursor})
167172
for raw_label in resp["issueLabels"]["nodes"]:
168173
label = Label.deserialize(raw_label)
169-
teams.setdefault(label.team.id, {})[label.name] = label
174+
team_id = getattr(label.team, 'id', None)
175+
if team_id is not None:
176+
teams.setdefault(team_id, {})[label.name] = label
177+
else:
178+
self.log.warning(f"Label {label.name} has no team ID")
170179
has_next_page = resp["issueLabels"]["pageInfo"]["hasNextPage"]
171180
cursor = resp["issueLabels"]["pageInfo"]["endCursor"]
172181
return teams

linearbot/api/queries.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@
7171
}
7272
}"""
7373

74+
# language=graphql
75+
get_issue_labels = """query GetIssueLabels($issueID: String!) {
76+
issue(id: $issueID) {
77+
id
78+
title
79+
labelIds
80+
}
81+
}"""
82+
7483
# language=graphql
7584
create_issue = """mutation CreateIssue($input: IssueCreateInput!) {
7685
issueCreate(input: $input) {

linearbot/api/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,10 @@ class IssueSummary(IssueMeta):
178178
project: Optional[MinimalProject] = None
179179
state: Optional[IssueState] = None
180180

181+
@dataclass
182+
class IssueLabels(MinimalIssue):
183+
label_ids: Optional[List[UUID]] = field(json="labelIds", default=None)
184+
181185
@dataclass(kw_only=True)
182186
class Issue(MinimalIssue, SerializableAttrs, LinearEventData):
183187
number: int

linearbot/bot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def do_update(self, helper: ConfigUpdateHelper) -> None:
3333
helper.copy("linear.client_id")
3434
helper.copy("linear.client_secret")
3535
helper.copy("linear.allowed_organizations")
36+
helper.copy("linear.release_label_ids")
3637

3738
helper.copy("gitlab.url")
3839
helper.copy("gitlab.token")
@@ -98,6 +99,7 @@ def get_config_class(cls) -> Type[BaseProxyConfig]:
9899
def on_external_config_update(self) -> None:
99100
self.config.load_and_update()
100101
self.linear_webhook.secret = self.config["linear.webhook_secret"]
102+
self.linear_webhook.release_label_ids = {UUID(id) for id in self.config["linear.release_label_ids"]}
101103
self.linear_bot.authorization = self.config["linear.token"]
102104
self.oauth_client_id = self.config["linear.client_id"]
103105
self.oauth_client_secret = self.config["linear.client_secret"]

linearbot/webhook.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Set, List, TYPE_CHECKING
1+
from typing import Set, List, Optional, TYPE_CHECKING
22
from uuid import UUID
33
import asyncio
44
import json
@@ -21,7 +21,6 @@
2121
spaces = re.compile(" +")
2222
space = " "
2323

24-
2524
class LinearWebhook:
2625
bot: 'LinearBot'
2726
secret: str
@@ -30,6 +29,8 @@ class LinearWebhook:
3029
handled_webhooks: Set[UUID]
3130
ignore_uuids: Set[UUID]
3231
messages: TemplateManager
32+
release_label_ids: Set[UUID]
33+
3334
# templates: TemplateManager
3435

3536
def __init__(self, bot: 'LinearBot') -> None:
@@ -39,6 +40,7 @@ def __init__(self, bot: 'LinearBot') -> None:
3940
self.joined_rooms = set()
4041
self.handled_webhooks = set()
4142
self.ignore_uuids = set()
43+
self.release_label_ids = set()
4244

4345
self.messages = TemplateManager(self.bot.loader, "templates/messages")
4446
# self.templates = TemplateManager(self.bot.loader, "templates/mixins")
@@ -51,7 +53,8 @@ async def stop(self) -> None:
5153
if self.task_list:
5254
await asyncio.wait(self.task_list, timeout=1)
5355

54-
async def handle_webhook(self, room_id: RoomID, evt: LinearEvent) -> None:
56+
async def handle_webhook(self, room_id: Optional[RoomID], release_room_id: Optional[RoomID],
57+
evt: LinearEvent) -> None:
5558
if evt.data.id in self.ignore_uuids:
5659
self.log.debug(f"Dropping webhook for {evt.type} {evt.data.id}"
5760
" that was marked to be ignored")
@@ -98,12 +101,22 @@ def abort() -> None:
98101
content.external_url = evt.url
99102
query = {"ts": int(evt.created_at.timestamp() * 1000)}
100103

101-
await self.bot.client.send_message(room_id, content, query_params=query)
104+
if room_id is not None:
105+
await self.bot.client.send_message(room_id, content, query_params=query)
106+
107+
# Only post in the release room if the issue has one of the release labels
108+
if release_room_id is not None:
109+
issue_id = getattr(evt.data, 'issue_id', None)
110+
if issue_id is not None:
111+
label_ids = await self.bot.linear_bot.get_issue_labels(issue_id)
112+
if set(label_ids) & self.release_label_ids:
113+
await self.bot.client.send_message(release_room_id, content, query_params=query)
102114

103-
async def _try_handle_webhook(self, delivery_id: UUID, room_id: RoomID, evt: LinearEvent
115+
async def _try_handle_webhook(self, delivery_id: UUID, room_id: Optional[RoomID], release_room_id: Optional[RoomID],
116+
evt: LinearEvent
104117
) -> None:
105118
try:
106-
await self.handle_webhook(room_id, evt)
119+
await self.handle_webhook(room_id, release_room_id, evt)
107120
except Exception:
108121
self.log.exception(f"Error handling webhook {delivery_id}")
109122
finally:
@@ -123,12 +136,23 @@ async def webhook(self, request: Request) -> Response:
123136
try:
124137
room_id = RoomID(request.url.query["room_id"])
125138
except KeyError:
126-
return Response(status=400, text="400: Bad Request\n"
127-
"Missing `room_id` query parameter\n")
128-
if room_id not in self.joined_rooms:
129-
return Response(text="403: Forbidden\nThe bot is not in the room. "
139+
room_id = None
140+
141+
if room_id is not None and room_id not in self.joined_rooms:
142+
return Response(text=f"403: Forbidden\nThe bot is not in the room {room_id}. "
143+
f"Please invite {self.bot.client.mxid} to the room.\n",
144+
status=403)
145+
146+
try:
147+
release_room_id = RoomID(request.url.query["release_room_id"])
148+
except KeyError:
149+
release_room_id = None
150+
151+
if release_room_id is not None and release_room_id not in self.joined_rooms:
152+
return Response(text=f"403: Forbidden\nThe bot is not in the room {release_room_id}. "
130153
f"Please invite {self.bot.client.mxid} to the room.\n",
131154
status=403)
155+
132156
try:
133157
delivery_id = UUID(request.headers["Linear-Delivery"])
134158
except (KeyError, ValueError):
@@ -161,7 +185,7 @@ async def webhook(self, request: Request) -> Response:
161185
self.log.debug("Recognized data in %s: %s", delivery_id, evt)
162186
except AttributeError:
163187
self.log.trace("Received event %s: %s", delivery_id, evt)
164-
task = asyncio.create_task(self._try_handle_webhook(delivery_id, room_id, evt))
188+
task = asyncio.create_task(self._try_handle_webhook(delivery_id, room_id, release_room_id, evt))
165189
self.task_list.append(task)
166190
return Response(status=202, text="202: Accepted\nWebhook processing started.\n")
167191

0 commit comments

Comments
 (0)