Skip to content

Commit 88dfdd4

Browse files
committed
Use per-message profiles for webhooks
1 parent b1e525c commit 88dfdd4

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

linearbot/api/types.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,14 @@ class Reaction(SerializableAttrs, LinearEventData):
251251

252252
class AttachmentSourceType(SerializableEnum):
253253
API = "api"
254+
GITHUB = "github"
254255

255256

256257
@dataclass(kw_only=True)
257258
class AttachmentSource(SerializableAttrs):
258259
type: AttachmentSourceType
259-
image_url: str = field(json="imageUrl")
260+
image_url: Optional[str] = field(json="imageUrl", default=None)
261+
pull_request_id: Optional[str] = field(json="pullRequestId", default=None)
260262

261263

262264
@dataclass(kw_only=True)
@@ -277,6 +279,14 @@ class UpdatedFrom(SerializableAttrs):
277279
updated_at: LinearDateTime = field(json="updatedAt")
278280

279281

282+
@dataclass
283+
class LinearActor(SerializableAttrs):
284+
id: UUID
285+
type: str = "user" # or integration
286+
email: Optional[str] = None
287+
name: Optional[str] = None
288+
avatar_url: str = field(json="avatarUrl", default="")
289+
280290
LinearEventContent = Union[Issue, Comment, Reaction, LabelEvent, Attachment]
281291
type_to_class = {
282292
LinearEventType.ISSUE: Issue,
@@ -294,7 +304,9 @@ class LinearEvent(SerializableAttrs):
294304
created_at: LinearDateTime = field(json="createdAt")
295305
type: LinearEventType
296306
data: LinearEventContent
307+
actor: Optional[LinearActor] = None
297308
url: Optional[str] = None
309+
organization_id: Optional[UUID] = field(json="organizationId", default=None)
298310

299311
@classmethod
300312
def deserialize(cls, data: JSON) -> 'LinearEvent':

linearbot/avatar_manager.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import TYPE_CHECKING
2+
import asyncio
3+
4+
from sqlalchemy import MetaData, Table, Column, Text
5+
from sqlalchemy.engine.base import Engine
6+
7+
from mautrix.types import ContentURI
8+
9+
if TYPE_CHECKING:
10+
from .bot import LinearBot
11+
12+
13+
class AvatarManager:
14+
bot: 'LinearBot'
15+
_avatars: dict[str, ContentURI]
16+
_table: Table
17+
_db: Engine
18+
_lock: asyncio.Lock
19+
20+
def __init__(self, bot: 'LinearBot', metadata: MetaData) -> None:
21+
self.bot = bot
22+
self._db = bot.database
23+
self._table = Table("avatar", metadata,
24+
Column("url", Text, primary_key=True),
25+
Column("mxc", Text, nullable=False))
26+
self._lock = asyncio.Lock()
27+
self._avatars = {}
28+
29+
def load_db(self) -> None:
30+
self._avatars = {url: ContentURI(mxc)
31+
for url, mxc
32+
in self._db.execute(self._table.select())}
33+
34+
async def get_mxc(self, url: str) -> ContentURI:
35+
try:
36+
return self._avatars[url]
37+
except KeyError:
38+
pass
39+
async with self.bot.http.get(url) as resp:
40+
resp.raise_for_status()
41+
data = await resp.read()
42+
async with self._lock:
43+
try:
44+
return self._avatars[url]
45+
except KeyError:
46+
pass
47+
mxc = await self.bot.client.upload_media(data)
48+
self._avatars[url] = mxc
49+
with self._db.begin() as conn:
50+
conn.execute(self._table.insert().values(url=url, mxc=mxc))
51+
return mxc

linearbot/bot.py

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .commands import LinearCommands
1717
from .client_manager import ClientManager
1818
from .label_manager import LabelManager
19+
from .avatar_manager import AvatarManager
1920
from .util.gitlab import GitLabMigrator
2021
from .util.prefixless_dm import DMCommandHandler
2122

@@ -54,6 +55,7 @@ class LinearBot(Plugin):
5455
linear_webhook: LinearWebhook
5556
clients: ClientManager
5657
labels: LabelManager
58+
avatars: AvatarManager
5759
linear_bot: LinearClient
5860
commands: LinearCommands
5961
migrator: GitLabMigrator
@@ -67,13 +69,15 @@ async def start(self):
6769
self.commands = LinearCommands(self)
6870
self.clients = ClientManager(self, db_metadata)
6971
self.labels = LabelManager(self, db_metadata)
72+
self.avatars = AvatarManager(self, db_metadata)
7073
self.migrator = GitLabMigrator(self)
7174
self.prefixless_dm = DMCommandHandler(self.commands)
7275

7376
self.on_external_config_update()
7477
db_metadata.create_all(self.database)
7578
self.clients.load_db()
7679
self.labels.load_db()
80+
self.avatars.load_db()
7781

7882
self.register_handler_class(self.linear_webhook)
7983
self.register_handler_class(self.commands)

linearbot/webhook.py

+12
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ def abort() -> None:
9696
"action": evt.action.value,
9797
"data": await evt.data.get_meta(client=self.bot.linear_bot),
9898
}
99+
if evt.actor and evt.actor.name:
100+
mxc = ""
101+
if evt.actor.avatar_url:
102+
try:
103+
mxc = await self.bot.avatars.get_mxc(evt.actor.avatar_url)
104+
except Exception:
105+
self.log.warning("Failed to get avatar URL", exc_info=True)
106+
content["com.beeper.per_message_profile"] = {
107+
"id": str(evt.actor.id),
108+
"displayname": evt.actor.name,
109+
"avatar_url": mxc,
110+
}
99111
content["com.beeper.linkpreviews"] = []
100112
if evt.url:
101113
content.external_url = evt.url

0 commit comments

Comments
 (0)