dump current state (wip-ish)
This commit is contained in:
parent
0124c35472
commit
51fb1c9f26
46 changed files with 3749 additions and 0 deletions
181
hotdog/functions.py
Normal file
181
hotdog/functions.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import locale
|
||||
import logging
|
||||
import unicodedata
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from io import StringIO
|
||||
from typing import *
|
||||
|
||||
import nio
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
tzdb = {
|
||||
"Europe/Berlin": timezone(timedelta(hours=2), "Europe/Berlin"),
|
||||
}
|
||||
|
||||
|
||||
def html_nametag(uid, name):
|
||||
return f'<a href="https://matrix.to/#/{escape(uid)}">{escape(name)}</a>'
|
||||
|
||||
|
||||
async def reply(
|
||||
message,
|
||||
plain: Optional[str] = None,
|
||||
*,
|
||||
html: Optional[str] = None,
|
||||
with_name: bool = False,
|
||||
in_thread: bool = False,
|
||||
) -> nio.RoomSendResponse:
|
||||
"""Reply to the given message in plain text or HTML.
|
||||
|
||||
If with_name is set the reply will be prefixed with the original
|
||||
message's sender's name.
|
||||
If in_thread is set the reply will reference the original message,
|
||||
allowing a client to connect the messages unambiguously.
|
||||
"""
|
||||
assert plain or html
|
||||
if with_name:
|
||||
if plain:
|
||||
plain = f"{message.sender_name}: {plain}"
|
||||
if html:
|
||||
sender = html_nametag(message.event.sender, message.sender_name)
|
||||
html = f"{sender}: {html}"
|
||||
args = {}
|
||||
if in_thread:
|
||||
args["reply_to_event_id"] = message.event.event_id
|
||||
if html:
|
||||
args["html"] = html
|
||||
if plain:
|
||||
args["plain"] = plain
|
||||
return await send_message(message.app.client, message.room.room_id, **args)
|
||||
|
||||
|
||||
async def react(message, emoji: str) -> nio.RoomSendResponse:
|
||||
"""Annotate a message with an Emoji.
|
||||
|
||||
Only a single Emoji character should be used for the reaction; compound
|
||||
characters are ok.
|
||||
"""
|
||||
# For details on Matrix reactions see MSC2677:
|
||||
# - https://github.com/uhoreg/matrix-doc/blob/aggregations-reactions/proposals/2677-reactions.md
|
||||
# - and https://github.com/matrix-org/matrix-appservice-slack/pull/522/commits/88e7076a595509b196f53369102a469cace6cc19
|
||||
vs16 = "\ufe0f" # see https://en.wikipedia.org/wiki/Variation_Selectors_%28Unicode_block%29
|
||||
if not emoji.endswith(vs16):
|
||||
if len(emoji) != 1:
|
||||
log.warning("Reactions should only use a single emoji, got: %s", emoji)
|
||||
else:
|
||||
emoji = unicodedata.normalize("NFC", emoji + vs16)
|
||||
return await message.app.client.room_send(
|
||||
room_id=message.room.room_id,
|
||||
message_type="m.reaction",
|
||||
content={
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.annotation",
|
||||
"event_id": message.event.event_id,
|
||||
"key": emoji,
|
||||
}
|
||||
},
|
||||
ignore_unverified_devices=True,
|
||||
)
|
||||
|
||||
|
||||
async def redact(message, reason: Optional[str] = None) -> nio.RoomRedactResponse:
|
||||
"""Redact a message.
|
||||
|
||||
This allows not only to redact text messages, but also reactions, i.e.
|
||||
take back a reaction.
|
||||
"""
|
||||
resp = await message.app.client.room_redact(
|
||||
room_id=message.room.room_id, event_id=message.event.event_id, reason=reason
|
||||
)
|
||||
if isinstance(resp, nio.RoomRedactError):
|
||||
raise RuntimeError(
|
||||
f"Could not redact: {message.event.event_id}: {resp.message}"
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
async def send_message(
|
||||
client: nio.AsyncClient,
|
||||
room_id: str,
|
||||
plain: str = "",
|
||||
*,
|
||||
html: Optional[str] = None,
|
||||
as_notice: bool = True,
|
||||
reply_to_event_id: Optional[str] = None,
|
||||
) -> nio.RoomSendResponse:
|
||||
"""Send text to a matrix room.
|
||||
|
||||
The message can be given as either plain text or HTML.
|
||||
If the plain text variant is not explicitly given, it will be
|
||||
generated from the rich text variant.
|
||||
You should always send the message as notice. Per convention
|
||||
bots don't react to notices, so sending only notices will avoid
|
||||
infinite loops between multiple bots in a channel.
|
||||
"""
|
||||
assert plain or html
|
||||
|
||||
content = {
|
||||
"msgtype": "m.notice" if as_notice else "m.text",
|
||||
"body": plain,
|
||||
}
|
||||
|
||||
if html:
|
||||
content["format"] = "org.matrix.custom.html"
|
||||
content["formatted_body"] = html
|
||||
if not plain:
|
||||
content["body"] = strip_tags(html)
|
||||
|
||||
if reply_to_event_id:
|
||||
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}}
|
||||
|
||||
try:
|
||||
return await client.room_send(
|
||||
room_id,
|
||||
"m.room.message",
|
||||
content,
|
||||
ignore_unverified_devices=True,
|
||||
)
|
||||
except nio.SendRetryError:
|
||||
log.exception(f"Unable to send message to room: {room_id}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def localized(lc: str, category=locale.LC_ALL):
|
||||
locale.setlocale(category, lc)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
locale.resetlocale(category)
|
||||
|
||||
|
||||
def localizedtz(dt: datetime, fmt: str, lc: str, tzname: str):
|
||||
tz = tzdb[tzname]
|
||||
with localized(lc, locale.LC_TIME):
|
||||
return dt.astimezone(tz).strftime(fmt)
|
||||
|
||||
|
||||
class TextonlyParser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__text = StringIO()
|
||||
|
||||
def handle_data(self, d):
|
||||
self.__text.write(d)
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self.__text.getvalue()
|
||||
|
||||
|
||||
def strip_tags(html):
|
||||
s = TextonlyParser()
|
||||
s.feed(html)
|
||||
return s.text
|
||||
|
||||
|
||||
def clamp(lower, x, upper):
|
||||
return max(lower, min(x, upper))
|
||||
Loading…
Add table
Add a link
Reference in a new issue