Update
This commit is contained in:
parent
f171e9c8d9
commit
71b069c7f7
BIN
nyx_bot/DroidSansFallback.ttf
Normal file
BIN
nyx_bot/DroidSansFallback.ttf
Normal file
Binary file not shown.
|
@ -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"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
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
61
nyx_bot/quote_image.py
Normal 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
|
Loading…
Reference in a new issue