Skip to content

Commit 1972b24

Browse files
authoredJan 6, 2023
feat: add image support (#11)
* feat: add image support * feat: integrate image feature into push route * fix: set initial thumbnail size in bot.py
1 parent 7607108 commit 1972b24

File tree

4 files changed

+115
-29
lines changed

4 files changed

+115
-29
lines changed
 

‎hasswebhook/bot.py

+38-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import asyncio
2+
import json
3+
from datetime import datetime, timedelta
4+
from typing import Type
25

36
import pytz
7+
from aiohttp.web import Request, Response
48
from maubot import Plugin, MessageEvent
59
from maubot.handlers import command, web
610
from mautrix.types import TextMessageEventContent, Format, MessageType
711
from mautrix.util import markdown
812

9-
from typing import Type
1013
from .config import Config
11-
from aiohttp.web import Request, Response
12-
from .roomposter import RoomPoster, RoomPosterType
13-
from .setupinstructions import HassWebhookSetupInstructions
1414
from .db import LifetimeDatabase, LifetimeEnd
15-
from datetime import datetime, timedelta
15+
from .roomposter import RoomPoster, RoomPosterType, Image
16+
from .setupinstructions import HassWebhookSetupInstructions
1617

1718

1819
class HassWebhook(Plugin):
@@ -93,34 +94,61 @@ async def setup_instructions(self, evt: MessageEvent) -> None:
9394
@web.post("/push/{room_id}")
9495
async def post_data(self, req: Request) -> Response:
9596
room_id: str = req.match_info["room_id"]
96-
self.log.info(await req.text())
97+
self.log.info(f"Request for room {room_id} data: {await req.text()}")
98+
9799
req_dict = await req.json()
98100
self.log.debug(req_dict)
101+
99102
message: str = req_dict.get(self.get_message_key())
100103
rp_type: RoomPosterType = RoomPosterType.get_type_from_str(req_dict.get("type", "message"))
101104
identifier: str = req_dict.get("identifier", "")
102105
callback_url: str = req_dict.get("callback_url", "")
106+
103107
lifetime: int = req_dict.get("lifetime", "")
104108
if lifetime == "" or int(lifetime) < 0:
105109
lifetime = -1
106110
else:
107111
lifetime = int(lifetime)
108-
self.log.info(f"Lifetime 1: {lifetime}")
112+
self.log.debug(f"Lifetime: {lifetime}")
113+
114+
# Image parameters
115+
content: str = req_dict.get("content")
116+
content_type: str = req_dict.get("contentType")
117+
name: str = req_dict.get("name")
118+
thumbnail_size: int = req_dict.get("thumbnailSize", 128)
119+
image = None
120+
self.log.info(content)
121+
if not content and rp_type == RoomPosterType.IMAGE:
122+
return Response(status=400, content_type="application/json", body=json.dumps(
123+
{"success": False,
124+
"error": "Type is set to image. Please pass at least the 'content' property (base64 image)"}))
125+
126+
if content and rp_type != RoomPosterType.IMAGE:
127+
rp_type = RoomPosterType.IMAGE
128+
129+
if rp_type == RoomPosterType.IMAGE:
130+
image = Image(content=content, content_type=content_type, name=name, thumbnail_size=thumbnail_size)
131+
self.log.info(f"Image content found: {content}")
132+
109133
room_poster: RoomPoster = RoomPoster(
110134
hasswebhook=self,
111135
message=message,
112136
identifier=identifier,
113137
rp_type=rp_type,
114138
room_id=room_id,
115139
callback_url=callback_url,
116-
lifetime=lifetime
140+
lifetime=lifetime,
141+
image=image,
117142
)
118143

119144
self.log.debug(f"Received data with ID {room_id}: {req_dict}")
120-
if await room_poster.post_to_room():
145+
146+
event_id = await room_poster.post_to_room()
147+
if rp_type == RoomPosterType.MESSAGE or rp_type == RoomPosterType.IMAGE:
148+
return Response(status=200, body=json.dumps({"event_id": event_id}), content_type="application/json")
149+
elif event_id:
121150
return Response(status=200)
122151
else:
123-
self.log.debug("I responded with 404")
124152
return Response(status=404)
125153

126154
@web.get("/health")

‎hasswebhook/db.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
from typing import Optional, Iterator, Dict, List
2-
from datetime import datetime
31
import logging
2+
from datetime import datetime
3+
from typing import Iterator
4+
45
import pytz
56
from attr import dataclass
6-
from sqlalchemy import (Column, String, Integer, Text, DateTime, ForeignKey, Table, MetaData,
7+
from mautrix.types import EventID, RoomID
8+
from sqlalchemy import (Column, String, Integer, DateTime, Table, MetaData,
79
select, and_)
810
from sqlalchemy.engine.base import Engine
911

10-
from mautrix.types import UserID, EventID, RoomID
11-
1212

1313
@dataclass
1414
class LifetimeEnd:
@@ -29,10 +29,10 @@ def __init__(self, db: Engine) -> None:
2929
meta.bind = db
3030

3131
self.lifetime_ends = Table("lifetime_ends", meta,
32-
Column("id", Integer, primary_key=True, autoincrement=True),
33-
Column("end_date", DateTime, nullable=False),
34-
Column("room_id", String(255), nullable=False),
35-
Column("event_id", String(255), nullable=False))
32+
Column("id", Integer, primary_key=True, autoincrement=True),
33+
Column("end_date", DateTime, nullable=False),
34+
Column("room_id", String(255), nullable=False),
35+
Column("event_id", String(255), nullable=False))
3636

3737
meta.create_all()
3838

@@ -47,7 +47,7 @@ def get_older_than(self, end_date: datetime) -> Iterator[LifetimeEnd]:
4747
rows = self.db.execute(select([self.lifetime_ends]).where(and_(*where)))
4848
for row in rows:
4949
yield LifetimeEnd(id=row[0], end_date=row[1].replace(tzinfo=pytz.UTC), room_id=row[2],
50-
event_id=row[3])
50+
event_id=row[3])
5151

