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 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.config import Config
from nyx_bot.storage import Storage from nyx_bot.storage import Storage
logger = logging.getLogger(__name__)
class Command: class Command:
def __init__( def __init__(
@ -57,7 +61,7 @@ class Command:
await send_text_to_room(self.client, self.room.room_id, response) await send_text_to_room(self.client, self.room.room_id, response)
async def _quote(self): 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): async def _show_help(self):
"""Show the help text""" """Show the help text"""

View file

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

View file

@ -1,17 +1,21 @@
import logging import logging
from io import BytesIO
from typing import Optional, Union from typing import Optional, Union
from urllib.parse import urlparse
from markdown import markdown from markdown import markdown
from nio import ( from nio import (
AsyncClient, AsyncClient,
ErrorResponse, ErrorResponse,
MatrixRoom, MatrixRoom,
MegolmEvent,
Response,
RoomMessageText, RoomMessageText,
RoomSendResponse, RoomSendResponse,
SendRetryError, SendRetryError,
UploadResponse,
) )
from wand.image import Image
from nyx_bot.quote_image import make_quote_image
logger = logging.getLogger(__name__) 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, client: AsyncClient,
room_id: str, room_id: str,
event_id: str, image: Image,
reaction_text: str, reply_to: Optional[str] = None,
) -> Union[Response, ErrorResponse]: ):
"""Reacts to a given event in a room with the given reaction text """Send sticker to toom. Hardcodes to WebP.
Args: Arguments:
client: The client to communicate to matrix with. ---------
client : Client
room_id : str
image : Image
room_id: The ID of the room to send the message to. This is a working example for a JPG image.
"content": {
event_id: The ID of the event to react to. "body": "someimage.jpg",
"info": {
reaction_text: The string to react with. Can also be (one or more) emoji characters. "size": 5420,
"mimetype": "image/jpeg",
Returns: "thumbnail_info": {
A nio.Response or nio.ErrorResponse if an error occurred. "w": 100,
"h": 100,
Raises: "mimetype": "image/jpeg",
SendRetryError: If the reaction was unable to be sent. "size": 2106
""" },
content = { "w": 100,
"m.relates_to": { "h": 100,
"rel_type": "m.annotation", "thumbnail_url": "mxc://example.com/SomeStrangeThumbnailUriKey"
"event_id": event_id, },
"key": reaction_text, "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( if reply_to:
room_id, content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to}}
"m.reaction",
content,
ignore_unverified_devices=True,
)
try:
async def decryption_failure(self, room: MatrixRoom, event: MegolmEvent) -> None: await client.room_send(room_id, message_type="m.sticker", content=content)
"""Callback for when an event fails to decrypt. Inform the user""" print("Image was sent successfully")
logger.error( except Exception:
f"Failed to decrypt event '{event.event_id}' in room '{room.room_id}'!" print(f"Image send of file {image} failed.")
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,
)

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", "matrix-nio>=0.10.0",
"Markdown>=3.1.1", "Markdown>=3.1.1",
"PyYAML>=5.1.2", "PyYAML>=5.1.2",
"Wand",
], ],
extras_require={ extras_require={
"postgres": ["psycopg2>=2.8.5"], "postgres": ["psycopg2>=2.8.5"],