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 = Размер шрифта