- Added rich text (bold/italic/underline) fixes #59

- Added option to show attachments fixes #57
- Include in-line custom emoji (why god why)
This commit is contained in:
trigg 2020-10-02 22:51:08 +01:00
parent 287f5c2872
commit 645fc7e9c7
5 changed files with 271 additions and 48 deletions

View file

@ -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"]]:

View file

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

View file

@ -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 = "<b>%s</b>" % (self.make_line(msg['content']))
elif msg['type'] == 'text':
ret = self.santize_string(msg['content'])
elif msg['type'] == 'link':
ret = "<u>%s</u>" % (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 = "<span font_family=\"monospace\" background=\"#0004\">%s</span>" % (
msg['content'])
elif msg['type'] == 'u':
ret = "<u>%s</u>" % (self.make_line(msg['content']))
elif msg['type'] == 'em':
ret = "<i>%s</i>" % (self.make_line(msg['content']))
elif msg['type'] == 's':
ret = "<s>%s</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<span foreground='%s'>%s</span>: %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, "<span foreground='%s'>%s</span>: %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.

View file

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

View file

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