diff --git a/.gitignore b/.gitignore index f774a56..b230d97 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ levels.db* /conf *.spec *.log +govno_rutony.py diff --git a/gui.py b/gui.py index c3fea83..661c407 100644 --- a/gui.py +++ b/gui.py @@ -1,11 +1,14 @@ import threading +from collections import OrderedDict + +import webbrowser import wx import wx.grid import os import logging from ConfigParser import ConfigParser from cefpython3.wx import chromectrl -from modules.helpers.system import MODULE_KEY, translate_key +from modules.helper.system import MODULE_KEY, translate_key # ToDO: Support customization of borders/spacings # ToDO: Exit by cancel button @@ -15,11 +18,13 @@ SECTION_GUI_TAG = '__gui' SKIP_TAGS = [INFORMATION_TAG] SKIP_TXT_CONTROLS = ['list_input', 'list_input2'] -SKIP_BUTTONS = ['list_add', 'list_remove'] +SKIP_BUTTONS = ['list_add', 'list_remove', 'apply_button', 'cancel_button'] +ITEM_SPACING_VERT = 6 +ITEM_SPACING_HORZ = 30 def get_id_from_name(name, error=False): - for item, item_id in IDS.items(): + for item, item_id in IDS.iteritems(): if item_id == name: return item if error: @@ -59,19 +64,17 @@ def check_duplicate(item, window): def create_categories(loaded_modules): - cat_dict = {} + cat_dict = OrderedDict() for module_name, module_config in loaded_modules.items(): - parser = module_config['parser'] # type: ConfigParser - if parser.has_section(INFORMATION_TAG) and parser.has_option(INFORMATION_TAG, 'category'): - tag = parser.get(INFORMATION_TAG, 'category') - item_dict = {module_name: module_config} - for key, value in parser.items(INFORMATION_TAG): - if key == 'hidden': - item_dict[module_name][key] = [h_item.strip() for h_item in value.split(',')] - if tag in cat_dict: - cat_dict[tag].append(item_dict) - else: - cat_dict[tag] = [item_dict] + if 'config' not in module_config: + continue + + config = module_config.get('config') + if INFORMATION_TAG in config: + tag = config[INFORMATION_TAG].get('category', 'undefined') + if tag not in cat_dict: + cat_dict[tag] = OrderedDict() + cat_dict[tag][module_name] = module_config return cat_dict @@ -80,13 +83,31 @@ def __init__(self, *args, **kwargs): self.keys = kwargs.pop('keys', []) wx.ListBox.__init__(self, *args, **kwargs) - def get_key_from_id(self, index): + 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'] + self.main_class = kwargs['main_class'] # type: ChatGui kwargs.pop('main_class') kwargs["style"] = wx.TB_NOICONS | wx.TB_TEXT @@ -95,7 +116,7 @@ def __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_about) + self.create_tool('menu.reload', self.main_class.on_toolbar_button) self.Realize() @@ -112,17 +133,20 @@ def create_tool(self, name, binding=None, style=wx.ITEM_NORMAL, s_help="", l_hel class SettingsWindow(wx.Frame): main_grid = None - notebook = None page_list = [] selected_cell = None def __init__(self, *args, **kwargs): self.spacer_size = (0, 10) self.main_class = kwargs.pop('main_class') # type: ChatGui + self.categories = kwargs.pop('categories') # type: dict wx.Frame.__init__(self, *args, **kwargs) self.settings_saved = True + self.tree_ctrl = None + self.content_page = None + self.sizer_dict = {} # Setting up the window self.SetBackgroundColour('cream') @@ -137,15 +161,16 @@ def __init__(self, *args, **kwargs): self.SetWindowStyle(styles) self.create_layout() + self.Show(True) def on_exit(self, event): log.debug(event) self.Destroy() def on_close(self, event): - dialog = wx.MessageDialog(self, message="Are you sure you want to quit?", + dialog = wx.MessageDialog(self, message=translate_key(MODULE_KEY.join(['main', 'quit'])), caption="Caption", - style=wx.YES_NO, + style=wx.YES_NO | wx.CANCEL, pos=wx.DefaultPosition) response = dialog.ShowModal() @@ -156,8 +181,7 @@ def on_close(self, event): def on_close_save(self, event): if not self.settings_saved: - dialog = wx.MessageDialog(self, message="Are you sure you want to quit?\n" - "Warning, your settings will not be saved.", + dialog = wx.MessageDialog(self, message=translate_key(MODULE_KEY.join(['main', 'quit', 'nosave'])), caption="Caption", style=wx.YES_NO, pos=wx.DefaultPosition) @@ -170,36 +194,56 @@ def on_close_save(self, event): else: self.on_exit(event) - def create_layout(self): - self.main_grid = wx.BoxSizer(wx.VERTICAL) - style = wx.NB_TOP - notebook_id = id_renew('settings.notebook', update=True) - self.notebook = wx.Notebook(self, id=notebook_id, style=style) - self.notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self.notebook_changed, id=notebook_id) - self.main_grid.Add(self.notebook, 1, wx.EXPAND) - self.SetSizer(self.main_grid) - self.Show(True) + def on_listbox_change(self, event): + item_object = event.EventObject + selection = item_object.get_key_from_index(item_object.GetSelection()) + description = translate_key(MODULE_KEY.join([selection, 'description'])) - def remove_pages(self, key): - for item in range(self.notebook.GetPageCount()): - text = self.notebook.GetPageText(0) - if not key == text and key not in self.page_list: - self.notebook.DeletePage(0) + item_key = IDS[event.GetId()].split(MODULE_KEY) + show_description = self.main_class.loaded_modules[item_key[0]]['gui'][item_key[1]].get('description', False) - def fill_notebook_with_modules(self, category_list, setting_category): - page_list = [] - self.settings_saved = False - for category_dict in category_list: - category_item, category_config = category_dict.iteritems().next() - translated_item = translate_key(category_item) - if translated_item not in self.page_list: - panel = wx.Panel(self.notebook) - self.fill_page_with_content(panel, setting_category, category_item, category_config) - self.notebook.AddPage(panel, translated_item) - page_list.append(translated_item) - else: - page_list.append(translated_item) - self.page_list = page_list + 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.SetLabel(description) + descr_static_text.Wrap(descr_static_text.GetSize()[0]) + + def create_layout(self): + self.main_grid = wx.BoxSizer(wx.HORIZONTAL) + tree_ctrl_size = wx.Size(220, -1) + style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.TR_TWIST_BUTTONS | wx.TR_NO_LINES + # style = wx.TR_HAS_BUTTONS | wx.TR_SINGLE | wx.TR_HIDE_ROOT + + tree_ctrl_id = id_renew('settings.tree', update=True) + tree_ctrl = wx.TreeCtrl(self, id=tree_ctrl_id, style=style) + tree_ctrl.SetMinSize(tree_ctrl_size) + root_key = MODULE_KEY.join(['settings', 'tree', 'root']) + root_node = tree_ctrl.AddRoot(translate_key(root_key)) + for item, value in self.categories.iteritems(): + item_key = MODULE_KEY.join(['settings', item]) + item_data = wx.TreeItemData() + item_data.SetData(item_key) + + item_node = tree_ctrl.AppendItem(root_node, translate_key(item_key), data=item_data) + for f_item, f_value in value.iteritems(): + if not f_item == item: + f_item_key = MODULE_KEY.join([item_key, f_item]) + f_item_data = wx.TreeItemData() + f_item_data.SetData(f_item_key) + tree_ctrl.AppendItem(item_node, translate_key(f_item), data=f_item_data) + tree_ctrl.ExpandAll() + + self.tree_ctrl = tree_ctrl + self.Bind(wx.EVT_TREE_SEL_CHANGED, self.tree_ctrl_changed, id=tree_ctrl_id) + self.main_grid.Add(self.tree_ctrl, 0, wx.EXPAND | wx.ALL, 7) + + content_page_id = id_renew(MODULE_KEY.join(['settings', 'content'])) + self.content_page = wx.Panel(self, id=content_page_id) + self.main_grid.Add(self.content_page, 1, wx.EXPAND) + + self.main_grid.Layout() + self.SetSizer(self.main_grid) + tree_ctrl.SelectItem(tree_ctrl.GetFirstChild(root_node)[0]) def fill_page_with_content(self, panel, setting_category, category_item, category_config): def create_button(button_key, function): @@ -208,32 +252,33 @@ def create_button(button_key, function): self.Bind(wx.EVT_BUTTON, function, id=button_id) return c_button + page_sizer = panel.GetSizer() # type: wx.Sizer + if not page_sizer: + page_sizer = wx.BoxSizer(wx.VERTICAL) + else: + page_sizer.DeleteWindows() + # Creating sizer for page sizer = wx.BoxSizer(wx.VERTICAL) # Window for settings - page_sc_window = wx.ScrolledWindow(panel, id=id_renew(category_item), style=wx.VSCROLL) - page_sc_window.SetScrollbars(5, 5, 10, 10) - - config = self.prepare_config_for_window(category_config) - - self.fill_sc_with_config(page_sc_window, config, category_item) - - sizer.Add(page_sc_window, 1, wx.EXPAND) + sizer.Add(self.fill_sc_with_config(panel, category_config, category_item), 1, wx.EXPAND) # Buttons button_sizer = wx.BoxSizer(wx.HORIZONTAL) for button_name in ['apply_button', 'cancel_button']: button_sizer.Add(create_button(MODULE_KEY.join([setting_category, category_item, button_name]), self.button_clicked), 0, wx.ALIGN_RIGHT) sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT) - panel.SetSizer(sizer) + page_sizer.Add(sizer, 1, wx.EXPAND) + page_sizer.Layout() + panel.SetSizer(page_sizer) panel.Layout() - pass - def fill_sc_with_config(self, page_sc_window, config, category_item): + def fill_sc_with_config(self, panel, category_config, category_item): + page_sc_window = wx.ScrolledWindow(panel, id=id_renew(category_item), style=wx.VSCROLL) + page_sc_window.SetScrollbars(5, 5, 10, 10) border_all = 5 sizer = wx.BoxSizer(wx.VERTICAL) - for section in config['sections']: - section_key, section_tuple = section + for section_key, section_items in category_config['config'].items(): if section_key in SKIP_TAGS: continue @@ -243,189 +288,220 @@ def fill_sc_with_config(self, page_sc_window, config, category_item): log.debug("Working on {0}".format(static_key)) - view = 'normal' - if section_key in config['gui']: # type: dict - log.debug('{0} has gui settings'.format(static_key)) - view = config['gui'][section_key].get('view', 'normal') - static_sizer.Add(self.create_items(static_box, static_key, - view, section_tuple, config['gui'].get(section_key, {})), + section_items, category_config.get('gui', {}).get(section_key, {})), 0, wx.EXPAND | wx.ALL, border_all) sizer.Add(static_sizer, 0, wx.EXPAND) page_sc_window.SetSizer(sizer) + return page_sc_window - @staticmethod - def prepare_config_for_window(category_config): - parser = ConfigParser(allow_no_value=True) # type: ConfigParser - parser.readfp(open(category_config['file'])) - config_dict = {'gui': {}, 'sections': []} - for section in parser.sections(): - if SECTION_GUI_TAG in section: - gui_dict = {} - section_items = None - for item, value in parser.items(section): - if item == 'for': - section_items = [value_item.strip() for value_item in value.split(',')] - elif item == 'hidden': - gui_dict[item] = [value_item.strip() for value_item in value.split(',')] - else: - gui_dict[item] = value - - if section_items: - for section_item in section_items: - config_dict['gui'][section_item] = gui_dict - else: - tag_values = [] - for item, value in parser.items(section): - tag_values.append((item, value)) - config_dict['sections'].append((section, tag_values)) - return config_dict - - def create_items(self, parent, key, view, section, section_gui): + def create_items(self, parent, key, section, section_gui): sizer = wx.BoxSizer(wx.VERTICAL) - addable_sizer = None + view = section_gui.get('view', 'normal') if 'list' in view: - is_dual = True if 'dual' in view else False - style = wx.ALIGN_CENTER_VERTICAL - item_sizer = wx.BoxSizer(wx.VERTICAL) - if section_gui.get('addable', False): - addable_sizer = wx.BoxSizer(wx.HORIZONTAL) - item_input_key = MODULE_KEY.join([key, 'list_input']) - addable_sizer.Add(wx.TextCtrl(parent, 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(parent, 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) - addable_sizer.Add(wx.Button(parent, id=item_apply_id, label=translate_key(item_apply_key)), 0, style) - self.Bind(wx.EVT_BUTTON, self.button_clicked, id=item_apply_id) - - item_remove_key = MODULE_KEY.join([key, 'list_remove']) - item_remove_id = id_renew(item_remove_key, update=True) - addable_sizer.Add(wx.Button(parent, id=item_remove_id, label=translate_key(item_remove_key)), 0, style) - self.Bind(wx.EVT_BUTTON, self.button_clicked, id=item_remove_id) - - item_sizer.Add(addable_sizer, 0, wx.EXPAND) - list_box = wx.grid.Grid(parent, 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, self.select_cell) - - for index, items in enumerate(section): - item, value = items - list_box.AppendRows(1) - if is_dual: - list_box.SetCellValue(index, 0, item.decode('utf-8')) - list_box.SetCellValue(index, 1, value.decode('utf-8')) - else: - list_box.SetCellValue(index, 0, item.decode('utf-8')) - 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() - - # Adding size of scrollbars - size = list_box.GetEffectiveMinSize() - size[0] += 18 - size[1] += 18 - list_box.SetMinSize(size) - item_sizer.Add(list_box, 1, wx.EXPAND) - - sizer.Add(item_sizer) + sizer.Add(self.create_list(parent, view, key, section, section_gui)) elif 'choose' in view: - is_single = True if 'single' in view else False - style = wx.LB_SINGLE if is_single else wx.LB_EXTENDED - item_sizer = wx.BoxSizer(wx.VERTICAL) - list_items = [] - translated_items = [] - - if section_gui['check_type'] in ['dir', 'folder', 'files']: - check_type = section_gui['check_type'] - remove_extension = section_gui['file_extension'] if 'file_extension' in section_gui else False - for item_in_list in os.listdir(os.path.join(self.main_class.main_config['root_folder'], - section_gui['check'])): - item_path = os.path.join(self.main_class.main_config['root_folder'], - section_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 remove_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)) - elif section_gui['check_type'] == 'sections': - parser = ConfigParser(allow_no_value=True) - parser.read(section_gui.get('check', '')) - for item in parser.sections(): - list_items.append(translate_key(item)) - - item_key = MODULE_KEY.join([key, 'list_box']) - label_text = translate_key(item_key) - if label_text: - item_sizer.Add(wx.StaticText(parent, label=label_text, style=wx.ALIGN_RIGHT)) + sizer.Add(self.create_choose(parent, view, key, section, section_gui)) + else: + sizer.Add(self.create_item(parent, view, key, section, section_gui)) + return sizer + + def create_list(self, parent, view, key, section, section_gui): + is_dual = True if 'dual' in view else False + style = wx.ALIGN_CENTER_VERTICAL + item_sizer = wx.BoxSizer(wx.VERTICAL) + addable_sizer = None + if section_gui.get('addable', False): + addable_sizer = wx.BoxSizer(wx.HORIZONTAL) + item_input_key = MODULE_KEY.join([key, 'list_input']) + addable_sizer.Add(wx.TextCtrl(parent, 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(parent, 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) + addable_sizer.Add(wx.Button(parent, id=item_apply_id, label=translate_key(item_apply_key)), 0, style) + self.Bind(wx.EVT_BUTTON, self.button_clicked, id=item_apply_id) + + item_remove_key = MODULE_KEY.join([key, 'list_remove']) + item_remove_id = id_renew(item_remove_key, update=True) + addable_sizer.Add(wx.Button(parent, id=item_remove_id, label=translate_key(item_remove_key)), 0, style) + self.Bind(wx.EVT_BUTTON, self.button_clicked, id=item_remove_id) + + item_sizer.Add(addable_sizer, 0, wx.EXPAND) + list_box = wx.grid.Grid(parent, 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, self.select_cell) + list_box.SetMinSize(wx.Size(-1, 100)) + + for index, (item, value) in enumerate(section.items()): + list_box.AppendRows(1) + if is_dual: + list_box.SetCellValue(index, 0, item.decode('utf-8')) + list_box.SetCellValue(index, 1, value.decode('utf-8')) + else: + list_box.SetCellValue(index, 0, item.decode('utf-8')) + 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() + + # Adding size of scrollbars + size = list_box.GetEffectiveMinSize() + size[0] += 18 + size[1] += 18 + list_box.SetMinSize(size) + item_sizer.Add(list_box, 1, wx.EXPAND) + return item_sizer + + def create_choose(self, parent, view, key, section, section_gui): + is_single = True if 'single' in view else False + description = section_gui.get('description', False) + style = wx.LB_SINGLE if is_single else wx.LB_EXTENDED + item_sizer = wx.BoxSizer(wx.VERTICAL) + list_items = [] + translated_items = [] + + if section_gui['check_type'] in ['dir', 'folder', 'files']: + check_type = section_gui['check_type'] + keep_extension = section_gui['file_extension'] if 'file_extension' in section_gui else False + for item_in_list in os.listdir(os.path.join(self.main_class.main_config['root_folder'], + section_gui['check'])): + item_path = os.path.join(self.main_class.main_config['root_folder'], + section_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)) + elif section_gui['check_type'] == 'sections': + parser = ConfigParser(allow_no_value=True) + parser.read(section_gui.get('check', '')) + for item in parser.sections(): + list_items.append(translate_key(item)) + + item_key = MODULE_KEY.join([key, 'list_box']) + label_text = translate_key(item_key) + if label_text: + item_sizer.Add(wx.StaticText(parent, label=label_text, style=wx.ALIGN_RIGHT)) + if is_single: item_list_box = KeyListBox(parent, id=id_renew(item_key, update=True), keys=list_items, choices=translated_items if translated_items else list_items, style=style) - section_for = section if 'multiple' in view else [section[0]] - for section_item, section_value in section_for: - try: - item_list_box.SetSelection(list_items.index(translate_key(section_item))) - except ValueError: - try: - item_list_box.SetSelection(list_items.index(section_item)) - except ValueError as exc: - log.debug("[create_items] Unable to find item {0} in list".format(exc.message)) - item_sizer.Add(item_list_box, 1, wx.EXPAND) - - sizer.Add(item_sizer) else: - items_to_add = [] - if not section: - return sizer - last_item = section[-1][0] - for item, value in section: - if not self.show_hidden and 'hidden' in section_gui and item in section_gui.get('hidden'): - continue - item_name = MODULE_KEY.join([key, item]) + item_list_box = KeyCheckListBox(parent, id=id_renew(item_key, update=True), keys=list_items, + choices=translated_items if translated_items else list_items) + self.Bind(wx.EVT_LISTBOX, self.on_listbox_change, item_list_box) + + section_for = section if not is_single else {section: None} + if is_single: + [item_list_box.SetSelection(list_items.index(item)) for item, value in section_for.items()] + else: + check_items = [list_items.index(item) for item, value in section_for.items()] + 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(parent, 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() + # descr_text.Wrap(descr_text.GetSize()[0]) + item_sizer.Add(adv_sizer) + else: + item_sizer.Add(item_list_box) + return item_sizer + + def create_dropdown(self, parent, view, key, section, section_gui, section_item=False, short_key=None): + item_text = wx.StaticText(parent, label=translate_key(key), + style=wx.ALIGN_RIGHT) + choices = section_gui.get('choices') + key = key if section_item else MODULE_KEY.join([key, 'dropdown']) + item_box = KeyChoice(parent, id=id_renew(key, update=True), + keys=choices, choices=choices) + item_value = section[short_key] if section_item else section + item_box.SetSelection(choices.index(item_value)) + return item_text, item_box + + def create_spin(self, parent, view, key, section, section_gui, section_item=False, short_key=None): + item_text = wx.StaticText(parent, label=translate_key(key), + style=wx.ALIGN_RIGHT) + key = key if section_item else MODULE_KEY.join([key, 'spin']) + value = short_key if section_item else section + item_box = wx.SpinCtrl(parent, id=id_renew(key, update=True), min=section_gui['min'], max=section_gui['max'], + initial=value) + return item_text, item_box + + def create_item(self, parent, view, key, section, section_gui): + flex_grid = wx.FlexGridSizer(0, 2, ITEM_SPACING_VERT, ITEM_SPACING_HORZ) + if not section: + return wx.Sizer() + for item, value in section.items(): + if not self.show_hidden and item in section_gui.get('hidden', []): + continue + item_name = MODULE_KEY.join([key, item]) + if item in section_gui: + if 'list' in section_gui[item].get('view'): + flex_grid.Add(self.create_list(parent, view, item_name, section, section_gui[item])) + flex_grid.AddSpacer(wx.Size(0, 0)) + elif 'choose' in section_gui[item].get('view'): + flex_grid.Add(self.create_choose(parent, view, item_name, section, section_gui[item])) + flex_grid.AddSpacer(wx.Size(0, 0)) + elif 'dropdown' in section_gui[item].get('view'): + text, control = self.create_dropdown(parent, view, item_name, section, section_gui[item], + section_item=True, short_key=item) + flex_grid.Add(text) + flex_grid.Add(control) + elif 'spin' in section_gui[item].get('view'): + text, control = self.create_spin(parent, view, item_name, section, section_gui[item], + section_item=True, short_key=section[item]) + flex_grid.Add(text) + flex_grid.Add(control) + else: # Checking type of an item style = wx.ALIGN_CENTER_VERTICAL - if not value: # Button + if value is None: # Button button_id = id_renew(item_name, update=True) item_button = wx.Button(parent, id=button_id, label=translate_key(item_name)) - items_to_add.append((item_button, 0, wx.ALIGN_LEFT)) + flex_grid.Add(item_button, 0, wx.ALIGN_LEFT) + flex_grid.AddSpacer(wx.Size(0, 0)) self.main_class.Bind(wx.EVT_BUTTON, self.button_clicked, id=button_id) - elif value.lower() in ['true', 'false']: # Checkbox + elif isinstance(value, bool): # Checkbox item_box = wx.CheckBox(parent, id=id_renew(item_name, update=True), label=translate_key(item_name), style=style) - item_box.SetValue(True if value.lower() == 'true' else False) - items_to_add.append((item_box, 0, wx.ALIGN_LEFT)) + item_box.SetValue(value) + flex_grid.Add(item_box, 0, wx.ALIGN_LEFT) + flex_grid.AddSpacer(wx.Size(0, 0)) else: # TextCtrl - item_sizer = wx.BoxSizer(wx.HORIZONTAL) item_box = wx.TextCtrl(parent, id=id_renew(item_name, update=True), - value=value.decode('utf-8')) + value=str(value).decode('utf-8')) item_text = wx.StaticText(parent, label=translate_key(item_name), - style=wx.ALIGN_RIGHT) - item_spacer = (10, 0) - item_sizer.AddMany([(item_text, 0, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL), - (item_spacer, 0, 0), - (item_box, 0)]) - items_to_add.append(item_sizer) - if not item == last_item: - items_to_add.append((self.spacer_size, 0, 0)) - sizer.AddMany(items_to_add) - return sizer + style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_HORIZONTAL) + flex_grid.Add(item_text) + flex_grid.Add(item_box) + flex_grid.Fit(parent) + return flex_grid def button_clicked(self, event): log.debug("[Settings] Button clicked: {0}".format(IDS[event.GetId()])) @@ -434,66 +510,142 @@ def button_clicked(self, event): if keys[-1] in ['list_add', 'list_remove']: self.list_operation(MODULE_KEY.join(keys[:-1]), action=keys[-1]) elif keys[-1] == 'apply_button': - self.save_settings(MODULE_KEY.join(keys[1:-1])) + module_name = MODULE_KEY.join(keys[1:-1]) + if self.save_settings(module_name): + log.debug('Got non-dynamic changes') + dialog = wx.MessageDialog(self, + message=translate_key(MODULE_KEY.join(['main', 'save', 'non_dynamic'])), + caption="Caption", + style=wx.OK_DEFAULT, + pos=wx.DefaultPosition) + response = dialog.ShowModal() + + if response == wx.ID_YES: + self.on_exit(event) + else: + event.StopPropagation() + module_class = self.main_class.loaded_modules[module_name].get('class') + if module_class: + module_class.apply_settings() self.settings_saved = True elif keys[-1] == 'cancel_button': self.on_close(event) event.Skip() - def notebook_changed(self, event): + def tree_ctrl_changed(self, event): self.settings_saved = False + tree_ctrl = event.EventObject # type: wx.TreeCtrl + selection = tree_ctrl.GetFocusedItem() + selection_text = tree_ctrl.GetItemData(selection).GetData() + key_list = selection_text.split(MODULE_KEY) + + # Drawing page + self.fill_page_with_content(self.content_page, key_list[1], key_list[-1], + self.main_class.loaded_modules[key_list[-1]]) + event.Skip() def save_settings(self, module): - module_config = self.main_class.loaded_modules.get(module, {}) - if module_config: - parser = module_config['parser'] + module_settings = self.main_class.loaded_modules.get(module, {}) + non_dynamic = module_settings.get('gui', {}).get('non_dynamic', []) + module_config = module_settings.get('config') + non_dynamic_check = False + if module_settings: + parser = module_settings['parser'] # type: ConfigParser items = get_list_of_ids_from_module_name(module, return_tuple=True) for item, name in items: module_name, section, item_name = name.split(MODULE_KEY) + + if not parser.has_section(section): + continue + # Check for non-dynamic items + for d_item in non_dynamic: + if section in d_item: + if MODULE_KEY.join([section, '*']) in d_item: + non_dynamic_check = True + break + elif MODULE_KEY.join([section, item_name]) in d_item: + non_dynamic_check = True + break + # Saving wx_window = wx.FindWindowById(item) if isinstance(wx_window, wx.CheckBox): - if name == MODULE_KEY.join(['config', 'gui', 'show_hidden']): + if name == MODULE_KEY.join(['main', 'gui', 'show_hidden']): self.show_hidden = wx_window.IsChecked() parser.set(section, item_name, wx_window.IsChecked()) + module_config[section][item_name] = wx_window.IsChecked() elif isinstance(wx_window, wx.TextCtrl): if item_name not in SKIP_TXT_CONTROLS: - parser.set(section, item_name, wx_window.GetValue().encode('utf-8')) + parser.set(section, item_name, wx_window.GetValue().encode('utf-8').strip()) + module_config[section][item_name] = wx_window.GetValue().encode('utf-8').strip() elif isinstance(wx_window, wx.grid.Grid): col_count = wx_window.GetNumberCols() row_count = wx_window.GetNumberRows() parser_options = parser.options(section) - grid_elements = [[wx_window.GetCellValue(row, col).encode('utf-8') + grid_elements = [[wx_window.GetCellValue(row, col).encode('utf-8').strip() for col in range(col_count)] for row in range(row_count)] if not grid_elements: for option in parser_options: parser.remove_option(section, option) + module_config[section].pop(option) else: + item_list = [item[0] for item in grid_elements] for option in parser_options: - for elements in grid_elements: - if option not in elements: - parser.remove_option(section, option) + if option not in item_list: + module_config[section].pop(option) + parser.remove_option(section, option) for elements in grid_elements: parser.set(section, *elements) + if len(elements) == 1: + module_config[section][elements[0]] = None + elif len(elements) == 2: + module_config[section][elements[0]] = elements[1] elif isinstance(wx_window, wx.Button): if item_name not in SKIP_BUTTONS: parser.set(section, item_name) + module_config[section][item_name] = None elif isinstance(wx_window, KeyListBox): - item_ids = wx_window.GetSelections() + item_id = wx_window.GetSelection() + parser_options = parser.options(section) + item_value = wx_window.get_key_from_index(item_id) + if not item_value: + for option in parser_options: + parser.remove_option(section, option) + module_config[section] = None + else: + for option in parser_options: + parser.remove_option(section, option) + parser.set(section, item_value) + module_config[section] = item_value + elif isinstance(wx_window, KeyCheckListBox): + item_ids = wx_window.GetChecked() parser_options = parser.options(section) - items_values = [wx_window.get_key_from_id(item_id) for item_id in item_ids] + items_values = [wx_window.get_key_from_index(item_id) for item_id in item_ids] if not items_values: for option in parser_options: parser.remove_option(section, option) + module_config[section].pop(option) else: for option in parser_options: if option not in items_values: parser.remove_option(section, option) + module_config[section].pop(option) for value in items_values: parser.set(section, value) - with open(module_config['file'], 'w') as config_file: + module_config[section][value] = None + elif isinstance(wx_window, KeyChoice): + item_id = wx_window.GetSelection() + item_value = wx_window.get_key_from_index(item_id) + parser.set(section, item_name, item_value) + module_config[section][item_name] = item_value + elif isinstance(wx_window, wx.SpinCtrl): + item_value = wx_window.GetValue() + parser.set(section, item_name, item_value) + module_config[section][item_name] = item_value + with open(module_settings['file'], 'w') as config_file: parser.write(config_file) + return non_dynamic_check def select_cell(self, event): self.selected_cell = (event.GetRow(), event.GetCol()) @@ -512,9 +664,9 @@ def list_operation(self, key, action): list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) list_box.AppendRows(1) row_count = list_box.GetNumberRows() - 1 - list_box.SetCellValue(row_count, 0, list_input_value) + list_box.SetCellValue(row_count, 0, list_input_value.strip()) if list_input2_value: - list_box.SetCellValue(row_count, 1, list_input2_value) + list_box.SetCellValue(row_count, 1, list_input2_value.strip()) elif action == 'list_remove': list_box = wx.FindWindowById(get_id_from_name(MODULE_KEY.join([key, 'list_box']))) @@ -535,16 +687,15 @@ def list_operation(self, key, action): class ChatGui(wx.Frame): - settings_window = None - def __init__(self, parent, title, url, **kwargs): - wx.Frame.__init__(self, parent, title=title, size=(450, 500)) - # Setting the settings self.main_config = kwargs.get('main_config') self.gui_settings = kwargs.get('gui_settings') self.loaded_modules = kwargs.get('loaded_modules') + self.queue = kwargs.get('queue') + self.settings_window = None + wx.Frame.__init__(self, parent, title=title, size=self.gui_settings.get('size')) # Set window style styles = wx.DEFAULT_FRAME_STYLE if self.gui_settings.get('on_top', False): @@ -560,87 +711,73 @@ def __init__(self, parent, title, url, **kwargs): # Creating main gui window vbox = wx.BoxSizer(wx.VERTICAL) self.toolbar = MainMenuToolBar(self, main_class=self) - self.settings_menu = self.create_menu("settings", self.sorted_categories) self.browser_window = chromectrl.ChromeCtrl(self, useTimer=False, url=str(url), hasNavBar=False) vbox.Add(self.toolbar, 0, wx.EXPAND) vbox.Add(self.browser_window, 1, wx.EXPAND) # Set events - self.Bind(wx.EVT_CLOSE, self.on_exit) + self.Bind(wx.EVT_CLOSE, self.on_close) # Show window after creation self.SetSizer(vbox) self.Show(True) - def on_about(self, event): - self.browser_window.Refresh() - event.Skip() + # Show update dialog if new version found + if self.main_config['update']: + dialog = wx.MessageDialog(self, message="There is new version, do you want to update?", + caption="New Update Available", + style=wx.YES_NO | wx.YES_DEFAULT, + pos=wx.DefaultPosition) + response = dialog.ShowModal() + if response == wx.ID_YES: + webbrowser.open(self.main_config['update_url']) - def on_exit(self, event): + def on_close(self, event): log.info("Exiting...") + # Saving last window size + parser = self.loaded_modules['main']['parser'] # type: ConfigParser + size = self.Size + parser.set('gui_information', 'width', size[0]) + parser.set('gui_information', 'height', size[1]) + parser.write(open(self.loaded_modules['main']['file'], 'w')) self.Destroy() - event.Skip() def on_right_down(self, event): log.info(event) event.Skip() def on_settings(self, event): - log.debug("Opening menu {0}".format(IDS[event.GetId()])) - tool_index = self.toolbar.GetToolPos(get_id_from_name('menu.settings')) - tool_size = self.toolbar.GetToolSize() - bar_position = self.toolbar.GetScreenPosition() - self.GetScreenPosition() - offset = tool_size[0] + (1 * tool_index) - lower_left_corner = (bar_position[0] + (offset * tool_index), - bar_position[1] + tool_size[1]) - menu_position = (lower_left_corner[0] - bar_position[0], - lower_left_corner[1] - bar_position[1]) - - self.PopupMenu(self.settings_menu, menu_position) - event.Skip() - - def on_settings_button(self, event): log.debug("Got event from {0}".format(IDS[event.GetId()])) module_groups = IDS[event.GetId()].split(MODULE_KEY) settings_category = MODULE_KEY.join(module_groups[1:-1]) settings_menu_id = id_renew(settings_category, update=True) if self.settings_window: - self.settings_window.notebook.Show(False) self.settings_window.SetFocus() - self.settings_window.SetTitle(translate_key(MODULE_KEY.join(module_groups[:-1]))) - self.settings_window.remove_pages(translate_key(module_groups[-1])) else: self.settings_window = SettingsWindow(self, id=settings_menu_id, - title=translate_key(MODULE_KEY.join(module_groups[:-1])), - size=(500, 400), - main_class=self) - - self.settings_window.fill_notebook_with_modules(self.sorted_categories[settings_category], settings_category) - self.settings_window.notebook.SetSelection(self.settings_window.page_list.index(translate_key(module_groups[-1]))) - self.settings_window.notebook.Show(True) - event.Skip() - - def create_menu(self, name, modules, menu_named=False): - settings_menu = wx.Menu(translate_key(name)) if menu_named else wx.Menu() - # Creating menu items - for category, category_items in modules.items(): - category_name = MODULE_KEY.join([name, category]) - category_sub_menu = wx.Menu() - for category_dict in category_items: - category_item_name, settings = category_dict.iteritems().next() - sub_name = MODULE_KEY.join([category_name, category_item_name]) - category_menu_item = category_sub_menu.Append(id_renew(sub_name, update=True), - translate_key(category_item_name)) - self.Bind(wx.EVT_MENU, self.on_settings_button, id=category_menu_item.GetId()) - settings_menu.AppendSubMenu(category_sub_menu, translate_key(category_name)) - return settings_menu + title=translate_key('settings'), + size=(700, 400), + main_class=self, + categories=self.sorted_categories) def button_clicked(self, event): - log.debug("[ChatGui] Button clicked: {0}".format(IDS[event.GetId()])) button_id = event.GetId() keys = 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) + 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) + else: + for module, settings in self.loaded_modules.items(): + if 'class' in settings: + settings['class'].gui_button_press(self, event, list_keys) event.Skip() @@ -649,19 +786,25 @@ class GuiThread(threading.Thread): url = 'http://localhost' port = '8080' - def __init__(self, **kwds): + def __init__(self, **kwargs): threading.Thread.__init__(self) self.daemon = True - self.gui_settings = kwds.get('gui_settings', {}) - self.loaded_modules = kwds.get('loaded_modules', {}) - self.main_config = kwds.get('main_config', {}) - if 'webchat' in self.loaded_modules: - self.port = self.loaded_modules['webchat']['port'] + self.gui = None + self.kwargs = kwargs + if 'webchat' in self.kwargs.get('loaded_modules'): + self.port = self.kwargs['loaded_modules']['webchat']['port'] def run(self): chromectrl.Initialize() - url = ':'.join([self.url, self.port]) + url = ':'.join([self.url, str(self.port)]) app = wx.App(False) # Create a new app, don't redirect stdout/stderr to a window. - ChatGui(None, "LalkaChat", url, main_config=self.main_config, gui_settings=self.gui_settings, - loaded_modules=self.loaded_modules) # A Frame is a top-level window. + self.gui = ChatGui(None, "LalkaChat", url, **self.kwargs) # A Frame is a top-level window. app.MainLoop() + self.quit() + + def quit(self): + try: + self.gui.on_close('event') + except wx.PyDeadObjectError: + pass + os._exit(0) diff --git a/http/czt/css/czt.css b/http/czt/css/style.css similarity index 97% rename from http/czt/css/czt.css rename to http/czt/css/style.css index ec1ea2c..b43106f 100644 --- a/http/czt/css/czt.css +++ b/http/czt/css/style.css @@ -12,7 +12,7 @@ body{ .msg{ background-color: rgba( 35, 35, 37, 0.627451 ); font-family: 'Consolas', serif; - font-size: 13pt; + font-size: {{ font_size }}pt; text-shadow: 0 1px 1px black; padding-top: 2px; word-wrap: break-word; diff --git a/http/czt/index.html b/http/czt/index.html index a257d0f..cf8d366 100644 --- a/http/czt/index.html +++ b/http/czt/index.html @@ -4,7 +4,7 @@ - + diff --git a/http/czt/js/socket.js b/http/czt/js/socket.js index 50d3bc8..cd9a876 100644 --- a/http/czt/js/socket.js +++ b/http/czt/js/socket.js @@ -1,30 +1,47 @@ +var MAX_MESSAGES = 70; var find_location = window.location.href; var RegExp = /:(\d+)/; var find_list = RegExp.exec(find_location.toString()); var find_port = find_list[1]; var ws_url = "ws://127.0.0.1:".concat(find_port, "/ws"); +// Chat settings +var timeout = 0; +var loadHistory = true; + var socket = new WebSocket(ws_url); + +var chatMessages; socket.onopen = function() { - //console.log("Соединение установлено."); + console.log("Socket connected") + chatMessages = document.getElementById('ChatContainer'); }; socket.onclose = function(event) { if (event.wasClean) { - //console.log('Соединение закрыто чисто'); - } else { - //console.log('Обрыв соединения'); // например, "убит" процесс сервера + console.log("Socket closed cleanly") + } + else { + console.log("Socket closed not cleanly") } - //console.log('Код: ' + event.code + ' причина: ' + event.reason); }; socket.onmessage = function(event) { - var incomingMessage = event.data; - showMessage(incomingMessage); + var incomingMessage = JSON.parse(event.data); + if(incomingMessage.hasOwnProperty('command')) { + runCommand(incomingMessage); + } + else { + if (loadHistory) { + showMessage(incomingMessage); + } + else if (!incomingMessage.hasOwnProperty('history')) { + showMessage(incomingMessage); + } + } }; socket.onerror = function(error) { - //console.log("Ошибка " + error.message); }; twitch_processEmoticons = function(message, emotes) { @@ -92,18 +109,37 @@ escapeHtml = (function () { }; }()); +function removeMessage(element) { + var elm = element || chatMessages.lastChild; + chatMessages.removeChild(elm); + } + +function updateMessages() { + if(chatMessages.children.length < MAX_MESSAGES) return; + var element = chatMessages.lastChild; + if(element.hasAttribute('timer-id')) { + var timerId = element.getAttribute('timer-id'); + window.clearTimeout(timerId); + } + removeMessage(element); + } + + function showMessage(message) { var badge_colors = 1; - + var elements = {}; elements['message'] = document.createElement('div'); elements.message.setAttribute('class', 'msg'); - - var messageJSON = JSON.parse(message); - + if(timeout > 0) { + elements.message.setAttribute('timer-id', setTimeout(removeMessage, timeout * 1000, elements.message)); + } + + var messageJSON = message; + if(messageJSON.hasOwnProperty('source')) { //console.log("message has source " + messageJSON.source); - + elements.message['source'] = document.createElement('div'); elements.message.source.setAttribute('class', 'msgSource'); @@ -119,7 +155,7 @@ function showMessage(message) { elements.message.source.appendChild(elements.message.source.img); elements.message.appendChild(elements.message.source); } - + if(messageJSON.hasOwnProperty('levels')) { elements.message['level'] = document.createElement('div'); elements.message.level.setAttribute('class', 'msgLevel'); @@ -127,13 +163,13 @@ function showMessage(message) { elements.message.level['img'] = document.createElement('img'); elements.message.level.img.setAttribute('class', 'imgLevel'); elements.message.level.img.setAttribute('src', messageJSON.levels.url); - + elements.message.level.appendChild(elements.message.level.img); elements.message.appendChild(elements.message.level); } - + if(messageJSON.hasOwnProperty('s_levels')) { - + for (i = 0; i < messageJSON.s_levels.length; i++) { elements.message['s_level'] = document.createElement('div'); elements.message.s_level.setAttribute('class', 'msgSLevel'); @@ -141,14 +177,14 @@ function showMessage(message) { elements.message.s_level['img'] = document.createElement('img'); elements.message.s_level.img.setAttribute('class', 'imgSLevel'); elements.message.s_level.img.setAttribute('src', messageJSON.s_levels[i].url); - + elements.message.s_level.appendChild(elements.message.s_level.img); elements.message.appendChild(elements.message.s_level); } } - + if(messageJSON.hasOwnProperty('badges')) { - + for (i = 0; i < messageJSON.badges.length; i++) { elements.message['badge'] = document.createElement('div'); elements.message.badge.setAttribute('class', 'msgBadge'); @@ -156,7 +192,7 @@ function showMessage(message) { elements.message.badge['img'] = document.createElement('img'); elements.message.badge.img.setAttribute('class', 'imgBadge'); elements.message.badge.img.setAttribute('src', messageJSON.badges[i].url); - + if(badge_colors) { if(messageJSON.badges[i].badge == 'broadcaster') { elements.message.badge.img.setAttribute('style', 'background-color: #e71818'); @@ -172,7 +208,7 @@ function showMessage(message) { elements.message.appendChild(elements.message.badge); } } - + if(messageJSON.hasOwnProperty('user')) { // console.log("message has user " + messageJSON.user); elements.message['user'] = document.createElement('div'); @@ -189,10 +225,10 @@ function showMessage(message) { } elements.message.user.appendChild(document.createTextNode(addString)); - + elements.message.appendChild(elements.message.user); } - + if(messageJSON.hasOwnProperty('text')) { // console.log("message has text " + messageJSON.text); elements.message['text'] = document.createElement('div'); @@ -208,7 +244,7 @@ function showMessage(message) { else { elements.message.text.setAttribute('class', 'msgText'); } - + if(messageJSON.source == 'tw') { messageJSON.text = htmlifyTwitchEmoticons(escapeHtml(twitch_processEmoticons(messageJSON.text, messageJSON.emotes))); if(messageJSON.hasOwnProperty('bttv_emotes')) { @@ -221,11 +257,19 @@ function showMessage(message) { else if(messageJSON.source == 'fs') { messageJSON.text = htmlifyGGEmoticons(escapeHtml(messageJSON.text), messageJSON.emotes) } - + // elements.message.text.appendChild(document.createTextNode(messageJSON.text)); elements.message.text.innerHTML = messageJSON.text; - + elements.message.appendChild(elements.message.text); + } document.getElementById('ChatContainer').appendChild(elements.message); + updateMessages(); +} + +function runCommand(message) { + if(message.command == 'reload'){ + window.location.reload(); + } } \ No newline at end of file diff --git a/http/czt_timed/css/style.css b/http/czt_timed/css/style.css new file mode 100644 index 0000000..b43106f --- /dev/null +++ b/http/czt_timed/css/style.css @@ -0,0 +1,77 @@ +::-webkit-scrollbar { + visibility: hidden; +} +body{ + margin: 0; +} +#ChatContainer{ + position: absolute; + bottom: 0; + width: 100%; +} +.msg{ + background-color: rgba( 35, 35, 37, 0.627451 ); + font-family: 'Consolas', serif; + font-size: {{ font_size }}pt; + text-shadow: 0 1px 1px black; + padding-top: 2px; + word-wrap: break-word; + color: #FFFFFF; +} +.msgSource, +.msgBadge, +.msgLevel, +.msgSLevel{ + width: 16px; + height: 16px; + background-color: rgba(0,0,0,0.6); + border-radius: 3px; + margin: 0 3px 0 0; + position: relative; + top: 3px; + left: 3px; + display: inline-block; +} +.msgUser{ + display: inline-block; + position: relative; + padding-left: 5px; +} +.msgText{ + display: inline; + height: auto; + padding-left: 5px; +} +.msgTextPriv{ + display: inline; + height: auto; + padding-left: 5px; + color: #e57017; +} +.msgTextMention{ + display: inline; + height: auto; + padding-left: 5px; + color: #c0ffc0; +} +.msgTextSystem{ + display: inline; + height: auto; + padding-left: 5px; + color: #ff68fb; +} +.imgSmile{ + margin-top: -4px; + height: 20px; + width: auto; + top: 2px; + position: relative; +} +.imgBadge, +.imgSource, +.imgLevel, +.imgSLevel{ + width: 16px; + height: 16px; + border-radius: 3px; +} \ No newline at end of file diff --git a/http/czt_timed/img/levels/0.png b/http/czt_timed/img/levels/0.png new file mode 100644 index 0000000..5ca9d8a Binary files /dev/null and b/http/czt_timed/img/levels/0.png differ diff --git a/http/czt_timed/img/levels/1.png b/http/czt_timed/img/levels/1.png new file mode 100644 index 0000000..66aa505 Binary files /dev/null and b/http/czt_timed/img/levels/1.png differ diff --git a/http/czt_timed/img/levels/10.png b/http/czt_timed/img/levels/10.png new file mode 100644 index 0000000..1ea1251 Binary files /dev/null and b/http/czt_timed/img/levels/10.png differ diff --git a/http/czt_timed/img/levels/11.png b/http/czt_timed/img/levels/11.png new file mode 100644 index 0000000..a22762c Binary files /dev/null and b/http/czt_timed/img/levels/11.png differ diff --git a/http/czt_timed/img/levels/12.png b/http/czt_timed/img/levels/12.png new file mode 100644 index 0000000..8c94a59 Binary files /dev/null and b/http/czt_timed/img/levels/12.png differ diff --git a/http/czt_timed/img/levels/13.png b/http/czt_timed/img/levels/13.png new file mode 100644 index 0000000..6878638 Binary files /dev/null and b/http/czt_timed/img/levels/13.png differ diff --git a/http/czt_timed/img/levels/14.png b/http/czt_timed/img/levels/14.png new file mode 100644 index 0000000..c08dd39 Binary files /dev/null and b/http/czt_timed/img/levels/14.png differ diff --git a/http/czt_timed/img/levels/15.png b/http/czt_timed/img/levels/15.png new file mode 100644 index 0000000..64353ec Binary files /dev/null and b/http/czt_timed/img/levels/15.png differ diff --git a/http/czt_timed/img/levels/16.png b/http/czt_timed/img/levels/16.png new file mode 100644 index 0000000..eaab90e Binary files /dev/null and b/http/czt_timed/img/levels/16.png differ diff --git a/http/czt_timed/img/levels/17.png b/http/czt_timed/img/levels/17.png new file mode 100644 index 0000000..682e71e Binary files /dev/null and b/http/czt_timed/img/levels/17.png differ diff --git a/http/czt_timed/img/levels/18.png b/http/czt_timed/img/levels/18.png new file mode 100644 index 0000000..246369a Binary files /dev/null and b/http/czt_timed/img/levels/18.png differ diff --git a/http/czt_timed/img/levels/19.png b/http/czt_timed/img/levels/19.png new file mode 100644 index 0000000..af9497d Binary files /dev/null and b/http/czt_timed/img/levels/19.png differ diff --git a/http/czt_timed/img/levels/2.png b/http/czt_timed/img/levels/2.png new file mode 100644 index 0000000..99bde2d Binary files /dev/null and b/http/czt_timed/img/levels/2.png differ diff --git a/http/czt_timed/img/levels/20.png b/http/czt_timed/img/levels/20.png new file mode 100644 index 0000000..0216450 Binary files /dev/null and b/http/czt_timed/img/levels/20.png differ diff --git a/http/czt_timed/img/levels/21.png b/http/czt_timed/img/levels/21.png new file mode 100644 index 0000000..d4b185e Binary files /dev/null and b/http/czt_timed/img/levels/21.png differ diff --git a/http/czt_timed/img/levels/22.png b/http/czt_timed/img/levels/22.png new file mode 100644 index 0000000..6939a2f Binary files /dev/null and b/http/czt_timed/img/levels/22.png differ diff --git a/http/czt_timed/img/levels/23.png b/http/czt_timed/img/levels/23.png new file mode 100644 index 0000000..9db3a5d Binary files /dev/null and b/http/czt_timed/img/levels/23.png differ diff --git a/http/czt_timed/img/levels/24.png b/http/czt_timed/img/levels/24.png new file mode 100644 index 0000000..3dcb03e Binary files /dev/null and b/http/czt_timed/img/levels/24.png differ diff --git a/http/czt_timed/img/levels/25.png b/http/czt_timed/img/levels/25.png new file mode 100644 index 0000000..db09752 Binary files /dev/null and b/http/czt_timed/img/levels/25.png differ diff --git a/http/czt_timed/img/levels/26.png b/http/czt_timed/img/levels/26.png new file mode 100644 index 0000000..a492bad Binary files /dev/null and b/http/czt_timed/img/levels/26.png differ diff --git a/http/czt_timed/img/levels/27.png b/http/czt_timed/img/levels/27.png new file mode 100644 index 0000000..75e398d Binary files /dev/null and b/http/czt_timed/img/levels/27.png differ diff --git a/http/czt_timed/img/levels/28.png b/http/czt_timed/img/levels/28.png new file mode 100644 index 0000000..2d01db2 Binary files /dev/null and b/http/czt_timed/img/levels/28.png differ diff --git a/http/czt_timed/img/levels/29.png b/http/czt_timed/img/levels/29.png new file mode 100644 index 0000000..9e78222 Binary files /dev/null and b/http/czt_timed/img/levels/29.png differ diff --git a/http/czt_timed/img/levels/3.png b/http/czt_timed/img/levels/3.png new file mode 100644 index 0000000..c5b451d Binary files /dev/null and b/http/czt_timed/img/levels/3.png differ diff --git a/http/czt_timed/img/levels/30.png b/http/czt_timed/img/levels/30.png new file mode 100644 index 0000000..33ab473 Binary files /dev/null and b/http/czt_timed/img/levels/30.png differ diff --git a/http/czt_timed/img/levels/31.png b/http/czt_timed/img/levels/31.png new file mode 100644 index 0000000..b71b907 Binary files /dev/null and b/http/czt_timed/img/levels/31.png differ diff --git a/http/czt_timed/img/levels/32.png b/http/czt_timed/img/levels/32.png new file mode 100644 index 0000000..6282eea Binary files /dev/null and b/http/czt_timed/img/levels/32.png differ diff --git a/http/czt_timed/img/levels/33.png b/http/czt_timed/img/levels/33.png new file mode 100644 index 0000000..ad748bb Binary files /dev/null and b/http/czt_timed/img/levels/33.png differ diff --git a/http/czt_timed/img/levels/34.png b/http/czt_timed/img/levels/34.png new file mode 100644 index 0000000..fa98e40 Binary files /dev/null and b/http/czt_timed/img/levels/34.png differ diff --git a/http/czt_timed/img/levels/35.png b/http/czt_timed/img/levels/35.png new file mode 100644 index 0000000..a620df4 Binary files /dev/null and b/http/czt_timed/img/levels/35.png differ diff --git a/http/czt_timed/img/levels/36.png b/http/czt_timed/img/levels/36.png new file mode 100644 index 0000000..4f1db46 Binary files /dev/null and b/http/czt_timed/img/levels/36.png differ diff --git a/http/czt_timed/img/levels/37.png b/http/czt_timed/img/levels/37.png new file mode 100644 index 0000000..d216a83 Binary files /dev/null and b/http/czt_timed/img/levels/37.png differ diff --git a/http/czt_timed/img/levels/38.png b/http/czt_timed/img/levels/38.png new file mode 100644 index 0000000..600785d Binary files /dev/null and b/http/czt_timed/img/levels/38.png differ diff --git a/http/czt_timed/img/levels/39.png b/http/czt_timed/img/levels/39.png new file mode 100644 index 0000000..c3524f5 Binary files /dev/null and b/http/czt_timed/img/levels/39.png differ diff --git a/http/czt_timed/img/levels/4.png b/http/czt_timed/img/levels/4.png new file mode 100644 index 0000000..80a6b2e Binary files /dev/null and b/http/czt_timed/img/levels/4.png differ diff --git a/http/czt_timed/img/levels/40.png b/http/czt_timed/img/levels/40.png new file mode 100644 index 0000000..5108e46 Binary files /dev/null and b/http/czt_timed/img/levels/40.png differ diff --git a/http/czt_timed/img/levels/41.png b/http/czt_timed/img/levels/41.png new file mode 100644 index 0000000..0088095 Binary files /dev/null and b/http/czt_timed/img/levels/41.png differ diff --git a/http/czt_timed/img/levels/42.png b/http/czt_timed/img/levels/42.png new file mode 100644 index 0000000..3a48399 Binary files /dev/null and b/http/czt_timed/img/levels/42.png differ diff --git a/http/czt_timed/img/levels/43.png b/http/czt_timed/img/levels/43.png new file mode 100644 index 0000000..a68a199 Binary files /dev/null and b/http/czt_timed/img/levels/43.png differ diff --git a/http/czt_timed/img/levels/5.png b/http/czt_timed/img/levels/5.png new file mode 100644 index 0000000..1887163 Binary files /dev/null and b/http/czt_timed/img/levels/5.png differ diff --git a/http/czt_timed/img/levels/6.png b/http/czt_timed/img/levels/6.png new file mode 100644 index 0000000..60d5b33 Binary files /dev/null and b/http/czt_timed/img/levels/6.png differ diff --git a/http/czt_timed/img/levels/7.png b/http/czt_timed/img/levels/7.png new file mode 100644 index 0000000..85df85b Binary files /dev/null and b/http/czt_timed/img/levels/7.png differ diff --git a/http/czt_timed/img/levels/8.png b/http/czt_timed/img/levels/8.png new file mode 100644 index 0000000..68ace6e Binary files /dev/null and b/http/czt_timed/img/levels/8.png differ diff --git a/http/czt_timed/img/levels/9.png b/http/czt_timed/img/levels/9.png new file mode 100644 index 0000000..8b33420 Binary files /dev/null and b/http/czt_timed/img/levels/9.png differ diff --git a/http/czt_timed/img/levels/cube.png b/http/czt_timed/img/levels/cube.png new file mode 100644 index 0000000..7d05d56 Binary files /dev/null and b/http/czt_timed/img/levels/cube.png differ diff --git a/http/czt_timed/img/sources/cybergame.png b/http/czt_timed/img/sources/cybergame.png new file mode 100644 index 0000000..60c23b6 Binary files /dev/null and b/http/czt_timed/img/sources/cybergame.png differ diff --git a/http/czt_timed/img/sources/empire.png b/http/czt_timed/img/sources/empire.png new file mode 100644 index 0000000..ca64fa4 Binary files /dev/null and b/http/czt_timed/img/sources/empire.png differ diff --git a/http/czt_timed/img/sources/fs.png b/http/czt_timed/img/sources/fs.png new file mode 100644 index 0000000..b2a2721 Binary files /dev/null and b/http/czt_timed/img/sources/fs.png differ diff --git a/http/czt_timed/img/sources/gamerstv.png b/http/czt_timed/img/sources/gamerstv.png new file mode 100644 index 0000000..70df90a Binary files /dev/null and b/http/czt_timed/img/sources/gamerstv.png differ diff --git a/http/czt_timed/img/sources/gg.png b/http/czt_timed/img/sources/gg.png new file mode 100644 index 0000000..dd76fbf Binary files /dev/null and b/http/czt_timed/img/sources/gg.png differ diff --git a/http/czt_timed/img/sources/gipsyteam.png b/http/czt_timed/img/sources/gipsyteam.png new file mode 100644 index 0000000..0c4a23e Binary files /dev/null and b/http/czt_timed/img/sources/gipsyteam.png differ diff --git a/http/czt_timed/img/sources/gohatv.png b/http/czt_timed/img/sources/gohatv.png new file mode 100644 index 0000000..5acd54b Binary files /dev/null and b/http/czt_timed/img/sources/gohatv.png differ diff --git a/http/czt_timed/img/sources/hitboxtv.png b/http/czt_timed/img/sources/hitboxtv.png new file mode 100644 index 0000000..e41c68d Binary files /dev/null and b/http/czt_timed/img/sources/hitboxtv.png differ diff --git a/http/czt_timed/img/sources/lalka_cup.png b/http/czt_timed/img/sources/lalka_cup.png new file mode 100644 index 0000000..2b4ef61 Binary files /dev/null and b/http/czt_timed/img/sources/lalka_cup.png differ diff --git a/http/czt_timed/img/sources/midlane.png b/http/czt_timed/img/sources/midlane.png new file mode 100644 index 0000000..13a9c87 Binary files /dev/null and b/http/czt_timed/img/sources/midlane.png differ diff --git a/http/czt_timed/img/sources/streamcube.png b/http/czt_timed/img/sources/streamcube.png new file mode 100644 index 0000000..cbf13ae Binary files /dev/null and b/http/czt_timed/img/sources/streamcube.png differ diff --git a/http/czt_timed/img/sources/tw.png b/http/czt_timed/img/sources/tw.png new file mode 100644 index 0000000..5904eaa Binary files /dev/null and b/http/czt_timed/img/sources/tw.png differ diff --git a/http/czt_timed/img/sources/youtube.png b/http/czt_timed/img/sources/youtube.png new file mode 100644 index 0000000..3632a0f Binary files /dev/null and b/http/czt_timed/img/sources/youtube.png differ diff --git a/http/czt_timed/index.html b/http/czt_timed/index.html new file mode 100644 index 0000000..72ee1e2 --- /dev/null +++ b/http/czt_timed/index.html @@ -0,0 +1,12 @@ + + LalkaChat + + + + + + + + +
+ diff --git a/http/czt_timed/js/socket.js b/http/czt_timed/js/socket.js new file mode 100644 index 0000000..79b1d22 --- /dev/null +++ b/http/czt_timed/js/socket.js @@ -0,0 +1,271 @@ +var MAX_MESSAGES = 70; +var find_location = window.location.href; +var RegExp = /:(\d+)/; +var find_list = RegExp.exec(find_location.toString()); +var find_port = find_list[1]; +var ws_url = "ws://127.0.0.1:".concat(find_port, "/ws"); + +// Chat settings +var timeout = 180; +var loadHistory = false; + +var socket = new WebSocket(ws_url); + +var chatMessages; +socket.onopen = function() { + chatMessages = document.getElementById('ChatContainer'); +}; + +socket.onclose = function(event) { + if (event.wasClean) { + } else { + } +}; + +socket.onmessage = function(event) { + var incomingMessage = JSON.parse(event.data); + if(incomingMessage.hasOwnProperty('command')) { + runCommand(incomingMessage); + } + else { + if (loadHistory) { + showMessage(incomingMessage); + } + else if (!incomingMessage.hasOwnProperty('history')) { + showMessage(incomingMessage); + } + } +}; + +socket.onerror = function(error) { +}; + +twitch_processEmoticons = function(message, emotes) { + if (!emotes) { + return message; + } + var placesToReplace = []; + for (var emote in emotes) { + for (var i = 0; i < emotes[emote]['emote_pos'].length; ++i) { + var range = emotes[emote]['emote_pos'][i]; + var rangeParts = range.split('-'); + placesToReplace.push({ + "emote_id": emotes[emote]['emote_id'], + "from": parseInt(rangeParts[0]), + "to": parseInt(rangeParts[1]) + 1 + }); + } + } + placesToReplace.sort(function(first, second) { + return second.from - first.from; + }); + for (var iPlace = 0; iPlace < placesToReplace.length; ++iPlace) { + var place = placesToReplace[iPlace]; + var emoticonRegex = message.substring(place.from, place.to); + // var url = "http://static-cdn.jtvnw.net/emoticons/v1/" + place.emote_id + "/1.0" + message = message.substring(0, place.from) + "$emoticon#" + place.emote_id + "$" + message.substring(place.to); + } + + return message; +}; + +htmlifyGGEmoticons = function(message, emotes) { + return message.replace(/:(\w+|\d+):/g, function (code, emote_key) { + for(var emote in emotes) { + if(emote_key == emotes[emote]['emote_id']) { + return ""; + } + } + return code; + }); +}; + +htmlifyBTTVEmoticons = function(message, emotes) { + return message.replace(/(^| )?(\S+)?( |$)/g, function (code, b1, emote_key, b2) { + for(var emote in emotes) { + if(emote_key == emotes[emote]['emote_id']) { + return ""; + } + } + return code; + }); +}; + +htmlifyTwitchEmoticons = function(message) { + return message.replace(/\$emoticon#(\d+)\$/g, function (code, emoteId) { + return ""; + }); +}; + +escapeHtml = (function () { + 'use strict'; + var chr = { '"': '"', '&': '&', '<': '<', '>': '>' }; + return function (text) { + return text.replace(/[\"&<>]/g, function (a) { return chr[a]; }); + }; +}()); + +function removeMessage(element) { + var elm = element || chatMessages.lastChild; + chatMessages.removeChild(elm); + } + +function updateMessages() { + if(chatMessages.children.length < MAX_MESSAGES) return; + var element = chatMessages.lastChild; + if(element.hasAttribute('timer-id')) { + var timerId = element.getAttribute('timer-id'); + window.clearTimeout(timerId); + } + removeMessage(element); + } + + +function showMessage(message) { + var badge_colors = 1; + + var elements = {}; + elements['message'] = document.createElement('div'); + elements.message.setAttribute('class', 'msg'); + if(timeout > 0) { + elements.message.setAttribute('timer-id', setTimeout(removeMessage, timeout * 1000, elements.message)); + } + + var messageJSON = message; + + if(messageJSON.hasOwnProperty('source')) { + //console.log("message has source " + messageJSON.source); + + elements.message['source'] = document.createElement('div'); + elements.message.source.setAttribute('class', 'msgSource'); + + elements.message.source['img'] = document.createElement('img'); + if(messageJSON.hasOwnProperty('source_icon')) { + elements.message.source.img.setAttribute('src', messageJSON.source_icon); + } + else{ + elements.message.source.img.setAttribute('src', '/img/sources/' + messageJSON.source + '.png'); + } + elements.message.source.img.setAttribute('class', 'imgSource'); + + elements.message.source.appendChild(elements.message.source.img); + elements.message.appendChild(elements.message.source); + } + + if(messageJSON.hasOwnProperty('levels')) { + elements.message['level'] = document.createElement('div'); + elements.message.level.setAttribute('class', 'msgLevel'); + + elements.message.level['img'] = document.createElement('img'); + elements.message.level.img.setAttribute('class', 'imgLevel'); + elements.message.level.img.setAttribute('src', messageJSON.levels.url); + + elements.message.level.appendChild(elements.message.level.img); + elements.message.appendChild(elements.message.level); + } + + if(messageJSON.hasOwnProperty('s_levels')) { + + for (i = 0; i < messageJSON.s_levels.length; i++) { + elements.message['s_level'] = document.createElement('div'); + elements.message.s_level.setAttribute('class', 'msgSLevel'); + + elements.message.s_level['img'] = document.createElement('img'); + elements.message.s_level.img.setAttribute('class', 'imgSLevel'); + elements.message.s_level.img.setAttribute('src', messageJSON.s_levels[i].url); + + elements.message.s_level.appendChild(elements.message.s_level.img); + elements.message.appendChild(elements.message.s_level); + } + } + + if(messageJSON.hasOwnProperty('badges')) { + + for (i = 0; i < messageJSON.badges.length; i++) { + elements.message['badge'] = document.createElement('div'); + elements.message.badge.setAttribute('class', 'msgBadge'); + + elements.message.badge['img'] = document.createElement('img'); + elements.message.badge.img.setAttribute('class', 'imgBadge'); + elements.message.badge.img.setAttribute('src', messageJSON.badges[i].url); + + if(badge_colors) { + if(messageJSON.badges[i].badge == 'broadcaster') { + elements.message.badge.img.setAttribute('style', 'background-color: #e71818'); + } + else if(messageJSON.badges[i].badge == 'mod') { + elements.message.badge.img.setAttribute('style', 'background-color: #34ae0a'); + } + else if(messageJSON.badges[i].badge == 'turbo') { + elements.message.badge.img.setAttribute('style', 'background-color: #6441a5'); + } + } + elements.message.badge.appendChild(elements.message.badge.img); + elements.message.appendChild(elements.message.badge); + } + } + + if(messageJSON.hasOwnProperty('user')) { + // console.log("message has user " + messageJSON.user); + elements.message['user'] = document.createElement('div'); + elements.message.user.setAttribute('class', 'msgUser'); + var addString = messageJSON.user; + + if (messageJSON.hasOwnProperty('msg_type')) { + if (messageJSON.msg_type == 'pubmsg') { + addString += ": " + } + } + else { + addString += ": " + } + + elements.message.user.appendChild(document.createTextNode(addString)); + + elements.message.appendChild(elements.message.user); + } + + if(messageJSON.hasOwnProperty('text')) { + // console.log("message has text " + messageJSON.text); + elements.message['text'] = document.createElement('div'); + if(messageJSON.source == 'sy') { + elements.message.text.setAttribute('class', 'msgTextSystem'); + } + else if(messageJSON.hasOwnProperty('pm') && messageJSON.pm == true) { + elements.message.text.setAttribute('class', 'msgTextPriv'); + } + else if(messageJSON.hasOwnProperty('mention') && messageJSON.mention == true){ + elements.message.text.setAttribute('class', 'msgTextMention'); + } + else { + elements.message.text.setAttribute('class', 'msgText'); + } + + if(messageJSON.source == 'tw') { + messageJSON.text = htmlifyTwitchEmoticons(escapeHtml(twitch_processEmoticons(messageJSON.text, messageJSON.emotes))); + if(messageJSON.hasOwnProperty('bttv_emotes')) { + messageJSON.text = htmlifyBTTVEmoticons(messageJSON.text, messageJSON.bttv_emotes); + } + } + else if(messageJSON.source == 'gg') { + messageJSON.text = htmlifyGGEmoticons(messageJSON.text, messageJSON.emotes) + } + else if(messageJSON.source == 'fs') { + messageJSON.text = htmlifyGGEmoticons(escapeHtml(messageJSON.text), messageJSON.emotes) + } + + // elements.message.text.appendChild(document.createTextNode(messageJSON.text)); + elements.message.text.innerHTML = messageJSON.text; + + elements.message.appendChild(elements.message.text); + + } + document.getElementById('ChatContainer').appendChild(elements.message); + updateMessages(); +} + +function runCommand(message) { + if(message.command == 'reload'){ + window.location.reload(); + } +} \ No newline at end of file diff --git a/http/czt_timed/levels.xml b/http/czt_timed/levels.xml new file mode 100644 index 0000000..5ae2fbf --- /dev/null +++ b/http/czt_timed/levels.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main.py b/main.py index 5f6fb7e..36bcccb 100644 --- a/main.py +++ b/main.py @@ -5,14 +5,18 @@ import Queue import messaging import gui -import thread import sys import logging import logging.config -from modules.helpers.parser import self_heal -from modules.helpers.system import load_translations_keys - - +import requests +import semantic_version +import locale +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.system import load_translations_keys + +VERSION = '0.3.0' +SEM_VERSION = semantic_version.Version(VERSION) if hasattr(sys, 'frozen'): PYTHON_FOLDER = os.path.dirname(sys.executable) else: @@ -21,7 +25,6 @@ CONF_FOLDER = os.path.join(PYTHON_FOLDER, "conf") MODULE_FOLDER = os.path.join(PYTHON_FOLDER, "modules") MAIN_CONF_FILE = os.path.join(CONF_FOLDER, "config.cfg") -HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") GUI_TAG = 'gui' LOG_FOLDER = os.path.join(PYTHON_FOLDER, "logs") @@ -30,30 +33,54 @@ LOG_FILE = os.path.join(LOG_FOLDER, 'chat_log.log') LOG_FORMAT = logging.Formatter("%(asctime)s [%(name)s] [%(levelname)s] %(message)s") -root_logger = logging.getLogger() -root_logger.setLevel(level=logging.INFO) -file_handler = logging.FileHandler(LOG_FILE) -file_handler.setFormatter(LOG_FORMAT) -root_logger.addHandler(file_handler) +LANGUAGE_DICT = { + 'en_US': 'en', + 'en_GB': 'en', + 'ru_RU': 'ru' +} -console_handler = logging.StreamHandler() -console_handler.setFormatter(LOG_FORMAT) -root_logger.addHandler(console_handler) -logger = logging.getLogger('main') +def get_update(): + github_url = "https://api.github.com/repos/DeForce/LalkaChat/releases" + try: + update_json = requests.get(github_url) + if update_json.status_code == 200: + update = False + update_url = None + update_list = update_json.json() + for update_item in update_list: + if semantic_version.Version.coerce(update_item['tag_name'].lstrip('v')) > SEM_VERSION: + update = True + update_url = update_item['html_url'] + return update, update_url + except Exception as exc: + log.info("Got exception: {0}".format(exc)) + return False, None + + +def get_language(): + local_name, local_encoding = locale.getdefaultlocale() + return LANGUAGE_DICT.get(local_name, 'en') def init(): + def close(): + if window: + window.gui.on_close('Closing Program from console') + else: + os._exit(0) # For system compatibility, loading chats - loaded_modules = {} + loaded_modules = OrderedDict() gui_settings = {} + window = None # Creating dict with folder settings main_config = {'root_folder': PYTHON_FOLDER, 'conf_folder': CONF_FOLDER, 'main_conf_file': MAIN_CONF_FILE, 'main_conf_file_loc': MAIN_CONF_FILE, - 'main_conf_file_name': ''.join(os.path.basename(MAIN_CONF_FILE).split('.')[:-1])} + 'main_conf_file_name': ''.join(os.path.basename(MAIN_CONF_FILE).split('.')[:-1]), + 'update': False} if not os.path.isdir(MODULE_FOLDER): logging.error("Was not able to find modules folder, check you installation") @@ -62,100 +89,97 @@ def init(): # Trying to load config file. # Create folder if doesn't exist if not os.path.isdir(CONF_FOLDER): - logger.error("Could not find {0} folder".format(CONF_FOLDER)) + log.error("Could not find {0} folder".format(CONF_FOLDER)) try: os.mkdir(CONF_FOLDER) except: - logger.error("Was unable to create {0} folder.".format(CONF_FOLDER)) + log.error("Was unable to create {0} folder.".format(CONF_FOLDER)) exit() - logger.info("Loading basic configuration") - main_config_dict = [ - {'gui_information': { - 'category': 'main'}}, - {'language__gui': { - 'for': 'language', + 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'] = OrderedDict() + main_config_dict['gui']['show_hidden'] = False + main_config_dict['gui']['gui'] = True + main_config_dict['gui']['on_top'] = True + main_config_dict['gui']['reload'] = None + main_config_dict['language'] = get_language() + + main_config_gui = { + 'language': { 'view': 'choose_single', 'check_type': 'dir', 'check': 'translations' - }}, - {'gui': { - 'show_hidden': True, - 'gui': True, - 'on_top': True, - 'reload': None - }}, - {'style__gui': { - 'check': 'http', - 'check_type': 'dir', - 'for': 'style', - 'view': 'choose_single'}}, - {'style': 'czt'}, - {'language': 'en'} - ] + }, + 'non_dynamic': ['language.list_box', 'gui.*'] + } config = self_heal(MAIN_CONF_FILE, main_config_dict) # Adding config for main module - loaded_modules['config'] = {'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} - - gui_settings['gui'] = config.get(GUI_TAG, 'gui') - gui_settings['on_top'] = config.get(GUI_TAG, 'gui') - gui_settings['language'], null_element = config.items('language')[0] - gui_settings['show_hidden'] = config.get(GUI_TAG, 'show_hidden') - # Fallback if style folder not found - fallback_style = 'czt' - if len(config.items('style')) > 0: - style, null_element = config.items('style')[0] - path = os.path.abspath(os.path.join(HTTP_FOLDER, style)) - if os.path.exists(path): - gui_settings['style'] = style - else: - gui_settings['style'] = fallback_style - else: - gui_settings['style'] = fallback_style - loaded_modules['config']['http_folder'] = os.path.join(HTTP_FOLDER, gui_settings['style']) - - logger.info("Loading Messaging Handler") - logger.info("Loading Queue for message handling") + loaded_modules['main'] = {'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, + 'gui': main_config_gui} + + gui_settings['gui'] = main_config_dict[GUI_TAG].get('gui') + gui_settings['on_top'] = main_config_dict[GUI_TAG].get('on_top') + gui_settings['language'] = main_config_dict.get('language') + gui_settings['show_hidden'] = main_config_dict[GUI_TAG].get('show_hidden') + gui_settings['size'] = (main_config_dict['gui_information'].get('width'), + main_config_dict['gui_information'].get('height')) + + # Checking updates + log.info("Checking for updates") + loaded_modules['main']['update'], loaded_modules['main']['update_url'] = get_update() + if loaded_modules['main']['update']: + log.info("There is new update, please update!") + + # Starting modules + log.info("Loading Messaging Handler") + log.info("Loading Queue for message handling") # Creating queues for messaging transfer between chat threads queue = Queue.Queue() # Loading module for message processing... msg = messaging.Message(queue) - loaded_modules.update(msg.load_modules(main_config, loaded_modules['config'])) + loaded_modules.update(msg.load_modules(main_config, loaded_modules['main'])) msg.start() - logger.info("Loading Chats") + 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_tag = "chats" - chat_location = os.path.join(MODULE_FOLDER, "chats") - chat_conf_dict = [ - {'gui_information': { - 'category': 'main'}}, - {'chats__gui': { - 'for': 'chats', + 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_gui = { + 'chats': { 'view': 'choose_multiple', 'check_type': 'files', - 'check': 'modules/chats', - 'file_extension': False}}, - {'chats': {}} - ] - + 'check': os.path.sep.join(['modules', 'chat']), + 'file_extension': False}, + 'non_dynamic': ['chats.list_box']} chat_config = self_heal(chat_modules, chat_conf_dict) - loaded_modules['chat_modules'] = {'folder': CONF_FOLDER, 'file': chat_modules, - 'filename': ''.join(os.path.basename(chat_modules).split('.')[:-1]), - 'parser': chat_config} + loaded_modules['chat'] = {'folder': CONF_FOLDER, 'file': chat_modules, + 'filename': ''.join(os.path.basename(chat_modules).split('.')[:-1]), + 'parser': chat_config, + 'config': chat_conf_dict, + 'gui': chat_conf_gui} for module, settings in chat_config.items(chat_tag): - logger.info("Loading chat module: {0}".format(module)) + log.info("Loading chat module: {0}".format(module)) module_location = os.path.join(chat_location, module + ".py") if os.path.isfile(module_location): - logger.info("found {0}".format(module)) + log.info("found {0}".format(module)) # After module is find, we are initializing it. # Class should be named as in config # Also passing core folder to module so it can load it's own @@ -164,36 +188,54 @@ def init(): tmp = imp.load_source(module, module_location) chat_init = getattr(tmp, module) class_module = chat_init(queue, PYTHON_FOLDER) - loaded_modules[module] = class_module.conf_params - loaded_modules[module]['class'] = class_module + loaded_modules[module] = class_module.conf_params() else: - logger.error("Unable to find {0} module") + log.error("Unable to find {0} module") + + # Actually loading modules + for f_module, f_config in loaded_modules.iteritems(): + if 'class' in f_config: + f_config['class'].load_module(main_settings=main_config, loaded_modules=loaded_modules, + queue=queue) try: load_translations_keys(TRANSLATION_FOLDER, gui_settings['language']) except: - logger.exception("Failed loading translations") + log.exception("Failed loading translations") if gui_settings['gui']: - logger.info("Loading GUI Interface") + log.info("Loading GUI Interface") window = gui.GuiThread(gui_settings=gui_settings, - main_config=loaded_modules['config'], - loaded_modules=loaded_modules) + main_config=loaded_modules['main'], + loaded_modules=loaded_modules, + queue=queue) window.start() try: while True: console = raw_input("> ") - logger.info(console) + log.info(console) if console == "exit": - logger.info("Exiting now!") - thread.interrupt_main() + log.info("Exiting now!") + close() else: - logger.info("Incorrect Command") + log.info("Incorrect Command") except (KeyboardInterrupt, SystemExit): - logger.info("Exiting now!") - thread.interrupt_main() + log.info("Exiting now!") + close() except Exception as exc: - logger.info(exc) - + log.info(exc) if __name__ == '__main__': + root_logger = logging.getLogger() + # Logging level + root_logger.setLevel(level=logging.INFO) + file_handler = logging.FileHandler(LOG_FILE) + file_handler.setFormatter(LOG_FORMAT) + root_logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(LOG_FORMAT) + root_logger.addHandler(console_handler) + logging.getLogger('requests').setLevel(logging.ERROR) + + log = logging.getLogger('main') init() diff --git a/messaging.py b/messaging.py index e89dd4f..f1cc88b 100644 --- a/messaging.py +++ b/messaging.py @@ -5,14 +5,26 @@ import imp import operator import logging +from collections import OrderedDict -from modules.helpers.system import ModuleLoadException -from modules.helpers.parser import self_heal +from modules.helper.system import ModuleLoadException, THREADS +from modules.helper.parser import self_heal log = logging.getLogger('messaging') MODULE_PRI_DEFAULT = '100' +class MessageHandler(threading.Thread): + def __init__(self, queue, process): + self.queue = queue + self.process = process + threading.Thread.__init__(self) + + def run(self): + while True: + self.process(self.queue.get()) + + class Message(threading.Thread): def __init__(self, queue): super(self.__class__, self).__init__() @@ -22,27 +34,30 @@ def __init__(self, queue): self.msg_counter = 0 self.queue = queue self.module_tag = "modules.messaging" + self.threads = [] - def load_modules(self, config_dict, settings): + def load_modules(self, main_config, settings): log.info("Loading configuration file for messaging") - modules_list = {} - - conf_file = os.path.join(config_dict['conf_folder'], "messaging.cfg") - conf_dict = [ - {'gui_information': { - 'category': 'main'}}, - {'messaging__gui': {'check': 'modules/messaging', - 'check_type': 'files', - 'file_extension': False, - 'for': 'messaging', - 'view': 'choose_multiple'}}, - {'messaging': { - 'webchat': None}} - ] + modules_list = OrderedDict() + + conf_file = os.path.join(main_config['conf_folder'], "messaging_modules.cfg") + conf_dict = OrderedDict() + conf_dict['gui_information'] = {'category': 'messaging'} + conf_dict['messaging'] = {'webchat': None} + + conf_gui = { + 'messaging': {'check': 'modules/messaging', + 'check_type': 'files', + 'file_extension': False, + 'view': 'choose_multiple', + 'description': True}, + 'non_dynamic': ['messaging.*']} config = self_heal(conf_file, conf_dict) - modules_list['messaging_modules'] = {'folder': config_dict['conf_folder'], 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} + modules_list['messaging'] = {'folder': main_config['conf_folder'], 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui} modules = {} # Loading modules from cfg. @@ -52,17 +67,18 @@ def load_modules(self, config_dict, settings): # We load the module, and then we initalize it. # When writing your modules you should have class with the # same name as module name - join_path = [config_dict['root_folder']] + self.module_tag.split('.') + ['{0}.py'.format(module)] + join_path = [main_config['root_folder']] + self.module_tag.split('.') + ['{0}.py'.format(module)] file_path = os.path.join(*join_path) try: tmp = imp.load_source(module, file_path) class_init = getattr(tmp, module) - class_module = class_init(config_dict['conf_folder'], root_folder=config_dict['root_folder'], + class_module = class_init(main_config['conf_folder'], root_folder=main_config['root_folder'], main_settings=settings) - if 'id' in class_module.conf_params: - priority = class_module.conf_params['id'] + params = class_module.conf_params() + if 'id' in params: + priority = params['id'] else: priority = MODULE_PRI_DEFAULT @@ -71,14 +87,14 @@ def load_modules(self, config_dict, settings): else: modules[int(priority)] = [class_module] - modules_list[module] = class_module.conf_params - modules_list[module]['class'] = class_module + modules_list[module] = params except ModuleLoadException as exc: log.error("Unable to load module {0}".format(module)) sorted_module = sorted(modules.items(), key=operator.itemgetter(0)) for sorted_priority, sorted_list in sorted_module: for sorted_list_item in sorted_list: self.modules.append(sorted_list_item) + return modules_list def msg_process(self, message): @@ -93,8 +109,10 @@ def msg_process(self, message): # content so it can be passed to new module, or to pass to CLI for module in self.modules: - message = module.get_message(message, self.queue) + message = module.process_message(message, self.queue) def run(self): - while True: - self.msg_process(self.queue.get()) + for thread in range(THREADS): + self.threads.append(MessageHandler(self.queue, self.msg_process)) + self.threads[thread].start() + diff --git a/modules/chats/__init__.py b/modules/chat/__init__.py similarity index 100% rename from modules/chats/__init__.py rename to modules/chat/__init__.py diff --git a/modules/chats/goodgame.py b/modules/chat/goodgame.py similarity index 67% rename from modules/chats/goodgame.py rename to modules/chat/goodgame.py index 5612338..270aa0d 100644 --- a/modules/chats/goodgame.py +++ b/modules/chat/goodgame.py @@ -5,32 +5,37 @@ import Queue import re import logging -from modules.helpers.parser import self_heal +import time +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.system import system_message +from modules.helper.modules import ChatModule from ws4py.client.threadedclient import WebSocketClient logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('goodgame') SOURCE = 'gg' SOURCE_ICON = 'http://goodgame.ru/images/icons/favicon.png' -CONF_DICT = [ - {'gui_information': { - 'category': 'chat'}}, - {'config__gui': { - 'for': 'config', - 'hidden': 'socket'}}, - {'config': { - 'channel_name': 'CHANGE_ME', - 'socket': 'ws://chat.goodgame.ru:8081/chat/websocket'}} - ] +SYSTEM_USER = 'GoodGame' +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'chat'} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['channel_name'] = 'CHANGE_ME' +CONF_DICT['config']['socket'] = 'ws://chat.goodgame.ru:8081/chat/websocket' + +CONF_GUI = { + 'config': { + 'hidden': ['socket']}, + 'non_dynamic': ['config.*']} class GoodgameMessageHandler(threading.Thread): - def __init__(self, ws_class, queue, gg_queue, **kwargs): + def __init__(self, ws_class, **kwargs): super(self.__class__, self).__init__() self.ws_class = ws_class # type: GGChat self.daemon = True - self.message_queue = queue - self.gg_queue = gg_queue + self.message_queue = kwargs.get('queue') + self.gg_queue = kwargs.get('gg_queue') self.source = SOURCE self.nick = kwargs.get('nick') @@ -88,6 +93,8 @@ def process_message(self, msg): if re.match('^{0},'.format(self.nick).lower(), comp['text'].lower()): comp['pm'] = True self.message_queue.put(comp) + elif msg['type'] == 'success_join': + self.ws_class.system_message('Successfully joined channel {0}'.format(self.nick)) elif msg['type'] == 'error': log.info("Received error message: {0}".format(msg)) if msg['data']['errorMsg'] == 'Invalid channel id': @@ -96,17 +103,24 @@ def process_message(self, msg): class GGChat(WebSocketClient): - def __init__(self, ws, protocols=None, queue=None, ch_id=None, nick=None, **kwargs): - super(self.__class__, self).__init__(ws, protocols=protocols) + def __init__(self, ws, **kwargs): + super(self.__class__, self).__init__(ws, heartbeat_freq=kwargs.get('heartbeat_freq'), + protocols=kwargs.get('protocols')) # Received value setting. - self.ch_id = ch_id + self.ch_id = kwargs.get('ch_id') + self.queue = kwargs.get('queue') self.gg_queue = Queue.Queue() - message_handler = GoodgameMessageHandler(self, queue, self.gg_queue, nick=nick, **kwargs) + self.main_thread = kwargs.get('main_thread') + self.crit_error = False + + message_handler = GoodgameMessageHandler(self, gg_queue=self.gg_queue, **kwargs) message_handler.start() def opened(self): - log.info("Connection Succesfull") + success_msg = "Connection Successful" + log.info(success_msg) + self.system_message(success_msg) # Sending join channel command to goodgame websocket join = json.dumps({'type': "join", 'data': {'channel_id': self.ch_id, 'hidden': "true"}}, sort_keys=False) self.send(join) @@ -116,14 +130,19 @@ def opened(self): def closed(self, code, reason=None): log.info("Connection Closed Down") if 'INV_CH_ID' in reason: - pass + self.crit_error = True else: - self.connect() - + self.system_message("Connection died, trying to reconnect") + timer = threading.Timer(5.0, self.main_thread.connect) + timer.start() + def received_message(self, mes): # Deserialize message to json for easier parsing - message = json.loads(str(mes)) - self.gg_queue.put(message) + self.gg_queue.put(json.loads(str(mes))) + + def system_message(self, msg): + system_message(msg, self.queue, SOURCE, + icon=SOURCE_ICON, from_user=SYSTEM_USER) class GGThread(threading.Thread): @@ -132,7 +151,7 @@ def __init__(self, queue, address, nick): # Basic value setting. # Daemon is needed so when main programm exits # all threads will exit too. - self.daemon = "True" + self.daemon = True self.queue = queue self.address = address self.nick = nick @@ -176,32 +195,45 @@ def load_config(self): return True def run(self): - if self.load_config(): - # Connecting to goodgame websocket - ws = GGChat(self.address, protocols=['websocket'], queue=self.queue, ch_id=self.ch_id, nick=self.nick, - **self.kwargs) - ws.connect() - ws.run_forever() - + self.connect() -class goodgame: + def connect(self): + try_count = 0 + while True: + try_count += 1 + log.info("Connecting, try {0}".format(try_count)) + if self.load_config(): + # Connecting to goodgame websocket + ws = GGChat(self.address, protocols=['websocket'], queue=self.queue, ch_id=self.ch_id, nick=self.nick, + heartbeat_freq=30, main_thread=self, **self.kwargs) + try: + ws.connect() + ws.run_forever() + log.debug("Connection closed") + break + except Exception as exc: + log.exception(exc) + + +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") log.info("Initializing goodgame chat") conf_file = os.path.join(conf_folder, "goodgame.cfg") config = self_heal(conf_file, CONF_DICT) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} - - # Checking config file for needed variables - conf_tag = 'config' - address = config.get(conf_tag, 'socket') - channel_name = config.get(conf_tag, 'channel_name') - # ch_id + self._conf_params = {'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.host = CONF_DICT['config']['socket'] + self.channel_name = CONF_DICT['config']['channel_name'] + def load_module(self, *args, **kwargs): # Creating new thread with queue in place for messaging transfers - gg = GGThread(queue, address, channel_name) + gg = GGThread(self.queue, self.host, self.channel_name) gg.start() diff --git a/modules/chats/sc2tv.py b/modules/chat/sc2tv.py similarity index 64% rename from modules/chats/sc2tv.py rename to modules/chat/sc2tv.py index f554c41..0d75f76 100644 --- a/modules/chats/sc2tv.py +++ b/modules/chat/sc2tv.py @@ -5,36 +5,45 @@ import requests import os import logging +from collections import OrderedDict from ws4py.client.threadedclient import WebSocketClient -from modules.helpers.parser import self_heal +from modules.helper.modules import ChatModule +from modules.helper.parser import self_heal +from modules.helper.system import system_message logging.getLogger('requests').setLevel(logging.ERROR) log = logging.getLogger('sc2tv') SOURCE = 'fs' SOURCE_ICON = 'http://funstream.tv/build/images/icon_home.png' -CONF_DICT = [ - {'gui_information': { - 'category': 'chat'}}, - {'config__gui': { - 'for': 'config', - 'hidden': 'socket'}}, - {'config': { - 'channel_name': 'CHANGE_ME', - 'socket': 'ws://funstream.tv/socket.io/'}} - ] +SYSTEM_USER = 'Funstream' + +PING_DELAY = 30 + +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'chat'} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['channel_name'] = 'CHANGE_ME' +CONF_DICT['config']['socket'] = 'ws://funstream.tv/socket.io/' + +CONF_GUI = { + 'config': { + 'hidden': ['socket']}, + 'non_dynamic': ['config.*']} class FsChat(WebSocketClient): - def __init__(self, ws, queue, channel_name, protocols=None, smiles=None): - super(self.__class__, self).__init__(ws, protocols=protocols) + def __init__(self, ws, queue, channel_name, **kwargs): + super(self.__class__, self).__init__(ws, protocols=kwargs.get('protocols', None)) # Received value setting. self.source = SOURCE self.queue = queue self.channel_name = channel_name + self.main_thread = kwargs.get('main_thread') # type: FsThread + self.crit_error = False self.channel_id = self.fs_get_id() - self.smiles = smiles + self.smiles = kwargs.get('smiles') self.smile_regex = ':(\w+|\d+):' # Because funstream API is fun, we have to iterate the @@ -57,9 +66,19 @@ def __init__(self, ws, queue, channel_name, protocols=None, smiles=None): def opened(self): log.info("Websocket Connection Succesfull") + self.fs_system_message("Connected") def closed(self, code, reason=None): - log.info("Websocket Connection Closed Down") + if reason == 'INV_CH_ID': + self.crit_error = True + else: + log.info("Websocket Connection Closed Down") + self.fs_system_message("Connection died, trying to reconnect") + timer = threading.Timer(5.0, self.main_thread.connect) + timer.start() + + def fs_system_message(self, message): + system_message(message, self.queue, source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER) @staticmethod def allow_smile(smile, subscriptions): @@ -101,6 +120,8 @@ def received_message(self, mes): # nickname of streamer we need to connect to. self.fs_join() self.fs_ping() + elif dict_item == 'status': + self.fs_system_message('Joined channel {0}'.format(self.channel_name)) elif dict_item == 'id': try: self.duplicates.index(message[dict_item]) @@ -133,17 +154,22 @@ def fs_get_id(self): # We get ID from POST request to funstream API, and it hopefuly # answers us the correct ID of the channel we need to connect to payload = "{'id': null, 'name': \"" + self.channel_name + "\"}" - request = requests.post("http://funstream.tv/api/user", data=payload) - if request.status_code == 200: - channel_id = json.loads(re.findall('{.*}', request.text)[0])['id'] - else: - error_message = request.json() - if 'message' in error_message: - log.error("Unable to get channel ID. {0}".format(error_message['message'])) + try: + request = requests.post("http://funstream.tv/api/user", data=payload, timeout=5) + if request.status_code == 200: + channel_id = json.loads(re.findall('{.*}', request.text)[0])['id'] + return channel_id else: - log.error("Unable to get channel ID. No message available") - channel_id = None - return channel_id + 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') + else: + log.error("Unable to get channel ID. No message available") + self.closed(0, 'INV_CH_ID') + except requests.ConnectionError: + log.info("Unable to get information from api") + return None def fs_join(self): # Because we need to iterate each message we iterate it! @@ -156,6 +182,7 @@ def fs_join(self): join = str(iter_sio) + "[\"/chat/join\", " + json.dumps({'channel': "stream/" + str(self.channel_id)}, sort_keys=False) + "]" self.send(join) + self.fs_system_message("Joining channel {0}".format(self.channel_name)) log.info("Joined channel {0}".format(self.channel_id)) def fs_ping(self): @@ -175,22 +202,12 @@ def __init__(self, ws): threading.Thread.__init__(self) self.daemon = "True" # Using main websocket - self.ws = ws + self.ws = ws # type: FsChat def run(self): - # Basically, if we are alive we send every 30 seconds special - # coded message, that is very hard to decode: - # - # 2 - # - # and they answer: - # - # 3 - # - # No idea why. - while True: + while not self.ws.terminated: self.ws.send("2") - time.sleep(30) + time.sleep(PING_DELAY) class FsThread(threading.Thread): @@ -206,42 +223,53 @@ def __init__(self, queue, socket, channel_name): self.smiles = [] def run(self): - # Let us get smiles for sc2tv - try: - smiles = requests.post('http://funstream.tv/api/smile') - 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.connect() + def connect(self): # Connecting to funstream websocket - ws = FsChat(self.socket, self.queue, self.channel_name, protocols=['websocket'], smiles=self.smiles) - ws.connect() - ws.run_forever() + try_count = 0 + 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") + ws = FsChat(self.socket, self.queue, self.channel_name, protocols=['websocket'], smiles=self.smiles, + main_thread=self) + if ws.crit_error: + log.critical("Got critical error, halting") + break + elif ws.channel_id and self.smiles: + ws.connect() + ws.run_forever() + break -class sc2tv: +class sc2tv(ChatModule): def __init__(self, queue, python_folder, **kwargs): + ChatModule.__init__(self) log.info("Initializing funstream chat") # Reading config from main directory. conf_folder = os.path.join(python_folder, "conf") conf_file = os.path.join(conf_folder, "sc2tv.cfg") config = self_heal(conf_file, CONF_DICT) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} - # Checking config file for needed variables - config_tag = 'config' - socket = config.get(config_tag, 'socket') - channel_name = config.get(config_tag, 'channel_name') - - # If any of the value are non-existent then exit the programm with error. - if (socket is None) or (channel_name is None): - log.critical("Config for funstream is not correct!") - - # Creating new thread with queue in place for messaging tranfers - fs = FsThread(queue, socket, channel_name) + self._conf_params = {'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.channel_name = CONF_DICT['config']['channel_name'] + + def load_module(self, *args, **kwargs): + # Creating new thread with queue in place for messaging transfers + fs = FsThread(self.queue, self.socket, self.channel_name) fs.start() diff --git a/modules/chats/twitch.py b/modules/chat/twitch.py similarity index 70% rename from modules/chats/twitch.py rename to modules/chat/twitch.py index 68e9414..90eca23 100644 --- a/modules/chats/twitch.py +++ b/modules/chat/twitch.py @@ -1,13 +1,17 @@ + import irc.client import threading import os import re import random import requests -import logging import logging.config import Queue -from modules.helpers.parser import self_heal +from collections import OrderedDict +import time +from modules.helper.parser import self_heal +from modules.helper.modules import ChatModule +from modules.helper.system import system_message logging.getLogger('irc').setLevel(logging.ERROR) logging.getLogger('requests').setLevel(logging.ERROR) @@ -19,18 +23,25 @@ NOT_FOUND = 'none' SOURCE = 'tw' SOURCE_ICON = 'https://www.twitch.tv/favicon.ico' -CONF_DICT = [ - {'gui_information': { - 'category': 'chat'}}, - {'config__gui': { - 'for': 'config', - 'hidden': 'host, port'}}, - {'config': { - 'bttv': 'true', - 'channel': 'CHANGE_ME', - 'host': 'irc.twitch.tv', - 'port': '6667'}} - ] +SYSTEM_USER = 'Twitch.TV' + +PING_DELAY = 30 + +CONF_DICT = OrderedDict() +CONF_DICT['gui_information'] = {'category': 'chat'} +CONF_DICT['config'] = OrderedDict() +CONF_DICT['config']['channel'] = 'CHANGE_ME' +CONF_DICT['config']['bttv'] = True +CONF_DICT['config']['host'] = 'irc.twitch.tv' +CONF_DICT['config']['port'] = 6667 +CONF_GUI = { + 'config': { + 'hidden': ['host', 'port']}, + 'non_dynamic': ['config.*']} + + +class TwitchUserError(Exception): + """Exception for twitch user error""" class TwitchMessageHandler(threading.Thread): @@ -110,19 +121,34 @@ def process_message(self, msg): comp['bttv_emotes'].append({'emote_id': bttv_smile['regex'], 'emote_url': 'http:{0}'.format(bttv_smile['url'])}) - if re.match('^@?{0}( |,)'.format(self.nick), comp['text'].lower()): + if re.match('^@?{0}[ ,]?'.format(self.nick), comp['text'].lower()): comp['pm'] = True self.message_queue.put(comp) +class TwitchPingHandler(threading.Thread): + def __init__(self, irc_connection): + threading.Thread.__init__(self) + self.irc_connection = irc_connection + + def run(self): + log.info("Ping started") + while self.irc_connection.connected: + self.irc_connection.ping("keep-alive") + time.sleep(PING_DELAY) + + class IRC(irc.client.SimpleIRCClient): def __init__(self, queue, channel, **kwargs): irc.client.SimpleIRCClient.__init__(self) # Basic variables, twitch channel are IRC so #channel self.channel = "#" + channel.lower() self.nick = channel.lower() + self.queue = queue self.twitch_queue = Queue.Queue() + self.tw_connection = None + self.main_class = kwargs.get('main_class') msg_handler = TwitchMessageHandler(queue, self.twitch_queue, nick=self.nick, @@ -131,19 +157,44 @@ def __init__(self, queue, channel, **kwargs): custom_badges=kwargs.get('custom_badges', {})) msg_handler.start() - def on_connect(self, connection, event): - log.info("Connected") + def system_message(self, message): + system_message(message, self.queue, + source=SOURCE, icon=SOURCE_ICON, from_user=SYSTEM_USER) + + def on_disconnect(self, connection, event): + log.info("Connection lost") + self.system_message("Connection died, trying to reconnect") + timer = threading.Timer(5.0, self.reconnect, + args=[self.main_class.host, self.main_class.port, self.main_class.nickname]) + timer.start() + + def reconnect(self, host, port, nickname): + try_count = 0 + while True: + try_count += 1 + log.info("Reconnecting, try {0}".format(try_count)) + try: + self.connect(host, port, nickname) + break + except Exception as exc: + log.exception(exc) def on_welcome(self, connection, event): log.info("Welcome Received, joining {0} channel".format(self.channel)) + self.tw_connection = connection + self.system_message('Joining channel {0}'.format(self.channel)) # After we receive IRC Welcome we send request for join and - # request for Capabilites (Twitch color, Display Name, + # request for Capabilities (Twitch color, Display Name, # Subscriber, etc) connection.join(self.channel) connection.cap('REQ', ':twitch.tv/tags') + ping_handler = TwitchPingHandler(connection) + ping_handler.start() def on_join(self, connection, event): - log.info("Joined {0} channel".format(self.channel)) + msg = "Joined {0} channel".format(self.channel) + log.info(msg) + self.system_message(msg) def on_pubmsg(self, connection, event): self.twitch_queue.put(event) @@ -182,19 +233,35 @@ def __init__(self, queue, host, port, channel, bttv_smiles, anon=True): self.nickname += str(random.randint(0, 9)) def run(self): - if self.load_config(): - # We are connecting via IRC handler. - irc_client = IRC(self.queue, self.channel, **self.kwargs) - irc_client.connect(self.host, self.port, self.nickname) - irc_client.start() + try_count = 0 + # We are connecting via IRC handler. + while True: + try_count += 1 + log.info("Connecting, try {0}".format(try_count)) + try: + if self.load_config(): + irc_client = IRC(self.queue, self.channel, main_class=self, **self.kwargs) + irc_client.connect(self.host, self.port, self.nickname) + irc_client.start() + log.info("Connection closed") + break + except TwitchUserError: + log.critical("Unable to find twitch user, please fix") + break + except Exception as exc: + log.exception(exc) def load_config(self): try: request = requests.get("https://api.twitch.tv/kraken/channels/{0}".format(self.channel), headers=headers) if request.status_code == 200: log.info("Channel found, continuing") + elif request.status_code == 404: + raise TwitchUserError else: raise Exception("Not successful status code: {0}".format(request.status_code)) + except TwitchUserError: + raise TwitchUserError except Exception as exc: log.error("Unable to get channel ID, error: {0}\nArgs: {1}".format(exc.message, exc.args)) return False @@ -247,8 +314,9 @@ def load_config(self): return True -class twitch: +class twitch(ChatModule): def __init__(self, queue, python_folder, **kwargs): + ChatModule.__init__(self) log.info("Initializing twitch chat") # Reading config from main directory. @@ -256,18 +324,19 @@ def __init__(self, queue, python_folder, **kwargs): conf_file = os.path.join(conf_folder, "twitch.cfg") config = self_heal(conf_file, CONF_DICT) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} - - config.read(conf_file) - # Checking config file for needed variables - config_tag = 'config' - host = config.get(config_tag, 'host') - port = int(config.get(config_tag, 'port')) - channel = config.get(config_tag, 'channel') - bttv_smiles = config.get(config_tag, 'bttv') - - # Creating new thread with queue in place for messaging tranfers - tw = twThread(queue, host, port, channel, bttv_smiles) + self._conf_params = {'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.host = CONF_DICT['config']['host'] + self.port = CONF_DICT['config']['port'] + self.channel = CONF_DICT['config']['channel'] + self.bttv = CONF_DICT['config']['bttv'] + + def load_module(self, *args, **kwargs): + # Creating new thread with queue in place for messaging transfers + tw = twThread(self.queue, self.host, self.port, self.channel, self.bttv) tw.start() diff --git a/modules/helpers/__init__.py b/modules/helper/__init__.py similarity index 100% rename from modules/helpers/__init__.py rename to modules/helper/__init__.py diff --git a/modules/helper/modules.py b/modules/helper/modules.py new file mode 100644 index 0000000..ab02447 --- /dev/null +++ b/modules/helper/modules.py @@ -0,0 +1,32 @@ + + +class BaseModule: + def __init__(self, *args, **kwargs): + self._conf_params = {} + + def conf_params(self): + params = self._conf_params + params['class'] = self + return params + + def load_module(self, *args, **kwargs): + pass + + def gui_button_press(self, *args): + pass + + def apply_settings(self): + pass + + +class MessagingModule(BaseModule): + def __init__(self, *args, **kwargs): + BaseModule.__init__(self, *args, **kwargs) + + def process_message(self, message, queue, **kwargs): + return message + + +class ChatModule(BaseModule): + def __init__(self, *args, **kwargs): + BaseModule.__init__(self, *args, **kwargs) diff --git a/modules/helper/parser.py b/modules/helper/parser.py new file mode 100644 index 0000000..c1e2187 --- /dev/null +++ b/modules/helper/parser.py @@ -0,0 +1,56 @@ +import os +from ConfigParser import RawConfigParser +from collections import OrderedDict + + +def self_heal(conf_file, heal_dict): + heal_config = get_config(conf_file) + for section, section_value in heal_dict.items(): + if not heal_config.has_section(section): + heal_config.add_section(section) + if type(section_value) in [OrderedDict, dict]: + if section_value: + for item, value in section_value.items(): + if not heal_config.has_option(section, item): + heal_config.set(section, item, value) + for item, value in heal_config.items(section): + heal_dict[section][item] = return_type(value) + else: + heal_dict[section] = OrderedDict() + for item, value in heal_config.items(section): + heal_dict[section][item] = value + else: + if len(heal_config.items(section)) != 1: + for r_item, r_value in heal_config.items(section): + heal_config.remove_option(section, r_item) + heal_config.set(section, section_value) + else: + heal_dict[section] = heal_config.items(section)[0][0] + + heal_config.write(open(conf_file, 'w')) + return heal_config + + +def return_type(item): + if item: + try: + if isinstance(item, bool): + return item + return int(item) + except: + if item.lower() == 'true': + return True + elif item.lower() == 'false': + return False + return item + + +def get_config(conf_file): + dir_name = os.path.dirname(conf_file) + if not os.path.exists(dir_name): + os.makedirs(os.path.dirname(conf_file)) + + heal_config = RawConfigParser(allow_no_value=True) + if os.path.exists(conf_file): + heal_config.read(conf_file) + return heal_config diff --git a/modules/helpers/system.py b/modules/helper/system.py similarity index 81% rename from modules/helpers/system.py rename to modules/helper/system.py index 727fafe..92f27ce 100644 --- a/modules/helpers/system.py +++ b/modules/helper/system.py @@ -4,6 +4,8 @@ log = logging.getLogger('system') +THREADS = 2 + SOURCE = 'sy' SOURCE_USER = 'System' SOURCE_ICON = '/img/sources/lalka_cup.png' @@ -56,14 +58,14 @@ def load_language(language_folder): log.warning("Unable to load language {0}".format(language_item)) -def find_key_translation(item, length=0, wildcard=1): - translation = TRANSLATIONS.get(item, item) - if item == translation: - if wildcard < length: - translation = find_key_translation(MODULE_KEY.join(['*'] + item.split(MODULE_KEY)[-wildcard:]), - length=length, wildcard=wildcard+1) +def find_key_translation(item): + translation = TRANSLATIONS.get(item) + if translation is None: + if len(item.split(MODULE_KEY)) > 2: + wildcard_item = MODULE_KEY.join([split for split in item.split(MODULE_KEY) if split != '*'][1:]) + return find_key_translation('*{0}{1}'.format(MODULE_KEY, wildcard_item)) else: - return translation + return item return translation @@ -78,11 +80,11 @@ def translate_key(item): item_no_flags = item.split('/')[0] old_item = item_no_flags - translation = find_key_translation(item_no_flags, length=len(item_no_flags.split(MODULE_KEY))) + translation = find_key_translation(item_no_flags) if re.match('\*', translation): return old_item - return translation.decode('utf-8') + return translation.replace('\\n', '\n').decode('utf-8') def translate(text): diff --git a/modules/helpers/parser.py b/modules/helpers/parser.py deleted file mode 100644 index b449301..0000000 --- a/modules/helpers/parser.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -from ConfigParser import RawConfigParser - - -def self_heal(conf_file, heal_dict): - heal_config = get_config(conf_file) - for heal_item in heal_dict: - section, section_value = heal_item.iteritems().next() - if not heal_config.has_section(section): - heal_config.add_section(section) - if type(section_value) == dict: - for item, value in section_value.items(): - if not heal_config.has_option(section, item): - heal_config.set(section, item, value) - else: - if len(heal_config.items(section)) != 1: - for r_item, r_value in heal_config.items(section): - heal_config.remove_option(section, r_item) - heal_config.set(section, section_value) - - heal_config.write(open(conf_file, 'w')) - return heal_config - - -def get_config(conf_file): - dir_name = os.path.dirname(conf_file) - if not os.path.exists(dir_name): - os.makedirs(os.path.dirname(conf_file)) - - heal_config = RawConfigParser(allow_no_value=True) - if os.path.exists(conf_file): - heal_config.read(conf_file) - return heal_config diff --git a/modules/messaging/blacklist.py b/modules/messaging/blacklist.py index 53002e5..cce810e 100644 --- a/modules/messaging/blacklist.py +++ b/modules/messaging/blacklist.py @@ -2,42 +2,54 @@ # -*- coding: utf-8 -*- import re import os -from modules.helpers.parser import self_heal +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.modules import MessagingModule DEFAULT_PRIORITY = 30 -class blacklist: +class blacklist(MessagingModule): users = {} words = {} def __init__(self, conf_folder, **kwargs): + MessagingModule.__init__(self) # Dwarf professions. conf_file = os.path.join(conf_folder, "blacklist.cfg") - config_dict = [ - {'gui_information': { - 'category': 'messaging', - 'id': DEFAULT_PRIORITY}}, - {'main': { - 'message': u'ignored message'}}, - {'users__gui': { - 'for': 'users_hide, users_block', - 'view': 'list', - 'addable': 'true'}}, - {'users_hide': {}}, - {'users_block': { - 'announce': None}}, - {'words__gui': { - 'for': 'words_hide, words_block', - 'addable': True, - 'view': 'list'}} - ] - config = self_heal(conf_file, config_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id')} + # Ordered because order matters + conf_dict = OrderedDict() + conf_dict['gui_information'] = { + 'category': 'messaging', + 'id': DEFAULT_PRIORITY} + conf_dict['main'] = {'message': u'ignored message'} + conf_dict['users_hide'] = {} + conf_dict['users_block'] = {} + conf_dict['words_hide'] = {} + conf_dict['words_block'] = {} + + conf_gui = { + 'words_hide': { + 'addable': True, + 'view': 'list'}, + 'words_block': { + 'addable': True, + 'view': 'list'}, + 'users_hide': { + 'view': 'list', + 'addable': 'true'}, + 'users_block': { + 'view': 'list', + 'addable': 'true'}, + 'non_dynamic': ['main.*']} + config = self_heal(conf_file, conf_dict) + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': config.get('gui_information', 'id'), + 'config': OrderedDict(conf_dict), + 'gui': conf_gui} for item in config.sections(): for param, value in config.items(item): @@ -53,16 +65,18 @@ def __init__(self, conf_folder, **kwargs): elif item == 'words_block': self.words[param] = 'b' - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): if message: - user = self.process_user(message) + if 'command' in message: + return message + user = self.blacklist_user_handler(message) # True = Hide, False = Del, None = Do Nothing if user: message['text'] = self.message elif user is False: return - words = self.process_message(message) + words = self.blacklist_message_handler(message) if words: message['text'] = self.message elif words is False: @@ -70,7 +84,7 @@ def get_message(self, message, queue): return message - def process_user(self, message): + def blacklist_user_handler(self, message): user = message.get('user').lower() if user in self.users: if self.users[user] == 'h': @@ -79,7 +93,7 @@ def process_user(self, message): return False return None - def process_message(self, message): + def blacklist_message_handler(self, message): for word in self.words: if re.search(word, message['text'].encode('utf-8')): if self.words[word] == 'h': diff --git a/modules/messaging/c2b.py b/modules/messaging/c2b.py index b0befda..451322f 100644 --- a/modules/messaging/c2b.py +++ b/modules/messaging/c2b.py @@ -4,7 +4,9 @@ import os import random import re -from modules.helpers.parser import self_heal +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.modules import MessagingModule DEFAULT_PRIORITY = 10 log = logging.getLogger('c2b') @@ -30,26 +32,30 @@ def twitch_replace_indexes(filter_name, text, filter_size, replace_size, emotes_ return emotes -class c2b: +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 = [ - {'gui_information': { - 'category': 'messaging', - 'id': DEFAULT_PRIORITY}}, - {'config__gui': { - 'for': 'config', - 'addable': 'true', - 'view': 'list_dual'}}, - {'config': {}} - ] + 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 = self_heal(conf_file, conf_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id')} + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': config.get('gui_information', 'id'), + 'config': conf_dict, + 'gui': conf_gui} tag_config = 'config' self.f_items = [] @@ -58,10 +64,12 @@ def __init__(self, conf_folder, **kwargs): f_item['replace'] = [item.strip().decode('utf-8') for item in f_item['replace']] self.f_items.append(f_item) - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): # Replacing the message if needed. # Please do the needful if message: + if 'command' in message: + return message for replace in self.f_items: if replace['filter'] in message['text']: replace_word = random.choice(replace['replace']) diff --git a/modules/messaging/df.py b/modules/messaging/df.py index 1a55514..8f05ad8 100644 --- a/modules/messaging/df.py +++ b/modules/messaging/df.py @@ -2,36 +2,39 @@ # -*- coding: utf-8 -*- import re import os -from modules.helpers.parser import self_heal +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.modules import MessagingModule -class df: + +class df(MessagingModule): def __init__(self, conf_folder, **kwargs): + MessagingModule.__init__(self) # Dwarf professions. conf_file = os.path.join(conf_folder, "df.cfg") - conf_dict = [ - {'gui_information': { - 'category': 'messaging'}}, - {'grep': { - 'symbol': '#', - 'file': 'logs/df.txt' - }}, - {'prof__gui': { - 'for': 'prof', + + 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'] = {'nothing': '([Нн]икто|[Nn]othing|\w*)'} + + conf_gui = { + 'prof': { 'view': 'list_dual', - 'addable': True - }}, - {'prof': { - 'Nothing': '([Нн]икто|[Nn]othing|\w*)' - }} - ] + 'addable': True}, + 'non_dynamic': ['grep.*']} config = self_heal(conf_file, conf_dict) grep_tag = 'grep' prof_tag = 'prof' - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui} self.symbol = config.get(grep_tag, 'symbol') self.file = config.get(grep_tag, 'file') @@ -56,8 +59,10 @@ def write_to_file(self, message): with open(self.file, 'a') as a_file: a_file.write("{0},{1}\n".format(message['user'], message['text'])) - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): if message: + if 'command' in message: + return message for regexp in self.prof: if re.search(regexp[1], message['text']): comp = {'user': message['user'], 'text': regexp[0]} diff --git a/modules/messaging/levels.py b/modules/messaging/levels.py index 0b14e17..dd3972c 100644 --- a/modules/messaging/levels.py +++ b/modules/messaging/levels.py @@ -6,59 +6,90 @@ import random import sqlite3 import xml.etree.ElementTree as ElementTree -import jinja2 -from modules.helpers.parser import self_heal -from modules.helpers.system import system_message, ModuleLoadException +from collections import OrderedDict +import datetime -logger = logging.getLogger('levels') +from modules.helper.parser import self_heal +from modules.helper.system import system_message, ModuleLoadException +from modules.helper.modules import MessagingModule +log = logging.getLogger('levels') -class levels: + +class levels(MessagingModule): @staticmethod def create_db(db_location): if not os.path.exists(db_location): db = sqlite3.connect(db_location) cursor = db.cursor() - logger.info("Creating new tables for levels") + log.info("Creating new tables for levels") cursor.execute('CREATE TABLE UserLevels (User, "Experience")') cursor.close() db.commit() db.close() def __init__(self, conf_folder, **kwargs): - # Creating filter and replace strings. - main_settings = kwargs.get('main_settings') + MessagingModule.__init__(self) conf_file = os.path.join(conf_folder, "levels.cfg") - conf_dict = [ - {'gui_information': { - 'category': u'messaging'}}, - {'config': { - 'message': u'{0} has leveled up, now he is {1}', - 'db': u'levels.db', - 'experience': u'geometrical', - 'exp_for_level': 200}} - ] + 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.*'], + 'config': { + 'experience': { + 'view': 'dropdown', + 'choices': ['static', 'geometrical', 'random']}}} config = self_heal(conf_file, conf_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} - tag_config = 'config' + self._conf_params = {'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 + self.experience = None + self.exp_for_level = None + self.exp_for_message = None + self.filename = None + self.levels = None + self.special_levels = None + self.db_location = None + self.message = None + self.decrease_window = None + self.threshold_users = None + + def load_module(self, *args, **kwargs): + main_settings = kwargs.get('main_settings') + loaded_modules = kwargs.get('loaded_modules') + if 'webchat' not in loaded_modules: + raise ModuleLoadException("Unable to find webchat module that is needed for level module") + + conf_folder = self._conf_params['folder'] + conf_dict = self._conf_params['config'] self.conf_folder = conf_folder - self.experience = config.get(tag_config, 'experience') - self.exp_for_level = int(config.get(tag_config, 'exp_for_level')) - self.exp_for_message = 1 - self.filename = os.path.abspath(os.path.join(main_settings['http_folder'], 'levels.xml')) + 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')) + self.filename = os.path.abspath(os.path.join(loaded_modules['webchat']['style_location'], 'levels.xml')) self.levels = [] self.special_levels = {} - self.db_location = os.path.join(conf_folder, config.get(tag_config, 'db')) - self.message = config.get(tag_config, 'message').decode('utf-8') + self.db_location = os.path.join(conf_dict['config'].get('db')) + self.message = conf_dict['config'].get('message').decode('utf-8') + self.decrease_window = int(conf_dict['config'].get('decrease_window')) + self.threshold_users = {} # Load levels if not os.path.exists(self.filename): - logger.error("{0} not found, generating from template".format(self.filename)) + log.error("{0} not found, generating from template".format(self.filename)) raise ModuleLoadException("{0} not found, generating from template".format(self.filename)) if self.experience == 'random': @@ -89,13 +120,14 @@ def set_level(self, user, queue): user_select = cursor.execute('SELECT User, Experience FROM UserLevels WHERE User = ?', [user]) user_select = user_select.fetchall() - experience = 1 + experience = self.exp_for_message + exp_to_add = self.calculate_experience(user) if len(user_select) == 1: row = user_select[0] - experience = int(row[1]) + self.exp_for_message + experience = int(row[1]) + exp_to_add cursor.execute('UPDATE UserLevels SET Experience = ? WHERE User = ? ', [experience, user]) elif len(user_select) > 1: - logger.error("Select yielded more than one User") + log.error("Select yielded more than one User") else: cursor.execute('INSERT INTO UserLevels VALUES (?, ?)', [user, experience]) db.commit() @@ -119,8 +151,10 @@ def set_level(self, user, queue): cursor.close() return self.levels[max_level] - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): if message: + if 'command' in message: + 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']] @@ -131,3 +165,11 @@ def get_message(self, message, queue): message['levels'] = self.set_level(message['user'], queue) return message + + def calculate_experience(self, user): + exp_to_add = self.exp_for_message + if user in self.threshold_users: + multiplier = (datetime.datetime.now() - self.threshold_users[user]).seconds / float(self.decrease_window) + exp_to_add *= multiplier if multiplier <= 1 else 1 + self.threshold_users[user] = datetime.datetime.now() + return exp_to_add diff --git a/modules/messaging/logger.py b/modules/messaging/logger.py index ec02376..e438b22 100644 --- a/modules/messaging/logger.py +++ b/modules/messaging/logger.py @@ -2,30 +2,38 @@ # -*- coding: utf-8 -*- import os import datetime -from modules.helpers.parser import self_heal +from collections import OrderedDict + +from modules.helper.parser import self_heal +from modules.helper.modules import MessagingModule DEFAULT_PRIORITY = 20 -class logger(): +class logger(MessagingModule): def __init__(self, conf_folder, **kwargs): + MessagingModule.__init__(self) # Creating filter and replace strings. conf_file = os.path.join(conf_folder, "logger.cfg") - conf_dict = [ - {'gui_information': { - 'category': u'messaging', - 'id': DEFAULT_PRIORITY}}, - {'config': { - 'file_format': u'%Y-%m-%d', - 'logging': u'true', - 'message_date_format': u'%Y-%m-%d %H:%M:%S', - 'rotation': u'daily'}} - ] + 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 = self_heal(conf_file, conf_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id')} + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': config.get('gui_information', 'id'), + 'config': conf_dict, + 'gui': conf_gui} tag_config = 'config' @@ -40,8 +48,10 @@ def __init__(self, conf_folder, **kwargs): if not os.path.exists(self.destination): os.makedirs(self.destination) - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): if message: + if 'command' in message: + 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'), diff --git a/modules/messaging/mentions.py b/modules/messaging/mentions.py index 049f2d5..5b46b9a 100644 --- a/modules/messaging/mentions.py +++ b/modules/messaging/mentions.py @@ -2,27 +2,36 @@ # -*- coding: utf-8 -*- import os import re -from modules.helpers.parser import self_heal +from collections import OrderedDict +from modules.helper.parser import self_heal +from modules.helper.modules import MessagingModule -class mentions(): + +class mentions(MessagingModule): def __init__(self, conf_folder, **kwargs): + MessagingModule.__init__(self) # Creating filter and replace strings. conf_file = os.path.join(conf_folder, "mentions.cfg") - conf_dict = [ - {'gui_information': { - 'category': 'messaging'}}, - {'config__gui': { + conf_dict = OrderedDict() + conf_dict['gui_information'] = {'category': 'messaging'} + conf_dict['mentions'] = {} + conf_dict['address'] = {} + + conf_gui = { + 'mentions': { 'addable': 'true', - 'for': 'mentions, address', - 'view': 'list'}}, - {'mentions': {}}, - {'address': {}} - ] + 'view': 'list'}, + 'address': { + 'addable': 'true', + 'view': 'list'}, + 'non_dynamic': ['mentions.*', 'address.*']} config = self_heal(conf_file, conf_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config} + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'config': conf_dict, + 'gui': conf_gui} mention_tag = 'mentions' address_tag = 'address' if config.has_section(mention_tag): @@ -31,22 +40,22 @@ def __init__(self, conf_folder, **kwargs): self.mentions = [] if config.has_section(address_tag): - self.addresses = [item for item, value in config.items(address_tag)] + self.addresses = [item.decode('utf-8').lower() for item, value in config.items(address_tag)] else: self.addresses = [] - def get_message(self, message, queue): + def process_message(self, message, queue, **kwargs): # Replacing the message if needed. # Please do the needful - if message is None: - return - else: + if message: + if 'command' in message: + return message for mention in self.mentions: if re.search(mention, message['text'].lower()): message['mention'] = True for address in self.addresses: - if re.match('^{0}(,| )'.format(address), message['text'].lower().encode('utf-8')): + if re.match(address, message['text'].lower()): message['pm'] = True if 'mention' in message and 'pm' in message: diff --git a/modules/messaging/webchat.py b/modules/messaging/webchat.py index bedb6ef..66f91b8 100644 --- a/modules/messaging/webchat.py +++ b/modules/messaging/webchat.py @@ -2,15 +2,24 @@ import threading import json import Queue +import socket import cherrypy import logging +from collections import OrderedDict +from jinja2 import Template from cherrypy.lib.static import serve_file from time import sleep from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool from ws4py.websocket import WebSocket -from modules.helpers.parser import self_heal +from modules.helper.parser import self_heal +from modules.helper.system import THREADS +from modules.helper.modules import MessagingModule +from gui import MODULE_KEY +from main import PYTHON_FOLDER, CONF_FOLDER DEFAULT_PRIORITY = 9001 +HISTORY_SIZE = 20 +HTTP_FOLDER = os.path.join(PYTHON_FOLDER, "http") s_queue = Queue.Queue() logging.getLogger('ws4py').setLevel(logging.ERROR) log = logging.getLogger('webchat') @@ -24,8 +33,9 @@ def __init__(self): def run(self): while True: message = s_queue.get() - cherrypy.engine.publish('add-history', message) cherrypy.engine.publish('websocket-broadcast', json.dumps(message)) + if 'command' not in message: + cherrypy.engine.publish('add-history', message) class FireFirstMessages(threading.Thread): @@ -38,12 +48,13 @@ def __init__(self, ws, history): def run(self): sleep(0.1) for item in self.history: - self.ws.send(json.dumps(item)) + if item: + self.ws.send(json.dumps(item)) class WebChatSocketServer(WebSocket): def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbeat_freq=None): - super(self.__class__, self).__init__(sock) + WebSocket.__init__(self, sock) self.clients = [] def opened(self): @@ -60,7 +71,7 @@ def __init__(self, bus): WebSocketPlugin.__init__(self, bus) self.clients = [] self.history = [] - self.history_size = 10 + self.history_size = HISTORY_SIZE def start(self): WebSocketPlugin.start(self) @@ -86,6 +97,7 @@ def del_client(self, addr): pass def add_history(self, message): + message['history'] = True self.history.append(message) if len(self.history) > self.history_size: self.history.pop(0) @@ -94,6 +106,18 @@ def get_history(self): return self.history +class CssRoot(object): + def __init__(self, http_folder, settings): + object.__init__(self) + self.http_folder = http_folder + self.settings = settings + + @cherrypy.expose() + def style_css(self): + with open(os.path.join(self.http_folder, 'css', 'style.css'), 'r') as css: + return Template(css.read()).render(**self.settings) + + class HttpRoot(object): def __init__(self, http_folder): object.__init__(self) @@ -120,10 +144,10 @@ def __init__(self, host, port, root_folder, **kwargs): self.port = port self.root_folder = root_folder self.style = kwargs.pop('style') + self.settings = kwargs.pop('settings') cherrypy.config.update({'server.socket_port': int(self.port), 'server.socket_host': self.host, - 'engine.autoreload.on': False - }) + 'engine.autoreload.on': False}) WebChatPlugin(cherrypy.engine).subscribe() cherrypy.tools.websocket = WebSocketTool() @@ -137,49 +161,118 @@ def run(self): cherrypy.log.access_log.propagate = False cherrypy.log.error_log.setLevel(logging.ERROR) - cherrypy.quickstart(HttpRoot(http_folder), '/', - config={'/ws': {'tools.websocket.on': True, - 'tools.websocket.handler_cls': WebChatSocketServer}, - '/js': {'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(http_folder, 'js')}, - '/css': {'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(http_folder, 'css')}, - '/img': {'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(http_folder, 'img')}}) + config = { + '/ws': {'tools.websocket.on': True, + 'tools.websocket.handler_cls': WebChatSocketServer}, + '/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(http_folder, 'js')}, + '/img': {'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(http_folder, 'img')}} + css_config = { + '/': {} + } -class webchat(): - def __init__(self, conf_folder, **kwargs): - main_settings = kwargs.get('main_settings') - conf_file = os.path.join(conf_folder, "webchat.cfg") - conf_dict = [ - {'gui_information': { - 'category': 'main', - 'id': DEFAULT_PRIORITY}}, - {'server': { - 'host': '127.0.0.1', - 'port': '8080'}}] + cherrypy.tree.mount(HttpRoot(http_folder), '', config) + cherrypy.tree.mount(CssRoot(http_folder, self.settings), '/css', css_config) + + cherrypy.engine.start() + cherrypy.engine.block() + + # cherrypy.quickstart(HttpRoot(http_folder), '/', + # config={'/ws': {'tools.websocket.on': True, + # 'tools.websocket.handler_cls': WebChatSocketServer}, + # '/js': {'tools.staticdir.on': True, + # 'tools.staticdir.dir': os.path.join(http_folder, 'js')}, + # '/img': {'tools.staticdir.on': True, + # 'tools.staticdir.dir': os.path.join(http_folder, 'img')}}) - config = self_heal(conf_file, conf_dict) - self.conf_params = {'folder': conf_folder, 'file': conf_file, - 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), - 'parser': config, - 'id': config.get('gui_information', 'id')} - tag_server = 'server' - host = config.get(tag_server, 'host') - port = config.get(tag_server, 'port') - style = main_settings['http_folder'] +def socket_open(host, port): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2) + return sock.connect_ex((host, int(port))) - self.conf_params['port'] = port - s_thread = SocketThread(host, port, conf_folder, style=style) - s_thread.start() +class webchat(MessagingModule): + def __init__(self, conf_folder, **kwargs): + MessagingModule.__init__(self) + 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'] = 'czt' + conf_dict['style_settings'] = { + 'font_size': 15 + } + conf_gui = { + 'style': { + 'check': 'http', + 'check_type': 'dir', + 'view': 'choose_single'}, + 'style_settings': { + 'font_size': {'view': 'spin', + 'min': 10, + 'max': 100}}, + 'non_dynamic': ['server.*']} - m_thread = MessagingThread() - m_thread.start() + config = self_heal(conf_file, conf_dict) - def get_message(self, message, queue): + fallback_style = 'czt' + path = os.path.abspath(os.path.join(HTTP_FOLDER, conf_dict['style'])) + if os.path.exists(path): + style_location = path + else: + style_location = os.path.join(HTTP_FOLDER, fallback_style) + + self._conf_params = {'folder': conf_folder, 'file': conf_file, + 'filename': ''.join(os.path.basename(conf_file).split('.')[:-1]), + 'parser': config, + 'id': config.get('gui_information', 'id'), + 'config': conf_dict, + 'gui': conf_gui, + 'host': conf_dict['server']['host'], + 'port': conf_dict['server']['port'], + 'style_location': style_location} + self.queue = None + self.message_threads = [] + + def load_module(self, *args, **kwargs): + self.queue = kwargs.get('queue') + conf_dict = self._conf_params + host = conf_dict['host'] + port = conf_dict['port'] + + if socket_open(host, port): + s_thread = SocketThread(host, port, CONF_FOLDER, style=self._conf_params['style_location'], + settings=self._conf_params['config']['style_settings']) + s_thread.start() + + for thread in range(THREADS+5): + self.message_threads.append(MessagingThread()) + self.message_threads[thread].start() + else: + log.error("Port is already used, please change webchat port") + + def reload_chat(self): + self.queue.put({'command': 'reload'}) + + def apply_settings(self): + self.reload_chat() + + def gui_button_press(self, gui_module, event, list_keys): + log.debug("Received button press for id {0}".format(event.GetId())) + keys = MODULE_KEY.join(list_keys) + if keys == 'menu.reload': + self.reload_chat() + event.Skip() + + def process_message(self, message, queue, **kwargs): if message: if 'flags' in message: if message['flags'] == 'hidden': diff --git a/setup.py b/setup.py index 6674076..720ea60 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ from distutils.core import setup +from main import VERSION setup( name='LalkaChat', - version='0.2.0', + version=VERSION, packages=['', 'modules', 'modules.helpers', 'modules.messaging'], - requires=['requests', 'cherrypy', 'ws4py', 'irc', 'wxpython', 'cefpython3'], + requires=['requests', 'cherrypy', 'ws4py', 'irc', 'wxpython', 'cefpython3', 'semantic_version', 'jinja2'], url='https://github.com/DeForce/LalkaChat', license='', author='CzT/DeForce', diff --git a/translations/en/blacklist.key b/translations/en/blacklist.key index 0a52a10..03f2695 100644 --- a/translations/en/blacklist.key +++ b/translations/en/blacklist.key @@ -1,4 +1,5 @@ blacklist = Blacklist +blacklist.description = Blacklist module allows you to block or hide unwanted messages or users blacklist.main = Main Configuration blacklist.main.message = Message replace blacklist.users_hide = Hide users diff --git a/translations/en/c2b.key b/translations/en/c2b.key index ec7447f..ade71c8 100644 --- a/translations/en/c2b.key +++ b/translations/en/c2b.key @@ -1,2 +1,3 @@ c2b = Cloud2Butt +c2b.description = Cloud2Butt module will allow you to replace words with other words c2b.config = Configuration \ No newline at end of file diff --git a/translations/en/df.key b/translations/en/df.key index 01e6b36..f1da5be 100644 --- a/translations/en/df.key +++ b/translations/en/df.key @@ -1,4 +1,5 @@ df = Dwarf Fortress +df.description = Dwarf Fortress module is a special module that will filter prefixed messages to file df.grep = Main Settings df.grep.symbol = Filter Symbol df.grep.file = Destination file for filter diff --git a/translations/en/goodgame.key b/translations/en/goodgame.key index 45bd609..f5d34e9 100644 --- a/translations/en/goodgame.key +++ b/translations/en/goodgame.key @@ -1,4 +1,4 @@ goodgame = GoodGame -goodgame.config = Main Settings +goodgame.config = Settings goodgame.config.socket = WebSocket parameters goodgame.config.channel_name = Channel name \ No newline at end of file diff --git a/translations/en/levels.key b/translations/en/levels.key index 06d9e33..0fb36c7 100644 --- a/translations/en/levels.key +++ b/translations/en/levels.key @@ -1,8 +1,10 @@ levels = Levels +levels.description = Levels modules allow your viewers to gain experience by sending messages levels.config = Main Settings -levels.config.experience = experience growth +levels.config.experience = Experience growth levels.config.exp_for_level = Experience for level levels.config.file = Levels settings file levels.config.db = Database location for levels levels.config.message = Message on level up - +levels.config.exp_for_message = Amount of experience for message +levels.config.decrease_window = Window of full experience diff --git a/translations/en/logger.key b/translations/en/logger.key index 1f29733..7a6bac3 100644 --- a/translations/en/logger.key +++ b/translations/en/logger.key @@ -1,4 +1,5 @@ logger = Logger +logger.description = Logger module will log all messages to file logger.config = Main configuration logger.config.logging = Enable Logging logger.config.rotation = Rotation time of logs diff --git a/translations/en/main.key b/translations/en/main.key index a5172ab..5f3ff33 100644 --- a/translations/en/main.key +++ b/translations/en/main.key @@ -4,29 +4,32 @@ menu.reload = Reload WebChat *.cancel_button = Cancel *.list_add = Add *.list_remove = Remove +*.descr_explain = Click on an item to view description of the item +*.description = No description +settings = Settings settings.main = Main Settings settings.messaging = Messaging Modules settings.chat = Chat Modules -config = Main -config.gui = GUI Settings -config.gui.show_hidden = Show hidden items -config.gui.gui = Is GUI enabled -config.gui.on_top = Show window on top -config.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Just a big parameter for test -config.gui.reload = Reload WebChat -config.gui.reload.button = Reload -config.language = Program Language -config.language.list_box = +main = Main +main.gui = GUI Settings +main.gui.show_hidden = Show hidden items +main.gui.gui = Is GUI enabled +main.gui.on_top = Show window on top +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 +main.quit = Are you sure you want to quit? +main.quit.nosave = Are you sure you want to quit?\nWarning, your settings will not be saved. +main.save.non_dynamic = Warning, you have saved setting that are not dynamic\nPlease restart program to apply changes +main.language = Program Language +main.language.list_box = -config.style = -config.style.list_box = Available styles +messaging = Message modules +messaging.messaging = List of available modules +messaging.messaging.list_box = -messaging_modules = Message modules -messaging_modules.messaging = -messaging_modules.messaging.list_box = List of available modules - -chat_modules = Chats -chat_modules.chats = -chat_modules.chats.list_box = Available chat modules +chat = Chats +chat.chats = Available chat modules +chat.chats.list_box = diff --git a/translations/en/mentions.key b/translations/en/mentions.key index d4d8867..2fe4601 100644 --- a/translations/en/mentions.key +++ b/translations/en/mentions.key @@ -1,3 +1,4 @@ mentions = Mentions +mentions.description = Mentions module will allow you to mark messages that are send with special keyworks (Your nickname, for example) mentions.mentions = Words for Mentions list mentions.address = Words for Address list \ No newline at end of file diff --git a/translations/en/sc2tv.key b/translations/en/sc2tv.key index 8f7cba9..ba80a33 100644 --- a/translations/en/sc2tv.key +++ b/translations/en/sc2tv.key @@ -1,4 +1,4 @@ sc2tv = sc2tv -sc2tv.config = Main settings +sc2tv.config = Settings sc2tv.config.socket = WebSocket parameters sc2tv.config.channel_name = Channel name \ No newline at end of file diff --git a/translations/en/twitch.key b/translations/en/twitch.key index 90d0d58..0e2f058 100644 --- a/translations/en/twitch.key +++ b/translations/en/twitch.key @@ -1,4 +1,5 @@ twitch = Twitch.TV +twitch.config = Settings twitch.config.host = IRC hostname twitch.config.port = IRC port twitch.config.channel = Channel name diff --git a/translations/en/webchat.key b/translations/en/webchat.key index 2b4a758..810c058 100644 --- a/translations/en/webchat.key +++ b/translations/en/webchat.key @@ -1,6 +1,10 @@ webchat = WebChat +webchat.description = WebChat module is a webserver for chat and allows you to see messages in GUI/Web webchat.server = Local server settings webchat.server.host = Host webchat.server.port = Port - +webchat.style = Available styles +webchat.style.list_box = +webchat.style_settings = Style Settings +webchat.style_settings.font_size = Font Size (pt) \ No newline at end of file diff --git a/translations/ru/blacklist.key b/translations/ru/blacklist.key index 68ff5b2..ec32146 100644 --- a/translations/ru/blacklist.key +++ b/translations/ru/blacklist.key @@ -1,4 +1,5 @@ blacklist = Чёрный список +blacklist.description = Модуль черного списка позволяет вам блокировать или скрывать сообщения или зрителей. blacklist.main = Основные параметры blacklist.main.message = Сообщение замены blacklist.users_hide = Список скрытых пользователей diff --git a/translations/ru/c2b.key b/translations/ru/c2b.key index 0724be1..ed0798f 100644 --- a/translations/ru/c2b.key +++ b/translations/ru/c2b.key @@ -1,2 +1,3 @@ c2b = Cloud2Butt +c2b.description = Модуль Cloud2Butt позволяет вам заменять одни слова на другие. c2b.config = Настройки \ No newline at end of file diff --git a/translations/ru/df.key b/translations/ru/df.key index 202a20e..693c9f7 100644 --- a/translations/ru/df.key +++ b/translations/ru/df.key @@ -1,4 +1,5 @@ df = Dwarf Fortress +df.description = Модуль Dwarf Fortress позволяет вам фильтровать сообщения по ключу в файл. df.grep = Главные настройки df.grep.symbol = Ключ фильтра df.grep.file = Файл для фильтра diff --git a/translations/ru/goodgame.key b/translations/ru/goodgame.key index 3956511..3d4ebc7 100644 --- a/translations/ru/goodgame.key +++ b/translations/ru/goodgame.key @@ -1,4 +1,4 @@ goodgame = GoodGame -goodgame.config = Основные параметры GoodGame +goodgame.config = Настройки goodgame.config.socket = Настройки WebSocket goodgame.config.channel_name = Название канала \ No newline at end of file diff --git a/translations/ru/levels.key b/translations/ru/levels.key index 2303480..4f84a1e 100644 --- a/translations/ru/levels.key +++ b/translations/ru/levels.key @@ -1,8 +1,10 @@ levels = Уровни +levels.description = Модуль Уровней позволяет зрителям получать опыт за написанные сообщения. levels.config = Настройки levels.config.experience = Рост опыта levels.config.exp_for_level = Нужный опыт для уровня levels.config.file = Файл настроек уровней levels.config.db = Место нахождения базы данных для уровней levels.config.message = Сообщение при получении нового уровня - +levels.config.exp_for_message = Количество опыта за сообщения +levels.config.decrease_window = Период получения полного опыта \ No newline at end of file diff --git a/translations/ru/logger.key b/translations/ru/logger.key index ced4cd1..f0cc11e 100644 --- a/translations/ru/logger.key +++ b/translations/ru/logger.key @@ -1,4 +1,5 @@ logger = Logger +logger.description = Модуль Logger записывает все присланные сообщения в файл. logger.config = Настройки logger.config.logging = Включить Логирование logger.config.rotation = Время чередования логов diff --git a/translations/ru/main.key b/translations/ru/main.key index 0b003dd..39c5fc2 100644 --- a/translations/ru/main.key +++ b/translations/ru/main.key @@ -4,29 +4,32 @@ menu.reload = Перезагрузить Чат *.cancel_button = Отменить *.list_add = Добавить *.list_remove = Удалить +*.descr_explain = Выберите вещь, описание которой вы хотите прочитать. +*.description = Описание не предоставлено +settings = Настройки settings.main = Главные Настройки settings.messaging = Настройки модулей сообщений settings.chat = Настройки чатов -config = Главные Настройки -config.gui = Настройки Интерфейса -config.gui.show_hidden = Показвать скрытые вещи -config.gui.gui = Интерфейс Включен -config.gui.on_top = Окно поверх всех -config.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Тестовый Параметр -config.gui.reload = Перезагрузить ВебЧат -config.gui.reload.button = Перезагрузить -config.language = Язык Интерфейса -config.language.list_box = +main = Главные Настройки +main.gui = Настройки Интерфейса +main.gui.show_hidden = Показвать скрытые вещи +main.gui.gui = Интерфейс Включен +main.gui.on_top = Окно поверх всех +main.gui.very_big_parameter_with_really_big_name_and_a_lot_of_not_needed_stuff = Тестовый Параметр +main.gui.reload = Перезагрузить ВебЧат +main.gui.reload.button = Перезагрузить +main.quit = Вы уверены что хотите выйти? +main.quit.nosave = Вы уверены что хотите выйти?\Внимание, ваши настройки не будут сохранены. +main.save.non_dynamic = Внимание, сохраненые настройки не будут работать до перезапуска.\nПожалуйста перезапустите программу что бы изменения вступили в силу. +main.language = Язык Интерфейса +main.language.list_box = -config.style = -config.style.list_box = Доступные Стили +messaging = Модули Сообщений +messaging.messaging = Список доступных модулей +messaging.messaging.list_box = -messaging_modules = Модули Сообщений -messaging_modules.messaging = -messaging_modules.messaging.list_box = Список доступных модулей - -chat_modules = Чаты -chat_modules.chats = -chat_modules.chats.list_box = Список доступных чатов +chat = Чаты +chat.chats = Список доступных чатов +chat.chats.list_box = diff --git a/translations/ru/mentions.key b/translations/ru/mentions.key index 9ff4843..c7aecb9 100644 --- a/translations/ru/mentions.key +++ b/translations/ru/mentions.key @@ -1,3 +1,4 @@ mentions = Упоминания +mentions.description = Модуль позволяет вам поменять сообщения в которых упоминаются определённые слова.\n(К примеру: ваш Ник) mentions.mentions = Список слов для фильтра упоминания mentions.address = Список слов для фильтра адресата \ No newline at end of file diff --git a/translations/ru/sc2tv.key b/translations/ru/sc2tv.key index cd0b1e4..75a820c 100644 --- a/translations/ru/sc2tv.key +++ b/translations/ru/sc2tv.key @@ -1,4 +1,4 @@ sc2tv = sc2tv -sc2tv.config = Основные параметры +sc2tv.config = Настройки sc2tv.config.socket = WS параметры sc2tv.config.channel_name = Название канала \ No newline at end of file diff --git a/translations/ru/twitch.key b/translations/ru/twitch.key index bdfead4..a966bea 100644 --- a/translations/ru/twitch.key +++ b/translations/ru/twitch.key @@ -1,4 +1,5 @@ twitch = Twitch.TV +twitch.config = Настройки twitch.config.host = IRC хостнейм twitch.config.port = IRC порт twitch.config.channel = Имя канала diff --git a/translations/ru/webchat.key b/translations/ru/webchat.key index 26c74e4..14e5dd4 100644 --- a/translations/ru/webchat.key +++ b/translations/ru/webchat.key @@ -1,6 +1,11 @@ webchat = Веб Чат +webchat.description = Модуль ВебЧата позволяет вам видеть сообщения в браузере и интерфейсе. webchat.server = Настройки локального сервера webchat.server.host = Хост webchat.server.port = Порт +webchat.style = Доступные Стили +webchat.style.list_box = +webchat.style_settings = Настройки Стиля +webchat.style_settings.font_size = Размер шрифта