5252
def remove(self, lifetime_end: LifetimeEnd) -> None:
5353
self.db.execute(self.lifetime_ends.delete().where(

‎hasswebhook/roomposter.py

+60-7
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1-
import logging
21
import re
2+
from base64 import b64decode
33
from datetime import datetime, timedelta
44
from enum import Enum
5+
from io import BytesIO
56
from typing import Optional
67

78
import pytz
9+
from PIL import Image as pil_image
810
from markdown import markdown
911
from maubot import Plugin
1012
from maubot.matrix import MaubotMessageEvent
13+
from mautrix.crypto.attachments import encrypt_attachment
1114
from mautrix.errors.request import MForbidden
1215
from mautrix.types import TextMessageEventContent, Format, MessageType, RoomID, PaginationDirection, \
13-
MessageEventContent, EventID
16+
MessageEventContent, EventID, MediaMessageEventContent, ImageInfo, EventType, ThumbnailInfo
1417

1518
from .db import LifetimeEnd
1619

1720

21+
class Image:
22+
content: str
23+
content_type: str
24+
name: str
25+
thumbnail_size: int
26+
27+
def __init__(self, content: str, content_type: str, name: str, thumbnail_size: int):
28+
self.content = content
29+
self.content_type = content_type
30+
self.name = name
31+
self.thumbnail_size = thumbnail_size
32+
33+
1834
class RoomPosterType(Enum):
1935
MESSAGE = 1
2036
EDIT = 2
2137
REDACTION = 3
2238
REACTION = 4
39+
IMAGE = 5
2340

2441
@classmethod
2542
def get_type_from_str(cls, mtype: str):
@@ -28,7 +45,8 @@ def get_type_from_str(cls, mtype: str):
2845
"message": RoomPosterType.MESSAGE,
2946
"redaction": RoomPosterType.REDACTION,
3047
"edit": RoomPosterType.EDIT,
31-
"reaction": RoomPosterType.REACTION
48+
"reaction": RoomPosterType.REACTION,
49+
"image": RoomPosterType.IMAGE
3250
}
3351
return type_switcher.get(mtype)
3452

@@ -43,22 +61,23 @@ class RoomPoster:
4361
lifetime: int
4462

4563
def __init__(self, hasswebhook: Plugin, identifier: str, rp_type: RoomPosterType, room_id: str,
46-
message="", callback_url="", lifetime=-1):
64+
image: Optional[Image] = None, message="", callback_url="", lifetime=-1):
4765
self.rp_type = rp_type
4866
self.room_id = RoomID(room_id)
4967
self.hasswebhook = hasswebhook
5068
self.identifier = identifier
5169
self.callback_url = callback_url
5270
self.message = message
5371
self.lifetime = lifetime
72+
self.image = image
5473

