979 lines
34 KiB
Python
Executable file
979 lines
34 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import gi
|
|
import os
|
|
import sys
|
|
import json
|
|
import math
|
|
import time
|
|
import cairo
|
|
import base64
|
|
import select
|
|
import urllib
|
|
import requests
|
|
import websocket
|
|
|
|
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
|
|
last_connection=""
|
|
|
|
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 not channel:
|
|
current_voice="0"
|
|
return
|
|
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, last_connection
|
|
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"])
|
|
if j["data"]["user_id"] in userlist:
|
|
userlist[j["data"]["user_id"]]["speaking"] = False
|
|
set_in_room(j["data"]["user_id"],True)
|
|
elif j["evt"] == "VOICE_CHANNEL_SELECT":
|
|
set_channel(ws, j["data"]["channel_id"])
|
|
elif j["evt"] == "VOICE_CONNECTION_STATUS":
|
|
# VOICE_CONNECTED > CONNECTING > AWAITING_ENDPOINT > DISCONNECTED
|
|
last_connection = j["data"]["state"]
|
|
else:
|
|
print(j)
|
|
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":
|
|
if j["data"]["evt"] == "SPEAKING_STOP" or j["data"]["evt"] == "SPEAKING_START" or j["data"]["evt"] == "VOICE_STATE_CREATE" or j["data"]["evt"] == "VOICE_STATE_UPDATE" or j["data"]["evt"] == "VOICE_STATE_DELETE" or j["data"]["evt"]=="VOICE_CHANNEL_SELECT" or j["data"]["evt"]=="VOICE_CONNECTION_STATUS" or j["data"]["evt"] == "VOICE_SETTINGS_UPDATE":
|
|
return
|
|
print(j)
|
|
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"]
|
|
return
|
|
print(j)
|
|
|
|
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, ws
|
|
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))
|
|
sub_server(ws)
|
|
|
|
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_raw(ws, cmd, channel, nonce):
|
|
ws.send("{\"cmd\":\"SUBSCRIBE\",\"args\":{%s},\"evt\":\"%s\",\"nonce\":\"%s\"}" % (channel, cmd, nonce))
|
|
|
|
def sub_server(ws):
|
|
# Experimental
|
|
sub_raw(ws,"VOICE_CHANNEL_SELECT", "", "VOICE_CHANNEL_SELECT")
|
|
sub_raw(ws,"VOICE_CONNECTION_STATUS", "", "VOICE_CONNECTION_STATUS")
|
|
#sub_raw(ws,"ACTIVITY_JOIN", "","ACTIVITY_JOIN")
|
|
#sub_raw(ws,"ACTIVITY_JOIN_REQUEST", "","ACTIVITY_JOIN_REQUEST")
|
|
#sub_raw(ws,"ACTIVITY_SPECTATE", "", "ACTIVITY_SPECTATE")
|
|
#sub_raw(ws,"ACTIVITY_INVITE","","ACTIVITY_INVITE")
|
|
#sub_raw(ws,"GAME_JOIN", "", "GAME_JOIN")
|
|
#sub_raw(ws,"GAME_SPECTATE", "", "GAME_SPECTATE")
|
|
sub_raw(ws,"VOICE_SETTINGS_UPDATE", "", "VOICE_SETTINGS_UPDATE")
|
|
#sub_raw(ws,"GUILD_STATUS", "\"guild_id\":\"147073008450666496\"", "GUILD_STATUS")
|
|
|
|
def sub_channel(ws, cmd, channel):
|
|
sub_raw(ws,cmd,"\"channel_id\":\"%s\""%(channel),channel)
|
|
|
|
def sub_voice_channel(ws, channel):
|
|
sub_channel(ws,"VOICE_STATE_CREATE", channel)
|
|
sub_channel(ws,"VOICE_STATE_UPDATE", channel)
|
|
sub_channel(ws,"VOICE_STATE_DELETE", channel)
|
|
sub_channel(ws,"SPEAKING_START", channel)
|
|
sub_channel(ws,"SPEAKING_STOP", 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)
|
|
win.set_connection(last_connection)
|
|
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 get_monitor_index(self, name):
|
|
display = Gdk.Display.get_default()
|
|
for i in range(0, display.get_n_monitors()):
|
|
if display.get_monitor(i).get_model() == name:
|
|
return i
|
|
print("Could not find monitor : %s" % (name))
|
|
return 0
|
|
|
|
|
|
|
|
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)
|
|
self.monitor = self.config.get("main", "monitor", fallback="None")
|
|
|
|
# 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)
|
|
self.overlay.set_monitor(self.get_monitor_index(self.monitor))
|
|
|
|
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)))
|
|
self.config.set("main","monitor",self.monitor)
|
|
|
|
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)
|
|
|
|
# Monitor & Alignment
|
|
align_label = Gtk.Label.new("Overlay Location")
|
|
|
|
monitor_store = Gtk.ListStore(str)
|
|
display = Gdk.Display.get_default()
|
|
for i in range(0, display.get_n_monitors()):
|
|
monitor_store.append([display.get_monitor(i).get_model()])
|
|
monitor = Gtk.ComboBox.new_with_model(monitor_store)
|
|
monitor.set_active(self.get_monitor_index(self.monitor))
|
|
monitor.connect("changed", self.change_monitor)
|
|
rt = Gtk.CellRendererText()
|
|
monitor.pack_start(rt,True)
|
|
monitor.add_attribute(rt, "text", 0)
|
|
|
|
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,3)
|
|
box.attach(monitor,1,6,1,1)
|
|
box.attach(align_x,1,7,1,1)
|
|
box.attach(align_y,1,8,1,1)
|
|
box.attach(icon_spacing_label,0,9,1,1)
|
|
box.attach(icon_spacing,1,9,1,1)
|
|
box.attach(text_padding_label,0,10,1,1)
|
|
box.attach(text_padding,1,10,1,1)
|
|
box.attach(square_avatar_label,0,11,1,1)
|
|
box.attach(square_avatar,1,11,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_monitor(self, button):
|
|
display = Gdk.Display.get_default()
|
|
mon = display.get_monitor(button.get_active())
|
|
m_s = mon.get_model()
|
|
self.overlay.set_monitor(button.get_active())
|
|
|
|
self.monitor = m_s
|
|
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.monitor = 0
|
|
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.connected=False
|
|
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_monitor(self, idx):
|
|
self.monitor = idx
|
|
self.force_location()
|
|
|
|
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()
|
|
monitor = display.get_monitor(self.monitor)
|
|
geometry = monitor.get_geometry()
|
|
scale_factor = monitor.get_scale_factor()
|
|
w = scale_factor * geometry.width
|
|
h = scale_factor * geometry.height
|
|
x = geometry.x
|
|
y = geometry.y
|
|
self.resize(400, h)
|
|
if self.align_right:
|
|
self.move(x+w-400,y+0)
|
|
else:
|
|
self.move(x,y)
|
|
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 set_connection(self, connection):
|
|
is_connected = connection == "VOICE_CONNECTED"
|
|
if self.connected != is_connected:
|
|
self.connected = is_connected
|
|
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)
|
|
if not self.connected:
|
|
return
|
|
|
|
# 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 and user["avatar"]:
|
|
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
|
|
pix = None
|
|
if user["id"] in self.avatars:
|
|
pix = self.avatars[user["id"]]
|
|
if self.align_right:
|
|
self.draw_text(context, user["username"],w-self.avatar_size,y)
|
|
self.draw_avatar_pix(context, pix,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, pix,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 pixbuf:
|
|
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)
|
|
|
|
try:
|
|
Gtk.main()
|
|
except:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ws=None
|
|
win=None
|
|
box=None
|
|
tray=None
|
|
settings=None
|
|
main()
|