Moved source file from other repo
parent
fda5e1b3d7
commit
e8ee438d65
|
@ -1,11 +1,9 @@
|
||||||
# =====CONFIG=====
|
# =====CONFIG=====
|
||||||
# File to run when make run is called
|
|
||||||
MAIN=main.py
|
|
||||||
# Source directory
|
# Source directory
|
||||||
SRC=frank
|
SRC=frank
|
||||||
# Directory name of the venv
|
# Directory name of the venv
|
||||||
# Don't put spaces in the VENV name, make does not like spaces
|
# Don't put spaces in the VENV name, make does not like spaces
|
||||||
# Run make clean first if you do this after already having created a venv
|
# Run make clean first if you change this after already having created a venv
|
||||||
VENV=venv
|
VENV=venv
|
||||||
# Docs directory
|
# Docs directory
|
||||||
DOCS=docs
|
DOCS=docs
|
||||||
|
@ -13,40 +11,18 @@ DOCS=docs
|
||||||
TESTS=tests
|
TESTS=tests
|
||||||
# Interpreter to create venv with
|
# Interpreter to create venv with
|
||||||
INTERPRETER=python3.8
|
INTERPRETER=python3.8
|
||||||
# Docker image name:tag
|
|
||||||
IMAGE='chewingbever/frank:latest'
|
|
||||||
|
|
||||||
|
|
||||||
all: run
|
all: run
|
||||||
|
|
||||||
|
|
||||||
# Re-create venv when needed
|
# Re-create venv when needed
|
||||||
$(VENV)/bin/activate: requirements.txt
|
$(VENV)/bin/activate: requirements.txt requirements-dev.txt
|
||||||
@ echo "Rebuilding venv..."
|
@ echo "Rebuilding venv..."
|
||||||
@ [ ! -e "$(VENV)" ] || rm -rf "$(VENV)"
|
@ [ ! -e "$(VENV)" ] || rm -rf "$(VENV)"
|
||||||
@ "$(INTERPRETER)" -m venv "$(VENV)"
|
@ "$(INTERPRETER)" -m venv "$(VENV)"
|
||||||
@ "$(VENV)/bin/pip" install -r requirements.txt
|
@ "$(VENV)/bin/pip" install -r requirements.txt -r requirements-dev.txt
|
||||||
|
|
||||||
build: $(VENV)/bin/activate
|
build-venv: $(VENV)/bin/activate
|
||||||
|
|
||||||
# Run script
|
|
||||||
run: build
|
|
||||||
@ "$(VENV)/bin/python" "$(MAIN)"
|
|
||||||
|
|
||||||
# =====DOCKER=====
|
|
||||||
# Build docker image
|
|
||||||
dbuild: docker/Dockerfile
|
|
||||||
@ docker build -f docker/Dockerfile -t $(IMAGE) .
|
|
||||||
|
|
||||||
# Run docker
|
|
||||||
drun: dbuild docker/docker-compose.yml
|
|
||||||
@ docker-compose -f docker/docker-compose.yml up
|
|
||||||
|
|
||||||
# run docker as daemon
|
|
||||||
drund: dbuild docker/docker-compose.yml
|
|
||||||
@ docker-compose -f docker/docker-compose.yml up -d
|
|
||||||
|
|
||||||
dpush: dbuild
|
|
||||||
@ docker push $(IMAGE)
|
|
||||||
|
|
||||||
# =====CLEANING=====
|
# =====CLEANING=====
|
||||||
clean: clean-venv clean-cache clean-docs
|
clean: clean-venv clean-cache clean-docs
|
||||||
|
@ -69,7 +45,7 @@ clean-docs:
|
||||||
|
|
||||||
|
|
||||||
# =====DOCS=====
|
# =====DOCS=====
|
||||||
$(VENV)/bin/sphinx-build: build
|
$(VENV)/bin/sphinx-build: build-venv
|
||||||
@ echo "Installing sphinx..."
|
@ echo "Installing sphinx..."
|
||||||
@ "$(VENV)/bin/pip" install --quiet sphinx
|
@ "$(VENV)/bin/pip" install --quiet sphinx
|
||||||
|
|
||||||
|
@ -79,11 +55,11 @@ docs: $(VENV)/bin/sphinx-build
|
||||||
|
|
||||||
|
|
||||||
# =====TESTS=====
|
# =====TESTS=====
|
||||||
$(VENV)/bin/pytest: build
|
$(VENV)/bin/pytest: build-venv
|
||||||
@ echo "Installing pytest..."
|
@ echo "Installing pytest..."
|
||||||
@ "$(VENV)/bin/pip" install --quiet pytest
|
@ "$(VENV)/bin/pip" install --quiet pytest
|
||||||
|
|
||||||
test: pytest.ini build
|
test: pytest.ini $(VENV)/bin/pytest
|
||||||
@ "$(VENV)/bin/pytest" --color=yes
|
@ "$(VENV)/bin/pytest" --color=yes
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,5 +74,5 @@ package: README.md LICENSE setup.py test
|
||||||
|
|
||||||
# Publish will also come here someday
|
# Publish will also come here someday
|
||||||
|
|
||||||
.PHONY: all run clean clean-venv clean-cache clean-docs test package docs \
|
.PHONY: all clean clean-venv clean-cache clean-docs test package docs \
|
||||||
build dbuild drun dpush drund
|
build-venv run-venv
|
||||||
|
|
|
@ -1,2 +1,18 @@
|
||||||
from .frank import Frank
|
from .frank import Frank
|
||||||
from .module import Module
|
from .module import (
|
||||||
|
Module, command, Command, default, Default, daemon, Daemon,
|
||||||
|
regex_command, RegexCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Frank',
|
||||||
|
'Module',
|
||||||
|
'command',
|
||||||
|
'Command',
|
||||||
|
'default',
|
||||||
|
'Default',
|
||||||
|
'daemon',
|
||||||
|
'Daemon',
|
||||||
|
'regex_command',
|
||||||
|
'RegexCommand',
|
||||||
|
]
|
||||||
|
|
|
@ -1,27 +1,53 @@
|
||||||
|
# =====IMPORTS=====
|
||||||
|
# Future imports
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
import shlex
|
import shlex
|
||||||
from typing import List
|
|
||||||
import discord
|
# Third-party imports
|
||||||
import yaml
|
import yaml
|
||||||
|
import discord
|
||||||
|
|
||||||
|
# Typing imports
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Own imports
|
||||||
|
from .module import Module
|
||||||
|
from discord import Message
|
||||||
|
|
||||||
|
|
||||||
class Frank(discord.Client):
|
class Frank(discord.Client):
|
||||||
PREFIX = "fr"
|
"""
|
||||||
|
Main class of the bot; works by adding modules, which all define
|
||||||
|
their own behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PREFIX = 'fr'
|
||||||
|
"""Prefix to use Frank inside Discord."""
|
||||||
|
|
||||||
|
def __init__(self, modules: List[Module], config_file: str = 'frank.yaml'):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
modules: modules to load
|
||||||
|
config_file: path to yaml config file; ignored if non-existent
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, modules: List["Module"],
|
|
||||||
config_file: str = "frank.yaml"):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._modules = modules
|
self._modules = modules
|
||||||
self._loaded_modules = []
|
self._loaded_modules = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_file, "r") as f:
|
with open(config_file, 'r') as f:
|
||||||
self._config = yaml.load(f, Loader=yaml.FullLoader)
|
self._config = yaml.load(f, Loader=yaml.FullLoader)
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self._config = None
|
self._config = None
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print("Connected")
|
"""Runs when the bot has succesfully connected to Discord"""
|
||||||
|
|
||||||
|
print('Connected')
|
||||||
|
|
||||||
# Startup all modules
|
# Startup all modules
|
||||||
for module in self._modules:
|
for module in self._modules:
|
||||||
|
@ -31,20 +57,36 @@ class Frank(discord.Client):
|
||||||
else:
|
else:
|
||||||
loaded = module(self)
|
loaded = module(self)
|
||||||
|
|
||||||
await loaded.start()
|
await loaded._start()
|
||||||
self._loaded_modules.append(loaded)
|
self._loaded_modules.append(loaded)
|
||||||
|
|
||||||
print("All modules loaded")
|
print('All modules loaded')
|
||||||
|
|
||||||
async def on_message(self, message: str):
|
async def stop(self):
|
||||||
|
"""Stop all module daemons and exit."""
|
||||||
|
|
||||||
|
for module in self._loaded_modules:
|
||||||
|
await module.stop()
|
||||||
|
|
||||||
|
async def on_message(self, message: Message):
|
||||||
|
"""
|
||||||
|
Runs when a new message is sent in the Discord channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: object representing the received message; see
|
||||||
|
https://discordpy.readthedocs.io/en/latest/api.html#message
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
cmd = shlex.split(message.content.strip())
|
cmd = shlex.split(message.content.strip())
|
||||||
|
|
||||||
if cmd[0] == self.PREFIX:
|
except ValueError:
|
||||||
matched_mods = (
|
return
|
||||||
mod for mod in self._loaded_modules if mod.match(cmd[1])
|
|
||||||
)
|
|
||||||
|
|
||||||
module = next(matched_mods, None)
|
if cmd and cmd[0] == self.PREFIX:
|
||||||
|
module = next((mod for mod in self._loaded_modules
|
||||||
|
if mod.match(cmd[1])), None)
|
||||||
|
|
||||||
if module:
|
if module:
|
||||||
await module.command(cmd[2:])
|
await module(cmd=cmd[2:], author=message.author,
|
||||||
|
channel=message.channel, mid=message.id)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
from .module import Module
|
||||||
|
from .decorators import (
|
||||||
|
command, Command, default, Default, daemon, Daemon, regex_command,
|
||||||
|
RegexCommand,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Module',
|
||||||
|
'command',
|
||||||
|
'Command',
|
||||||
|
'default',
|
||||||
|
'Default',
|
||||||
|
'daemon',
|
||||||
|
'Daemon',
|
||||||
|
'regex_command',
|
||||||
|
'RegexCommand',
|
||||||
|
]
|
|
@ -0,0 +1,13 @@
|
||||||
|
from .classes import Command, RegexCommand, Daemon, Default
|
||||||
|
from .functions import command, regex_command, daemon, default
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'command',
|
||||||
|
'Command',
|
||||||
|
'regex_command',
|
||||||
|
'RegexCommand',
|
||||||
|
'default',
|
||||||
|
'Default',
|
||||||
|
'daemon',
|
||||||
|
'Daemon',
|
||||||
|
]
|
|
@ -0,0 +1,120 @@
|
||||||
|
# =====IMPORTS=====
|
||||||
|
# Future imports
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Simple:
|
||||||
|
"""
|
||||||
|
Acts as a base class for all other types; behaves like the given
|
||||||
|
function
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, func: callable):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
func: function to mimic
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.func = func
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
All this call does is call the wrapped function. Because we overwrote
|
||||||
|
__get__, we can pass self to the function, making it behave as a class
|
||||||
|
method of the instance calling it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.func.__call__(self._obj, *args, **kwargs)
|
||||||
|
|
||||||
|
def __get__(self, instance, owner) -> Simple:
|
||||||
|
"""
|
||||||
|
We use __get__ to get the class calling the function. This allows us to
|
||||||
|
pass 'self' to the wrapped function, effectively making this class
|
||||||
|
fully behave as a class method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
instance: instance calling the function
|
||||||
|
owner: type of the function
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._cls = owner
|
||||||
|
self._obj = instance
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class Command(Simple):
|
||||||
|
"""
|
||||||
|
Represents a command of the module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, func: callable, cmd: str, help_str: str = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
func: function to wrap
|
||||||
|
cmd: keyword used to call this function
|
||||||
|
help_str: short description of the command
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(func)
|
||||||
|
|
||||||
|
self.cmd = cmd
|
||||||
|
self.help_str = help_str
|
||||||
|
|
||||||
|
def match(self, prefix: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns wether the command matches the given prefix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prefix: string to match own prefix against
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.cmd == prefix
|
||||||
|
|
||||||
|
|
||||||
|
class RegexCommand(Command):
|
||||||
|
"""
|
||||||
|
A subclass of Command that can use a regex pattern instead of a fixed
|
||||||
|
prefix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def match(self, prefix: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns wether the regex pattern matches the given prefix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prefix: string to match pattern against; Pattern must match entire
|
||||||
|
prefix
|
||||||
|
"""
|
||||||
|
|
||||||
|
return bool(re.fullmatch(self.cmd, prefix))
|
||||||
|
|
||||||
|
|
||||||
|
class Daemon(Simple):
|
||||||
|
"""
|
||||||
|
Represents a daemon. Currently, it's only used as its own type, but writing
|
||||||
|
it this way allows us to easily expand upon its functionality later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Default(Simple):
|
||||||
|
"""
|
||||||
|
Represents a default command (a.k.a. when the module is called without a
|
||||||
|
command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, func: callable, help_str: str = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
func: function to wrap
|
||||||
|
help_str: short description of the default command
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().__init__(func)
|
||||||
|
|
||||||
|
self.help_str = help_str
|
|
@ -0,0 +1,58 @@
|
||||||
|
# =====IMPORTS=====
|
||||||
|
# Own imports
|
||||||
|
from .classes import Command, RegexCommand, Daemon, Default
|
||||||
|
|
||||||
|
|
||||||
|
def command(cmd, help_str: str = None) -> callable:
|
||||||
|
"""
|
||||||
|
Converts a method into a command by replacing it with a Command object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: keyword used to call this function
|
||||||
|
help_str: short description of the command
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(func):
|
||||||
|
return Command(func, cmd, help_str)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def regex_command(pattern: str, help_str: str = None) -> callable:
|
||||||
|
"""
|
||||||
|
Converts the method into a RegexCommand.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pattern: regex pattern to match command with
|
||||||
|
help_str: short description of the command
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(func):
|
||||||
|
return RegexCommand(func, pattern, help_str)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
def daemon() -> callable:
|
||||||
|
"""
|
||||||
|
Converts the method into a Daemon, which will then be run when the module
|
||||||
|
is started.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(func):
|
||||||
|
return Daemon(func)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make sure the default is unique
|
||||||
|
def default(help_str: str = None) -> callable:
|
||||||
|
"""
|
||||||
|
Converts the method into the Default method, making it the default command
|
||||||
|
when the module is run without a command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def inner(func):
|
||||||
|
return Default(func, help_str)
|
||||||
|
|
||||||
|
return inner
|
|
@ -0,0 +1,10 @@
|
||||||
|
class InvalidCommand(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateCommand(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleDefaults(Exception):
|
||||||
|
message = 'Multiple default commands detected'
|
|
@ -0,0 +1,45 @@
|
||||||
|
# =====IMPORTS=====
|
||||||
|
# Future imports
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
# Own imports
|
||||||
|
from .decorators import Command, Daemon, Default
|
||||||
|
|
||||||
|
# Typing imports
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Built-in imports
|
||||||
|
from typing import List, Any
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleMeta:
|
||||||
|
def _filter_attrs(self, condition: callable[[Any], bool]) -> List[Any]:
|
||||||
|
illegal_names = ['commands', 'daemons', 'default']
|
||||||
|
|
||||||
|
output = []
|
||||||
|
|
||||||
|
for attr in filter(lambda x: x not in illegal_names, dir(self)):
|
||||||
|
value = getattr(self, attr)
|
||||||
|
|
||||||
|
if condition(value):
|
||||||
|
output.append(value)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def commands(self) -> List[Command]:
|
||||||
|
# This also matches RegexCommand objects
|
||||||
|
# TODO: sort this to put RegexCommand's at the back
|
||||||
|
return self._filter_attrs(lambda val: isinstance(val, Command))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def daemons(self) -> List[Daemon]:
|
||||||
|
return self._filter_attrs(lambda val: isinstance(val, Daemon))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def default(self) -> Default:
|
||||||
|
return next(iter(self._filter_attrs(
|
||||||
|
lambda val: isinstance(val, Default))), None)
|
|
@ -0,0 +1,125 @@
|
||||||
|
# =====IMPORTS=====
|
||||||
|
# Future imports
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Built-in imports
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Own imports
|
||||||
|
from .exceptions import InvalidCommand
|
||||||
|
from .meta import ModuleMeta
|
||||||
|
from .decorators import RegexCommand
|
||||||
|
|
||||||
|
# Typing imports
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
# Built-in imports
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from discord.abc import User, Messageable
|
||||||
|
|
||||||
|
# Own imports
|
||||||
|
from suzybot.frank import Frank
|
||||||
|
|
||||||
|
|
||||||
|
class Module(ModuleMeta):
|
||||||
|
"""Base class for modules; all custom modules should inherit from this."""
|
||||||
|
|
||||||
|
PREFIX = []
|
||||||
|
"""Prefix to activate this module."""
|
||||||
|
|
||||||
|
NAME = ''
|
||||||
|
"""The name is used in various places, such as the config file and the
|
||||||
|
help function."""
|
||||||
|
|
||||||
|
HELP = ''
|
||||||
|
"""Short description of the module to use in the help function."""
|
||||||
|
|
||||||
|
def __init__(self, client: Frank, config: Dict = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
client: client using this module; used to communicate.
|
||||||
|
config: dict containing the config for this module (Frank client
|
||||||
|
reads this from the config file).
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._client = client
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
self._tasks = []
|
||||||
|
|
||||||
|
def pre_start(self):
|
||||||
|
"""
|
||||||
|
Overwrite this function to run code (e.g. add variables...) before
|
||||||
|
starting the daemons.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _start(self):
|
||||||
|
"""Start up defined daemons for this module."""
|
||||||
|
|
||||||
|
self.pre_start()
|
||||||
|
|
||||||
|
for daemon in self.daemons: # pylint: disable=no-member
|
||||||
|
task = asyncio.create_task(daemon())
|
||||||
|
self._tasks.append(task)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop all tasks for this module."""
|
||||||
|
|
||||||
|
for task in self._tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
async def __call__(self, cmd: List[str], author: User,
|
||||||
|
channel: Messageable, mid: int):
|
||||||
|
"""
|
||||||
|
Execute the command, if found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd: list of command arguments; if empty, default command is used
|
||||||
|
author: author of message
|
||||||
|
channel: channel the message was sent in
|
||||||
|
mid: message id
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cmd:
|
||||||
|
func = next((func for func in self.commands
|
||||||
|
if func.match(cmd[0])), None)
|
||||||
|
|
||||||
|
if func:
|
||||||
|
# A RegexCommand can use the prefix, as it's not a fixed string
|
||||||
|
if isinstance(func, RegexCommand):
|
||||||
|
await func(prefix=cmd[0], cmd=cmd[1:], author=author,
|
||||||
|
channel=channel, mid=mid)
|
||||||
|
|
||||||
|
else:
|
||||||
|
await func(cmd=cmd[1:], author=author, channel=channel,
|
||||||
|
mid=mid)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidCommand(f'Unknown command: {cmd}')
|
||||||
|
|
||||||
|
elif self.default:
|
||||||
|
await self.default(author=author, channel=channel, mid=mid)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def match(cls, prefix: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks wether the given prefix matches the module.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prefix: prefix to check
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cls.PREFIX:
|
||||||
|
if isinstance(cls.PREFIX, list):
|
||||||
|
return prefix in cls.PREFIX
|
||||||
|
|
||||||
|
else:
|
||||||
|
return prefix == cls.PREFIX
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False
|
|
@ -1,2 +1 @@
|
||||||
from .testmod import TestMod
|
from .test import TestMod
|
||||||
from .mcstat import McStat
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
from .. import Module
|
|
||||||
from mcstatus import MinecraftServer
|
|
||||||
|
|
||||||
|
|
||||||
class McStat(Module):
|
|
||||||
PREFIX = "mc"
|
|
||||||
NAME = "mcstat"
|
|
||||||
|
|
||||||
async def command(self, cmd):
|
|
||||||
if cmd[0] == "online":
|
|
||||||
address = self._config["domain"]
|
|
||||||
port = self._config.get("port")
|
|
||||||
|
|
||||||
if port:
|
|
||||||
address += ":" + str(port)
|
|
||||||
|
|
||||||
server = MinecraftServer.lookup(address)
|
|
||||||
status = server.status()
|
|
||||||
|
|
||||||
if status.players.sample is not None:
|
|
||||||
players = [player.name for player in status.players.sample]
|
|
||||||
|
|
||||||
else:
|
|
||||||
players = None
|
|
||||||
|
|
||||||
channel = self._client.get_channel(self._config["channel_id"])
|
|
||||||
|
|
||||||
if players:
|
|
||||||
await channel.send(f'Currently online: {",".join(players)}')
|
|
||||||
|
|
||||||
else:
|
|
||||||
await channel.send("No one is here bro")
|
|
10
main.py
10
main.py
|
@ -1,10 +0,0 @@
|
||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from frank.modules import TestMod, McStat
|
|
||||||
from frank import Frank
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
load_dotenv()
|
|
||||||
client = Frank([TestMod, McStat])
|
|
||||||
client.run(os.getenv('DISCORD_TOKEN'))
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
jedi~=0.17.2
|
||||||
|
flake8~=3.8.3
|
||||||
|
flake8-bugbear~=20.1.4
|
||||||
|
flake8-builtins~=1.5.3
|
||||||
|
flake8-commas~=2.0.0
|
||||||
|
flake8-comprehensions~=3.2.3
|
||||||
|
flake8-eradicate~=0.4.0
|
||||||
|
flake8-quotes~=3.2.0
|
||||||
|
flake8-variables-names~=0.0.3
|
|
@ -1,6 +1,2 @@
|
||||||
discord.py
|
discord.py~=1.4.1
|
||||||
pylint
|
PyYAML~=5.3.1
|
||||||
jedi
|
|
||||||
python-dotenv
|
|
||||||
pyyaml
|
|
||||||
mcstatus
|
|
||||||
|
|
Reference in New Issue