merge with plugins

This commit is contained in:
ingvar1995 2016-05-28 13:06:13 +03:00
parent b2fa484bb3
commit a214ab12c7
12 changed files with 579 additions and 12 deletions

View file

@ -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)

View file

@ -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

View file

@ -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
# -----------------------------------------------------------------------------------------------------------------

View file

@ -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))

View file

@ -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))

146
src/plugin_support.py Normal file
View 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
View file

View 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

View file

@ -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'],

View file

@ -79,7 +79,8 @@ class Settings(Singleton, dict):
'friends_aliases': [],
'typing_notifications': False,
'calls_sound': True,
'blocked': []
'blocked': [],
'plugins': []
}
@staticmethod