Improve support for plugins
parent
8d8f167d7f
commit
098abb8e55
|
@ -22,7 +22,7 @@ import sqlite3, os, sys, time, math, base64, tarfile, getpass, simplecrypt, hash
|
|||
#from Crypto import Random
|
||||
import netcontroller
|
||||
|
||||
import onionrutils, onionrcrypto, btc
|
||||
import onionrutils, onionrcrypto, btc, onionrevents as events
|
||||
|
||||
if sys.version_info < (3, 6):
|
||||
try:
|
||||
|
@ -89,10 +89,13 @@ class Core:
|
|||
c.execute('INSERT INTO peers (id, name, dateSeen) VALUES(?, ?, ?);', t)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return True
|
||||
|
||||
def addAddress(self, address):
|
||||
'''Add an address to the address database (only tor currently)'''
|
||||
'''
|
||||
Add an address to the address database (only tor currently)
|
||||
'''
|
||||
if self._utils.validateID(address):
|
||||
conn = sqlite3.connect(self.addressDB)
|
||||
c = conn.cursor()
|
||||
|
@ -114,12 +117,17 @@ class Core:
|
|||
c.execute('INSERT INTO adders (address, type) VALUES(?, ?);', t)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
events.event('address_add', data = {'address': address}, onionr = None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def removeAddress(self, address):
|
||||
'''Remove an address from the address database'''
|
||||
'''
|
||||
Remove an address from the address database
|
||||
'''
|
||||
if self._utils.validateID(address):
|
||||
conn = sqlite3.connect(self.addressDB)
|
||||
c = conn.cursor()
|
||||
|
@ -127,6 +135,9 @@ class Core:
|
|||
c.execute('Delete from adders where address=?;', t)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
events.event('address_remove', data = {'address': address}, onionr = None)
|
||||
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
@ -330,6 +341,8 @@ class Core:
|
|||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
events.event('queue_pop', data = {'data': retData}, onionr = None)
|
||||
|
||||
return retData
|
||||
|
||||
def daemonQueueAdd(self, command, data=''):
|
||||
|
@ -345,6 +358,8 @@ class Core:
|
|||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
events.event('queue_push', data = {'command': command, 'data': data}, onionr = None)
|
||||
|
||||
return
|
||||
|
||||
def clearDaemonQueue(self):
|
||||
|
@ -354,11 +369,12 @@ class Core:
|
|||
conn = sqlite3.connect(self.queueDB)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute('delete from commands;')
|
||||
c.execute('DELETE FROM commands;')
|
||||
conn.commit()
|
||||
except:
|
||||
pass
|
||||
conn.close()
|
||||
events.event('queue_clear', onionr = None)
|
||||
|
||||
return
|
||||
|
||||
|
@ -564,4 +580,5 @@ class Core:
|
|||
announceAmount = len(nodeList)
|
||||
for i in range(announceAmount):
|
||||
self.daemonQueueAdd('announceNode', nodeList[i])
|
||||
events.event('introduction', onionr = None)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
'''
|
||||
Default plugin template file
|
||||
Generated on $date by $user.
|
||||
'''
|
||||
|
||||
# Imports some useful libraries
|
||||
import logger, config
|
||||
|
||||
def on_init(api, data = None):
|
||||
'''
|
||||
This event is called after Onionr is initialized, but before the command
|
||||
inputted is executed. Could be called when daemon is starting or when
|
||||
just the client is running.
|
||||
'''
|
||||
|
||||
# Doing this makes it so that the other functions can access the api object
|
||||
# by simply referencing the variable `pluginapi`.
|
||||
global pluginapi
|
||||
pluginapi = api
|
||||
|
||||
return
|
||||
|
||||
def on_start(api, data = None):
|
||||
'''
|
||||
This event can be called for multiple reasons:
|
||||
1) The daemon is starting
|
||||
2) The user called `onionr --start-plugins` or `onionr --reload-plugins`
|
||||
3) For whatever reason, the plugins are reloading
|
||||
'''
|
||||
|
||||
return
|
||||
|
||||
def on_stop(api, data = None):
|
||||
'''
|
||||
This event can be called for multiple reasons:
|
||||
1) The daemon is stopping
|
||||
2) The user called `onionr --stop-plugins` or `onionr --reload-plugins`
|
||||
3) For whatever reason, the plugins are reloading
|
||||
'''
|
||||
|
||||
return
|
|
@ -20,8 +20,8 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
import sys, os, base64, random, getpass, shutil, subprocess, requests, time, platform
|
||||
import api, core, config, logger, onionrplugins as plugins
|
||||
import sys, os, base64, random, getpass, shutil, subprocess, requests, time, platform, datetime, re
|
||||
import api, core, config, logger, onionrplugins as plugins, onionrevents as events
|
||||
from onionrutils import OnionrUtils
|
||||
from netcontroller import NetController
|
||||
|
||||
|
@ -137,6 +137,9 @@ class Onionr:
|
|||
'reloadplugin': self.reloadPlugin,
|
||||
'reload-plugins': self.reloadPlugin,
|
||||
'reloadplugins': self.reloadPlugin,
|
||||
'create-plugin': self.createPlugin,
|
||||
'createplugin': self.createPlugin,
|
||||
'plugin-create': self.createPlugin,
|
||||
|
||||
'listkeys': self.listKeys,
|
||||
'list-keys': self.listKeys,
|
||||
|
@ -159,6 +162,7 @@ class Onionr:
|
|||
'addaddr': self.addAddress,
|
||||
'addaddress': self.addAddress,
|
||||
|
||||
'introduce': self.onionrCore.introduceNode,
|
||||
'connect': self.addAddress
|
||||
}
|
||||
|
||||
|
@ -172,14 +176,19 @@ class Onionr:
|
|||
'enable-plugin': 'Enables and starts a plugin',
|
||||
'disable-plugin': 'Disables and stops a plugin',
|
||||
'reload-plugin': 'Reloads a plugin',
|
||||
'create-plugin': 'Creates directory structure for a plugin',
|
||||
'add-peer': 'Adds a peer (?)',
|
||||
'list-peers': 'Displays a list of peers',
|
||||
'add-msg': 'Broadcasts a message to the Onionr network',
|
||||
'pm': 'Adds a private message to block',
|
||||
'get-pms': 'Shows private messages sent to you',
|
||||
'gui': 'Opens a graphical interface for Onionr'
|
||||
'gui': 'Opens a graphical interface for Onionr',
|
||||
'introduce': 'Introduce your node to the public Onionr network (DAEMON MUST BE RUNNING)',
|
||||
}
|
||||
|
||||
# initialize plugins
|
||||
events.event('init', onionr = self)
|
||||
|
||||
command = ''
|
||||
try:
|
||||
command = sys.argv[1].lower()
|
||||
|
@ -238,6 +247,7 @@ class Onionr:
|
|||
'''
|
||||
Executes a command
|
||||
'''
|
||||
|
||||
argument = argument[argument.startswith('--') and len('--'):] # remove -- if it starts with it
|
||||
|
||||
# define commands
|
||||
|
@ -256,6 +266,7 @@ class Onionr:
|
|||
'''
|
||||
Displays the Onionr version
|
||||
'''
|
||||
|
||||
logger.info('Onionr ' + ONIONR_VERSION + ' (' + platform.machine() + ') - API v' + API_VERSION)
|
||||
if verbosity >= 1:
|
||||
logger.info(ONIONR_TAGLINE)
|
||||
|
@ -268,6 +279,7 @@ class Onionr:
|
|||
'''
|
||||
Create a private message and send it
|
||||
'''
|
||||
|
||||
invalidID = True
|
||||
while invalidID:
|
||||
try:
|
||||
|
@ -404,6 +416,34 @@ class Onionr:
|
|||
|
||||
return
|
||||
|
||||
def createPlugin(self):
|
||||
'''
|
||||
Creates the directory structure for a plugin name
|
||||
'''
|
||||
|
||||
if len(sys.argv) >= 3:
|
||||
try:
|
||||
plugin_name = re.sub('[^0-9a-zA-Z]+', '', str(sys.argv[2]).lower())
|
||||
|
||||
if not plugins.exists(plugin_name):
|
||||
logger.info('Creating plugin \"' + plugin_name + '\"...')
|
||||
|
||||
os.makedirs(plugins.get_plugins_folder(plugin_name))
|
||||
with open(plugins.get_plugins_folder(plugin_name) + '/main.py', 'a') as main:
|
||||
main.write(open('default_plugin.txt').read().replace('$user', os.getlogin()).replace('$date', datetime.datetime.now().strftime('%Y-%m-%d')))
|
||||
|
||||
logger.info('Enabling plugin \"' + plugin_name + '\"...')
|
||||
plugins.enable(plugin_name, self)
|
||||
else:
|
||||
logger.warn('Cannot create plugin directory structure; plugin "' + plugin_name + '" exists.')
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Failed to create plugin directory structure.', e)
|
||||
else:
|
||||
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>')
|
||||
|
||||
return
|
||||
|
||||
def notFound(self):
|
||||
'''
|
||||
Displays a "command not found" message
|
||||
|
@ -451,6 +491,7 @@ class Onionr:
|
|||
time.sleep(1)
|
||||
subprocess.Popen(["./communicator.py", "run", str(net.socksPort)])
|
||||
logger.debug('Started communicator')
|
||||
events.event('daemon_start', onionr = self)
|
||||
api.API(self.debug)
|
||||
|
||||
return
|
||||
|
@ -461,6 +502,7 @@ class Onionr:
|
|||
'''
|
||||
|
||||
logger.warn('Killing the running daemon')
|
||||
events.event('daemon_stop', onionr = self)
|
||||
net = NetController(config.get('client')['port'])
|
||||
try:
|
||||
self.onionrUtils.localCommand('shutdown')
|
||||
|
|
|
@ -20,19 +20,20 @@
|
|||
|
||||
import config, logger, onionrplugins as plugins, onionrpluginapi as pluginapi
|
||||
|
||||
def get_pluginapi(onionr):
|
||||
return pluginapi.PluginAPI(onionr)
|
||||
def get_pluginapi(onionr, data):
|
||||
return pluginapi.pluginapi(onionr, data)
|
||||
|
||||
def event(event_name, data = None, onionr = None):
|
||||
def event(event_name, data = {}, onionr = None):
|
||||
'''
|
||||
Calls an event on all plugins (if defined)
|
||||
'''
|
||||
|
||||
for plugin in plugins.get_enabled_plugins():
|
||||
try:
|
||||
call(plugins.get_plugin(plugin), event_name, data, self.get_pluginapi(onionr))
|
||||
except:
|
||||
call(plugins.get_plugin(plugin), event_name, data, get_pluginapi(onionr, data))
|
||||
except Exception as e:
|
||||
logger.warn('Event \"' + event_name + '\" failed for plugin \"' + plugin + '\".')
|
||||
logger.debug(str(e))
|
||||
|
||||
def call(plugin, event_name, data = None, pluginapi = None):
|
||||
'''
|
||||
|
@ -45,12 +46,12 @@ def call(plugin, event_name, data = None, pluginapi = None):
|
|||
|
||||
# TODO: Use multithreading perhaps?
|
||||
if hasattr(plugin, attribute):
|
||||
logger.debug('Calling event ' + str(event_name))
|
||||
getattr(plugin, attribute)(pluginapi, data)
|
||||
#logger.debug('Calling event ' + str(event_name))
|
||||
getattr(plugin, attribute)(pluginapi)
|
||||
|
||||
return True
|
||||
except:
|
||||
logger.warn('Failed to call event ' + str(event_name) + ' on module.')
|
||||
except Exception as e:
|
||||
logger.debug(str(e))
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
|
@ -20,131 +20,135 @@
|
|||
|
||||
import onionrplugins as plugins, logger
|
||||
|
||||
class DaemonAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def start(self):
|
||||
self.pluginapi.get_onionr().daemon()
|
||||
|
||||
return
|
||||
|
||||
def stop(self):
|
||||
self.pluginapi.get_onionr().killDaemon()
|
||||
|
||||
return
|
||||
|
||||
def queue(self, command, data = ''):
|
||||
self.pluginapi.get_core().daemonQueueAdd(command, data)
|
||||
|
||||
return
|
||||
|
||||
def local_command(self, command):
|
||||
self.pluginapi.get_utils().localCommand(self, command)
|
||||
|
||||
return
|
||||
|
||||
def queue_pop(self):
|
||||
return self.get_core().daemonQueue()
|
||||
|
||||
class PluginAPI:
|
||||
def __init__(self, onionr):
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def start(self, name):
|
||||
plugins.start(name)
|
||||
|
||||
def stop(self, name):
|
||||
plugins.stop(name)
|
||||
|
||||
def reload(self, name):
|
||||
plugins.reload(name)
|
||||
|
||||
def enable(self, name):
|
||||
plugins.enable(name)
|
||||
|
||||
def disable(self, name):
|
||||
plugins.disable(name)
|
||||
|
||||
def is_enabled(self, name):
|
||||
return plugins.is_enabled(name)
|
||||
|
||||
def get_enabled_plugins(self):
|
||||
return plugins.get_enabled_plugins()
|
||||
|
||||
class CommandAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def register(self, names, call = None):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().addCommand(name, call)
|
||||
|
||||
return
|
||||
|
||||
def unregister(self, names):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().delCommand(name)
|
||||
|
||||
return
|
||||
|
||||
def register_help(self, names, description):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().addHelp(name, description)
|
||||
|
||||
return
|
||||
|
||||
def unregister_help(self, names):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().delHelp(name)
|
||||
|
||||
return
|
||||
|
||||
def call(self, name):
|
||||
self.pluginapi.get_onionr().execute(name)
|
||||
|
||||
return
|
||||
|
||||
def get_commands(self):
|
||||
return self.pluginapi.get_onionr().getCommands()
|
||||
|
||||
class pluginapi:
|
||||
def __init__(self, onionr, data):
|
||||
self.onionr = onionr
|
||||
|
||||
self.data = data
|
||||
|
||||
self.daemon = DaemonAPI(self)
|
||||
self.plugin = PluginAPI(self)
|
||||
self.command = CommandAPI(self)
|
||||
|
||||
self.plugins = PluginAPI(self)
|
||||
self.commands = CommandAPI(self)
|
||||
|
||||
def get_onionr(self):
|
||||
return self.onionr
|
||||
|
||||
|
||||
def get_data(self):
|
||||
return self.data
|
||||
|
||||
def get_core(self):
|
||||
return self.get_onionr().onionrCore
|
||||
|
||||
|
||||
def get_utils(self):
|
||||
return self.get_onionr().onionrUtils
|
||||
|
||||
|
||||
def get_daemonapi(self):
|
||||
return self.daemon
|
||||
|
||||
|
||||
def get_pluginapi(self):
|
||||
return self.plugin
|
||||
|
||||
return self.plugins
|
||||
|
||||
def get_commandapi(self):
|
||||
return self.command
|
||||
|
||||
return self.commands
|
||||
|
||||
def is_development_mode(self):
|
||||
return self.get_onionr()._developmentMode
|
||||
|
||||
class DaemonAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def start(self):
|
||||
self.pluginapi.get_onionr().daemon()
|
||||
|
||||
return
|
||||
|
||||
def stop(self):
|
||||
self.pluginapi.get_onionr().killDaemon()
|
||||
|
||||
return
|
||||
|
||||
def queue(self, command, data = ''):
|
||||
self.pluginapi.get_core().daemonQueueAdd(command, data)
|
||||
|
||||
return
|
||||
|
||||
def local_command(self, command):
|
||||
self.pluginapi.get_utils().localCommand(self, command)
|
||||
|
||||
return
|
||||
|
||||
def queue_pop(self):
|
||||
return self.get_core().daemonQueue()
|
||||
|
||||
class PluginAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def start(self, name):
|
||||
plugins.start(name)
|
||||
|
||||
def stop(self, name):
|
||||
plugins.stop(name)
|
||||
|
||||
def reload(self, name):
|
||||
plugins.reload(name)
|
||||
|
||||
def enable(self, name):
|
||||
plugins.enable(name)
|
||||
|
||||
def disable(self, name):
|
||||
plugins.disable(name)
|
||||
|
||||
def is_enabled(self, name):
|
||||
return plugins.is_enabled(name)
|
||||
|
||||
def get_enabled_plugins(self):
|
||||
return plugins.get_enabled_plugins()
|
||||
|
||||
class CommandAPI:
|
||||
def __init__(self, pluginapi):
|
||||
self.pluginapi = pluginapi
|
||||
|
||||
def register(self, names, call = None):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().addCommand(name, call)
|
||||
|
||||
return
|
||||
|
||||
def unregister(self, names):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().delCommand(name)
|
||||
|
||||
return
|
||||
|
||||
def register_help(self, names, description):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().addHelp(name, description)
|
||||
|
||||
return
|
||||
|
||||
def unregister_help(self, names):
|
||||
if isinstance(names, str):
|
||||
names = [names]
|
||||
|
||||
for name in names:
|
||||
self.pluginapi.get_onionr().delHelp(name)
|
||||
|
||||
return
|
||||
|
||||
def call(self, name):
|
||||
self.pluginapi.get_onionr().execute(name)
|
||||
|
||||
return
|
||||
|
||||
def get_commands(self):
|
||||
return self.pluginapi.get_onionr().getCommands()
|
||||
|
|
|
@ -225,5 +225,5 @@ def check():
|
|||
if not os.path.exists(os.path.dirname(get_plugins_folder())):
|
||||
logger.debug('Generating plugin data folder...')
|
||||
os.makedirs(os.path.dirname(get_plugins_folder()))
|
||||
|
||||
|
||||
return
|
||||
|
|
|
@ -129,7 +129,7 @@ class OnionrUtils:
|
|||
logger.error('Failed to read my address.', error=error)
|
||||
return ''
|
||||
|
||||
def localCommand(self, command):
|
||||
def localCommand(self, command, silent = True):
|
||||
'''
|
||||
Send a command to the local http API server, securely. Intended for local clients, DO NOT USE for remote peers.
|
||||
'''
|
||||
|
@ -140,7 +140,8 @@ class OnionrUtils:
|
|||
try:
|
||||
retData = requests.get('http://' + open('data/host.txt', 'r').read() + ':' + str(config.get('client')['port']) + '/client/?action=' + command + '&token=' + str(config.get('client')['client_hmac']) + '&timingToken=' + self.timingToken).text
|
||||
except Exception as error:
|
||||
logger.error('Failed to make local request (command: ' + str(command) + ').', error=error)
|
||||
if not silent:
|
||||
logger.error('Failed to make local request (command: ' + str(command) + ').', error=error)
|
||||
retData = False
|
||||
|
||||
return retData
|
||||
|
|
|
@ -134,7 +134,7 @@ class OnionrTests(unittest.TestCase):
|
|||
if not onionrplugins.exists('test'):
|
||||
os.makedirs(onionrplugins.get_plugins_folder('test'))
|
||||
with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(onionr = None, data = None):\n print('received test event!')\n return True\n\ndef on_start(onionr = None, data = None):\n print('start event called')\n\ndef on_stop(onionr = None, data = None):\n print('stop event called')\n\ndef on_enable(onionr = None, data = None):\n print('enable event called')\n\ndef on_disable(onionr = None, data = None):\n print('disable event called')\n")
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
onionrplugins.enable('test')
|
||||
|
||||
try:
|
||||
|
@ -152,7 +152,7 @@ class OnionrTests(unittest.TestCase):
|
|||
if not onionrplugins.exists('test'):
|
||||
os.makedirs(onionrplugins.get_plugins_folder('test'))
|
||||
with open(onionrplugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(onionr = None, data = None):\n print('received test event!')\n return True\n\ndef on_start(onionr = None, data = None):\n print('start event called')\n\ndef on_stop(onionr = None, data = None):\n print('stop event called')\n\ndef on_enable(onionr = None, data = None):\n print('enable event called')\n\ndef on_disable(onionr = None, data = None):\n print('disable event called')\n")
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
onionrplugins.enable('test')
|
||||
|
||||
try:
|
||||
|
@ -171,7 +171,7 @@ class OnionrTests(unittest.TestCase):
|
|||
if not plugins.exists('test'):
|
||||
os.makedirs(plugins.get_plugins_folder('test'))
|
||||
with open(plugins.get_plugins_folder('test') + '/main.py', 'a') as main:
|
||||
main.write("print('Running')\n\ndef on_test(onionr = None, data = None):\n print('received test event!')\n return True\n\ndef on_start(onionr = None, data = None):\n print('start event called')\n\ndef on_stop(onionr = None, data = None):\n print('stop event called')\n\ndef on_enable(onionr = None, data = None):\n print('enable event called')\n\ndef on_disable(onionr = None, data = None):\n print('disable event called')\n")
|
||||
main.write("print('Running')\n\ndef on_test(pluginapi, data = None):\n print('received test event!')\n return True\n\ndef on_start(pluginapi, data = None):\n print('start event called')\n\ndef on_stop(pluginapi, data = None):\n print('stop event called')\n\ndef on_enable(pluginapi, data = None):\n print('enable event called')\n\ndef on_disable(pluginapi, data = None):\n print('disable event called')\n")
|
||||
plugins.enable('test')
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue