From 645fc7e9c79b1d4432af9a4b4a658c08eaabb78e Mon Sep 17 00:00:00 2001 From: trigg Date: Fri, 2 Oct 2020 22:51:08 +0100 Subject: [PATCH] - Added rich text (bold/italic/underline) fixes #59 - Added option to show attachments fixes #57 - Include in-line custom emoji (why god why) --- discover_overlay/discord_connector.py | 23 +-- discover_overlay/image_getter.py | 63 +++++++- discover_overlay/text_overlay.py | 207 ++++++++++++++++++++++---- discover_overlay/text_settings.py | 18 +++ discover_overlay/voice_overlay.py | 8 +- 5 files changed, 271 insertions(+), 48 deletions(-) diff --git a/discover_overlay/discord_connector.py b/discover_overlay/discord_connector.py index 20d9131..6cba8a2 100644 --- a/discover_overlay/discord_connector.py +++ b/discover_overlay/discord_connector.py @@ -34,7 +34,6 @@ class DiscordConnector: self.last_connection = "" self.text = [] self.authed = False - self.timeregex = re.compile(r"\.\d+") def get_access_token_stage1(self): global oauth_token @@ -87,12 +86,8 @@ class DiscordConnector: self.in_room.remove(userid) def add_text(self, message): - # Remove micro time - time_input = message["timestamp"] - time_input = self.timeregex.sub("", time_input) - print(time_input) utc_time = time.strptime( - time_input, "%Y-%m-%dT%H:%M:%S%z") + message["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z") t = time.time() epoch_time = calendar.timegm(utc_time) un = message["author"]["username"] @@ -107,6 +102,7 @@ class DiscordConnector: 'nick': un, 'nick_col': ac, 'time': epoch_time, + 'attach': self.get_attachment_from_message(message), }) self.text_altered = True @@ -118,7 +114,8 @@ class DiscordConnector: 'content': self.get_message_from_message(message_in), 'nick': message['nick'], 'nick_col': message['nick_col'], - 'time': message['time']} + 'time': message['time'], + 'attach': message['attach']} self.text[idx] = new_message self.text_altered = True return @@ -133,7 +130,9 @@ class DiscordConnector: return def get_message_from_message(self, message): - if "content" in message and len(message["content"]) > 0: + if "content_parsed" in message: + return message["content_parsed"] + elif "content" in message and len(message["content"]) > 0: return message["content"] elif len(message["embeds"]) == 1: if "rawDescription" in message["embeds"][0]: @@ -141,10 +140,14 @@ class DiscordConnector: if "author" in message["embeds"][0]: return message["embeds"][0]["author"]["name"] elif len(message["attachments"]) == 1: - # Need to care - return "-- attachment --" + return "" return "" + def get_attachment_from_message(self, message): + if len(message["attachments"]) == 1: + return message["attachments"] + return None + def update_user(self, user): if user["id"] in self.userlist: if not "mute" in user and "mute" in self.userlist[user["id"]]: diff --git a/discover_overlay/image_getter.py b/discover_overlay/image_getter.py index a9a11ae..674c7b8 100644 --- a/discover_overlay/image_getter.py +++ b/discover_overlay/image_getter.py @@ -3,10 +3,14 @@ gi.require_version("Gtk", "3.0") gi.require_version('PangoCairo', '1.0') gi.require_version('GdkPixbuf', '2.0') import urllib +import requests import threading from gi.repository.GdkPixbuf import Pixbuf from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo +import cairo import logging +import PIL.Image as Image +from io import BytesIO class Image_Getter(): @@ -26,16 +30,69 @@ class Image_Getter(): input_stream = Gio.MemoryInputStream.new_from_data( response.read(), None) pixbuf = Pixbuf.new_from_stream(input_stream, None) - pixbuf = pixbuf.scale_simple(self.size, self.size, - GdkPixbuf.InterpType.BILINEAR) + if self.size: + pixbuf = pixbuf.scale_simple(self.size, self.size, + GdkPixbuf.InterpType.BILINEAR) + # elif self.limit_x or self.limit_y: + # px = pixbuf.width() + # py = pixbuf.height() + # aspect = px / py + # scale = 1.0 + # if self.limit_x and self.limit_y: + # scale = min(self.limit_x / px, self.limit_y / py, 1.0) + # elif self.limit_x: + # scale = min(self.limit_x / px, 1.0) + # elif self.limit_y: + # scale = min(self.limit_y / py, 1.0)## +# +# pixbuf = pixbuf.scale_simple(int(px * scale), int(py * scale), +# GdkPixbuf.InterpType.BILINEAR) + self.func(self.id, pixbuf) except Exception as e: logging.error( - "Could not access : %s" % (url)) + "Could not access : %s" % (self.url)) logging.error(e) +class Surface_Getter(): + def __init__(self, func, url, id, size): + self.func = func + self.id = id + self.url = url + self.size = size + + def get_url(self): + + im = Image.open(requests.get(self.url, stream=True, headers={ + 'Referer': 'https://streamkit.discord.com/overlay/voice', 'User-Agent': 'Mozilla/5.0'}).raw) + surf = self.from_pil(im) + + self.func(self.id, surf) + + def from_pil(self, im, alpha=1.0, format=cairo.FORMAT_ARGB32): + """ + :param im: Pillow Image + :param alpha: 0..1 alpha to add to non-alpha images + :param format: Pixel format for output surface + """ + assert format in ( + cairo.FORMAT_RGB24, cairo.FORMAT_ARGB32), "Unsupported pixel format: %s" % format + if 'A' not in im.getbands(): + im.putalpha(int(alpha * 256.)) + arr = bytearray(im.tobytes('raw', 'BGRa')) + surface = cairo.ImageSurface.create_for_data( + arr, format, im.width, im.height) + return surface + + def get_image(func, id, ava, size): image_getter = Image_Getter(func, id, ava, size) t = threading.Thread(target=image_getter.get_url, args=()) t.start() + + +def get_surface(func, id, ava, size): + image_getter = Surface_Getter(func, id, ava, size) + t = threading.Thread(target=image_getter.get_url, args=()) + t.start() diff --git a/discover_overlay/text_overlay.py b/discover_overlay/text_overlay.py index 77af430..3b40616 100644 --- a/discover_overlay/text_overlay.py +++ b/discover_overlay/text_overlay.py @@ -9,6 +9,9 @@ from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango, PangoCairo import cairo import logging import time +import sys +import re +from .image_getter import get_surface class TextOverlayWindow(OverlayWindow): @@ -18,10 +21,18 @@ class TextOverlayWindow(OverlayWindow): self.content = [] self.text_font = None self.text_size = 13 + # 0, 0, self.text_size, self.text_size) + self.pango_rect = Pango.Rectangle() + self.pango_rect.width = self.text_size * Pango.SCALE + self.pango_rect.height = self.text_size * Pango.SCALE + self.connected = True self.bg_col = [0.0, 0.6, 0.0, 0.1] self.fg_col = [1.0, 1.0, 1.0, 1.0] - #self.text_time = 30 + self.attachment = {} + + self.imgList = [] + self.imgFinder = re.compile(r"`") def set_text_time(self, t): self.text_time = t @@ -45,40 +56,128 @@ class TextOverlayWindow(OverlayWindow): self.bg_col = bg_col self.redraw() + def set_show_attach(self, att): + self.show_attach = att + self.redraw() + def set_popup_style(self, b): self.popup_style = b + def set_font(self, name, size): + self.text_font = name + self.text_size = size + self.pango_rect = Pango.Rectangle() + self.pango_rect.width = self.text_size * Pango.SCALE + self.pango_rect.height = self.text_size * Pango.SCALE + self.redraw() + + def make_line(self, msg): + ret = "" + if isinstance(msg, list): + for a in msg: + ret = "%s%s" % (ret, self.make_line(a)) + elif isinstance(msg, str): + ret = msg + elif msg['type'] == 'strong': + ret = "%s" % (self.make_line(msg['content'])) + elif msg['type'] == 'text': + ret = self.santize_string(msg['content']) + elif msg['type'] == 'link': + ret = "%s" % (self.make_line(msg['content'])) + elif msg['type'] == 'emoji': + if 'surrogate' in msg: + # ['src'] is SVG URL + ret = msg + # ret = msg['surrogate'] + else: + ### Add Image ### + url = ("https://cdn.discordapp.com/emojis/%s.png?v=1" % + (msg['emojiId'])) + img = {"url": url} + self.imgList.append(img) + ret = "`" + elif msg['type'] == 'inlineCode' or msg['type'] == 'codeBlock' or msg['type'] == 'blockQuote': + ret = "%s" % ( + msg['content']) + elif msg['type'] == 'u': + ret = "%s" % (self.make_line(msg['content'])) + elif msg['type'] == 'em': + ret = "%s" % (self.make_line(msg['content'])) + elif msg['type'] == 's': + ret = "%s" % (self.make_line(msg['content'])) + elif msg['type'] == 'channel': + ret = self.make_line(msg['content']) + elif msg['type'] == 'mention': + ret = self.make_line(msg['content']) + elif msg['type'] == 'br': + ret = '\n' + else: + logging.warning("Unknown text type : %s" % (msg["type"])) + return ret + + def recv_attach(self, id, pix): + self.attachment[id] = pix + self.redraw() + def do_draw(self, context): self.context = context - context.set_antialias(self.compositing) + context.set_antialias(cairo.ANTIALIAS_GOOD) (w, h) = self.get_size() # Make background transparent context.set_source_rgba(0.0, 0.0, 0.0, 0.0) context.paint() - if not self.connected: - return - long_string = "" - sep = "" - tnow = time.time() - for line in self.content: - if not self.popup_style or tnow - line['time'] < self.text_time: - col = "#fff" - if 'nick_col' in line and line['nick_col']: - col = line['nick_col'] - long_string = "%s%s%s: %s" % ( - long_string, - sep, - self.santize_string(col), - self.santize_string(line["nick"]), - self.santize_string(line["content"])) - sep = '\n' - if len(long_string) == 0: - return - layout = self.create_pango_layout(long_string) - layout.set_markup(long_string, -1) - attr = Pango.AttrList() + cy = h + for line in reversed(self.content): + out_line = "" + self.imgList = [] + + col = "#fff" + if 'nick_col' in line and line['nick_col']: + col = line['nick_col'] + for in_line in line['content']: + out_line = "%s%s" % (out_line, self.make_line(in_line)) + if line['attach'] and self.show_attach: + at = line['attach'][0] + url = at['url'] + if url in self.attachment: + cy = self.draw_attach(cy, url) + else: + get_surface(self.recv_attach, + url, + url, None) + # cy = self.draw_text(cy, "%s" % (line['attach'])) + cy = self.draw_text(cy, "%s: %s" % (self.santize_string(col), + self.santize_string(line["nick"]), out_line)) + if cy <= 0: + # We've done enough + break + + def draw_attach(self, y, url): + (w, h) = self.get_size() + if self.attachment[url]: + pix = self.attachment[url] + h = pix.get_height() + self.col(self.bg_col) + self.context.rectangle(0, y - h, w, h) + self.context.fill() + self.context.set_operator(cairo.OPERATOR_OVER) + self.col([1, 1, 1, 1]) + self.context.set_source_surface(pix, 0, y - h) + self.context.rectangle(0, y - h, w, h) + self.context.fill() + # Gdk.cairo_set_source_pixbuf(self.context, pix, 0, y - h) + # self.context.paint() + return y - h + return y + + def draw_text(self, y, text): + (w, h) = self.get_size() + + layout = self.create_pango_layout(text) + layout.set_markup(text, -1) + attr = layout.get_attributes() layout.set_width(Pango.SCALE * w) layout.set_spacing(Pango.SCALE * 3) @@ -87,16 +186,62 @@ class TextOverlayWindow(OverlayWindow): "%s %s" % (self.text_font, self.text_size)) layout.set_font_description(font) tw, th = layout.get_pixel_size() - - # Don't layer drawing over each other, always replace self.col(self.bg_col) - context.rectangle(0, -th + h, w, th) - context.fill() - context.set_operator(cairo.OPERATOR_OVER) + self.context.rectangle(0, y - th, w, th) + self.context.fill() + self.context.set_operator(cairo.OPERATOR_OVER) self.col(self.fg_col) - context.move_to(0, -th + h) - PangoCairo.show_layout(context, layout) + self.context.move_to(0, y - th) + PangoCairo.context_set_shape_renderer( + self.get_pango_context(), self.render_custom, None) + + text = layout.get_text() + count = 0 + + for loc in self.imgFinder.finditer(text): + idx = loc.start() + + if len(self.imgList) <= count: + break # We fucked up. Who types ` anyway + url = self.imgList[count] + + at = Pango.attr_shape_new_with_data( + self.pango_rect, self.pango_rect, count, None) + at.start_index = idx + at.end_index = idx + 1 + attr.insert(at) + count += 1 + layout.set_attributes(attr) + + PangoCairo.show_layout(self.context, layout) + return y - th + + def render_custom(self, ctx, shape, path, data): + key = self.imgList[shape.data]['url'] + if key not in self.attachment: + get_surface(self.recv_attach, + key, + key, None) + return + pix = self.attachment[key] + (x, y) = ctx.get_current_point() + px = pix.get_width() + py = pix.get_height() + ctx.save() + ctx.translate(x, y - self.text_size) + ctx.scale(self.text_size, self.text_size) + ctx.scale(1 / px, 1 / py) + ctx.set_source_surface(pix, 0, 0) + + ctx.rectangle(0, 0, px, py) + if not path: + ctx.fill() + ctx.restore() + + ctx.move_to(x + self.text_size, y) + + return True def santize_string(self, string): # I hate that Pango has nothing for this. diff --git a/discover_overlay/text_settings.py b/discover_overlay/text_settings.py index 7df0a5f..157f8ab 100644 --- a/discover_overlay/text_settings.py +++ b/discover_overlay/text_settings.py @@ -109,6 +109,8 @@ class TextSettingsWindow(SettingsWindow): self.popup_style = config.getboolean( "text", "popup_style", fallback=False) self.text_time = config.getint("text", "text_time", fallback=30) + self.show_attach = config.getboolean( + "text", "show_attach", fallback=True) logging.info( "Loading saved channel %s" % (self.channel)) @@ -124,6 +126,7 @@ class TextSettingsWindow(SettingsWindow): self.overlay.set_fg(self.fg_col) self.overlay.set_popup_style(self.popup_style) self.overlay.set_text_time(self.text_time) + self.overlay.set_show_attach(self.show_attach) def save_config(self): config = ConfigParser(interpolation=None) @@ -145,6 +148,7 @@ class TextSettingsWindow(SettingsWindow): config.set("text", "fg_col", json.dumps(self.fg_col)) config.set("text", "popup_style", "%s" % (int(self.popup_style))) config.set("text", "text_time", "%s" % (int(self.text_time))) + config.set("text", "show_attach", "%s" % (int(self.show_attach))) if self.font: config.set("text", "font", self.font) @@ -255,6 +259,12 @@ class TextSettingsWindow(SettingsWindow): channel.add_attribute(rt, "text", 0) channel.add_attribute(rt, 'sensitive', 1) + # Show Attachments + show_attach_label = Gtk.Label.new("Show Attachments") + show_attach = Gtk.CheckButton.new() + show_attach.set_active(self.show_attach) + show_attach.connect("toggled", self.change_show_attach) + self.align_x_widget = align_x self.align_y_widget = align_y self.align_monitor_widget = monitor @@ -283,6 +293,8 @@ class TextSettingsWindow(SettingsWindow): box.attach(align_x, 1, 9, 1, 1) box.attach(align_y, 1, 10, 1, 1) box.attach(align_placement_button, 1, 11, 1, 1) + box.attach(show_attach_label, 0, 12, 1, 1) + box.attach(show_attach, 1, 12, 1, 1) self.add(box) @@ -413,3 +425,9 @@ class TextSettingsWindow(SettingsWindow): self.fg_col = c self.save_config() + + def change_show_attach(self, button): + self.overlay.set_show_attach(button.get_active()) + + self.show_attach = button.get_active() + self.save_config() diff --git a/discover_overlay/voice_overlay.py b/discover_overlay/voice_overlay.py index 13d1f66..8591965 100644 --- a/discover_overlay/voice_overlay.py +++ b/discover_overlay/voice_overlay.py @@ -104,9 +104,9 @@ class VoiceOverlayWindow(OverlayWindow): def reset_avatar(self): self.avatars = {} - self.def_avatar = get_image(self.recv_avatar, - "https://cdn.discordapp.com/embed/avatars/3.png", - 'def', self.avatar_size) + get_image(self.recv_avatar, + "https://cdn.discordapp.com/embed/avatars/3.png", + 'def', self.avatar_size) def set_user_list(self, userlist, alt): self.userlist = userlist @@ -263,7 +263,7 @@ class VoiceOverlayWindow(OverlayWindow): def draw_avatar_pix(self, context, pixbuf, x, y, c, alpha): if not pixbuf: pixbuf = self.def_avatar - + if not pixbuf: return context.move_to(x, y)