This commit is contained in:
夜坂雅 2022-09-07 19:40:00 +08:00
parent f171e9c8d9
commit 71b069c7f7
7 changed files with 201 additions and 61 deletions

Binary file not shown.

View file

@ -1,9 +1,13 @@
import logging
from nio import AsyncClient, MatrixRoom, RoomMessageText
from nyx_bot.chat_functions import send_text_to_room
from nyx_bot.chat_functions import send_quote_image, send_text_to_room
from nyx_bot.config import Config
from nyx_bot.storage import Storage
logger = logging.getLogger(__name__)
class Command:
def __init__(
@ -57,7 +61,7 @@ class Command:
await send_text_to_room(self.client, self.room.room_id, response)
async def _quote(self):
raise NotImplementedError("TBD !")
await send_quote_image(self.client, self.room, self.event, self.reply_to)
async def _show_help(self):
"""Show the help text"""

View file

@ -73,7 +73,7 @@ class Callbacks:
has_jerryxiao_prefix = True
msg = i
break
elif msg.startswith(self.command_prefix):
elif i.startswith(self.command_prefix):
has_command_prefix = True
msg = i
break
@ -91,7 +91,6 @@ class Callbacks:
await send_jerryxiao(
self.client, room, event, "¡¡", reply_to, msg, True
)
return
# Treat it as a command only if it has a prefix
if has_command_prefix:
@ -105,7 +104,7 @@ class Callbacks:
await command.process()
except Exception as inst:
lines = ["An Exception occoured:\n"]
lines.extend(traceback.format_exception(inst, limit=1, chain=False))
lines.extend(traceback.format_exception(inst, limit=-1, chain=False))
string = "".join(lines).rstrip()
await send_text_to_room(
self.client, room.room_id, string, True, False, event.event_id, True

View file

@ -1,17 +1,21 @@
import logging
from io import BytesIO
from typing import Optional, Union
from urllib.parse import urlparse
from markdown import markdown
from nio import (
AsyncClient,
ErrorResponse,
MatrixRoom,
MegolmEvent,
Response,
RoomMessageText,
RoomSendResponse,
SendRetryError,
UploadResponse,
)
from wand.image import Image
from nyx_bot.quote_image import make_quote_image
logger = logging.getLogger(__name__)
@ -173,65 +177,136 @@ async def send_jerryxiao(
)
async def react_to_event(
async def send_quote_image(
client: AsyncClient,
room: MatrixRoom,
event: RoomMessageText,
reply_to: str,
):
if not reply_to:
await send_text_to_room(
client,
room.room_id,
"Please reply to a text message.",
True,
False,
event.event_id,
True,
)
return
target_response = await client.room_get_event(room.room_id, reply_to)
target_event = target_response.event
if isinstance(target_event, RoomMessageText):
sender = target_event.sender
body = target_event.body
sender_name = room.user_name(sender)
sender_avatar = room.avatar_url(sender)
image = None
if sender_avatar:
url = urlparse(sender_avatar)
server_name = url.netloc
media_id = url.path.replace("/", "")
avatar_resp = await client.download(server_name, media_id)
data = avatar_resp.body
bytesio = BytesIO(data)
image = Image(file=bytesio)
else:
image = Image(width=64, height=64, background="#FFFF00")
quote_image = make_quote_image(sender_name, body, image)
await send_sticker_image(client, room.room_id, quote_image, reply_to)
else:
await send_text_to_room(
client,
room.room_id,
"Please reply to a normal text message.",
True,
False,
event.event_id,
True,
)
async def send_sticker_image(
client: AsyncClient,
room_id: str,
event_id: str,
reaction_text: str,
) -> Union[Response, ErrorResponse]:
"""Reacts to a given event in a room with the given reaction text
image: Image,
reply_to: Optional[str] = None,
):
"""Send sticker to toom. Hardcodes to WebP.
Args:
client: The client to communicate to matrix with.
Arguments:
---------
client : Client
room_id : str
image : Image
room_id: The ID of the room to send the message to.
event_id: The ID of the event to react to.
reaction_text: The string to react with. Can also be (one or more) emoji characters.
Returns:
A nio.Response or nio.ErrorResponse if an error occurred.
Raises:
SendRetryError: If the reaction was unable to be sent.
"""
content = {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": event_id,
"key": reaction_text,
This is a working example for a JPG image.
"content": {
"body": "someimage.jpg",
"info": {
"size": 5420,
"mimetype": "image/jpeg",
"thumbnail_info": {
"w": 100,
"h": 100,
"mimetype": "image/jpeg",
"size": 2106
},
"w": 100,
"h": 100,
"thumbnail_url": "mxc://example.com/SomeStrangeThumbnailUriKey"
},
"msgtype": "m.image",
"url": "mxc://example.com/SomeStrangeUriKey"
}
"""
(width, height) = (image.width, image.height)
bytesio = BytesIO()
with image:
image.format = "webp"
image.save(file=bytesio)
length = bytesio.getbuffer().nbytes
bytesio.seek(0)
logger.debug(f'Sending Image with length {length}, width={width}, height={height}')
resp, maybe_keys = await client.upload(
bytesio,
content_type="image/webp", # image/jpeg
filename="image.webp",
filesize=length,
)
if isinstance(resp, UploadResponse):
print("Image was uploaded successfully to server. ")
else:
print(f"Failed to upload image. Failure response: {resp}")
content = {
"body": "[Image]",
"info": {
"size": length,
"mimetype": "image/webp",
"thumbnail_info": {
"mimetype": "image/webp",
"size": length,
"w": width, # width in pixel
"h": height, # height in pixel
},
"w": width, # width in pixel
"h": height, # height in pixel
"thumbnail_url": resp.content_uri,
},
"msgtype": "m.image",
"url": resp.content_uri,
}
return await client.room_send(
room_id,
"m.reaction",
content,
ignore_unverified_devices=True,
)
if reply_to:
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to}}
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None:
"""Callback for when an event fails to decrypt. Inform the user"""
logger.error(
f"Failed to decrypt event '{event.event_id}' in room '{room.room_id}'!"
f"\n\n"
f"Tip: try using a different device ID in your config file and restart."
f"\n\n"
f"If all else fails, delete your store directory and let the bot recreate "
f"it (your reminders will NOT be deleted, but the bot may respond to existing "
f"commands a second time)."
)
user_msg = (
"Unable to decrypt this message. "
"Check whether you've chosen to only encrypt to trusted devices."
)
await send_text_to_room(
self.client,
room.room_id,
user_msg,
reply_to_event_id=event.event_id,
)
try:
await client.room_send(room_id, message_type="m.sticker", content=content)
print("Image was sent successfully")
except Exception:
print(f"Image send of file {image} failed.")

BIN
nyx_bot/mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

61
nyx_bot/quote_image.py Normal file
View file

@ -0,0 +1,61 @@
from wand.drawing import Drawing
from wand.image import Image
from wand.color import Color
import os.path
import nyx_bot
TEXTBOX_PADDING_PIX = 8
TEXTBOX_INNER_MARGIN = 4
AVATAR_SIZE = 48
AVATAR_RIGHT_PADDING = 6
BORDER_MARGIN = 8
MIN_TEXTBOX_WIDTH = 256
FONT_FILE = os.path.join(nyx_bot.__path__[0], "DroidSansFallback.ttf")
MASK_FILE = os.path.join(nyx_bot.__path__[0], "mask.png")
def make_quote_image(sender: str, text: str, avatar: Image):
draw = Drawing()
draw.font = FONT_FILE
draw.font_size = 15
with Image(width=2000, height=2000) as i:
bbox = draw.get_font_metrics(i, text, True)
sender_bbox = draw.get_font_metrics(i, sender, False)
text_width = max(bbox.text_width, sender_bbox.text_width)
textbox_height = (TEXTBOX_PADDING_PIX * 2) + bbox.text_height + sender_bbox.text_height + TEXTBOX_INNER_MARGIN
# Original textbox width
textbox_width_orig = (TEXTBOX_PADDING_PIX * 2) + text_width
# Final textbox width
textbox_width = max(textbox_width_orig, MIN_TEXTBOX_WIDTH)
# Final calculated height
final_height = (BORDER_MARGIN * 2) + textbox_height
width = (BORDER_MARGIN * 2) + AVATAR_SIZE + AVATAR_RIGHT_PADDING + textbox_width
height = max(final_height, AVATAR_SIZE + (BORDER_MARGIN * 2))
# Textbox
textbox_x = BORDER_MARGIN + AVATAR_SIZE + AVATAR_RIGHT_PADDING
textbox_y = BORDER_MARGIN
# Make a mask
with avatar.clone() as img, Image(filename=MASK_FILE) as mask:
img.resize(AVATAR_SIZE, AVATAR_SIZE)
img.alpha_channel = True
img.composite_channel("default", mask, "copy_alpha", 0, 0)
draw.composite("overlay", BORDER_MARGIN, BORDER_MARGIN, AVATAR_SIZE, AVATAR_SIZE, img)
# Make image
draw.fill_color = "#111111"
draw.stroke_width = 0
draw.rectangle(textbox_x, textbox_y, width=textbox_width, height=textbox_height, radius=8)
# Draw text
draw.fill_color = "#FFFFFF"
name_x = textbox_x + TEXTBOX_PADDING_PIX
name_y = textbox_y + TEXTBOX_PADDING_PIX + 14
draw.text(int(name_x), int(name_y), sender)
text_x = name_x
text_y = name_y + TEXTBOX_INNER_MARGIN + sender_bbox.text_height
draw.text(int(text_x), int(text_y), text)
ret = Image(width=int(width), height=int(height))
draw(ret)
return ret

View file

@ -34,6 +34,7 @@ setup(
"matrix-nio>=0.10.0",
"Markdown>=3.1.1",
"PyYAML>=5.1.2",
"Wand",
],
extras_require={
"postgres": ["psycopg2>=2.8.5"],