Add gitlab webhook support

This commit is contained in:
Abel Luck 2022-12-01 13:47:27 +00:00
parent 9d41d56e0c
commit a1ae717c8f
26 changed files with 1824 additions and 8 deletions

62
ops_bot/util/contrast.py Normal file
View file

@ -0,0 +1,62 @@
# Based on https://github.com/gsnedders/wcag-contrast-ratio
# Copyright (c) 2015 Geoffrey Sneddon
# Copyright (c) 2019 Tulir Asokan
# MIT license
from typing import Tuple
RGB = Tuple[float, float, float]
def hex_to_rgb(color: str) -> RGB:
color = color.lstrip("#")
if len(color) != 3 and len(color) != 6:
raise ValueError("Invalid hex length")
step = 1 if len(color) == 3 else 2
try:
r = int(color[0:step], 16)
g = int(color[step:2 * step], 16)
b = int(color[2 * step:3 * step], 16)
except ValueError as e:
raise ValueError("Invalid hex value") from e
return r / 255, g / 255, b / 255
def rgb_to_hex(rgb: RGB) -> str:
r, g, b = rgb
r = int(r * 255)
g = int(g * 255)
b = int(b * 255)
return f"{r:02x}{g:02x}{b:02x}"
def contrast(rgb1: RGB, rgb2: RGB) -> float:
for r, g, b in (rgb1, rgb2):
if not 0.0 <= r <= 1.0:
raise ValueError(f"r {r} is out of valid range (0.0 - 1.0)")
if not 0.0 <= g <= 1.0:
raise ValueError(f"g {g} is out of valid range (0.0 - 1.0)")
if not 0.0 <= b <= 1.0:
raise ValueError(f"b {b} is out of valid range (0.0 - 1.0)")
l1 = _relative_luminance(*rgb1)
l2 = _relative_luminance(*rgb2)
if l1 > l2:
return (l1 + 0.05) / (l2 + 0.05)
else:
return (l2 + 0.05) / (l1 + 0.05)
def _relative_luminance(r: float, g: float, b: float) -> float:
r = _linearize(r)
g = _linearize(g)
b = _linearize(b)
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def _linearize(v: float) -> float:
if v <= 0.03928:
return v / 12.92
else:
return ((v + 0.055) / 1.055) ** 2.4

36
ops_bot/util/markdown.py Normal file
View file

@ -0,0 +1,36 @@
# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import commonmark
class HtmlEscapingRenderer(commonmark.HtmlRenderer):
def __init__(self, allow_html: bool = False):
super().__init__()
self.allow_html = allow_html
def lit(self, s):
if self.allow_html:
return super().lit(s)
return super().lit(s.replace("<", "&lt;").replace(">", "&gt;"))
def image(self, node, entering):
prev = self.allow_html
self.allow_html = True
super().image(node, entering)
self.allow_html = prev
md_parser = commonmark.Parser()
yes_html_renderer = commonmark.HtmlRenderer()
no_html_renderer = HtmlEscapingRenderer()
def render(message: str, allow_html: bool = False) -> str:
parsed = md_parser.parse(message)
if allow_html:
return yes_html_renderer.render(parsed)
else:
return no_html_renderer.render(parsed)

132
ops_bot/util/template.py Normal file
View file

@ -0,0 +1,132 @@
# gitlab - A GitLab client and webhook receiver for maubot
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Dict, Any, Tuple, Callable, Iterable, List, Union
import os.path
from jinja2 import Environment as JinjaEnvironment, Template, BaseLoader, TemplateNotFound, FileSystemLoader
from ops_bot.util import markdown
def sync_read_file(path: str) -> str:
with open(path) as file:
return file.read()
def sync_list_files(directory: str) -> list[str]:
return os.listdir(directory)
class TemplateUtil:
@staticmethod
def bold_scope(label: str) -> str:
try:
scope, label = label.rsplit("::", 1)
return f"{scope}::<strong>{label}</strong>"
except ValueError:
return label
@staticmethod
def pluralize(val: int, unit: str) -> str:
if val == 1:
return f"{val} {unit}"
return f"{val} {unit}s"
@classmethod
def format_time(cls, seconds: Union[int, float], enable_days: bool = False) -> str:
seconds = abs(seconds)
frac_seconds = round(seconds - int(seconds), 1)
minutes, seconds = divmod(int(seconds), 60)
hours, minutes = divmod(minutes, 60)
if enable_days:
days, hours = divmod(hours, 24)
else:
days = 0
parts = []
if days > 0:
parts.append(cls.pluralize(days, "day"))
if hours > 0:
parts.append(cls.pluralize(hours, "hour"))
if minutes > 0:
parts.append(cls.pluralize(minutes, "minute"))
if seconds > 0 or len(parts) == 0:
parts.append(cls.pluralize(seconds + frac_seconds, "second"))
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + f" and {parts[-1]}"
@staticmethod
def join_human_list(data: List[str], *, joiner: str = ", ", final_joiner: str = " and ",
mutate: Callable[[str], str] = lambda val: val) -> str:
if not data:
return ""
elif len(data) == 1:
return mutate(data[0])
return joiner.join(mutate(val) for val in data[:-1]) + final_joiner + mutate(data[-1])
class TemplateProxy:
_env: JinjaEnvironment
_args: Dict[str, Any]
def __init__(self, env: JinjaEnvironment, args: Dict[str, Any]) -> None:
self._env = env
self._args = args
def __getattr__(self, item: str) -> str:
try:
tpl = self._env.get_template(item)
except TemplateNotFound:
raise AttributeError(item)
return tpl.render(**self._args)
class PluginTemplateLoader(BaseLoader):
directory: str
macros: str
def __init__(self, base: str, directory: str) -> None:
self.directory = os.path.join("templates", base, directory)
self.macros = sync_read_file(os.path.join("templates", base, "macros.html"))
def get_source(self, environment: Any, name: str) -> Tuple[str, str, Callable[[], bool]]:
path = f"{os.path.join(self.directory, name)}.html"
try:
tpl = sync_read_file(path)
except KeyError:
raise TemplateNotFound(name)
return self.macros + tpl, name, lambda: True
def list_templates(self) -> Iterable[str]:
return [os.path.splitext(os.path.basename(path))[0]
for path in sync_list_files(self.directory)
if path.endswith(".html")]
class TemplateManager:
_env: JinjaEnvironment
_loader: PluginTemplateLoader
def __init__(self, base: str, directory: str) -> None:
#self._loader = FileSystemLoader(os.path.join("templates/", base))
self._loader = PluginTemplateLoader(base, directory)
self._env = JinjaEnvironment(loader=self._loader, lstrip_blocks=True, trim_blocks=True,
extensions=["jinja2.ext.do"])
self._env.filters["markdown"] = lambda message: markdown.render(message, allow_html=True)
def __getitem__(self, item: str) -> Template:
return self._env.get_template(item)
def proxy(self, args: Dict[str, Any]) -> TemplateProxy:
return TemplateProxy(self._env, args)