Lua modules, storage

This commit is contained in:
fomys 2019-06-10 17:46:33 +02:00
parent 5dac2114ff
commit c99bc0a723
15 changed files with 318 additions and 240 deletions

47
main.py
View File

@ -34,6 +34,20 @@ class Module:
self.name = name
MODULES.update({self.name: self})
@property
def type(self) -> str:
"""
Return module type. It can be python or lua
:return: Module type
:rtype: str
"""
if not os.path.exists(os.path.join("modules", self.name, "version.json")):
return ""
with open(os.path.join("modules", self.name, "version.json")) as file:
versions = json.load(file)
return versions["type"]
@property
def exists(self) -> bool:
"""
@ -65,6 +79,8 @@ class Module:
return False
if "bot_version" not in versions.keys():
return False
if "type" not in versions.keys():
return False
return True
@property
@ -274,21 +290,34 @@ class LBI(discord.Client):
for dep in deps.keys():
if dep not in self.modules.keys():
self.load_module(dep)
try:
self.info("Start loading module {module}...".format(module=module))
imported = importlib.import_module('modules.' + module)
if MODULES[module].type == "python":
try:
self.info("Start loading module {module}...".format(module=module))
imported = importlib.import_module('modules.' + module)
importlib.reload(imported)
initialized_class = imported.MainClass(self)
self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}})
self.info("Module {module} successfully imported.".format(module=module))
initialized_class.dispatch("load")
if module not in self.config["modules"]:
self.config["modules"].append(module)
self.save_config()
except AttributeError as e:
self.error("Module {module} doesn't have MainClass.".format(module=module))
return e
return 0
elif MODULES[module].type == "lua":
self.info(f"Start loading module {module}...")
imported = importlib.import_module('modules.base.BaseLua')
importlib.reload(imported)
initialized_class = imported.MainClass(self)
initialized_class = imported.BaseClassLua(self, path=f"modules/{module}/main")
self.modules.update({module: {"imported": imported, "initialized_class": initialized_class}})
self.info("Module {module} successfully imported.".format(module=module))
self.info(f"Module {module} successfully imported.")
initialized_class.dispatch("load")
if module not in self.config["modules"]:
self.config["modules"].append(module)
self.save_config()
except AttributeError as e:
self.error("Module {module} doesn't have MainClass.".format(module=module))
return e
return 0
return 0
@modules_edit
def unload_module(self, module):

195
modules/base/Base.py Normal file
View File

@ -0,0 +1,195 @@
"""Base class for module, never use directly !!!"""
import asyncio
import sys
import pickle
import traceback
import discord
from storage import FSStorage, FSObjects
import storage.path as path
class BaseClass:
"""Base class for all modules, Override it to make submodules"""
name = ""
help = {
"description": "",
"commands": {
}
}
help_active = False
color = 0x000000
command_text = None
super_users = []
authorized_roles = []
def __init__(self, client):
"""Initialize module class
Initialize module class, always call it to set self.client when you override it.
:param client: client instance
:type client: NikolaTesla"""
self.client = client
self.storage = FSStorage(path.join(self.client.base_path, self.name))
self.objects = FSObjects(self.storage)
if not self.storage.isdir(path.join("storage", self.name)):
self.storage.makedirs(path.join("storage", self.name), exist_ok=True)
async def send_help(self, channel):
embed = discord.Embed(
title="[{nom}] - Aide".format(nom=self.name),
description=self.help["description"].format(prefix=self.client.config['prefix']),
color=self.color
)
for command, description in self.help["commands"].items():
embed.add_field(name=command.format(prefix=self.client.config['prefix'], command=self.command_text),
value=description.format(prefix=self.client.config['prefix'], command=self.command_text),
inline=False)
await channel.send(embed=embed)
async def auth(self, user, role_list):
if type(role_list) == list:
if user.id in self.client.owners:
return True
for guild in self.client.guilds:
if guild.get_member(user.id):
for role_id in role_list:
if role_id in [r.id for r in guild.get_member(user.id).roles]:
return True
elif type(role_list) == str:
module_name = role_list
if user.id in self.client.owners:
return True
authorized_roles = self.client.modules[module_name]["class"].authorized_roles
if len(authorized_roles):
for guild in self.client.guilds:
if guild.get_member(user.id):
for role_id in authorized_roles:
if role_id in [r.id for r in guild.get_member(user.id).roles]:
return True
else:
return True
return False
async def parse_command(self, message):
"""Parse a command_text from received message and execute function
%git update
com_update(m..)
Parse message like `{prefix}{command_text} subcommand` and call class method `com_{subcommand}`.
:param message: message to parse
:type message: discord.Message"""
if message.content.startswith(self.client.config["prefix"] + (self.command_text if self.command_text else "")):
content = message.content.lstrip(
self.client.config["prefix"] + (self.command_text if self.command_text else ""))
sub_command, args, kwargs = self._parse_command_content(content)
sub_command = "com_" + sub_command
if sub_command in dir(self):
await self.__getattribute__(sub_command)(message, args, kwargs)
else:
await self.command(message, [sub_command[4:]] + args, kwargs)
@staticmethod
def _parse_command_content(content):
"""Parse string
Parse string like `subcommand argument "argument with spaces" -o -shortwaytopassoncharacteroption --longoption
-o "option with argument"`. You can override this function to change parsing.
:param content: content to parse
:type content: str
:return: parsed arguments: [subcommand, [arg1, arg2, ...], [(option1, arg1), (option2, arg2), ...]]
:rtype: list[str, list, list]"""
if not len(content.split()):
return "", [], []
# Sub_command
sub_command = content.split()[0]
args_ = []
kwargs = []
if len(content.split()) > 1:
# Take the other part of command_text
content = content.split(" ", 1)[1].replace("\"", "\"\"")
# Splitting around quotes
quotes = [element.split("\" ") for element in content.split(" \"")]
# Split all sub chains but brute chains and flat the resulting list
args = [item.split() if item[0] != "\"" else [item, ] for sublist in quotes for item in sublist]
# Second plating
args = [item for sublist in args for item in sublist]
# args_ are arguments, kwargs are options with arguments
i = 0
while i < len(args):
if args[i].startswith("\""):
args_.append(args[i][1:-1])
elif args[i].startswith("--"):
if i + 1 >= len(args):
kwargs.append((args[i].lstrip("-"), None))
break
if args[i + 1][0] != "-":
kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\"")))
i += 1
else:
kwargs.append((args[i].lstrip("-"), None))
elif args[i].startswith("-"):
if len(args[i]) == 2:
if i + 1 >= len(args):
break
if args[i + 1][0] != "-":
kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\"")))
i += 1
else:
kwargs.append((args[i].lstrip("-"), None))
else:
kwargs.extend([(arg, None) for arg in args[i][1:]])
else:
args_.append(args[i])
i += 1
return sub_command, args_, kwargs
async def on_message(self, message):
"""Override this function to deactivate command_text parsing"""
await self.parse_command(message)
async def command(self, message, args, kwargs):
"""Override this function to handle all messages starting with `{prefix}{command_text}`
Function which is executed for all command_text doesn't match with a `com_{subcommand}` function"""
pass
def dispatch(self, event, *args, **kwargs):
# Method to call
method = 'on_' + event
try:
# Try to get coro, if not exists pass without raise an error
coro = getattr(self, method)
except AttributeError:
pass
else:
# Run event
asyncio.ensure_future(self._run_event(coro, method, *args, **kwargs), loop=self.client.loop)
async def _run_event(self, coro, event_name, *args, **kwargs):
# Run event
try:
await coro(*args, **kwargs)
except asyncio.CancelledError:
# If function is cancelled pass silently
pass
except Exception:
try:
# Call error function
await self.on_error(event_name, *args, **kwargs)
except asyncio.CancelledError:
# If error event is canceled pass silently
pass
async def on_error(self, event_method, *args, **kwargs):
# Basic error handler
print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
traceback.print_exc()

