urlinfo: allow sub-modules and add module for IMDb movies
The urlinfo plugin is now set up to look up URL information for any URL occurring in text, not only when triggered explicitly as a command. The youtube plugin should probably be integrated into this setup, replacing the bot plugin with a urlinfo extension.
This commit is contained in:
parent
81a176eb0c
commit
efc6ecbb45
5 changed files with 460 additions and 114 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import locale
|
||||
import logging
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, fields
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from html import escape
|
||||
from html import escape as html_escape
|
||||
from html.parser import HTMLParser
|
||||
from io import StringIO
|
||||
from typing import *
|
||||
|
|
@ -18,7 +20,7 @@ tzdb = {
|
|||
|
||||
|
||||
def html_nametag(uid, name):
|
||||
return f'<a href="https://matrix.to/#/{escape(uid)}">{escape(name)}</a>'
|
||||
return f'<a href="https://matrix.to/#/{html_escape(uid)}">{html_escape(name)}</a>'
|
||||
|
||||
|
||||
async def reply(
|
||||
|
|
@ -143,6 +145,67 @@ async def send_message(
|
|||
log.exception(f"Unable to send message to room: {room_id}")
|
||||
|
||||
|
||||
async def send_image(
|
||||
client: nio.AsyncClient,
|
||||
room_id: str,
|
||||
url: str,
|
||||
description: str,
|
||||
*,
|
||||
width: Optional[int] = None,
|
||||
height: Optional[int] = None,
|
||||
size: Optional[int] = None,
|
||||
mimetype: Optional[str] = None,
|
||||
thumbnail_url: Optional[str] = None,
|
||||
thumbnail_width: Optional[int] = None,
|
||||
thumbnail_height: Optional[int] = None,
|
||||
thumbnail_size: Optional[int] = None,
|
||||
thumbnail_mimetype: Optional[str] = None,
|
||||
) -> nio.RoomSendResponse:
|
||||
# https://matrix.org/docs/spec/client_server/r0.6.1#m-image
|
||||
content = defaultdict(
|
||||
dict,
|
||||
{
|
||||
"body": description,
|
||||
"msgtype": "m.image",
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
|
||||
# Map all image keyword args into the content dict.
|
||||
kwds = locals()
|
||||
kwmap = {
|
||||
"width": "w",
|
||||
"height": "h",
|
||||
"size": "size",
|
||||
"mimetype": "mimetype",
|
||||
"thumbnail_url": "thumbnail_url",
|
||||
}
|
||||
for kwarg, carg in kwmap.items():
|
||||
if kwds[kwarg] is not None:
|
||||
content["info"][carg] = kwds[kwarg]
|
||||
|
||||
# Map all thumbnail keyword args into the content dict.
|
||||
kwmap = {
|
||||
"thumbnail_width": "w",
|
||||
"thumbnail_height": "h",
|
||||
"thumbnail_size": "size",
|
||||
"thumbnail_mimetype": "mimetype",
|
||||
}
|
||||
thumbinfo = defaultdict(dict)
|
||||
for kwarg, carg in kwmap.items():
|
||||
if kwds[kwarg] is not None:
|
||||
thumbinfo[carg] = kwds[kwarg]
|
||||
if thumbinfo:
|
||||
content["info"]["thumbnail_info"] = thumbinfo
|
||||
|
||||
return await client.room_send(
|
||||
room_id,
|
||||
"m.room.message",
|
||||
content,
|
||||
ignore_unverified_devices=True,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def localized(lc: str, category=locale.LC_ALL):
|
||||
locale.setlocale(category, lc)
|
||||
|
|
@ -179,3 +242,81 @@ def strip_tags(html):
|
|||
|
||||
def clamp(lower, x, upper):
|
||||
return max(lower, min(x, upper))
|
||||
|
||||
|
||||
def pretty_duration(seconds: int) -> str:
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds - hours * 3600) // 60
|
||||
seconds = seconds % 60
|
||||
|
||||
# full: 1h 23m 13s
|
||||
# 0 seconds: 1h 23m
|
||||
# 0 hours: 23m 13s
|
||||
# 0 hours 0 seconds: 23m 00s
|
||||
|
||||
parts = {}
|
||||
if hours:
|
||||
parts["h"] = f"{hours}h"
|
||||
parts["m"] = f"{minutes:02}m"
|
||||
if seconds or not hours:
|
||||
parts["s"] = f"{seconds:02}s"
|
||||
|
||||
return " ".join(parts.values())
|
||||
|
||||
|
||||
def capped_text(text: str, max_len: int, mark=" […]") -> str:
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
|
||||
capped = ""
|
||||
for word in text.split(" "):
|
||||
if len(capped + f" {word}") > max_len - len(mark):
|
||||
capped += mark
|
||||
break
|
||||
capped += f" {word}"
|
||||
return capped
|
||||
|
||||
|
||||
class ElementParser(HTMLParser):
|
||||
"""Parse HTML for the first matching element"""
|
||||
|
||||
def __init__(self, selector: Callable[[str, Mapping[str, str]], bool]):
|
||||
super().__init__()
|
||||
self.selector = selector
|
||||
self.__active_tag = None
|
||||
self.__done = False
|
||||
self.__value = ""
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if self.selector(tag, attrs):
|
||||
self.__active_tag = tag
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == self.__active_tag:
|
||||
self.__done = True
|
||||
self.__active_tag = None
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.__active_tag and not self.__done:
|
||||
self.__value += data
|
||||
|
||||
@property
|
||||
def value(self) -> Optional[str]:
|
||||
return self.__value if self.__done else None
|
||||
|
||||
def load_chunks(self, content: Iterable[str]) -> None:
|
||||
for chunk in content:
|
||||
self.feed(chunk)
|
||||
if self.__done:
|
||||
break
|
||||
|
||||
|
||||
def escape_all(dc: dataclass, escape: Callable[[str], str] = html_escape) -> None:
|
||||
"""Patch a dataclass to escape all strings."""
|
||||
for f in fields(dc):
|
||||
if f.type is str:
|
||||
setattr(dc, f.name, escape(getattr(dc, f.name)))
|
||||
elif get_origin(f.type) is list and get_args(f.type)[0] is str:
|
||||
setattr(dc, f.name, [escape(x) for x in getattr(dc, f.name)])
|
||||
elif get_origin(f.type) is dict and get_args(f.type)[1] is str:
|
||||
setattr(dc, f.name, {k: escape(v) for k, v in getattr(dc, f.name).items()})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue