Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions urlpreview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ If the link returns a 404, the bot will return an emoji `no_results_react` (💨
- `no_results_react` - Adds a reaction emoji to the message to show that no results were returned. Put `''` to disable.
- `url_blacklist` - Disable urlpreview for an IP range or a Regex entry
- `user_blacklist` - Disable urlpreview for a user
- `html_custom_title_style` - Set to a CSS string in order to apply that style as an html attribute to the title tag in a response/reply. Put `''` to disable.
- `html_custom_description_style` - Set to a CSS string in order to apply that style as an html attribute to the description tag in a response/reply. Put `''` to disable.
- `html_custom_image_style` - '' Set to a CSS string in order to apply that style as an html attribute to the image tag in a response/reply. Put `''` to disable.
- `respond_instead_of_reply` - Set to True in order to respond rather than reply. A reply quotes the original message that prompted this bot, while a response simply sends the message without quoting.
- `use_divtag_instead_of_htag_for_title` - Set to True to use an HTML div tag instead of the default h3 tag in the title.
- `use_divtag_instead_of_ptag_for_description` - Set to True to use an HTML div tag instead of the default p tag .
- `indent_description` - Set to True to apply a 4-space indent and a right-arrow to the beginning of the description to improve readability when other customizations are used.

<details open>
{( <summary><b>htmlparser</b></summary> )}
Expand Down
8 changes: 8 additions & 0 deletions urlpreview/base-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ json_max_char: 2000
max_links: 3
min_image_width: 475
max_image_embed: 300
html_custom_title_style: '' # 'font-size: medium; padding-bottom: 8px; line-height:1.1; margin-bottom: 5px;'
html_custom_description_style: '' # 'font-size: small; line-height: 1; padding-bottom: 8px; margin-bottom: 8px; padding-top:1px; margin-top:2px;'
html_custom_image_style: '' # 'max-height: 60px;'
respond_instead_of_reply: False
use_divtag_instead_of_htag_for_title: False
use_divtag_instead_of_ptag_for_description: False
indent_description: False
yt_dlp_storage_path: '.'
no_results_react: 💨
url_blacklist:
- ':\/\/matrix\.to\/'
Expand Down
Binary file not shown.
30 changes: 24 additions & 6 deletions urlpreview/urlpreview/urlpreview.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("no_results_react")
helper.copy("url_blacklist")
helper.copy("user_blacklist")
helper.copy("html_custom_title_style")
helper.copy("html_custom_description_style")
helper.copy("html_custom_image_style")
helper.copy("respond_instead_of_reply")
helper.copy("use_divtag_instead_of_htag_for_title")
helper.copy("use_divtag_instead_of_ptag_for_description")
helper.copy("indent_description")

class UrlPreviewBot(Plugin):
async def start(self) -> None:
Expand Down Expand Up @@ -61,6 +68,14 @@ async def handler(self, evt: MessageEvent, matches: List[str]) -> None:
MAX_IMAGE_EMBED = self.config["max_image_embed"]
NO_RESULTS_REACT = self.config["no_results_react"]
URL_BLACKLIST = self.config["url_blacklist"]
self.HTML_CUSTOM_STYLES_DICT = {}
self.HTML_CUSTOM_STYLES_DICT['TITLE'] = self.config["html_custom_title_style"]
self.HTML_CUSTOM_STYLES_DICT['DESCRIPTION'] = self.config["html_custom_description_style"]
self.HTML_CUSTOM_STYLES_DICT['IMAGE'] = self.config["html_custom_image_style"]
self.RESPOND_INSTEAD_OF_REPLY = self.config["respond_instead_of_reply"]
self.USE_DIVTAG_INSTEAD_OF_HTAG_FOR_TITLE = self.config["use_divtag_instead_of_htag_for_title"]
self.USE_DIVTAG_INSTEAD_OF_PTAG_FOR_DESCRIPTION = self.config["use_divtag_instead_of_ptag_for_description"]
self.INDENT_DESCRIPTION = self.config['indent_description']
await evt.mark_read()

embeds = []
Expand Down Expand Up @@ -102,7 +117,10 @@ async def handler(self, evt: MessageEvent, matches: List[str]) -> None:
pass
return
to_send = "".join(embeds)
return await evt.reply(to_send, allow_html=True)
if self.RESPOND_INSTEAD_OF_REPLY:
return await evt.respond(to_send, allow_html=True)
else:
return await evt.reply(to_send, allow_html=True)


# Utility Commands
Expand Down Expand Up @@ -142,7 +160,7 @@ async def embed_url_preview(self, url_str, og, html_custom_headers=None, max_ima
return None
if all(v is None for v in og):
return None
# Fetch image_mxc

image_mxc = og.get('image_mxc', None)
if image_mxc is None:
image_mxc = await process_image(self, image=og.get('image', None), html_custom_headers=html_custom_headers, content_type=og.get('content_type', None))
Expand All @@ -153,10 +171,10 @@ async def embed_url_preview(self, url_str, og, html_custom_headers=None, max_ima
return f"<blockquote>{image_solo}</blockquote>"
return None # Everything is empty
# Default message
title = format_title(og.get('title', None), url_str)
description = format_description(og.get('description', None))
image = format_image(image_mxc, url_str, og.get('content_type', None), format_image_width(og.get('image_width', None), max_image_embed))
message = "".join(filter(None, [title, description, image]))
title = format_title(og.get('title', None), url_str, custom_styles=self.HTML_CUSTOM_STYLES_DICT['TITLE'], use_divtag_instead_of_htag=self.USE_DIVTAG_INSTEAD_OF_HTAG_FOR_TITLE)
description = format_description(og.get('description'), custom_styles=self.HTML_CUSTOM_STYLES_DICT['DESCRIPTION'], indent_description=self.INDENT_DESCRIPTION, use_divtag_instead_of_ptag=self.USE_DIVTAG_INSTEAD_OF_PTAG_FOR_DESCRIPTION)
image = format_image(image_mxc, url_str, og.get('content_type', None), format_image_width(og.get('image_width', None), max_image_embed), custom_styles=self.HTML_CUSTOM_STYLES_DICT['IMAGE'])
message = "".join(filter(None, [title, description, image]))
if message:
return f"<blockquote>{message}</blockquote>"
return None
76 changes: 67 additions & 9 deletions urlpreview/urlpreview/urlpreview_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import re
import socket
import urllib.parse
import os
import subprocess
from datetime import datetime as dt
import hashlib
from urllib.parse import urlparse

IMAGE_TYPES = ["image/gif", "image/jpg", "image/jpeg", "image/png", "image/webp"]
Expand Down Expand Up @@ -31,23 +35,26 @@ def check_line_breaks(text: str):
return None
return text.replace('\n', '<br />')

def format_title(title, url_str: str=""):
def format_title(title, url_str: str="", custom_styles: str="",use_divtag_instead_of_htag: bool=False):
if not title:
return None
tag = 'div' if use_divtag_instead_of_htag else 'h3'
if url_str:
return f'<h3><a href="{url_str}">{str(title)}</a></h3>'
return f'<{tag} style="{custom_styles}"><a href="{url_str}">{str(title)}</a></{tag}>'
else:
return f'<h3>{str(title)}</h3>'
return f'<{tag} style="{custom_styles}" >{str(title)}</{tag}>'

def format_description(description, preserve_line_breaks: bool=False):
def format_description(description, preserve_line_breaks: bool=False, custom_styles: str="", indent_description: bool=False, use_divtag_instead_of_ptag: bool=False):
if not description:
return None
prefix = '&#8250;&nbsp;&nbsp;&nbsp;&nbsp;' if indent_description else ''
tag = 'div' if use_divtag_instead_of_ptag else 'p'
if preserve_line_breaks is False:
return f'<p>'+str(description).replace('\r', ' ').replace('\n', ' ')+'</p>'
return f'<{tag} style="{custom_styles}">{prefix}'+str(description).replace('\r', ' ').replace('\n', ' ')+f'</{tag}>'
else:
return f'<p>'+str(description)+'</p>'
return f'<{tag} style="{custom_styles}">{prefix}'+str(description)+f'</{tag}>'

def format_image(image_mxc, url_str: str='', content_type: str=None, max_image_embed: int=300):
def format_image(image_mxc, url_str: str='', content_type: str=None, max_image_embed: int=300, custom_styles: str=""):
if not image_mxc:
return None
if not content_type:
Expand All @@ -56,9 +63,19 @@ def format_image(image_mxc, url_str: str='', content_type: str=None, max_image_e
if max_image_embed > 0:
width = f'width="{str(max_image_embed)}" '
if url_str:
return f'<a href="{url_str}"><img src="{image_mxc}" alt="{content_type}" {width}/></a>'
return f'<a href="{url_str}"><img style="{custom_styles}" src="{image_mxc}" alt="{content_type}" {width}/></a>'
else:
return f'<img src="{image_mxc}" alt="{content_type}" {width}/>'
return f'<img style="{custom_styles}" src="{image_mxc}" alt="{content_type}" {width}/>'
def format_video(video_mxc, url_str: str='', content_type: str=None):
if not video_mxc:
return None
if not content_type:
content_type = "Video"

if url_str:
return f'<a href="{url_str}"><video width="320" height="240" controls><source src="{video_mxc}" type="video/mp4"></video></a>'
else:
return f'<video width="320" height="240" controls><source src="{video_mxc}" type="video/mp4"></video>'

def format_image_width(image_width, max_image_embed: int=300):
if image_width is None:
Expand Down Expand Up @@ -104,6 +121,47 @@ async def matrix_get_image(self, image_url: str, html_custom_headers=None, mime_
self.log.exception(f"[urlpreview] [utils] Error matrix_get_image client.upload_media: {str(err)}")
return None
return mxc
async def process_video(self, video: str, html_custom_headers=None, content_type: str=None):
if not video:
return None
video_url = urlparse(video)
# URL is mxc
if video_url.scheme == 'mxc':
return video
# URL is not mxc
# yt-dlp puts out either mp4 or webm
if not content_type:
content_type = 'video/webm'
image_mxc = await matrix_get_youtube_video(
self,
video_url=video,
html_custom_headers=html_custom_headers,
mime_type=content_type,
filename=content_type.replace('/', '.').replace('jpeg', 'jpg')
)
return image_mxc
async def matrix_get_youtube_video(self, video_url: str, html_custom_headers=None, mime_type: str="video/webm", filename: str=""):
if not filename:
h = hashlib.md5(bytes(dt.now()))
filename = str(dt.now() + h.hexdigest())
if not video_url:
return None
try:
result = subprocess.run(f'yt-dlp -p {self.YT_DLP_STORAGE_PATH} --no-part {video_url} -o "{filename}"')
except Exception as err:
self.log.exception(f"[urlpreview] [utils] Error matrix_get_youtube_video http.get: {str(err)}")
return None

og_video = None
with open(filename, 'rb') as h:
og_video = h.read()
try:
mxc = await self.client.upload_media(og_video, mime_type=mime_type, filename=filename)
except Exception as err:
self.log.exception(f"[urlpreview] [utils] Error matrix_get_youtube_video client.upload_media: {str(err)}")
return None
return mxc


def url_check_is_in_range(ip, unsafe_url, ranges):
for r in ranges:
Expand Down