View File

@ -1,5 +1,6 @@
"""Base class for module, never use directly !!!"""
import asyncio
import os
import sys
import pickle
import traceback
@ -7,11 +8,12 @@ import traceback
import discord
import lupa
from modules.base.Base import BaseClass
from storage import FSStorage
import storage.path as path
class BaseClassLua:
class BaseClassLua(BaseClass):
"""Base class for all modules, Override it to make submodules"""
name = ""
help = {
@ -26,22 +28,48 @@ class BaseClassLua:
super_users = []
authorized_roles = []
def __init__(self, client):
def __init__(self, client, path):
"""Initialize module class
Initialize module class, always call it to set self.client when you override it.
:param client: client instance
:type client: NikolaTesla"""
self.client = client
self.storage = FSStorage(path.join(self.client.base_path, self.name))
if not self.storage.isdir(path.join("storage", self.name)):
self.storage.makedirs(path.join("storage", self.name), exist_ok=True)
super().__init__(client)
# Get lua globals
self.lua = lupa.LuaRuntime(unpack_returned_tuples=True)
self.luaMethods = self.lua.require("modules/test_lua/main")
print(os.path.abspath(path))
self.luaMethods = self.lua.require(path)
def dispatch(self, event, *args, **kwargs):
if self.luaMethods["on_"+event] is not None:
self.luaMethods["on_"+event](asyncio.ensure_future, self.client, *args, **kwargs)
method = "on_"+event
if self.luaMethods[method] is not None:
self.luaMethods[method](asyncio.ensure_future, self, *args, **kwargs)
else: # If lua methods not found, dispatch to python methods
super().dispatch(event, *args, **kwargs)
async def _run_event(self, coro, event_name, *args, **kwargs):
# Overide here to execute lua on_error if it exists
# Run event
try:
await coro(*args, **kwargs)
except asyncio.CancelledError:
# If function is cancelled pass silently
pass
except Exception:
try:
# Call error function
if self.luaMethods["on_error"] is not None:
self.luaMethods["on_error"](self, asyncio.ensure_future, discord, *args, **kwargs)
else:
await self.on_error(event_name, *args, **kwargs)
except asyncio.CancelledError:
# If error event is canceled pass silently
pass
async def on_error(self, event_method, *args, **kwargs):
# Base on_error event, executed if lua not provide it
# Basic error handler
print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
traceback.print_exc()

View File

@ -1,209 +1,8 @@
"""Base class for module, never use directly !!!"""
import asyncio
import sys
import pickle
import traceback
import discord
from storage import FSStorage
import storage.path as path
from modules.base.Base import BaseClass
class BaseClassPython:
class BaseClassPython(BaseClass):
"""Base class for all modules, Override it to make submodules"""
name = ""
help = {
"description": "",
"commands": {
}
}
help_active = False
color = 0x000000
command_text = None
super_users = []
authorized_roles = []
def __init__(self, client):
"""Initialize module class
Initialize module class, always call it to set self.client when you override it.
:param client: client instance
:type client: NikolaTesla"""
self.client = client
self.storage = FSStorage(path.join(self.client.base_path, self.name))
if not self.storage.isdir(path.join("storage", self.name)):
self.storage.makedirs(path.join("storage", self.name), exist_ok=True)
async def send_help(self, channel):
embed = discord.Embed(
title="[{nom}] - Aide".format(nom=self.name),
description=self.help["description"].format(prefix=self.client.config['prefix']),
color=self.color
)
for command, description in self.help["commands"].items():
embed.add_field(name=command.format(prefix=self.client.config['prefix'], command=self.command_text),
value=description.format(prefix=self.client.config['prefix'], command=self.command_text),
inline=False)
await channel.send(embed=embed)
async def auth(self, user, role_list):
if type(role_list) == list:
if user.id in self.client.owners:
return True
for guild in self.client.guilds:
if guild.get_member(user.id):
for role_id in role_list:
if role_id in [r.id for r in guild.get_member(user.id).roles]:
return True
elif type(role_list) == str:
module_name = role_list
if user.id in self.client.owners:
return True
authorized_roles = self.client.modules[module_name]["class"].authorized_roles
if len(authorized_roles):
for guild in self.client.guilds:
if guild.get_member(user.id):
for role_id in authorized_roles:
if role_id in [r.id for r in guild.get_member(user.id).roles]:
return True
else:
return True
return False
async def parse_command(self, message):
"""Parse a command_text from received message and execute function
%git update
com_update(m..)
Parse message like `{prefix}{command_text} subcommand` and call class method `com_{subcommand}`.
:param message: message to parse
:type message: discord.Message"""
if message.content.startswith(self.client.config["prefix"] + (self.command_text if self.command_text else "")):
content = message.content.lstrip(
self.client.config["prefix"] + (self.command_text if self.command_text else ""))
sub_command, args, kwargs = self._parse_command_content(content)
sub_command = "com_" + sub_command
if sub_command in dir(self):
await self.__getattribute__(sub_command)(message, args, kwargs)
else:
await self.command(message, [sub_command[4:]] + args, kwargs)
@staticmethod
def _parse_command_content(content):
"""Parse string
Parse string like `subcommand argument "argument with spaces" -o -shortwaytopassoncharacteroption --longoption
-o "option with argument"`. You can override this function to change parsing.
:param content: content to parse
:type content: str
:return: parsed arguments: [subcommand, [arg1, arg2, ...], [(option1, arg1), (option2, arg2), ...]]
:rtype: list[str, list, list]"""
if not len(content.split()):
return "", [], []
# Sub_command
sub_command = content.split()[0]
args_ = []
kwargs = []
if len(content.split()) > 1:
# Take the other part of command_text
content = content.split(" ", 1)[1].replace("\"", "\"\"")
# Splitting around quotes
quotes = [element.split("\" ") for element in content.split(" \"")]
# Split all sub chains but brute chains and flat the resulting list
args = [item.split() if item[0] != "\"" else [item, ] for sublist in quotes for item in sublist]
# Second plating
args = [item for sublist in args for item in sublist]
# args_ are arguments, kwargs are options with arguments
i = 0
while i < len(args):
if args[i].startswith("\""):
args_.append(args[i][1:-1])
elif args[i].startswith("--"):
if i + 1 >= len(args):
kwargs.append((args[i].lstrip("-"), None))
break
if args[i + 1][0] != "-":
kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\"")))
i += 1
else:
kwargs.append((args[i].lstrip("-"), None))
elif args[i].startswith("-"):
if len(args[i]) == 2:
if i + 1 >= len(args):
break
if args[i + 1][0] != "-":
kwargs.append((args[i].lstrip("-"), args[i + 1].strip("\"")))
i += 1
else:
kwargs.append((args[i].lstrip("-"), None))
else:
kwargs.extend([(arg, None) for arg in args[i][1:]])
else:
args_.append(args[i])
i += 1
return sub_command, args_, kwargs
async def on_message(self, message):
"""Override this function to deactivate command_text parsing"""
await self.parse_command(message)
async def command(self, message, args, kwargs):
"""Override this function to handle all messages starting with `{prefix}{command_text}`
Function which is executed for all command_text doesn't match with a `com_{subcommand}` function"""
pass
def save_object(self, object_instance, object_name):
"""Save object into pickle file"""
with self.storage.open(object_name, "wb") as f:
pickler = pickle.Pickler(f)
pickler.dump(object_instance)
def load_object(self, object_name):
"""Load object from pickle file"""
if self.save_exists(object_name):
with self.storage.open(object_name, "rb") as f:
unpickler = pickle.Unpickler(f)
return unpickler.load()
def save_exists(self, object_name):
"""Check if pickle file exists"""
return self.storage.exists(object_name)
def dispatch(self, event, *args, **kwargs):
# Method to call
method = 'on_' + event
try:
# Try to get coro, if not exists pass without raise an error
coro = getattr(self, method)
except AttributeError:
pass
else:
# Run event
asyncio.ensure_future(self._run_event(coro, method, *args, **kwargs), loop=self.client.loop)
async def _run_event(self, coro, event_name, *args, **kwargs):
# Run event
try:
await coro(*args, **kwargs)
except asyncio.CancelledError:
# If function is cancelled pass silently
pass
except Exception:
try:
# Call error function
await self.on_error(event_name, *args, **kwargs)
except asyncio.CancelledError:
# If error event is canceled pass silently
pass
async def on_error(self, event_method, *args, **kwargs):
# Basic error handler
print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
traceback.print_exc()
pass

View File

@ -1,5 +1,6 @@
{
"version":"0.1.0",
"type": "python",
"dependencies": {
},

View File

@ -32,8 +32,8 @@ class MainClass(BaseClassPython):
self.icon = ""
async def on_ready(self):
if self.save_exists('errorsDeque'):
self.errorsDeque = self.load_object('errorsDeque')
if self.objects.save_exists('errorsDeque'):
self.errorsDeque = self.objects.load_object('errorsDeque')
else:
self.errorsDeque = collections.deque()
for i in range(len(self.errorsDeque)):
@ -44,7 +44,7 @@ class MainClass(BaseClassPython):
await delete_message.delete()
except:
raise
self.save_object(self.errorsDeque, 'errorsDeque')
self.objects.save_object(self.errorsDeque, 'errorsDeque')
async def command(self, message, args, kwargs):
raise Exception("Si cette erreur apparait, alors tout est normal")
@ -72,7 +72,7 @@ class MainClass(BaseClassPython):
embed=embed.set_footer(text="Ce message ne s'autodétruira pas.", icon_url=self.icon))
except:
pass
self.save_object(self.errorsDeque, 'errorsDeque')
self.objects.save_object(self.errorsDeque, 'errorsDeque')
await asyncio.sleep(60)
try:
channel = self.client.get_channel(message_list[0])
@ -85,4 +85,4 @@ class MainClass(BaseClassPython):
self.errorsDeque.remove(message_list)
except ValueError:
pass
self.save_object(self.errorsDeque, 'errorsDeque')
self.objects.save_object(self.errorsDeque, 'errorsDeque')

View File

@ -1,5 +1,6 @@
{
"version": "0.1.0",
"type": "python",
"dependencies": {
"base": {
"min": "0.1.0",

View File

@ -17,7 +17,7 @@ class MainClass(BaseClassPython):
"`{prefix}{command} list`": "List of available modules.",
"`{prefix}{command} enable <module>`": "Enable module `<module>`.",
"`{prefix}{command} disable <module>`": "Disable module `<module>`.",
"`{prefix}{command} reload <module>`":"Reload module `<module>`",
"`{prefix}{command} reload <module>`": "Reload module `<module>`",
"`{prefix}{command} web_list`": "List all available modules from repository",
}
}

View File

@ -1,5 +1,6 @@
{
"version": "0.1.0",
"type": "python",
"dependencies": {
"base": {
"min": "0.1.0",

View File

@ -1,5 +0,0 @@
from modules.base import BaseClassLua
class MainClass(BaseClassLua):
pass

View File

@ -1,11 +1,8 @@
main = {}
function main.on_message(await, client, message, ...)
print("I LOVE LUA")
print(message.content)
function main.on_message(self, await, discord, message)
if message.author.bot == false then
await(message.channel.send("Tu n'es pas un bot"))
end
end

View File

@ -1,5 +1,6 @@
{
"version": "0.1.0",
"type":"lua",
"dependencies": {
"base": {
"min": "0.1.0",

View File

@ -1,7 +1,6 @@
import os
from storage.base import Storage
from storage.base import Storage, Objects
class FSStorage(Storage):
"""
@ -73,3 +72,6 @@ class FSStorage(Storage):
def isdir(self, path):
return os.path.isdir(self._topath(path))
class FSObjects(Objects):
pass

View File

@ -1 +1,2 @@
from storage.FileSystem import FSStorage
from storage.FileSystem import FSObjects

View File

@ -1,3 +1,8 @@
import pickle
from storage import path as pth
class Storage:
"""Basic class for storage interface
@ -52,6 +57,7 @@ class Storage:
async def makedirs(self, path, exist_ok=False):
"""
Create directory `path`
:param exist_ok: Not return error if dir exists
:param path: directory to create
:return: Path to new directory
"""
@ -113,3 +119,25 @@ class Storage:
:return: True if path is a directory
"""
pass
class Objects:
def __init__(self, storage):
self.storage = storage
def save_object(self, object_name, object_instance):
"""Save object into pickle file"""
with self.storage.open(pth.join("objects", object_name), "wb") as f:
pickler = pickle.Pickler(f)
pickler.dump(object_instance)
def load_object(self, object_name):
"""Load object from pickle file"""
if self.save_exists(object_name):
with self.storage.open(pth.join("objects", object_name), "rb") as f:
unpickler = pickle.Unpickler(f)
return unpickler.load()
def save_exists(self, object_name):
"""Check if pickle file exists"""
return self.storage.exists(pth.join("objects", object_name))