merge with plugins
This commit is contained in:
parent
b2fa484bb3
commit
a214ab12c7
12 changed files with 579 additions and 12 deletions
36
docs/plugin_api.md
Normal file
36
docs/plugin_api.md
Normal file
|
@ -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 <plugin_short_name> <command>'. 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.
|
||||||
|
|
||||||
|
|
17
docs/plugins.md
Normal file
17
docs/plugins.md
Normal file
|
@ -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!
|
||||||
|
|
|
@ -8,7 +8,7 @@ from profile import Profile
|
||||||
from toxcore_enums_and_consts import *
|
from toxcore_enums_and_consts import *
|
||||||
from toxav_enums import *
|
from toxav_enums import *
|
||||||
from tox import bin_to_string
|
from tox import bin_to_string
|
||||||
from ctypes import c_char_p, cast, pointer
|
from plugin_support import PluginLoader
|
||||||
|
|
||||||
|
|
||||||
class InvokeEvent(QtCore.QEvent):
|
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'])
|
sound_notification(SOUND_NOTIFICATION['FRIEND_CONNECTION_STATUS'])
|
||||||
elif friend.status is None:
|
elif friend.status is None:
|
||||||
invoke_in_main_thread(profile.send_avatar, friend_num)
|
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):
|
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']:
|
elif file_control == TOX_FILE_CONTROL['RESUME']:
|
||||||
Profile.get_instance().resume_transfer(friend_number, file_number, True)
|
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
|
# Callbacks - audio
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
def call_state(toxav, friend_number, mask, user_data):
|
def call_state(toxav, friend_number, mask, user_data):
|
||||||
"""New call state"""
|
"""
|
||||||
|
New call state
|
||||||
|
"""
|
||||||
print friend_number, mask
|
print friend_number, mask
|
||||||
if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']:
|
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)
|
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):
|
def call(toxav, friend_number, audio, video, user_data):
|
||||||
"""Incoming call from friend"""
|
"""
|
||||||
|
Incoming call from friend
|
||||||
|
"""
|
||||||
print friend_number, audio, video
|
print friend_number, audio, video
|
||||||
invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number)
|
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):
|
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
|
print audio_samples_per_channel, audio_channels_count, rate
|
||||||
Profile.get_instance().call.chunk(
|
Profile.get_instance().call.chunk(
|
||||||
''.join(chr(x) for x in samples[:audio_samples_per_channel * 2 * audio_channels_count]),
|
''.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_call(call, 0)
|
||||||
toxav.callback_audio_receive_frame(callback_audio, 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)
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class LoginScreen(CenteredWidget):
|
||||||
def update_select(self, data):
|
def update_select(self, data):
|
||||||
list_of_profiles = []
|
list_of_profiles = []
|
||||||
for elem in data:
|
for elem in data:
|
||||||
list_of_profiles.append(self.tr(elem))
|
list_of_profiles.append(elem)
|
||||||
self.comboBox.addItems(list_of_profiles)
|
self.comboBox.addItems(list_of_profiles)
|
||||||
self.create_only = not list_of_profiles
|
self.create_only = not list_of_profiles
|
||||||
|
|
||||||
|
|
10
src/main.py
10
src/main.py
|
@ -15,6 +15,7 @@ import locale
|
||||||
import toxencryptsave
|
import toxencryptsave
|
||||||
from passwordscreen import PasswordScreen
|
from passwordscreen import PasswordScreen
|
||||||
import profile
|
import profile
|
||||||
|
from plugin_support import PluginLoader
|
||||||
|
|
||||||
|
|
||||||
class Toxygen(object):
|
class Toxygen(object):
|
||||||
|
@ -191,6 +192,9 @@ class Toxygen(object):
|
||||||
self.ms.show()
|
self.ms.show()
|
||||||
QtGui.QApplication.setStyle(get_style(settings['theme'])) # set application style
|
QtGui.QApplication.setStyle(get_style(settings['theme'])) # set application style
|
||||||
|
|
||||||
|
plugin_helper = PluginLoader(self.tox, settings) # plugin support
|
||||||
|
plugin_helper.load()
|
||||||
|
|
||||||
# init thread
|
# init thread
|
||||||
self.init = self.InitThread(self.tox, self.ms, self.tray)
|
self.init = self.InitThread(self.tox, self.ms, self.tray)
|
||||||
self.init.start()
|
self.init.start()
|
||||||
|
@ -200,11 +204,13 @@ class Toxygen(object):
|
||||||
self.mainloop.start()
|
self.mainloop.start()
|
||||||
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
||||||
self.avloop.start()
|
self.avloop.start()
|
||||||
|
|
||||||
app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()"))
|
app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()"))
|
||||||
app.exec_()
|
app.exec_()
|
||||||
self.init.stop = True
|
self.init.stop = True
|
||||||
self.mainloop.stop = True
|
self.mainloop.stop = True
|
||||||
self.avloop.stop = True
|
self.avloop.stop = True
|
||||||
|
plugin_helper.stop()
|
||||||
self.mainloop.wait()
|
self.mainloop.wait()
|
||||||
self.init.wait()
|
self.init.wait()
|
||||||
self.avloop.wait()
|
self.avloop.wait()
|
||||||
|
@ -239,6 +245,10 @@ class Toxygen(object):
|
||||||
|
|
||||||
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
self.avloop = self.ToxAVIterateThread(self.tox.AV)
|
||||||
self.avloop.start()
|
self.avloop.start()
|
||||||
|
|
||||||
|
plugin_helper = PluginLoader.get_instance()
|
||||||
|
plugin_helper.set_tox(self.tox)
|
||||||
|
|
||||||
return self.tox
|
return self.tox
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------------------------------
|
||||||
|
|
|
@ -4,6 +4,7 @@ from menu import *
|
||||||
from profile import *
|
from profile import *
|
||||||
from list_items import *
|
from list_items import *
|
||||||
from widgets import QRightClickButton
|
from widgets import QRightClickButton
|
||||||
|
import plugin_support
|
||||||
|
|
||||||
|
|
||||||
class MessageArea(QtGui.QPlainTextEdit):
|
class MessageArea(QtGui.QPlainTextEdit):
|
||||||
|
@ -48,12 +49,16 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
self.menubar.setMinimumSize(self.width(), 25)
|
self.menubar.setMinimumSize(self.width(), 25)
|
||||||
self.menubar.setMaximumSize(self.width(), 25)
|
self.menubar.setMaximumSize(self.width(), 25)
|
||||||
self.menubar.setBaseSize(self.width(), 25)
|
self.menubar.setBaseSize(self.width(), 25)
|
||||||
|
|
||||||
self.menuProfile = QtGui.QMenu(self.menubar)
|
self.menuProfile = QtGui.QMenu(self.menubar)
|
||||||
self.menuProfile.setObjectName("menuProfile")
|
self.menuProfile.setObjectName("menuProfile")
|
||||||
self.menuSettings = QtGui.QMenu(self.menubar)
|
self.menuSettings = QtGui.QMenu(self.menubar)
|
||||||
self.menuSettings.setObjectName("menuSettings")
|
self.menuSettings.setObjectName("menuSettings")
|
||||||
|
self.menuPlugins = QtGui.QMenu(self.menubar)
|
||||||
|
self.menuPlugins.setObjectName("menuPlugins")
|
||||||
self.menuAbout = QtGui.QMenu(self.menubar)
|
self.menuAbout = QtGui.QMenu(self.menubar)
|
||||||
self.menuAbout.setObjectName("menuAbout")
|
self.menuAbout.setObjectName("menuAbout")
|
||||||
|
|
||||||
self.actionAdd_friend = QtGui.QAction(MainWindow)
|
self.actionAdd_friend = QtGui.QAction(MainWindow)
|
||||||
self.actionAdd_friend.setObjectName("actionAdd_friend")
|
self.actionAdd_friend.setObjectName("actionAdd_friend")
|
||||||
self.actionProfile_settings = QtGui.QAction(MainWindow)
|
self.actionProfile_settings = QtGui.QAction(MainWindow)
|
||||||
|
@ -71,6 +76,7 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
self.actionSettings = QtGui.QAction(MainWindow)
|
self.actionSettings = QtGui.QAction(MainWindow)
|
||||||
self.actionSettings.setObjectName("actionSettings")
|
self.actionSettings.setObjectName("actionSettings")
|
||||||
self.audioSettings = QtGui.QAction(MainWindow)
|
self.audioSettings = QtGui.QAction(MainWindow)
|
||||||
|
self.pluginData = QtGui.QAction(MainWindow)
|
||||||
self.menuProfile.addAction(self.actionAdd_friend)
|
self.menuProfile.addAction(self.actionAdd_friend)
|
||||||
self.menuProfile.addAction(self.actionSettings)
|
self.menuProfile.addAction(self.actionSettings)
|
||||||
self.menuSettings.addAction(self.actionPrivacy_settings)
|
self.menuSettings.addAction(self.actionPrivacy_settings)
|
||||||
|
@ -78,9 +84,11 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
self.menuSettings.addAction(self.actionNotifications)
|
self.menuSettings.addAction(self.actionNotifications)
|
||||||
self.menuSettings.addAction(self.actionNetwork)
|
self.menuSettings.addAction(self.actionNetwork)
|
||||||
self.menuSettings.addAction(self.audioSettings)
|
self.menuSettings.addAction(self.audioSettings)
|
||||||
|
self.menuPlugins.addAction(self.pluginData)
|
||||||
self.menuAbout.addAction(self.actionAbout_program)
|
self.menuAbout.addAction(self.actionAbout_program)
|
||||||
self.menubar.addAction(self.menuProfile.menuAction())
|
self.menubar.addAction(self.menuProfile.menuAction())
|
||||||
self.menubar.addAction(self.menuSettings.menuAction())
|
self.menubar.addAction(self.menuSettings.menuAction())
|
||||||
|
self.menubar.addAction(self.menuPlugins.menuAction())
|
||||||
self.menubar.addAction(self.menuAbout.menuAction())
|
self.menubar.addAction(self.menuAbout.menuAction())
|
||||||
|
|
||||||
self.actionAbout_program.triggered.connect(self.about_program)
|
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.actionInterface_settings.triggered.connect(self.interface_settings)
|
||||||
self.actionNotifications.triggered.connect(self.notification_settings)
|
self.actionNotifications.triggered.connect(self.notification_settings)
|
||||||
self.audioSettings.triggered.connect(self.audio_settings)
|
self.audioSettings.triggered.connect(self.audio_settings)
|
||||||
|
self.pluginData.triggered.connect(self.plugins_menu)
|
||||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||||
|
|
||||||
def languageChange(self, *args, **kwargs):
|
def languageChange(self, *args, **kwargs):
|
||||||
self.retranslateUi()
|
self.retranslateUi()
|
||||||
|
|
||||||
def retranslateUi(self):
|
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.menuProfile.setTitle(QtGui.QApplication.translate("MainWindow", "Profile", None, QtGui.QApplication.UnicodeUTF8))
|
||||||
self.menuSettings.setTitle(QtGui.QApplication.translate("MainWindow", "Settings", 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))
|
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 = NetworkSettings(self.reset)
|
||||||
self.n_s.show()
|
self.n_s.show()
|
||||||
|
|
||||||
|
def plugins_menu(self):
|
||||||
|
self.p_s = PluginsSettings()
|
||||||
|
self.p_s.show()
|
||||||
|
|
||||||
def add_contact(self):
|
def add_contact(self):
|
||||||
self.a_c = AddContact()
|
self.a_c = AddContact()
|
||||||
self.a_c.show()
|
self.a_c.show()
|
||||||
|
@ -431,7 +446,7 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
def friend_right_click(self, pos):
|
def friend_right_click(self, pos):
|
||||||
item = self.friends_list.itemAt(pos)
|
item = self.friends_list.itemAt(pos)
|
||||||
num = self.friends_list.indexFromItem(item).row()
|
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()
|
settings = Settings.get_instance()
|
||||||
allowed = friend.tox_id in settings['auto_accept_from_friends']
|
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)
|
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))
|
copy_key_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Copy public key', None, QtGui.QApplication.UnicodeUTF8))
|
||||||
auto_accept_item = self.listMenu.addAction(auto)
|
auto_accept_item = self.listMenu.addAction(auto)
|
||||||
remove_item = self.listMenu.addAction(QtGui.QApplication.translate("MainWindow", 'Remove friend', None, QtGui.QApplication.UnicodeUTF8))
|
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(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(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num))
|
||||||
self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num))
|
self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num))
|
||||||
|
|
78
src/menu.py
78
src/menu.py
|
@ -8,6 +8,7 @@ from util import get_style, curr_directory
|
||||||
from widgets import CenteredWidget, DataLabel
|
from widgets import CenteredWidget, DataLabel
|
||||||
import pyaudio
|
import pyaudio
|
||||||
import toxencryptsave
|
import toxencryptsave
|
||||||
|
import plugin_support
|
||||||
|
|
||||||
|
|
||||||
class AddContact(CenteredWidget):
|
class AddContact(CenteredWidget):
|
||||||
|
@ -311,8 +312,8 @@ class NetworkSettings(CenteredWidget):
|
||||||
# recreate tox instance
|
# recreate tox instance
|
||||||
Profile.get_instance().reset(self.reset)
|
Profile.get_instance().reset(self.reset)
|
||||||
self.close()
|
self.close()
|
||||||
except:
|
except Exception as ex:
|
||||||
pass
|
log('Exception in restart: ' + str(ex))
|
||||||
|
|
||||||
|
|
||||||
class PrivacySettings(CenteredWidget):
|
class PrivacySettings(CenteredWidget):
|
||||||
|
@ -589,3 +590,76 @@ class AudioSettings(CenteredWidget):
|
||||||
settings.audio['input'] = self.in_indexes[self.input.currentIndex()]
|
settings.audio['input'] = self.in_indexes[self.input.currentIndex()]
|
||||||
settings.audio['output'] = self.out_indexes[self.output.currentIndex()]
|
settings.audio['output'] = self.out_indexes[self.output.currentIndex()]
|
||||||
settings.save()
|
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))
|
||||||
|
|
146
src/plugin_support.py
Normal file
146
src/plugin_support.py
Normal file
|
@ -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]
|
0
src/plugins/__init__.py
Normal file
0
src/plugins/__init__.py
Normal file
224
src/plugins/plugin_super_class.py
Normal file
224
src/plugins/plugin_super_class.py
Normal file
|
@ -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
|
|
@ -16,6 +16,7 @@ from file_transfers import *
|
||||||
import time
|
import time
|
||||||
import calls
|
import calls
|
||||||
import avwidgets
|
import avwidgets
|
||||||
|
import plugin_support
|
||||||
|
|
||||||
|
|
||||||
class Contact(object):
|
class Contact(object):
|
||||||
|
@ -383,6 +384,9 @@ class Profile(Contact, Singleton):
|
||||||
def get_friend_by_number(self, num):
|
def get_friend_by_number(self, num):
|
||||||
return filter(lambda x: x.number == num, self._friends)[0]
|
return filter(lambda x: x.number == num, self._friends)[0]
|
||||||
|
|
||||||
|
def get_friend(self, num):
|
||||||
|
return self._friends[num]
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------------------------------
|
||||||
# Work with active friend
|
# Work with active friend
|
||||||
# -----------------------------------------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------------------------------------
|
||||||
|
@ -557,7 +561,10 @@ class Profile(Contact, Singleton):
|
||||||
Send message to active friend
|
Send message to active friend
|
||||||
:param text: message text
|
: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 '):
|
if text.startswith('/me '):
|
||||||
message_type = TOX_MESSAGE_TYPE['ACTION']
|
message_type = TOX_MESSAGE_TYPE['ACTION']
|
||||||
text = text[4:]
|
text = text[4:]
|
||||||
|
@ -1024,12 +1031,15 @@ class Profile(Contact, Singleton):
|
||||||
st.set_state_changed_handler(item.update)
|
st.set_state_changed_handler(item.update)
|
||||||
self._messages.scrollToBottom()
|
self._messages.scrollToBottom()
|
||||||
|
|
||||||
def send_file(self, path):
|
def send_file(self, path, number=None):
|
||||||
"""
|
"""
|
||||||
Send file to current active friend
|
Send file to current active friend
|
||||||
:param path: file path
|
: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)
|
st = SendTransfer(path, self._tox, friend_number)
|
||||||
self._file_transfers[(friend_number, st.get_file_number())] = st
|
self._file_transfers[(friend_number, st.get_file_number())] = st
|
||||||
tm = TransferMessage(MESSAGE_OWNER['ME'],
|
tm = TransferMessage(MESSAGE_OWNER['ME'],
|
||||||
|
|
|
@ -79,7 +79,8 @@ class Settings(Singleton, dict):
|
||||||
'friends_aliases': [],
|
'friends_aliases': [],
|
||||||
'typing_notifications': False,
|
'typing_notifications': False,
|
||||||
'calls_sound': True,
|
'calls_sound': True,
|
||||||
'blocked': []
|
'blocked': [],
|
||||||
|
'plugins': []
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
Loading…
Reference in a new issue