From 40671d79b3d868e89f1af81a876b6185d1334bc2 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Thu, 30 Mar 2017 21:04:43 +0300 Subject: [PATCH 01/43] LC-350 Rewrote module init logic --- gui.py | 3 + main.py | 21 ++---- messaging.py | 4 +- modules/chat/goodgame.py | 48 ++++--------- modules/chat/sc2tv.py | 49 ++++--------- modules/chat/twitch.py | 51 +++++-------- modules/helper/module.py | 92 +++++++++++++++++++++--- modules/helper/parser.py | 7 +- modules/messaging/blacklist.py | 44 +++++------- modules/messaging/c2b.py | 46 ++++++------ modules/messaging/df.py | 48 ++++++------- modules/messaging/levels.py | 96 ++++++++++++------------- modules/messaging/logger.py | 58 +++++++-------- modules/messaging/mentions.py | 46 ++++++------ modules/messaging/test_module.py | 23 +----- modules/messaging/webchat.py | 118 +++++++++++++++---------------- 16 files changed, 356 insertions(+), 398 deletions(-) diff --git a/gui.py b/gui.py index 3aa59bb..fd0c850 100644 --- a/gui.py +++ b/gui.py @@ -1456,6 +1456,9 @@ def run(self): app.MainLoop() self.quit() + def apply_settings(self, **kwargs): + pass + def quit(self): try: self.gui.on_close('event') diff --git a/main.py b/main.py index 3604c2a..ae76c1a 100644 --- a/main.py +++ b/main.py @@ -91,20 +91,16 @@ def close(): 'ignored_sections': ['gui.reload'], 'non_dynamic': ['language.list_box', 'gui.*', 'system.*'] } - config = load_from_config_file(MAIN_CONF_FILE, main_config_dict) root_logger.setLevel(level=logging.getLevelName(main_config_dict['system'].get('log_level', 'INFO'))) # Adding config for main module main_class = BaseModule( conf_params={ - 'folder': CONF_FOLDER, - 'file': main_config['main_conf_file_loc'], - 'filename': main_config['main_conf_file_name'], - 'parser': config, 'root_folder': main_config['root_folder'], 'logs_folder': LOG_FOLDER, - 'config': main_config_dict, + 'config': load_from_config_file(MAIN_CONF_FILE, main_config_dict), 'gui': main_config_gui - } + }, + conf_file_name='config.cfg' ) loaded_modules['main'] = main_class.conf_params() @@ -154,16 +150,13 @@ def close(): 'check': os.path.sep.join(['modules', 'chat']), 'file_extension': False}, 'non_dynamic': ['chats.list_box']} - chat_config = load_from_config_file(chat_modules, chat_conf_dict) chat_module = BaseModule( conf_params={ - 'folder': CONF_FOLDER, 'file': chat_modules, - 'filename': ''.join(os.path.basename(chat_modules).split('.')[:-1]), - 'parser': chat_config, - 'config': chat_conf_dict, + 'config': load_from_config_file(chat_modules, chat_conf_dict), 'gui': chat_conf_gui - } + }, + conf_file_name='chat_modules.cfg' ) loaded_modules['chat'] = chat_module.conf_params() @@ -179,7 +172,7 @@ def close(): tmp = imp.load_source(chat_module, module_location) chat_init = getattr(tmp, chat_module) - class_module = chat_init(queue, PYTHON_FOLDER, + class_module = chat_init(queue=queue, conf_folder=CONF_FOLDER, conf_file=os.path.join(CONF_FOLDER, '{0}.cfg'.format(chat_module)), testing=main_config_dict['system']['testing_mode']) diff --git a/messaging.py b/messaging.py index 6033185..f1700cc 100644 --- a/messaging.py +++ b/messaging.py @@ -62,7 +62,9 @@ def load_modules(self, main_config, settings): 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), 'parser': config, 'config': conf_dict, - 'gui': conf_gui}) + 'gui': conf_gui}, + conf_file_name='messaging_modules.cfg' + ) modules_list['messaging'] = messaging_module.conf_params() diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 8b9ebef..1e8f7eb 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -363,7 +363,7 @@ def __init__(self, main_class): def run(self): while True: try: - thread = self.main_class.gg.items()[0][1] + thread = self.main_class.channels.items()[0][1] if thread.ws: self.gg_handler = thread.ws.message_handler break @@ -379,45 +379,23 @@ def send_message(self, *args, **kwargs): class goodgame(ChatModule): - def __init__(self, queue, python_folder, **kwargs): - ChatModule.__init__(self) - # Reading config from main directory. - conf_folder = os.path.join(python_folder, "conf") - + def __init__(self, *args, **kwargs): log.info("Initializing goodgame chat") - conf_file = os.path.join(conf_folder, "goodgame.cfg") - config = load_from_config_file(conf_file, CONF_DICT) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI, - 'settings': {}}) + ChatModule.__init__(self, *args, **kwargs) - self.queue = queue self.host = CONF_DICT['config']['socket'] - self.channels_list = CONF_DICT['config']['channels_list'] - self.gg = {} - - if len(self.channels_list) == 1: - if CONF_DICT['config']['show_channel_names']: - CONF_DICT['config']['show_channel_names'] = False - - self.testing = kwargs.get('testing') - if self.testing: - self.testing = TestGG(self) def load_module(self, *args, **kwargs): ChatModule.load_module(self, *args, **kwargs) if 'webchat' in self._loaded_modules: self._loaded_modules['webchat']['class'].add_depend('goodgame') self._conf_params['settings']['remove_text'] = self.get_remove_text() - # Creating new thread with queue in place for messaging transfers - for channel in self.channels_list: - self._set_chat_online(channel) - if self.testing: - self.testing.start() + + def _gui_settings(self): + return CONF_GUI + + def _test_class(self): + return TestGG(self) @staticmethod def get_viewers(channel): @@ -438,20 +416,20 @@ def get_viewers(channel): def _set_chat_offline(self, chat): ChatModule.set_chat_offline(self, chat) try: - self.gg[chat].stop() + self.channels[chat].stop() except Exception as exc: log.debug(exc) - del self.gg[chat] + del self.channels[chat] def _set_chat_online(self, chat): ChatModule.set_chat_online(self, chat) gg = GGThread(self.queue, self.host, chat, settings=self._conf_params['settings'], chat_module=self) - self.gg[chat] = gg + self.channels[chat] = gg gg.start() def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() - self._check_chats(self.gg.keys()) + self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index d305c4e..4e627d3 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -347,7 +347,7 @@ def __init__(self, main_class): def run(self): while True: try: - thread = self.main_class.fs_thread.items()[0][1] + thread = self.main_class.channels.items()[0][1] if thread.ws: self.fs_thread = thread.ws break @@ -363,41 +363,20 @@ def send_message(self, *args, **kwargs): class sc2tv(ChatModule): - def __init__(self, queue, python_folder, **kwargs): - ChatModule.__init__(self) + def __init__(self, *args, **kwargs): log.info("Initializing funstream chat") + ChatModule.__init__(self, *args, **kwargs) - # Reading config from main directory. - conf_folder = os.path.join(python_folder, "conf") - conf_file = os.path.join(conf_folder, "sc2tv.cfg") - config = load_from_config_file(conf_file, CONF_DICT) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI}) - - self.queue = queue self.socket = CONF_DICT['config']['socket'] - self.channels_list = CONF_DICT['config']['channels_list'] - self.fs_thread = {} - if len(self.channels_list) == 1: - if CONF_DICT['config']['show_channel_names']: - CONF_DICT['config']['show_channel_names'] = False + def _conf_settings(self, *args, **kwargs): + return CONF_DICT - self.testing = kwargs.get('testing') - if self.testing: - self.testing = TestSc2tv(self) + def _gui_settings(self, *args, **kwargs): + return CONF_GUI - def load_module(self, *args, **kwargs): - ChatModule.load_module(self, *args, **kwargs) - # Creating new thread with queue in place for messaging transfers - for channel in self.channels_list: - self._set_chat_online(channel) - if self.testing: - self.testing.start() + def _test_class(self): + return TestSc2tv(self) def get_viewers(self, ws): user_data = {'name': ws.channel_name} @@ -426,16 +405,16 @@ def get_viewers(self, ws): def _set_chat_offline(self, chat): ChatModule.set_chat_offline(self, chat) try: - self.fs_thread[chat].stop() + self.channels[chat].stop() except Exception as exc: log.debug(exc) - del self.fs_thread[chat] + del self.channels[chat] def _set_chat_online(self, chat): ChatModule.set_chat_online(self, chat) - self.fs_thread[chat] = FsThread(self.queue, self.socket, chat, chat_module=self) - self.fs_thread[chat].start() + self.channels[chat] = FsThread(self.queue, self.socket, chat, chat_module=self) + self.channels[chat].start() def apply_settings(self, **kwargs): - self._check_chats(self.fs_thread.keys()) + self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 6cf543a..5038c84 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -545,7 +545,7 @@ def __init__(self, main_class): def run(self): while True: try: - thread = self.main_class.tw_dict.items()[0][1] + thread = self.main_class.channels.items()[0][1] if thread.irc.twitch_queue: self.tw_queue = thread.irc.twitch_queue break @@ -563,47 +563,28 @@ def send_message(self, *args, **kwargs): class twitch(ChatModule): - def __init__(self, queue, python_folder, **kwargs): - ChatModule.__init__(self) + def __init__(self, *args, **kwargs): log.info("Initializing twitch chat") + ChatModule.__init__(self, *args, **kwargs) - # Reading config from main directory. - conf_folder = os.path.join(python_folder, "conf") - conf_file = os.path.join(conf_folder, "twitch.cfg") - - config = load_from_config_file(conf_file, CONF_DICT) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': CONF_DICT, - 'gui': CONF_GUI, - 'settings': {}}) - - self.queue = queue self.host = CONF_DICT['config']['host'] self.port = int(CONF_DICT['config']['port']) - self.channels_list = CONF_DICT['config']['channels_list'] self.bttv = CONF_DICT['config']['bttv'] - self.tw_dict = {} - if len(self.channels_list) == 1: - if CONF_DICT['config']['show_channel_names']: - CONF_DICT['config']['show_channel_names'] = False + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI - self.testing = kwargs.get('testing') - if self.testing: - self.testing = TestTwitch(self) + def _test_class(self): + return TestTwitch(self) def load_module(self, *args, **kwargs): ChatModule.load_module(self, *args, **kwargs) if 'webchat' in self._loaded_modules: self._loaded_modules['webchat']['class'].add_depend('twitch') self._conf_params['settings']['remove_text'] = self.get_remove_text() - for channel in self.channels_list: - self._set_chat_online(channel) - if self.testing: - self.testing.start() @staticmethod def get_viewers(channel): @@ -623,19 +604,19 @@ def get_viewers(channel): def _set_chat_offline(self, chat): ChatModule.set_chat_offline(self, chat) try: - self.tw_dict[chat].stop() + self.channels[chat].stop() except Exception as exc: log.debug(exc) - del self.tw_dict[chat] + del self.channels[chat] def _set_chat_online(self, chat): ChatModule.set_chat_online(self, chat) - self.tw_dict[chat] = TWThread(self.queue, self.host, self.port, chat, self.bttv, - settings=self._conf_params['settings'], chat_module=self) - self.tw_dict[chat].start() + self.channels[chat] = TWThread(self.queue, self.host, self.port, chat, self.bttv, + settings=self._conf_params['settings'], chat_module=self) + self.channels[chat].start() def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() - self._check_chats(self.tw_dict.keys()) + self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/helper/module.py b/modules/helper/module.py index b3c4344..ad2feb5 100644 --- a/modules/helper/module.py +++ b/modules/helper/module.py @@ -1,20 +1,44 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov -from parser import save_settings -from system import RestApiException +import copy +import os +from collections import OrderedDict +from parser import save_settings, load_from_config_file +from system import RestApiException, CONF_FOLDER + BASE_DICT = { 'custom_renderer': False } +CHAT_DICT = OrderedDict() +CHAT_DICT['show_channel_names'] = True +CHAT_DICT['channels_list'] = [] + class BaseModule: def __init__(self, *args, **kwargs): self._conf_params = BASE_DICT.copy() - self._conf_params.update(kwargs.get('conf_params', {})) - self._loaded_modules = None + self._conf_params['dependencies'] = set() + + self._loaded_modules = {} self._rest_api = {} self._module_name = self.__class__.__name__ self._load_queue = {} + if 'conf_file_name' in kwargs: + conf_file_name = kwargs.get('conf_file_name') + else: + conf_file_name = os.path.join(CONF_FOLDER, "{}.cfg".format(self._module_name)) + conf_file = os.path.join(CONF_FOLDER, conf_file_name) + + self._conf_params.update( + {'folder': CONF_FOLDER, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'config': load_from_config_file(conf_file, self._conf_settings()), + 'gui': self._gui_settings(), + 'settings': {}}) + + self._conf_params.update(kwargs.get('conf_params', {})) + def add_to_queue(self, q_type, data): if q_type not in self._load_queue: self._load_queue[q_type] = [] @@ -23,11 +47,31 @@ def add_to_queue(self, q_type, data): def get_queue(self, q_type): return self._load_queue.get(q_type, {}) + def add_depend(self, module_name): + self._conf_params['dependencies'].add(module_name) + + def remove_depend(self, module_name): + self._conf_params['dependencies'].discard(module_name) + def conf_params(self): params = self._conf_params params['class'] = self return params + def _conf_settings(self, *args, **kwargs): + """ + Override this method + :rtype: object + """ + return {} + + def _gui_settings(self, *args, **kwargs): + """ + Override this method + :return: Settings for GUI (dict) + """ + return {} + def load_module(self, *args, **kwargs): self._loaded_modules = kwargs.get('loaded_modules') @@ -70,21 +114,39 @@ def rest_add(self, method, path, function_to_call): class MessagingModule(BaseModule): def __init__(self, *args, **kwargs): BaseModule.__init__(self, *args, **kwargs) - self._conf_params['dependencies'] = set() def process_message(self, message, queue, **kwargs): return message - def add_depend(self, module_name): - self._conf_params['dependencies'].add(module_name) - - def remove_depend(self, module_name): - self._conf_params['dependencies'].discard(module_name) - class ChatModule(BaseModule): def __init__(self, *args, **kwargs): BaseModule.__init__(self, *args, **kwargs) + self.queue = kwargs.get('queue') + self.channels = {} + + conf_params = self._conf_params['config'] + + self.channels_list = conf_params['config']['channels_list'] + if len(self.channels_list) == 1: + if conf_params['config']['show_channel_names']: + conf_params['config']['show_channel_names'] = False + + self.testing = kwargs.get('testing') + if self.testing: + self.testing = self._test_class() + + def load_module(self, *args, **kwargs): + BaseModule.load_module(self, *args, **kwargs) + for channel in self.channels_list: + self._set_chat_online(channel) + + def _test_class(self): + """ + Override this method + :return: Chat test class (object/Class) + """ + return object() def apply_settings(self, **kwargs): BaseModule.apply_settings(self, **kwargs) @@ -97,6 +159,14 @@ def refresh_channel_names(self): if gui_class.gui.status_frame: gui_class.gui.status_frame.refresh_labels(self._module_name) + def get_viewers(self, *args, **kwargs): + """ + Overwrite this method + :param args: + :param kwargs: + """ + pass + def set_viewers(self, channel, viewers): if 'gui' in self._loaded_modules: gui_class = self._loaded_modules['gui']['class'] diff --git a/modules/helper/parser.py b/modules/helper/parser.py index 63d3e5e..a4c6566 100644 --- a/modules/helper/parser.py +++ b/modules/helper/parser.py @@ -16,14 +16,17 @@ def update(d, u): return d -def load_from_config_file(conf_file, conf_dict): +def load_from_config_file(conf_file, conf_dict={}): if not os.path.exists(conf_file): - return + return {} with open(conf_file, 'r') as conf_f: loaded_dict = yaml.safe_load(conf_f.read()) if loaded_dict: update(conf_dict, loaded_dict) + if conf_dict: + return conf_dict + def return_type(item): if item: diff --git a/modules/messaging/blacklist.py b/modules/messaging/blacklist.py index ff3960d..3e1353c 100644 --- a/modules/messaging/blacklist.py +++ b/modules/messaging/blacklist.py @@ -2,33 +2,32 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov import re -import os from collections import OrderedDict -from modules.helper.parser import load_from_config_file from modules.helper.module import MessagingModule from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 30 +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = { + 'category': 'messaging', + 'id': DEFAULT_PRIORITY} +CONF_DICT['main'] = {'message': 'ignored message'} +CONF_DICT['users_hide'] = [] +CONF_DICT['users_block'] = [] +CONF_DICT['words_hide'] = [] +CONF_DICT['words_block'] = [] + class blacklist(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - # Dwarf professions. - conf_file = os.path.join(conf_folder, "blacklist.cfg") + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) - # Ordered because order matters - conf_dict = OrderedDict() - conf_dict['gui_information'] = { - 'category': 'messaging', - 'id': DEFAULT_PRIORITY} - conf_dict['main'] = {'message': 'ignored message'} - conf_dict['users_hide'] = [] - conf_dict['users_block'] = [] - conf_dict['words_hide'] = [] - conf_dict['words_block'] = [] + def _conf_settings(self, *args, **kwargs): + return CONF_DICT - conf_gui = { + def _gui_settings(self): + return { 'words_hide': { 'addable': True, 'view': 'list'}, @@ -41,15 +40,8 @@ def __init__(self, conf_folder, **kwargs): 'users_block': { 'view': 'list', 'addable': 'true'}, - 'non_dynamic': ['main.*']} - config = load_from_config_file(conf_file, conf_dict) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': conf_dict['gui_information']['id'], - 'config': OrderedDict(conf_dict), - 'gui': conf_gui}) + 'non_dynamic': ['main.*'] + } def process_message(self, message, queue, **kwargs): if message: diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index df190d2..8ce34f5 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -2,17 +2,27 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov import logging -import os import random import re from collections import OrderedDict -from modules.helper.parser import load_from_config_file from modules.helper.module import MessagingModule from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 10 log = logging.getLogger('c2b') +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = { + 'category': 'messaging', + 'id': DEFAULT_PRIORITY} +CONF_DICT['config'] = {} + +CONF_GUI = { + 'config': { + 'addable': 'true', + 'view': 'list_dual'}, + 'non_dynamic': ['config.*']} + def twitch_replace_indexes(filter_name, text, filter_size, replace_size, emotes_list): emotes = [] @@ -35,30 +45,8 @@ def twitch_replace_indexes(filter_name, text, filter_size, replace_size, emotes_ class c2b(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - # Creating filter and replace strings. - conf_file = os.path.join(conf_folder, "c2b.cfg") - - conf_dict = OrderedDict() - conf_dict['gui_information'] = { - 'category': 'messaging', - 'id': DEFAULT_PRIORITY} - conf_dict['config'] = {} - - conf_gui = { - 'config': { - 'addable': 'true', - 'view': 'list_dual'}, - 'non_dynamic': ['config.*']} - config = load_from_config_file(conf_file, conf_dict) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': conf_dict['gui_information']['id'], - 'config': conf_dict, - 'gui': conf_gui}) + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) def process_message(self, message, queue, **kwargs): # Replacing the message if needed. @@ -76,3 +64,9 @@ def process_message(self, message, queue, **kwargs): message.get('emotes', [])) message['text'] = message['text'].replace(item, replace_word) return message + + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI diff --git a/modules/messaging/df.py b/modules/messaging/df.py index 5e6f0b7..dbe8d3d 100644 --- a/modules/messaging/df.py +++ b/modules/messaging/df.py @@ -5,38 +5,28 @@ import os from collections import OrderedDict -from modules.helper.parser import load_from_config_file from modules.helper.module import MessagingModule from modules.helper.system import IGNORED_TYPES +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'messaging'} +CONF_DICT['grep'] = OrderedDict() +CONF_DICT['grep']['symbol'] = '#' +CONF_DICT['grep']['file'] = 'logs/df.txt' +CONF_DICT['prof'] = OrderedDict() -class df(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - # Dwarf professions. - conf_file = os.path.join(conf_folder, "df.cfg") +CONF_GUI = { + 'prof': { + 'view': 'list_dual', + 'addable': True}, + 'non_dynamic': ['grep.*']} - conf_dict = OrderedDict() - conf_dict['gui_information'] = {'category': 'messaging'} - conf_dict['grep'] = OrderedDict() - conf_dict['grep']['symbol'] = '#' - conf_dict['grep']['file'] = 'logs/df.txt' - conf_dict['prof'] = OrderedDict() - conf_gui = { - 'prof': { - 'view': 'list_dual', - 'addable': True}, - 'non_dynamic': ['grep.*']} - config = load_from_config_file(conf_file, conf_dict) - - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui}) - self.file = conf_dict['grep']['file'] +class df(MessagingModule): + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) + # Dwarf professions. + self.file = CONF_DICT['grep']['file'] dir_name = os.path.dirname(self.file) if not os.path.exists(dir_name): @@ -46,6 +36,12 @@ def __init__(self, conf_folder, **kwargs): with open(self.file, 'w'): pass + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI + def write_to_file(self, user, role): with open(self.file, 'r') as f: for line in f.readlines(): diff --git a/modules/messaging/levels.py b/modules/messaging/levels.py index 7389ad0..e8ef2ee 100644 --- a/modules/messaging/levels.py +++ b/modules/messaging/levels.py @@ -10,12 +10,49 @@ from collections import OrderedDict import datetime -from modules.helper.parser import load_from_config_file, save_settings +from modules.helper.parser import save_settings from modules.helper.system import system_message, ModuleLoadException, IGNORED_TYPES from modules.helper.module import MessagingModule log = logging.getLogger('levels') +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'messaging'} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['message'] = u'{0} has leveled up, now he is {1}' +CONF_DICT['config']['db'] = os.path.join('conf', u'levels.db') +CONF_DICT['config']['experience'] = u'geometrical' +CONF_DICT['config']['exp_for_level'] = 200 +CONF_DICT['config']['exp_for_message'] = 1 +CONF_DICT['config']['decrease_window'] = 60 + + +CONF_GUI = { + 'non_dynamic': [ + 'config.db', 'config.experience', + 'config.exp_for_level', 'config.exp_for_message', + 'decrease_window'], + 'config': { + 'experience': { + 'view': 'dropdown', + 'choices': ['static', 'geometrical', 'random']}, + 'exp_for_level': { + 'view': 'spin', + 'min': 0, + 'max': 100000 + }, + 'exp_for_message': { + 'view': 'spin', + 'min': 0, + 'max': 100000 + }, + 'decrease_window': { + 'view': 'spin', + 'min': 0, + 'max': 100000 + } + }} + class levels(MessagingModule): @staticmethod @@ -29,52 +66,9 @@ def create_db(db_location): db.commit() db.close() - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - - conf_file = os.path.join(conf_folder, "levels.cfg") - conf_dict = OrderedDict() - conf_dict['gui_information'] = {'category': 'messaging'} - conf_dict['config'] = OrderedDict() - conf_dict['config']['message'] = u'{0} has leveled up, now he is {1}' - conf_dict['config']['db'] = os.path.join('conf', u'levels.db') - conf_dict['config']['experience'] = u'geometrical' - conf_dict['config']['exp_for_level'] = 200 - conf_dict['config']['exp_for_message'] = 1 - conf_dict['config']['decrease_window'] = 60 - conf_gui = {'non_dynamic': ['config.db', 'config.experience', - 'config.exp_for_level', 'config.exp_for_message', - 'decrease_window'], - 'config': { - 'experience': { - 'view': 'dropdown', - 'choices': ['static', 'geometrical', 'random']}, - 'exp_for_level': { - 'view': 'spin', - 'min': 0, - 'max': 100000 - }, - 'exp_for_message': { - 'view': 'spin', - 'min': 0, - 'max': 100000 - }, - 'decrease_window': { - 'view': 'spin', - 'min': 0, - 'max': 100000 - } - }} - config = load_from_config_file(conf_file, conf_dict) - - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui}) - - self.conf_folder = None + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) + self.experience = None self.exp_for_level = None self.exp_for_message = None @@ -85,6 +79,12 @@ def __init__(self, conf_folder, **kwargs): self.decrease_window = None self.threshold_users = None + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI + def load_module(self, *args, **kwargs): MessagingModule.load_module(self, *args, **kwargs) if 'webchat' not in self._loaded_modules: @@ -92,10 +92,8 @@ def load_module(self, *args, **kwargs): else: self._loaded_modules['webchat']['class'].add_depend('levels') - conf_folder = self._conf_params['folder'] conf_dict = self._conf_params['config'] - self.conf_folder = conf_folder self.experience = conf_dict['config'].get('experience') self.exp_for_level = float(conf_dict['config'].get('exp_for_level')) self.exp_for_message = float(conf_dict['config'].get('exp_for_message')) diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index 5260f0b..87a4abe 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -5,50 +5,46 @@ import datetime from collections import OrderedDict -from modules.helper.parser import load_from_config_file from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES +from modules.helper.system import IGNORED_TYPES, CONF_FOLDER DEFAULT_PRIORITY = 20 +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = { + 'category': 'messaging', + 'id': DEFAULT_PRIORITY +} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['logging'] = True +CONF_DICT['config']['file_format'] = '%Y-%m-%d' +CONF_DICT['config']['message_date_format'] = '%Y-%m-%d %H:%M:%S' +CONF_DICT['config']['rotation'] = 'daily' + +CONF_GUI = {'non_dynamic': ['config.*']} + class logger(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) # Creating filter and replace strings. - conf_file = os.path.join(conf_folder, "logger.cfg") - conf_dict = OrderedDict() - conf_dict['gui_information'] = { - 'category': 'messaging', - 'id': DEFAULT_PRIORITY - } - conf_dict['config'] = OrderedDict() - conf_dict['config']['logging'] = True - conf_dict['config']['file_format'] = '%Y-%m-%d' - conf_dict['config']['message_date_format'] = '%Y-%m-%d %H:%M:%S' - conf_dict['config']['rotation'] = 'daily' - conf_gui = {'non_dynamic': ['config.*']} - - config = load_from_config_file(conf_file, conf_dict) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': conf_dict['gui_information']['id'], - 'config': conf_dict, - 'gui': conf_gui}) - - self.format = conf_dict['config']['file_format'] - self.ts_format = conf_dict['config']['message_date_format'] - self.logging = conf_dict['config']['logging'] - self.rotation = conf_dict['config']['rotation'] + self.format = CONF_DICT['config']['file_format'] + self.ts_format = CONF_DICT['config']['message_date_format'] + self.logging = CONF_DICT['config']['logging'] + self.rotation = CONF_DICT['config']['rotation'] self.folder = 'logs' - self.destination = os.path.join(conf_folder, '..', self.folder) + self.destination = os.path.join(CONF_FOLDER, '..', self.folder) if not os.path.exists(self.destination): os.makedirs(self.destination) + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI + def process_message(self, message, queue, **kwargs): if message: if message['type'] in IGNORED_TYPES: diff --git a/modules/messaging/mentions.py b/modules/messaging/mentions.py index 657ed8d..bd8eb29 100644 --- a/modules/messaging/mentions.py +++ b/modules/messaging/mentions.py @@ -1,39 +1,37 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov -import os import re from collections import OrderedDict -from modules.helper.parser import load_from_config_file from modules.helper.module import MessagingModule from modules.helper.system import IGNORED_TYPES +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'messaging'} +CONF_DICT['mentions'] = [] +CONF_DICT['address'] = [] + +CONF_GUI = { + 'mentions': { + 'addable': 'true', + 'view': 'list'}, + 'address': { + 'addable': 'true', + 'view': 'list'} +} + class mentions(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) # Creating filter and replace strings. - conf_file = os.path.join(conf_folder, "mentions.cfg") - conf_dict = OrderedDict() - conf_dict['gui_information'] = {'category': 'messaging'} - conf_dict['mentions'] = [] - conf_dict['address'] = [] - - conf_gui = { - 'mentions': { - 'addable': 'true', - 'view': 'list'}, - 'address': { - 'addable': 'true', - 'view': 'list'}} - config = load_from_config_file(conf_file, conf_dict) - self._conf_params.update( - {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'config': conf_dict, - 'gui': conf_gui}) + + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI def process_message(self, message, queue, **kwargs): # Replacing the message if needed. diff --git a/modules/messaging/test_module.py b/modules/messaging/test_module.py index ff9abc1..3ad7e12 100644 --- a/modules/messaging/test_module.py +++ b/modules/messaging/test_module.py @@ -11,27 +11,8 @@ class test_module(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - # Dwarf professions. - conf_file = os.path.join(conf_folder, CONFIG_FILE) - - # Ordered because order matters - conf_dict = OrderedDict() - conf_dict['gui_information'] = { - 'category': 'test', - 'id': DEFAULT_PRIORITY} - - config = load_from_config_file(conf_file, conf_dict) - self._conf_params.update({ - 'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': conf_dict['gui_information']['id'], - 'config': OrderedDict(conf_dict), - 'custom_renderer': True - }) + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) def render(self, *args, **kwargs): print "HelloWorld" - pass diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 62a56e7..bc70c25 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -1,6 +1,5 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import os -import re import threading import json import Queue @@ -16,7 +15,7 @@ from cherrypy.lib.static import serve_file from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket -from modules.helper.parser import load_from_config_file, save_settings +from modules.helper.parser import save_settings from modules.helper.system import THREADS, PYTHON_FOLDER, CONF_FOLDER, remove_message_by_id from modules.helper.module import MessagingModule from gui import MODULE_KEY @@ -33,6 +32,20 @@ WS_THREADS = THREADS + 3 +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = { + 'category': 'main', + 'id': DEFAULT_PRIORITY +} +CONF_DICT['server'] = OrderedDict() +CONF_DICT['server']['host'] = '127.0.0.1' +CONF_DICT['server']['port'] = '8080' +CONF_DICT['style_gui'] = DEFAULT_STYLE +CONF_DICT['style_gui_settings'] = OrderedDict() +CONF_DICT['style'] = DEFAULT_STYLE +CONF_DICT['style_settings'] = OrderedDict() +CONF_DICT['style_settings']['show_system_msg'] = True + def prepare_message(msg, style_settings): message = copy.deepcopy(msg) @@ -458,68 +471,13 @@ def socket_open(host, port): class webchat(MessagingModule): - def __init__(self, conf_folder, **kwargs): - MessagingModule.__init__(self) - # Module configuration - conf_file = os.path.join(conf_folder, "webchat.cfg") - conf_dict = OrderedDict() - conf_dict['gui_information'] = { - 'category': 'main', - 'id': DEFAULT_PRIORITY - } - conf_dict['server'] = OrderedDict() - conf_dict['server']['host'] = '127.0.0.1' - conf_dict['server']['port'] = '8080' - conf_dict['style_gui'] = DEFAULT_STYLE - conf_dict['style_gui_settings'] = OrderedDict() - conf_dict['style'] = DEFAULT_STYLE - conf_dict['style_settings'] = OrderedDict() - conf_dict['style_settings']['show_system_msg'] = True - - conf_gui = { - 'style_gui': { - 'check': 'http', - 'check_type': 'dir', - 'view': 'choose_single' - }, - 'style_gui_settings': {}, - 'style': { - 'check': 'http', - 'check_type': 'dir', - 'view': 'choose_single' - }, - 'style_settings': {}, - 'non_dynamic': ['server.*'], - 'ignored_sections': ['style_settings', 'style_gui_settings'], - 'redraw': { - 'style_settings': { - 'redraw_trigger': ['style'], - 'type': 'chat', - 'get_config': self.load_style_settings, - 'get_gui': self.get_style_gui_from_file - }, - 'style_gui_settings': { - 'redraw_trigger': ['style_gui'], - 'type': 'gui', - 'get_config': self.load_style_settings, - 'get_gui': self.get_style_gui_from_file - }, - } - } - - parser = load_from_config_file(conf_file, conf_dict) + def __init__(self, *args, **kwargs): + MessagingModule.__init__(self, *args, **kwargs) + conf_params = self._conf_params['config'] self._conf_params.update({ - 'folder': conf_folder, - 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': parser, - 'id': conf_dict['gui_information']['id'], - 'config': conf_dict, - 'gui': conf_gui, - - 'host': conf_dict['server']['host'], - 'port': conf_dict['server']['port'], + 'host': conf_params['server']['host'], + 'port': conf_params['server']['port'], 'style_settings': { 'gui': { @@ -691,3 +649,39 @@ def prepare_style_settings(self): gui_style_settings['style_name'] = gui_config_style gui_style_settings['location'] = self.get_style_path(gui_config_style) gui_style_settings['keys'] = self.load_style_settings(gui_config_style, 'gui') + + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self): + return { + 'style_gui': { + 'check': 'http', + 'check_type': 'dir', + 'view': 'choose_single' + }, + 'style_gui_settings': {}, + 'style': { + 'check': 'http', + 'check_type': 'dir', + 'view': 'choose_single' + }, + 'style_settings': {}, + 'non_dynamic': ['server.*'], + 'ignored_sections': ['style_settings', 'style_gui_settings'], + 'redraw': { + 'style_settings': { + 'redraw_trigger': ['style'], + 'type': 'chat', + 'get_config': self.load_style_settings, + 'get_gui': self.get_style_gui_from_file + }, + 'style_gui_settings': { + 'redraw_trigger': ['style_gui'], + 'type': 'gui', + 'get_config': self.load_style_settings, + 'get_gui': self.get_style_gui_from_file + }, + } + } + From 98903ab509a9452d90bb1af7170866bace7e9535 Mon Sep 17 00:00:00 2001 From: Nikolay Mokrinskiy Date: Fri, 31 Mar 2017 21:04:07 +0200 Subject: [PATCH 02/43] Transparency, Borderless mode, Keep position after restart --- gui.py | 13 +++++++++++-- main.py | 8 ++++++++ translations/en/main.key | 2 ++ translations/ru/main.key | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/gui.py b/gui.py index fd0c850..cb5aabb 100644 --- a/gui.py +++ b/gui.py @@ -1317,9 +1317,16 @@ def __init__(self, parent, title, url, **kwargs): self.status_frame = None self.browser = None - wx.Frame.__init__(self, parent, title=title, size=self.gui_settings.get('size')) + wx.Frame.__init__(self, parent, title=title, size=self.gui_settings.get('size'), pos=self.gui_settings.get('position')) # Set window style - styles = wx.DEFAULT_FRAME_STYLE + if self.gui_settings.get('transparent', False): + log.info("Application is transparent") + self.SetTransparent(200) + if self.gui_settings.get('borderless', False): + log.info("Application is in borderless mode") + styles = wx.CLIP_CHILDREN | wx.BORDER_NONE | wx.FRAME_SHAPED + else: + styles = wx.DEFAULT_FRAME_STYLE if self.gui_settings.get('on_top', False): log.info("Application is on top") styles = styles | wx.STAY_ON_TOP @@ -1386,6 +1393,8 @@ def on_close(self, event): size = self.Size config['width'] = size[0] config['height'] = size[1] + config['pos_x'] = self.Position.x + config['pos_y'] = self.Position.y for module_name, module_dict in self.loaded_modules.iteritems(): module_dict['class'].apply_settings(system_exit=True) diff --git a/main.py b/main.py index ae76c1a..0609123 100644 --- a/main.py +++ b/main.py @@ -62,6 +62,8 @@ def close(): main_config_dict['gui_information']['category'] = 'main' main_config_dict['gui_information']['width'] = '450' main_config_dict['gui_information']['height'] = '500' + main_config_dict['gui_information']['pos_x'] = '10' + main_config_dict['gui_information']['pos_y'] = '10' main_config_dict['system'] = OrderedDict() main_config_dict['system']['log_level'] = 'INFO' main_config_dict['system']['testing_mode'] = False @@ -73,6 +75,8 @@ def close(): main_config_dict['gui']['on_top'] = True main_config_dict['gui']['show_browser'] = True main_config_dict['gui']['show_counters'] = True + main_config_dict['gui']['transparent'] = False + main_config_dict['gui']['borderless'] = False main_config_dict['gui']['reload'] = None main_config_dict['language'] = get_language() @@ -106,10 +110,14 @@ def close(): gui_settings['gui'] = main_config_dict[GUI_TAG].get('gui') gui_settings['on_top'] = main_config_dict[GUI_TAG].get('on_top') + gui_settings['transparent'] = main_config_dict[GUI_TAG].get('transparent') + gui_settings['borderless'] = main_config_dict[GUI_TAG].get('borderless') gui_settings['language'] = main_config_dict.get('language') gui_settings['show_hidden'] = main_config_dict[GUI_TAG].get('show_hidden') gui_settings['size'] = (int(main_config_dict['gui_information'].get('width')), int(main_config_dict['gui_information'].get('height'))) + gui_settings['position'] = (int(main_config_dict['gui_information'].get('pos_x')), + int(main_config_dict['gui_information'].get('pos_y'))) gui_settings['show_browser'] = main_config_dict['gui'].get('show_browser') # Checking updates diff --git a/translations/en/main.key b/translations/en/main.key index 8c13cfb..2f11dd5 100644 --- a/translations/en/main.key +++ b/translations/en/main.key @@ -24,6 +24,8 @@ main.gui.gui = Is GUI enabled main.gui.show_browser = Show browser window main.gui.show_counters = Show channel view counters main.gui.on_top = Show window on top +main.gui.transparent = Make window transparent +main.gui.borderless = Make window borderless main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Just a big parameter for test main.gui.reload = Reload WebChat main.gui.reload.button = Reload diff --git a/translations/ru/main.key b/translations/ru/main.key index a74547a..5b20b9a 100644 --- a/translations/ru/main.key +++ b/translations/ru/main.key @@ -24,6 +24,8 @@ main.gui.gui = Интерфейс Включен main.gui.show_browser = Показывать окно браузера main.gui.show_counters = Показывать счетчики зрителей main.gui.on_top = Окно поверх всех +main.gui.transparent = Сделать окно прозрачным +main.gui.borderless = Не показывать рамки окна main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Тестовый Параметр main.gui.reload = Перезагрузить ВебЧат main.gui.reload.button = Перезагрузить From 8736f93520e8a9edbc660797fb28b701ab0a9d59 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Thu, 30 Mar 2017 21:04:43 +0300 Subject: [PATCH 03/43] LC-123 Add beam.pro chat --- gui.py | 14 +- img/beampro.png | Bin 0 -> 764 bytes main.py | 2 +- modules/chat/beampro.py | 342 ++++++++++++++++++++++++++++++ modules/chat/goodgame.py | 12 +- modules/chat/sc2tv.py | 9 - modules/chat/twitch.py | 12 +- modules/helper/module.py | 49 +++-- modules/helper/parser.py | 14 +- modules/helper/system.py | 2 +- modules/messaging/webchat.py | 7 +- requires_linux.txt | 2 +- src/jenkins/cfg/beampro.cfg | 3 + src/jenkins/cfg/chat_modules.cfg | 1 + src/jenkins/chat_tests/beampro.sh | 8 + src/jenkins/run_chat.sh | 5 + translations/en/beampro.key | 7 + translations/ru/beampro.key | 6 + 18 files changed, 440 insertions(+), 55 deletions(-) create mode 100644 img/beampro.png create mode 100644 modules/chat/beampro.py create mode 100644 src/jenkins/cfg/beampro.cfg create mode 100644 src/jenkins/chat_tests/beampro.sh create mode 100644 translations/en/beampro.key create mode 100644 translations/ru/beampro.key diff --git a/gui.py b/gui.py index fd0c850..1099026 100644 --- a/gui.py +++ b/gui.py @@ -212,6 +212,10 @@ def create_tool(self, name, binding=None, style=wx.ITEM_NORMAL, s_help="", l_hel return button +class GuiCreationError(Exception): + pass + + class SettingsWindow(wx.Frame): main_grid = None page_list = [] @@ -288,6 +292,10 @@ def __init__(self, *args, **kwargs): 'function': self.create_textctrl, 'bind': self.on_textctrl }, + int: { + 'function': self.create_textctrl, + 'bind': self.on_textctrl + }, 'spin': { 'function': self.create_spin, 'bind': self.on_spinctrl @@ -740,7 +748,7 @@ def create_button(self, **kwargs): def create_static_box(self, **kwargs): panel = kwargs.get('panel') - value = kwargs.get('value') + item_value = kwargs.get('value') gui = kwargs.get('gui') key = kwargs.get('key') @@ -755,7 +763,7 @@ def create_static_box(self, **kwargs): spacer = False hidden_items = gui.get('hidden', []) - for item, value in value.items(): + for item, value in item_value.items(): if item in hidden_items and not self.show_hidden: continue view = gui.get(item, {}).get('view', type(value)) @@ -764,7 +772,7 @@ def create_static_box(self, **kwargs): elif callable(value): fnction = self.value_map['button'] else: - return + raise GuiCreationError('Unable to create item, bad value map') item_dict = fnction['function'](panel=static_box, item=item, value=value, key=key + [item], bind=fnction['bind'], gui=gui.get(item, {}), from_sb=True) if 'text_size' in item_dict: diff --git a/img/beampro.png b/img/beampro.png new file mode 100644 index 0000000000000000000000000000000000000000..bba1cd8bb2146c01cad688fa184c4db1dd40f0bf GIT binary patch literal 764 zcmVYGPG#8ylhc*w~-p z_4Unax7$e>08k0KPEB@U3Jd`8IMCL+%>#%jIlW#>%E}5(7!0~+0HAU;_S-pw`2m-a zDI~+v+8jdu5ls91dZXFwFU}E6N=>ciNjSWnRa;yCMwMT2ESGZg+SgYA;z_N<&~&@iGWdGY ztF*&$EO?%`F}Kd7bJcd%OLq4Wo*hqtlTA9>2W?%=;zA@85A2%gSnENRuI`9Bzp;uB zwDoTDpGINXM<+#KRJJ_49E8Kgd*cV95r$GZ1OQq!v9_Mq9KLhi=_iEz2PHU^R+89` z=L5C%J`G11N~PbDBr7WBY=yb7{c-bLstJL^SO-0ymB>V6GUTZ2Q`OfuOaQ1inG9)4 zF)!DsFUhL(&-~OFGt-LUFrGt}wmjuM%fY{^Vnn<`0AOh7M|XL7(Z%%il&=u$Y8Dp) zqw>C)L})lRzcSPF{L8UR3;+a!VuQ!y8{XX9O4czK?rQ1YaqPgepTjiV000aCxZO?~ z42mZ`9$$~oH@&LNM585Ehl}?rJuj{=tjw>>c8BR!^^5N=d1pF%qOWhHR1h>RJkQy9 uo)Z?PqTQ&@$@+h`VIDq7t7YQ=0000=0.4.0 +ws4py>=0.4.2 irc semantic_version jinja2 diff --git a/src/jenkins/cfg/beampro.cfg b/src/jenkins/cfg/beampro.cfg new file mode 100644 index 0000000..6e53d2c --- /dev/null +++ b/src/jenkins/cfg/beampro.cfg @@ -0,0 +1,3 @@ +config: + channels_list: + - czt diff --git a/src/jenkins/cfg/chat_modules.cfg b/src/jenkins/cfg/chat_modules.cfg index 23041fa..2fdb6ab 100644 --- a/src/jenkins/cfg/chat_modules.cfg +++ b/src/jenkins/cfg/chat_modules.cfg @@ -2,3 +2,4 @@ chats: - goodgame - sc2tv - twitch +- beampro diff --git a/src/jenkins/chat_tests/beampro.sh b/src/jenkins/chat_tests/beampro.sh new file mode 100644 index 0000000..d1a4192 --- /dev/null +++ b/src/jenkins/chat_tests/beampro.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +PORT=8080 + +curl -s -X POST -H 'Content-Type: application/json' -d '{"nickname":"BeamProTest","text":"bpTestMessage"}' http://localhost:${PORT}/rest/beampro/push_message + +sleep 1 + +curl -s http://localhost:${PORT}/rest/webchat/history | grep bpTestMessage diff --git a/src/jenkins/run_chat.sh b/src/jenkins/run_chat.sh index 91a0cfc..22c40eb 100644 --- a/src/jenkins/run_chat.sh +++ b/src/jenkins/run_chat.sh @@ -20,6 +20,11 @@ while [ ${ATTEMPTS} -lt 20 ]; do if ! grep "twitch Testing mode online" chat.log; then continue fi + + if ! grep "BeamPro Testing mode online" chat.log; then + continue + fi + sleep 5 exit 0 done diff --git a/translations/en/beampro.key b/translations/en/beampro.key new file mode 100644 index 0000000..d5ebd7d --- /dev/null +++ b/translations/en/beampro.key @@ -0,0 +1,7 @@ +beampro = BeamPro +beampro.config = Settings +beampro.connection_success = Connection to {0} Successful +beampro.connection_died = Connection {0} died, trying to reconnect +beampro.connection_closed= Connection {0} closed +beampro.join_success = Successfully joined channel {0} + diff --git a/translations/ru/beampro.key b/translations/ru/beampro.key new file mode 100644 index 0000000..797f84a --- /dev/null +++ b/translations/ru/beampro.key @@ -0,0 +1,6 @@ +beampro = BeamPro +beampro.config = Настройки +beampro.connection_success = Соединение к {0} установлено +beampro.connection_died = Соединение к {0} было прервано, попытка переподключения... +beampro.connection_closed = Соединение к {0} остановлено +beampro.join_success = Подключение к каналу {0} успешно From 49788894e10135cf744c820131d838bb344ebe22 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Fri, 31 Mar 2017 23:19:15 +0300 Subject: [PATCH 04/43] LC-360 Configurable Transparency --- gui.py | 8 +++++--- main.py | 11 ++++++++--- translations/en/main.key | 2 +- translations/ru/main.key | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/gui.py b/gui.py index a807551..4d96196 100644 --- a/gui.py +++ b/gui.py @@ -1325,11 +1325,13 @@ def __init__(self, parent, title, url, **kwargs): self.status_frame = None self.browser = None - wx.Frame.__init__(self, parent, title=title, size=self.gui_settings.get('size'), pos=self.gui_settings.get('position')) + wx.Frame.__init__(self, parent, title=title, + size=self.gui_settings.get('size'), + pos=self.gui_settings.get('position')) # Set window style - if self.gui_settings.get('transparent', False): + if self.gui_settings.get('transparency') < 100: log.info("Application is transparent") - self.SetTransparent(200) + self.SetTransparent(self.gui_settings['transparency'] * 2.55) if self.gui_settings.get('borderless', False): log.info("Application is in borderless mode") styles = wx.CLIP_CHILDREN | wx.BORDER_NONE | wx.FRAME_SHAPED diff --git a/main.py b/main.py index c9d1e06..64aad6e 100644 --- a/main.py +++ b/main.py @@ -75,7 +75,7 @@ def close(): main_config_dict['gui']['on_top'] = True main_config_dict['gui']['show_browser'] = True main_config_dict['gui']['show_counters'] = True - main_config_dict['gui']['transparent'] = False + main_config_dict['gui']['transparency'] = 50 main_config_dict['gui']['borderless'] = False main_config_dict['gui']['reload'] = None main_config_dict['language'] = get_language() @@ -90,7 +90,12 @@ def close(): 'hidden': ['log_level', 'testing_mode'], }, 'gui': { - 'hidden': ['cli'] + 'hidden': ['cli'], + 'transparency': { + 'view': 'slider', + 'min': 10, + 'max': 100 + } }, 'ignored_sections': ['gui.reload'], 'non_dynamic': ['language.list_box', 'gui.*', 'system.*'] @@ -110,7 +115,7 @@ def close(): gui_settings['gui'] = main_config_dict[GUI_TAG].get('gui') gui_settings['on_top'] = main_config_dict[GUI_TAG].get('on_top') - gui_settings['transparent'] = main_config_dict[GUI_TAG].get('transparent') + gui_settings['transparency'] = main_config_dict[GUI_TAG].get('transparency') gui_settings['borderless'] = main_config_dict[GUI_TAG].get('borderless') gui_settings['language'] = main_config_dict.get('language') gui_settings['show_hidden'] = main_config_dict[GUI_TAG].get('show_hidden') diff --git a/translations/en/main.key b/translations/en/main.key index 2f11dd5..af805f8 100644 --- a/translations/en/main.key +++ b/translations/en/main.key @@ -24,7 +24,7 @@ main.gui.gui = Is GUI enabled main.gui.show_browser = Show browser window main.gui.show_counters = Show channel view counters main.gui.on_top = Show window on top -main.gui.transparent = Make window transparent +main.gui.transparency = Transparency main.gui.borderless = Make window borderless main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Just a big parameter for test main.gui.reload = Reload WebChat diff --git a/translations/ru/main.key b/translations/ru/main.key index 5b20b9a..1e67068 100644 --- a/translations/ru/main.key +++ b/translations/ru/main.key @@ -24,7 +24,7 @@ main.gui.gui = Интерфейс Включен main.gui.show_browser = Показывать окно браузера main.gui.show_counters = Показывать счетчики зрителей main.gui.on_top = Окно поверх всех -main.gui.transparent = Сделать окно прозрачным +main.gui.transparency = Прозрачность main.gui.borderless = Не показывать рамки окна main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Тестовый Параметр main.gui.reload = Перезагрузить ВебЧат From f0192074e8f76b986ae7cbb1271248a3d5cf4e90 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sat, 1 Apr 2017 12:19:16 +0300 Subject: [PATCH 05/43] LC-366 cloud2butt should only replace whole words --- modules/messaging/c2b.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 8ce34f5..77a5148 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -58,11 +58,7 @@ def process_message(self, message, queue, **kwargs): for item, replace in self._conf_params['config']['config'].iteritems(): if item in message['text']: replace_word = random.choice(replace.split('/')) - if message['source'] == 'tw': - message['emotes'] = twitch_replace_indexes(item, message['text'], - len(item), len(replace_word), - message.get('emotes', [])) - message['text'] = message['text'].replace(item, replace_word) + message['text'] = re.sub(r'\b{}\b'.format(item), replace_word, message['text']) return message def _conf_settings(self, *args, **kwargs): From 0ba23b57993af77be43d9adfd8cdbbbb5d8c7766 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sat, 1 Apr 2017 12:31:57 +0300 Subject: [PATCH 06/43] LC-366 cloud2butt should only replace whole words --- modules/messaging/c2b.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 77a5148..2df4347 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -58,7 +58,7 @@ def process_message(self, message, queue, **kwargs): for item, replace in self._conf_params['config']['config'].iteritems(): if item in message['text']: replace_word = random.choice(replace.split('/')) - message['text'] = re.sub(r'\b{}\b'.format(item), replace_word, message['text']) + message['text'] = re.sub(ur'\b{}\b'.format(item), replace_word, message['text'], flags=re.UNICODE) return message def _conf_settings(self, *args, **kwargs): From 5286b4336ba9dea0015d4b65f32bb03eefd2917e Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sat, 1 Apr 2017 16:47:10 +0300 Subject: [PATCH 07/43] LC-338 hitbox chat --- modules/chat/beampro.py | 1 - modules/chat/goodgame.py | 1 - modules/chat/hitbox.py | 408 +++++++++++++++++++++++++++++++ modules/chat/sc2tv.py | 1 - modules/chat/twitch.py | 1 - modules/helper/module.py | 3 +- modules/helper/system.py | 2 +- modules/messaging/c2b.py | 5 +- src/jenkins/cfg/chat_modules.cfg | 1 + src/jenkins/cfg/hitbox.cfg | 3 + src/jenkins/chat_tests/hitbox.sh | 8 + src/jenkins/run_chat.sh | 4 + 12 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 modules/chat/hitbox.py create mode 100644 src/jenkins/cfg/hitbox.cfg create mode 100644 src/jenkins/chat_tests/hitbox.sh diff --git a/modules/chat/beampro.py b/modules/chat/beampro.py index 46266c6..e1da9e0 100644 --- a/modules/chat/beampro.py +++ b/modules/chat/beampro.py @@ -338,5 +338,4 @@ def _set_chat_online(self, chat): def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() - self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index e7f23fe..5b56532 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -425,5 +425,4 @@ def _set_chat_online(self, chat): def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() - self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/chat/hitbox.py b/modules/chat/hitbox.py new file mode 100644 index 0000000..a76cf73 --- /dev/null +++ b/modules/chat/hitbox.py @@ -0,0 +1,408 @@ +# Copyright (C) 2016 CzT/Vladislav Ivanov +import Queue +import json +import random +import re +import threading +import os +import logging +from collections import OrderedDict +import HTMLParser +import time + +import requests +from ws4py.client.threadedclient import WebSocketClient +from modules.helper.module import ChatModule +from modules.helper.system import translate_key, system_message, remove_message_by_id, remove_message_by_user, \ + EMOTE_FORMAT + +logging.getLogger('requests').setLevel(logging.ERROR) +log = logging.getLogger('hitbox') +SOURCE = 'hb' +SOURCE_ICON = 'http://www.hitbox.tv/favicon.ico' +FILE_ICON = os.path.join('img', 'hitboxtv.png') +SYSTEM_USER = 'Hitbox.tv' +ID_PREFIX = 'hb_{}' +SMILE_REGEXP = r'(^|\s)({})(?=(\s|$))' +SMILE_FORMAT = r'\1:{}:\3' + +API_URL = 'https://api.hitbox.tv{}' +CDN_URL = 'http://edge.sf.hitbox.tv{}' + +PING_DELAY = 10 + +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'chat'} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['show_nickname_colors'] = False + +CONF_GUI = { + 'config': { + 'channels_list': { + 'view': 'list', + 'addable': 'true' + }, + }, + 'icon': FILE_ICON} + + +class HitboxAPIError(Exception): + pass + + +class HitboxMessageHandler(threading.Thread): + def __init__(self, *args, **kwargs): + threading.Thread.__init__(self) + + self.queue = kwargs.get('queue') + self.message_queue = kwargs.get('message_queue') + self.channel = kwargs.get('channel') + self.main_class = kwargs.get('main_class') # type: hitbox + self.smiles = kwargs.get('smiles') + + def run(self): + while True: + self.process_message(self.queue.get()) + + def process_message(self, message): + for msg in message['args']: + self._process_message(json.loads(msg)) + + def _process_message(self, message): + method = message['method'] + if method == 'loginMsg': + self.main_class.set_online(self.channel) + elif method == 'serverMsg': + self._process_trash_msg(message['params']) + elif method == 'pollMsg': + self._process_trash_msg(message['params']) + elif method == 'motdMsg': + self._process_trash_msg(message['params']) + elif method == 'chatMsg': + self._process_chat_msg(message['params']) + elif method == 'infoMsg': + self._process_info_msg(message['params']) + elif method == 'userList': + self._process_user_list(message['params']) + else: + log.debug(message) + + def _process_user_list(self, message): + viewers = [] + for item, data in message['data'].items(): + if item == 'Guests': + continue + else: + viewers += data + + viewers = len(set(viewers)) + viewers += message['data']['Guests'] if message['data']['Guests'] else 0 + self.main_class.set_viewers(self.channel, viewers) + + def _process_chat_msg(self, message): + msg = { + 'id': ID_PREFIX.format(message['id']), + 'source': SOURCE, + 'source_icon': SOURCE_ICON, + 'user': message['name'], + 'text': message['text'], + 'emotes': {}, + 'nick_color': '#{}'.format(message['nameColor']) if self._show_color() else None, + 'type': 'message' + } + self._send_message(msg) + + def _process_info_msg(self, message): + user_to_delete = message['variables']['user'] + self._send_message( + remove_message_by_user( + user_to_delete, + text=self.main_class.conf_params()['settings'].get('remove_text') + ) + ) + + def _show_color(self): + return self.main_class.conf_params()['config']['config']['show_nickname_colors'] + + def _post_process_emotes(self, message): + for word in message['text'].split(): + if word in self.smiles: + if word not in message['emotes']: + message['text'] = re.sub(SMILE_REGEXP.format(re.escape(word)), + r'\1{}\3'.format(EMOTE_FORMAT.format(word)), + message['text']) + message['emotes'][word] = { + 'emote_id': word, + 'emote_url': CDN_URL.format(self.smiles[word]) + } + + def _send_message(self, message): + self._post_process_emotes(message) + self.message_queue.put(message) + + def _process_trash_msg(self, message): + pass + + +class HitboxViewersWS(WebSocketClient): + def __init__(self, url, **kwargs): + WebSocketClient.__init__(self, url, heartbeat_freq=30) + + self.main_class = kwargs.get('main_class') # type: hitbox + self.channel = kwargs.get('channel') + + def opened(self): + self.send(json.dumps({ + 'method': 'joinChannel', + 'params': { + 'channel': self.channel.lower(), + 'name': 'UnknownSoldier', + 'token': None, + 'hideBuffered': True + } + })) + + def closed(self, code, reason=None): + log.info('Viewers Connection closed %s, %s', code, reason) + + def received_message(self, message): + data = json.loads(message.data) + method = data['method'] + if method == 'infoMsg': + self.main_class.set_viewers(self.channel, data['params']['viewers']) + + +class HitboxClient(WebSocketClient): + def __init__(self, url, **kwargs): + ws_url = self.get_connection_url(url) + + WebSocketClient.__init__(self, ws_url) + + self.channel = kwargs.get('channel') + self.main_class = kwargs.get('main_class') + self.exited = False + + self.ws_queue = Queue.Queue() + self.message_handler = HitboxMessageHandler( + queue=self.ws_queue, + message_queue=self.main_class.queue, + channel=self.channel, + main_class=self.main_class, + smiles=kwargs.get('smiles') + ) + self.message_handler.start() + + self._viewers_th = HitboxViewersWS( + url='wss://{}/viewer'.format(random.choice(kwargs.get('viewerss'))), + main_class=self.main_class, + channel=self.channel + ) + self._viewers_th.daemon = True + + def opened(self): + log.info("Connection Successful") + self.system_message( + translate_key('hitbox.connection_success').format(self.channel) + ) + self._join_channel() + self._viewers_th.connect() + + def closed(self, code, reason=None): + """ + Codes used by LC + 4000 - Normal disconnect by LC + + :param code: + :param reason: + """ + log.debug("%s %s", code, reason) + log.info("Connection closed") + self._viewers_th.close() + if code in [4000]: + self.system_message( + translate_key('hitbox.connection_closed').format(self.channel) + ) + self.exited = True + else: + self.system_message( + translate_key('hitbox.connection_died').format(self.channel) + ) + self.main_class.set_offline(self.channel) + + def received_message(self, message): + if message.data == '2::': + self._respond_ping() + elif message.data.startswith('5::'): + self.ws_queue.put(json.loads(message.data[4:])) + + def _respond_ping(self): + self.send('2::') + + def _join_channel(self): + login_command = { + 'name': 'message', + 'args': [ + { + 'method': 'joinChannel', + 'params': { + 'channel': self.channel.lower(), + 'name': 'UnknownSoldier', + 'token': None, + 'hideBuffered': True + } + } + ] + } + self.send_message(5, login_command) + + def send_message(self, msg_type, data): + message = "{}:::{}".format(msg_type, json.dumps(data)) + self.send(message) + + @staticmethod + def get_connection_url(url): + user_id_req = requests.get('https://{}/socket.io/1/'.format(url)) + if not user_id_req.ok: + raise HitboxAPIError("Unable to get userid") + user_id = user_id_req.text.split(':')[0] + return 'wss://{}/socket.io/1/websocket/{}'.format(url, user_id) + + def system_message(self, msg, category='system'): + system_message(msg, self.main_class.queue, source=SOURCE, + icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) + + +class HitboxInitThread(threading.Thread): + def __init__(self, *args, **kwargs): + threading.Thread.__init__(self) + self.daemon = True + + self.queue = kwargs.get('queue') + self.channel = kwargs.get('channel') + self.settings = kwargs.get('settings') + self.main_class = kwargs.get('chat_module') + self.ws = None + + self.endpoints = [] + self.viewer_endpoints = [] + self.smiles = {} + + def run(self): + try_count = 0 + while True: + try_count += 1 + log.info("Connecting, try {0}".format(try_count)) + self.get_connection_info() + self.ws = HitboxClient( + random.choice(self.endpoints), + viewerss=self.viewer_endpoints, + channel=self.channel, + main_class=self.main_class, + smiles=self.smiles + ) + self.ws.connect() + self.ws.run_forever() + if self.ws.exited: + break + time.sleep(5) + + def get_connection_info(self): + servers_req = requests.get(API_URL.format('/chat/servers')) + if not servers_req.ok: + raise HitboxAPIError("Unable to get server list") + self.endpoints = [item['server_ip'] for item in servers_req.json()] + + smiles_req = requests.get(API_URL.format('/chat/icons/{}'.format(self.channel))) + if not smiles_req.ok: + raise HitboxAPIError("Unable to get smiles") + self.smiles = {} + for smile in smiles_req.json()['items']: + self.smiles[smile['icon_short']] = smile['icon_path'] + self.smiles[smile['icon_short_alt']] = smile['icon_path'] + + viewers_req = requests.get(API_URL.format('/player/server')) + if not servers_req.ok: + raise HitboxAPIError("Unable to get viewer server settings") + viewers = viewers_req.json() + self.viewer_endpoints = [server['server_ip'] for server in viewers] + + def stop(self): + self.ws.close(4000) + + +class HitboxMessage(object): + def __init__(self, nickname, text): + self.data = { + u'args': [json.dumps({ + u'params': { + u'name': nickname, + u'id': ID_PREFIX.format(random.randint(1, 50)), + u'text': text, + u'nameColor': '000000' + }, + u'method': u'chatMsg' + })] + } + + +class TestHitbox(threading.Thread): + def __init__(self, main_class, **kwargs): + super(TestHitbox, self).__init__() + self.main_class = main_class # type: hitbox + self.main_class.rest_add('POST', 'push_message', self.send_message) + self.chat = None + + def run(self): + while True: + try: + thread = self.main_class.channels.items()[0][1] + if thread.ws: + self.chat = thread.ws.message_handler + break + except: + continue + log.info("Hitbox Testing mode online") + + def send_message(self, *args, **kwargs): + nickname = kwargs.get('nickname', 'super_tester') + text = kwargs.get('text', 'Kappa 123') + + self.chat.process_message(HitboxMessage(nickname, text).data) + + +class hitbox(ChatModule): + def __init__(self, *args, **kwargs): + log.info("Initializing hitbox chat") + ChatModule.__init__(self, *args, **kwargs) + + def _conf_settings(self, *args, **kwargs): + return CONF_DICT + + def _gui_settings(self, *args, **kwargs): + return CONF_GUI + + def _test_class(self): + return TestHitbox(self) + + def _set_chat_online(self, chat): + ChatModule._set_chat_online(self, chat) + self.channels[chat] = HitboxInitThread( + queue=self.queue, + channel=chat, + settings=self._conf_params['settings'], + chat_module=self) + self.channels[chat].start() + + def get_viewers(self, ws, channel): + request = { + "name": "message", + "args": [ + { + "method": "getChannelUserList", + "params": { + "channel": channel + } + } + ] + } + ws.send_message(5, request) diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index ce12c29..1f49f9f 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -407,5 +407,4 @@ def _set_chat_online(self, chat): self.channels[chat].start() def apply_settings(self, **kwargs): - self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 53e2627..91f30af 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -610,5 +610,4 @@ def _set_chat_online(self, chat): def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() - self._check_chats(self.channels.keys()) ChatModule.apply_settings(self, **kwargs) diff --git a/modules/helper/module.py b/modules/helper/module.py index e05cf03..5db7c18 100644 --- a/modules/helper/module.py +++ b/modules/helper/module.py @@ -163,10 +163,11 @@ def _test_class(self): Override this method :return: Chat test class (object/Class) """ - return object() + return {} def apply_settings(self, **kwargs): BaseModule.apply_settings(self, **kwargs) + self._check_chats(self.channels.keys()) self.refresh_channel_names() def refresh_channel_names(self): diff --git a/modules/helper/system.py b/modules/helper/system.py index 8f7c7ba..3a51710 100644 --- a/modules/helper/system.py +++ b/modules/helper/system.py @@ -145,7 +145,7 @@ def random_string(length): def remove_message_by_user(user, text=None): command = {'type': 'command', 'command': 'remove_by_user', - 'user': user} + 'user': user if isinstance(user, list) else [user]} if text: command['text'] = text command['command'] = 'replace_by_user' diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 2df4347..034db72 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -22,6 +22,7 @@ 'addable': 'true', 'view': 'list_dual'}, 'non_dynamic': ['config.*']} +C2B_REGEXP = ur'(^|\s)({})(?=(\s|$))' def twitch_replace_indexes(filter_name, text, filter_size, replace_size, emotes_list): @@ -58,7 +59,9 @@ def process_message(self, message, queue, **kwargs): for item, replace in self._conf_params['config']['config'].iteritems(): if item in message['text']: replace_word = random.choice(replace.split('/')) - message['text'] = re.sub(ur'\b{}\b'.format(item), replace_word, message['text'], flags=re.UNICODE) + message['text'] = re.sub(C2B_REGEXP.format(item), + r'\1{}\3'.format(replace_word), + message['text'], flags=re.UNICODE) return message def _conf_settings(self, *args, **kwargs): diff --git a/src/jenkins/cfg/chat_modules.cfg b/src/jenkins/cfg/chat_modules.cfg index 2fdb6ab..b5041aa 100644 --- a/src/jenkins/cfg/chat_modules.cfg +++ b/src/jenkins/cfg/chat_modules.cfg @@ -3,3 +3,4 @@ chats: - sc2tv - twitch - beampro +- hitbox diff --git a/src/jenkins/cfg/hitbox.cfg b/src/jenkins/cfg/hitbox.cfg new file mode 100644 index 0000000..6e53d2c --- /dev/null +++ b/src/jenkins/cfg/hitbox.cfg @@ -0,0 +1,3 @@ +config: + channels_list: + - czt diff --git a/src/jenkins/chat_tests/hitbox.sh b/src/jenkins/chat_tests/hitbox.sh new file mode 100644 index 0000000..0e0b606 --- /dev/null +++ b/src/jenkins/chat_tests/hitbox.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +PORT=8080 + +curl -s -X POST -H 'Content-Type: application/json' -d '{"nickname":"HitboxTest","text":"hbTestMessage"}' http://localhost:${PORT}/rest/hitbox/push_message + +sleep 1 + +curl -s http://localhost:${PORT}/rest/webchat/history | grep hbTestMessage diff --git a/src/jenkins/run_chat.sh b/src/jenkins/run_chat.sh index 22c40eb..54d5dc3 100644 --- a/src/jenkins/run_chat.sh +++ b/src/jenkins/run_chat.sh @@ -25,6 +25,10 @@ while [ ${ATTEMPTS} -lt 20 ]; do continue fi + if ! grep "Hitbox Testing mode online" chat.log; then + continue + fi + sleep 5 exit 0 done From 0072791f4029d61d4b26bb30305e799a0065e866 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 2 Apr 2017 17:22:44 +0300 Subject: [PATCH 08/43] LC-373 Convert python tests report to junit format --- Jenkinsfile | 2 ++ docker/dockerfiles/alpine/testing/Dockerfile | 2 +- docker/dockerfiles/fedora/testing/Dockerfile | 2 +- src/jenkins/tests_to_xml.py | 16 ++++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/jenkins/tests_to_xml.py diff --git a/Jenkinsfile b/Jenkinsfile index ba882e5..eaffe1b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -75,6 +75,8 @@ node('docker-host') { } } finally { sh 'cat chat.log' + sh "python src/jenkins/tests_to_xml.py ${container}" + junit 'results/chat_tests.xml' archive 'results/**' } } diff --git a/docker/dockerfiles/alpine/testing/Dockerfile b/docker/dockerfiles/alpine/testing/Dockerfile index 4211d09..87362ab 100644 --- a/docker/dockerfiles/alpine/testing/Dockerfile +++ b/docker/dockerfiles/alpine/testing/Dockerfile @@ -4,5 +4,5 @@ MAINTAINER CzT/DeForce # Deps for testing RUN apk --update add curl bash nodejs-npm rsync -RUN pip install git-lint pep8 pylint +RUN pip install git-lint pep8 pylint junit-xml RUN npm install -g csslint diff --git a/docker/dockerfiles/fedora/testing/Dockerfile b/docker/dockerfiles/fedora/testing/Dockerfile index 0a167da..2fa9875 100644 --- a/docker/dockerfiles/fedora/testing/Dockerfile +++ b/docker/dockerfiles/fedora/testing/Dockerfile @@ -2,5 +2,5 @@ FROM deforce/lc-fedora-build-deps # Dependancies for Testing RUN dnf -y install nodejs -RUN pip install git-lint pep8 pylint +RUN pip install git-lint pep8 pylint junit-xml RUN npm install -g csslint \ No newline at end of file diff --git a/src/jenkins/tests_to_xml.py b/src/jenkins/tests_to_xml.py new file mode 100644 index 0000000..fd3d68f --- /dev/null +++ b/src/jenkins/tests_to_xml.py @@ -0,0 +1,16 @@ +import json + +import sys +from junit_xml import TestCase, TestSuite + +if __name__ == '__main__': + docker_image = sys.argv[1] + test_cases = [] + with open('results/chat_test.txt') as chat_tests: + test_data = json.loads(chat_tests.read()) + for test_name, test_result in test_data.items(): + test_cases.append(TestCase(test_name.split('/')[-1].split('.')[0], docker_image, 1, '')) + + suite = TestSuite('Python Tests', test_cases) + with open('results/chat_tests.xml', 'w') as f: + TestSuite.to_file(f, [suite], prettyprint=False) \ No newline at end of file From 30c42f371c190d8e1ad43925def4dc6756a0762b Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 2 Apr 2017 18:48:42 +0300 Subject: [PATCH 09/43] LC-349 Fix docker jenkins --- Jenkinsfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index eaffe1b..6aeac29 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -112,7 +112,8 @@ node('docker-host') { currentBuild.result = 'UNSTABLE' } sh 'rm -rf dist/' - sh 'docker rmi -f $(docker images | grep \'^\' | awk \'{print \$3}\') || true' + sh 'docker rm $(docker ps -aq) || true' + sh 'docker rmi -f $(docker images -a | grep \'^\' | awk \'{print \$3}\') || true' deleteDir() } } From e84815f32b9afeb8503f677901ecb287ac6cbd13 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 2 Apr 2017 19:37:31 +0300 Subject: [PATCH 10/43] LC-355 Message Refactor --- Jenkinsfile | 2 +- messaging.py | 14 +- modules/chat/beampro.py | 49 ++++--- modules/chat/goodgame.py | 131 +++++++++-------- modules/chat/hitbox.py | 62 ++++---- modules/chat/sc2tv.py | 165 ++++++++++++--------- modules/chat/twitch.py | 119 ++++++++------- modules/helper/message.py | 254 +++++++++++++++++++++++++++++++++ modules/helper/module.py | 12 +- modules/helper/system.py | 20 --- modules/messaging/blacklist.py | 42 +++--- modules/messaging/c2b.py | 25 ++-- modules/messaging/df.py | 21 ++- modules/messaging/levels.py | 45 +++--- modules/messaging/logger.py | 25 ++-- modules/messaging/mentions.py | 41 +++--- modules/messaging/webchat.py | 110 +++++++++----- 17 files changed, 750 insertions(+), 387 deletions(-) create mode 100644 modules/helper/message.py diff --git a/Jenkinsfile b/Jenkinsfile index 6aeac29..0ab8bd3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -87,7 +87,7 @@ node('docker-host') { if (env.BRANCH_NAME == 'develop' || env.BRANCH_NAME == 'master') { def ZipName = env.BUILD_TAG.replace('jenkins-', '') echo ZipName - def container = 'deforce/ubuntu-builder' + def container = 'deforce/lc-ubuntu-builder' sh "cp requires_windows.txt requirements.txt" def binariesLocation = "http://repo.intra.czt.lv/lalkachat/" sh "wget -r --cut-dirs=1 -nH -np --reject index.html ${binariesLocation} " diff --git a/messaging.py b/messaging.py index f1700cc..f450866 100644 --- a/messaging.py +++ b/messaging.py @@ -8,7 +8,7 @@ import logging from collections import OrderedDict -from modules.helper.module import BaseModule +from modules.helper.module import BaseModule, MessagingModule from modules.helper.system import ModuleLoadException, THREADS, CONF_FOLDER from modules.helper.parser import load_from_config_file @@ -34,7 +34,6 @@ def __init__(self, queue): # Creating dict for dynamic modules self.modules = [] self.daemon = True - self.msg_counter = 0 self.queue = queue self.module_tag = "modules.messaging" self.threads = [] @@ -109,18 +108,11 @@ def load_modules(self, main_config, settings): return modules_list def msg_process(self, message): - if ('to' in message) and (message['to'] is not None): - message['text'] = ', '.join([message['to'], message['text']]) - - if 'id' not in message: - message['id'] = self.msg_counter - self.msg_counter += 1 # When we receive message we pass it via all loaded modules # All modules should return the message with modified/not modified # content so it can be passed to new module, or to pass to CLI - - for m_module in self.modules: - message = m_module.process_message(message, self.queue) + for m_module in self.modules: # type: MessagingModule + message = m_module.process_message(message, queue=self.queue) def run(self): for thread in range(THREADS): diff --git a/modules/chat/beampro.py b/modules/chat/beampro.py index e1da9e0..e609f59 100644 --- a/modules/chat/beampro.py +++ b/modules/chat/beampro.py @@ -12,11 +12,11 @@ import time +from modules.helper.message import TextMessage, SystemMessage, RemoveMessageByUser, RemoveMessageByID from modules.helper.module import ChatModule from ws4py.client.threadedclient import WebSocketClient -from modules.helper.system import NA_MESSAGE, system_message, translate_key, remove_message_by_id, \ - remove_message_by_user +from modules.helper.system import NA_MESSAGE, translate_key logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('beampro') @@ -48,6 +48,18 @@ class BeamProAPIException(Exception): pass +class BeamProTextMessage(TextMessage): + def __init__(self, user, text, mid): + TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=user, text=text, mid=mid) + + +class BeamProSystemMessage(SystemMessage): + def __init__(self, text, category='system'): + SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + user=SYSTEM_USER, category=category) + + class BeamProMessageHandler(threading.Thread): def __init__(self, *args, **kwargs): threading.Thread.__init__(self) @@ -77,17 +89,13 @@ def _process_event(self, message): self._process_purge_event(message) def _process_chat_message(self, message): - log.info(message) - - msg = { - 'id': ID_PREFIX.format(message['data']['id']), - 'source': SOURCE, - 'source_icon': SOURCE_ICON, - 'user': message['data']['user_name'], - 'text': self._get_text_message(message), - 'emotes': {}, - 'type': 'message' - } + log.debug(message) + + msg = BeamProTextMessage( + message['data']['user_name'], + self._get_text_message(message), + ID_PREFIX.format(message['data']['id']) + ) self._send_message(msg) @staticmethod @@ -97,8 +105,8 @@ def _get_text_message(message): def _process_delete_event(self, message): id_to_delete = ID_PREFIX.format(message['data']['id']) - self._send_message( - remove_message_by_id( + self.message_queue.put( + RemoveMessageByID( id_to_delete, text=self.main_class.conf_params()['settings'].get('remove_text') ) @@ -111,7 +119,7 @@ def _process_purge_event(self, message): raise BeamProAPIException("Unable to get user nickname") nickname = nickname_req.json() self._send_message( - remove_message_by_user( + RemoveMessageByUser( nickname, text=self.main_class.conf_params()['settings'].get('remove_text') ) @@ -119,7 +127,7 @@ def _process_purge_event(self, message): def _post_process_multiple_channels(self, message): if self.main_class.conf_params()['config']['config']['show_channel_names']: - message['channel_name'] = self.channel_nick + message.channel_name = self.channel_nick def _send_message(self, message): self._post_process_multiple_channels(message) @@ -186,8 +194,11 @@ def received_message(self, message): self.ws_queue.put(json.loads(message.data)) def system_message(self, msg, category='system'): - system_message(msg, self.main_class.queue, source=SOURCE, - icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) + self.main_class.queue.put( + BeamProSystemMessage( + msg, category + ) + ) def send_payload(self, payload): payload['id'] = self.id diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 5b56532..48d03a8 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -12,7 +12,9 @@ import logging import time from collections import OrderedDict -from modules.helper.system import system_message, translate_key, remove_message_by_id, EMOTE_FORMAT, NA_MESSAGE + +from modules.helper.message import TextMessage, SystemMessage, Emote, RemoveMessageByID +from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE from modules.helper.module import ChatModule from ws4py.client.threadedclient import WebSocketClient from gui import MODULE_KEY @@ -46,6 +48,56 @@ 'icon': FILE_ICON} +class GoodgameTextMessage(TextMessage): + def __init__(self, text, user, mid=None): + TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=user, text=text, mid=mid) + + def process_smiles(self, smiles, rights, premium, prems, payments): + emotes = {} + smiles_array = re.findall(SMILE_REGEXP, self._text) + for smile in smiles_array: + if smile in smiles: + smile_info = smiles.get(smile) + allow = False + gif = False + if rights >= 40: + allow = True + elif rights >= 20 \ + and (smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603'): + allow = True + elif smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603': + if not smile_info['is_premium']: + if smile_info['donate_lvl'] == 0: + allow = True + elif smile_info['donate_lvl'] <= int(payments): + allow = True + else: + if premium: + allow = True + + for premium_item in prems: + if smile_info['channel_id'] == str(premium_item): + if smile_info['is_premium']: + allow = True + gif = True + + if allow: + self.text = self.text.replace(SMILE_FORMAT.format(smile), EMOTE_FORMAT.format(smile)) + if smile not in emotes: + if gif and smile_info['urls']['gif']: + emotes[smile] = {'emote_url': smile_info['urls']['gif']} + else: + emotes[smile] = {'emote_url': smile_info['urls']['big']} + self._emotes = [Emote(emote, data['emote_url']) for emote, data in emotes.items()] + + +class GoodgameSystemMessage(SystemMessage): + def __init__(self, text, category='system'): + SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + user=SYSTEM_USER, category=category) + + class GoodgameMessageHandler(threading.Thread): def __init__(self, ws_class, **kwargs): super(self.__class__, self).__init__() @@ -84,20 +136,23 @@ def process_message(self, msg): def _process_message(self, msg): # Getting all needed data from received message # and sending it to queue for further message handling - comp = {'id': ID_PREFIX.format(msg['data']['message_id']), - 'source': self.source, - 'source_icon': SOURCE_ICON, - 'user': msg['data']['user_name'], - 'text': msg['data']['text'], - 'emotes': {}, - 'type': 'message'} - - self._process_smiles(comp, msg) + message = GoodgameTextMessage( + msg['data']['text'], + msg['data']['user_name'], + mid=ID_PREFIX.format(msg['data']['message_id']) + ) + message.process_smiles( + self.smiles, + msg['data'].get('user_rights', 0), + msg['data'].get('premium', 1), + msg['data'].get('premiums', []), + msg['data'].get('payments', '0') + ) - if re.match('^{0},'.format(self.nick).lower(), comp['text'].lower()): + if re.match('^{0},'.format(self.nick).lower(), message.text.lower()): if self.chat_module.conf_params()['config']['config'].get('show_pm'): - comp['pm'] = True - self._send_message(comp) + message.pm = True + self._send_message(message) def _process_join(self): self.ws_class.system_message(translate_key(MODULE_KEY.join(['goodgame', 'join_success'])).format(self.nick), @@ -115,7 +170,9 @@ def _process_user_warn(self, msg): def _process_remove_message(self, msg): remove_id = ID_PREFIX.format(msg['data']['message_id']) - self.message_queue.put(remove_message_by_id([remove_id], text=self.kwargs['settings'].get('remove_text'))) + self.message_queue.put( + RemoveMessageByID(remove_id, text=self.kwargs['settings'].get('remove_text')) + ) def _process_user_ban(self, msg): if msg['data']['duration']: @@ -146,53 +203,12 @@ def _process_channel_counters(self): def _post_process_multiple_channels(self, message): if self.chat_module.conf_params()['config']['config']['show_channel_names']: - message['channel_name'] = self.ws_class.main_thread.nick + message.channel_name = self.ws_class.main_thread.nick def _send_message(self, comp): self._post_process_multiple_channels(comp) self.message_queue.put(comp) - def _process_smiles(self, comp, msg): - smiles_array = re.findall(SMILE_REGEXP, comp['text']) - for smile in smiles_array: - if smile in self.smiles: - smile_info = self.smiles.get(smile) - allow = False - gif = False - if msg['data']['user_rights'] >= 40: - allow = True - elif msg['data']['user_rights'] >= 20 \ - and (smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603'): - allow = True - elif smile_info['channel_id'] == '0' or smile_info['channel_id'] == '10603': - if not smile_info['is_premium']: - if smile_info['donate_lvl'] == 0: - allow = True - elif smile_info['donate_lvl'] <= int(msg['data']['payments']): - allow = True - else: - if msg['data']['premium']: - allow = True - - for premium in msg['data']['premiums']: - if smile_info['channel_id'] == str(premium): - if smile_info['is_premium']: - allow = True - gif = True - - if allow: - comp['text'] = comp['text'].replace(SMILE_FORMAT.format(smile), EMOTE_FORMAT.format(smile)) - if smile not in comp['emotes']: - if gif and smile_info['urls']['gif']: - comp['emotes'][smile] = {'emote_url': smile_info['urls']['gif']} - else: - comp['emotes'][smile] = {'emote_url': smile_info['urls']['big']} - emotes_list = [] - for emote, data in comp['emotes'].iteritems(): - emotes_list.append({'emote_id': emote, - 'emote_url': data['emote_url']}) - comp['emotes'] = emotes_list - class GGChat(WebSocketClient): def __init__(self, ws, **kwargs): @@ -257,8 +273,7 @@ def received_message(self, mes): self.gg_queue.put(json.loads(str(mes))) def system_message(self, msg, category='system'): - system_message(msg, self.queue, source=SOURCE, - icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) + self.queue.put(GoodgameSystemMessage(msg, category=category)) class GGThread(threading.Thread): diff --git a/modules/chat/hitbox.py b/modules/chat/hitbox.py index a76cf73..0820409 100644 --- a/modules/chat/hitbox.py +++ b/modules/chat/hitbox.py @@ -7,14 +7,14 @@ import os import logging from collections import OrderedDict -import HTMLParser import time import requests from ws4py.client.threadedclient import WebSocketClient + +from modules.helper.message import TextMessage, Emote, SystemMessage, RemoveMessageByUser from modules.helper.module import ChatModule -from modules.helper.system import translate_key, system_message, remove_message_by_id, remove_message_by_user, \ - EMOTE_FORMAT +from modules.helper.system import translate_key, EMOTE_FORMAT logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('hitbox') @@ -50,6 +50,18 @@ class HitboxAPIError(Exception): pass +class HitboxTextMessage(TextMessage): + def __init__(self, user, text, mid, nick_colour): + TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=user, text=text, mid=mid, nick_colour=nick_colour) + + +class HitboxSystemMessage(SystemMessage): + def __init__(self, text, category='system'): + SystemMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=SYSTEM_USER, text=text, category=category) + + class HitboxMessageHandler(threading.Thread): def __init__(self, *args, **kwargs): threading.Thread.__init__(self) @@ -100,22 +112,19 @@ def _process_user_list(self, message): self.main_class.set_viewers(self.channel, viewers) def _process_chat_msg(self, message): - msg = { - 'id': ID_PREFIX.format(message['id']), - 'source': SOURCE, - 'source_icon': SOURCE_ICON, - 'user': message['name'], - 'text': message['text'], - 'emotes': {}, - 'nick_color': '#{}'.format(message['nameColor']) if self._show_color() else None, - 'type': 'message' - } + msg = HitboxTextMessage( + user=message['name'], + text=message['text'], + mid=ID_PREFIX.format(message['id']), + nick_colour='#{}'.format(message['nameColor']) if self._show_color() else None, + ) + self._send_message(msg) def _process_info_msg(self, message): user_to_delete = message['variables']['user'] - self._send_message( - remove_message_by_user( + self.message_queue.put( + RemoveMessageByUser( user_to_delete, text=self.main_class.conf_params()['settings'].get('remove_text') ) @@ -125,16 +134,16 @@ def _show_color(self): return self.main_class.conf_params()['config']['config']['show_nickname_colors'] def _post_process_emotes(self, message): - for word in message['text'].split(): + for word in message.text.split(): if word in self.smiles: - if word not in message['emotes']: - message['text'] = re.sub(SMILE_REGEXP.format(re.escape(word)), - r'\1{}\3'.format(EMOTE_FORMAT.format(word)), - message['text']) - message['emotes'][word] = { - 'emote_id': word, - 'emote_url': CDN_URL.format(self.smiles[word]) - } + message.text = re.sub(SMILE_REGEXP.format(re.escape(word)), + r'\1{}\3'.format(EMOTE_FORMAT.format(word)), + message.text) + message.emotes.append(Emote(word, CDN_URL.format(self.smiles[word]))) + + def _post_process_multiple_channels(self, message): + if self.main_class.conf_params()['config']['config']['show_channel_names']: + message.channel_name = self.channel def _send_message(self, message): self._post_process_emotes(message) @@ -268,8 +277,9 @@ def get_connection_url(url): return 'wss://{}/socket.io/1/websocket/{}'.format(url, user_id) def system_message(self, msg, category='system'): - system_message(msg, self.main_class.queue, source=SOURCE, - icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) + self.main_class.queue.put( + HitboxSystemMessage(msg, category=category) + ) class HitboxInitThread(threading.Thread): diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index 1f49f9f..367d22f 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -10,8 +10,10 @@ import logging from collections import OrderedDict from ws4py.client.threadedclient import WebSocketClient + +from modules.helper.message import TextMessage, SystemMessage, Emote from modules.helper.module import ChatModule -from modules.helper.system import system_message, translate_key, EMOTE_FORMAT +from modules.helper.system import translate_key, EMOTE_FORMAT from gui import MODULE_KEY logging.getLogger('requests').setLevel(logging.ERROR) @@ -22,6 +24,7 @@ SYSTEM_USER = 'Peka2.tv' SMILE_REGEXP = r':(\w+|\d+):' SMILE_FORMAT = ':{}:' +API_URL = 'http://funstream.tv/api{}' PING_DELAY = 10 @@ -45,6 +48,65 @@ 'icon': FILE_ICON} +class Peka2TVAPIError(Exception): + pass + + +def get_channel_name(channel_name): + payload = { + 'slug': channel_name + } + channel_req = requests.post(API_URL.format('/stream'), timeout=5, data=payload) + if channel_req.ok: + return channel_req.json()['owner']['name'] + raise Peka2TVAPIError("Unable to get channel name") + + +def allow_smile(smile, subscriptions, allow=False): + if smile['user']: + channel_id = smile['user']['id'] + for sub in subscriptions: + if sub == channel_id: + allow = True + else: + allow = True + return allow + + +class FsChatMessage(TextMessage): + def __init__(self, user, text, subscr): + self._user = user + self._text = text + self._subscriptions = subscr + + TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=self.user, text=self.text) + + def process_smiles(self, smiles): + smiles_array = re.findall(SMILE_REGEXP, self._text) + for smile in smiles_array: + for smile_find in smiles: + if smile_find['code'] == smile.lower(): + if allow_smile(smile_find, self._subscriptions): + self._text = self._text.replace(SMILE_FORMAT.format(smile), + EMOTE_FORMAT.format(smile)) + self._emotes.append(Emote(smile, smile_find['url'])) + + def process_pm(self, to_name, channel_name, show_pm): + self.text = u'@{},{}'.format(to_name, self.text) + if to_name == channel_name: + if show_pm: + self._pm = True + + +class FsSystemMessage(SystemMessage): + def __init__(self, text, emotes=None, category='system'): + if emotes is None: + emotes = [] + SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + user=SYSTEM_USER, emotes=emotes, category=category) + + class FsChat(WebSocketClient): def __init__(self, ws, queue, channel_name, **kwargs): super(self.__class__, self).__init__(ws, protocols=kwargs.get('protocols', None)) @@ -52,8 +114,9 @@ def __init__(self, ws, queue, channel_name, **kwargs): self.source = SOURCE self.queue = queue self.channel_name = channel_name + self.glob = kwargs.get('glob') self.main_thread = kwargs.get('main_thread') # type: FsThread - self.chat_module = kwargs.get('chat_module') + self.chat_module = kwargs.get('chat_module') # type: sc2tv self.crit_error = False self.channel_id = self.fs_get_id() @@ -79,36 +142,22 @@ def closed(self, code, reason=None): :param code: :param reason: """ - self.chat_module.set_offline(self.channel_name) + self.chat_module.set_offline(self.glob) if code in [4000, 4001]: self.crit_error = True self.fs_system_message(translate_key( - MODULE_KEY.join(['sc2tv', 'connection_closed'])).format(self.channel_name), + MODULE_KEY.join(['sc2tv', 'connection_closed'])).format(self.glob), category='connection') else: log.info("Websocket Connection Closed Down") self.fs_system_message( - translate_key(MODULE_KEY.join(['sc2tv', 'connection_died'])).format(self.channel_name), + translate_key(MODULE_KEY.join(['sc2tv', 'connection_died'])).format(self.glob), category='connection') timer = threading.Timer(5.0, self.main_thread.connect) timer.start() def fs_system_message(self, message, category='system'): - system_message(message, self.queue, source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) - - @staticmethod - def allow_smile(smile, subscriptions): - allow = False - - if smile['user']: - channel_id = smile['user']['id'] - for sub in subscriptions: - if sub == channel_id: - allow = True - else: - allow = True - - return allow + self.queue.put(FsSystemMessage(message, category=category)) def received_message(self, mes): if mes.data == '40': @@ -132,7 +181,7 @@ def fs_get_id(self): 'name': self.channel_name } try: - request = requests.post("http://funstream.tv/api/user", data=payload, timeout=5) + request = requests.post(API_URL.format("/user"), data=payload, timeout=5) if request.status_code == 200: channel_id = json.loads(re.findall('{.*}', request.text)[0])['id'] return channel_id @@ -140,10 +189,10 @@ def fs_get_id(self): error_message = request.json() if 'message' in error_message: log.error("Unable to get channel ID. {0}".format(error_message['message'])) - self.closed(0, 'INV_CH_ID') + self.closed(1000, 'INV_CH_ID') else: log.error("Unable to get channel ID. No message available") - self.closed(0, 'INV_CH_ID') + self.closed(1000, 'INV_CH_ID') except requests.ConnectionError: log.info("Unable to get information from api") return None @@ -161,7 +210,7 @@ def fs_join(self): self.fs_send(payload) msg_joining = translate_key(MODULE_KEY.join(['sc2tv', 'joining'])) - self.fs_system_message(msg_joining.format(self.channel_name), category='connection') + self.fs_system_message(msg_joining.format(self.glob), category='connection') log.info(msg_joining.format(self.channel_id)) def fs_send(self, payload): @@ -211,45 +260,28 @@ def _process_message(self, message): try: self.duplicates.index(message['id']) except ValueError: - comp = {'source': self.source, - 'source_icon': SOURCE_ICON, - 'user': message['from']['name'], - 'text': message['text'], - 'emotes': [], - 'type': 'message'} - if message['to'] is not None: - comp['to'] = message['to']['name'] - if comp['to'] == self.channel_name: - if self.chat_module.conf_params()['config']['config'].get('show_pm'): - comp['pm'] = True - else: - comp['to'] = None - - smiles_array = re.findall(SMILE_REGEXP, comp['text']) - for smile in smiles_array: - for smile_find in self.smiles: - if smile_find['code'] == smile.lower(): - if self.allow_smile(smile_find, message['store']['subscriptions']): - comp['text'] = comp['text'].replace(SMILE_FORMAT.format(smile), - EMOTE_FORMAT.format(smile)) - comp['emotes'].append({'emote_id': smile, 'emote_url': smile_find['url']}) + msg = FsChatMessage(message['from']['name'], message['text'], message['store']['subscriptions']) + msg.process_smiles(self.smiles) + if message['to']: + msg.process_pm(message['to'].get('name'), self.channel_name, + self.chat_module.conf_params()['config']['config'].get('show_pm')) self.duplicates.append(message['id']) if len(self.duplicates) > self.bufferForDup: self.duplicates.pop(0) - self._send_message(comp) + self._send_message(msg) def _process_joined(self): - self.chat_module.set_online(self.channel_name) + self.chat_module.set_online(self.glob) self.fs_system_message( - translate_key(MODULE_KEY.join(['sc2tv', 'join_success'])).format(self.channel_name), category='connection') + translate_key(MODULE_KEY.join(['sc2tv', 'join_success'])).format(self.glob), category='connection') def _process_channel_list(self, message): - self.chat_module.set_viewers(self.channel_name, message['result']['amount']) + self.chat_module.set_viewers(self.glob, message['result']['amount']) def _post_process_multiple_channels(self, message): if self.chat_module.conf_params()['config']['config']['show_channel_names']: - message['channel_name'] = self.channel_name + message.channel_name = self.glob def _send_message(self, comp): self._post_process_multiple_channels(comp) @@ -279,7 +311,8 @@ def __init__(self, queue, socket, channel_name, **kwargs): self.daemon = "True" self.queue = queue self.socket = socket - self.channel_name = channel_name + self.channel_name = get_channel_name(channel_name) + self.glob = channel_name self.chat_module = kwargs.get('chat_module') self.smiles = [] self.ws = None @@ -294,16 +327,9 @@ def connect(self): while True: try_count += 1 log.info("Connecting, try {0}".format(try_count)) - if not self.smiles: - try: - smiles = requests.post('http://funstream.tv/api/smile', timeout=5) - if smiles.status_code == 200: - smiles_answer = smiles.json() - for smile in smiles_answer: - self.smiles.append(smile) - except requests.ConnectionError: - log.error("Unable to get smiles") - self.ws = FsChat(self.socket, self.queue, self.channel_name, protocols=['websocket'], smiles=self.smiles, + self._get_info() + self.ws = FsChat(self.socket, self.queue, self.channel_name, glob=self.glob, + protocols=['websocket'], smiles=self.smiles, main_thread=self, **self.kwargs) if self.ws.crit_error: log.critical("Got critical error, halting") @@ -318,6 +344,17 @@ def stop(self): self.ws.send("11") self.ws.close(4000, reason="CLOSE_OK") + def _get_info(self): + if not self.smiles: + try: + smiles = requests.post(API_URL.format('/smile'), timeout=5) + if smiles.status_code == 200: + smiles_answer = smiles.json() + for smile in smiles_answer: + self.smiles.append(smile) + except requests.ConnectionError: + log.error("Unable to get smiles") + class Sc2tvMessage(object): def __init__(self, nickname, text): @@ -383,14 +420,14 @@ def get_viewers(self, ws): request = ['/chat/channel/list', {'channel': 'stream/{0}'.format(str(ws.channel_id))}] try: - user_request = requests.post('http://funstream.tv/api/user', timeout=5, data=user_data) + user_request = requests.post(API_URL.format('/user'), timeout=5, data=user_data) if user_request.status_code == 200: status_data['slug'] = user_request.json()['slug'] except requests.ConnectionError: log.error("Unable to get smiles") try: - status_request = requests.post('http://funstream.tv/api/stream', timeout=5, data=status_data) + status_request = requests.post(API_URL.format('/stream'), timeout=5, data=status_data) if status_request.status_code == 200: if status_request.json()['online']: self.set_online(ws.channel_name) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 91f30af..488310c 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -9,9 +9,10 @@ import Queue import time from collections import OrderedDict -from modules.helper.parser import load_from_config_file + +from modules.helper.message import TextMessage, SystemMessage, Badge, Emote, RemoveMessageByUser from modules.helper.module import ChatModule -from modules.helper.system import system_message, translate_key, remove_message_by_user, EMOTE_FORMAT, NA_MESSAGE +from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE from gui import MODULE_KEY logging.getLogger('irc').setLevel(logging.ERROR) @@ -61,6 +62,25 @@ class TwitchNormalDisconnect(Exception): """Normal Disconnect exception""" +class TwitchTextMessage(TextMessage): + def __init__(self, user, text): + self.bttv_emotes = {} + TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + user=user, text=text) + + +class TwitchSystemMessage(SystemMessage): + def __init__(self, text, category='system', emotes=None): + SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + user=SYSTEM_USER, emotes=emotes, category=category) + + +class TwitchEmote(Emote): + def __init__(self, emote_id, emote_url, positions): + Emote.__init__(self, emote_id=emote_id, emote_url=emote_url) + self.positions = positions + + class TwitchMessageHandler(threading.Thread): def __init__(self, queue, twitch_queue, **kwargs): super(self.__class__, self).__init__() @@ -114,37 +134,41 @@ def _handle_badges(self, message, badges): url = badge_info.get('image_url_4x') else: url = NOT_FOUND - message['badges'].append({'badge': badge_tag, 'size': badge_size, 'url': url}) + message.badges.append(Badge(badge_tag, url)) @staticmethod def _handle_display_name(message, name): - message['display_name'] = name if name else message['user'] + message.display_name = name if name else message.user + message.jsonable += ['display_name'] @staticmethod def _handle_emotes(message, tag_value): for emote in tag_value.split('/'): emote_id, emote_pos_diap = emote.split(':') - message['emotes'].append({'emote_id': emote_id, - 'positions': emote_pos_diap.split(','), - 'emote_url': EMOTE_SMILE_URL.format(id=emote_id)}) + message.emotes.append( + TwitchEmote(emote_id, + EMOTE_SMILE_URL.format(id=emote_id), + emote_pos_diap.split(',')) + ) def _handle_bttv_smiles(self, message): - for word in message['text'].split(): + for word in message.text.split(): if word in self.bttv: bttv_smile = self.bttv.get(word) - message['bttv_emotes'][bttv_smile['regex']] = { - 'emote_id': bttv_smile['regex'], - 'emote_url': 'https:{0}'.format(bttv_smile['url']) - } + message.bttv_emotes[bttv_smile['regex']] = Emote( + bttv_smile['regex'], + 'https:{0}'.format(bttv_smile['url']) + ) def _handle_pm(self, message): - if re.match('^@?{0}[ ,]?'.format(self.nick), message['text'].lower()): + if re.match('^@?{0}[ ,]?'.format(self.nick), message.text.lower()): if self.chat_module.conf_params()['config']['config'].get('show_pm'): - message['pm'] = True + message.pm = True def _handle_clearchat(self, msg): - self.message_queue.put(remove_message_by_user(msg.arguments, - text=self.kwargs['settings'].get('remove_text'))) + self.message_queue.put( + RemoveMessageByUser(msg.arguments, + text=self.kwargs['settings'].get('remove_text'))) def _handle_usernotice(self, msg): for tag in msg.tags: @@ -157,20 +181,9 @@ def _handle_usernotice(self, msg): self._handle_message(msg, sub_message=True) def _handle_message(self, msg, sub_message=False): - message = {'source': self.source, - 'source_icon': SOURCE_ICON, - 'badges': [], - 'emotes': [], - 'bttv_emotes': {}, - 'user': msg.source.split('!')[0], - 'type': 'message', - 'msg_type': msg.type} - - if message['user'] == 'twitchnotify': - self.irc_class.system_message(msg.arguments.pop(), category='chat') - return - - message['text'] = msg.arguments.pop() + message = TwitchTextMessage(msg.source.split('!')[0], msg.arguments.pop()) + if message.user == 'twitchnotify': + self.irc_class.queue.put(TwitchSystemMessage(message.text, category='chat')) for tag in msg.tags: tag_value, tag_key = tag.values() @@ -195,11 +208,11 @@ def _handle_message(self, msg, sub_message=False): def _handle_viewer_color(self, message, value): if self.irc_class.chat_module.conf_params()['config']['config']['show_nickname_colors']: - message['nick_color'] = value + message.nick_color = value @staticmethod def _handle_bits(message, amount): - regexp = re.search(BITS_REGEXP.format(amount), message['text']) + regexp = re.search(BITS_REGEXP.format(amount), message.text) emote = regexp.group(2) emote_smile = '{}{}'.format(emote, amount) @@ -214,7 +227,7 @@ def _handle_bits(message, amount): else: color = 'gray' - message['bits'] = { + message.bits = { 'bits': emote_smile, 'amount': amount, 'theme': BITS_THEME, @@ -222,11 +235,12 @@ def _handle_bits(message, amount): 'color': color, 'size': 4 } - message['text'] = message['text'].replace(emote_smile, EMOTE_FORMAT.format(emote_smile)) + message.text = message.text.replace(emote_smile, EMOTE_FORMAT.format(emote_smile)) @staticmethod def _handle_sub_message(message): - message['sub_message'] = True + message.sub_message = True + message.jsonable += ['sub_message'] def _send_message(self, message): self._post_process_emotes(message) @@ -237,45 +251,45 @@ def _send_message(self, message): @staticmethod def _post_process_bits(message): - if 'bits' not in message: + if not hasattr(message, 'bits'): return - bits = message['bits'] - message['emotes'].append({ - 'emote_id': bits['bits'], - 'emote_url': BITS_URL.format( + bits = message.bits + message['emotes'].append(Emote( + bits['bits'], + BITS_URL.format( theme=bits['theme'], type=bits['type'], color=bits['color'], size=bits['size'] ) - }) + )) @staticmethod def _post_process_emotes(message): conveyor_emotes = [] - for emote in message['emotes']: - for position in emote['positions']: + for emote in message.emotes: + for position in emote.positions: start, end = position.split('-') - conveyor_emotes.append({'emote_id': emote['emote_id'], + conveyor_emotes.append({'emote_id': emote.id, 'start': int(start), 'end': int(end)}) conveyor_emotes = sorted(conveyor_emotes, key=lambda k: k['start'], reverse=True) for emote in conveyor_emotes: - message['text'] = u'{start}{emote}{end}'.format(start=message['text'][:emote['start']], - end=message['text'][emote['end'] + 1:], - emote=EMOTE_FORMAT.format(emote['emote_id'])) + message.text = u'{start}{emote}{end}'.format(start=message.text[:emote['start']], + end=message.text[emote['end'] + 1:], + emote=EMOTE_FORMAT.format(emote['emote_id'])) @staticmethod def _post_process_bttv_emotes(message): - for emote, data in message['bttv_emotes'].iteritems(): - message['text'] = message['text'].replace(emote, EMOTE_FORMAT.format(emote)) - message['emotes'].append(data) + for emote, data in message.bttv_emotes.iteritems(): + message.text = message.text.replace(emote, EMOTE_FORMAT.format(emote)) + message.emotes.append(data) def _post_process_multiple_channels(self, message): channel_class = self.irc_class.main_class if channel_class.chat_module.conf_params()['config']['config']['show_channel_names']: - message['channel_name'] = channel_class.display_name + message.channel_name = channel_class.display_name class TwitchPingHandler(threading.Thread): @@ -316,8 +330,7 @@ def __init__(self, queue, channel, **kwargs): self.msg_handler.start() def system_message(self, message, category='system'): - system_message(message, self.queue, - source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER, category=category) + self.queue.put(TwitchSystemMessage(message, category=category)) def on_disconnect(self, connection, event): if 'CLOSE_OK' in event.arguments: diff --git a/modules/helper/message.py b/modules/helper/message.py new file mode 100644 index 0000000..0f54904 --- /dev/null +++ b/modules/helper/message.py @@ -0,0 +1,254 @@ +import uuid +import logging +import datetime +from modules.helper.system import SOURCE, SOURCE_ICON, SOURCE_USER + +log = logging.getLogger('helper.message') + +AVAILABLE_COMMANDS = ['remove_by_user', 'remove_by_id', 'replace_by_user', 'replace_by_id'] + + +def _validate_command(command): + """ + Checks if command exists and raises an error + if it doesn't exist + :param command: command string + :return: command string + """ + if command not in AVAILABLE_COMMANDS: + raise NonExistentCommand() + return command + + +def process_text_messages(func): + def validate_class(self_class, message, **kwargs): + if message: + if isinstance(message, TextMessage): + return func(self_class, message, **kwargs) + return message + return validate_class + + +def ignore_system_messages(func): + def validate_class(self_class, message, **kwargs): + if not isinstance(message, SystemMessage): + return func(self_class, message, **kwargs) + return message + return validate_class + + +class NonExistentCommand(Exception): + pass + + +class Message(object): + def __init__(self): + """ + Basic Message class + """ + self._jsonable = [] + self._timestamp = datetime.datetime.now().isoformat() + + def json(self): + return {attr: getattr(self, attr) for attr in self._jsonable} + + @property + def timestamp(self): + return self._timestamp + + @property + def jsonable(self): + return self._jsonable + + @jsonable.setter + def jsonable(self, value): + if isinstance(value, list): + self._jsonable = value + + +class CommandMessage(Message): + def __init__(self, command=''): + """ + Command Message class + Used to control chat behaviour + :param command: Which command to use + """ + Message.__init__(self) + self._command = _validate_command(command) + self._jsonable += ['command'] + + @property + def command(self): + return self._command + + +class RemoveMessageByUser(CommandMessage): + def __init__(self, user, text=None): + if text: + CommandMessage.__init__(self, command='remove_by_user') + self.text = text + else: + CommandMessage.__init__(self, command='replace_by_user') + self._user = user if isinstance(user, list) else [user] + self._jsonable += ['user'] + + @property + def user(self): + return self._user + + +class RemoveMessageByID(CommandMessage): + def __init__(self, message_id, text=None): + if text: + CommandMessage.__init__(self, command='remove_by_user') + self.text = text + else: + CommandMessage.__init__(self, command='replace_by_user') + self._message_ids = message_id if isinstance(message_id, list) else [message_id] + self._jsonable += ['user'] + + @property + def message_ids(self): + return self._message_ids + + +class TextMessage(Message): + def __init__(self, source, source_icon, user, text, + emotes=None, badges=None, pm=False, + nick_colour=None, mid=None): + """ + Text message used by main chat logic + :param source: Chat source (gg/twitch/beampro etc.) + :param source_icon: Chat icon (as url) + :param user: nickname + :param text: message text + :param emotes: + :param pm: + """ + Message.__init__(self) + + self._source = source + self._source_icon = source_icon + self._user = user + self._text = text + self._emotes = [] if emotes is None else emotes + self._badges = [] if badges is None else badges + self._pm = pm + self._nick_colour = nick_colour + self._channel_name = None + self._id = str(mid) if mid else str(uuid.uuid1()) + + self._jsonable += ['user', 'text', 'emotes', 'badges', + 'id', 'source', 'source_icon', 'pm', + 'nick_colour', 'channel_name'] + + @property + def source(self): + return self._source + + @property + def source_icon(self): + return self._source_icon + + @property + def user(self): + return self._user + + @property + def text(self): + """ + :rtype: str + """ + return self._text + + @text.setter + def text(self, value): + self._text = value + + @property + def emotes(self): + return self._emotes + + @emotes.setter + def emotes(self, value): + self._emotes = value + + @property + def badges(self): + return self._badges + + @badges.setter + def badges(self, value): + self._badges = value + + @property + def pm(self): + return self._pm + + @pm.setter + def pm(self, value): + self._pm = value + + @property + def nick_colour(self): + return self._nick_colour + + @nick_colour.setter + def nick_colour(self, value): + self._nick_colour = value + + @property + def channel_name(self): + return self._channel_name + + @channel_name.setter + def channel_name(self, value): + self._channel_name = value + + @property + def id(self): + return self._id + + +class SystemMessage(TextMessage): + def __init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, user=SOURCE_USER, emotes=None, category='system'): + """ + Text message used by main chat logic + Serves system messages from modules + :param source: TextMessage.source + :param source_icon: TextMessage.source_icon + :param user: TextMessage.user + :param text: TextMessage.text + :param category: System message category, can be filtered + """ + if emotes is None: + emotes = [] + TextMessage.__init__(self, source, source_icon, user, text, emotes) + self._category = category + + @property + def category(self): + return self._category + + @property + def user(self): + return self._user + + +class Emote(object): + def __init__(self, emote_id, emote_url): + self._id = emote_id + self._url = emote_url + + @property + def id(self): + return self._id + + @property + def url(self): + return self._url + + +class Badge(Emote): + def __init__(self, badge_id, badge_url): + Emote.__init__(self, badge_id, badge_url) diff --git a/modules/helper/module.py b/modules/helper/module.py index 5db7c18..001caae 100644 --- a/modules/helper/module.py +++ b/modules/helper/module.py @@ -4,6 +4,7 @@ from collections import OrderedDict from modules.helper import parser +from modules.helper.message import TextMessage, Message from parser import save_settings, load_from_config_file from system import RestApiException, CONF_FOLDER @@ -130,7 +131,16 @@ class MessagingModule(BaseModule): def __init__(self, *args, **kwargs): BaseModule.__init__(self, *args, **kwargs) - def process_message(self, message, queue, **kwargs): + def process_message(self, message, queue=None): + """ + + :param message: Received Message class + :type message: TextMessage + :param queue: Main queue + :type queue: Queue.Queue + :return: Message Class, could be None if message is "cleared" + :rtype: Message + """ return message diff --git a/modules/helper/system.py b/modules/helper/system.py index 3a51710..be7b4cf 100644 --- a/modules/helper/system.py +++ b/modules/helper/system.py @@ -142,26 +142,6 @@ def random_string(length): return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)) -def remove_message_by_user(user, text=None): - command = {'type': 'command', - 'command': 'remove_by_user', - 'user': user if isinstance(user, list) else [user]} - if text: - command['text'] = text - command['command'] = 'replace_by_user' - return command - - -def remove_message_by_id(ids, text=None): - command = {'type': 'command', - 'command': 'remove_by_id', - 'ids': ids if isinstance(ids, list) else [ids]} - if text: - command['text'] = text - command['command'] = 'replace_by_id' - return command - - def get_update(sem_version): github_url = "https://api.github.com/repos/DeForce/LalkaChat/releases" try: diff --git a/modules/messaging/blacklist.py b/modules/messaging/blacklist.py index 3e1353c..0f1a869 100644 --- a/modules/messaging/blacklist.py +++ b/modules/messaging/blacklist.py @@ -3,8 +3,11 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import re from collections import OrderedDict + +import logging + +from modules.helper.message import ignore_system_messages, process_text_messages from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 30 @@ -17,6 +20,7 @@ CONF_DICT['users_block'] = [] CONF_DICT['words_hide'] = [] CONF_DICT['words_block'] = [] +log = logging.getLogger('blacklist') class blacklist(MessagingModule): @@ -43,24 +47,26 @@ def _gui_settings(self): 'non_dynamic': ['main.*'] } - def process_message(self, message, queue, **kwargs): - if message: - if message['type'] in IGNORED_TYPES: - return message + @process_text_messages + @ignore_system_messages + def process_message(self, message, **kwargs): + self._blocked(message) + if self._hidden(message): + message.hidden = True + return message - if message['user'].lower() in self._conf_params['config']['users_hide']: - return + def _hidden(self, message): + if message.user.lower() in self._conf_params['config']['users_hide']: + return True - for word in self._conf_params['config']['words_hide']: - if re.search(word, message['text'].encode('utf-8')): - return + for word in self._conf_params['config']['words_hide']: + if re.search(word, message.text.encode('utf-8')): + return True - if message['user'].lower() in self._conf_params['config']['users_block']: - message['text'] = self._conf_params['config']['main']['message'] - return message + def _blocked(self, message): + if message.user.lower() in self._conf_params['config']['users_block']: + message.text = self._conf_params['config']['main']['message'] - for word in self._conf_params['config']['words_block']: - if re.search(word, message['text'].encode('utf-8')): - message['text'] = self._conf_params['config']['main']['message'] - return message - return message + for word in self._conf_params['config']['words_block']: + if re.search(word, message.text.encode('utf-8')): + message.text = self._conf_params['config']['main']['message'] diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 034db72..4f8717b 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -5,8 +5,9 @@ import random import re from collections import OrderedDict + +from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES DEFAULT_PRIORITY = 10 log = logging.getLogger('c2b') @@ -49,20 +50,18 @@ class c2b(MessagingModule): def __init__(self, *args, **kwargs): MessagingModule.__init__(self, *args, **kwargs) - def process_message(self, message, queue, **kwargs): + @process_text_messages + @ignore_system_messages + def process_message(self, message, **kwargs): # Replacing the message if needed. # Please do the needful - if message: - if message['type'] in IGNORED_TYPES: - return message - - for item, replace in self._conf_params['config']['config'].iteritems(): - if item in message['text']: - replace_word = random.choice(replace.split('/')) - message['text'] = re.sub(C2B_REGEXP.format(item), - r'\1{}\3'.format(replace_word), - message['text'], flags=re.UNICODE) - return message + for item, replace in self._conf_params['config']['config'].iteritems(): + if item in message.text: + replace_word = random.choice(replace.split('/')) + message.text = re.sub(C2B_REGEXP.format(item), + r'\1{}\3'.format(replace_word), + message.text, flags=re.UNICODE) + return message def _conf_settings(self, *args, **kwargs): return CONF_DICT diff --git a/modules/messaging/df.py b/modules/messaging/df.py index dbe8d3d..ce1875f 100644 --- a/modules/messaging/df.py +++ b/modules/messaging/df.py @@ -5,8 +5,8 @@ import os from collections import OrderedDict +from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES CONF_DICT = OrderedDict() CONF_DICT['gui_information'] = {'category': 'messaging'} @@ -50,13 +50,12 @@ def write_to_file(self, user, role): with open(self.file, 'a') as a_file: a_file.write("{0},{1}\n".format(user, role)) - def process_message(self, message, queue, **kwargs): - if message: - if message['type'] in IGNORED_TYPES: - return message - for role, regexp in self._conf_params['config']['prof'].iteritems(): - if re.search('{0}{1}'.format(self._conf_params['config']['grep']['symbol'], regexp).decode('utf-8'), - message['text']): - self.write_to_file(message['user'], role.capitalize()) - break - return message + @process_text_messages + @ignore_system_messages + def process_message(self, message, **kwargs): + for role, regexp in self._conf_params['config']['prof'].iteritems(): + if re.search('{0}{1}'.format(self._conf_params['config']['grep']['symbol'], regexp).decode('utf-8'), + message.text): + self.write_to_file(message.user, role.capitalize()) + break + return message diff --git a/modules/messaging/levels.py b/modules/messaging/levels.py index e8ef2ee..606c116 100644 --- a/modules/messaging/levels.py +++ b/modules/messaging/levels.py @@ -10,8 +10,9 @@ from collections import OrderedDict import datetime +from modules.helper.message import process_text_messages, SystemMessage, ignore_system_messages from modules.helper.parser import save_settings -from modules.helper.system import system_message, ModuleLoadException, IGNORED_TYPES +from modules.helper.system import ModuleLoadException from modules.helper.module import MessagingModule log = logging.getLogger('levels') @@ -152,8 +153,6 @@ def apply_settings(self, **kwargs): self.load_levels() def set_level(self, user, queue): - if user == 'System': - return [] db = sqlite3.connect(self.db_location) cursor = db.cursor() @@ -187,29 +186,31 @@ def set_level(self, user, queue): db.commit() else: max_level += 1 - system_message( - self._conf_params['config']['config']['message'].decode('utf-8').format( - user, - self.levels[max_level]['name']), - queue, category='module' + queue.put( + SystemMessage( + self._conf_params['config']['config']['message'].decode('utf-8').format( + user, + self.levels[max_level]['name']), + category='module' + ) ) cursor.close() return self.levels[max_level].copy() - def process_message(self, message, queue, **kwargs): - if message: - if message['type'] in IGNORED_TYPES: - return message - if 'system_msg' not in message or not message['system_msg']: - if 'user' in message and message['user'] in self.special_levels: - level_info = self.special_levels[message['user']] - if 's_levels' in message: - message['s_levels'].append(level_info.copy()) - else: - message['s_levels'] = [level_info.copy()] - - message['levels'] = self.set_level(message['user'], queue) - return message + @process_text_messages + @ignore_system_messages + def process_message(self, message, queue=None, **kwargs): + if message.user in self.special_levels: + level_info = self.special_levels[message.user] + try: + message.s_levels.append(level_info.copy()) + except AttributeError: + message.s_levels = [level_info.copy()] + message.jsonable.append('s_levels') + + message.levels = self.set_level(message.user, queue) + message.jsonable.append('levels') + return message def calculate_experience(self, user): exp_to_add = self.exp_for_message diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index 87a4abe..0ccecfd 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -5,8 +5,9 @@ import datetime from collections import OrderedDict +from modules.helper.message import process_text_messages from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES, CONF_FOLDER +from modules.helper.system import CONF_FOLDER DEFAULT_PRIORITY = 20 @@ -45,15 +46,13 @@ def _conf_settings(self, *args, **kwargs): def _gui_settings(self, *args, **kwargs): return CONF_GUI - def process_message(self, message, queue, **kwargs): - if message: - if message['type'] in IGNORED_TYPES: - return message - with open('{0}.txt'.format( - os.path.join(self.destination, datetime.datetime.now().strftime(self.format))), 'a') as f: - f.write('[{3}] [{0}] {1}: {2}\n'.format( - message['source'].encode('utf-8'), - message['user'].encode('utf-8'), - message['text'].encode('utf-8'), - datetime.datetime.now().strftime(self.ts_format).encode('utf-8'))) - return message + @process_text_messages + def process_message(self, message, **kwargs): + with open('{0}.txt'.format( + os.path.join(self.destination, datetime.datetime.now().strftime(self.format))), 'a') as f: + f.write('[{3}] [{0}] {1}: {2}\n'.format( + message.source.encode('utf-8'), + message.user.encode('utf-8'), + message.text.encode('utf-8'), + datetime.datetime.now().strftime(self.ts_format).encode('utf-8'))) + return message diff --git a/modules/messaging/mentions.py b/modules/messaging/mentions.py index bd8eb29..2aaf21d 100644 --- a/modules/messaging/mentions.py +++ b/modules/messaging/mentions.py @@ -4,8 +4,8 @@ import re from collections import OrderedDict +from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule -from modules.helper.system import IGNORED_TYPES CONF_DICT = OrderedDict() CONF_DICT['gui_information'] = {'category': 'messaging'} @@ -33,24 +33,25 @@ def _conf_settings(self, *args, **kwargs): def _gui_settings(self, *args, **kwargs): return CONF_GUI - def process_message(self, message, queue, **kwargs): + @process_text_messages + @ignore_system_messages + def process_message(self, message, **kwargs): # Replacing the message if needed. # Please do the needful - if message: - if message['type'] in IGNORED_TYPES: - return message - - for mention in self._conf_params['config']['mentions']: - if re.search(mention, message['text'].lower()): - message['mention'] = True - break - - for address in self._conf_params['config']['address']: - if re.match(address, message['text'].lower()): - message['pm'] = True - break - - if 'mention' in message and 'pm' in message: - message.pop('mention') - - return message + self._check_addressed(message) + if not message.pm: + self._check_mentions(message) + return message + + def _check_mentions(self, message): + for mention in self._conf_params['config']['mentions']: + if re.search(mention, message.text.lower()): + message.mention = True + message.jsonable += ['mention'] + break + + def _check_addressed(self, message): + for address in self._conf_params['config']['address']: + if re.match(address, message.text.lower()): + message.pm = True + break diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 5ec4543..d05dc33 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -15,16 +15,17 @@ from cherrypy.lib.static import serve_file from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket + +from modules.helper.message import TextMessage, CommandMessage, SystemMessage, RemoveMessageByID from modules.helper.parser import save_settings -from modules.helper.system import THREADS, PYTHON_FOLDER, CONF_FOLDER, remove_message_by_id +from modules.helper.system import THREADS, PYTHON_FOLDER, CONF_FOLDER from modules.helper.module import MessagingModule from gui import MODULE_KEY logging.getLogger('ws4py').setLevel(logging.ERROR) DEFAULT_STYLE = 'default' DEFAULT_PRIORITY = 9001 -HISTORY_SIZE = 20 -HISTORY_TYPES = ['system_message', 'message'] +HISTORY_SIZE = 5 HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") s_queue = Queue.Queue() log = logging.getLogger('webchat') @@ -46,8 +47,21 @@ CONF_DICT['style_settings'] = OrderedDict() CONF_DICT['style_settings']['show_system_msg'] = True +TYPE_DICT = { + TextMessage: 'message', + CommandMessage: 'command' +} + + +def process_emotes(emotes): + return [{'emote_id': emote.id, 'emote_url': emote.url} for emote in emotes] -def prepare_message(msg, style_settings): + +def process_badges(badges): + return [{'badge': badge.id, 'url': badge.url} for badge in badges] + + +def prepare_message(msg, style_settings, msg_class): message = copy.deepcopy(msg) if 'levels' in message: @@ -56,14 +70,36 @@ def prepare_message(msg, style_settings): if 'text' in message and message['text'] == REMOVED_TRIGGER: message['text'] = style_settings.get('remove_text') + if 'type' not in message: + for m_class, m_type in TYPE_DICT.items(): + if issubclass(msg_class, m_class): + message['type'] = m_type + + if 'emotes' in message: + message['emotes'] = process_emotes(message['emotes']) + + if 'badges' in message: + message['badges'] = process_badges(message['badges']) + if 'command' in message: if message['command'].startswith('replace'): message['text'] = style_settings['keys']['remove_text'] + return message message['id'] = str(message['id']) return message +def add_to_history(message): + if isinstance(message, TextMessage): + cherrypy.engine.publish('add-history', message) + + +def process_command(message): + if isinstance(message, CommandMessage): + cherrypy.engine.publish('process-command', message.command, message) + + class MessagingThread(threading.Thread): def __init__(self, settings): super(self.__class__, self).__init__() @@ -75,18 +111,16 @@ def run(self): while self.running: message = s_queue.get() - if 'timestamp' not in message: - message['timestamp'] = datetime.datetime.now().isoformat() + if isinstance(message, dict): + raise Exception("Got dict message {}".format(message)) - if message['type'] in HISTORY_TYPES: - cherrypy.engine.publish('add-history', message) - elif message['type'] == 'command': - cherrypy.engine.publish('process-command', message['command'], message) + add_to_history(message) + process_command(message) - if message['type'] == 'system_message' and not self.settings['chat']['keys'].get('show_system_msg', True): + if isinstance(message, SystemMessage) and not self.settings['chat']['keys'].get('show_system_msg', True): continue - log.debug("%s", message) + log.debug("%s", message.json()) self.send_message(message, 'chat') self.send_message(message, 'gui') log.info("Messaging thread stopping") @@ -95,12 +129,13 @@ def stop(self): self.running = False def send_message(self, message, chat_type): - send_message = prepare_message(message, self.settings[chat_type]) + send_message = prepare_message(message.json(), self.settings[chat_type], type(message)) ws_list = cherrypy.engine.publish('get-clients', chat_type)[0] for ws in ws_list: try: ws.send(json.dumps(send_message)) - except: + except Exception as exc: + log.exception(exc) log.info(send_message) @@ -116,19 +151,16 @@ def run(self): show_system_msg = self.settings['keys'].get('show_system_msg', True) if self.ws.stream: for item in self.history: - if item['type'] == 'system_message' and not show_system_msg: + if isinstance(item, SystemMessage) and not show_system_msg: continue - try: - timestamp = datetime.datetime.strptime(item['timestamp'], "%Y-%m-%dT%H:%M:%S.%f") - except: - timestamp = datetime.datetime.strptime(item['timestamp'], "%Y-%m-%dT%H:%M:%S") + timestamp = datetime.datetime.strptime(item.timestamp, "%Y-%m-%dT%H:%M:%S.%f") timedelta = datetime.datetime.now() - timestamp timer = int(self.settings['keys'].get('timer', 0)) if timer > 0: if timedelta > datetime.timedelta(seconds=timer): continue - message = prepare_message(item, self.settings) + message = prepare_message(item.json(), self.settings, type(item)) self.ws.send(json.dumps(message)) @@ -220,7 +252,7 @@ def del_history(self, msg_id): return for index, item in enumerate(self.history): - if str(item['id']) == msg_id[0]: + if str(item.id) == msg_id[0]: self.history.pop(index) def get_settings(self, style_type): @@ -231,13 +263,13 @@ def get_history(self): def process_command(self, command, values): if command == 'remove_by_id': - self._remove_by_id(values['ids']) + self._remove_by_id(values.message_ids) elif command == 'remove_by_user': - self._remove_by_user(values['user']) + self._remove_by_user(values.user) elif command == 'replace_by_id': - self._replace_by_id(values['ids']) + self._replace_by_id(values.message_ids) elif command == 'replace_by_user': - self._replace_by_user(values['user']) + self._replace_by_user(values.user) def _remove_by_id(self, ids): for item in ids: @@ -248,7 +280,7 @@ def _remove_by_id(self, ids): def _remove_by_user(self, users): for item in users: for message in reversed(self.history): - if message.get('item') == item: + if message.user == item: self.history.remove(message) def _replace_by_id(self, ids): @@ -493,6 +525,7 @@ def __init__(self, *args, **kwargs): } }}) self.prepare_style_settings() + self.style_settings = self._conf_params['style_settings'] self.s_thread = None self.queue = None @@ -538,7 +571,7 @@ def get_style_path(style): return None def reload_chat(self): - self.queue.put({'type': 'command', 'command': 'reload'}) + self.queue.put(CommandMessage('reload')) def apply_settings(self, **kwargs): save_settings(self.conf_params(), ignored_sections=self._conf_params['gui'].get('ignored_sections', ())) @@ -581,25 +614,28 @@ def gui_button_press(self, gui_module, event, list_keys): self.reload_chat() event.Skip() - def process_message(self, message, queue, **kwargs): - if message: - if 'flags' in message: - if 'hidden' in message['flags']: - return message + def process_message(self, message, **kwargs): + if not hasattr(message, 'hidden'): s_queue.put(message) - return message + return message def rest_get_style_settings(self, *args): return json.dumps(self._conf_params['style_settings'][args[0][0]]['keys']) - @staticmethod - def rest_get_history(*args, **kwargs): - return json.dumps(cherrypy.engine.publish('get-history')[0]) + def rest_get_history(self, *args, **kwargs): + return json.dumps( + [prepare_message( + message.json(), + self.style_settings['chat'], + type(message)) + for message in cherrypy.engine.publish('get-history')[0]] + ) @staticmethod def rest_delete_history(path, **kwargs): cherrypy.engine.publish('del-history', path) - cherrypy.engine.publish('websocket-broadcast', json.dumps(remove_message_by_id(list(path)))) + cherrypy.engine.publish('websocket-broadcast', + RemoveMessageByID(list(path)).json()) def get_style_from_file(self, style_name): file_path = os.path.join(self.get_style_path(style_name), 'settings.json') From 2a33f42bc4cee7ecbce0d2b9e316c932129bcf28 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Fri, 14 Apr 2017 13:59:39 +0300 Subject: [PATCH 11/43] LC-377 Twitch subscriptions --- modules/chat/twitch.py | 27 ++++++++++++++++++++++----- modules/helper/message.py | 2 +- modules/helper/module.py | 2 +- src/themes/default/assets/index.html | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 488310c..7440575 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -121,7 +121,12 @@ def _handle_badges(self, message, badges): # Fix some of the names badge_tag = badge_tag.replace('moderator', 'mod') - if badge_tag in self.badges: + if badge_tag in self.custom_badges: + badge_info = self.custom_badges.get(badge_tag)['versions'][badge_size] + url = badge_info.get('image_url_4x', + badge_info.get('image_url_2x', + badge_info.get('image_url_1x'))) + elif badge_tag in self.badges: badge_info = self.badges.get(badge_tag) if 'svg' in badge_info: url = badge_info.get('svg') @@ -129,9 +134,6 @@ def _handle_badges(self, message, badges): url = badge_info.get('image') else: url = 'none' - elif badge_tag in self.custom_badges: - badge_info = self.custom_badges.get(badge_tag)['versions'][badge_size] - url = badge_info.get('image_url_4x') else: url = NOT_FOUND message.badges.append(Badge(badge_tag, url)) @@ -208,7 +210,7 @@ def _handle_message(self, msg, sub_message=False): def _handle_viewer_color(self, message, value): if self.irc_class.chat_module.conf_params()['config']['config']['show_nickname_colors']: - message.nick_color = value + message.nick_colour = value @staticmethod def _handle_bits(message, amount): @@ -422,6 +424,7 @@ def __init__(self, queue, host, port, channel, bttv_smiles, anon=True, **kwargs) self.kwargs = kwargs self.chat_module = kwargs.get('chat_module') self.display_name = None + self.channel_id = None self.irc = None if bttv_smiles: @@ -471,6 +474,7 @@ def load_config(self): log.info("Channel found, continuing") data = request.json() self.display_name = data['display_name'] + self.channel_id = data['_id'] elif request.status_code == 404: raise TwitchUserError else: @@ -526,6 +530,19 @@ def load_config(self): log.warning("Unable to get twitch undocumented api badges, error {0}\n" "Args: {1}".format(exc.message, exc.args)) + try: + # Warning, undocumented, can change a LOT + # Getting CUSTOM twitch badges + badges_url = "https://badges.twitch.tv/v1/badges/channels/{0}/display" + request = requests.get(badges_url.format(self.channel_id)) + if request.status_code == 200: + self.kwargs['custom_badges'].update(request.json()['badge_sets']) + else: + raise Exception("Not successful status code: {0}".format(request.status_code)) + except Exception as exc: + log.warning("Unable to get twitch undocumented api badges, error {0}\n" + "Args: {1}".format(exc.message, exc.args)) + return True def stop(self): diff --git a/modules/helper/message.py b/modules/helper/message.py index 0f54904..1380f5f 100644 --- a/modules/helper/message.py +++ b/modules/helper/message.py @@ -105,7 +105,7 @@ def __init__(self, message_id, text=None): else: CommandMessage.__init__(self, command='replace_by_user') self._message_ids = message_id if isinstance(message_id, list) else [message_id] - self._jsonable += ['user'] + self._jsonable += ['message_ids'] @property def message_ids(self): diff --git a/modules/helper/module.py b/modules/helper/module.py index 001caae..e40429d 100644 --- a/modules/helper/module.py +++ b/modules/helper/module.py @@ -15,7 +15,7 @@ CHAT_DICT = OrderedDict() CHAT_DICT['config'] = OrderedDict() -CHAT_DICT['config']['show_channel_names'] = True +CHAT_DICT['config']['show_channel_names'] = False CHAT_DICT['config']['channels_list'] = [] CHAT_GUI = { diff --git a/src/themes/default/assets/index.html b/src/themes/default/assets/index.html index d794e10..2d470dc 100644 --- a/src/themes/default/assets/index.html +++ b/src/themes/default/assets/index.html @@ -33,7 +33,7 @@
[{{message.channel_name}}]
-
{{message.display_name || message.user}}
+
{{message.display_name || message.user}}
:
From 481e884a4cfa8aeb495ce9f691477f676398066d Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Fri, 14 Apr 2017 15:57:12 +0300 Subject: [PATCH 12/43] LC-335 Split GUI file --- main.py | 14 +- modules/chat/goodgame.py | 17 +- modules/chat/sc2tv.py | 11 +- modules/chat/twitch.py | 15 +- gui.py => modules/gui.py | 610 ++++----------------------------- modules/interface/__init__.py | 0 modules/interface/controls.py | 155 +++++++++ modules/interface/functions.py | 354 +++++++++++++++++++ modules/messaging/webchat.py | 21 +- 9 files changed, 608 insertions(+), 589 deletions(-) rename gui.py => modules/gui.py (60%) create mode 100644 modules/interface/__init__.py create mode 100644 modules/interface/controls.py create mode 100644 modules/interface/functions.py diff --git a/main.py b/main.py index 64aad6e..3681794 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,19 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov -import os -import imp import Queue -from time import sleep -import messaging +import imp import logging import logging.config -import semantic_version +import os from collections import OrderedDict +from time import sleep +import semantic_version +import messaging +from modules.helper.module import BaseModule from modules.helper.parser import load_from_config_file from modules.helper.system import load_translations_keys, PYTHON_FOLDER, CONF_FOLDER, MAIN_CONF_FILE, MODULE_FOLDER, \ LOG_FOLDER, GUI_TAG, TRANSLATION_FOLDER, LOG_FILE, LOG_FORMAT, get_language, get_update, ModuleLoadException -from modules.helper.module import BaseModule VERSION = '0.3.6' SEM_VERSION = semantic_version.Version(VERSION) @@ -206,7 +206,7 @@ def close(): log.info('LalkaChat loaded successfully') if gui_settings['gui']: - import gui + from modules import gui log.info("Loading GUI Interface") window = gui.GuiThread(gui_settings=gui_settings, main_config=loaded_modules['main'], diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 48d03a8..72aaebc 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -1,23 +1,24 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov +import Queue import json +import logging +import os import random +import re import string import threading -import os -import requests -import Queue -import re -import logging import time from collections import OrderedDict +import requests +from ws4py.client.threadedclient import WebSocketClient + +from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, SystemMessage, Emote, RemoveMessageByID -from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE from modules.helper.module import ChatModule -from ws4py.client.threadedclient import WebSocketClient -from gui import MODULE_KEY +from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('goodgame') diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index 367d22f..16cc4f5 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -1,20 +1,21 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import json +import logging +import os import random +import re import string import threading import time -import re -import requests -import os -import logging from collections import OrderedDict + +import requests from ws4py.client.threadedclient import WebSocketClient +from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, SystemMessage, Emote from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT -from gui import MODULE_KEY logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('sc2tv') diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 7440575..cb4a345 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -1,19 +1,20 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov -import irc.client -import threading +import Queue +import logging.config import os -import re import random -import requests -import logging.config -import Queue +import re +import threading import time from collections import OrderedDict +import irc.client +import requests + +from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, SystemMessage, Badge, Emote, RemoveMessageByUser from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE -from gui import MODULE_KEY logging.getLogger('irc').setLevel(logging.ERROR) logging.getLogger('requests').setLevel(logging.ERROR) diff --git a/gui.py b/modules/gui.py similarity index 60% rename from gui.py rename to modules/gui.py index 4d96196..92b398e 100644 --- a/gui.py +++ b/modules/gui.py @@ -1,4 +1,8 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov +from modules.interface.controls import KeyListBox, MainMenuToolBar + +import modules.interface.functions + try: from cefpython3.wx import chromectrl as browser HAS_CHROME = True @@ -12,13 +16,11 @@ import logging import webbrowser import wx -import wx.grid -from modules.helper.system import MODULE_KEY, translate_key, PYTHON_FOLDER +from modules.helper.system import MODULE_KEY, translate_key from modules.helper.parser import return_type from modules.helper.module import BaseModule # ToDO: Support customization of borders/spacings -IDS = {} log = logging.getLogger('chat_gui') INFORMATION_TAG = 'gui_information' SECTION_GUI_TAG = '__gui' @@ -29,37 +31,6 @@ ITEM_SPACING_HORZ = 30 -def get_id_from_name(name): - for item, item_id in IDS.iteritems(): - if item_id == name: - return item - return None - - -def id_renew(name, update=False, multiple=False): - module_id = get_id_from_name(name) - if not multiple and module_id: - del IDS[module_id] - new_id = wx.Window.NewControlId(1) - if update: - IDS[new_id] = name - return new_id - - -def get_list_of_ids_from_module_name(name, id_group=1, return_tuple=False): - split_key = MODULE_KEY - - id_array = [] - for item_key, item in IDS.items(): - item_name = split_key.join(item.split(split_key)[:id_group]) - if item_name == name: - if return_tuple: - id_array.append((item_key, item)) - else: - id_array.append(item_key) - return id_array - - def check_duplicate(item, window): items = window.GetItems() if item in items: @@ -99,123 +70,6 @@ class ModuleKeyError(Exception): pass -class CustomColourPickerCtrl(object): - def __init__(self): - self.panel = None - self.button = None - self.text = None - self.event = None - self.key = None - - def create(self, panel, value="#FFFFFF", orientation=wx.HORIZONTAL, event=None, key=None, - *args, **kwargs): - item_sizer = wx.BoxSizer(orientation) - - self.event = event - self.key = key - label_panel = wx.Panel(panel, style=wx.BORDER_SIMPLE) - label_sizer = wx.BoxSizer(wx.HORIZONTAL) - label_sizer2 = wx.BoxSizer(wx.VERTICAL) - label_text = wx.StaticText(label_panel, label=unicode(value), style=wx.ALIGN_CENTER) - self.text = label_text - label_sizer.Add(label_text, 1, wx.ALIGN_CENTER) - label_sizer2.Add(label_sizer, 1, wx.ALIGN_CENTER) - label_panel.SetSizer(label_sizer2) - label_panel.SetBackgroundColour(value) - self.panel = label_panel - - button = wx.Button(panel, label=translate_key(MODULE_KEY.join(key + ['button']))) - button.Bind(wx.EVT_BUTTON, self.on_button_press) - border_size = wx.SystemSettings_GetMetric(wx.SYS_BORDER_Y) - button_size = button.GetSize() - if button_size[0] > 150: - button_size[0] = 150 - button_size[1] -= border_size*2 - self.button = button - - label_panel.SetMinSize(button_size) - label_panel.SetSize(button_size) - - item_sizer.Add(label_panel, 0, wx.ALIGN_CENTER) - item_sizer.AddSpacer(2) - item_sizer.Add(button, 0, wx.EXPAND) - return item_sizer - - def on_button_press(self, event): - dialog = wx.ColourDialog(self.panel) - if dialog.ShowModal() == wx.ID_OK: - colour = dialog.GetColourData() - hex_colour = colour.Colour.GetAsString(flags=wx.C2S_HTML_SYNTAX) - self.panel.SetBackgroundColour(colour.Colour) - self.panel.Refresh() - self.text.SetLabel(hex_colour) - self.panel.Layout() - col = colour.Colour - if (col.red * 0.299 + col.green * 0.587 + col.blue * 0.114) > 186: - self.text.SetForegroundColour('black') - else: - self.text.SetForegroundColour('white') - - self.event({'colour': colour.Colour, 'hex': hex_colour, 'key': self.key}) - - -class KeyListBox(wx.ListBox): - def __init__(self, *args, **kwargs): - self.keys = kwargs.pop('keys', []) - wx.ListBox.__init__(self, *args, **kwargs) - - def get_key_from_index(self, index): - return self.keys[index] - - -class KeyCheckListBox(wx.CheckListBox): - def __init__(self, *args, **kwargs): - self.keys = kwargs.pop('keys', []) - wx.CheckListBox.__init__(self, *args, **kwargs) - - def get_key_from_index(self, index): - return self.keys[index] - - -class KeyChoice(wx.Choice): - def __init__(self, *args, **kwargs): - self.keys = kwargs.pop('keys', []) - wx.Choice.__init__(self, *args, **kwargs) - - def get_key_from_index(self, index): - return self.keys[index] - - -class MainMenuToolBar(wx.ToolBar): - def __init__(self, *args, **kwargs): - self.main_class = kwargs['main_class'] # type: ChatGui - kwargs.pop('main_class') - - kwargs["style"] = wx.TB_NOICONS | wx.TB_TEXT - - wx.ToolBar.__init__(self, *args, **kwargs) - self.SetToolBitmapSize((0, 0)) - - self.create_tool('menu.settings', self.main_class.on_settings) - self.create_tool('menu.reload', self.main_class.on_toolbar_button) - - self.Realize() - - def create_tool(self, name, binding=None, style=wx.ITEM_NORMAL, s_help="", l_help=""): - l_id = id_renew(name) - IDS[l_id] = name - label_text = translate_key(IDS[l_id]) - button = self.AddLabelTool(l_id, label_text, wx.NullBitmap, wx.NullBitmap, - style, s_help, l_help) - if binding: - self.main_class.Bind(wx.EVT_TOOL, binding, id=l_id) - return button - - -class GuiCreationError(Exception): - pass - - class SettingsWindow(wx.Frame): main_grid = None page_list = [] @@ -235,17 +89,17 @@ def __init__(self, *args, **kwargs): self.sizer_dict = {} self.changes = {} self.buttons = {} - self.function_map = { + self.groups = { dict: { - 'function': self.create_static_box, + 'function': modules.interface.functions.create_static_box, 'bind': None }, OrderedDict: { - 'function': self.create_static_box, + 'function': modules.interface.functions.create_static_box, 'bind': None }, 'list_dual': { - 'function': self.create_list, + 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, 'remove': self.button_clicked, @@ -253,7 +107,7 @@ def __init__(self, *args, **kwargs): } }, 'list': { - 'function': self.create_list, + 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, 'remove': self.button_clicked, @@ -261,59 +115,59 @@ def __init__(self, *args, **kwargs): } }, 'choose_multiple': { - 'function': self.create_choose, + 'function': modules.interface.functions.create_choose, 'bind': { 'change': self.on_listbox_change, 'check_change': self.on_checklist_box_change } }, 'choose_single': { - 'function': self.create_choose, + 'function': modules.interface.functions.create_choose, 'bind': { 'change': self.on_listbox_change, 'check_change': self.on_checklist_box_change } } } - self.value_map = { + self.controls = { type(None): { - 'function': self.create_button, + 'function': modules.interface.functions.create_button, 'bind': self.button_clicked }, bool: { - 'function': self.create_checkbox, + 'function': modules.interface.functions.create_checkbox, 'bind': self.on_check_change }, str: { - 'function': self.create_textctrl, + 'function': modules.interface.functions.create_textctrl, 'bind': self.on_textctrl }, unicode: { - 'function': self.create_textctrl, + 'function': modules.interface.functions.create_textctrl, 'bind': self.on_textctrl }, int: { - 'function': self.create_textctrl, + 'function': modules.interface.functions.create_textctrl, 'bind': self.on_textctrl }, 'spin': { - 'function': self.create_spin, + 'function': modules.interface.functions.create_spin, 'bind': self.on_spinctrl }, 'dropdown': { - 'function': self.create_dropdown, + 'function': modules.interface.functions.create_dropdown, 'bind': self.on_dropdown }, 'slider': { - 'function': self.create_slider, + 'function': modules.interface.functions.create_slider, 'bind': self.on_sliderctrl }, 'colour_picker': { - 'function': self.create_colour_picker, + 'function': modules.interface.functions.create_colour_picker, 'bind': self.on_color_picker }, 'list': { - 'function': self.create_list, + 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, 'remove': self.button_clicked, @@ -321,7 +175,7 @@ def __init__(self, *args, **kwargs): } }, 'button': { - 'function': self.create_button, + 'function': modules.interface.functions.create_button, 'bind': self.button_clicked } } @@ -356,15 +210,17 @@ def on_listbox_change(self, event): selection = item_object.get_key_from_index(item_object.GetSelection()) description = translate_key(MODULE_KEY.join([selection, 'description'])) - item_key = IDS[event.GetId()].split(MODULE_KEY) + item_key = modules.interface.controls.IDS[event.GetId()].split(MODULE_KEY) show_description = self.main_class.loaded_modules[item_key[0]]['gui'][item_key[1]].get('description', False) if isinstance(item_object, KeyListBox): - self.on_change(IDS[event.GetId()], selection, item_type='listbox', section=True) + self.on_change(modules.interface.controls.IDS[event.GetId()], selection, item_type='listbox', section=True) if show_description: item_id_key = MODULE_KEY.join(item_key[:-1]) - descr_static_text = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([item_id_key, 'descr_explain']))) + descr_static_text = wx.FindWindowById( + modules.interface.controls.get_id_from_name(MODULE_KEY.join([item_id_key, 'descr_explain'])) + ) descr_static_text.SetLabel(description) descr_static_text.Wrap(descr_static_text.GetSize()[0]) @@ -372,7 +228,7 @@ def on_checklist_box_change(self, event): window = event.EventObject item_ids = window.GetChecked() items_values = [window.get_key_from_index(item_id) for item_id in item_ids] - self.on_change(IDS[event.GetId()], items_values, item_type='listbox_check', section=True) + self.on_change(modules.interface.controls.IDS[event.GetId()], items_values, item_type='listbox_check', section=True) def on_change(self, key, value, item_type=None, section=False): def enable_button(): @@ -457,35 +313,35 @@ def on_tree_ctrl_changed(self, event): def on_textctrl(self, event): text_ctrl = event.EventObject - self.on_change(IDS[event.GetId()], text_ctrl.GetValue().encode('utf-8'), item_type='textctrl') + self.on_change(modules.interface.controls.IDS[event.GetId()], text_ctrl.GetValue().encode('utf-8'), item_type='textctrl') event.Skip() def on_spinctrl(self, event): spin_ctrl = event.EventObject - self.on_change(IDS[event.GetId()], spin_ctrl.GetValue(), item_type='spinctrl') + self.on_change(modules.interface.controls.IDS[event.GetId()], spin_ctrl.GetValue(), item_type='spinctrl') event.Skip() def on_sliderctrl(self, event): ctrl = event.EventObject - self.on_change(IDS[event.GetId()], ctrl.GetValue(), item_type='sliderctrl') + self.on_change(modules.interface.controls.IDS[event.GetId()], ctrl.GetValue(), item_type='sliderctrl') event.Skip() def on_dropdown(self, event): drop_ctrl = event.EventObject - self.on_change(IDS[event.GetId()], drop_ctrl.GetString(drop_ctrl.GetCurrentSelection()), + self.on_change(modules.interface.controls.IDS[event.GetId()], drop_ctrl.GetString(drop_ctrl.GetCurrentSelection()), item_type='dropctrl') event.Skip() def on_check_change(self, event): check_ctrl = event.EventObject - self.on_change(IDS[event.GetId()], check_ctrl.IsChecked(), item_type='checkbox') + self.on_change(modules.interface.controls.IDS[event.GetId()], check_ctrl.IsChecked(), item_type='checkbox') event.Skip() def on_list_operation(self, key, action): - list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) + list_box = wx.FindWindowById(modules.interface.controls.get_id_from_name(MODULE_KEY.join([key, 'list_box']))) if action == 'list_add': list_input_value = wx.FindWindowById( - get_id_from_name(MODULE_KEY.join([key, 'list_input']))).GetValue().strip() + modules.interface.controls.get_id_from_name(MODULE_KEY.join([key, 'list_input']))).GetValue().strip() row_count = list_box.GetNumberRows() row_values = [list_box.GetCellValue(f_row, 0).lower() for f_row in range(0, row_count)] @@ -493,7 +349,7 @@ def on_list_operation(self, key, action): list_box.AppendRows(1) list_box.SetCellValue(row_count, 0, list_input_value) - list_input2_id = get_id_from_name(MODULE_KEY.join([key, 'list_input2'])) + list_input2_id = modules.interface.controls.get_id_from_name(MODULE_KEY.join([key, 'list_input2'])) if list_input2_id: list_input2_value = wx.FindWindowById(list_input2_id).GetValue().strip() row_values = [list_box.GetCellValue(f_row, 1).lower() for f_row in range(0, row_count)] @@ -551,7 +407,7 @@ def create_layout(self): image_list = wx.ImageList(16, 16) - tree_ctrl_id = id_renew('settings.tree', update=True) + tree_ctrl_id = modules.interface.functions.id_renew('settings.tree', update=True) tree_ctrl = wx.TreeCtrl(self, id=tree_ctrl_id, style=style) root_key = MODULE_KEY.join(['settings', 'tree', 'root']) root_node = tree_ctrl.AddRoot(translate_key(root_key)) @@ -585,7 +441,7 @@ def create_layout(self): self.main_grid.Add(tree_ctrl, 7, wx.EXPAND | wx.ALL, 7) - content_page_id = id_renew(MODULE_KEY.join(['settings', 'content'])) + content_page_id = modules.interface.functions.id_renew(MODULE_KEY.join(['settings', 'content'])) self.content_page = wx.Panel(self, id=content_page_id) self.main_grid.Add(self.content_page, 15, wx.EXPAND) @@ -653,7 +509,7 @@ def create_page(self, sizer, panel, config, gui, key): self.create_page_buttons(sizer=page_sizer, panel=panel) def create_page_items(self, page_sizer, panel, config, gui, key): - page_sc_window = wx.ScrolledWindow(panel, id=id_renew(gui), style=wx.VSCROLL) + page_sc_window = wx.ScrolledWindow(panel, id=modules.interface.functions.id_renew(gui), style=wx.VSCROLL) page_sc_window.SetScrollbars(5, 5, 10, 10) sizer = wx.BoxSizer(wx.VERTICAL) joined_keys = MODULE_KEY.join(key) @@ -678,12 +534,13 @@ def create_page_items(self, page_sizer, panel, config, gui, key): continue view = gui.get(section_key, {}).get('view', type(section_items)) - if view in self.function_map.keys(): - data = self.function_map[view] + if view in self.groups.keys(): + data = self.groups[view] gui_settings = gui.get(section_key, {}).copy() item_keys = key + [section_key] sizer_item = data['function']( - panel=page_sc_window, item=section_key, value=section_items, bind=data['bind'], + source_class=self, panel=page_sc_window, item=section_key, + value=section_items, bind=data['bind'], gui=gui_settings, key=item_keys, from_sb=False) if joined_keys in self.redraw_map.keys(): if section_key in self.redraw_map[joined_keys]: @@ -702,373 +559,22 @@ def create_page_items(self, page_sizer, panel, config, gui, key): def create_page_buttons(self, sizer, panel): button_sizer = wx.BoxSizer(wx.HORIZONTAL) button_sizer.Add( - self.create_button( - panel=panel, key=['settings', 'ok_button'], + modules.interface.functions.create_button( + self, panel=panel, key=['settings', 'ok_button'], bind=self.button_clicked, multiple=True)['item'], 0, wx.ALIGN_RIGHT) button_sizer.Add( - self.create_button( - panel=panel, key=['settings', 'apply_button'], + modules.interface.functions.create_button( + self, panel=panel, key=['settings', 'apply_button'], bind=self.button_clicked, enabled=False, multiple=True)['item'], 0, wx.ALIGN_RIGHT) button_sizer.Add( - self.create_button( - panel=panel, key=['settings', 'cancel_button'], + modules.interface.functions.create_button( + self, panel=panel, key=['settings', 'cancel_button'], bind=self.button_clicked, multiple=True)['item'], 0, wx.ALIGN_RIGHT) sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4) - def create_button(self, **kwargs): - panel = kwargs.get('panel') - key = kwargs.get('key') - value = kwargs.get('value') - bind = kwargs.get('bind') - enabled = kwargs.get('enabled', True) - multiple = kwargs.get('multiple') - - item_sizer = wx.BoxSizer(wx.VERTICAL) - item_name = MODULE_KEY.join(key) - button_id = id_renew(item_name, update=True, multiple=multiple) - c_button = wx.Button(panel, id=button_id, label=translate_key(item_name)) - if not enabled: - c_button.Disable() - - if item_name in self.buttons: - self.buttons[item_name].append(c_button) - else: - self.buttons[item_name] = [c_button] - - if value: - c_button.Bind(wx.EVT_BUTTON, value, id=button_id) - else: - c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) - - item_sizer.Add(c_button) - return {'item': item_sizer} - - def create_static_box(self, **kwargs): - panel = kwargs.get('panel') - item_value = kwargs.get('value') - gui = kwargs.get('gui') - key = kwargs.get('key') - - static_box = wx.StaticBox(panel, label=translate_key(MODULE_KEY.join(key))) - static_sizer = wx.StaticBoxSizer(static_box, wx.VERTICAL) - instatic_sizer = wx.BoxSizer(wx.VERTICAL) - spacer_size = 7 - - max_text_size = 0 - text_ctrls = [] - log.debug("Working on {0}".format(MODULE_KEY.join(key))) - spacer = False - hidden_items = gui.get('hidden', []) - - for item, value in item_value.items(): - if item in hidden_items and not self.show_hidden: - continue - view = gui.get(item, {}).get('view', type(value)) - if view in self.value_map.keys(): - fnction = self.value_map[view] - elif callable(value): - fnction = self.value_map['button'] - else: - raise GuiCreationError('Unable to create item, bad value map') - item_dict = fnction['function'](panel=static_box, item=item, value=value, key=key + [item], - bind=fnction['bind'], gui=gui.get(item, {}), from_sb=True) - if 'text_size' in item_dict: - if max_text_size < item_dict.get('text_size'): - max_text_size = item_dict['text_size'] - - text_ctrls.append(item_dict['text_ctrl']) - spacer = True if not spacer else instatic_sizer.AddSpacer(spacer_size) - instatic_sizer.Add(item_dict['item'], 0, wx.EXPAND, 5) - - if max_text_size: - for ctrl in text_ctrls: - ctrl.SetMinSize((max_text_size + 50, ctrl.GetSize()[1])) - - item_count = instatic_sizer.GetItemCount() - if not item_count: - static_sizer.Destroy() - return wx.BoxSizer(wx.VERTICAL) - - static_sizer.Add(instatic_sizer, 0, wx.EXPAND | wx.ALL, 5) - return static_sizer - - @staticmethod - def create_checkbox(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - style = wx.ALIGN_CENTER_VERTICAL - item_key = MODULE_KEY.join(key) - item_box = wx.CheckBox(panel, id=id_renew(item_key, update=True), - label=translate_key(item_key), style=style) - item_box.SetValue(value) - item_box.Bind(wx.EVT_CHECKBOX, bind) - item_sizer.Add(item_box, 0, wx.ALIGN_LEFT) - return {'item': item_sizer} - - @staticmethod - def create_textctrl(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - item_name = MODULE_KEY.join(key) - item_box = wx.TextCtrl(panel, id=id_renew(item_name, update=True), - value=unicode(value)) - item_box.Bind(wx.EVT_TEXT, bind) - item_text = wx.StaticText(panel, label=translate_key(item_name)) - item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) - item_sizer.Add(item_box) - return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} - - @staticmethod - def create_spin(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - item_name = MODULE_KEY.join(key) - style = wx.ALIGN_LEFT - item_box = wx.SpinCtrl(panel, id=id_renew(item_name, update=True), min=gui['min'], max=gui['max'], - initial=int(value), style=style) - item_text = wx.StaticText(panel, label=translate_key(item_name)) - item_box.Bind(wx.EVT_SPINCTRL, bind) - item_box.Bind(wx.EVT_TEXT, bind) - item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) - item_sizer.Add(item_box) - return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} - - @staticmethod - def create_list(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - from_sb = kwargs.get('from_sb') - - view = gui.get('view') - is_dual = True if 'dual' in view else False - style = wx.ALIGN_CENTER_VERTICAL - border_sizer = wx.BoxSizer(wx.VERTICAL) - item_sizer = wx.BoxSizer(wx.VERTICAL) - - static_label = MODULE_KEY.join(key) - static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) - item_sizer.Add(static_text) - - addable_sizer = wx.BoxSizer(wx.HORIZONTAL) if gui.get('addable') else None - if addable_sizer: - item_input_key = MODULE_KEY.join(key + ['list_input']) - addable_sizer.Add(wx.TextCtrl(panel, id=id_renew(item_input_key, update=True)), 0, style) - if is_dual: - item_input2_key = MODULE_KEY.join(key + ['list_input2']) - addable_sizer.Add(wx.TextCtrl(panel, id=id_renew(item_input2_key, update=True)), 0, style) - - item_apply_key = MODULE_KEY.join(key + ['list_add']) - item_apply_id = id_renew(item_apply_key, update=True) - item_apply = wx.Button(panel, id=item_apply_id, label=translate_key(item_apply_key)) - addable_sizer.Add(item_apply, 0, style) - item_apply.Bind(wx.EVT_BUTTON, bind['add'], id=item_apply_id) - - item_remove_key = MODULE_KEY.join(key + ['list_remove']) - item_remove_id = id_renew(item_remove_key, update=True) - item_remove = wx.Button(panel, id=item_remove_id, label=translate_key(item_remove_key)) - addable_sizer.Add(item_remove, 0, style) - item_remove.Bind(wx.EVT_BUTTON, bind['remove'], id=item_remove_id) - - item_sizer.Add(addable_sizer, 0, wx.EXPAND) - - list_box = wx.grid.Grid(panel, id=id_renew(MODULE_KEY.join(key + ['list_box']), update=True)) - list_box.CreateGrid(0, 2 if is_dual else 1) - list_box.DisableDragColSize() - list_box.DisableDragRowSize() - list_box.Bind(wx.grid.EVT_GRID_SELECT_CELL, bind['select']) - - if is_dual: - for index, (item, item_value) in enumerate(value.items()): - list_box.AppendRows(1) - list_box.SetCellValue(index, 0, item) - list_box.SetCellValue(index, 1, item_value) - else: - for index, item in enumerate(value): - list_box.AppendRows(1) - list_box.SetCellValue(index, 0, item) - - list_box.SetColLabelSize(1) - list_box.SetRowLabelSize(1) - - if addable_sizer: - col_size = addable_sizer.GetMinSize()[0] - 2 - if is_dual: - first_col_size = list_box.GetColSize(0) - second_col_size = col_size - first_col_size if first_col_size < col_size else -1 - list_box.SetColSize(1, second_col_size) - else: - list_box.SetDefaultColSize(col_size, resizeExistingCols=True) - else: - list_box.AutoSize() - - item_sizer.Add(list_box) - - border_sizer.Add(item_sizer, 0, wx.EXPAND | wx.ALL, 5) - if from_sb: - return {'item': border_sizer} - else: - return border_sizer - - @staticmethod - def create_colour_picker(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - - item_name = MODULE_KEY.join(key) - colour_picker = CustomColourPickerCtrl() - item_box = colour_picker.create(panel, value=value, event=bind, key=key) - - item_text = wx.StaticText(panel, label=translate_key(item_name)) - item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) - item_sizer.Add(item_box, 1, wx.EXPAND) - return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} - - @staticmethod - def create_choose(**kwargs): - panel = kwargs.get('panel') - item_list = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - - view = gui.get('view') - is_single = True if 'single' in view else False - description = gui.get('description', False) - style = wx.LB_SINGLE if is_single else wx.LB_EXTENDED - border_sizer = wx.BoxSizer(wx.VERTICAL) - item_sizer = wx.BoxSizer(wx.VERTICAL) - list_items = [] - translated_items = [] - - static_label = MODULE_KEY.join(key) - static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) - item_sizer.Add(static_text) - - if gui['check_type'] in ['dir', 'folder', 'files']: - check_type = gui['check_type'] - keep_extension = gui['file_extension'] if 'file_extension' in gui else False - for item_in_list in os.listdir(os.path.join(PYTHON_FOLDER, gui['check'])): - item_path = os.path.join(PYTHON_FOLDER, gui['check'], item_in_list) - if check_type in ['dir', 'folder'] and os.path.isdir(item_path): - list_items.append(item_in_list) - elif check_type == 'files' and os.path.isfile(item_path): - if not keep_extension: - item_in_list = ''.join(os.path.basename(item_path).split('.')[:-1]) - if '__init__' not in item_in_list: - if item_in_list not in list_items: - list_items.append(item_in_list) - translated_items.append(translate_key(item_in_list)) - - item_key = MODULE_KEY.join(key + ['list_box']) - label_text = translate_key(item_key) - if label_text: - item_sizer.Add(wx.StaticText(panel, label=label_text, style=wx.ALIGN_RIGHT)) - if is_single: - item_list_box = KeyListBox(panel, id=id_renew(item_key, update=True), keys=list_items, - choices=translated_items if translated_items else list_items, style=style) - else: - item_list_box = KeyCheckListBox(panel, id=id_renew(item_key, update=True), keys=list_items, - choices=translated_items if translated_items else list_items) - item_list_box.Bind(wx.EVT_CHECKLISTBOX, bind['check_change']) - item_list_box.Bind(wx.EVT_LISTBOX, bind['change']) - - section_for = item_list if not is_single else {item_list: None} - if is_single: - item, value = section_for.items()[0] - if item not in item_list_box.GetItems(): - if item_list_box.GetItems(): - item_list_box.SetSelection(0) - else: - item_list_box.SetSelection(list_items.index(item)) - else: - check_items = [list_items.index(item) for item in section_for] - item_list_box.SetChecked(check_items) - - if description: - adv_sizer = wx.BoxSizer(wx.HORIZONTAL) - adv_sizer.Add(item_list_box, 0, wx.EXPAND) - - descr_key = MODULE_KEY.join(key + ['descr_explain']) - descr_text = wx.StaticText(panel, id=id_renew(descr_key, update=True), - label=translate_key(descr_key), style=wx.ST_NO_AUTORESIZE) - adv_sizer.Add(descr_text, 0, wx.EXPAND | wx.LEFT, 10) - - sizes = descr_text.GetSize() - sizes[0] -= 20 - descr_text.SetMinSize(sizes) - descr_text.Fit() - item_sizer.Add(adv_sizer) - else: - item_sizer.Add(item_list_box) - border_sizer.Add(item_sizer, 0, wx.EXPAND | wx.ALL, 5) - return border_sizer - - @staticmethod - def create_dropdown(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - choices = gui.get('choices', []) - item_name = MODULE_KEY.join(key) - item_text = wx.StaticText(panel, label=translate_key(item_name)) - item_box = KeyChoice(panel, id=id_renew(item_name, update=True), - keys=choices, choices=choices) - item_box.Bind(wx.EVT_CHOICE, bind) - item_box.SetSelection(choices.index(value)) - item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) - item_sizer.Add(item_box) - return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} - - @staticmethod - def create_slider(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - - item_sizer = wx.BoxSizer(wx.HORIZONTAL) - item_name = MODULE_KEY.join(key) - style = wx.SL_VALUE_LABEL | wx.SL_AUTOTICKS - item_box = wx.Slider(panel, id=id_renew(item_name, update=True), - minValue=gui['min'], maxValue=gui['max'], - value=int(value), style=style) - freq = (gui['max'] - gui['min'])/5 - item_box.SetTickFreq(freq) - item_box.SetLineSize(4) - item_box.Bind(wx.EVT_SCROLL, bind) - item_text = wx.StaticText(panel, label=translate_key(item_name)) - item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) - item_sizer.Add(item_box, 1, wx.EXPAND) - return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} - def redraw_item(self, redraw_keys, redraw_value): sizer = redraw_keys['item'] sizer_parent = redraw_keys['sizer_parent'] @@ -1112,9 +618,9 @@ def detach_all_children(self, sizer): sizer.Remove(index) def button_clicked(self, event): - log.debug("[Settings] Button clicked: {0}".format(IDS[event.GetId()])) + log.debug("[Settings] Button clicked: {0}".format(modules.interface.controls.IDS[event.GetId()])) button_id = event.GetId() - keys = IDS[button_id].split(MODULE_KEY) + keys = modules.interface.controls.IDS[button_id].split(MODULE_KEY) last_key = keys[-1] if last_key in ['list_add', 'list_remove']: self.on_list_operation(MODULE_KEY.join(keys[:-1]), action=last_key) @@ -1417,10 +923,10 @@ def on_right_down(event): event.Skip() def on_settings(self, event): - log.debug("Got event from {0}".format(IDS[event.GetId()])) - module_groups = IDS[event.GetId()].split(MODULE_KEY) + log.debug("Got event from {0}".format(modules.interface.controls.IDS[event.GetId()])) + module_groups = modules.interface.controls.IDS[event.GetId()].split(MODULE_KEY) settings_category = MODULE_KEY.join(module_groups[1:-1]) - settings_menu_id = id_renew(settings_category, update=True) + settings_menu_id = modules.interface.functions.id_renew(settings_category, update=True) if self.settings_window: self.settings_window.SetFocus() else: @@ -1434,13 +940,13 @@ def on_settings(self, event): @staticmethod def button_clicked(event): button_id = event.GetId() - keys = IDS[event.GetId()].split(MODULE_KEY) + keys = modules.interface.controls.IDS[event.GetId()].split(MODULE_KEY) log.debug("[ChatGui] Button clicked: {0}, {1}".format(keys, button_id)) event.Skip() def on_toolbar_button(self, event): button_id = event.GetId() - list_keys = IDS[event.GetId()].split(MODULE_KEY) + list_keys = modules.interface.controls.IDS[event.GetId()].split(MODULE_KEY) log.debug("[ChatGui] Toolbar clicked: {0}, {1}".format(list_keys, button_id)) if list_keys[0] in self.loaded_modules: self.loaded_modules[list_keys[0]]['class'].gui_button_press(self, event, list_keys) diff --git a/modules/interface/__init__.py b/modules/interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/interface/controls.py b/modules/interface/controls.py new file mode 100644 index 0000000..aef6902 --- /dev/null +++ b/modules/interface/controls.py @@ -0,0 +1,155 @@ +import wx +import wx.grid + +from modules.helper.system import translate_key, MODULE_KEY + +IDS = {} + + +def get_list_of_ids_from_module_name(name, id_group=1, return_tuple=False): + split_key = MODULE_KEY + + id_array = [] + for item_key, item in IDS.items(): + item_name = split_key.join(item.split(split_key)[:id_group]) + if item_name == name: + if return_tuple: + id_array.append((item_key, item)) + else: + id_array.append(item_key) + return id_array + + +def get_id_from_name(name): + for item, item_id in IDS.iteritems(): + if item_id == name: + return item + return None + + +def id_renew(name, update=False, multiple=False): + module_id = get_id_from_name(name) + if not multiple and module_id: + del IDS[module_id] + new_id = wx.Window.NewControlId(1) + if update: + IDS[new_id] = name + return new_id + + +class GuiCreationError(Exception): + pass + + +class CustomColourPickerCtrl(object): + def __init__(self): + self.panel = None + self.button = None + self.text = None + self.event = None + self.key = None + + def create(self, panel, value="#FFFFFF", orientation=wx.HORIZONTAL, event=None, key=None, + *args, **kwargs): + item_sizer = wx.BoxSizer(orientation) + + self.event = event + self.key = key + label_panel = wx.Panel(panel, style=wx.BORDER_SIMPLE) + label_sizer = wx.BoxSizer(wx.HORIZONTAL) + label_sizer2 = wx.BoxSizer(wx.VERTICAL) + label_text = wx.StaticText(label_panel, label=unicode(value), style=wx.ALIGN_CENTER) + self.text = label_text + label_sizer.Add(label_text, 1, wx.ALIGN_CENTER) + label_sizer2.Add(label_sizer, 1, wx.ALIGN_CENTER) + label_panel.SetSizer(label_sizer2) + label_panel.SetBackgroundColour(value) + self.panel = label_panel + + button = wx.Button(panel, label=translate_key(MODULE_KEY.join(key + ['button']))) + button.Bind(wx.EVT_BUTTON, self.on_button_press) + border_size = wx.SystemSettings_GetMetric(wx.SYS_BORDER_Y) + button_size = button.GetSize() + if button_size[0] > 150: + button_size[0] = 150 + button_size[1] -= border_size*2 + self.button = button + + label_panel.SetMinSize(button_size) + label_panel.SetSize(button_size) + + item_sizer.Add(label_panel, 0, wx.ALIGN_CENTER) + item_sizer.AddSpacer(2) + item_sizer.Add(button, 0, wx.EXPAND) + return item_sizer + + def on_button_press(self, event): + dialog = wx.ColourDialog(self.panel) + if dialog.ShowModal() == wx.ID_OK: + colour = dialog.GetColourData() + hex_colour = colour.Colour.GetAsString(flags=wx.C2S_HTML_SYNTAX) + self.panel.SetBackgroundColour(colour.Colour) + self.panel.Refresh() + self.text.SetLabel(hex_colour) + self.panel.Layout() + col = colour.Colour + if (col.red * 0.299 + col.green * 0.587 + col.blue * 0.114) > 186: + self.text.SetForegroundColour('black') + else: + self.text.SetForegroundColour('white') + + self.event({'colour': colour.Colour, 'hex': hex_colour, 'key': self.key}) + + +class KeyListBox(wx.ListBox): + def __init__(self, *args, **kwargs): + self.keys = kwargs.pop('keys', []) + wx.ListBox.__init__(self, *args, **kwargs) + + def get_key_from_index(self, index): + return self.keys[index] + + +class KeyCheckListBox(wx.CheckListBox): + def __init__(self, *args, **kwargs): + self.keys = kwargs.pop('keys', []) + wx.CheckListBox.__init__(self, *args, **kwargs) + + def get_key_from_index(self, index): + return self.keys[index] + + +class KeyChoice(wx.Choice): + def __init__(self, *args, **kwargs): + self.keys = kwargs.pop('keys', []) + wx.Choice.__init__(self, *args, **kwargs) + + def get_key_from_index(self, index): + return self.keys[index] + + +class MainMenuToolBar(wx.ToolBar): + def __init__(self, *args, **kwargs): + self.main_class = kwargs['main_class'] # type: ChatGui + kwargs.pop('main_class') + + kwargs["style"] = wx.TB_NOICONS | wx.TB_TEXT + + wx.ToolBar.__init__(self, *args, **kwargs) + self.SetToolBitmapSize((0, 0)) + + self.create_tool('menu.settings', self.main_class.on_settings) + self.create_tool('menu.reload', self.main_class.on_toolbar_button) + + self.Realize() + + def create_tool(self, name, binding=None, style=wx.ITEM_NORMAL, s_help="", l_help=""): + l_id = id_renew(name) + IDS[l_id] = name + label_text = translate_key(IDS[l_id]) + button = self.AddLabelTool(l_id, label_text, wx.NullBitmap, wx.NullBitmap, + style, s_help, l_help) + if binding: + self.main_class.Bind(wx.EVT_TOOL, binding, id=l_id) + return button + diff --git a/modules/interface/functions.py b/modules/interface/functions.py new file mode 100644 index 0000000..864908f --- /dev/null +++ b/modules/interface/functions.py @@ -0,0 +1,354 @@ +import os +import wx +import wx.grid + +from modules.helper.system import MODULE_KEY, translate_key, log, PYTHON_FOLDER +from modules.interface.controls import GuiCreationError, CustomColourPickerCtrl, KeyListBox, KeyCheckListBox, KeyChoice, \ + id_renew + + +def create_textctrl(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + item_name = MODULE_KEY.join(key) + item_box = wx.TextCtrl(panel, id=id_renew(item_name, update=True), + value=unicode(value)) + item_box.Bind(wx.EVT_TEXT, bind) + item_text = wx.StaticText(panel, label=translate_key(item_name)) + item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) + item_sizer.Add(item_box) + return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} + + +def create_button(source_class=None, panel=None, key=None, value=None, + bind=None, enabled=True, multiple=None, **kwargs): + item_sizer = wx.BoxSizer(wx.VERTICAL) + item_name = MODULE_KEY.join(key) + button_id = id_renew(item_name, update=True, multiple=multiple) + c_button = wx.Button(panel, id=button_id, label=translate_key(item_name)) + if not enabled: + c_button.Disable() + + if item_name in source_class.buttons: + source_class.buttons[item_name].append(c_button) + else: + source_class.buttons[item_name] = [c_button] + + if value: + c_button.Bind(wx.EVT_BUTTON, value, id=button_id) + else: + c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) + + item_sizer.Add(c_button) + return {'item': item_sizer} + + +def create_static_box(source_class, panel=None, value=None, + gui=None, key=None, show_hidden=None, **kwargs): + item_value = value + + static_box = wx.StaticBox(panel, label=translate_key(MODULE_KEY.join(key))) + static_sizer = wx.StaticBoxSizer(static_box, wx.VERTICAL) + instatic_sizer = wx.BoxSizer(wx.VERTICAL) + spacer_size = 7 + + max_text_size = 0 + text_ctrls = [] + log.debug("Working on {0}".format(MODULE_KEY.join(key))) + spacer = False + hidden_items = gui.get('hidden', []) + + for item, value in item_value.items(): + if item in hidden_items and not show_hidden: + continue + view = gui.get(item, {}).get('view', type(value)) + if view in source_class.controls.keys(): + bind_fn = source_class.controls[view] + elif callable(value): + bind_fn = source_class.controls['button'] + else: + raise GuiCreationError('Unable to create item, bad value map') + item_dict = bind_fn['function'](source_class=source_class, panel=static_box, item=item, + value=value, key=key + [item], + bind=bind_fn['bind'], gui=gui.get(item, {}), + from_sb=True) + if 'text_size' in item_dict: + if max_text_size < item_dict.get('text_size'): + max_text_size = item_dict['text_size'] + + text_ctrls.append(item_dict['text_ctrl']) + spacer = True if not spacer else instatic_sizer.AddSpacer(spacer_size) + instatic_sizer.Add(item_dict['item'], 0, wx.EXPAND, 5) + + if max_text_size: + for ctrl in text_ctrls: + ctrl.SetMinSize((max_text_size + 50, ctrl.GetSize()[1])) + + item_count = instatic_sizer.GetItemCount() + if not item_count: + static_sizer.Destroy() + return wx.BoxSizer(wx.VERTICAL) + + static_sizer.Add(instatic_sizer, 0, wx.EXPAND | wx.ALL, 5) + return static_sizer + + +def create_spin(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + gui = kwargs.get('gui') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + item_name = MODULE_KEY.join(key) + style = wx.ALIGN_LEFT + item_box = wx.SpinCtrl(panel, id=id_renew(item_name, update=True), min=gui['min'], max=gui['max'], + initial=int(value), style=style) + item_text = wx.StaticText(panel, label=translate_key(item_name)) + item_box.Bind(wx.EVT_SPINCTRL, bind) + item_box.Bind(wx.EVT_TEXT, bind) + item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) + item_sizer.Add(item_box) + return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} + + +def create_list(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + gui = kwargs.get('gui') + from_sb = kwargs.get('from_sb') + + view = gui.get('view') + is_dual = True if 'dual' in view else False + style = wx.ALIGN_CENTER_VERTICAL + border_sizer = wx.BoxSizer(wx.VERTICAL) + item_sizer = wx.BoxSizer(wx.VERTICAL) + + static_label = MODULE_KEY.join(key) + static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) + item_sizer.Add(static_text) + + addable_sizer = wx.BoxSizer(wx.HORIZONTAL) if gui.get('addable') else None + if addable_sizer: + item_input_key = MODULE_KEY.join(key + ['list_input']) + addable_sizer.Add(wx.TextCtrl(panel, id=id_renew(item_input_key, update=True)), 0, style) + if is_dual: + item_input2_key = MODULE_KEY.join(key + ['list_input2']) + addable_sizer.Add(wx.TextCtrl(panel, id=id_renew(item_input2_key, update=True)), 0, style) + + item_apply_key = MODULE_KEY.join(key + ['list_add']) + item_apply_id = id_renew(item_apply_key, update=True) + item_apply = wx.Button(panel, id=item_apply_id, label=translate_key(item_apply_key)) + addable_sizer.Add(item_apply, 0, style) + item_apply.Bind(wx.EVT_BUTTON, bind['add'], id=item_apply_id) + + item_remove_key = MODULE_KEY.join(key + ['list_remove']) + item_remove_id = id_renew(item_remove_key, update=True) + item_remove = wx.Button(panel, id=item_remove_id, label=translate_key(item_remove_key)) + addable_sizer.Add(item_remove, 0, style) + item_remove.Bind(wx.EVT_BUTTON, bind['remove'], id=item_remove_id) + + item_sizer.Add(addable_sizer, 0, wx.EXPAND) + + list_box = wx.grid.Grid(panel, id=id_renew(MODULE_KEY.join(key + ['list_box']), update=True)) + list_box.CreateGrid(0, 2 if is_dual else 1) + list_box.DisableDragColSize() + list_box.DisableDragRowSize() + list_box.Bind(wx.grid.EVT_GRID_SELECT_CELL, bind['select']) + + if is_dual: + for index, (item, item_value) in enumerate(value.items()): + list_box.AppendRows(1) + list_box.SetCellValue(index, 0, item) + list_box.SetCellValue(index, 1, item_value) + else: + for index, item in enumerate(value): + list_box.AppendRows(1) + list_box.SetCellValue(index, 0, item) + + list_box.SetColLabelSize(1) + list_box.SetRowLabelSize(1) + + if addable_sizer: + col_size = addable_sizer.GetMinSize()[0] - 2 + if is_dual: + first_col_size = list_box.GetColSize(0) + second_col_size = col_size - first_col_size if first_col_size < col_size else -1 + list_box.SetColSize(1, second_col_size) + else: + list_box.SetDefaultColSize(col_size, resizeExistingCols=True) + else: + list_box.AutoSize() + + item_sizer.Add(list_box) + + border_sizer.Add(item_sizer, 0, wx.EXPAND | wx.ALL, 5) + if from_sb: + return {'item': border_sizer} + else: + return border_sizer + + +def create_colour_picker(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + + item_name = MODULE_KEY.join(key) + colour_picker = CustomColourPickerCtrl() + item_box = colour_picker.create(panel, value=value, event=bind, key=key) + + item_text = wx.StaticText(panel, label=translate_key(item_name)) + item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) + item_sizer.Add(item_box, 1, wx.EXPAND) + return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} + + +def create_choose(**kwargs): + panel = kwargs.get('panel') + item_list = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + gui = kwargs.get('gui') + + view = gui.get('view') + is_single = True if 'single' in view else False + description = gui.get('description', False) + style = wx.LB_SINGLE if is_single else wx.LB_EXTENDED + border_sizer = wx.BoxSizer(wx.VERTICAL) + item_sizer = wx.BoxSizer(wx.VERTICAL) + list_items = [] + translated_items = [] + + static_label = MODULE_KEY.join(key) + static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) + item_sizer.Add(static_text) + + if gui['check_type'] in ['dir', 'folder', 'files']: + check_type = gui['check_type'] + keep_extension = gui['file_extension'] if 'file_extension' in gui else False + for item_in_list in os.listdir(os.path.join(PYTHON_FOLDER, gui['check'])): + item_path = os.path.join(PYTHON_FOLDER, gui['check'], item_in_list) + if check_type in ['dir', 'folder'] and os.path.isdir(item_path): + list_items.append(item_in_list) + elif check_type == 'files' and os.path.isfile(item_path): + if not keep_extension: + item_in_list = ''.join(os.path.basename(item_path).split('.')[:-1]) + if '__init__' not in item_in_list: + if item_in_list not in list_items: + list_items.append(item_in_list) + translated_items.append(translate_key(item_in_list)) + + item_key = MODULE_KEY.join(key + ['list_box']) + label_text = translate_key(item_key) + if label_text: + item_sizer.Add(wx.StaticText(panel, label=label_text, style=wx.ALIGN_RIGHT)) + if is_single: + item_list_box = KeyListBox(panel, id=id_renew(item_key, update=True), keys=list_items, + choices=translated_items if translated_items else list_items, style=style) + else: + item_list_box = KeyCheckListBox(panel, id=id_renew(item_key, update=True), keys=list_items, + choices=translated_items if translated_items else list_items) + item_list_box.Bind(wx.EVT_CHECKLISTBOX, bind['check_change']) + item_list_box.Bind(wx.EVT_LISTBOX, bind['change']) + + section_for = item_list if not is_single else {item_list: None} + if is_single: + item, value = section_for.items()[0] + if item not in item_list_box.GetItems(): + if item_list_box.GetItems(): + item_list_box.SetSelection(0) + else: + item_list_box.SetSelection(list_items.index(item)) + else: + check_items = [list_items.index(item) for item in section_for] + item_list_box.SetChecked(check_items) + + if description: + adv_sizer = wx.BoxSizer(wx.HORIZONTAL) + adv_sizer.Add(item_list_box, 0, wx.EXPAND) + + descr_key = MODULE_KEY.join(key + ['descr_explain']) + descr_text = wx.StaticText(panel, id=id_renew(descr_key, update=True), + label=translate_key(descr_key), style=wx.ST_NO_AUTORESIZE) + adv_sizer.Add(descr_text, 0, wx.EXPAND | wx.LEFT, 10) + + sizes = descr_text.GetSize() + sizes[0] -= 20 + descr_text.SetMinSize(sizes) + descr_text.Fit() + item_sizer.Add(adv_sizer) + else: + item_sizer.Add(item_list_box) + border_sizer.Add(item_sizer, 0, wx.EXPAND | wx.ALL, 5) + return border_sizer + + +def create_dropdown(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + gui = kwargs.get('gui') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + choices = gui.get('choices', []) + item_name = MODULE_KEY.join(key) + item_text = wx.StaticText(panel, label=translate_key(item_name)) + item_box = KeyChoice(panel, id=id_renew(item_name, update=True), + keys=choices, choices=choices) + item_box.Bind(wx.EVT_CHOICE, bind) + item_box.SetSelection(choices.index(value)) + item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) + item_sizer.Add(item_box) + return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} + + +def create_slider(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + gui = kwargs.get('gui') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + item_name = MODULE_KEY.join(key) + style = wx.SL_VALUE_LABEL | wx.SL_AUTOTICKS + item_box = wx.Slider(panel, id=id_renew(item_name, update=True), + minValue=gui['min'], maxValue=gui['max'], + value=int(value), style=style) + freq = (gui['max'] - gui['min'])/5 + item_box.SetTickFreq(freq) + item_box.SetLineSize(4) + item_box.Bind(wx.EVT_SCROLL, bind) + item_text = wx.StaticText(panel, label=translate_key(item_name)) + item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) + item_sizer.Add(item_box, 1, wx.EXPAND) + return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} + + +def create_checkbox(**kwargs): + panel = kwargs.get('panel') + value = kwargs.get('value') + key = kwargs.get('key') + bind = kwargs.get('bind') + + item_sizer = wx.BoxSizer(wx.HORIZONTAL) + style = wx.ALIGN_CENTER_VERTICAL + item_key = MODULE_KEY.join(key) + item_box = wx.CheckBox(panel, id=id_renew(item_key, update=True), + label=translate_key(item_key), style=style) + item_box.SetValue(value) + item_box.Bind(wx.EVT_CHECKBOX, bind) + item_sizer.Add(item_box, 0, wx.ALIGN_LEFT) + return {'item': item_sizer} diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index d05dc33..d06b666 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -1,26 +1,27 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov -import os -import threading -import json import Queue +import copy +import datetime +import json +import logging +import os import socket +import threading +from collections import OrderedDict + import cherrypy -import logging -import datetime -import copy +from cherrypy.lib.static import serve_file from scss import Compiler from scss.namespace import Namespace from scss.types import Color, Boolean, String, Number -from collections import OrderedDict -from cherrypy.lib.static import serve_file from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket +from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, CommandMessage, SystemMessage, RemoveMessageByID +from modules.helper.module import MessagingModule from modules.helper.parser import save_settings from modules.helper.system import THREADS, PYTHON_FOLDER, CONF_FOLDER -from modules.helper.module import MessagingModule -from gui import MODULE_KEY logging.getLogger('ws4py').setLevel(logging.ERROR) DEFAULT_STYLE = 'default' From f2f1b3b1460feba81831bddc8eb2231ad24b74d5 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Thu, 20 Apr 2017 21:18:16 +0300 Subject: [PATCH 13/43] LC-370 fix twitch /me --- modules/chat/twitch.py | 12 +++++++----- modules/helper/message.py | 19 ++++++++++++++++--- src/themes/default/assets/index.html | 4 +++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index cb4a345..df5a34c 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -64,10 +64,10 @@ class TwitchNormalDisconnect(Exception): class TwitchTextMessage(TextMessage): - def __init__(self, user, text): + def __init__(self, user, text, me): self.bttv_emotes = {} TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, - user=user, text=text) + user=user, text=text, me=me) class TwitchSystemMessage(SystemMessage): @@ -109,8 +109,10 @@ def process_message(self, msg): # Also, there is slight problem with some users, they don't have # the display-name tag, so we have to check their "real" username # and capitalize it because twitch does so, so we do the same. - if msg.type in ['pubmsg', 'action']: + if msg.type in ['pubmsg']: self._handle_message(msg) + elif msg.type in ['action']: + self._handle_message(msg, me=True) elif msg.type in ['clearchat']: self._handle_clearchat(msg) elif msg.type in ['usernotice']: @@ -183,8 +185,8 @@ def _handle_usernotice(self, msg): if msg.arguments: self._handle_message(msg, sub_message=True) - def _handle_message(self, msg, sub_message=False): - message = TwitchTextMessage(msg.source.split('!')[0], msg.arguments.pop()) + def _handle_message(self, msg, sub_message=False, me=False): + message = TwitchTextMessage(msg.source.split('!')[0], msg.arguments.pop(), me) if message.user == 'twitchnotify': self.irc_class.queue.put(TwitchSystemMessage(message.text, category='chat')) diff --git a/modules/helper/message.py b/modules/helper/message.py index 1380f5f..9a6638c 100644 --- a/modules/helper/message.py +++ b/modules/helper/message.py @@ -5,7 +5,7 @@ log = logging.getLogger('helper.message') -AVAILABLE_COMMANDS = ['remove_by_user', 'remove_by_id', 'replace_by_user', 'replace_by_id'] +AVAILABLE_COMMANDS = ['remove_by_user', 'remove_by_id', 'replace_by_user', 'replace_by_id', 'reload'] def _validate_command(command): @@ -115,9 +115,13 @@ def message_ids(self): class TextMessage(Message): def __init__(self, source, source_icon, user, text, emotes=None, badges=None, pm=False, - nick_colour=None, mid=None): + nick_colour=None, mid=None, me=False): """ Text message used by main chat logic + :param badges: Badges to display + :param nick_colour: Nick colour + :param mid: Message ID + :param me: /me notation :param source: Chat source (gg/twitch/beampro etc.) :param source_icon: Chat icon (as url) :param user: nickname @@ -134,13 +138,14 @@ def __init__(self, source, source_icon, user, text, self._emotes = [] if emotes is None else emotes self._badges = [] if badges is None else badges self._pm = pm + self._me = me self._nick_colour = nick_colour self._channel_name = None self._id = str(mid) if mid else str(uuid.uuid1()) self._jsonable += ['user', 'text', 'emotes', 'badges', 'id', 'source', 'source_icon', 'pm', - 'nick_colour', 'channel_name'] + 'nick_colour', 'channel_name', 'me'] @property def source(self): @@ -209,6 +214,14 @@ def channel_name(self, value): def id(self): return self._id + @property + def me(self): + return self._me + + @me.setter + def me(self, value): + self._me = value + class SystemMessage(TextMessage): def __init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, user=SOURCE_USER, emotes=None, category='system'): diff --git a/src/themes/default/assets/index.html b/src/themes/default/assets/index.html index 2d470dc..57fe650 100644 --- a/src/themes/default/assets/index.html +++ b/src/themes/default/assets/index.html @@ -34,7 +34,9 @@
{{message.display_name || message.user}}
-
:
+
From 257836d342f4b4a5aa32948c443bad2e9ff8baf9 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 7 May 2017 12:50:37 +0300 Subject: [PATCH 14/43] LC-385 Allow to use channelid instead of nickname --- modules/chat/goodgame.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 72aaebc..1bdfa00 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -33,6 +33,8 @@ CONF_DICT['config']['show_pm'] = True CONF_DICT['config']['socket'] = 'ws://chat.goodgame.ru:8081/chat/websocket' CONF_DICT['config']['show_channel_names'] = True +CONF_DICT['config']['use_channel_id'] = False +CONF_DICT['config']['check_viewers'] = True CONF_DICT['config']['channels_list'] = [] SMILE_REGEXP = r':(\w+|\d+):' SMILE_FORMAT = ':{}:' @@ -278,7 +280,7 @@ def system_message(self, msg, category='system'): class GGThread(threading.Thread): - def __init__(self, queue, address, nick, **kwargs): + def __init__(self, queue, address, nick, use_chid, **kwargs): threading.Thread.__init__(self) # Basic value setting. # Daemon is needed so when main programm exits @@ -287,7 +289,8 @@ def __init__(self, queue, address, nick, **kwargs): self.queue = queue self.address = address self.nick = nick - self.ch_id = None + if use_chid: + self.ch_id = nick if int(nick) else None self.kwargs = kwargs self.ws = None @@ -415,8 +418,9 @@ def _gui_settings(self): def _test_class(self): return TestGG(self) - @staticmethod - def get_viewers(channel): + def get_viewers(self, channel): + if not self._conf_params['config']['config']['check_viewers']: + return NA_MESSAGE streams_url = 'http://api2.goodgame.ru/streams/{0}'.format(channel) try: request = requests.get(streams_url) @@ -434,6 +438,7 @@ def get_viewers(channel): def _set_chat_online(self, chat): ChatModule.set_chat_online(self, chat) gg = GGThread(self.queue, self.host, chat, + self._conf_params['config']['config']['use_channel_id'], settings=self._conf_params['settings'], chat_module=self) self.channels[chat] = gg gg.start() From 222302d2d5a24b57c3fae56050e1557296993b19 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 7 May 2017 13:00:30 +0300 Subject: [PATCH 15/43] LC-387 Fix gg tests --- .../{alpine => _alpine}/build-deps/Dockerfile | 0 .../dockerfiles/{alpine => _alpine}/build_order.json | 0 .../dockerfiles/{alpine => _alpine}/testing/Dockerfile | 0 .../{alpine => _alpine}/wxpython/Dockerfile | 0 modules/messaging/webchat.py | 2 +- src/jenkins/cfg/goodgame.cfg | 3 ++- src/jenkins/chat_tests/_get_history.sh | 10 ++++++++++ src/jenkins/chat_tests/get_history.sh | 6 ------ 8 files changed, 13 insertions(+), 8 deletions(-) rename docker/dockerfiles/{alpine => _alpine}/build-deps/Dockerfile (100%) rename docker/dockerfiles/{alpine => _alpine}/build_order.json (100%) rename docker/dockerfiles/{alpine => _alpine}/testing/Dockerfile (100%) rename docker/dockerfiles/{alpine => _alpine}/wxpython/Dockerfile (100%) create mode 100644 src/jenkins/chat_tests/_get_history.sh delete mode 100644 src/jenkins/chat_tests/get_history.sh diff --git a/docker/dockerfiles/alpine/build-deps/Dockerfile b/docker/dockerfiles/_alpine/build-deps/Dockerfile similarity index 100% rename from docker/dockerfiles/alpine/build-deps/Dockerfile rename to docker/dockerfiles/_alpine/build-deps/Dockerfile diff --git a/docker/dockerfiles/alpine/build_order.json b/docker/dockerfiles/_alpine/build_order.json similarity index 100% rename from docker/dockerfiles/alpine/build_order.json rename to docker/dockerfiles/_alpine/build_order.json diff --git a/docker/dockerfiles/alpine/testing/Dockerfile b/docker/dockerfiles/_alpine/testing/Dockerfile similarity index 100% rename from docker/dockerfiles/alpine/testing/Dockerfile rename to docker/dockerfiles/_alpine/testing/Dockerfile diff --git a/docker/dockerfiles/alpine/wxpython/Dockerfile b/docker/dockerfiles/_alpine/wxpython/Dockerfile similarity index 100% rename from docker/dockerfiles/alpine/wxpython/Dockerfile rename to docker/dockerfiles/_alpine/wxpython/Dockerfile diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index d06b666..9cdff89 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -26,7 +26,7 @@ logging.getLogger('ws4py').setLevel(logging.ERROR) DEFAULT_STYLE = 'default' DEFAULT_PRIORITY = 9001 -HISTORY_SIZE = 5 +HISTORY_SIZE = 40 HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") s_queue = Queue.Queue() log = logging.getLogger('webchat') diff --git a/src/jenkins/cfg/goodgame.cfg b/src/jenkins/cfg/goodgame.cfg index 6e53d2c..bd1be8d 100644 --- a/src/jenkins/cfg/goodgame.cfg +++ b/src/jenkins/cfg/goodgame.cfg @@ -1,3 +1,4 @@ config: + use_channel_id: true channels_list: - - czt + - 15009 diff --git a/src/jenkins/chat_tests/_get_history.sh b/src/jenkins/chat_tests/_get_history.sh new file mode 100644 index 0000000..d0f6a08 --- /dev/null +++ b/src/jenkins/chat_tests/_get_history.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +PORT=8080 + +echo "This is get_history test" +if [ -z "$(curl http://localhost:${PORT}/rest/webchat/history)" ]; then + exit 1 +fi + +curl http://localhost:${PORT}/rest/webchat/history diff --git a/src/jenkins/chat_tests/get_history.sh b/src/jenkins/chat_tests/get_history.sh deleted file mode 100644 index 93ba403..0000000 --- a/src/jenkins/chat_tests/get_history.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -echo "This is get_history test" -if [ -z "$(curl localhost:8080/rest/webchat/history)" ]; then - exit 1 -fi From 90088695c1b9256d2a9a87324997988b8e949773 Mon Sep 17 00:00:00 2001 From: Arthur Bondarenko Date: Thu, 11 May 2017 20:28:48 +0300 Subject: [PATCH 16/43] Updated "Special Thanks" in README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5cf6034..27f8f79 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Docker build is available for testing LalkaChat, uses XPRA to display GUI from b - `docker run -rm --name chat-test -p 8080:8080 -v :/usr/lib/python2.7/site-packages/LalkaChat/conf deforce/lalkachat:testing` ## Special Thanks: -ftpud - for fixing IE compatibility (Old problem with IE Browser) -JAre - for being awesome with his docker stuff -ichursin - for deep knowledge in JavaScript and helping me with code -l0stparadis3 - for helping and testing in Linux environment +ftpud - for fixing IE compatibility (Old problem with IE Browser) +JAre - for being awesome with his docker stuff +[ichursin](https://github.com/ichursin) - for deep knowledge in JavaScript and helping me with code +l0stparadis3 - for helping and testing in Linux environment From 529dfb46330c0cd9aee0ddea914595bdeb619e15 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sat, 22 Apr 2017 15:49:54 +0300 Subject: [PATCH 17/43] LC-393 Interface types change to Objects --- main.py | 109 ++++++++-------- messaging.py | 24 ++-- modules/chat/beampro.py | 11 +- modules/chat/goodgame.py | 28 ++-- modules/chat/hitbox.py | 11 +- modules/chat/sc2tv.py | 14 +- modules/chat/twitch.py | 22 ++-- modules/gui.py | 227 +++++++++++++++++++-------------- modules/helper/functions.py | 29 +++++ modules/helper/module.py | 17 ++- modules/helper/parser.py | 61 ++++++--- modules/helper/system.py | 4 + modules/interface/controls.py | 2 +- modules/interface/functions.py | 117 +++++++---------- modules/interface/types.py | 198 ++++++++++++++++++++++++++++ modules/messaging/blacklist.py | 15 ++- modules/messaging/c2b.py | 6 +- modules/messaging/df.py | 11 +- modules/messaging/levels.py | 19 ++- modules/messaging/logger.py | 14 +- modules/messaging/mentions.py | 7 +- modules/messaging/webchat.py | 158 +++++++++++++---------- translations/en/webchat.key | 11 +- 23 files changed, 706 insertions(+), 409 deletions(-) create mode 100644 modules/helper/functions.py create mode 100644 modules/interface/types.py diff --git a/main.py b/main.py index 3681794..0d079c7 100644 --- a/main.py +++ b/main.py @@ -14,11 +14,16 @@ from modules.helper.parser import load_from_config_file from modules.helper.system import load_translations_keys, PYTHON_FOLDER, CONF_FOLDER, MAIN_CONF_FILE, MODULE_FOLDER, \ LOG_FOLDER, GUI_TAG, TRANSLATION_FOLDER, LOG_FILE, LOG_FORMAT, get_language, get_update, ModuleLoadException +from modules.interface.types import LCStaticBox, LCText, LCBool, LCButton, LCPanel, LCSpin, LCSlider, LCChooseMultiple VERSION = '0.3.6' SEM_VERSION = semantic_version.Version(VERSION) +def button_test(): + log.info('HelloWorld') + + def init(): def close(): for l_module, l_module_dict in loaded_modules.iteritems(): @@ -34,7 +39,7 @@ def close(): window = None # Creating dict with folder settings - main_config = {'root_folder': PYTHON_FOLDER, + base_config = {'root_folder': PYTHON_FOLDER, 'conf_folder': CONF_FOLDER, 'main_conf_file': MAIN_CONF_FILE, 'main_conf_file_loc': MAIN_CONF_FILE, @@ -57,28 +62,28 @@ def close(): exit() log.info("Loading basic configuration") - main_config_dict = OrderedDict() - main_config_dict['gui_information'] = OrderedDict() - main_config_dict['gui_information']['category'] = 'main' - main_config_dict['gui_information']['width'] = '450' - main_config_dict['gui_information']['height'] = '500' - main_config_dict['gui_information']['pos_x'] = '10' - main_config_dict['gui_information']['pos_y'] = '10' - main_config_dict['system'] = OrderedDict() - main_config_dict['system']['log_level'] = 'INFO' - main_config_dict['system']['testing_mode'] = False - main_config_dict['gui'] = OrderedDict() - main_config_dict['gui']['cli'] = False - main_config_dict['gui']['show_icons'] = False - main_config_dict['gui']['show_hidden'] = False - main_config_dict['gui']['gui'] = True - main_config_dict['gui']['on_top'] = True - main_config_dict['gui']['show_browser'] = True - main_config_dict['gui']['show_counters'] = True - main_config_dict['gui']['transparency'] = 50 - main_config_dict['gui']['borderless'] = False - main_config_dict['gui']['reload'] = None - main_config_dict['language'] = get_language() + main_config_dict = LCPanel() + main_config_dict['gui_information'] = LCStaticBox() + main_config_dict['gui_information']['category'] = LCText('main') + main_config_dict['gui_information']['width'] = LCText('450') + main_config_dict['gui_information']['height'] = LCText('500') + main_config_dict['gui_information']['pos_x'] = LCText('10') + main_config_dict['gui_information']['pos_y'] = LCText('10') + main_config_dict['system'] = LCStaticBox() + main_config_dict['system']['log_level'] = LCText('INFO') + main_config_dict['system']['testing_mode'] = LCBool(False) + main_config_dict['gui'] = LCStaticBox() + main_config_dict['gui']['cli'] = LCBool(False) + main_config_dict['gui']['show_icons'] = LCBool(False) + main_config_dict['gui']['show_hidden'] = LCBool(False) + main_config_dict['gui']['gui'] = LCBool(True) + main_config_dict['gui']['on_top'] = LCBool(True) + main_config_dict['gui']['show_browser'] = LCBool(True) + main_config_dict['gui']['show_counters'] = LCBool(True) + main_config_dict['gui']['transparency'] = LCSlider(100, min_v=0, max_v=100) + main_config_dict['gui']['borderless'] = LCBool(False) + main_config_dict['gui']['reload'] = LCButton(button_test) + main_config_dict['language'] = LCText(get_language()) main_config_gui = { 'language': { @@ -103,27 +108,28 @@ def close(): # Adding config for main module main_class = BaseModule( conf_params={ - 'root_folder': main_config['root_folder'], + 'root_folder': base_config['root_folder'], 'logs_folder': LOG_FOLDER, - 'config': load_from_config_file(MAIN_CONF_FILE, main_config_dict), - 'gui': main_config_gui }, + config=main_config_dict, + gui=main_config_gui, conf_file_name='config.cfg' ) loaded_modules['main'] = main_class.conf_params() - root_logger.setLevel(level=logging.getLevelName(main_config_dict['system'].get('log_level', 'INFO'))) - - gui_settings['gui'] = main_config_dict[GUI_TAG].get('gui') - gui_settings['on_top'] = main_config_dict[GUI_TAG].get('on_top') - gui_settings['transparency'] = main_config_dict[GUI_TAG].get('transparency') - gui_settings['borderless'] = main_config_dict[GUI_TAG].get('borderless') - gui_settings['language'] = main_config_dict.get('language') - gui_settings['show_hidden'] = main_config_dict[GUI_TAG].get('show_hidden') - gui_settings['size'] = (int(main_config_dict['gui_information'].get('width')), - int(main_config_dict['gui_information'].get('height'))) - gui_settings['position'] = (int(main_config_dict['gui_information'].get('pos_x')), - int(main_config_dict['gui_information'].get('pos_y'))) - gui_settings['show_browser'] = main_config_dict['gui'].get('show_browser') + main_config = main_class.conf_params()['config'] + root_logger.setLevel(level=logging.getLevelName(main_config['system'].get('log_level', 'INFO'))) + + gui_settings['gui'] = main_config[GUI_TAG].get('gui') + gui_settings['on_top'] = main_config[GUI_TAG].get('on_top') + gui_settings['transparency'] = main_config[GUI_TAG].get('transparency') + gui_settings['borderless'] = main_config[GUI_TAG].get('borderless') + gui_settings['language'] = main_config.get('language') + gui_settings['show_hidden'] = main_config[GUI_TAG].get('show_hidden') + gui_settings['size'] = (int(main_config['gui_information'].get('width')), + int(main_config['gui_information'].get('height'))) + gui_settings['position'] = (int(main_config['gui_information'].get('pos_x')), + int(main_config['gui_information'].get('pos_y'))) + gui_settings['show_browser'] = main_config['gui'].get('show_browser') # Checking updates log.info("Checking for updates") @@ -145,35 +151,36 @@ def close(): queue = Queue.Queue() # Loading module for message processing... msg = messaging.Message(queue) - loaded_modules.update(msg.load_modules(main_config, loaded_modules['main'])) + loaded_modules.update(msg.load_modules(base_config, loaded_modules['main'])) msg.start() log.info("Loading Chats") # Trying to dynamically load chats that are in config file. - chat_modules = os.path.join(CONF_FOLDER, "chat_modules.cfg") + chat_modules_file = os.path.join(CONF_FOLDER, "chat_modules.cfg") chat_location = os.path.join(MODULE_FOLDER, "chat") chat_conf_dict = OrderedDict() chat_conf_dict['gui_information'] = {'category': 'chat'} - chat_conf_dict['chats'] = [] + chat_conf_dict['chats'] = LCChooseMultiple( + [], + check_type='files', + folder=os.path.sep.join(['modules', 'chat']), + keep_extension=False + ) chat_conf_gui = { - 'chats': { - 'view': 'choose_multiple', - 'check_type': 'files', - 'check': os.path.sep.join(['modules', 'chat']), - 'file_extension': False}, - 'non_dynamic': ['chats.list_box']} + 'non_dynamic': ['chats.list_box'] + } chat_module = BaseModule( conf_params={ - 'config': load_from_config_file(chat_modules, chat_conf_dict), + 'config': load_from_config_file(chat_modules_file, chat_conf_dict), 'gui': chat_conf_gui }, conf_file_name='chat_modules.cfg' ) loaded_modules['chat'] = chat_module.conf_params() - for chat_module in chat_conf_dict['chats']: + for chat_module in chat_conf_dict['chats'].simple(): log.info("Loading chat module: {0}".format(chat_module)) module_location = os.path.join(chat_location, chat_module + ".py") if os.path.isfile(module_location): @@ -197,7 +204,7 @@ def close(): for f_module, f_config in loaded_modules.iteritems(): if 'class' in f_config: try: - f_config['class'].load_module(main_settings=main_config, loaded_modules=loaded_modules, + f_config['class'].load_module(main_settings=base_config, loaded_modules=loaded_modules, queue=queue) log.debug('loaded module {}'.format(f_module)) except ModuleLoadException: diff --git a/messaging.py b/messaging.py index f450866..f18dfda 100644 --- a/messaging.py +++ b/messaging.py @@ -11,7 +11,7 @@ from modules.helper.module import BaseModule, MessagingModule from modules.helper.system import ModuleLoadException, THREADS, CONF_FOLDER from modules.helper.parser import load_from_config_file - +from modules.interface.types import LCPanel, LCChooseMultiple log = logging.getLogger('messaging') MODULE_PRI_DEFAULT = '100' @@ -43,17 +43,19 @@ def load_modules(self, main_config, settings): modules_list = OrderedDict() conf_file = os.path.join(main_config['conf_folder'], "messaging_modules.cfg") - conf_dict = OrderedDict() + conf_dict = LCPanel() conf_dict['gui_information'] = {'category': 'messaging'} - conf_dict['messaging'] = {'webchat': None} + conf_dict['messaging'] = LCChooseMultiple( + ['webchat'], + check_type='files', + folder='modules/messaging', + keep_extension=False, + description=True + ) conf_gui = { - 'messaging': {'check': 'modules/messaging', - 'check_type': 'files', - 'file_extension': False, - 'view': 'choose_multiple', - 'description': True}, - 'non_dynamic': ['messaging.*']} + 'non_dynamic': ['messaging.*'] + } config = load_from_config_file(conf_file, conf_dict) messaging_module = BaseModule( conf_params={ @@ -69,8 +71,8 @@ def load_modules(self, main_config, settings): modules = {} # Loading modules from cfg. - if len(conf_dict['messaging']) > 0: - for m_module in conf_dict['messaging']: + if len(conf_dict['messaging'].value) > 0: + for m_module in conf_dict['messaging'].value: log.info("Loading %s" % m_module) # We load the module, and then we initalize it. # When writing your modules you should have class with the diff --git a/modules/chat/beampro.py b/modules/chat/beampro.py index e609f59..d95dee5 100644 --- a/modules/chat/beampro.py +++ b/modules/chat/beampro.py @@ -8,7 +8,6 @@ import requests import Queue import logging -from collections import OrderedDict import time @@ -17,6 +16,7 @@ from ws4py.client.threadedclient import WebSocketClient from modules.helper.system import NA_MESSAGE, translate_key +from modules.interface.types import LCStaticBox, LCBool, LCPanel logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('beampro') @@ -28,10 +28,10 @@ API_URL = 'https://beam.pro/api/v1{}' -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel(icon=FILE_ICON) CONF_DICT['gui_information'] = {'category': 'chat'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['show_pm'] = True +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['show_pm'] = LCBool(True) CONF_GUI = { 'config': { @@ -39,8 +39,7 @@ 'view': 'list', 'addable': 'true' } - }, - 'icon': FILE_ICON + } } diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 1bdfa00..7256747 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -10,7 +10,6 @@ import string import threading import time -from collections import OrderedDict import requests from ws4py.client.threadedclient import WebSocketClient @@ -19,6 +18,7 @@ from modules.helper.message import TextMessage, SystemMessage, Emote, RemoveMessageByID from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE +from modules.interface.types import LCStaticBox, LCPanel, LCBool, LCText, LCGridSingle logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('goodgame') @@ -27,15 +27,15 @@ FILE_ICON = os.path.join('img', 'gg.png') SYSTEM_USER = 'GoodGame' ID_PREFIX = 'gg_{0}' -CONF_DICT = OrderedDict() + +CONF_DICT = LCPanel(icon=FILE_ICON) CONF_DICT['gui_information'] = {'category': 'chat'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['show_pm'] = True -CONF_DICT['config']['socket'] = 'ws://chat.goodgame.ru:8081/chat/websocket' -CONF_DICT['config']['show_channel_names'] = True -CONF_DICT['config']['use_channel_id'] = False -CONF_DICT['config']['check_viewers'] = True -CONF_DICT['config']['channels_list'] = [] +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['show_pm'] = LCBool(True) +CONF_DICT['config']['socket'] = LCText('ws://chat.goodgame.ru:8081/chat/websocket') +CONF_DICT['config']['show_channel_names'] = LCBool(True) +CONF_DICT['config']['use_channel_id'] = LCBool(False) +CONF_DICT['config']['check_viewers'] = LCBool(True) SMILE_REGEXP = r':(\w+|\d+):' SMILE_FORMAT = ':{}:' @@ -47,8 +47,8 @@ 'addable': 'true' } }, - 'non_dynamic': ['config.socket'], - 'icon': FILE_ICON} + 'non_dynamic': ['config.socket'] +} class GoodgameTextMessage(TextMessage): @@ -289,8 +289,10 @@ def __init__(self, queue, address, nick, use_chid, **kwargs): self.queue = queue self.address = address self.nick = nick - if use_chid: - self.ch_id = nick if int(nick) else None + try: + self.ch_id = int(nick) + except: + self.ch_id = None self.kwargs = kwargs self.ws = None diff --git a/modules/chat/hitbox.py b/modules/chat/hitbox.py index 0820409..bde6dcb 100644 --- a/modules/chat/hitbox.py +++ b/modules/chat/hitbox.py @@ -15,6 +15,7 @@ from modules.helper.message import TextMessage, Emote, SystemMessage, RemoveMessageByUser from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT +from modules.interface.types import LCStaticBox, LCBool, LCPanel logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('hitbox') @@ -31,10 +32,10 @@ PING_DELAY = 10 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel(icon=FILE_ICON) CONF_DICT['gui_information'] = {'category': 'chat'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['show_nickname_colors'] = False +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['show_nickname_colors'] = LCBool(False) CONF_GUI = { 'config': { @@ -42,8 +43,8 @@ 'view': 'list', 'addable': 'true' }, - }, - 'icon': FILE_ICON} + } +} class HitboxAPIError(Exception): diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index 16cc4f5..aa173a3 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -16,6 +16,7 @@ from modules.helper.message import TextMessage, SystemMessage, Emote from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT +from modules.interface.types import LCStaticBox, LCPanel, LCBool, LCText logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('sc2tv') @@ -29,13 +30,12 @@ PING_DELAY = 10 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel(icon=FILE_ICON) CONF_DICT['gui_information'] = {'category': 'chat'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['show_pm'] = True -CONF_DICT['config']['socket'] = 'ws://funstream.tv/socket.io/' -CONF_DICT['config']['show_channel_names'] = True -CONF_DICT['config']['channels_list'] = [] +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['show_pm'] = LCBool(True) +CONF_DICT['config']['socket'] = LCText('ws://funstream.tv/socket.io/') +CONF_DICT['config']['show_channel_names'] = LCBool(True) CONF_GUI = { 'config': { @@ -46,7 +46,7 @@ }, }, 'non_dynamic': ['config.socket'], - 'icon': FILE_ICON} +} class Peka2TVAPIError(Exception): diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index df5a34c..7160eb2 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -15,6 +15,7 @@ from modules.helper.message import TextMessage, SystemMessage, Badge, Emote, RemoveMessageByUser from modules.helper.module import ChatModule from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE +from modules.interface.types import LCStaticBox, LCPanel, LCText, LCBool logging.getLogger('irc').setLevel(logging.ERROR) logging.getLogger('requests').setLevel(logging.ERROR) @@ -33,16 +34,15 @@ PING_DELAY = 10 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel(icon=FILE_ICON) CONF_DICT['gui_information'] = {'category': 'chat'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['host'] = 'irc.twitch.tv' -CONF_DICT['config']['port'] = 6667 -CONF_DICT['config']['show_pm'] = True -CONF_DICT['config']['bttv'] = True -CONF_DICT['config']['show_channel_names'] = True -CONF_DICT['config']['show_nickname_colors'] = False -CONF_DICT['config']['channels_list'] = [] +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['host'] = LCText('irc.twitch.tv') +CONF_DICT['config']['port'] = LCText(6667) +CONF_DICT['config']['show_pm'] = LCBool(True) +CONF_DICT['config']['bttv'] = LCBool(True) +CONF_DICT['config']['show_channel_names'] = LCBool(True) +CONF_DICT['config']['show_nickname_colors'] = LCBool(False) CONF_GUI = { 'config': { 'hidden': ['host', 'port'], @@ -51,8 +51,8 @@ 'addable': 'true' } }, - 'non_dynamic': ['config.host', 'config.port', 'config.bttv'], - 'icon': FILE_ICON} + 'non_dynamic': ['config.host', 'config.port', 'config.bttv'] +} class TwitchUserError(Exception): diff --git a/modules/gui.py b/modules/gui.py index 92b398e..b67fe3e 100644 --- a/modules/gui.py +++ b/modules/gui.py @@ -1,7 +1,11 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov +import collections + +from modules.helper.functions import find_by_type, parse_keys_to_string, deep_get from modules.interface.controls import KeyListBox, MainMenuToolBar import modules.interface.functions +from modules.interface.types import * try: from cefpython3.wx import chromectrl as browser @@ -16,7 +20,7 @@ import logging import webbrowser import wx -from modules.helper.system import MODULE_KEY, translate_key +from modules.helper.system import MODULE_KEY, translate_key, get_key from modules.helper.parser import return_type from modules.helper.module import BaseModule # ToDO: Support customization of borders/spacings @@ -90,15 +94,15 @@ def __init__(self, *args, **kwargs): self.changes = {} self.buttons = {} self.groups = { - dict: { - 'function': modules.interface.functions.create_static_box, + LCPanel: { + 'function': modules.interface.functions.create_panel, 'bind': None }, - OrderedDict: { + LCStaticBox: { 'function': modules.interface.functions.create_static_box, 'bind': None }, - 'list_dual': { + LCGridDual: { 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, @@ -106,7 +110,7 @@ def __init__(self, *args, **kwargs): 'select': self.select_cell } }, - 'list': { + LCGridSingle: { 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, @@ -114,59 +118,51 @@ def __init__(self, *args, **kwargs): 'select': self.select_cell } }, - 'choose_multiple': { + LCChooseMultiple: { 'function': modules.interface.functions.create_choose, 'bind': { 'change': self.on_listbox_change, 'check_change': self.on_checklist_box_change } }, - 'choose_single': { + LCChooseSingle: { 'function': modules.interface.functions.create_choose, 'bind': { 'change': self.on_listbox_change, 'check_change': self.on_checklist_box_change } - } + }, } self.controls = { - type(None): { + LCButton: { 'function': modules.interface.functions.create_button, 'bind': self.button_clicked }, - bool: { + LCBool: { 'function': modules.interface.functions.create_checkbox, 'bind': self.on_check_change }, - str: { - 'function': modules.interface.functions.create_textctrl, - 'bind': self.on_textctrl - }, - unicode: { + LCText: { 'function': modules.interface.functions.create_textctrl, 'bind': self.on_textctrl }, - int: { - 'function': modules.interface.functions.create_textctrl, - 'bind': self.on_textctrl - }, - 'spin': { + LCSpin: { 'function': modules.interface.functions.create_spin, 'bind': self.on_spinctrl }, - 'dropdown': { + LCDropdown: { 'function': modules.interface.functions.create_dropdown, 'bind': self.on_dropdown }, - 'slider': { + LCSlider: { 'function': modules.interface.functions.create_slider, 'bind': self.on_sliderctrl }, - 'colour_picker': { + LCColour: { 'function': modules.interface.functions.create_colour_picker, 'bind': self.on_color_picker }, - 'list': { + LCList: { 'function': modules.interface.functions.create_list, 'bind': { 'add': self.button_clicked, @@ -174,10 +170,6 @@ def __init__(self, *args, **kwargs): 'select': self.select_cell } }, - 'button': { - 'function': modules.interface.functions.create_button, - 'bind': self.button_clicked - } } self.list_map = {} self.redraw_map = {} @@ -258,46 +250,40 @@ def clear_changes(remote_change=None): split_keys = key.split(MODULE_KEY) module_name = split_keys[0] - config_section_name = split_keys[1] - if module_name in self.redraw_map: - for section, section_config in self.redraw_map[module_name].items(): + panel_key_list = split_keys[1:-2] + config_section_name = split_keys[-2] + if get_key(module_name, *panel_key_list) in self.redraw_map: + for section_name, section_config in self.redraw_map[get_key(module_name, *panel_key_list)].items(): if config_section_name in section_config['redraw_trigger']: redraw_key = MODULE_KEY.join(section_config['key']) self.redraw_item(section_config, value) clear_changes(redraw_key) enable_button() - - config = self.main_class.loaded_modules[module_name]['config'] - change_item = config_section_name + if panel_key_list: + config = deep_get(self.main_class.loaded_modules[module_name]['config'], *panel_key_list) + else: + config = self.main_class.loaded_modules[module_name]['config'] + ch_item = config_section_name if section: + check = config[ch_item].value if isinstance(config[ch_item], LCObject) else config[ch_item] if isinstance(value, list): - if set(config[change_item]) != set(value): - apply_changes() - else: - clear_changes() + apply_changes() if set(check) != set(value) else clear_changes() else: - if config[change_item] != return_type(value): + if check != return_type(value): apply_changes() else: clear_changes() elif item_type == 'gridbox': - main_tuple = config[change_item] - - if compare_2d_lists(value, main_tuple): + if compare_2d_lists(value, config[split_keys[-1]].simple()): clear_changes() else: apply_changes() else: - if isinstance(value, bool): - if config[change_item][split_keys[2]] != value: - apply_changes() - else: - clear_changes() + test_value = value if isinstance(value, bool) else return_type(value) + if config[ch_item][split_keys[-1]].simple() != test_value: + apply_changes() else: - if config[change_item][split_keys[2]] != return_type(value): - apply_changes() - else: - clear_changes() + clear_changes() def on_tree_ctrl_changed(self, event): self.settings_saved = False @@ -400,6 +386,14 @@ def on_list_operation(self, key, action): def on_color_picker(self, event): self.on_change(MODULE_KEY.join(event['key']), event['hex']) + def get_tree_item(self, key, node, image=-1, name_key=None): + item_data = wx.TreeItemData() + item_data.SetData(key) + return self.tree_ctrl.AppendItem( + node, translate_key(name_key if name_key else key), + data=item_data, + image=image) + def create_layout(self): self.main_grid = wx.BoxSizer(wx.HORIZONTAL) style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.TR_TWIST_BUTTONS | wx.TR_NO_LINES @@ -408,38 +402,44 @@ def create_layout(self): image_list = wx.ImageList(16, 16) tree_ctrl_id = modules.interface.functions.id_renew('settings.tree', update=True) - tree_ctrl = wx.TreeCtrl(self, id=tree_ctrl_id, style=style) - root_key = MODULE_KEY.join(['settings', 'tree', 'root']) - root_node = tree_ctrl.AddRoot(translate_key(root_key)) - for cat_name, category in self.categories.iteritems(): - item_key = MODULE_KEY.join(['settings', cat_name]) - item_data = wx.TreeItemData() - item_data.SetData(item_key) - - item_node = tree_ctrl.AppendItem(root_node, translate_key(item_key), data=item_data) - for module_name, module_settings in category.iteritems(): - if not module_name == cat_name: - if module_settings.get('gui', {}).get('icon'): - icon = wx.Bitmap(module_settings['gui']['icon']) - self.tree_ctrl_image_dict[module_name] = image_list.GetImageCount() - image_list.Add(icon) - else: - self.tree_ctrl_image_dict[module_name] = -1 - - f_item_key = MODULE_KEY.join([item_key, module_name]) - f_item_data = wx.TreeItemData() - f_item_data.SetData(f_item_key) - tree_ctrl.AppendItem(item_node, translate_key(module_name), - image=self.tree_ctrl_image_dict[module_name], - data=f_item_data) + self.tree_ctrl = wx.TreeCtrl(self, id=tree_ctrl_id, style=style) + root_key = get_key('settings', 'tree', 'root') + root_node = self.tree_ctrl.AddRoot(translate_key(root_key)) + for cat_name, category in self.categories.items(): + item_node = self.get_tree_item(get_key('settings', cat_name), root_node) + for module_name, module_settings in category.items(): + if module_name == cat_name: + continue + if '.' in module_name: + continue + + config = module_settings.get('config') + if config.icon: + icon = wx.Bitmap(config.icon) + self.tree_ctrl_image_dict[module_name] = image_list.GetImageCount() + image_list.Add(icon) + else: + self.tree_ctrl_image_dict[module_name] = -1 + + module_node = self.get_tree_item( + get_key('settings', cat_name, module_name), + item_node, name_key=module_name, + image=self.tree_ctrl_image_dict[module_name] + ) + + tree_nodes = self._get_panels(config) + if not tree_nodes: + continue + self.create_nested_tree_item(tree_nodes, node=module_node, + key=get_key('settings', cat_name, module_name)) + if self.show_icons: - tree_ctrl.AssignImageList(image_list) - tree_ctrl.ExpandAll() + self.tree_ctrl.AssignImageList(image_list) + self.tree_ctrl.ExpandAll() - self.tree_ctrl = tree_ctrl self.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_ctrl_changed, id=tree_ctrl_id) - self.main_grid.Add(tree_ctrl, 7, wx.EXPAND | wx.ALL, 7) + self.main_grid.Add(self.tree_ctrl, 7, wx.EXPAND | wx.ALL, 7) content_page_id = modules.interface.functions.id_renew(MODULE_KEY.join(['settings', 'content'])) self.content_page = wx.Panel(self, id=content_page_id) @@ -447,7 +447,7 @@ def create_layout(self): self.main_grid.Layout() self.SetSizer(self.main_grid) - tree_ctrl.SelectItem(tree_ctrl.GetFirstChild(root_node)[0]) + self.tree_ctrl.SelectItem(self.tree_ctrl.GetFirstChild(root_node)[0]) def fill_page_with_content(self, panel, keys): @@ -464,7 +464,7 @@ def fill_page_with_content(self, panel, keys): raise ModuleKeyError("Key not found in modules") module_data = self.categories[category][module_id] - custom_renderer = module_data['custom_renderer'] + custom_renderer = module_data.get('custom_renderer', False) module_config = module_data.get('config', {}) module_gui_config = module_data.get('gui', {}) @@ -513,7 +513,7 @@ def create_page_items(self, page_sizer, panel, config, gui, key): page_sc_window.SetScrollbars(5, 5, 10, 10) sizer = wx.BoxSizer(wx.VERTICAL) joined_keys = MODULE_KEY.join(key) - if 'redraw' in gui: + if gui and 'redraw' in gui: for redraw_target, redraw_settings in gui['redraw'].items(): if joined_keys not in self.redraw_map: self.redraw_map[joined_keys] = {} @@ -532,16 +532,19 @@ def create_page_items(self, page_sizer, panel, config, gui, key): for section_key, section_items in config.items(): if section_key in SKIP_TAGS: continue - - view = gui.get(section_key, {}).get('view', type(section_items)) + if isinstance(section_items, LCObject): + view = type(section_items) + else: + view = gui.get(section_key, {}).get('view', type(section_items)) if view in self.groups.keys(): data = self.groups[view] - gui_settings = gui.get(section_key, {}).copy() + gui_settings = gui.get(section_key, {}).copy() if gui else {} item_keys = key + [section_key] sizer_item = data['function']( source_class=self, panel=page_sc_window, item=section_key, value=section_items, bind=data['bind'], - gui=gui_settings, key=item_keys, from_sb=False) + gui=gui_settings, key=item_keys, from_sb=False, + show_hidden=self.show_hidden) if joined_keys in self.redraw_map.keys(): if section_key in self.redraw_map[joined_keys]: self.redraw_map[joined_keys][section_key].update({ @@ -598,11 +601,11 @@ def redraw_item(self, redraw_keys, redraw_value): if static_box: static_box.Destroy() sizer.Destroy() - new_sizer = fnc(panel=panel, item=redraw_keys['redraw_target'], + new_sizer = fnc(source_class=self, panel=panel, item=redraw_keys['redraw_target'], value=config, bind=bind, gui=config_gui, key=key) sizer_parent.Insert(item_index, new_sizer, 0, wx.EXPAND) - self.redraw_map[key[0]][key[1]]['item'] = new_sizer + self.redraw_map[get_key(*key[:-1])][key[-1]]['item'] = new_sizer self.main_grid.Layout() @@ -665,7 +668,11 @@ def save_module(self, module_name, changed_items): for item, change in changed_items.iteritems(): item_split = item.split(MODULE_KEY) - section, item_name = item_split[1:] if len(item_split) > 2 else (item_split[1], None) + panel_keys = item_split[1:-2] + section = item_split[-2] + item_name = item_split[-1] + + deep_config = deep_get(module_config, *panel_keys) for d_item in non_dynamic: if section in d_item: if MODULE_KEY.join([section, '*']) in d_item: @@ -676,13 +683,16 @@ def save_module(self, module_name, changed_items): break if item_split[-1] in ['list_box']: del item_split[-1] + item_name = item_split[-1] if len(item_split) == 2: - module_config[section] = change['value'] + item_type = type(deep_config[item_name]) + deep_config[item_name] = item_type(change['value']) else: value = change['value'] - if item == MODULE_KEY.join(['main', 'gui', 'show_hidden']): + if item == get_key('main', 'gui', 'show_hidden'): self.show_hidden = value - module_config[section][item_name] = value + item_type = type(deep_config[section][item_name]) + deep_config[section][item_name] = item_type(value) if 'class' in module_settings: module_settings['class'].apply_settings() return non_dynamic_check @@ -691,6 +701,31 @@ def select_cell(self, event): self.selected_cell = (event.GetRow(), event.GetCol()) event.Skip() + @staticmethod + def _get_panels(module_settings): + return find_by_type(module_settings, LCPanel) + + def create_nested_tree_item(self, tree_item, node=None, key=None): + for name, data in tree_item.items(): + category, module_name = key.split(MODULE_KEY)[1:3] + key_left = key.split(MODULE_KEY)[3:] + full_key_list = key_left + [name] + key_string = get_key(module_name, *full_key_list) + + node_l = self.get_tree_item(get_key(key, name), node, + name_key=get_key(key, name)) + if isinstance(data, collections.Mapping): + self.create_nested_tree_item(data, node_l, get_key(key, name)) + + if key_string in self.categories[category]: + continue + + panel_data = deep_get(self.categories[category][module_name]['config'], *full_key_list) + gui_data = deep_get(self.categories[category][module_name]['gui'], *full_key_list) + self.categories[category][key_string] = {} + self.categories[category][key_string]['config'] = panel_data + self.categories[category][key_string]['gui'] = gui_data + class StatusFrame(wx.Panel): def __init__(self, parent, **kwargs): @@ -770,9 +805,9 @@ def set_chat_online(self, module_name, channel): if module_name not in self.chats: self.chats[module_name] = {} if channel.lower() not in self.chats[module_name]: - config = self.chat_modules.get(module_name)['class'].conf_params() - icon = config['gui']['icon'] - multiple = config['config']['config']['show_channel_names'] + config = self.chat_modules.get(module_name)['class'].conf_params()['config'] + icon = config.icon + multiple = config['config']['show_channel_names'] self.chats[module_name][channel.lower()] = self._create_item(channel, icon, multiple) self.Layout() self.Refresh() diff --git a/modules/helper/functions.py b/modules/helper/functions.py new file mode 100644 index 0000000..27ceaf5 --- /dev/null +++ b/modules/helper/functions.py @@ -0,0 +1,29 @@ +import collections + + +def find_by_type(data, type_to_find): + found = collections.OrderedDict() + for key, value in data.iteritems(): + if isinstance(value, type_to_find): + found[key] = None + if isinstance(value, collections.Mapping): + types = find_by_type(data.get(key, {}), type_to_find) + if types: + found[key] = types + continue + return found + + +def parse_keys_to_string(data): + keys = [] + for name, value in data.items(): + keys.append(name) + if isinstance(value, collections.Mapping): + keys_from_dict = parse_keys_to_string(data.get(name, {})) + if keys_from_dict: + keys.extend(['.'.join([name] + [item]) for item in keys_from_dict]) + return keys + + +def deep_get(dictionary, *keys): + return reduce(lambda d, key: d.get(key, None) if isinstance(d, collections.Mapping) else None, keys, dictionary) diff --git a/modules/helper/module.py b/modules/helper/module.py index e40429d..8971398 100644 --- a/modules/helper/module.py +++ b/modules/helper/module.py @@ -5,6 +5,7 @@ from modules.helper import parser from modules.helper.message import TextMessage, Message +from modules.interface.types import LCPanel, LCStaticBox, LCBool, LCList from parser import save_settings, load_from_config_file from system import RestApiException, CONF_FOLDER @@ -13,10 +14,10 @@ 'custom_renderer': False } -CHAT_DICT = OrderedDict() -CHAT_DICT['config'] = OrderedDict() -CHAT_DICT['config']['show_channel_names'] = False -CHAT_DICT['config']['channels_list'] = [] +CHAT_DICT = LCPanel() +CHAT_DICT['config'] = LCStaticBox() +CHAT_DICT['config']['show_channel_names'] = LCBool(False) +CHAT_DICT['config']['channels_list'] = LCList() CHAT_GUI = { 'config': { @@ -35,6 +36,9 @@ def __init__(self, *args, **kwargs): self._conf_params = BASE_DICT.copy() self._conf_params['dependencies'] = set() + self.__conf_settings = kwargs.get('config', {}) + self.__gui_settings = kwargs.get('gui', {}) + self._loaded_modules = {} self._rest_api = {} self._module_name = self.__class__.__name__ @@ -52,7 +56,6 @@ def __init__(self, *args, **kwargs): 'config': load_from_config_file(conf_file, self._conf_settings()), 'gui': self._gui_settings(), 'settings': {}}) - self._conf_params.update(kwargs.get('conf_params', {})) def add_to_queue(self, q_type, data): @@ -79,14 +82,14 @@ def _conf_settings(self, *args, **kwargs): Override this method :rtype: object """ - return {} + return self.__conf_settings def _gui_settings(self, *args, **kwargs): """ Override this method :return: Settings for GUI (dict) """ - return {} + return self.__gui_settings def load_module(self, *args, **kwargs): self._loaded_modules = kwargs.get('loaded_modules') diff --git a/modules/helper/parser.py b/modules/helper/parser.py index 91950f2..f0e71ce 100644 --- a/modules/helper/parser.py +++ b/modules/helper/parser.py @@ -1,28 +1,45 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import os import collections + import yaml -from collections import OrderedDict from ConfigParser import RawConfigParser +from modules.interface.types import * + +DICT_MAPPING = { + LCPanel: dict, + LCStaticBox: dict, +} + + +def update(dst, src, overwrite=True): + for k, v in src.items(): + has_key = k in dst + dst_type = type(dst.get(k, v)) -def update(d, u, overwrite=True): - for k, v in u.iteritems(): if isinstance(v, collections.Mapping): - r = update(d.get(k, {}), v, overwrite=overwrite) - d[k] = r + r = update(dst.get(k, {}), v, overwrite=overwrite) + dst[k] = r else: - if not overwrite: - if k not in d: - d[k] = u[k] + if has_key and not overwrite: + if isinstance(v, LCObject) and not isinstance(dst.get(k), LCObject): + dst[k] = type(v)(dst.get(k)) + continue + elif has_key: + if isinstance(dst.get(k), LCObject): + if hasattr(dst[k], '_value'): + dst[k].value = v.value if isinstance(v, LCObject) else v + else: + dst[k] = dst_type(v) else: - continue + dst[k] = v else: - d[k] = u[k] - return d + dst[k] = dst_type(v) + return dst -def load_from_config_file(conf_file, conf_dict={}): +def load_from_config_file(conf_file, conf_dict=None): if not os.path.exists(conf_file): return conf_dict with open(conf_file, 'r') as conf_f: @@ -58,28 +75,36 @@ def get_config(conf_file): return heal_config -def convert_to_yaml(source, ignored_keys): - output = {} +def convert_to_dict(source, ignored_keys=(), ordered=False): + output = OrderedDict() if ordered else {} if not source: return output for item, value in source.items(): if item in ignored_keys: continue - if isinstance(value, OrderedDict): - output[item] = convert_to_yaml( + if type(value) in DICT_MAPPING: + output[item] = convert_to_dict( + value, + [key.replace('{}.'.format(item), '') for key in ignored_keys if key.startswith(item)] + ) + elif isinstance(value, OrderedDict): + output[item] = convert_to_dict( value, [key.replace('{}.'.format(item), '') for key in ignored_keys if key.startswith(item)] ) else: - output[item] = value + try: + output[item] = value.simple() + except: + output[item] = value return output def save_settings(conf_dict, ignored_sections=()): if 'file' not in conf_dict: return - output = convert_to_yaml(conf_dict.get('config'), ignored_sections) + output = convert_to_dict(conf_dict.get('config'), ignored_sections) with open(conf_dict.get('file'), 'w+') as conf_file: dump_text = yaml.safe_dump(output, default_flow_style=False, allow_unicode=True) conf_file.write(dump_text) diff --git a/modules/helper/system.py b/modules/helper/system.py index be7b4cf..7535e16 100644 --- a/modules/helper/system.py +++ b/modules/helper/system.py @@ -163,3 +163,7 @@ def get_update(sem_version): def get_language(): local_name, local_encoding = locale.getdefaultlocale() return LANGUAGE_DICT.get(local_name, 'en') + + +def get_key(*args): + return MODULE_KEY.join(args) diff --git a/modules/interface/controls.py b/modules/interface/controls.py index aef6902..595dc63 100644 --- a/modules/interface/controls.py +++ b/modules/interface/controls.py @@ -63,7 +63,7 @@ def create(self, panel, value="#FFFFFF", orientation=wx.HORIZONTAL, event=None, label_sizer.Add(label_text, 1, wx.ALIGN_CENTER) label_sizer2.Add(label_sizer, 1, wx.ALIGN_CENTER) label_panel.SetSizer(label_sizer2) - label_panel.SetBackgroundColour(value) + label_panel.SetBackgroundColour(str(value)) self.panel = label_panel button = wx.Button(panel, label=translate_key(MODULE_KEY.join(key + ['button']))) diff --git a/modules/interface/functions.py b/modules/interface/functions.py index 864908f..483a290 100644 --- a/modules/interface/functions.py +++ b/modules/interface/functions.py @@ -5,14 +5,10 @@ from modules.helper.system import MODULE_KEY, translate_key, log, PYTHON_FOLDER from modules.interface.controls import GuiCreationError, CustomColourPickerCtrl, KeyListBox, KeyCheckListBox, KeyChoice, \ id_renew +from modules.interface.types import LCPanel -def create_textctrl(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - +def create_textctrl(panel=None, value=None, key=None, bind=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_name = MODULE_KEY.join(key) item_box = wx.TextCtrl(panel, id=id_renew(item_name, update=True), @@ -39,7 +35,8 @@ def create_button(source_class=None, panel=None, key=None, value=None, source_class.buttons[item_name] = [c_button] if value: - c_button.Bind(wx.EVT_BUTTON, value, id=button_id) + # TODO: Implement button function pressing + c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) else: c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) @@ -49,6 +46,8 @@ def create_button(source_class=None, panel=None, key=None, value=None, def create_static_box(source_class, panel=None, value=None, gui=None, key=None, show_hidden=None, **kwargs): + if isinstance(value, LCPanel): + return wx.BoxSizer(wx.VERTICAL) item_value = value static_box = wx.StaticBox(panel, label=translate_key(MODULE_KEY.join(key))) @@ -65,16 +64,18 @@ def create_static_box(source_class, panel=None, value=None, for item, value in item_value.items(): if item in hidden_items and not show_hidden: continue - view = gui.get(item, {}).get('view', type(value)) + + view = type(value) if view in source_class.controls.keys(): bind_fn = source_class.controls[view] elif callable(value): bind_fn = source_class.controls['button'] else: + # bind_fn = {'function': create_empty} raise GuiCreationError('Unable to create item, bad value map') item_dict = bind_fn['function'](source_class=source_class, panel=static_box, item=item, value=value, key=key + [item], - bind=bind_fn['bind'], gui=gui.get(item, {}), + bind=bind_fn.get('bind'), gui=gui.get(item, {}), from_sb=True) if 'text_size' in item_dict: if max_text_size < item_dict.get('text_size'): @@ -97,17 +98,13 @@ def create_static_box(source_class, panel=None, value=None, return static_sizer -def create_spin(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - +def create_spin(panel=None, value=None, key=None, bind=None, **kwargs): + item_class = value item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_name = MODULE_KEY.join(key) style = wx.ALIGN_LEFT - item_box = wx.SpinCtrl(panel, id=id_renew(item_name, update=True), min=gui['min'], max=gui['max'], + item_box = wx.SpinCtrl(panel, id=id_renew(item_name, update=True), + min=item_class.min, max=item_class.max, initial=int(value), style=style) item_text = wx.StaticText(panel, label=translate_key(item_name)) item_box.Bind(wx.EVT_SPINCTRL, bind) @@ -117,14 +114,7 @@ def create_spin(**kwargs): return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} -def create_list(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - from_sb = kwargs.get('from_sb') - +def create_list(panel=None, value=None, key=None, bind=None, gui=None, from_sb=None, **kwargs): view = gui.get('view') is_dual = True if 'dual' in view else False style = wx.ALIGN_CENTER_VERTICAL @@ -196,12 +186,7 @@ def create_list(**kwargs): return border_sizer -def create_colour_picker(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - +def create_colour_picker(panel=None, value=None, key=None, bind=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_name = MODULE_KEY.join(key) @@ -214,16 +199,11 @@ def create_colour_picker(**kwargs): return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} -def create_choose(**kwargs): - panel = kwargs.get('panel') - item_list = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') +def create_choose(panel=None, value=None, key=None, bind=None, **kwargs): + item_list = value.value + item_class = value - view = gui.get('view') - is_single = True if 'single' in view else False - description = gui.get('description', False) + is_single = False if value.multiple else True style = wx.LB_SINGLE if is_single else wx.LB_EXTENDED border_sizer = wx.BoxSizer(wx.VERTICAL) item_sizer = wx.BoxSizer(wx.VERTICAL) @@ -231,18 +211,18 @@ def create_choose(**kwargs): translated_items = [] static_label = MODULE_KEY.join(key) - static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) - item_sizer.Add(static_text) - - if gui['check_type'] in ['dir', 'folder', 'files']: - check_type = gui['check_type'] - keep_extension = gui['file_extension'] if 'file_extension' in gui else False - for item_in_list in os.listdir(os.path.join(PYTHON_FOLDER, gui['check'])): - item_path = os.path.join(PYTHON_FOLDER, gui['check'], item_in_list) + if not item_class.empty_label: + static_text = wx.StaticText(panel, label=u'{}:'.format(translate_key(static_label)), style=wx.ALIGN_RIGHT) + item_sizer.Add(static_text) + + if item_class.check_type in ['dir', 'folder', 'files']: + check_type = item_class.check_type + for item_in_list in os.listdir(os.path.join(PYTHON_FOLDER, item_class.folder)): + item_path = os.path.join(PYTHON_FOLDER, item_class.folder, item_in_list) if check_type in ['dir', 'folder'] and os.path.isdir(item_path): list_items.append(item_in_list) elif check_type == 'files' and os.path.isfile(item_path): - if not keep_extension: + if not item_class.keep_extension: item_in_list = ''.join(os.path.basename(item_path).split('.')[:-1]) if '__init__' not in item_in_list: if item_in_list not in list_items: @@ -274,7 +254,7 @@ def create_choose(**kwargs): check_items = [list_items.index(item) for item in section_for] item_list_box.SetChecked(check_items) - if description: + if item_class.description: adv_sizer = wx.BoxSizer(wx.HORIZONTAL) adv_sizer.Add(item_list_box, 0, wx.EXPAND) @@ -294,33 +274,21 @@ def create_choose(**kwargs): return border_sizer -def create_dropdown(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - +def create_dropdown(panel=None, value=None, key=None, bind=None, gui=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) - choices = gui.get('choices', []) + choices = value.list item_name = MODULE_KEY.join(key) item_text = wx.StaticText(panel, label=translate_key(item_name)) item_box = KeyChoice(panel, id=id_renew(item_name, update=True), keys=choices, choices=choices) item_box.Bind(wx.EVT_CHOICE, bind) - item_box.SetSelection(choices.index(value)) + item_box.SetSelection(choices.index(str(value))) item_sizer.Add(item_text, 0, wx.ALIGN_CENTER) item_sizer.Add(item_box) return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} -def create_slider(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - gui = kwargs.get('gui') - +def create_slider(panel=None, value=None, key=None, bind=None, gui=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_name = MODULE_KEY.join(key) style = wx.SL_VALUE_LABEL | wx.SL_AUTOTICKS @@ -337,18 +305,21 @@ def create_slider(**kwargs): return {'item': item_sizer, 'text_size': item_text.GetSize()[0], 'text_ctrl': item_text} -def create_checkbox(**kwargs): - panel = kwargs.get('panel') - value = kwargs.get('value') - key = kwargs.get('key') - bind = kwargs.get('bind') - +def create_checkbox(panel=None, value=None, key=None, bind=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) style = wx.ALIGN_CENTER_VERTICAL item_key = MODULE_KEY.join(key) item_box = wx.CheckBox(panel, id=id_renew(item_key, update=True), label=translate_key(item_key), style=style) - item_box.SetValue(value) + item_box.SetValue(bool(value)) item_box.Bind(wx.EVT_CHECKBOX, bind) item_sizer.Add(item_box, 0, wx.ALIGN_LEFT) return {'item': item_sizer} + + +def create_panel(*args, **kwargs): + return create_static_box(*args, **kwargs) + + +def create_empty(*args, **kwargs): + return {'item': wx.BoxSizer(wx.HORIZONTAL)} diff --git a/modules/interface/types.py b/modules/interface/types.py new file mode 100644 index 0000000..6a22c92 --- /dev/null +++ b/modules/interface/types.py @@ -0,0 +1,198 @@ +from collections import OrderedDict + +import logging + + +class LCObject(object): + def __init__(self, value, *args, **kwargs): + self._value = value + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = value + + +class LCPanel(OrderedDict, LCObject): + def __init__(self, icon=None, *args, **kwargs): + self.icon = icon + OrderedDict.__init__(self, *args, **kwargs) + + +class LCStaticBox(OrderedDict, LCObject): + def __init__(self, *args, **kwargs): + OrderedDict.__init__(self, *args, **kwargs) + + +class LCText(unicode, LCObject): + def __init__(self, value=None): + unicode.__init__(value) + + def simple(self): + return unicode(self) + + +class LCColour(LCObject): + def __init__(self, *args, **kwargs): + super(LCColour, self).__init__(*args, **kwargs) + + def __repr__(self): + return str(self._value) + + def __getitem__(self, item): + return str(self._value)[item] + + def simple(self): + return unicode(self._value) + + def startswith(self, value): + return str(self._value).startswith(value) + + +class LCBool(LCObject): + def __init__(self, value, *args, **kwargs): + super(LCBool, self).__init__(value, *args, **kwargs) + + def __nonzero__(self): + return bool(self._value) + + def __repr__(self): + return str(bool(self._value)) + + def simple(self): + return bool(self) + + +class LCList(list, LCObject): + def __init__(self, value=()): + list.__init__(self, value) + + def simple(self): + return list(self) + + +class LCButton(LCObject): + def __init__(self, fnc=None, *args, **kwargs): + super(LCButton, self).__init__(fnc, *args, **kwargs) + + def __repr__(self): + return str(self.value) + + +class LCSpin(LCObject): + def __init__(self, value, min_v=0, max_v=100, *args, **kwargs): + super(LCSpin, self).__init__(value, *args, **kwargs) + self.min = min_v + self.max = max_v + + def __repr__(self): + return str(self._value) + + def __int__(self): + return self.value + + def simple(self): + return int(self.value) + + +class LCSlider(LCObject): + def __init__(self, value, min_v=0, max_v=100, *args, **kwargs): + super(LCSlider, self).__init__(value, *args, **kwargs) + self.min = min_v + self.max = max_v + + def __repr__(self): + return str(self._value) + + def __int__(self): + return self._value + + def __mul__(self, other): + return self._value * other + + def simple(self): + return int(self._value) + + +class LCDropdown(LCObject): + def __init__(self, value, available_list=()): + super(LCDropdown, self).__init__(value) + self.list = available_list + + def __repr__(self): + return self._value + + def simple(self): + return str(self._value) + + +class LCGridDual(OrderedDict, LCObject): + def __init__(self, *args, **kwargs): + OrderedDict.__init__(self, *args, **kwargs) + + def simple(self): + return OrderedDict(self) + + +class LCGridSingle(list, LCObject): + def __init__(self, value=list()): + list.__init__(self, value) + + def simple(self): + return list(self) + + +class LCChooseSingle(LCObject): + def __init__(self, value=(), check_type=None, empty_label=False, *args, **kwargs): + super(LCChooseSingle, self).__init__(value, *args, **kwargs) + self.multiple = False + self.check_type = check_type + self.keep_extension = kwargs.get('keep_extension', False) + self.description = kwargs.get('description') + self.empty_label = empty_label + if check_type in ['dir', 'folder', 'files']: + self.folder = kwargs.get('folder') + + def simple(self): + return self.value + + +class LCChooseMultiple(LCChooseSingle): + def __init__(self, value=(), *args, **kwargs): + super(LCChooseMultiple, self).__init__(value, *args, **kwargs) + self.multiple = True + + def simple(self): + return list(self.value) + +TYPE_TO_LC = { + OrderedDict: LCStaticBox, + bool: LCBool, + str: LCText, + unicode: LCText, + int: LCText, + 'spin': LCSpin, + 'dropdown': list, + 'slider': int, + 'colour_picker': LCColour, + 'list': list, + 'button': LCButton +} + + +def alter_data_to_lc_style(data, gui): + new_data = LCStaticBox() + for item, value in data.items(): + item_type = gui.get(item, {}).get('view', type(value)) + logging.info('item: %s, value: %s, type: %s', item, value, item_type) + if item_type not in TYPE_TO_LC: + new_data[item] = value + else: + try: + new_data[item] = TYPE_TO_LC[item_type](value, **gui.get(item, {})) + except: + new_data[item] = value + return new_data diff --git a/modules/messaging/blacklist.py b/modules/messaging/blacklist.py index 0f1a869..8395325 100644 --- a/modules/messaging/blacklist.py +++ b/modules/messaging/blacklist.py @@ -2,24 +2,25 @@ # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov import re -from collections import OrderedDict import logging from modules.helper.message import ignore_system_messages, process_text_messages from modules.helper.module import MessagingModule +from modules.interface.types import LCStaticBox, LCText, LCGridSingle, LCPanel DEFAULT_PRIORITY = 30 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = { 'category': 'messaging', 'id': DEFAULT_PRIORITY} -CONF_DICT['main'] = {'message': 'ignored message'} -CONF_DICT['users_hide'] = [] -CONF_DICT['users_block'] = [] -CONF_DICT['words_hide'] = [] -CONF_DICT['words_block'] = [] +CONF_DICT['main'] = LCStaticBox() +CONF_DICT['main']['message'] = LCText('ignored message') +CONF_DICT['users_hide'] = LCGridSingle() +CONF_DICT['users_block'] = LCGridSingle() +CONF_DICT['words_hide'] = LCGridSingle() +CONF_DICT['words_block'] = LCGridSingle() log = logging.getLogger('blacklist') diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index 4f8717b..5297b7c 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -4,19 +4,19 @@ import logging import random import re -from collections import OrderedDict from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule +from modules.interface.types import LCGridDual, LCPanel DEFAULT_PRIORITY = 10 log = logging.getLogger('c2b') -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = { 'category': 'messaging', 'id': DEFAULT_PRIORITY} -CONF_DICT['config'] = {} +CONF_DICT['config'] = LCGridDual() CONF_GUI = { 'config': { diff --git a/modules/messaging/df.py b/modules/messaging/df.py index ce1875f..d5b56f3 100644 --- a/modules/messaging/df.py +++ b/modules/messaging/df.py @@ -7,13 +7,14 @@ from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule +from modules.interface.types import LCStaticBox, LCText, LCGridDual, LCPanel -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = {'category': 'messaging'} -CONF_DICT['grep'] = OrderedDict() -CONF_DICT['grep']['symbol'] = '#' -CONF_DICT['grep']['file'] = 'logs/df.txt' -CONF_DICT['prof'] = OrderedDict() +CONF_DICT['grep'] = LCStaticBox() +CONF_DICT['grep']['symbol'] = LCText('#') +CONF_DICT['grep']['file'] = LCText('logs/df.txt') +CONF_DICT['prof'] = LCGridDual() CONF_GUI = { 'prof': { diff --git a/modules/messaging/levels.py b/modules/messaging/levels.py index 606c116..d97cd6d 100644 --- a/modules/messaging/levels.py +++ b/modules/messaging/levels.py @@ -1,31 +1,30 @@ # This Python file uses the following encoding: utf-8 # -*- coding: utf-8 -*- # Copyright (C) 2016 CzT/Vladislav Ivanov -import logging import math import os import random import sqlite3 import xml.etree.ElementTree as ElementTree -from collections import OrderedDict import datetime from modules.helper.message import process_text_messages, SystemMessage, ignore_system_messages from modules.helper.parser import save_settings from modules.helper.system import ModuleLoadException from modules.helper.module import MessagingModule +from modules.interface.types import * log = logging.getLogger('levels') -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = {'category': 'messaging'} -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['message'] = u'{0} has leveled up, now he is {1}' -CONF_DICT['config']['db'] = os.path.join('conf', u'levels.db') -CONF_DICT['config']['experience'] = u'geometrical' -CONF_DICT['config']['exp_for_level'] = 200 -CONF_DICT['config']['exp_for_message'] = 1 -CONF_DICT['config']['decrease_window'] = 60 +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['message'] = LCText(u'{0} has leveled up, now he is {1}') +CONF_DICT['config']['db'] = LCText(os.path.join('conf', u'levels.db')) +CONF_DICT['config']['experience'] = LCDropdown('geometrical', ['geometrical', 'static', 'random']) +CONF_DICT['config']['exp_for_level'] = LCText(200) +CONF_DICT['config']['exp_for_message'] = LCText(1) +CONF_DICT['config']['decrease_window'] = LCText(1) CONF_GUI = { diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index 0ccecfd..81b4858 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -3,24 +3,24 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import os import datetime -from collections import OrderedDict from modules.helper.message import process_text_messages from modules.helper.module import MessagingModule from modules.helper.system import CONF_FOLDER +from modules.interface.types import * DEFAULT_PRIORITY = 20 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = { 'category': 'messaging', 'id': DEFAULT_PRIORITY } -CONF_DICT['config'] = OrderedDict() -CONF_DICT['config']['logging'] = True -CONF_DICT['config']['file_format'] = '%Y-%m-%d' -CONF_DICT['config']['message_date_format'] = '%Y-%m-%d %H:%M:%S' -CONF_DICT['config']['rotation'] = 'daily' +CONF_DICT['config'] = LCStaticBox() +CONF_DICT['config']['logging'] = LCBool(True) +CONF_DICT['config']['file_format'] = LCText('%Y-%m-%d') +CONF_DICT['config']['message_date_format'] = LCText('%Y-%m-%d %H:%M:%S') +CONF_DICT['config']['rotation'] = LCText('daily') CONF_GUI = {'non_dynamic': ['config.*']} diff --git a/modules/messaging/mentions.py b/modules/messaging/mentions.py index 2aaf21d..2168527 100644 --- a/modules/messaging/mentions.py +++ b/modules/messaging/mentions.py @@ -6,11 +6,12 @@ from modules.helper.message import process_text_messages, ignore_system_messages from modules.helper.module import MessagingModule +from modules.interface.types import LCGridSingle, LCPanel -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = {'category': 'messaging'} -CONF_DICT['mentions'] = [] -CONF_DICT['address'] = [] +CONF_DICT['mentions'] = LCGridSingle() +CONF_DICT['address'] = LCGridSingle() CONF_GUI = { 'mentions': { diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 9cdff89..05e7130 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -7,7 +7,6 @@ import os import socket import threading -from collections import OrderedDict import cherrypy from cherrypy.lib.static import serve_file @@ -20,13 +19,15 @@ from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, CommandMessage, SystemMessage, RemoveMessageByID from modules.helper.module import MessagingModule -from modules.helper.parser import save_settings +from modules.helper.parser import save_settings, convert_to_dict from modules.helper.system import THREADS, PYTHON_FOLDER, CONF_FOLDER +from modules.interface.types import * logging.getLogger('ws4py').setLevel(logging.ERROR) DEFAULT_STYLE = 'default' +DEFAULT_GUI_STYLE = 'default_gui' DEFAULT_PRIORITY = 9001 -HISTORY_SIZE = 40 +HISTORY_SIZE = 50 HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") s_queue = Queue.Queue() log = logging.getLogger('webchat') @@ -34,19 +35,32 @@ WS_THREADS = THREADS + 3 -CONF_DICT = OrderedDict() +CONF_DICT = LCPanel() CONF_DICT['gui_information'] = { 'category': 'main', 'id': DEFAULT_PRIORITY } -CONF_DICT['server'] = OrderedDict() -CONF_DICT['server']['host'] = '127.0.0.1' -CONF_DICT['server']['port'] = '8080' -CONF_DICT['style_gui'] = DEFAULT_STYLE -CONF_DICT['style_gui_settings'] = OrderedDict() -CONF_DICT['style'] = DEFAULT_STYLE -CONF_DICT['style_settings'] = OrderedDict() -CONF_DICT['style_settings']['show_system_msg'] = True +CONF_DICT['server'] = LCStaticBox() +CONF_DICT['server']['host'] = LCText('127.0.0.1') +CONF_DICT['server']['port'] = LCText('8080') + +CONF_DICT['gui_chat'] = LCPanel() +CONF_DICT['gui_chat']['style'] = LCChooseSingle( + DEFAULT_GUI_STYLE, + check_type='dir', + folder='http', + empty_label=True) +CONF_DICT['gui_chat']['style_settings'] = LCStaticBox() +CONF_DICT['gui_chat']['style_settings']['show_system_msg'] = LCBool(True) + +CONF_DICT['server_chat'] = LCPanel() +CONF_DICT['server_chat']['style'] = LCChooseSingle( + DEFAULT_STYLE, + check_type='dir', + folder='http', + empty_label=True) +CONF_DICT['server_chat']['style_settings'] = LCStaticBox() +CONF_DICT['server_chat']['style_settings']['show_system_msg'] = LCBool(True) TYPE_DICT = { TextMessage: 'message', @@ -372,15 +386,15 @@ def style_css(self, *path): def style_scss(self, *path): css_namespace = Namespace() for key, value in self.settings['keys'].items(): - if isinstance(value, basestring): - if value.startswith('#'): - css_value = Color.from_hex(value) - else: - css_value = String(value) - elif isinstance(value, bool): - css_value = Boolean(value) - elif isinstance(value, int) or isinstance(value, float): - css_value = Number(value) + log.info('%s, %s', key, type(value)) + if isinstance(value, LCText): + css_value = String(value) + elif isinstance(value, LCColour): + css_value = Color.from_hex(value) + elif isinstance(value, LCBool): + css_value = Boolean(bool(value)) + elif isinstance(value, LCSpin) or isinstance(value, float): + css_value = Number(int(value)) else: raise ValueError("Unable to find comparable values") css_namespace.set_variable('${}'.format(key), css_value) @@ -524,7 +538,8 @@ def __init__(self, *args, **kwargs): 'location': None, 'keys': {} } - }}) + } + }) self.prepare_style_settings() self.style_settings = self._conf_params['style_settings'] @@ -560,7 +575,8 @@ def start_webserver(self): @staticmethod def get_style_path(style): - path = os.path.abspath(os.path.join(HTTP_FOLDER, style)) + path_file = style.value if isinstance(style, LCObject) else style + path = os.path.abspath(os.path.join(HTTP_FOLDER, path_file)) if os.path.exists(path): return path elif os.path.exists(os.path.join(HTTP_FOLDER, DEFAULT_STYLE)): @@ -581,8 +597,8 @@ def apply_settings(self, **kwargs): style_changed = False - chat_style = self._conf_params['config']['style'] - gui_style = self._conf_params['config']['style_gui'] + chat_style = self._conf_params['config']['server_chat']['style'] + gui_style = self._conf_params['config']['gui_chat']['style'] style_config = self._conf_params['style_settings'] self.update_style_settings(chat_style, gui_style) @@ -621,7 +637,8 @@ def process_message(self, message, **kwargs): return message def rest_get_style_settings(self, *args): - return json.dumps(self._conf_params['style_settings'][args[0][0]]['keys']) + return json.dumps( + convert_to_dict(self._conf_params['style_settings'][args[0][0]]['keys'])) def rest_get_history(self, *args, **kwargs): return json.dumps( @@ -648,7 +665,8 @@ def get_style_from_file(self, style_name): def write_style_to_file(self, style_name, style_type): file_path = os.path.join(self.get_style_path(style_name), 'settings.json') with open(file_path, 'w') as style_file: - json.dump(self._conf_params['style_settings'][style_type]['keys'], style_file, indent=2) + data = self._conf_params['style_settings'][style_type]['keys'] + json.dump(convert_to_dict(data, ordered=True), style_file, indent=2) def get_style_gui_from_file(self, style_name): file_path = os.path.join(self.get_style_path(style_name), 'settings_gui.json') @@ -658,68 +676,70 @@ def get_style_gui_from_file(self, style_name): return {} def load_style_settings(self, style_name, style_type=None, keys=None): + # Shortcut + params = self._conf_params + if keys: style_type = keys['all_settings']['type'] - settings_path = 'style_settings' if style_type == 'chat' else 'style_gui_settings' + web_type = 'gui' if style_type == 'gui_chat' else 'chat' - self._conf_params['config'][settings_path] = self.get_style_from_file(style_name) - self._conf_params['style_settings'][style_type]['keys'] = self._conf_params['config']['style_settings'] - self._conf_params['gui'][settings_path] = self.get_style_gui_from_file(style_name) - return self._conf_params['config'][settings_path] + lc_settings = alter_data_to_lc_style(self.get_style_from_file(style_name), + self.get_style_gui_from_file(style_name)) + + params['config'][style_type].update({'style_settings': lc_settings}) + params['style_settings'][web_type]['keys'] = params['config'][style_type]['style_settings'] + params['gui'][style_type].update( + {'style_settings': self.get_style_gui_from_file(style_name)}) + return params['config'][style_type]['style_settings'] def update_style_settings(self, chat_style, gui_style): - self._conf_params['style_settings']['chat']['keys'] = self._conf_params['config']['style_settings'] - self._conf_params['style_settings']['gui']['keys'] = self._conf_params['config']['style_gui_settings'] + params = self._conf_params['style_settings'] + params['chat']['keys'] = self._conf_params['config']['server_chat']['style_settings'] + params['gui']['keys'] = self._conf_params['config']['gui_chat']['style_settings'] self.write_style_to_file(chat_style, 'chat') self.write_style_to_file(gui_style, 'gui') def prepare_style_settings(self): - style_settings = self._conf_params['style_settings']['chat'] + server_style_settings = self._conf_params['style_settings']['chat'] gui_style_settings = self._conf_params['style_settings']['gui'] - config_style = self._conf_params['config']['style'] - gui_config_style = self._conf_params['config']['style_gui'] + server_style = self._conf_params['config']['server_chat']['style'] + gui_style = self._conf_params['config']['gui_chat']['style'] - style_settings['style_name'] = config_style - style_settings['location'] = self.get_style_path(config_style) - style_settings['keys'] = self.load_style_settings(config_style, 'chat') + server_style_settings['style_name'] = server_style + server_style_settings['location'] = self.get_style_path(server_style) + server_style_settings['keys'] = self.load_style_settings(server_style, 'server_chat') - gui_style_settings['style_name'] = gui_config_style - gui_style_settings['location'] = self.get_style_path(gui_config_style) - gui_style_settings['keys'] = self.load_style_settings(gui_config_style, 'gui') + gui_style_settings['style_name'] = gui_style + gui_style_settings['location'] = self.get_style_path(gui_style) + gui_style_settings['keys'] = self.load_style_settings(gui_style, 'gui_chat') def _conf_settings(self, *args, **kwargs): return CONF_DICT def _gui_settings(self): return { - 'style_gui': { - 'check': 'http', - 'check_type': 'dir', - 'view': 'choose_single' + 'server_chat': { + 'redraw': { + 'style_settings': { + 'redraw_trigger': ['style'], + 'type': 'server_chat', + 'get_config': self.load_style_settings, + 'get_gui': self.get_style_gui_from_file + }, + } }, - 'style_gui_settings': {}, - 'style': { - 'check': 'http', - 'check_type': 'dir', - 'view': 'choose_single' + 'gui_chat': { + 'redraw': { + 'style_settings': { + 'redraw_trigger': ['style'], + 'type': 'gui_chat', + 'get_config': self.load_style_settings, + 'get_gui': self.get_style_gui_from_file + }, + } }, - 'style_settings': {}, 'non_dynamic': ['server.*'], - 'ignored_sections': ['style_settings', 'style_gui_settings'], - 'redraw': { - 'style_settings': { - 'redraw_trigger': ['style'], - 'type': 'chat', - 'get_config': self.load_style_settings, - 'get_gui': self.get_style_gui_from_file - }, - 'style_gui_settings': { - 'redraw_trigger': ['style_gui'], - 'type': 'gui', - 'get_config': self.load_style_settings, - 'get_gui': self.get_style_gui_from_file - }, - } - } + 'ignored_sections': ['gui_chat.style_settings', 'server_chat.style_settings'], + } diff --git a/translations/en/webchat.key b/translations/en/webchat.key index bf814d3..bfaa93a 100644 --- a/translations/en/webchat.key +++ b/translations/en/webchat.key @@ -11,13 +11,12 @@ webchat.description = WebChat module is a webserver for chat and allows you to s *.badge_size = Badge Size (px) *.background_colour = Background Colour *.background_colour.button = Select Colour +*.style_settings = Style Settings +*.style.list_box = Style for WebChat webchat.server = Local server settings webchat.server.host = Host webchat.server.port = Port -webchat.style = Style for WebChat -webchat.style.list_box = -webchat.style_settings = Style Settings -webchat.style_gui = Style for GUI -webchat.style_gui.list_box = -webchat.style_gui_settings = Style Settings + +*.gui_chat = GUI Style Settings +*.server_chat = Server Style Settings From 055d9c71648638186baca19da0b6db534000d735 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Wed, 14 Jun 2017 18:50:44 +0300 Subject: [PATCH 18/43] LC-396 Remove display name from twitch --- modules/chat/twitch.py | 3 +-- modules/gui.py | 2 +- modules/helper/message.py | 4 ++++ src/jenkins/chat_tests/hitbox.sh | 8 -------- src/jenkins/run_chat.sh | 6 +++--- 5 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 src/jenkins/chat_tests/hitbox.sh diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 7160eb2..5993b02 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -143,8 +143,7 @@ def _handle_badges(self, message, badges): @staticmethod def _handle_display_name(message, name): - message.display_name = name if name else message.user - message.jsonable += ['display_name'] + message.user = name if name else message.user @staticmethod def _handle_emotes(message, tag_value): diff --git a/modules/gui.py b/modules/gui.py index b67fe3e..6ff62c1 100644 --- a/modules/gui.py +++ b/modules/gui.py @@ -274,7 +274,7 @@ def clear_changes(remote_change=None): else: clear_changes() elif item_type == 'gridbox': - if compare_2d_lists(value, config[split_keys[-1]].simple()): + if compare_2d_lists(value, config[ch_item][split_keys[-1]].simple()): clear_changes() else: apply_changes() diff --git a/modules/helper/message.py b/modules/helper/message.py index 9a6638c..4b73445 100644 --- a/modules/helper/message.py +++ b/modules/helper/message.py @@ -159,6 +159,10 @@ def source_icon(self): def user(self): return self._user + @user.setter + def user(self, value): + self._user = value + @property def text(self): """ diff --git a/src/jenkins/chat_tests/hitbox.sh b/src/jenkins/chat_tests/hitbox.sh deleted file mode 100644 index 0e0b606..0000000 --- a/src/jenkins/chat_tests/hitbox.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -PORT=8080 - -curl -s -X POST -H 'Content-Type: application/json' -d '{"nickname":"HitboxTest","text":"hbTestMessage"}' http://localhost:${PORT}/rest/hitbox/push_message - -sleep 1 - -curl -s http://localhost:${PORT}/rest/webchat/history | grep hbTestMessage diff --git a/src/jenkins/run_chat.sh b/src/jenkins/run_chat.sh index 54d5dc3..ecd5cf3 100644 --- a/src/jenkins/run_chat.sh +++ b/src/jenkins/run_chat.sh @@ -25,9 +25,9 @@ while [ ${ATTEMPTS} -lt 20 ]; do continue fi - if ! grep "Hitbox Testing mode online" chat.log; then - continue - fi +# if ! grep "Hitbox Testing mode online" chat.log; then +# continue +# fi sleep 5 exit 0 From 59bc3b59e8ac4845d34729eb769f316df8a8b177 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Wed, 14 Jun 2017 19:35:58 +0300 Subject: [PATCH 19/43] LC-395 Refactor platform format --- modules/chat/beampro.py | 4 ++-- modules/chat/goodgame.py | 4 ++-- modules/chat/hitbox.py | 4 ++-- modules/chat/sc2tv.py | 4 ++-- modules/chat/twitch.py | 4 ++-- modules/helper/message.py | 43 ++++++++++++++++++++++-------------- modules/interface/types.py | 2 +- modules/messaging/logger.py | 2 +- modules/messaging/webchat.py | 9 ++++++-- 9 files changed, 45 insertions(+), 31 deletions(-) diff --git a/modules/chat/beampro.py b/modules/chat/beampro.py index d95dee5..3062ee5 100644 --- a/modules/chat/beampro.py +++ b/modules/chat/beampro.py @@ -49,13 +49,13 @@ class BeamProAPIException(Exception): class BeamProTextMessage(TextMessage): def __init__(self, user, text, mid): - TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + TextMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=user, text=text, mid=mid) class BeamProSystemMessage(SystemMessage): def __init__(self, text, category='system'): - SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + SystemMessage.__init__(self, text, platform_id=SOURCE, icon=SOURCE_ICON, user=SYSTEM_USER, category=category) diff --git a/modules/chat/goodgame.py b/modules/chat/goodgame.py index 7256747..15f52a0 100644 --- a/modules/chat/goodgame.py +++ b/modules/chat/goodgame.py @@ -53,7 +53,7 @@ class GoodgameTextMessage(TextMessage): def __init__(self, text, user, mid=None): - TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + TextMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=user, text=text, mid=mid) def process_smiles(self, smiles, rights, premium, prems, payments): @@ -97,7 +97,7 @@ def process_smiles(self, smiles, rights, premium, prems, payments): class GoodgameSystemMessage(SystemMessage): def __init__(self, text, category='system'): - SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + SystemMessage.__init__(self, text, platform_id=SOURCE, icon=SOURCE_ICON, user=SYSTEM_USER, category=category) diff --git a/modules/chat/hitbox.py b/modules/chat/hitbox.py index bde6dcb..e028983 100644 --- a/modules/chat/hitbox.py +++ b/modules/chat/hitbox.py @@ -53,13 +53,13 @@ class HitboxAPIError(Exception): class HitboxTextMessage(TextMessage): def __init__(self, user, text, mid, nick_colour): - TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + TextMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=user, text=text, mid=mid, nick_colour=nick_colour) class HitboxSystemMessage(SystemMessage): def __init__(self, text, category='system'): - SystemMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + SystemMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=SYSTEM_USER, text=text, category=category) diff --git a/modules/chat/sc2tv.py b/modules/chat/sc2tv.py index aa173a3..df64925 100644 --- a/modules/chat/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -80,7 +80,7 @@ def __init__(self, user, text, subscr): self._text = text self._subscriptions = subscr - TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + TextMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=self.user, text=self.text) def process_smiles(self, smiles): @@ -104,7 +104,7 @@ class FsSystemMessage(SystemMessage): def __init__(self, text, emotes=None, category='system'): if emotes is None: emotes = [] - SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + SystemMessage.__init__(self, text, platform_id=SOURCE, icon=SOURCE_ICON, user=SYSTEM_USER, emotes=emotes, category=category) diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index 5993b02..c215661 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -66,13 +66,13 @@ class TwitchNormalDisconnect(Exception): class TwitchTextMessage(TextMessage): def __init__(self, user, text, me): self.bttv_emotes = {} - TextMessage.__init__(self, source=SOURCE, source_icon=SOURCE_ICON, + TextMessage.__init__(self, platform_id=SOURCE, icon=SOURCE_ICON, user=user, text=text, me=me) class TwitchSystemMessage(SystemMessage): def __init__(self, text, category='system', emotes=None): - SystemMessage.__init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, + SystemMessage.__init__(self, text, platform_id=SOURCE, icon=SOURCE_ICON, user=SYSTEM_USER, emotes=emotes, category=category) diff --git a/modules/helper/message.py b/modules/helper/message.py index 4b73445..7d8a828 100644 --- a/modules/helper/message.py +++ b/modules/helper/message.py @@ -113,7 +113,7 @@ def message_ids(self): class TextMessage(Message): - def __init__(self, source, source_icon, user, text, + def __init__(self, platform_id, icon, user, text, emotes=None, badges=None, pm=False, nick_colour=None, mid=None, me=False): """ @@ -122,8 +122,8 @@ def __init__(self, source, source_icon, user, text, :param nick_colour: Nick colour :param mid: Message ID :param me: /me notation - :param source: Chat source (gg/twitch/beampro etc.) - :param source_icon: Chat icon (as url) + :param platform_id: Chat source (gg/twitch/beampro etc.) + :param icon: Chat icon (as url) :param user: nickname :param text: message text :param emotes: @@ -131,8 +131,7 @@ def __init__(self, source, source_icon, user, text, """ Message.__init__(self) - self._source = source - self._source_icon = source_icon + self._platform = Platform(platform_id, icon) self._user = user self._text = text self._emotes = [] if emotes is None else emotes @@ -144,16 +143,12 @@ def __init__(self, source, source_icon, user, text, self._id = str(mid) if mid else str(uuid.uuid1()) self._jsonable += ['user', 'text', 'emotes', 'badges', - 'id', 'source', 'source_icon', 'pm', - 'nick_colour', 'channel_name', 'me'] + 'id', 'platform', 'pm', 'nick_colour', + 'channel_name', 'me'] @property - def source(self): - return self._source - - @property - def source_icon(self): - return self._source_icon + def platform(self): + return self._platform @property def user(self): @@ -228,19 +223,19 @@ def me(self, value): class SystemMessage(TextMessage): - def __init__(self, text, source=SOURCE, source_icon=SOURCE_ICON, user=SOURCE_USER, emotes=None, category='system'): + def __init__(self, text, platform_id=SOURCE, icon=SOURCE_ICON, user=SOURCE_USER, emotes=None, category='system'): """ Text message used by main chat logic Serves system messages from modules - :param source: TextMessage.source - :param source_icon: TextMessage.source_icon + :param platform_id: TextMessage.source + :param icon: TextMessage.source_icon :param user: TextMessage.user :param text: TextMessage.text :param category: System message category, can be filtered """ if emotes is None: emotes = [] - TextMessage.__init__(self, source, source_icon, user, text, emotes) + TextMessage.__init__(self, platform_id, icon, user, text, emotes) self._category = category @property @@ -269,3 +264,17 @@ def url(self): class Badge(Emote): def __init__(self, badge_id, badge_url): Emote.__init__(self, badge_id, badge_url) + + +class Platform(object): + def __init__(self, platform_id, icon): + self._id = platform_id + self._icon = icon + + @property + def id(self): + return self._id + + @property + def icon(self): + return self._icon diff --git a/modules/interface/types.py b/modules/interface/types.py index 6a22c92..be3a695 100644 --- a/modules/interface/types.py +++ b/modules/interface/types.py @@ -1,6 +1,7 @@ from collections import OrderedDict import logging +log = logging.getLogger('interface/types') class LCObject(object): @@ -187,7 +188,6 @@ def alter_data_to_lc_style(data, gui): new_data = LCStaticBox() for item, value in data.items(): item_type = gui.get(item, {}).get('view', type(value)) - logging.info('item: %s, value: %s, type: %s', item, value, item_type) if item_type not in TYPE_TO_LC: new_data[item] = value else: diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index 81b4858..2fa2535 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -51,7 +51,7 @@ def process_message(self, message, **kwargs): with open('{0}.txt'.format( os.path.join(self.destination, datetime.datetime.now().strftime(self.format))), 'a') as f: f.write('[{3}] [{0}] {1}: {2}\n'.format( - message.source.encode('utf-8'), + message.platform.id.encode('utf-8'), message.user.encode('utf-8'), message.text.encode('utf-8'), datetime.datetime.now().strftime(self.ts_format).encode('utf-8'))) diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 05e7130..32e7c83 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -3,7 +3,6 @@ import copy import datetime import json -import logging import os import socket import threading @@ -76,6 +75,10 @@ def process_badges(badges): return [{'badge': badge.id, 'url': badge.url} for badge in badges] +def process_platform(platform): + return {'id': platform.id, 'icon': platform.icon} + + def prepare_message(msg, style_settings, msg_class): message = copy.deepcopy(msg) @@ -96,6 +99,9 @@ def prepare_message(msg, style_settings, msg_class): if 'badges' in message: message['badges'] = process_badges(message['badges']) + if 'platform' in message: + message['platform'] = process_platform(message['platform']) + if 'command' in message: if message['command'].startswith('replace'): message['text'] = style_settings['keys']['remove_text'] @@ -386,7 +392,6 @@ def style_css(self, *path): def style_scss(self, *path): css_namespace = Namespace() for key, value in self.settings['keys'].items(): - log.info('%s, %s', key, type(value)) if isinstance(value, LCText): css_value = String(value) elif isinstance(value, LCColour): From 4fe0bcdf02f3b44b541bad9547cba2d9129ef0aa Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Tue, 27 Jun 2017 20:12:48 +0300 Subject: [PATCH 20/43] LC-337 OAuth for Twitch.TV --- modules/chat/twitch.py | 67 ++++++++++++++++++++++++++++++++-- modules/gui.py | 5 +++ modules/helper/system.py | 16 ++++++++ modules/interface/frames.py | 23 ++++++++++++ modules/interface/functions.py | 3 ++ modules/messaging/webchat.py | 7 +++- 6 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 modules/interface/frames.py diff --git a/modules/chat/twitch.py b/modules/chat/twitch.py index c215661..3e54bb1 100644 --- a/modules/chat/twitch.py +++ b/modules/chat/twitch.py @@ -6,7 +6,6 @@ import re import threading import time -from collections import OrderedDict import irc.client import requests @@ -14,8 +13,8 @@ from modules.gui import MODULE_KEY from modules.helper.message import TextMessage, SystemMessage, Badge, Emote, RemoveMessageByUser from modules.helper.module import ChatModule -from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE -from modules.interface.types import LCStaticBox, LCPanel, LCText, LCBool +from modules.helper.system import translate_key, EMOTE_FORMAT, NA_MESSAGE, register_iodc +from modules.interface.types import LCStaticBox, LCPanel, LCText, LCBool, LCButton logging.getLogger('irc').setLevel(logging.ERROR) logging.getLogger('requests').setLevel(logging.ERROR) @@ -31,6 +30,7 @@ FILE_ICON = os.path.join('img', 'tw.png') SYSTEM_USER = 'Twitch.TV' BITS_REGEXP = r'(^|\s)(\w+){}(\s|$)' +API_URL = 'https://api.twitch.tv/kraken/{}' PING_DELAY = 10 @@ -43,6 +43,8 @@ CONF_DICT['config']['bttv'] = LCBool(True) CONF_DICT['config']['show_channel_names'] = LCBool(True) CONF_DICT['config']['show_nickname_colors'] = LCBool(False) +CONF_DICT['config']['register_oidc'] = LCButton(register_iodc) + CONF_GUI = { 'config': { 'hidden': ['host', 'port'], @@ -51,7 +53,8 @@ 'addable': 'true' } }, - 'non_dynamic': ['config.host', 'config.port', 'config.bttv'] + 'non_dynamic': ['config.host', 'config.port', 'config.bttv'], + 'ignored_sections': ['config.register_oidc'], } @@ -63,6 +66,10 @@ class TwitchNormalDisconnect(Exception): """Normal Disconnect exception""" +class TwitchAPIError(Exception): + """Exception of API""" + + class TwitchTextMessage(TextMessage): def __init__(self, user, text, me): self.bttv_emotes = {} @@ -602,6 +609,12 @@ def __init__(self, *args, **kwargs): self.host = CONF_DICT['config']['host'] self.port = int(CONF_DICT['config']['port']) self.bttv = CONF_DICT['config']['bttv'] + self.access_code = self._conf_params['config'].get('access_code') + if self.access_code: + headers['Authorization'] = 'OAuth {}'.format(self.access_code) + + self.rest_add('GET', 'oidc', self.parse_oidc_request) + self.rest_add('POST', 'oidc', self.oidc_code) def _conf_settings(self, *args, **kwargs): return CONF_DICT @@ -643,3 +656,49 @@ def apply_settings(self, **kwargs): if 'webchat' in kwargs.get('from_depend', []): self._conf_params['settings']['remove_text'] = self.get_remove_text() ChatModule.apply_settings(self, **kwargs) + + def parse_oidc_request(self, req): + return '' + + def oidc_code(self, req, **kwargs): + def remove_hash(item): + return item if '#' not in item else item[1:] + items_list = map(remove_hash, kwargs.get('request').split('&')) + item_dict = {} + for item in items_list: + item_dict[item.split('=')[0]] = item.split('=')[1] + + if not self.access_code: + self.access_code = item_dict['access_token'] + self._conf_params['config']['access_code'] = self.access_code + + headers['Authorization'] = 'OAuth {}'.format(self.access_code) + if self.channels: + self.api_call('channels/{}/editors'.format(self.channels.items()[0][0])) + return 'Access Code saved' + + def api_call(self, key): + req = requests.get(API_URL.format(key), headers=headers) + if req.ok: + return req.json() + raise TwitchAPIError('Unable to get {}'.format(key)) + + def register_iodc(self, parent_window): + port = self._loaded_modules['webchat']['port'] + + url = 'https://api.twitch.tv/kraken/oauth2/authorize?client_id={}' \ + '&redirect_uri={}' \ + '&response_type={}' \ + '&scope={}'.format(headers['Client-ID'], 'http://localhost:{}/{}'.format(port, 'rest/twitch/oidc'), + 'token', 'channel_editor channel_read') + request = requests.get(url) + + if request.ok: + parent_window.create_browser(request.url) + pass diff --git a/modules/gui.py b/modules/gui.py index 6ff62c1..add7a6e 100644 --- a/modules/gui.py +++ b/modules/gui.py @@ -5,6 +5,7 @@ from modules.interface.controls import KeyListBox, MainMenuToolBar import modules.interface.functions +from modules.interface.frames import OAuthBrowser from modules.interface.types import * try: @@ -991,6 +992,10 @@ def on_toolbar_button(self, event): settings['class'].gui_button_press(self, event, list_keys) event.Skip() + def create_browser(self, url): + browser_window = OAuthBrowser(self, url) + pass + class GuiThread(threading.Thread, BaseModule): title = 'LalkaChat' diff --git a/modules/helper/system.py b/modules/helper/system.py index 7535e16..cfdfda3 100644 --- a/modules/helper/system.py +++ b/modules/helper/system.py @@ -167,3 +167,19 @@ def get_language(): def get_key(*args): return MODULE_KEY.join(args) + + +def register_iodc(event): + parent = get_wx_parent(event.GetEventObject()).Parent + twitch = parent.loaded_modules.get('twitch')['class'] + if not twitch: + raise ValueError('Unable to find loaded Twitch.TV Module') + + twitch.register_iodc(parent) + pass + + +def get_wx_parent(item): + if item.GrandParent: + return get_wx_parent(item.GrandParent) + return item diff --git a/modules/interface/frames.py b/modules/interface/frames.py new file mode 100644 index 0000000..fb206fb --- /dev/null +++ b/modules/interface/frames.py @@ -0,0 +1,23 @@ +import wx +try: + from cefpython3.wx import chromectrl as browser + HAS_CHROME = True +except ImportError: + from wx import html2 as browser + + +class OAuthBrowser(wx.Frame): + def __init__(self, parent, url): + wx.Frame.__init__(self, parent) + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + + if HAS_CHROME: + self.browser = browser.ChromeWindow(self, url) + else: + self.browser = browser.WebView.New(parent=self, url=url, name='LalkaWebViewGui') + + self.sizer.Add(self.browser, 1, wx.EXPAND) + + self.SetSizer(self.sizer) + self.Show() + self.SetFocus() diff --git a/modules/interface/functions.py b/modules/interface/functions.py index 483a290..d5c6ad0 100644 --- a/modules/interface/functions.py +++ b/modules/interface/functions.py @@ -8,6 +8,7 @@ from modules.interface.types import LCPanel + def create_textctrl(panel=None, value=None, key=None, bind=None, **kwargs): item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_name = MODULE_KEY.join(key) @@ -36,6 +37,8 @@ def create_button(source_class=None, panel=None, key=None, value=None, if value: # TODO: Implement button function pressing + if callable(value.value): + c_button.Bind(wx.EVT_BUTTON, value.value, id=button_id) c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) else: c_button.Bind(wx.EVT_BUTTON, bind, id=button_id) diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index 32e7c83..ba8fbc3 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -345,8 +345,11 @@ def default(self, *args, **kwargs): body = cherrypy.request.body if cherrypy.request.method in cherrypy.request.methods_with_bodies: - data = json.load(body) - kwargs.update(data) + try: + data = json.load(body) + kwargs.update(data) + except: + pass if len(args) > 1: module_name = args[0] From c7a1bbcd1d64f3f2bb7f6eef48261440c718ee74 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 9 Jul 2017 15:38:17 +0300 Subject: [PATCH 21/43] LC-260 Fix Counters to be "thread-safe" --- modules/gui.py | 28 +++++++++++++++++++++++----- modules/interface/events.py | 3 +++ modules/messaging/webchat.py | 4 ++-- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 modules/interface/events.py diff --git a/modules/gui.py b/modules/gui.py index add7a6e..57fcacc 100644 --- a/modules/gui.py +++ b/modules/gui.py @@ -1,10 +1,13 @@ # Copyright (C) 2016 CzT/Vladislav Ivanov import collections +import sys + from modules.helper.functions import find_by_type, parse_keys_to_string, deep_get from modules.interface.controls import KeyListBox, MainMenuToolBar import modules.interface.functions +from modules.interface.events import StatusChangeEvent, EVT_STATUS_CHANGE from modules.interface.frames import OAuthBrowser from modules.interface.types import * @@ -35,6 +38,8 @@ ITEM_SPACING_VERT = 6 ITEM_SPACING_HORZ = 30 +WINDOWS = True if sys.platform == 'win32' else False + def check_duplicate(item, window): items = window.GetItems() @@ -177,7 +182,8 @@ def __init__(self, *args, **kwargs): self.show_icons = self.main_class.main_config['config']['gui']['show_icons'] # Setting up the window - self.SetBackgroundColour('cream') + if WINDOWS: + self.SetBackgroundColour('cream') self.show_hidden = self.main_class.gui_settings.get('show_hidden') # Setting up events @@ -204,7 +210,7 @@ def on_listbox_change(self, event): description = translate_key(MODULE_KEY.join([selection, 'description'])) item_key = modules.interface.controls.IDS[event.GetId()].split(MODULE_KEY) - show_description = self.main_class.loaded_modules[item_key[0]]['gui'][item_key[1]].get('description', False) + show_description = self.main_class.loaded_modules[item_key[0]]['config'][item_key[1]].description if isinstance(item_object, KeyListBox): self.on_change(modules.interface.controls.IDS[event.GetId()], selection, item_type='listbox', section=True) @@ -732,7 +738,10 @@ class StatusFrame(wx.Panel): def __init__(self, parent, **kwargs): self.chat_modules = kwargs.get('chat_modules') wx.Panel.__init__(self, parent, size=wx.Size(-1, 24)) - self.SetBackgroundColour('cream') + self.parent = parent + + if WINDOWS: + self.SetBackgroundColour('cream') self.chats = {} self.border_sizer = self._create_sizer() @@ -852,8 +861,10 @@ def set_viewers(self, module_name, channel, viewers): viewers = '{0}k'.format(viewers[:-3]) if module_name in self.chats: if channel.lower() in self.chats[module_name]: - self.chats[module_name][channel.lower()]['label'].SetLabel(str(viewers)) - self.Layout() + wx.PostEvent(self.parent, StatusChangeEvent(data={ + 'label': self.chats[module_name][channel.lower()]['label'], + 'value': str(viewers) + })) class ChatGui(wx.Frame): @@ -915,6 +926,7 @@ def __init__(self, parent, title, url, **kwargs): # Set events self.Bind(wx.EVT_CLOSE, self.on_close) + self.Bind(EVT_STATUS_CHANGE, self.process_status_change) # Show window after creation self.SetSizer(vbox) @@ -996,6 +1008,12 @@ def create_browser(self, url): browser_window = OAuthBrowser(self, url) pass + def process_status_change(self, event): + data = event.data + data['label'].SetLabel(data['value']) + self.Layout() + pass + class GuiThread(threading.Thread, BaseModule): title = 'LalkaChat' diff --git a/modules/interface/events.py b/modules/interface/events.py new file mode 100644 index 0000000..c76d511 --- /dev/null +++ b/modules/interface/events.py @@ -0,0 +1,3 @@ +import wx.lib.newevent + +StatusChangeEvent, EVT_STATUS_CHANGE = wx.lib.newevent.NewEvent() diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index ba8fbc3..fb09432 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -532,8 +532,8 @@ def __init__(self, *args, **kwargs): conf_params = self._conf_params['config'] self._conf_params.update({ - 'host': conf_params['server']['host'], - 'port': conf_params['server']['port'], + 'host': str(conf_params['server']['host']), + 'port': str(conf_params['server']['port']), 'style_settings': { 'gui': { From b8f06defdded7c54123af54d0cd778ea8a9ee2bc Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Sun, 9 Jul 2017 15:54:24 +0300 Subject: [PATCH 22/43] LC-405 Adapt html to new message format --- src/themes/default/assets/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/default/assets/index.html b/src/themes/default/assets/index.html index 57fe650..66d46af 100644 --- a/src/themes/default/assets/index.html +++ b/src/themes/default/assets/index.html @@ -16,7 +16,7 @@
- +