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:
ducklet 2020-11-07 20:35:52 +01:00
parent 81a176eb0c
commit efc6ecbb45
5 changed files with 460 additions and 114 deletions

View file

@ -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()})