- 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:
parent
287f5c2872
commit
645fc7e9c7
5 changed files with 271 additions and 48 deletions
|
|
@ -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"]]:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue