614 lines
23 KiB
Python
Executable File
614 lines
23 KiB
Python
Executable File
'''
|
|
Onionr - Private P2P Communication
|
|
|
|
This plugin acts as a plugin manager, and allows the user to install other plugins distributed over Onionr.
|
|
'''
|
|
'''
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
# useful libraries
|
|
import logger, config
|
|
import os, sys, json, time, random, shutil, base64, getpass, datetime, re
|
|
from onionrblockapi import Block
|
|
|
|
plugin_name = 'pluginmanager'
|
|
|
|
keys_data = {'keys' : {}, 'plugins' : [], 'repositories' : {}}
|
|
|
|
# key functions
|
|
|
|
def writeKeys():
|
|
'''
|
|
Serializes and writes the keystore in memory to file
|
|
'''
|
|
|
|
with open(keys_file, 'w') as file:
|
|
file.write(json.dumps(keys_data, indent=4, sort_keys=True))
|
|
file.close()
|
|
|
|
def readKeys():
|
|
'''
|
|
Loads the keystore into memory
|
|
'''
|
|
|
|
global keys_data
|
|
with open(keys_file) as file:
|
|
keys_data = json.loads(file.read())
|
|
return keys_data
|
|
|
|
def getKey(plugin):
|
|
'''
|
|
Returns the public key for a given plugin
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
return (keys_data['keys'][plugin] if plugin in keys_data['keys'] else None)
|
|
|
|
def saveKey(plugin, key):
|
|
'''
|
|
Saves the public key for a plugin to keystore
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
keys_data['keys'][plugin] = key
|
|
writeKeys()
|
|
|
|
def getPlugins():
|
|
'''
|
|
Returns a list of plugins installed by the plugin manager
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
return keys_data['plugins']
|
|
|
|
def addPlugin(plugin):
|
|
'''
|
|
Saves the plugin name, to remember that it was installed by the pluginmanager
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
if not plugin in keys_data['plugins']:
|
|
keys_data['plugins'].append(plugin)
|
|
writeKeys()
|
|
|
|
def removePlugin(plugin):
|
|
'''
|
|
Removes the plugin name from the pluginmanager's records
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
if plugin in keys_data['plugins']:
|
|
keys_data['plugins'].remove(plugin)
|
|
writeKeys()
|
|
|
|
def getRepositories():
|
|
'''
|
|
Returns a list of plugins installed by the plugin manager
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
return keys_data['repositories']
|
|
|
|
def addRepository(blockhash, data):
|
|
'''
|
|
Saves the plugin name, to remember that it was installed by the pluginmanager
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
keys_data['repositories'][blockhash] = data
|
|
writeKeys()
|
|
|
|
def removeRepository(blockhash):
|
|
'''
|
|
Removes the plugin name from the pluginmanager's records
|
|
'''
|
|
|
|
global keys_data
|
|
readKeys()
|
|
if blockhash in keys_data['repositories']:
|
|
del keys_data['repositories'][blockhash]
|
|
writeKeys()
|
|
|
|
def createRepository(plugins):
|
|
contents = {'plugins' : plugins, 'author' : getpass.getuser(), 'compiled-by' : plugin_name}
|
|
|
|
block = Block(core = pluginapi.get_core())
|
|
|
|
block.setType('repository')
|
|
block.setContent(json.dumps(contents))
|
|
|
|
return block.save(True)
|
|
|
|
def check():
|
|
'''
|
|
Checks to make sure the keystore file still exists
|
|
'''
|
|
|
|
global keys_file
|
|
keys_file = pluginapi.plugins.get_data_folder(plugin_name) + 'keystore.json'
|
|
if not os.path.isfile(keys_file):
|
|
writeKeys()
|
|
|
|
# plugin management
|
|
|
|
def sanitize(name):
|
|
return re.sub('[^0-9a-zA-Z_]+', '', str(name).lower())[:255]
|
|
|
|
def blockToPlugin(block):
|
|
try:
|
|
block = Block(block, core = pluginapi.get_core())
|
|
blockContent = json.loads(block.getContent())
|
|
|
|
name = sanitize(blockContent['name'])
|
|
author = blockContent['author']
|
|
date = blockContent['date']
|
|
version = None
|
|
|
|
if 'version' in blockContent['info']:
|
|
version = blockContent['info']['version']
|
|
|
|
content = base64.b64decode(blockContent['content'].encode())
|
|
|
|
source = pluginapi.plugins.get_data_folder(plugin_name) + 'plugin.zip'
|
|
destination = pluginapi.plugins.get_folder(name)
|
|
|
|
with open(source, 'wb') as f:
|
|
f.write(content)
|
|
|
|
if os.path.exists(destination) and not os.path.isfile(destination):
|
|
shutil.rmtree(destination)
|
|
|
|
shutil.unpack_archive(source, destination)
|
|
pluginapi.plugins.enable(name)
|
|
|
|
logger.info('Installation of %s complete.' % name, terminal=True)
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True)
|
|
|
|
return False
|
|
|
|
def pluginToBlock(plugin, import_block = True):
|
|
try:
|
|
plugin = sanitize(plugin)
|
|
|
|
directory = pluginapi.get_pluginapi().get_folder(plugin)
|
|
data_directory = pluginapi.get_pluginapi().get_data_folder(plugin)
|
|
zipfile = pluginapi.get_pluginapi().get_data_folder(plugin_name) + 'plugin.zip'
|
|
|
|
if os.path.exists(directory) and not os.path.isfile(directory):
|
|
if os.path.exists(data_directory) and not os.path.isfile(data_directory):
|
|
shutil.rmtree(data_directory)
|
|
if os.path.exists(zipfile) and os.path.isfile(zipfile):
|
|
os.remove(zipfile)
|
|
if os.path.exists(directory + '__pycache__') and not os.path.isfile(directory + '__pycache__'):
|
|
shutil.rmtree(directory + '__pycache__')
|
|
|
|
shutil.make_archive(zipfile[:-4], 'zip', directory)
|
|
data = ''
|
|
with open(zipfile, 'rb') as file:
|
|
data = base64.b64encode(file.read())
|
|
|
|
author = getpass.getuser()
|
|
description = 'Default plugin description'
|
|
info = {"name" : plugin}
|
|
try:
|
|
if os.path.exists(directory + 'info.json'):
|
|
info = ''
|
|
with open(directory + 'info.json').read() as file:
|
|
info = json.loads(file.read())
|
|
|
|
if 'author' in info:
|
|
author = info['author']
|
|
if 'description' in info:
|
|
description = info['description']
|
|
except:
|
|
pass
|
|
|
|
metadata = {'author' : author, 'date' : str(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), 'name' : plugin, 'info' : info, 'compiled-by' : plugin_name, 'content' : data.decode('utf-8'), 'description' : description}
|
|
|
|
block = Block(core = pluginapi.get_core())
|
|
|
|
block.setType('plugin')
|
|
block.setContent(json.dumps(metadata))
|
|
|
|
hash = block.save(True)
|
|
# hash = pluginapi.get_core().insertBlock(, header = 'plugin', sign = True)
|
|
|
|
if import_block:
|
|
pluginapi.get_utils().importNewBlocks()
|
|
|
|
return hash
|
|
else:
|
|
logger.error('Plugin %s does not exist.' % plugin, terminal=True)
|
|
except Exception as e:
|
|
logger.error('Failed to convert plugin to block.', error = e, timestamp = False, terminal=True)
|
|
|
|
return False
|
|
|
|
def installBlock(block):
|
|
try:
|
|
block = Block(block, core = pluginapi.get_core())
|
|
blockContent = json.loads(block.getContent())
|
|
|
|
name = sanitize(blockContent['name'])
|
|
author = blockContent['author']
|
|
date = blockContent['date']
|
|
version = None
|
|
|
|
if 'version' in blockContent['info']:
|
|
version = blockContent['info']['version']
|
|
|
|
install = False
|
|
|
|
logger.info(('Will install %s' + (' v' + version if not version is None else '') + ' (%s), by %s') % (name, date, author), terminal=True)
|
|
|
|
# TODO: Convert to single line if statement
|
|
if os.path.exists(pluginapi.plugins.get_folder(name)):
|
|
install = logger.confirm(message = 'Continue with installation (will overwrite existing plugin) %s?')
|
|
else:
|
|
install = logger.confirm(message = 'Continue with installation %s?')
|
|
|
|
if install:
|
|
blockToPlugin(block.getHash())
|
|
addPlugin(name)
|
|
else:
|
|
logger.info('Installation cancelled.', terminal=True)
|
|
return False
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error('Failed to install plugin.', error = e, timestamp = False, terminal=True)
|
|
return False
|
|
|
|
def uninstallPlugin(plugin):
|
|
try:
|
|
plugin = sanitize(plugin)
|
|
|
|
pluginFolder = pluginapi.plugins.get_folder(plugin)
|
|
exists = (os.path.exists(pluginFolder) and not os.path.isfile(pluginFolder))
|
|
installedByPluginManager = plugin in getPlugins()
|
|
remove = False
|
|
|
|
if not exists:
|
|
logger.warn('Plugin %s does not exist.' % plugin, timestamp = False, terminal=True)
|
|
return False
|
|
|
|
default = 'y'
|
|
if not installedByPluginManager:
|
|
logger.warn('The plugin %s was not installed by %s.' % (plugin, plugin_name), timestamp = False, terminal=True)
|
|
default = 'n'
|
|
remove = logger.confirm(message = 'All plugin data will be lost. Are you sure you want to proceed %s?', default = default)
|
|
|
|
if remove:
|
|
if installedByPluginManager:
|
|
removePlugin(plugin)
|
|
pluginapi.plugins.disable(plugin)
|
|
shutil.rmtree(pluginFolder)
|
|
|
|
logger.info('Uninstallation of %s complete.' % plugin, terminal=True)
|
|
|
|
return True
|
|
else:
|
|
logger.info('Uninstallation cancelled.')
|
|
except Exception as e:
|
|
logger.error('Failed to uninstall plugin.', error = e, terminal=True)
|
|
return False
|
|
|
|
# command handlers
|
|
|
|
def help():
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]', terminal=True)
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]', terminal=True)
|
|
|
|
def commandInstallPlugin():
|
|
if len(sys.argv) >= 3:
|
|
check()
|
|
|
|
pluginname = sys.argv[2]
|
|
pkobh = None # public key or block hash
|
|
|
|
version = None
|
|
if ':' in pluginname:
|
|
details = pluginname
|
|
pluginname = sanitize(details[0])
|
|
version = details[1]
|
|
|
|
sanitize(pluginname)
|
|
|
|
if len(sys.argv) >= 4:
|
|
# public key or block hash specified
|
|
pkobh = sys.argv[3]
|
|
else:
|
|
# none specified, check if in config file
|
|
pkobh = getKey(pluginname)
|
|
|
|
if pkobh is None:
|
|
# still nothing found, try searching repositories
|
|
logger.info('Searching for public key in repositories...', terminal=True)
|
|
try:
|
|
repos = getRepositories()
|
|
distributors = list()
|
|
for repo, records in repos.items():
|
|
if pluginname in records:
|
|
logger.debug('Found %s in repository %s for plugin %s.' % (records[pluginname], repo, pluginname), terminal=True)
|
|
distributors.append(records[pluginname])
|
|
|
|
if len(distributors) != 0:
|
|
distributor = None
|
|
|
|
if len(distributors) == 1:
|
|
logger.info('Found distributor: %s' % distributors[0], terminal=True)
|
|
distributor = distributors[0]
|
|
else:
|
|
distributors_message = ''
|
|
|
|
index = 1
|
|
for dist in distributors:
|
|
distributors_message += ' ' + logger.colors.bold + str(index) + ') ' + logger.colors.reset + str(dist) + '\n'
|
|
index += 1
|
|
|
|
logger.info((logger.colors.bold + 'Found distributors (%s):' + logger.colors.reset + '\n' + distributors_message) % len(distributors), terminal=True)
|
|
|
|
valid = False
|
|
while not valid:
|
|
choice = logger.readline('Select the number of the key to use, from 1 to %s, or press Ctrl+C to cancel:' % (index - 1), terminal=True)
|
|
|
|
try:
|
|
choice = int(choice)
|
|
if choice <= index and choice >= 1:
|
|
distributor = distributors[int(choice)]
|
|
valid = True
|
|
except KeyboardInterrupt:
|
|
logger.info('Installation cancelled.', terminal=True)
|
|
return True
|
|
except:
|
|
pass
|
|
|
|
if not distributor is None:
|
|
pkobh = distributor
|
|
except Exception as e:
|
|
logger.warn('Failed to lookup plugin in repositories.', timestamp = False, terminal=True)
|
|
return True
|
|
|
|
if pkobh is None:
|
|
logger.error('No key for this plugin found in keystore or repositories, please specify.', timestamp = False, terminal=True)
|
|
|
|
return True
|
|
|
|
valid_hash = pluginapi.get_utils().validateHash(pkobh)
|
|
real_block = False
|
|
valid_key = pluginapi.get_utils().validatePubKey(pkobh)
|
|
real_key = False
|
|
|
|
if valid_hash:
|
|
real_block = Block.exists(pkobh)
|
|
elif valid_key:
|
|
real_key = pluginapi.get_utils().hasKey(pkobh)
|
|
|
|
blockhash = None
|
|
|
|
if valid_hash and not real_block:
|
|
logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True)
|
|
logger.debug('Is valid hash, but does not belong to a known block.', terminal=True)
|
|
|
|
return True
|
|
elif valid_hash and real_block:
|
|
blockhash = str(pkobh)
|
|
logger.debug('Using block %s...' % blockhash, terminal=True)
|
|
|
|
installBlock(blockhash)
|
|
elif valid_key and not real_key:
|
|
logger.error('Public key not found. Try adding the node by address manually, if possible.', timestamp = False, terminal=True)
|
|
logger.debug('Is valid key, but the key is not a known one.', terminal=True)
|
|
elif valid_key and real_key:
|
|
publickey = str(pkobh)
|
|
logger.debug('Using public key %s...' % publickey, terminal=True)
|
|
|
|
saveKey(pluginname, pkobh)
|
|
|
|
signedBlocks = Block.getBlocks(type = 'plugin', signed = True, signer = publickey)
|
|
|
|
mostRecentTimestamp = None
|
|
mostRecentVersionBlock = None
|
|
|
|
for block in signedBlocks:
|
|
try:
|
|
blockContent = json.loads(block.getContent())
|
|
|
|
if not (('author' in blockContent) and ('info' in blockContent) and ('date' in blockContent) and ('name' in blockContent)):
|
|
raise ValueError('Missing required parameter `date` in block %s.' % block.getHash())
|
|
|
|
blockDatetime = datetime.datetime.strptime(blockContent['date'], '%Y-%m-%d %H:%M:%S')
|
|
|
|
if blockContent['name'] == pluginname:
|
|
if ('version' in blockContent['info']) and (blockContent['info']['version'] == version) and (not version is None):
|
|
mostRecentTimestamp = blockDatetime
|
|
mostRecentVersionBlock = block.getHash()
|
|
break
|
|
elif mostRecentTimestamp is None:
|
|
mostRecentTimestamp = blockDatetime
|
|
mostRecentVersionBlock = block.getHash()
|
|
elif blockDatetime > mostRecentTimestamp:
|
|
mostRecentTimestamp = blockDatetime
|
|
mostRecentVersionBlock = block.getHash()
|
|
except Exception as e:
|
|
pass
|
|
|
|
logger.warn('Only continue the installation if you are absolutely certain that you trust the plugin distributor. Public key of plugin distributor: %s' % publickey, timestamp = False, terminal=True)
|
|
logger.debug('Most recent block matching parameters is %s' % mostRecentVersionBlock, terminal=True)
|
|
installBlock(mostRecentVersionBlock)
|
|
else:
|
|
logger.error('Unknown data "%s"; must be public key or block hash.' % str(pkobh), timestamp = False, terminal=True)
|
|
return
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin> [public key/block hash]', terminal=True)
|
|
|
|
return True
|
|
|
|
def commandUninstallPlugin():
|
|
if len(sys.argv) >= 3:
|
|
uninstallPlugin(sys.argv[2])
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>', terminal=True)
|
|
|
|
return True
|
|
|
|
def commandSearchPlugin():
|
|
logger.info('This feature has not been created yet. Please check back later.', terminal=True)
|
|
return True
|
|
|
|
def commandAddRepository():
|
|
if len(sys.argv) >= 3:
|
|
check()
|
|
|
|
blockhash = sys.argv[2]
|
|
|
|
if pluginapi.get_utils().validateHash(blockhash):
|
|
if Block.exists(blockhash):
|
|
try:
|
|
blockContent = json.loads(Block(blockhash, core = pluginapi.get_core()).getContent())
|
|
|
|
pluginslist = dict()
|
|
|
|
for pluginname, distributor in blockContent['plugins']:
|
|
if pluginapi.get_utils().validatePubKey(distributor):
|
|
pluginslist[pluginname] = distributor
|
|
|
|
logger.debug('Found %s records in repository.' % len(pluginslist), terminal=True)
|
|
|
|
if len(pluginslist) != 0:
|
|
addRepository(blockhash, pluginslist)
|
|
logger.info('Successfully added repository.', terminal=True)
|
|
else:
|
|
logger.error('Repository contains no records, not importing.', timestamp = False, terminal=True)
|
|
except Exception as e:
|
|
logger.error('Failed to parse block.', error = e, terminal=True)
|
|
else:
|
|
logger.error('Block hash not found. Perhaps it has not been synced yet?', timestamp = False, terminal=True)
|
|
logger.debug('Is valid hash, but does not belong to a known block.')
|
|
else:
|
|
logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), timestamp = False, terminal=True)
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True)
|
|
|
|
return True
|
|
|
|
def commandRemoveRepository():
|
|
if len(sys.argv) >= 3:
|
|
check()
|
|
|
|
blockhash = sys.argv[2]
|
|
|
|
if pluginapi.get_utils().validateHash(blockhash):
|
|
if blockhash in getRepositories():
|
|
try:
|
|
removeRepository(blockhash)
|
|
logger.info('Successfully removed repository.', terminal=True)
|
|
except Exception as e:
|
|
logger.error('Failed to parse block.', error = e, terminal=True)
|
|
else:
|
|
logger.error('Repository has not been imported, nothing to remove.', timestamp = False, terminal=True)
|
|
else:
|
|
logger.error('Unknown data "%s"; must be block hash.' % str(pkobh), terminal=True)
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [block hash]', terminal=True)
|
|
|
|
return True
|
|
|
|
def commandPublishPlugin():
|
|
if len(sys.argv) >= 3:
|
|
check()
|
|
|
|
pluginname = sanitize(sys.argv[2])
|
|
pluginfolder = pluginapi.plugins.get_folder(pluginname)
|
|
|
|
if os.path.exists(pluginfolder) and not os.path.isfile(pluginfolder):
|
|
block = pluginToBlock(pluginname)
|
|
logger.info('Plugin saved in block %s.' % block, terminal=True)
|
|
else:
|
|
logger.error('Plugin %s does not exist.' % pluginname, timestamp = False, terminal=True)
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' <plugin>', terminal=True)
|
|
|
|
def commandCreateRepository():
|
|
if len(sys.argv) >= 3:
|
|
check()
|
|
|
|
plugins = list()
|
|
script = sys.argv[0]
|
|
|
|
del sys.argv[:2]
|
|
success = True
|
|
for pluginname in sys.argv:
|
|
distributor = None
|
|
|
|
if ':' in pluginname:
|
|
split = pluginname.split(':')
|
|
pluginname = split[0]
|
|
distributor = split[1]
|
|
|
|
pluginname = sanitize(pluginname)
|
|
|
|
if distributor is None:
|
|
distributor = getKey(pluginname)
|
|
if distributor is None:
|
|
logger.error('No distributor key was found for the plugin %s.' % pluginname, timestamp = False, terminal=True)
|
|
success = False
|
|
|
|
plugins.append([pluginname, distributor])
|
|
|
|
if not success:
|
|
logger.error('Please correct the above errors, then recreate the repository.', terminal=True)
|
|
return True
|
|
|
|
blockhash = createRepository(plugins)
|
|
if not blockhash is None:
|
|
logger.info('Successfully created repository. Execute the following command to add the repository:\n ' + logger.colors.underline + '%s --add-repository %s' % (script, blockhash), terminal=True)
|
|
else:
|
|
logger.error('Failed to create repository, an unknown error occurred.', terminal=True)
|
|
else:
|
|
logger.info(sys.argv[0] + ' ' + sys.argv[1] + ' [plugins...]', terminal=True)
|
|
|
|
return True
|
|
|
|
# event listeners
|
|
|
|
def on_init(api, data = None):
|
|
global pluginapi
|
|
pluginapi = api
|
|
check()
|
|
|
|
# register some commands
|
|
api.commands.register(['install-plugin', 'installplugin', 'plugin-install', 'install', 'plugininstall'], commandInstallPlugin)
|
|
api.commands.register(['remove-plugin', 'removeplugin', 'plugin-remove', 'uninstall-plugin', 'uninstallplugin', 'plugin-uninstall', 'uninstall', 'remove', 'pluginremove'], commandUninstallPlugin)
|
|
api.commands.register(['search', 'filter-plugins', 'search-plugins', 'searchplugins', 'search-plugin', 'searchplugin', 'findplugin', 'find-plugin', 'filterplugin', 'plugin-search', 'pluginsearch'], commandSearchPlugin)
|
|
api.commands.register(['add-repo', 'add-repository', 'addrepo', 'addrepository', 'repository-add', 'repo-add', 'repoadd', 'addrepository', 'add-plugin-repository', 'add-plugin-repo', 'add-pluginrepo', 'add-pluginrepository', 'addpluginrepo', 'addpluginrepository'], commandAddRepository)
|
|
api.commands.register(['remove-repo', 'remove-repository', 'removerepo', 'removerepository', 'repository-remove', 'repo-remove', 'reporemove', 'removerepository', 'remove-plugin-repository', 'remove-plugin-repo', 'remove-pluginrepo', 'remove-pluginrepository', 'removepluginrepo', 'removepluginrepository', 'rm-repo', 'rm-repository', 'rmrepo', 'rmrepository', 'repository-rm', 'repo-rm', 'reporm', 'rmrepository', 'rm-plugin-repository', 'rm-plugin-repo', 'rm-pluginrepo', 'rm-pluginrepository', 'rmpluginrepo', 'rmpluginrepository'], commandRemoveRepository)
|
|
api.commands.register(['publish-plugin', 'plugin-publish', 'publishplugin', 'pluginpublish', 'publish'], commandPublishPlugin)
|
|
api.commands.register(['create-repository', 'create-repo', 'createrepo', 'createrepository', 'repocreate'], commandCreateRepository)
|
|
|
|
# add help menus once the features are actually implemented
|
|
|
|
return
|