dump current state (wip-ish)

This commit is contained in:
ducklet 2020-11-01 16:31:37 +01:00
parent 0124c35472
commit 51fb1c9f26
46 changed files with 3749 additions and 0 deletions

181
hotdog/functions.py Normal file
View 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))