From a214ab12c77985835305bd5ca2ea159cd8a192e1 Mon Sep 17 00:00:00 2001 From: ingvar1995 Date: Sat, 28 May 2016 13:06:13 +0300 Subject: [PATCH] merge with plugins --- docs/plugin_api.md | 36 +++++ docs/plugins.md | 17 +++ src/callbacks.py | 38 ++++- src/loginscreen.py | 2 +- src/main.py | 10 ++ src/mainscreen.py | 21 ++- src/menu.py | 78 ++++++++++- src/plugin_support.py | 146 +++++++++++++++++++ src/plugins/__init__.py | 0 src/plugins/plugin_super_class.py | 224 ++++++++++++++++++++++++++++++ src/profile.py | 16 ++- src/settings.py | 3 +- 12 files changed, 579 insertions(+), 12 deletions(-) create mode 100644 docs/plugin_api.md create mode 100644 docs/plugins.md create mode 100644 src/plugin_support.py create mode 100644 src/plugins/__init__.py create mode 100644 src/plugins/plugin_super_class.py diff --git a/docs/plugin_api.md b/docs/plugin_api.md new file mode 100644 index 0000000..93b3683 --- /dev/null +++ b/docs/plugin_api.md @@ -0,0 +1,36 @@ +#Plugins API + +In Toxygen plugin is single python module (.py file) and directory with data associated with it. +Every module must contain one class derived from PluginSuperClass defined in [plugin_super_class.py](/src/plugins/plugin_super_class.py). Instance of this class will be created by PluginLoader class (defined in [plugin_support.py](/src/plugin_support.py) ). This class can enable/disable plugins and send data to it. + +Every plugin has it's own full name and unique short name (1-5 symbols). Main app can get it using special methods. + +All plugin's data should be stored in following structure: + +``` +/plugins/ +|---plugin_short_name.py +|---/plugin_short_name/ + |---settings.json + |---other_files +``` + +Plugin can override following methods: +- get_description - this method should return plugin description. +- get_menu - plugins allowed to add items in friend menu. You can open this menu making right click on friend in friends list. This method should return list of QAction's. Plugin must connect to QAction's triggered() signal. +- get_window - plugins can have GUI, this method should return window instance or None for plugins without GUI. +- start - plugin was started. +- stop - plugin was stopped. +- command - new command to plugin. Command can be entered in message field in format '/plugin '. Command 'help' should show user list of supported commands. +- lossless_packet - callback - incoming lossless packet from friend. +- lossy_packet - callback - incoming lossy packet from friend. +- friend_connected - callback - friend became online. + +Other methods: +- send_lossless - this method send custom lossless packet. Plugins MUST send lossless packets using this method. +- send_lossy - this method send custom lossy packet. Plugins MUST send lossy packets using this method. +- load_settings - loads settings stored in default location. +- save_settings - saves settings to default location. +- load_translator - loads translations. Translations must be stored in directory with plugin's data. Files with translations must have the same name as in main app. + + diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 0000000..ca32b51 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,17 @@ +#Plugins + +Toxygen is the first [Tox](https://tox.chat/) client with plugins support. Plugin is Python module and directory with plugin's data which provide some additional functionality. + +#How to write plugin + +Check [Plugin API](/docs/plugin_api.md) for more info + +#How to install plugin + +1. Put plugin and directory with it's data into /src/plugins/ +2. Restart Toxygen + +#Plugins list + +WARNING: It is unsecure to install plugin not from this list! + diff --git a/src/callbacks.py b/src/callbacks.py index 840c786..aa89016 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -8,7 +8,7 @@ from profile import Profile from toxcore_enums_and_consts import * from toxav_enums import * from tox import bin_to_string -from ctypes import c_char_p, cast, pointer +from plugin_support import PluginLoader class InvokeEvent(QtCore.QEvent): @@ -85,6 +85,7 @@ def friend_connection_status(tox, friend_num, new_status, user_data): sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS']) elif friend.status is None: invoke_in_main_thread(profile.send_avatar, friend_num) + PluginLoader.get_instance().friend_online(friend_num) def friend_name(tox, friend_num, name, size, user_data): @@ -221,13 +222,35 @@ def file_recv_control(tox, friend_number, file_number, file_control, user_data): elif file_control == TOX_FILE_CONTROL['RESUME']: Profile.get_instance().resume_transfer(friend_number, file_number, True) +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - custom packets +# ----------------------------------------------------------------------------------------------------------------- + + +def lossless_packet(tox, friend_number, data, length, user_data): + """ + Incoming lossless packet + """ + plugin = PluginLoader.get_instance() + invoke_in_main_thread(plugin.callback_lossless, friend_number, data, length) + + +def lossy_packet(tox, friend_number, data, length, user_data): + """ + Incoming lossy packet + """ + plugin = PluginLoader.get_instance() + invoke_in_main_thread(plugin.callback_lossy, friend_number, data, length) + # ----------------------------------------------------------------------------------------------------------------- # Callbacks - audio # ----------------------------------------------------------------------------------------------------------------- def call_state(toxav, friend_number, mask, user_data): - """New call state""" + """ + New call state + """ print friend_number, mask if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True) @@ -236,13 +259,17 @@ def call_state(toxav, friend_number, mask, user_data): def call(toxav, friend_number, audio, video, user_data): - """Incoming call from friend""" + """ + Incoming call from friend + """ print friend_number, audio, video invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number) def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): - """New audio chunk""" + """ + New audio chunk + """ print audio_samples_per_channel, audio_channels_count, rate Profile.get_instance().call.chunk( ''.join(chr(x) for x in samples[:audio_samples_per_channel * 2 * audio_channels_count]), @@ -282,3 +309,6 @@ def init_callbacks(tox, window, tray): toxav.callback_call(call, 0) toxav.callback_audio_receive_frame(callback_audio, 0) + tox.callback_friend_lossless_packet(lossless_packet, 0) + tox.callback_friend_lossy_packet(lossy_packet, 0) + diff --git a/src/loginscreen.py b/src/loginscreen.py index ddac965..9190243 100644 --- a/src/loginscreen.py +++ b/src/loginscreen.py @@ -95,7 +95,7 @@ class LoginScreen(CenteredWidget): def update_select(self, data): list_of_profiles = [] for elem in data: - list_of_profiles.append(self.tr(elem)) + list_of_profiles.append(elem) self.comboBox.addItems(list_of_profiles) self.create_only = not list_of_profiles diff --git a/src/main.py b/src/main.py index 685c29f..d49614b 100644 --- a/src/main.py +++ b/src/main.py @@ -15,6 +15,7 @@ import locale import toxencryptsave from passwordscreen import PasswordScreen import profile +from plugin_support import PluginLoader class Toxygen(object): @@ -191,6 +192,9 @@ class Toxygen(object): self.ms.show() QtGui.QApplication.setStyle(get_style(settings['theme'])) # set application style + plugin_helper = PluginLoader(self.tox, settings) # plugin support + plugin_helper.load() + # init thread self.init = self.InitThread(self.tox, self.ms, self.tray) self.init.start() @@ -200,11 +204,13 @@ class Toxygen(object): self.mainloop.start() self.avloop = self.ToxAVIterateThread(self.tox.AV) self.avloop.start() + app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()")) app.exec_() self.init.stop = True self.mainloop.stop = True self.avloop.stop = True + plugin_helper.stop() self.mainloop.wait() self.init.wait() self.avloop.wait() @@ -239,6 +245,10 @@ class Toxygen(object): self.avloop = self.ToxAVIterateThread(self.tox.AV) self.avloop.start() + + plugin_helper = PluginLoader.get_instance() + plugin_helper.set_tox(self.tox) + return self.tox # ----------------------------------------------------------------------------------------------------------------- diff --git a/src/mainscreen.py b/src/mainscreen.py index 49a15a2..2d34e3a 100644 --- a/src/mainscreen.py +++ b/src/mainscreen.py @@ -4,6 +4,7 @@ from menu import * from profile import * from list_items import * from widgets import QRightClickButton +import plugin_support class MessageArea(QtGui.QPlainTextEdit): @@ -48,12 +49,16 @@ class MainWindow(QtGui.QMainWindow): self.menubar.setMinimumSize(self.width(), 25) self.menubar.setMaximumSize(self.width(), 25) self.menubar.setBaseSize(self.width(), 25) + self.menuProfile = QtGui.QMenu(self.menubar) self.menuProfile.setObjectName("menuProfile") self.menuSettings = QtGui.QMenu(self.menubar) self.menuSettings.setObjectName("menuSettings") + self.menuPlugins = QtGui.QMenu(self.menubar) + self.menuPlugins.setObjectName("menuPlugins") self.menuAbout = QtGui.QMenu(self.menubar) self.menuAbout.setObjectName("menuAbout") + self.actionAdd_friend = QtGui.QAction(MainWindow) self.actionAdd_friend.setObjectName("actionAdd_friend") self.actionProfile_settings = QtGui.QAction(MainWindow) @@ -71,6 +76,7 @@ class MainWindow(QtGui.QMainWindow): self.actionSettings = QtGui.QAction(MainWindow) self.actionSettings.setObjectName("actionSettings") self.audioSettings = QtGui.QAction(MainWindow) + self.pluginData = QtGui.QAction(MainWindow) self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionSettings) self.menuSettings.addAction(self.actionPrivacy_settings) @@ -78,9 +84,11 @@ class MainWindow(QtGui.QMainWindow): self.menuSettings.addAction(self.actionNotifications) self.menuSettings.addAction(self.actionNetwork) self.menuSettings.addAction(self.audioSettings) + self.menuPlugins.addAction(self.pluginData) self.menuAbout.addAction(self.actionAbout_program) self.menubar.addAction(self.menuProfile.menuAction()) self.menubar.addAction(self.menuSettings.menuAction()) + self.menubar.addAction(self.menuPlugins.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) self.actionAbout_program.triggered.connect(self.about_program) @@ -91,12 +99,15 @@ class MainWindow(QtGui.QMainWindow): self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionNotifications.triggered.connect(self.notification_settings) self.audioSettings.triggered.connect(self.audio_settings) + self.pluginData.triggered.connect(self.plugins_menu) QtCore.QMetaObject.connectSlotsByName(MainWindow) def languageChange(self, *args, **kwargs): self.retranslateUi() def retranslateUi(self): + self.menuPlugins.setTitle(QtGui.QApplication.translate("MainWindow", "Plugins", None, QtGui.QApplication.UnicodeUTF8)) + self.pluginData.setText(QtGui.QApplication.translate("MainWindow", "List of plugins", None, QtGui.QApplication.UnicodeUTF8)) self.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8)) self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) self.menuAbout.setTitle(QtGui.QApplication.translate("MainWindow", "About", None, QtGui.QApplication.UnicodeUTF8)) @@ -358,6 +369,10 @@ class MainWindow(QtGui.QMainWindow): self.n_s = NetworkSettings(self.reset) self.n_s.show() + def plugins_menu(self): + self.p_s = PluginsSettings() + self.p_s.show() + def add_contact(self): self.a_c = AddContact() self.a_c.show() @@ -431,7 +446,7 @@ class MainWindow(QtGui.QMainWindow): def friend_right_click(self, pos): item = self.friends_list.itemAt(pos) num = self.friends_list.indexFromItem(item).row() - friend = Profile.get_instance().get_friend_by_number(num) + friend = Profile.get_instance().get_friend(num) settings = Settings.get_instance() allowed = friend.tox_id in settings['auto_accept_from_friends'] auto = QtGui.QApplication.translate("MainWindow", 'Disallow auto accept', None, QtGui.QApplication.UnicodeUTF8) if allowed else QtGui.QApplication.translate("MainWindow", 'Allow auto accept', None, QtGui.QApplication.UnicodeUTF8) @@ -442,6 +457,10 @@ class MainWindow(QtGui.QMainWindow): copy_key_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Copy public key', None, QtGui.QApplication.UnicodeUTF8)) auto_accept_item = self.listMenu.addAction(auto) remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8)) + submenu = plugin_support.PluginLoader.get_instance().get_menu(self.listMenu, num) + if len(submenu): + plug = self.listMenu.addMenu(QtGui.QApplication.translate("MainWindow", 'Plugins', None, QtGui.QApplication.UnicodeUTF8)) + plug.addActions(submenu) self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num)) self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num)) self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num)) diff --git a/src/menu.py b/src/menu.py index 3c4f6db..c2165b3 100644 --- a/src/menu.py +++ b/src/menu.py @@ -8,6 +8,7 @@ from util import get_style, curr_directory from widgets import CenteredWidget, DataLabel import pyaudio import toxencryptsave +import plugin_support class AddContact(CenteredWidget): @@ -311,8 +312,8 @@ class NetworkSettings(CenteredWidget): # recreate tox instance Profile.get_instance().reset(self.reset) self.close() - except: - pass + except Exception as ex: + log('Exception in restart: ' + str(ex)) class PrivacySettings(CenteredWidget): @@ -589,3 +590,76 @@ class AudioSettings(CenteredWidget): settings.audio['input'] = self.in_indexes[self.input.currentIndex()] settings.audio['output'] = self.out_indexes[self.output.currentIndex()] settings.save() + + +class PluginsSettings(CenteredWidget): + + def __init__(self): + super(PluginsSettings, self).__init__() + self.initUI() + self.center() + self.retranslateUi() + + def initUI(self): + self.resize(400, 210) + self.setMinimumSize(QtCore.QSize(400, 210)) + self.setMaximumSize(QtCore.QSize(400, 210)) + self.comboBox = QtGui.QComboBox(self) + self.comboBox.setGeometry(QtCore.QRect(30, 10, 340, 30)) + self.label = QtGui.QLabel(self) + self.label.setGeometry(QtCore.QRect(30, 40, 340, 90)) + self.label.setWordWrap(True) + self.button = QtGui.QPushButton(self) + self.button.setGeometry(QtCore.QRect(30, 130, 340, 30)) + self.button.clicked.connect(self.button_click) + self.open = QtGui.QPushButton(self) + self.open.setGeometry(QtCore.QRect(30, 170, 340, 30)) + self.open.clicked.connect(self.open_plugin) + self.pl_loader = plugin_support.PluginLoader.get_instance() + self.update_list() + self.comboBox.currentIndexChanged.connect(self.show_data) + self.show_data() + + def retranslateUi(self): + self.setWindowTitle(QtGui.QApplication.translate('PluginsForm', "Plugins", None, QtGui.QApplication.UnicodeUTF8)) + self.open.setText(QtGui.QApplication.translate('PluginsForm', "Open selected plugin", None, QtGui.QApplication.UnicodeUTF8)) + + def open_plugin(self): + ind = self.comboBox.currentIndex() + plugin = self.data[ind] + window = self.pl_loader.plugin_window(plugin[-1]) + if window is not None: + self.window = window + self.window.show() + else: + msgBox = QtGui.QMessageBox() + text = (QtGui.QApplication.translate("PluginsForm", 'No GUI found for this plugin', None, + QtGui.QApplication.UnicodeUTF8)) + msgBox.setText(text) + msgBox.exec_() + + def update_list(self): + self.comboBox.clear() + data = self.pl_loader.get_plugins_list() + self.comboBox.addItems(map(lambda x: x[0], data)) + self.data = data + + def show_data(self): + ind = self.comboBox.currentIndex() + plugin = self.data[ind] + descr = plugin[2] or QtGui.QApplication.translate("PluginsForm", "No description available", None, QtGui.QApplication.UnicodeUTF8) + self.label.setText(descr) + if plugin[1]: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) + + def button_click(self): + ind = self.comboBox.currentIndex() + plugin = self.data[ind] + self.pl_loader.toggle_plugin(plugin[-1]) + plugin[1] = not plugin[1] + if plugin[1]: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Disable plugin", None, QtGui.QApplication.UnicodeUTF8)) + else: + self.button.setText(QtGui.QApplication.translate("PluginsForm", "Enable plugin", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/src/plugin_support.py b/src/plugin_support.py new file mode 100644 index 0000000..f4618f8 --- /dev/null +++ b/src/plugin_support.py @@ -0,0 +1,146 @@ +import util +import profile +import os +import imp +import inspect +import plugins.plugin_super_class as pl +import toxencryptsave + + +class PluginLoader(util.Singleton): + + def __init__(self, tox, settings): + self._profile = profile.Profile.get_instance() + self._settings = settings + self._plugins = {} # dict. key - plugin unique short name, value - tuple (plugin instance, is active) + self._tox = tox + self._encr = toxencryptsave.LibToxEncryptSave.get_instance() + + def set_tox(self, tox): + """ + New tox instance + """ + self._tox = tox + for value in self._plugins.values(): + value[0].set_tox(tox) + + def load(self): + """ + Load all plugins in plugins folder + """ + path = util.curr_directory() + '/plugins/' + files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + for fl in files: + if fl in ('plugin_super_class.py', '__init__.py') or not fl.endswith('.py'): + continue + name = fl[:-3] # module name without .py + try: + module = imp.load_source('plugins.' + name, path + fl) # import plugin + except ImportError: + util.log('Import error in module ' + name) + continue + except Exception as ex: + util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + continue + for elem in dir(module): + obj = getattr(module, elem) + if inspect.isclass(obj) and issubclass(obj, pl.PluginSuperClass): # looking for plugin class in module + print 'Plugin', elem + try: # create instance of plugin class + inst = obj(self._tox, self._profile, self._settings, self._encr) + autostart = inst.get_short_name() in self._settings['plugins'] + if autostart: + inst.start() + except Exception as ex: + util.log('Exception in module ' + name + ' Exception: ' + str(ex)) + continue + self._plugins[inst.get_short_name()] = [inst, autostart] # (inst, is active) + break + + def callback_lossless(self, friend_number, data, length): + """ + New incoming custom lossless packet (callback) + """ + l = data[0] - pl.LOSSLESS_FIRST_BYTE + name = ''.join(chr(x) for x in data[1:l + 1]) + if name in self._plugins and self._plugins[name][1]: + self._plugins[name][0].lossless_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number) + + def callback_lossy(self, friend_number, data, length): + """ + New incoming custom lossy packet (callback) + """ + l = data[0] - pl.LOSSY_FIRST_BYTE + name = ''.join(chr(x) for x in data[1:l + 1]) + if name in self._plugins and self._plugins[name][1]: + self._plugins[name][0].lossy_packet(''.join(chr(x) for x in data[l + 1:length]), friend_number) + + def friend_online(self, friend_number): + for elem in self._plugins.values(): + if elem[1]: + elem[0].friend_connected(friend_number) + + def get_plugins_list(self): + """ + Returns list of all plugins + """ + result = [] + for data in self._plugins.values(): + result.append([data[0].get_name(), # plugin full name + data[1], # is enabled + data[0].get_description(), # plugin description + data[0].get_short_name()]) # key - short unique name + return result + + def plugin_window(self, key): + """ + Return window or None for specified plugin + """ + return self._plugins[key][0].get_window() + + def toggle_plugin(self, key): + """ + Enable/disable plugin + :param key: plugin short name + """ + plugin = self._plugins[key] + if plugin[1]: + plugin[0].stop() + else: + plugin[0].start() + plugin[1] = not plugin[1] + if plugin[1]: + self._settings['plugins'].append(key) + else: + self._settings['plugins'].remove(key) + self._settings.save() + + def command(self, text): + """ + New command for plugin + """ + text = text.strip() + name = text.split()[0] + if name in self._plugins and self._plugins[name][1]: + self._plugins[name][0].command(text[len(name) + 1:]) + + def get_menu(self, menu, num): + """ + Return list of items for menu + """ + result = [] + for elem in self._plugins.values(): + if elem[1]: + try: + result.extend(elem[0].get_menu(menu, num)) + except: + continue + return result + + def stop(self): + """ + App is closing, stop all plugins + """ + for key in self._plugins.keys(): + self._plugins[key][0].close() + del self._plugins[key] diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/plugin_super_class.py b/src/plugins/plugin_super_class.py new file mode 100644 index 0000000..17089db --- /dev/null +++ b/src/plugins/plugin_super_class.py @@ -0,0 +1,224 @@ +import os +try: + from PySide import QtCore, QtGui +except ImportError: + from PyQt4 import QtCore, QtGui + + +MAX_SHORT_NAME_LENGTH = 5 + +LOSSY_FIRST_BYTE = 200 + +LOSSLESS_FIRST_BYTE = 160 + + +def path_to_data(name): + """ + :param name: plugin unique name + :return path do plugin's directory + """ + return os.path.dirname(os.path.realpath(__file__)) + '/' + name + '/' + + +class PluginSuperClass(object): + """ + Superclass for all plugins. Plugin is python module with at least one class derived from PluginSuperClass. + """ + + def __init__(self, name, short_name, tox=None, profile=None, settings=None, encrypt_save=None): + """ + :param name: plugin full name + :param short_name: plugin unique short name (length of short name should not exceed MAX_SHORT_NAME_LENGTH) + :param tox: tox instance + :param profile: profile instance + :param settings: profile settings + :param encrypt_save: LibToxEncryptSave instance. + """ + self._settings = settings + self._profile = profile + self._tox = tox + name = name.strip() + short_name = short_name.strip() + if not name or not short_name: + raise NameError('Wrong name') + self._name = name + self._short_name = short_name[:MAX_SHORT_NAME_LENGTH] + self._translator = None # translator for plugin's GUI + self._encrypt_save = encrypt_save + + # ----------------------------------------------------------------------------------------------------------------- + # Get methods + # ----------------------------------------------------------------------------------------------------------------- + + def get_name(self): + """ + :return plugin full name + """ + return self._name + + def get_short_name(self): + """ + :return plugin unique (short) name + """ + return self._short_name + + def get_description(self): + """ + Should return plugin description + """ + return self.__doc__ + + def get_menu(self, menu, row_number): + """ + This method creates items for menu which called on right click in list of friends + :param menu: menu instance + :param row_number: number of selected row in list of contacts + :return list of QAction's + """ + return [] + + def get_window(self): + """ + This method should return window for plugins with GUI or None + """ + return None + + def set_tox(self, tox): + """ + New tox instance + """ + self._tox = tox + + # ----------------------------------------------------------------------------------------------------------------- + # Plugin was stopped, started or new command received + # ----------------------------------------------------------------------------------------------------------------- + + def start(self): + """ + This method called when plugin was started + """ + pass + + def stop(self): + """ + This method called when plugin was stopped + """ + pass + + def close(self): + """ + App is closing + """ + pass + + def command(self, command): + """ + New command. On 'help' this method should provide user list of available commands + :param command: string with command + """ + msgbox = QtGui.QMessageBox() + title = QtGui.QApplication.translate("PluginWindow", "List of commands for plugin {}", None, QtGui.QApplication.UnicodeUTF8) + msgbox.setWindowTitle(title.format(self._name)) + msgbox.setText(QtGui.QApplication.translate("PluginWindow", "No commands available", None, QtGui.QApplication.UnicodeUTF8)) + msgbox.exec_() + + # ----------------------------------------------------------------------------------------------------------------- + # Translations support + # ----------------------------------------------------------------------------------------------------------------- + + def load_translator(self): + """ + This method loads translations for GUI + """ + app = QtGui.QApplication.instance() + langs = self._settings.supported_languages() + curr_lang = self._settings['language'] + if curr_lang in map(lambda x: x[0], langs): + if self._translator is not None: + app.removeTranslator(self._translator) + self._translator = QtCore.QTranslator() + lang_path = filter(lambda x: x[0] == curr_lang, langs)[0][1] + self._translator.load(path_to_data(self._short_name) + lang_path) + app.installTranslator(self._translator) + + # ----------------------------------------------------------------------------------------------------------------- + # Settings loading and saving + # ----------------------------------------------------------------------------------------------------------------- + + def load_settings(self): + """ + This method loads settings of plugin and returns raw data + """ + with open(path_to_data(self._short_name) + 'settings.json') as fl: + data = fl.read() + return data + + def save_settings(self, data): + """ + This method saves plugin's settings to file + :param data: string with data + """ + with open(path_to_data(self._short_name) + 'settings.json', 'wb') as fl: + fl.write(data) + + # ----------------------------------------------------------------------------------------------------------------- + # Callbacks + # ----------------------------------------------------------------------------------------------------------------- + + def lossless_packet(self, data, friend_number): + """ + Incoming lossless packet + :param data: string with data + :param friend_number: number of friend who sent packet + """ + pass + + def lossy_packet(self, data, friend_number): + """ + Incoming lossy packet + :param data: string with data + :param friend_number: number of friend who sent packet + """ + pass + + def friend_connected(self, friend_number): + """ + Friend with specified number is online now + """ + pass + + # ----------------------------------------------------------------------------------------------------------------- + # Custom packets sending + # ----------------------------------------------------------------------------------------------------------------- + + def send_lossless(self, data, friend_number): + """ + This method sends lossless packet to friend + Wrapper for self._tox.friend_send_lossless_packet + Use it instead of direct using self._tox.friend_send_lossless_packet + :return True on success + """ + if data is None: + data = '' + try: + return self._tox.friend_send_lossless_packet(friend_number, + chr(len(self._short_name) + LOSSLESS_FIRST_BYTE) + + self._short_name + str(data)) + except: + return False + + def send_lossy(self, data, friend_number): + """ + This method sends lossy packet to friend + Wrapper for self._tox.friend_send_lossy_packet + Use it instead of direct using self._tox.friend_send_lossy_packet + :return True on success + """ + if data is None: + data = '' + try: + return self._tox.friend_send_lossy_packet(friend_number, + chr(len(self._short_name) + LOSSY_FIRST_BYTE) + + self._short_name + str(data)) + except: + return False diff --git a/src/profile.py b/src/profile.py index 131e3af..ebda428 100644 --- a/src/profile.py +++ b/src/profile.py @@ -16,6 +16,7 @@ from file_transfers import * import time import calls import avwidgets +import plugin_support class Contact(object): @@ -383,6 +384,9 @@ class Profile(Contact, Singleton): def get_friend_by_number(self, num): return filter(lambda x: x.number == num, self._friends)[0] + def get_friend(self, num): + return self._friends[num] + # ----------------------------------------------------------------------------------------------------------------- # Work with active friend # ----------------------------------------------------------------------------------------------------------------- @@ -557,7 +561,10 @@ class Profile(Contact, Singleton): Send message to active friend :param text: message text """ - if self.is_active_online() and text: + if text.startswith('/plugin '): + plugin_support.PluginLoader.get_instance().command(text[8:]) + self._screen.messageEdit.clear() + elif self.is_active_online() and text: if text.startswith('/me '): message_type = TOX_MESSAGE_TYPE['ACTION'] text = text[4:] @@ -1024,12 +1031,15 @@ class Profile(Contact, Singleton): st.set_state_changed_handler(item.update) self._messages.scrollToBottom() - def send_file(self, path): + def send_file(self, path, number=None): """ Send file to current active friend :param path: file path + :param number: friend_number """ - friend_number = self.get_active_number() + friend_number = number or self.get_active_number() + if self.get_friend_by_number(friend_number).status is None: + return st = SendTransfer(path, self._tox, friend_number) self._file_transfers[(friend_number, st.get_file_number())] = st tm = TransferMessage(MESSAGE_OWNER['ME'], diff --git a/src/settings.py b/src/settings.py index 631bfd7..f5be3a7 100644 --- a/src/settings.py +++ b/src/settings.py @@ -79,7 +79,8 @@ class Settings(Singleton, dict): 'friends_aliases': [], 'typing_notifications': False, 'calls_sound': True, - 'blocked': [] + 'blocked': [], + 'plugins': [] } @staticmethod