5574
# Send a POST as a callback containing the event_id of the sent message
5675
async def callback(self, event_id: str) -> None:
5776
if self.callback_url:
5877
await self.hasswebhook.http.post(self.callback_url, json={'event_id': event_id})
5978

6079
# Switch for each RoomPosterType
61-
async def post_to_room(self) -> bool:
80+
async def post_to_room(self):
6281
if self.rp_type == RoomPosterType.MESSAGE:
6382
return await self.post_message()
6483
if self.rp_type == RoomPosterType.REDACTION:
@@ -67,10 +86,44 @@ async def post_to_room(self) -> bool:
6786
return await self.post_edit()
6887
if self.rp_type == RoomPosterType.REACTION:
6988
return await self.post_reaction()
89+
if self.rp_type == RoomPosterType.IMAGE:
90+
return await self.post_image()
7091
return False
7192

93+
async def post_image(self) -> str:
94+
media_event = MediaMessageEventContent(body=self.image.name, msgtype=MessageType.IMAGE)
95+
96+
upload_mime = "application/octet-stream"
97+
bytes_image = b64decode(self.image.content)
98+
encrypted_image, file = encrypt_attachment(bytes_image)
99+
file.url = await self.hasswebhook.client.upload_media(encrypted_image, mime_type=upload_mime)
100+
media_event.file = file
101+
102+
img = pil_image.open(BytesIO(bytes_image))
103+
image_info = ImageInfo(mimetype=self.image.content_type, height=img.height, width=img.width)
104+
media_event.info = image_info
105+
106+
byt_arr_tn = BytesIO()
107+
img.thumbnail((self.image.thumbnail_size, self.image.thumbnail_size), pil_image.ANTIALIAS)
108+
img.save(byt_arr_tn, format='PNG')
109+
110+
tn_img = pil_image.open(byt_arr_tn)
111+
enc_tn, tn_file = encrypt_attachment(byt_arr_tn.getvalue())
112+
113+
image_info.thumbnail_info = ThumbnailInfo(mimetype="image/png", height=tn_img.height, width=tn_img.width)
114+
tn_file.url = await self.hasswebhook.client.upload_media(enc_tn, mime_type=upload_mime)
115+
image_info.thumbnail_file = tn_file
116+
117+
img.close()
118+
tn_img.close()
119+
120+
event_id = await self.hasswebhook.client.send_message_event(self.room_id, event_type=EventType.ROOM_MESSAGE,
121+
content=media_event)
122+
await self.callback(event_id)
123+
return event_id
124+
72125
# Send message to room
73-
async def post_message(self) -> bool:
126+
async def post_message(self):
74127
body = "{message} by {identifier}".format(
75128
message=self.message, identifier=self.identifier) if self.identifier else self.message
76129
content = TextMessageEventContent(
@@ -87,10 +140,10 @@ async def post_message(self) -> bool:
87140
end_time = datetime.now(tz=pytz.UTC) + timedelta(minutes=self.lifetime)
88141
self.hasswebhook.db.insert(
89142
LifetimeEnd(end_date=end_time, room_id=self.room_id, event_id=event_id_req))
143+
return event_id_req
90144
except MForbidden:
91145
self.hasswebhook.log.error("Wrong Room ID")
92146
return False
93-
return True
94147

95148
# Redact message
96149
async def post_redaction(self) -> bool:

‎maubot.yaml

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
maubot: 0.1.0
22
id: com.valentinriess.hasswebhook
3-
version: 0.0.14
3+
version: 0.0.15
44
license: MIT
55
modules:
66
- hasswebhook
77
main_class: HassWebhook
88
config: true
99
extra_files:
10-
- base-config.yaml
10+
- base-config.yaml
1111
database: true
1212
webapp: true
13+
soft_dependencies:
14+
- Pillow
15+
dependencies:
16+
- Markdown
17+
- pytz

0 commit comments

Comments
 (0)