From 220c2e634ee0f1f901c867c7839b695786e2d845 Mon Sep 17 00:00:00 2001 From: trigg Date: Wed, 23 Sep 2020 15:21:22 +0100 Subject: [PATCH] - Initial code commit --- discover.py | 883 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100755 discover.py diff --git a/discover.py b/discover.py new file mode 100755 index 0000000..01ede24 --- /dev/null +++ b/discover.py @@ -0,0 +1,883 @@ +#!/usr/bin/python3 + +import websocket +import json +import requests +import time +import sys +import gi +import select +import urllib +import cairo +import math +import base64 +import os + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Gio, GdkPixbuf, Gdk, Pango +from gi.repository.GdkPixbuf import Pixbuf +from configparser import ConfigParser +from xdg.BaseDirectory import xdg_config_home + +access_token = "none" +# TODO Magic number +oauth_token = "207646673902501888" +access_delay = 0 + +guilds = {} +channels = {} +user = {} +userlist = {} +in_room= [] +current_voice = "0" +list_altered = False + +def get_access_token_stage1(ws): + global oauth_token + ws.send("{\"cmd\":\"AUTHORIZE\",\"args\":{\"client_id\":\"%s\",\"scopes\":[\"rpc\",\"messages.read\"],\"prompt\":\"none\"},\"nonce\":\"deadbeef\"}" % (oauth_token)) + +def get_access_token_stage2(ws, code1): + global access_token, access_delay + time.sleep(access_delay) + access_delay+=1 + if access_delay > 5: + access_delay = 5 + url = "https://streamkit.discord.com/overlay/token" + myobj = {"code" : code1} + x = requests.post(url, json=myobj) + try: + j = json.loads(x.text) + except JSONDecodeError: + j = {} + if "access_token" in j: + access_token = j["access_token"] + req_auth(ws) + else: + sys.exit(1) + +def set_channel(ws,channel): + global current_voice, channels + if channel != current_voice: + cn = channels[channel]['name'] + current_voice = channel + req_channel_details(ws, channel) + +def set_in_room(userid, present): + global in_room + if present: + if userid not in in_room: + in_room.append(userid) + else: + if userid in in_room: + in_room.remove(userid) + +def update_user(user): + global userlist + if user["id"] in userlist: + if not "mute" in user and "mute" in userlist[user["id"]]: + user["mute"] = userlist[user["id"]]["mute"] + if not "deaf" in user and "deaf" in userlist[user["id"]]: + user["deaf"] = userlist[user["id"]]["deaf"] + if not "speaking" in user and "speaking" in userlist[user["id"]]: + user["speaking"] = userlist[user["id"]]["speaking"] + userlist[user["id"]]=user + +def on_message(ws, message): + global guilds, user, access_delay, channels, userlist, current_voice,list_altered,in_room + j = json.loads(message) + if j["cmd"] == "AUTHORIZE": + get_access_token_stage2(ws,j["data"]["code"]) + return + elif j["cmd"] == "DISPATCH": + if j["evt"] == "READY": + req_auth(ws) + elif j["evt"] == "VOICE_STATE_UPDATE": + list_altered=True + thisuser = j["data"]["user"] + un=j["data"]["user"]["username"] + mute = j["data"]["voice_state"]["mute"] or j["data"]["voice_state"]["self_mute"] + deaf = j["data"]["voice_state"]["deaf"] or j["data"]["voice_state"]["self_deaf"] + thisuser["mute"]=mute + thisuser["deaf"]=deaf + update_user(thisuser) + elif j["evt"] == "VOICE_STATE_CREATE": + list_altered=True + update_user(j["data"]["user"]) + # If someone joins any voice room grab it fresh from server + req_channel_details(ws,current_voice) + un=j["data"]["user"]["username"] + elif j["evt"] == "VOICE_STATE_DELETE": + list_altered=True + set_in_room(j["data"]["user"]["id"], False) + if j["data"]["user"]["id"] == user["id"]: + in_room=[] + sub_all_voice(ws) + else: + un = j["data"]["user"]["username"] + elif j["evt"] == "SPEAKING_START": + list_altered=True + # It's only possible to get alerts for the room you're in + set_channel(ws,j["data"]["channel_id"]) + userlist[j["data"]["user_id"]]["speaking"] = True + set_in_room(j["data"]["user_id"],True) + elif j["evt"] == "SPEAKING_STOP": + list_altered=True + # It's only possible to get alerts for the room you're in + set_channel(ws,j["data"]["channel_id"]) + userlist[j["data"]["user_id"]]["speaking"] = False + set_in_room(j["data"]["user_id"],True) + return + elif j["cmd"] == "AUTHENTICATE": + if j["evt"] == "ERROR": + get_access_token_stage1(ws) + return + else: + req_guilds(ws) + user=j["data"]["user"] + print("Logged in as %s" % (user["username"])) + return + elif j["cmd"] == "GET_GUILDS": + for guild in j["data"]["guilds"]: + req_channels(ws, guild["id"]) + guilds[guild["id"]]=guild + return + elif j["cmd"] == "GET_CHANNELS": + guilds[j['nonce']]["channels"] = j["data"]["channels"] + for channel in j["data"]["channels"]: + channels[channel["id"]] = channel + if channel["type"] == 2: + req_channel_details(ws, channel["id"]) + check_guilds() + sub_all_voice_guild(ws,j["nonce"]) + return + elif j["cmd"] == "SUBSCRIBE": + return + elif j["cmd"] == "GET_CHANNEL": + if j["evt"] == "ERROR": + print("Could not get room") + elif j["data"]["id"] == current_voice: + list_altered=True + in_room=[] + for voice in j["data"]["voice_states"]: + update_user(voice["user"]) + set_in_room(voice["user"]["id"], True) + if voice["user"]["id"] == user["id"]: + current_channel = j["data"]["id"] + +def check_guilds(): + global guilds + # Check if all of the guilds contain a channel + for guild in guilds.values(): + if "channels" not in guild: + return + # All guilds are filled! + on_connected() + +def on_connected(): + global guilds + for guild in guilds.values(): + channels = "" + for channel in guild["channels"]: + if channel["type"] == 2: + channels = channels+" "+channel["name"] + print("%s: %s" % (guild["name"], channels)) + +def on_error(ws, error): + print("ERROR : %s" % (error)) + +def on_close(): + global ws + print("Connection closed") + ws = None + +def req_auth(ws): + ws.send("{\"cmd\":\"AUTHENTICATE\",\"args\":{\"access_token\":\"%s\"},\"nonce\":\"deadbeef\"}" % (access_token)) + +def req_guilds(ws): + ws.send("{\"cmd\":\"GET_GUILDS\",\"args\":{},\"nonce\":\"3333\"}") + +def req_channels(ws, guild): + ws.send("{\"cmd\":\"GET_CHANNELS\",\"args\":{\"guild_id\":\"%s\"},\"nonce\":\"%s\"}" % (guild, guild)) + +def req_channel_details(ws, channel): + ws.send("{\"cmd\":\"GET_CHANNEL\",\"args\":{\"channel_id\":\"%s\"},\"nonce\":\"%s\"}" % (channel, channel)) + +def find_user(ws): + global channels + for channel in channels: + req_channel_details(ws, channel) + +def sub_voice_channel(ws, channel): + ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{\"channel_id\":\"%s\"},\"evt\":\"VOICE_STATE_CREATE\",\"nonce\":\"deadbeef\"}" % (channel)) + ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{\"channel_id\":\"%s\"},\"evt\":\"VOICE_STATE_UPDATE\",\"nonce\":\"deadbeef\"}" % (channel)) + ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{\"channel_id\":\"%s\"},\"evt\":\"VOICE_STATE_DELETE\",\"nonce\":\"deadbeef\"}" % (channel)) + ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{\"channel_id\":\"%s\"},\"evt\":\"SPEAKING_START\",\"nonce\":\"deadbeef\"}" % (channel)) + ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{\"channel_id\":\"%s\"},\"evt\":\"SPEAKING_STOP\",\"nonce\":\"deadbeef\"}" % (channel)) + +def sub_all_voice_guild(ws, gid): + global guilds + for channel in guilds[gid]["channels"]: + sub_voice_channel(ws, channel["id"]) + +def sub_all_voice(ws): + for guild in guilds: + sub_all_voice_guild(ws, guild) + +def do_read(): + global ws, win, userlist, list_altered + if not ws: + # Reconnect if needed + connect() + return True + # Recreate a list of users in current room + newlist = [] + for userid in in_room: + newlist.append(userlist[userid]) + win.set_user_list(newlist, list_altered) + list_altered=False + + # Poll socket for new information + r,w,e=select.select((ws.sock,),(),(),0) + while r: + try: + # Recieve & send to on_message + a = ws.recv() + on_message(ws, a) + r,w,e=select.select((ws.sock,),(),(),0) + except websocket._exceptions.WebSocketConnectionClosedException: + on_close() + return True + return True + +class SettingsWindow(Gtk.Window): + def __init__(self, overlay): + Gtk.Window.__init__(self) + self.overlay = overlay + self.set_size_request(400,200) + + # Find config file + self.configDir = os.path.join(xdg_config_home, "discover") + os.makedirs(self.configDir, exist_ok=True) + self.configFile = os.path.join(self.configDir, "discover.ini") + self.config = ConfigParser(interpolation=None) + self.config.read(self.configFile) + + self.read_config() + + self.create_gui() + + def read_config(self): + self.align_x = self.config.getboolean("main", "rightalign", fallback=True) + self.align_y = self.config.getint("main", "topalign", fallback=1) + self.bg_col = json.loads(self.config.get("main","bg_col",fallback="[0.0,0.0,0.0,0.5]")) + self.fg_col = json.loads(self.config.get("main","fg_col",fallback="[1.0,1.0,1.0,1.0]")) + self.tk_col = json.loads(self.config.get("main","tk_col",fallback="[0.0,0.7,0.0,1.0]")) + self.mt_col = json.loads(self.config.get("main","mt_col",fallback="[0.6,0.0,0.0,1.0]")) + self.avatar_size = self.config.getint("main","avatar_size", fallback=48) + self.icon_spacing = self.config.getint("main","icon_spacing", fallback=8) + self.text_padding = self.config.getint("main","text_padding", fallback=6) + self.font = self.config.get("main","font",fallback=None) + self.square_avatar = self.config.getboolean("main","square_avatar", fallback=False) + + # Pass all of our config over to the overlay + self.overlay.set_align_x(self.align_x) + self.overlay.set_align_y(self.align_y) + self.overlay.set_bg(self.bg_col) + self.overlay.set_fg(self.fg_col) + self.overlay.set_tk(self.tk_col) + self.overlay.set_mt(self.mt_col) + self.overlay.set_avatar_size(self.avatar_size) + self.overlay.set_icon_spacing(self.icon_spacing) + self.overlay.set_text_padding(self.text_padding) + self.overlay.set_square_avatar(self.square_avatar) + + if self.font: + desc = Pango.FontDescription.from_string(self.font) + s = desc.get_size() + if not desc.get_size_is_absolute(): + s = s / Pango.SCALE + self.overlay.set_font(desc.get_family(), s) + + + def save_config(self): + if not self.config.has_section("main"): + self.config.add_section("main") + + self.config.set("main","rightalign", "%d" % (int(self.align_x))) + self.config.set("main","topalign", "%d" % (self.align_y)) + self.config.set("main","bg_col",json.dumps(self.bg_col)) + self.config.set("main","fg_col",json.dumps(self.fg_col)) + self.config.set("main","tk_col",json.dumps(self.tk_col)) + self.config.set("main","mt_col",json.dumps(self.mt_col)) + self.config.set("main","avatar_size", "%d" % (self.avatar_size)) + self.config.set("main","icon_spacing", "%d" % (self.icon_spacing)) + self.config.set("main","text_padding", "%d" % (self.text_padding)) + if self.font: + self.config.set("main","font",self.font) + self.config.set("main","square_avatar","%d"%(int(self.square_avatar))) + + with open(self.configFile, 'w') as file: + self.config.write(file) + + def create_gui(self): + box = Gtk.Grid() + + # Font chooser + font_label = Gtk.Label.new("Font") + font = Gtk.FontButton() + if self.font: + font.set_font(self.font) + font.connect("font-set", self.change_font) + + # Colours + bg_col_label = Gtk.Label.new("Background colour") + bg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.bg_col[0],self.bg_col[1],self.bg_col[2],self.bg_col[3])) + fg_col_label = Gtk.Label.new("Text colour") + fg_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.fg_col[0],self.fg_col[1],self.fg_col[2],self.fg_col[3])) + tk_col_label = Gtk.Label.new("Talk colour") + tk_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.tk_col[0],self.tk_col[1],self.tk_col[2],self.tk_col[3])) + mt_col_label = Gtk.Label.new("Mute colour") + mt_col = Gtk.ColorButton.new_with_rgba(Gdk.RGBA(self.mt_col[0],self.mt_col[1],self.mt_col[2],self.mt_col[3])) + bg_col.set_use_alpha(True) + fg_col.set_use_alpha(True) + tk_col.set_use_alpha(True) + mt_col.set_use_alpha(True) + bg_col.connect("color-set", self.change_bg) + fg_col.connect("color-set", self.change_fg) + tk_col.connect("color-set", self.change_tk) + mt_col.connect("color-set", self.change_mt) + + # Avatar size + avatar_size_label = Gtk.Label.new("Avatar size") + avatar_adjustment = Gtk.Adjustment.new(self.avatar_size,8,128,2,8,8) + avatar_size = Gtk.SpinButton.new(avatar_adjustment,0,0) + avatar_size.connect("value-changed", self.change_avatar_size) + + # Alignment + align_label = Gtk.Label.new("Overlay Location") + align_x_store = Gtk.ListStore(str) + align_x_store.append(["Left"]) + align_x_store.append(["Right"]) + align_x = Gtk.ComboBox.new_with_model(align_x_store) + align_x.set_active(True if self.align_x else False) + align_x.connect("changed", self.change_align_x) + rt = Gtk.CellRendererText() + align_x.pack_start(rt, True) + align_x.add_attribute(rt,"text",0) + + align_y_store = Gtk.ListStore(str) + align_y_store.append(["Top"]) + align_y_store.append(["Middle"]) + align_y_store.append(["Bottom"]) + align_y = Gtk.ComboBox.new_with_model(align_y_store) + align_y.set_active(self.align_y) + align_y.connect("changed", self.change_align_y) + rt = Gtk.CellRendererText() + align_y.pack_start(rt, True) + align_y.add_attribute(rt,"text",0) + + # Icon spacing + icon_spacing_label = Gtk.Label.new("Icon Spacing") + icon_spacing_adjustment = Gtk.Adjustment.new(self.icon_spacing,0,64,2,8,8) + icon_spacing = Gtk.SpinButton.new(icon_spacing_adjustment,0,0) + icon_spacing.connect("value-changed", self.change_icon_spacing) + + # Text padding + text_padding_label = Gtk.Label.new("Text Padding") + text_padding_adjustment = Gtk.Adjustment.new(self.text_padding,0,64,1,8,8) + text_padding = Gtk.SpinButton.new(text_padding_adjustment,0,0) + text_padding.connect("value-changed", self.change_text_padding) + + # Avatar shape + square_avatar_label = Gtk.Label.new("Square Avatar") + square_avatar = Gtk.CheckButton.new() + square_avatar.set_active(self.square_avatar) + square_avatar.connect("toggled", self.change_square_avatar) + + box.attach(font_label,0,0,1,1) + box.attach(font,1,0,1,1) + box.attach(bg_col_label,0,1,1,1) + box.attach(bg_col,1,1,1,1) + box.attach(fg_col_label,0,2,1,1) + box.attach(fg_col,1,2,1,1) + box.attach(tk_col_label,0,3,1,1) + box.attach(tk_col,1,3,1,1) + box.attach(mt_col_label,0,4,1,1) + box.attach(mt_col,1,4,1,1) + box.attach(avatar_size_label,0,5,1,1) + box.attach(avatar_size,1,5,1,1) + box.attach(align_label,0,6,1,2) + box.attach(align_x,1,6,1,1) + box.attach(align_y,1,7,1,1) + box.attach(icon_spacing_label,0,8,1,1) + box.attach(icon_spacing,1,8,1,1) + box.attach(text_padding_label,0,9,1,1) + box.attach(text_padding,1,9,1,1) + box.attach(square_avatar_label,0,10,1,1) + box.attach(square_avatar,1,10,1,1) + + self.add(box) + + pass + + def change_font(self, button): + font = button.get_font() + desc = Pango.FontDescription.from_string(font) + s = desc.get_size() + if not desc.get_size_is_absolute(): + s = s / Pango.SCALE + self.overlay.set_font(desc.get_family(), s) + + self.font = desc.to_string() + self.save_config() + + def change_bg(self, button): + c= button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_bg(c) + + self.bg_col = c + self.save_config() + + def change_fg(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_fg(c) + + self.fg_col = c + self.save_config() + + def change_tk(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_tk(c) + + self.tk_col = c + self.save_config() + + def change_mt(self, button): + c = button.get_rgba() + c = [c.red, c.green, c.blue, c.alpha] + self.overlay.set_mt(c) + + self.mt_col = c + self.save_config() + + def change_avatar_size(self, button): + self.overlay.set_avatar_size(button.get_value()) + + self.avatar_size = button.get_value() + self.save_config() + + def change_align_x(self, button): + self.overlay.set_align_x(button.get_active()==1) + + self.align_x = (button.get_active()==1) + self.save_config() + + def change_align_y(self, button): + self.overlay.set_align_y(button.get_active()) + + self.align_y = button.get_active() + self.save_config() + + def change_icon_spacing(self, button): + self.overlay.set_icon_spacing(button.get_value()) + + self.icon_spacing = int(button.get_value()) + self.save_config() + + def change_text_padding(self, button): + self.overlay.set_text_padding(button.get_value()) + + self.text_padding = button.get_value() + self.save_config() + + def change_square_avatar(self, button): + self.overlay.set_square_avatar(button.get_active()) + + self.square_avatar = button.get_active() + self.save_config() + + +class OverlayWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) + + self.set_size_request(400, 220) + + self.connect('draw', self.draw) + + # Set RGBA + screen = self.get_screen() + visual = screen.get_rgba_visual() + if visual and screen.is_composited(): + self.set_visual(visual) + else: + print("REQUIRES COMPOSITING") + sys.exit(1) + + self.set_app_paintable(True) + + self.show_all() + + self.avatars = {} + + self.avatar_size=48 + self.align_right=True + self.align_vert=1 + self.text_pad=6 + self.text_font=None + self.text_size=13 + self.icon_spacing=8 + + self.round_avatar=True + self.talk_col = [0.0,0.6,0.0,0.1] + self.text_col = [1.0,1.0,1.0,1.0] + self.norm_col = [0.0,0.0,0.0,0.5] + self.wind_col = [0.0,0.0,0.0,0.0] + self.mute_col = [0.7,0.0,0.0,1.0] + self.userlist=[] + self.set_untouchable() + self.force_location() + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + + def set_font(self, name, size): + self.text_font=name + self.text_size=size + self.queue_draw() + + def set_bg(self, bg): + self.norm_col = bg + self.queue_draw() + + def set_fg(self, fg): + self.text_col = fg + self.queue_draw() + + def set_tk(self, tk): + self.talk_col = tk + self.queue_draw() + + def set_mt(self, mt): + self.mute_col = mt + self.queue_draw() + + def set_avatar_size(self, size): + self.avatar_size=size + self.reset_avatar() + self.queue_draw() + + def set_align_x(self, b): + self.align_right = b + self.force_location() + + def set_align_y(self, i): + self.align_vert = i + self.force_location() + + def set_icon_spacing(self, i): + self.icon_spacing = i + self.queue_draw() + + def set_text_padding(self, i): + self.text_pad = i + self.queue_draw() + + def set_square_avatar(self, i): + self.round_avatar = not i + self.queue_draw() + + def set_untouchable(self): + (w, h) = self.get_size() + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) + surface_ctx = cairo.Context(surface) + surface_ctx.set_source_rgba(0.0,0.0,0.0,0.0) + surface_ctx.set_operator(cairo.OPERATOR_SOURCE) + surface_ctx.paint() + reg = Gdk.cairo_region_create_from_surface(surface) + self.input_shape_combine_region(reg) + + def force_location(self): + self.set_decorated(False) + self.set_keep_above(True) + display = Gdk.Display.get_default() + # TODO care about mulitmonitor + monitor = display.get_primary_monitor() + geometry = monitor.get_geometry() + scale_factor = monitor.get_scale_factor() + w = scale_factor * geometry.width + h = scale_factor * geometry.height + self.resize(400, h) + if self.align_right: + self.move(w-400, 0) + else: + self.move(0,0) + self.queue_draw() + + def col(self,c,a=1.0): + self.context.set_source_rgba(c[0],c[1],c[2],c[3]*a) + + def set_wind_col(self): + self.col(self.wind_col) + + def set_text_col(self): + self.col(self.text_col) + + def set_norm_col(self): + self.col(self.norm_col) + + def set_talk_col(self,a=1.0): + self.col(self.talk_col,a) + + def set_mute_col(self,a=1.0): + self.col(self.mute_col,a) + + def reset_avatar(self): + self.avatars = {} + + def set_user_list(self, userlist,alt): + self.userlist = userlist + self.userlist.sort(key=lambda x: x["username"]) + if alt: + self.queue_draw() + + def draw(self, widget, context): + self.context = context + + # Make background transparent + self.set_wind_col() + # Don't layer drawing over each other, always replace + context.set_operator(cairo.OPERATOR_SOURCE) + context.paint() + context.set_operator(cairo.OPERATOR_OVER) + + # Get size of window + (w,h) = self.get_size() + # Calculate height needed to show overlay + height = (len(self.userlist) * self.avatar_size) + (len(self.userlist)+1)*self.icon_spacing + + # Choose where to start drawing + rh = 0 + if self.align_vert==1: + rh = (h/2) - (height/2) + elif self.align_vert==2: + rh = h-height + # Iterate users in room. + for user in self.userlist: + self.draw_avatar(context, user, rh) + # Shift the relative position down to next location + rh+=self.avatar_size+self.icon_spacing + + # Don't hold a ref + self.context=None + + def draw_avatar(self, context, user,y): + # Ensure pixbuf for avatar + if user["id"] not in self.avatars: + url= "https://cdn.discordapp.com/avatars/%s/%s.jpg" % (user["id"], user["avatar"]) + req = urllib.request.Request(url) + req.add_header('Referer','https://streamkit.discord.com/overlay/voice') + req.add_header('User-Agent', 'Mozilla/5.0') + try: + response = urllib.request.urlopen(req) + input_stream = Gio.MemoryInputStream.new_from_data(response.read(), None) + pixbuf = Pixbuf.new_from_stream(input_stream, None) + pixbuf = pixbuf.scale_simple(self.avatar_size, self.avatar_size, + GdkPixbuf.InterpType.BILINEAR) + self.avatars[user["id"]] = pixbuf + except: + print("Could not access : %s"%(url)) + + (w,h)=self.get_size() + c = None + mute=False + alpha = 1.0 + if "speaking" in user and user["speaking"]: + c = self.talk_col + if "mute" in user and user["mute"]: + mute=True + if "deaf" in user and user["deaf"]: + alpha=0.5 + if self.align_right: + self.draw_text(context, user["username"],w-self.avatar_size,y) + self.draw_avatar_pix(context, self.avatars[user["id"]],w-self.avatar_size,y,c,alpha) + if mute: + self.draw_mute(context, w-self.avatar_size, y,alpha) + else: + self.draw_text(context, user["username"],self.avatar_size,y) + self.draw_avatar_pix(context, self.avatars[user["id"]],0,y,c,alpha) + if mute: + self.draw_mute(context, 0,y,alpha) + + def draw_text(self,context, string,x,y): + if self.text_font: + context.set_font_face(cairo.ToyFontFace(self.text_font,cairo.FontSlant.NORMAL,cairo.FontWeight.NORMAL)) + context.set_font_size(self.text_size) + xb, yb, w, h, dx, dy = context.text_extents(string) + ho = (self.avatar_size/2) - (h/2) + if self.align_right: + context.move_to(0,0) + self.set_norm_col() + context.rectangle(x-w-(self.text_pad*2),y+ho-self.text_pad,w+(self.text_pad*4),h+(self.text_pad*2)) + context.fill() + + self.set_text_col() + context.move_to(x-w-self.text_pad,y+ho+h) + context.show_text(string) + else: + context.move_to(0,0) + self.set_norm_col() + context.rectangle(x-(self.text_pad*2),y+ho-self.text_pad,w+(self.text_pad*4),h+(self.text_pad*2)) + context.fill() + + self.set_text_col() + context.move_to(x+self.text_pad,y+ho+h) + context.show_text(string) + + def draw_avatar_pix(self, context, pixbuf,x,y,c,alpha): + context.move_to(x,y) + context.save() + #context.set_source_pixbuf(pixbuf, 0.0, 0.0) + if self.round_avatar: + context.arc(x+(self.avatar_size/2), y+(self.avatar_size/2), self.avatar_size/2,0,2*math.pi) + context.clip() + self.set_wind_col() + context.set_operator(cairo.OPERATOR_SOURCE) + context.rectangle(x,y,self.avatar_size,self.avatar_size) + context.fill() + context.set_operator(cairo.OPERATOR_OVER) + Gdk.cairo_set_source_pixbuf(context,pixbuf,x,y) + context.paint_with_alpha(alpha) + context.restore() + if c: + if self.round_avatar: + context.arc(x+(self.avatar_size/2), y+(self.avatar_size/2), self.avatar_size/2, 0, 2*math.pi) + self.col(c) + context.stroke() + else: + context.rectangle(x,y,self.avatar_size,self.avatar_size) + self.col(c) + context.stroke() + + def draw_mute(self, context, x, y, a): + context.save() + context.translate(x,y) + context.scale(self.avatar_size, self.avatar_size) + self.set_mute_col(a) + context.save() + + # Clip Strike-through + context.set_fill_rule(cairo.FILL_RULE_EVEN_ODD) + context.set_line_width(0.1) + context.move_to(0.0,0.0) + context.line_to(1.0,0.0) + context.line_to(1.0,1.0) + context.line_to(0.0,1.0) + context.line_to(0.0,0.0) + context.close_path() + context.move_to(0.9 - 0.035, 0.1 - 0.035) + context.arc(0.9,0.1,0.05,1.25*math.pi, 2.25*math.pi) + context.arc(0.1,0.9,0.05,.25*math.pi,1.25*math.pi) + context.close_path() + context.clip() + + # Center + context.set_line_width(0.07) + context.arc(0.5,0.3,0.1,math.pi, 2*math.pi) + context.arc(0.5,0.5,0.1,0, math.pi) + context.close_path() + context.fill() + + context.set_line_width(0.05) + + # Stand rounded + context.arc(0.5,0.5,0.15,0, 1.0*math.pi) + context.stroke() + + # Stand vertical + context.move_to(0.5,0.65) + context.line_to(0.5,0.75) + context.stroke() + + # Stand horizontal + context.move_to(0.35,0.75) + context.line_to(0.65,0.75) + context.stroke() + + context.restore() + # Strike through + context.arc(0.7,0.3,0.035,1.25*math.pi, 2.25*math.pi) + context.arc(0.3,0.7,0.035,.25*math.pi,1.25*math.pi) + context.close_path() + context.fill() + + context.restore() + +def create_gui(): + global win, box, tray, settings, menu + win = OverlayWindow() + + # Ol' reliable + trayImgBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AUEDxsTIFcmagAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAN+SURBVFjDzZcxaCJpGIafmTuyisVgsDCJiyApR3FAokmjsT5juuC0l2BzjVyfwv5Ic43EW0ijWCb2JtMEZxFGximDMKzhLIaYKcK4zeaKM8ux5A5vN8Pmbef/53t4v3/+eT+B5fUGSAKbwAawCgQXzzzgDrgFboAR8HGZlwpLrFkD8sBWo9EQ0+k0sViMcDhMIBAAYD6fM5vNmEwmDIdDqtXqJ+A9oAF/fgvAXqFQKB4fH5PL5ZhOp1iWhWmaGIaBYRgAKIqCoiikUilkWSYajdLv96nX61xdXfWAi/8LsAao7Xb7bblcxrIsWq0WkiSRz+dJJBJEIhGCwb874HkejuMwHo/RNA3XdVFVFVmWOT8/p1KpfABaz7nxHEAC+FnX9VA8HqfZbCJJEqVSiXg8vtRhsW2bbreL67ocHh5i2zbZbPYB+AMY/xfAGvDLaDQKCYLA3t4enU6HTCbD12gwGHBwcMDFxQWPj48kk8kH4Pd/OiF+sUfVdT0kCAK1Wo1er/fVxQEymQy9Xo9arYYgCOi6HgLUf3Ngr91uF3d3d9nZ2aHX6y1t+TItKRaLXF9fc3l5SaVS+XwwnxxYKxQKxXK5TLPZpNPpvFhxgHg8TqfTodlsUi6XKRQKxUW7+WGx5qd3797F7u/vcRyH/f19Xlrr6+uYpsnKygrb29ucnZ39CFji4obbyuVytFotSqUSfqlUKtFqtcjlcgBbwBsRSDYaDXE6nSJJ0ota/1wrJEliOp3SaDREICkCm+l0GsuyyOfz+K18Po9lWaTTaYBNEdiIxWKYpkkikfAdIJFIYJomsVgMYEMEVsPhMIZhEIlEfAeIRCIYhkE4HAZYFYFgIBDAMIzPd7ufCgaDGIbx9CcNinxniYA3n89RFAXP83wv6HkeiqIwn88BPBG4m81mKIqC4zi+AziOg6IozGYzgDsRuJ1MJqRSKcbjse8A4/GYVCrFZDIBuBWBm+FwiCzLaJrmO4CmaciyzHA4BLgRgVG1Wv0UjUZxXRfbtn0rbts2rusSjUafcuNIXKTX9/1+H1VV6Xa7vgF0u11UVaXf77MIrR+fPkOtXq8jyzKu6zIYDF68+GAwwHVdZFmmXq+zSMzfP5B8mQl/1XX9bSgUolarcXp6+s0Qtm1zdHTEyckJDw8PZLPZD8BvryaUvrpY/ioGk1cxmr2a4dT38fwv9cLeiMwLuMsAAAAASUVORK5CYII=" + + # Create System Menu + menu = Gtk.Menu() + settings_opt = Gtk.MenuItem.new_with_label("Settings") + close_opt = Gtk.MenuItem.new_with_label("Close") + + menu.append(settings_opt) + menu.append(close_opt) + + settings_opt.connect("activate", show_settings) + close_opt.connect("activate", close) + + # Create System Tray + pbl = GdkPixbuf.PixbufLoader.new() + pbl.write(base64.b64decode(trayImgBase64)) + pbl.close() + image = pbl.get_pixbuf() + tray = Gtk.StatusIcon.new_from_pixbuf(image) + tray.connect('popup-menu', show_menu) + + settings = SettingsWindow(win) + +def show_menu(obj, button, time): + menu.show_all() + menu.popup(None,None,Gtk.StatusIcon.position_menu,obj,button,time) + +def show_settings(obj=None, data=None): + global settings + settings.show_all() + +def close(a=None, b=None, c=None): + Gtk.main_quit() + +def connect(): + global ws, oauth_token + if ws: + return + try: + ws = websocket.create_connection("ws://127.0.0.1:6463/?v=1&client_id=%s" % (oauth_token), + origin="https://streamkit.discord.com") + except: + pass + +def main(): + connect() + + create_gui() + win.show_all() + + GLib.timeout_add((1000/60), do_read) + + Gtk.main() + +if __name__ == "__main__": + ws=None + win=None + box=None + tray=None + settings=None + main()