Compare commits

..

No commits in common. "master" and "rewrite" have entirely different histories.

225 changed files with 4786 additions and 9463 deletions

View file

@ -1,8 +0,0 @@
version = 1
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"

View file

@ -1,7 +0,0 @@
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=tuxbot_bot
POSTGRES_USER=debug
POSTGRES_PASSWORD=debug

View file

@ -1,4 +0,0 @@
# General
# ------------------------------------------------------------------------------
USE_DOCKER=yes
IPYTHONDIR=/app/.ipython

View file

@ -1,6 +1,9 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
@ -25,4 +28,4 @@ If applicable, add screenshots to help explain your problem.
- Python Version [e.g. 3.7.4]
**Additional context**
<-- Add any other context about the problem here. -->
Add any other context about the problem here.

158
.gitignore vendored
View file

@ -1,52 +1,146 @@
#Python
__pycache__/
*.pyc
.env
configs/config.cfg
configs/prefixes.cfg
configs/fallbacks.cfg
.DS_Store
private.py
#jetbrains
.idea/
# other
logs/tuxbot.log
utils/images/tmp/*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# C extensions
*.so
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Generated files
.idea/**/contentModel.xml
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
.idea/sonarlint/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
venv
venv3.8
venv3.9
venv3.11
dist
build
*.egg
*.egg-info
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
.ipython/
# Environments
.env
.envs/*
!.envs/.local/
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
data/settings/
dump.rdb
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View file

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -1,71 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="romain">
<words>
<w>aaaa</w>
<w>ajout</w>
<w>anglais</w>
<w>anonyme</w>
<w>appdirs</w>
<w>apres</w>
<w>asctime</w>
<w>commandstats</w>
<w>crimeflare</w>
<w>ctype</w>
<w>debian</w>
<w>dnskey</w>
<w>découverte</w>
<w>ffff</w>
<w>fonction</w>
<w>francais</w>
<w>français</w>
<w>gitea</w>
<w>gnous</w>
<w>ipinfo</w>
<w>iplocalise</w>
<w>ipwhois</w>
<w>jishaku</w>
<w>langue</w>
<w>latlon</w>
<w>levelname</w>
<w>liste</w>
<w>localiseip</w>
<w>lundi</w>
<w>octobre</w>
<w>outout</w>
<w>outoutxyz</w>
<w>outouxyz</w>
<w>pacman</w>
<w>peeringdb</w>
<w>perso</w>
<w>postgre</w>
<w>postgresql</w>
<w>pred</w>
<w>pydig</w>
<w>pylint</w>
<w>regle</w>
<w>regles</w>
<w>releaselevel</w>
<w>rprint</w>
<w>skipcq</w>
<w>socketstats</w>
<w>soit</w>
<w>sondage</w>
<w>sondages</w>
<w>splt</w>
<w>suivante</w>
<w>systemd</w>
<w>tablename</w>
<w>tempmute</w>
<w>tldr</w>
<w>tutux</w>
<w>tuxbot</w>
<w>tuxbot's</w>
<w>tuxvenv</w>
<w>venv</w>
<w>webhook</w>
<w>webhooks</w>
<w>youtrack</w>
<w>écrite</w>
</words>
</dictionary>
</component>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
</component>
<component name="ProjectNotificationSettings">
<option name="askShowProject" value="false" />
</component>
</project>

View file

@ -1,15 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="discord" />
<item index="1" class="java.lang.String" itemvalue="tortoise" />
</list>
</value>
</option>
</inspection_tool>
</profile>
</component>

View file

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (tuxbot_bot)" project-jdk-type="Python SDK" />
</project>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tuxbot_bot.iml" filepath="$PROJECT_DIR$/.idea/tuxbot_bot.iml" />
</modules>
</component>
</project>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Statistic">
<option name="excludedDirectories">
<list>
<option value="$PROJECT_DIR$/Tuxbot_bot.egg-info" />
<option value="$PROJECT_DIR$/venv" />
</list>
</option>
</component>
</project>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
<excludeFolder url="file://$MODULE_DIR$/data" />
<excludeFolder url="file://$MODULE_DIR$/.mypy_cache" />
<excludeFolder url="file://$MODULE_DIR$/venv3.8" />
<excludeFolder url="file://$MODULE_DIR$/venv3.9" />
<excludeFolder url="file://$MODULE_DIR$/venv3.11" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (tuxbot_bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="NUMPY" />
<option name="myDocStringFormat" value="NumPy" />
</component>
</module>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="GrazieCommit" enabled="true" level="TYPO" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WebResourcesPaths">
<contentEntries>
<entry url="file://$PROJECT_DIR$">
<entryData>
<resourceRoots>
<path value="file://$PROJECT_DIR$" />
</resourceRoots>
</entryData>
</entry>
</contentEntries>
</component>
</project>

View file

@ -1,3 +0,0 @@
[mypy]
ignore_missing_imports = True
exclude = venv

View file

@ -1,22 +0,0 @@
[BASIC]
good-names=
e, # (exception) as e
f, # (file) as f
k, # for k, v in
v, # for k, v in
dt, # datetime
[MASTER]
disable=
C0103, # invalid-name
C0114, # missing-module-docstring
C0115, # missing-class-docstring
C0116, # missing-function-docstring
C0415, # import-outside-toplevel
W0703, # broad-except
W0707, # raise-missing-from
R0801, # duplicate-code
R0901, # too-many-ancestors
R0902, # too-many-instance-attributes
R0903, # too-few-public-methods
E1136, # unsubscriptable-object (false positive with python 3.9)

1098
LICENSE Normal file → Executable file

File diff suppressed because it is too large Load diff

View file

@ -1,86 +0,0 @@
ifeq ($(ISPROD), 1)
DOCKER_LOCAL := docker-compose -f production.yml
else
DOCKER_LOCAL := docker-compose -f local.yml
endif
INSTANCE := preprod
DOCKER_TUXBOT := $(DOCKER_LOCAL) run --rm tuxbot
VIRTUAL_ENV := venv
PYTHON_PATH := $(VIRTUAL_ENV)/bin/python
XGETTEXT_FLAGS := --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
# Init
.PHONY: main
main:
$(VIRTUAL_ENV)/bin/pip install -U pip setuptools
.PHONY: install
install:
$(VIRTUAL_ENV)/bin/pip install .
.PHONY: install-dev
install-dev:
$(VIRTUAL_ENV)/bin/pip install -r dev.requirements.txt
.PHONY: update
update:
$(VIRTUAL_ENV)/bin/pip install --upgrade .
.PHONY: update-all
update-all:
$(VIRTUAL_ENV)/bin/pip install --upgrade --force-reinstall .
.PHONY: dev
dev: style update
$(VIRTUAL_ENV)/bin/tuxbot
# Docker
.PHONY: docker
docker:
$(DOCKER_LOCAL) build
$(DOCKER_LOCAL) up -d
.PHONY: docker-start
docker-start:
$(DOCKER_TUXBOT) tuxbot
# Blackify code
.PHONY: black
black:
$(PYTHON_PATH) -m black `git ls-files "*.py"` --line-length=79
.PHONY: lint
lint:
$(PYTHON_PATH) -m pylint tuxbot
.PHONY: type
type:
$(PYTHON_PATH) -m mypy tuxbot
.PHONY: style
style: black lint type
# Translations
.PHONY: xgettext
xgettext:
for cog in tuxbot/cogs/*/; do \
xgettext `find $$cog -type f -name '*.py'` --output=$$cog/locales/messages.pot $(XGETTEXT_FLAGS); \
done
.PHONY: msginit
msginit:
for cog in tuxbot/cogs/*/; do \
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/fr-FR.po --locale=fr_FR.UTF-8 --no-translator; \
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/en-US.po --locale=en_US.UTF-8 --no-translator; \
done
.PHONY: msgmerge
msgmerge:
for cog in tuxbot/cogs/*/; do \
msgmerge --update $$cog/locales/fr-FR.po $$cog/locales/messages.pot; \
msgmerge --update $$cog/locales/en-US.po $$cog/locales/messages.pot; \
done

86
README.md Normal file
View file

@ -0,0 +1,86 @@
# News
- [ ] i18n for messages
- [x] Custom prefixes
- [ ] Better help command
- [ ] Alias system for commands (e.g. `.alias .ci show .cs`)
- [x] Migrate MySQL to postgresql
- [x] Prepare bot for python 3.8 and discord.py 1.3.0
- [ ] Create launcher
- [ ] Create documentation
## Launcher requirements :
- [ ] Can install the bot
- [ ] Can launch the bot
- [ ] Can propose updates
## New commands :
- [x] `.sondage --anonyme <...>` (create à sondage with the possibility of answering anonymously)
- [ ] `.sondage --edit <id>` (edit a sondage if we are the author or an admin)
## Documentation:
- [ ] How to use ?
- [ ] How to add more commands ?
## Ultimate :
- [ ] Send email or Telegram's message when something is wrong on the bot
- [ ] Create skynet (group of multiple commands about sky (planes, meteo, AI,...))
---
# Cogs.admin commands
- [x] upload `removed`, cause : `never used`
- [x] ban
- [x] kick
- [x] clear
- [x] say
- [x] sayto `removed`, now : `say to`
- [x] sayto_dm `removed`, now : `say to`
- [x] editsay `removed`, now : `say edit`
- [x] addreaction `renamed`, now `react add`
- [x] delete
- [x] deletefrom `removed`, now `delete (from|to|in)`
- [x] embed `removed`, cause : `never used`
- [x] warn `new command`
---
# Cogs.basics commands
- [x] ping
- [x] info
- [ ] help
- [x] credits `new command`
---
# Cogs.ci commands `canceled until the frontend development`
- [ ] ci (help?)
- [ ] ci show
- [ ] ci register
- [ ] ci delete
- [ ] ci update
- [ ] ci setconfig
- [ ] ci setos
- [ ] ci setcountry
- [ ] ci online_edit `renamed`, cause : `website down`
- [ ] ci list
---
# Cogs.utility commands
- [ ] clock `removed` ?
- [ ] clock * `removed` ?
- [ ] ytdiscover `removed` ?
- [x] iplocalise
- [x] getheaders
- [x] git
- [x] quote
---
# Cogs.sondage commands `(renamed as cogs.poll)` `canceled until the frontend development`
- [ ] sondage (help?)

View file

@ -1,141 +0,0 @@
|image0| |image1| |image2| |image3|
.. role:: bash(code)
:language: bash
Installing Tuxbot
=================
It is preferable to install the bot on a dedicated user. If you don't
know how to do it, please refer to `this guide <https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-ubuntu-quickstart>`__
Installing the pre-requirements
-------------------------------
- The pre-requirements are:
- Python 3.8 or greater
- Pip
- Git
Operating systems
~~~~~~~~~~~~~~~~~
Arch Linux
^^^^^^^^^^
.. code-block:: bash
$ sudo pacman -Syu python python-pip python-virtualenv git make gcc postgresql
Continue to `configure postgresql <#configure-postgresql>`__.
--------------
Debian
^^^^^^
.. code-block:: bash
$ sudo apt update
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git make gcc postgresql postgresql-client
Continue to `configure postgresql <#configure-postgresql>`__.
--------------
RHEL and derivatives (CentOS, Fedora...)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
$ sudo dnf update
$ sudo dnf install python3 python3-devel python3-pip python3-virtualenv git make gcc postgresql-server postgresql-contrib
Continue to `configure postgresql <#configure-postgresql>`__.
--------------
Windows
^^^^^^^
*not for now and not for the future*
--------------
Configure PostgreSQL
--------------------
Now, you need to setup PostgreSQL
Operating systems
~~~~~~~~~~~~~~~~~
Arch Linux
^^^^^^^^^^
https://wiki.archlinux.org/index.php/PostgreSQL
Continue to `create the venv <#creating-the-virtual-environment>`__.
--------------
Debian
^^^^^^
https://wiki.debian.org/PostgreSql
Continue to `create the venv <#creating-the-virtual-environment>`__.
--------------
RHEL and derivatives (CentOS, Fedora...)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
https://fedoraproject.org/wiki/PostgreSQL
Continue to `create the venv <#creating-the-virtual-environment>`__.
--------------
Creating the Virtual Environment
--------------------------------
To set up the virtual environment and install the bot, simply run this
two commands:
.. code-block:: bash
$ make
$ make install
Now, switch your environment to the virtual one by run this single
command: :bash:`source ~/venv/bin/activate`
Configuration
-------------
It's time to set up your first instance, to do this, you can simply
execute this command:
:bash:`tuxbot-setup`
After following the instructions, you can run your instance by executing
this command:
:bash:`tuxbot`
Update
------
To update the whole bot after a :bash:`git pull`, just execute
.. code-block:: bash
$ make update
.. |image0| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-%23007ec6
.. |image1| image:: https://img.shields.io/github/issues/Rom1-J/tuxbot-bot
.. |image2| image:: https://img.shields.io/badge/code%20style-black-000000.svg
.. |image3| image:: https://wakatime.com/badge/github/Rom1-J/tuxbot-bot.svg
:target: https://wakatime.com/badge/github/Rom1-J/tuxbot-bot

250
bot.py Executable file
View file

@ -0,0 +1,250 @@
import contextlib
import datetime
import json
import logging
import sys
from collections import deque, Counter
from typing import List
import aiohttp
import discord
import git
import sqlalchemy
from discord.ext import commands
from utils.functions import Config
from utils.functions import Texts
from utils.functions import Version
from utils.functions import ContextPlus
from utils.models import metadata, database
description = """
Je suis TuxBot, le bot qui vit de l'OpenSource ! ;)
"""
build = git.Repo(search_parent_directories=True).head.object.hexsha
version = (2, 0, 0)
log = logging.getLogger(__name__)
l_extensions: List[str] = [
'cogs.Admin',
'cogs.API',
'cogs.Help',
'cogs.Logs',
# 'cogs.Monitoring',
'cogs.Poll',
'cogs.Useful',
'cogs.User',
'jishaku',
]
async def _prefix_callable(bot, message: discord.message) -> list:
<<<<<<< HEAD
extras = [bot.cluster.get('Name') + '.']
if message.guild is not None:
if str(message.guild.id) in bot.prefixes:
extras.extend(
bot.prefixes.get(str(message.guild.id), "prefixes").split(
bot.config.get("misc", "Separator")
)
)
=======
try:
with open(f'./configs/guilds/{message.guild.id}.json', 'r') as f:
data = json.load(f)
custom_prefix = data['prefixes']
except FileNotFoundError:
custom_prefix = ['']
extras = [bot.cluster.get('Name') + '.']
extras.extend(custom_prefix)
>>>>>>> cce7bb409303e9ad27ef4e5617d0bc9068810f13
return commands.when_mentioned_or(*extras)(bot, message)
class TuxBot(commands.AutoShardedBot):
def __init__(self, ):
super().__init__(command_prefix=_prefix_callable, pm_help=None,
help_command=None, description=description,
help_attrs=dict(hidden=True),
activity=discord.Game(
name=Texts().get('Starting...'))
)
self.socket_stats = Counter()
self.command_stats = Counter()
self.config = Config('./configs/config.cfg')
self.blacklist = Config('./configs/blacklist.cfg')
self.fallbacks = Config('./configs/fallbacks.cfg')
self.cluster = self.fallbacks.find('True', key='This', first=True)
self.uptime: datetime = datetime.datetime.utcnow()
self._prev_events = deque(maxlen=10)
self.session = aiohttp.ClientSession(loop=self.loop)
self.database, self.metadata = database, metadata
self.engine = sqlalchemy.create_engine(str(self.database.url))
self.metadata.create_all(self.engine)
self.version = Version(*version, pre_release='rc2', build=build)
self.owners_id = [int(owner_id) for owner_id in self.config.get('permissions', 'Owners').split(', ')]
self.owner_id = int(self.owners_id[0])
for extension in l_extensions:
try:
self.load_extension(extension)
print(Texts().get("Extension loaded successfully : ")
+ extension)
log.info(Texts().get("Extension loaded successfully : ")
+ extension)
except Exception as e:
print(Texts().get("Failed to load extension : ")
+ extension, file=sys.stderr)
print(e)
log.error(Texts().get("Failed to load extension : ")
+ extension, exc_info=e)
@property
def owner(self):
return self.get_user(self.owner_id)
@property
def owners(self):
return [self.get_user(owner_id) for owner_id in self.owners_id]
async def is_owner(self, user: discord.User) -> bool:
return user in self.owners
async def get_context(self, message, *, cls=None):
return await super().get_context(message, cls=cls or ContextPlus)
async def on_socket_response(self, msg):
self._prev_events.append(msg)
async def on_command_error(self, ctx: discord.ext.commands.Context, error):
if isinstance(error, commands.NoPrivateMessage):
await ctx.author.send(
Texts().get("This command cannot be used in private messages.")
)
elif isinstance(error, commands.DisabledCommand):
await ctx.author.send(
Texts().get(
"Sorry. This command is disabled and cannot be used."
)
)
elif isinstance(error, commands.CommandOnCooldown):
await ctx.send(str(error))
async def process_commands(self, message: discord.message):
ctx: commands.Context = await self.get_context(message)
if ctx.command is None:
return
await self.invoke(ctx)
async def on_message(self, message: discord.message):
if message.author.id in self.blacklist \
or (message.guild is not None
and message.guild.id in self.blacklist):
return
if message.author.bot and message.author.id != int(
self.config.get('bot', 'Tester')):
return
await self.process_commands(message)
async def on_ready(self):
if not hasattr(self, 'uptime'):
self.uptime = datetime.datetime.utcnow()
print('-' * 60)
print(Texts().get("Ready:") + f' {self.user} (ID: {self.user.id})')
print(self.version)
presence: dict = dict(status=discord.Status.dnd)
if self.config.get("bot", "Activity", fallback=None) is not None:
presence.update(
activity=discord.Game(
name=self.config.get("bot", "Activity")
)
)
print(f"Discord.py: {discord.__version__}")
print(f"Server: {self.cluster.get('Name')}")
print('-' * 60)
await self.change_presence(**presence)
@staticmethod
async def on_resumed():
print('resumed...')
@property
def logs_webhook(self) -> discord.Webhook:
webhook_config = self.config["webhook"]
webhook = discord.Webhook.partial(
id=webhook_config.get('ID'),
token=webhook_config.get('Token'),
adapter=discord.AsyncWebhookAdapter(
self.session
)
)
return webhook
async def close(self):
extensions = self.extensions.copy()
for extension in extensions:
self.unload_extension(extension)
await super().close()
await self.session.close()
def run(self):
super().run(self.config.get("bot", "Token"), reconnect=True)
@contextlib.contextmanager
def setup_logging():
logging.getLogger('discord').setLevel(logging.INFO)
logging.getLogger('discord.http').setLevel(logging.WARNING)
log = logging.getLogger()
log.setLevel(logging.INFO)
try:
handler = logging.FileHandler(filename='logs/tuxbot.log',
encoding='utf-8', mode='w')
fmt = logging.Formatter('[{levelname:<7}] [{asctime}]'
' {name}: {message}',
'%Y-%m-%d %H:%M:%S', style='{')
handler.setFormatter(fmt)
log.addHandler(handler)
yield
finally:
handlers = log.handlers[:]
for handler in handlers:
handler.close()
log.removeHandler(handler)
if __name__ == "__main__":
print(Texts().get('Starting...'))
app = TuxBot()
try:
with setup_logging():
app.run()
except KeyboardInterrupt:
app.close()

56
cogs/API.py Normal file
View file

@ -0,0 +1,56 @@
import logging
import discord
from aiohttp import web
from discord.ext import commands
from bot import TuxBot
log = logging.getLogger(__name__)
class API(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.site = web.TCPSite
app = web.Application()
app.add_routes([web.get('/users/{user_id}', self.users)])
self.runner = web.AppRunner(app)
self.bot.loop.create_task(self.start_HTTPMonitoring_server())
async def start_HTTPMonitoring_server(self):
host = self.bot.config.get('API', 'Host')
port = self.bot.config.get('API', 'Port')
print(f"Starting API server on {host}:{port}")
await self.runner.setup()
self.site = web.TCPSite(self.runner, host, port)
await self.site.start()
async def users(self, request):
try:
user = await self.bot.fetch_user(request.match_info['user_id'])
except discord.NotFound:
return web.Response(status=404)
json = {
'id': user.id,
'username': user.name,
'discriminator': user.discriminator,
'avatar': user.avatar,
'default_avatar': user.default_avatar.value,
'bot': user.bot,
'system': user.system,
}
return web.json_response(
json
)
def setup(bot: TuxBot):
bot.add_cog(API(bot))

534
cogs/Admin.py Normal file
View file

@ -0,0 +1,534 @@
import asyncio
import datetime
import logging
from typing import Union
import discord
import humanize
from discord.ext import commands
from bot import TuxBot
from utils import Texts
from utils.models import WarnModel
from utils import command_extra, group_extra
log = logging.getLogger(__name__)
class Admin(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.icon = ":shield:"
self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/160/twitter/233/shield_1f6e1.png"
async def cog_check(self, ctx: commands.Context) -> bool:
permissions: discord.Permissions = ctx.channel.permissions_for(
ctx.author)
has_permission = permissions.administrator
is_owner = await self.bot.is_owner(ctx.author)
return has_permission or is_owner
@staticmethod
async def kick_ban_message(ctx: commands.Context,
**kwargs) -> discord.Embed:
member: discord.Member = kwargs.get('member')
reason = kwargs.get(
'reason',
Texts('admin', ctx).get("Please enter a reason")
)
if kwargs.get('type') == 'ban':
title = '**Ban** ' + str(len(await ctx.guild.bans()))
color = discord.Color.dark_red()
else:
title = '**Kick**'
color = discord.Color.red()
e = discord.Embed(
title=title,
description=reason,
timestamp=datetime.datetime.utcnow(),
color=color
)
e.set_author(
name=f'{member.name}#{member.discriminator} ({member.id})',
icon_url=member.avatar_url_as(format='jpg')
)
e.set_footer(
text=f'{ctx.author.name}#{ctx.author.discriminator}',
icon_url=ctx.author.avatar_url_as(format='png')
)
return e
###########################################################################
@group_extra(name='say', invoke_without_command=True, category='text')
async def _say(self, ctx: commands.Context, *, content: str):
if ctx.invoked_subcommand is None:
try:
await ctx.message.delete()
except discord.errors.Forbidden:
pass
await ctx.send(content)
@_say.command(name='edit')
async def _say_edit(self, ctx: commands.Context, message_id: int, *,
content: str):
try:
await ctx.message.delete()
except discord.errors.Forbidden:
pass
try:
message: discord.Message = await ctx.channel.fetch_message(
message_id)
await message.edit(content=content)
except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_say.command(name='to')
async def _say_to(self, ctx: commands.Context,
channel: Union[discord.TextChannel, discord.User], *,
content):
try:
await ctx.message.delete()
except discord.errors.Forbidden:
pass
await channel.send(content)
###########################################################################
@command_extra(name='ban', category='administration')
async def _ban(self, ctx: commands.Context, user: discord.Member, *,
reason=""):
try:
member: discord.Member = await ctx.guild.fetch_member(user.id)
try:
await member.ban(reason=reason)
e: discord.Embed = await self.kick_ban_message(
ctx,
member=member,
type='ban',
reason=reason
)
await ctx.send(embed=e)
except discord.Forbidden:
await ctx.send(
Texts('admin', ctx).get("Unable to ban this user"),
delete_after=5)
except discord.errors.NotFound:
await ctx.send(
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5)
###########################################################################
@command_extra(name='kick', category='administration')
async def _kick(self, ctx: commands.Context, user: discord.Member, *,
reason=""):
try:
member: discord.Member = await ctx.guild.fetch_member(user.id)
try:
await member.kick(reason=reason)
e: discord.Embed = await self.kick_ban_message(
ctx,
member=member,
type='kick',
reason=reason
)
await ctx.send(embed=e)
except discord.Forbidden:
await ctx.send(
Texts('admin', ctx).get("Unable to kick this user"),
delete_after=5)
except discord.errors.NotFound:
await ctx.send(
Texts('utils', ctx).get("Unable to find the user..."),
delete_after=5)
###########################################################################
@command_extra(name='clear', category='text')
async def _clear(self, ctx: commands.Context, count: int):
try:
await ctx.message.delete()
await ctx.channel.purge(limit=count)
except discord.errors.Forbidden:
pass
###########################################################################
@group_extra(name='react', category='text')
async def _react(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('react')
@_react.command(name='add')
async def _react_add(self, ctx: commands.Context, message_id: int, *,
emojis: str):
emojis: list = emojis.split(' ')
try:
message: discord.Message = await ctx.channel.fetch_message(
message_id)
for emoji in emojis:
await message.add_reaction(emoji)
except discord.errors.NotFound:
await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_react.command(name='remove', aliases=['clear'])
async def _react_remove(self, ctx: commands.Context, message_id: int):
try:
message: discord.Message = await ctx.channel.fetch_message(
message_id)
await message.clear_reactions()
except discord.errors.NotFound:
await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
###########################################################################
@group_extra(name='delete', invoke_without_command=True, category='text')
async def _delete(self, ctx: commands.Context, message_id: int):
try:
await ctx.message.delete()
except discord.errors.Forbidden:
pass
try:
message: discord.Message = await ctx.channel.fetch_message(
message_id)
await message.delete()
except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5)
@_delete.command(name='from', aliases=['to', 'in'])
async def _delete_from(self, ctx: commands.Context,
channel: discord.TextChannel, message_id: int):
try:
await ctx.message.delete()
except discord.errors.Forbidden:
pass
try:
message: discord.Message = await channel.fetch_message(
message_id
)
await message.delete()
except (discord.errors.NotFound, discord.errors.Forbidden):
await ctx.send(
Texts('utils', ctx).get("Unable to find the message"),
delete_after=5
)
###########################################################################
async def get_warn(self, ctx: commands.Context,
member: discord.Member = False):
await ctx.trigger_typing()
if member:
warns = WarnModel.objects.filter(
server_id=str(ctx.guild.id),
user_id=member.id
)
else:
warns = WarnModel.objects.filter(
server_id=str(ctx.guild.id)
)
warns_list = ''
for warn in await warns.all():
row_id = warn.id
user_id = warn.user_id
user = await self.bot.fetch_user(user_id)
reason = warn.reason
ago = humanize.naturaldelta(
datetime.datetime.now() - warn.created_at
)
warns_list += f"[{row_id}] **{user}**: `{reason}` *({ago} ago)*\n"
return warns_list, warns
async def add_warn(self, ctx: commands.Context, member: discord.Member,
reason):
now = datetime.datetime.now()
warn = WarnModel(server_id=ctx.guild.id, user_id=member.id,
reason=reason,
created_at=now)
self.bot.database.session.add(warn)
self.bot.database.session.commit()
@group_extra(name='warn', aliases=['warns'], category='administration')
async def _warn(self, ctx: commands.Context):
await ctx.trigger_typing()
if ctx.invoked_subcommand is None:
warns_list, warns = await self.get_warn(ctx)
e = discord.Embed(
title=f"{warns.count()} {Texts('admin', ctx).get('last warns')}: ",
description=warns_list
)
await ctx.send(embed=e)
@_warn.command(name='add', aliases=['new'])
async def _warn_add(self, ctx: commands.Context, member: discord.Member,
*, reason="N/A"):
if not member:
return await ctx.send(
Texts('utils', ctx).get("Unable to find the user...")
)
def check(pld: discord.RawReactionActionEvent):
if pld.message_id != choice.id \
or pld.user_id != ctx.author.id:
return False
return pld.emoji.name in ('1⃣', '2⃣', '3⃣')
warns_list, warns = await self.get_warn(ctx, member)
if warns.count() >= 2:
e = discord.Embed(
title=Texts('admin', ctx).get('More than 2 warns'),
description=f"{member.mention} "
+ Texts('admin', ctx).get('has more than 2 warns')
)
e.add_field(
name='__Actions__',
value=':one: kick\n'
':two: ban\n'
':three: ' + Texts('admin', ctx).get('ignore')
)
choice = await ctx.send(embed=e)
for reaction in ('1⃣', '2⃣', '3⃣'):
await choice.add_reaction(reaction)
try:
payload = await self.bot.wait_for(
'raw_reaction_add',
check=check,
timeout=50.0
)
except asyncio.TimeoutError:
return await ctx.send(
Texts('admin', ctx).get('Took too long. Aborting.')
)
finally:
await choice.delete()
if payload.emoji.name == '1⃣':
from jishaku.models import copy_context_with
alt_ctx = await copy_context_with(
ctx,
content=f"{ctx.prefix}"
f"kick "
f"{member} "
f"{Texts('admin', ctx).get('More than 2 warns')}"
)
return await alt_ctx.command.invoke(alt_ctx)
elif payload.emoji.name == '2⃣':
from jishaku.models import copy_context_with
alt_ctx = await copy_context_with(
ctx,
content=f"{ctx.prefix}"
f"ban "
f"{member} "
f"{Texts('admin', ctx).get('More than 2 warns')}"
)
return await alt_ctx.command.invoke(alt_ctx)
await self.add_warn(ctx, member, reason)
await ctx.send(
content=f"{member.mention} "
f"**{Texts('admin', ctx).get('got a warn')}**"
f"\n**{Texts('admin', ctx).get('Reason')}:** `{reason}`"
)
@_warn.command(name='remove', aliases=['revoke', 'del', 'delete'])
async def _warn_remove(self, ctx: commands.Context, warn_id: int):
warn = self.bot.database.session \
.query(WarnModel) \
.filter(WarnModel.id == warn_id) \
.one()
self.bot.database.session.delete(warn)
await ctx.send(f"{Texts('admin', ctx).get('Warn with id')} `{warn_id}`"
f" {Texts('admin', ctx).get('successfully removed')}")
@_warn.command(name='show', aliases=['list', 'all'])
async def _warn_show(self, ctx: commands.Context, member: discord.Member):
warns_list, warns = await self.get_warn(ctx, member)
e = discord.Embed(
title=f"{warns.count()} {Texts('admin', ctx).get('last warns')}: ",
description=warns_list
)
await ctx.send(embed=e)
@_warn.command(name='edit', aliases=['change', 'modify'])
async def _warn_edit(self, ctx: commands.Context, warn_id: int, *, reason):
warn = self.bot.database.session \
.query(WarnModel) \
.filter(WarnModel.id == warn_id) \
.one()
warn.reason = reason
self.bot.database.session.commit()
await ctx.send(f"{Texts('admin', ctx).get('Warn with id')} `{warn_id}`"
f" {Texts('admin', ctx).get('successfully edited')}")
###########################################################################
@command_extra(name='language', aliases=['lang', 'langue', 'langage'], category='server')
async def _language(self, ctx: commands.Context, locale: str):
available = self.bot.database.session \
.query(LangModel.value) \
.filter(LangModel.key == 'available') \
.first()[0] \
.split(',')
if locale.lower() not in available:
await ctx.send(
Texts('admin', ctx).get('Unable to find this language'))
else:
current = self.bot.database.session \
.query(LangModel) \
.filter(LangModel.key == str(ctx.guild.id))
if current.count() > 0:
current = current.one()
current.value = locale.lower()
self.bot.database.session.commit()
else:
new_row = LangModel(key=str(ctx.guild.id),
value=locale.lower())
self.bot.database.session.add(new_row)
self.bot.database.session.commit()
await ctx.send(
Texts('admin', ctx).get('Language changed successfully'))
###########################################################################
@group_extra(name='prefix', aliases=['prefixes'], category='server')
async def _prefix(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('prefix')
@_prefix.command(name='add', aliases=['set', 'new'])
async def _prefix_add(self, ctx: commands.Context, prefix: str):
if str(ctx.guild.id) in self.bot.prefixes:
prefixes = self.bot.prefixes.get(
str(ctx.guild.id), "prefixes"
).split(
self.bot.config.get("misc", "separator")
)
if prefix in prefixes:
return await ctx.send(
Texts('admin', ctx).get('This prefix already exists')
)
else:
prefixes.append(prefix)
self.bot.prefixes.set(
str(ctx.guild.id),
"prefixes",
self.bot.config.get("misc", "separator")
.join(prefixes)
)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
else:
self.bot.prefixes.add_section(str(ctx.guild.id))
self.bot.prefixes.set(str(ctx.guild.id), "prefixes", prefix)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
await ctx.send(
Texts('admin', ctx).get('Prefix added successfully')
)
@_prefix.command(name='remove', aliases=['drop', 'del', 'delete'])
async def _prefix_remove(self, ctx: commands.Context, prefix: str):
if str(ctx.guild.id) in self.bot.prefixes:
prefixes = self.bot.prefixes.get(
str(ctx.guild.id), "prefixes"
).split(
self.bot.config.get("misc", "separator")
)
if prefix in prefixes:
prefixes.remove(prefix)
self.bot.prefixes.set(
str(ctx.guild.id),
"prefixes",
self.bot.config.get("misc", "separator")
.join(prefixes)
)
with open('./configs/prefixes.cfg', 'w') as configfile:
self.bot.prefixes.write(configfile)
return await ctx.send(
Texts('admin', ctx).get('Prefix removed successfully')
)
await ctx.send(
Texts('admin', ctx).get('This prefix does not exist')
)
@_prefix.command(name='list', aliases=['show', 'all'])
async def _prefix_list(self, ctx: commands.Context):
extras = ['.']
if ctx.message.guild is not None:
extras = []
if str(ctx.message.guild.id) in self.bot.prefixes:
extras.extend(
self.bot.prefixes.get(str(ctx.message.guild.id),
"prefixes").split(
self.bot.config.get("misc", "separator")
)
)
prefixes = [self.bot.user.mention]
prefixes.extend(extras)
if len(prefixes) <= 1:
text = Texts('admin', ctx) \
.get('The only prefix for this guild is :\n')
else:
text = Texts('admin', ctx) \
.get('Available prefixes for this guild are :\n')
await ctx.send(text + "\n".join(prefixes))
def setup(bot: TuxBot):
bot.add_cog(Admin(bot))

227
cogs/Help.py Normal file
View file

@ -0,0 +1,227 @@
# Created by romain at 04/01/2020
import logging
import discord
from discord.ext import commands
from bot import TuxBot
from utils import Texts, GroupPlus
from utils import FieldPages
log = logging.getLogger(__name__)
class HelpCommand(commands.HelpCommand):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ignore_cogs = ["Monitoring", "Help", "Jishaku"]
self.owner_cogs = []
self.admin_cogs = ["Admin"]
def command_formatting(self, e, command):
prefix = self.context.prefix \
if str(self.context.bot.user.id) not in self.context.prefix \
else f"@{self.context.bot.user.name}"
file = Texts(command.cog.qualified_name.lower() + '_help', self.context)
if command.parent is not None:
description = file.get(f"_{command.parent}_{command.name}")
usage = file.get(f"_{command.parent}_{command.name}__usage")
else:
description = file.get(f"_{command.name}")
usage = file.get(f"_{command.name}__usage")
e.title = self.get_command_signature(command) + usage
e.description = description
e.add_field(
name=Texts(
'help', self.context
).get(
'command_help.params'
),
value=usage
)
e.add_field(
name=Texts(
'help', self.context
).get(
'command_help.usage'
),
value=f"{prefix}{command.qualified_name} " + usage
)
aliases = "`" + '`, `'.join(command.aliases) + "`"
if aliases == "``":
aliases = Texts(
'help', self.context
).get(
'command_help.no_aliases'
)
e.add_field(
name=Texts(
'help', self.context
).get(
'command_help.aliases'
),
value=aliases
)
return e
async def send_bot_help(self, mapping):
owners = self.context.bot.owners
prefix = self.context.prefix \
if str(self.context.bot.user.id) not in self.context.prefix \
else f"@{self.context.bot.user.name} "
e = discord.Embed(
color=discord.Color.blue(),
description=Texts(
'help', self.context
).get(
'main_page.description'
)
)
e.set_author(
icon_url=self.context.author.avatar_url_as(format='png'),
name=self.context.author
)
e.set_footer(
text=Texts(
'help', self.context
).get(
'main_page.footer'
).format(
prefix
)
)
for extension in self.context.bot.cogs.values():
if self.context.author not in owners \
and extension.__class__.__name__ in self.owner_cogs:
continue
if extension.__class__.__name__ in self.ignore_cogs:
continue
count = len(extension.get_commands())
text = Texts('help', self.context).get('main_page.commands')
if count <= 1:
text = text[:-1]
e.add_field(
name=f"__{extension.icon} **{extension.qualified_name}**__",
value=f"{count} {text}"
)
await self.context.send(embed=e)
async def send_cog_help(self, cog):
pages = {}
prefix = self.context.prefix \
if str(self.context.bot.user.id) not in self.context.prefix \
else f"@{self.context.bot.user.name}"
file = Texts(cog.qualified_name.lower() + '_help', self.context)
if cog.__class__.__name__ in self.owner_cogs \
and self.context.author not in self.context.bot.owners:
return self.command_not_found(cog.qualified_name)
for cmd in cog.get_commands():
if self.context.author not in self.context.bot.owners \
and (cmd.hidden or cmd.category == "Hidden"):
continue
if cmd.category not in pages:
pages[cmd.category] = "```asciidoc\n"
pages[cmd.category] \
+= f"{cmd.name}" \
+ ' ' * int(13 - len(cmd.name)) \
+ f":: {file.get(f'_{cmd.name}__short')}\n"
if isinstance(cmd, GroupPlus):
for group_command in cmd.commands:
pages[cmd.category] \
+= f"└> {group_command.name}" \
+ ' ' * int(10 - len(group_command.name)) \
+ f":: {file.get(f'_{group_command.parent}_{group_command.name}__short')}\n"
for e in pages:
pages[e] += "```"
formatted = []
for name, cont in pages.items():
formatted.append((name, cont))
footer_text = Texts('help', self.context) \
.get('main_page.footer') \
.format(prefix)
pages = FieldPages(
self.context,
embed_color=discord.Color.blue(),
entries=formatted,
title=cog.qualified_name.upper(),
thumbnail=cog.big_icon,
footericon=self.context.bot.user.avatar_url,
footertext=footer_text,
per_page=1
)
await pages.paginate()
async def send_group_help(self, group):
if group.cog_name in self.ignore_cogs:
return await self.send_error_message(
self.command_not_found(group.name)
)
file = Texts(group.qualified_name.lower() + '_help', self.context)
formatted = self.command_formatting(
discord.Embed(color=discord.Color.blue()),
group
)
sub_command_list = "" # this is braille, please don't touch unless you know what you're doing
for group_command in group.commands:
sub_command_list += f"└> **{group_command.name}** - {file.get(f'_{group_command.parent}_{group_command.name}')}\n"
subcommands = Texts(
'help', self.context
).get(
'command_help.subcommands'
)
formatted.add_field(name=subcommands, value=sub_command_list, inline=False)
await self.context.send(embed=formatted)
async def send_command_help(self, command):
if isinstance(command, commands.Group):
return await self.send_group_help(command)
if command.cog_name in self.ignore_cogs:
return await self.send_error_message(
self.command_not_found(command.name))
formatted = self.command_formatting(
discord.Embed(color=discord.Color.blue()),
command
)
await self.context.send(embed=formatted)
def command_not_found(self, command):
return Texts(
'help', self.context
).get(
'main_page.not_found'
).format(
command
)
class Help(commands.Cog):
def __init__(self, bot: TuxBot):
bot.help_command = HelpCommand()
def setup(bot: TuxBot):
bot.add_cog(Help(bot))

304
cogs/Logs.py Normal file
View file

@ -0,0 +1,304 @@
"""
Based on https://github.com/Rapptz/RoboDanny/blob/3d94e89ef27f702a5f57f432a9131bdfb60bb3ec/cogs/stats.py
Adapted by Romain J.
"""
import asyncio
import datetime
import json
import logging
import textwrap
import traceback
from collections import defaultdict
import discord
import humanize
import psutil
from discord.ext import commands, tasks
from bot import TuxBot
from utils import Texts
from utils import command_extra
log = logging.getLogger(__name__)
class GatewayHandler(logging.Handler):
def __init__(self, cog):
self.cog = cog
super().__init__(logging.INFO)
def filter(self, record):
return record.name == 'discord.gateway' \
or 'Shard ID' in record.msg \
or 'Websocket closed ' in record.msg
def emit(self, record):
self.cog.add_record(record)
class Logs(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.process = psutil.Process()
self._batch_lock = asyncio.Lock(loop=bot.loop)
self._data_batch = []
self._gateway_queue = asyncio.Queue(loop=bot.loop)
self.gateway_worker.start()
self._resumes = []
self._identifies = defaultdict(list)
self.icon = ":newspaper:"
self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/233/newspaper_1f4f0.png"
def _clear_gateway_data(self):
one_week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7)
to_remove = [
index for index, dt in enumerate(self._resumes)
if dt < one_week_ago
]
for index in reversed(to_remove):
del self._resumes[index]
for shard_id, dates in self._identifies.items():
to_remove = [index for index, dt in enumerate(dates) if
dt < one_week_ago]
for index in reversed(to_remove):
del dates[index]
@tasks.loop(seconds=0.0)
async def gateway_worker(self):
record = await self._gateway_queue.get()
await self.notify_gateway_status(record)
async def register_command(self, ctx):
if ctx.command is None:
return
command = ctx.command.qualified_name
self.bot.command_stats[command] += 1
message = ctx.message
if ctx.guild is None:
destination = 'Private Message'
guild_id = None
else:
destination = f'#{message.channel} ({message.guild})'
guild_id = ctx.guild.id
log.info(
f'{message.created_at}: {message.author} '
f'in {destination}: {message.content}')
async with self._batch_lock:
self._data_batch.append({
'guild': guild_id,
'channel': ctx.channel.id,
'author': ctx.author.id,
'used': message.created_at.isoformat(),
'prefix': ctx.prefix,
'command': command,
'failed': ctx.command_failed,
})
@commands.Cog.listener()
async def on_command_completion(self, ctx):
await self.register_command(ctx)
@commands.Cog.listener()
async def on_socket_response(self, msg):
self.bot.socket_stats[msg.get('t')] += 1
@property
def webhook(self):
return self.bot.logs_webhook
async def log_error(self, *, ctx=None, extra=None):
e = discord.Embed(title='Error', colour=0xdd5f53)
e.description = f'```py\n{traceback.format_exc()}\n```'
e.add_field(name='Extra', value=extra, inline=False)
e.timestamp = datetime.datetime.utcnow()
if ctx is not None:
fmt = '{0} (ID: {0.id})'
author = fmt.format(ctx.author)
channel = fmt.format(ctx.channel)
guild = 'None' if ctx.guild is None else fmt.format(ctx.guild)
e.add_field(name='Author', value=author)
e.add_field(name='Channel', value=channel)
e.add_field(name='Guild', value=guild)
await self.webhook.send(embed=e)
async def send_guild_stats(self, e, guild):
e.add_field(name='Name', value=guild.name)
e.add_field(name='ID', value=guild.id)
e.add_field(name='Shard ID', value=guild.shard_id or 'N/A')
e.add_field(name='Owner',
value=f'{guild.owner} (ID: {guild.owner.id})')
bots = sum(member.bot for member in guild.members)
total = guild.member_count
online = sum(member.status is discord.Status.online
for member in guild.members)
e.add_field(name='Members', value=str(total))
e.add_field(name='Bots', value=f'{bots} ({bots / total:.2%})')
e.add_field(name='Online', value=f'{online} ({online / total:.2%})')
if guild.icon:
e.set_thumbnail(url=guild.icon_url)
if guild.me:
e.timestamp = guild.me.joined_at
await self.webhook.send(embed=e)
@commands.Cog.listener()
async def on_guild_join(self, guild: discord.guild):
e = discord.Embed(colour=0x53dda4, title='New Guild') # green colour
await self.send_guild_stats(e, guild)
@commands.Cog.listener()
async def on_guild_remove(self, guild: discord.guild):
e = discord.Embed(colour=0xdd5f53, title='Left Guild') # red colour
await self.send_guild_stats(e, guild)
@commands.Cog.listener()
async def on_message(self, message: discord.message):
if message.guild is None:
e = discord.Embed(colour=0x0a97f5, title='New DM') # blue colour
e.set_author(
name=message.author,
icon_url=message.author.avatar_url_as(format='png')
)
e.description = message.content
if len(message.attachments) > 0:
e.set_image(url=message.attachments[0].url)
e.set_footer(text=f"User ID: {message.author.id}")
await self.webhook.send(embed=e)
@commands.Cog.listener()
async def on_command_error(self, ctx, error):
await self.register_command(ctx)
if not isinstance(error, (
commands.CommandInvokeError, commands.ConversionError)):
return
error = error.original
if isinstance(error, (discord.Forbidden, discord.NotFound)):
return
e = discord.Embed(title='Command Error', colour=0xcc3366)
e.add_field(name='Name', value=ctx.command.qualified_name)
e.add_field(name='Author', value=f'{ctx.author} (ID: {ctx.author.id})')
fmt = f'Channel: {ctx.channel} (ID: {ctx.channel.id})'
if ctx.guild:
fmt = f'{fmt}\nGuild: {ctx.guild} (ID: {ctx.guild.id})'
e.add_field(name='Location', value=fmt, inline=False)
e.add_field(name='Content', value=textwrap.shorten(
ctx.message.content,
width=512
))
exc = ''.join(traceback.format_exception(
type(error), error, error.__traceback__,
chain=False)
)
e.description = f'```py\n{exc}\n```'
e.timestamp = datetime.datetime.utcnow()
await self.webhook.send(embed=e)
@commands.Cog.listener()
async def on_socket_raw_send(self, data):
if '"op":2' not in data and '"op":6' not in data:
return
back_to_json = json.loads(data)
if back_to_json['op'] == 2:
payload = back_to_json['d']
inner_shard = payload.get('shard', [0])
self._identifies[inner_shard[0]].append(datetime.datetime.utcnow())
else:
self._resumes.append(datetime.datetime.utcnow())
self._clear_gateway_data()
def add_record(self, record):
self._gateway_queue.put_nowait(record)
async def notify_gateway_status(self, record):
types = {
'INFO': ':information_source:',
'WARNING': ':warning:'
}
emoji = types.get(record.levelname, ':heavy_multiplication_x:')
dt = datetime.datetime.utcfromtimestamp(record.created)
msg = f'{emoji} `[{dt:%Y-%m-%d %H:%M:%S}] {record.message}`'
await self.webhook.send(msg)
@command_extra(name='commandstats', hidden=True, category='misc')
@commands.is_owner()
async def _commandstats(self, ctx, limit=20):
counter = self.bot.command_stats
width = len(max(counter, key=len))
if limit > 0:
common = counter.most_common(limit)
else:
common = counter.most_common()[limit:]
output = '\n'.join(f'{k:<{width}}: {c}' for k, c in common)
await ctx.send(f'```\n{output}\n```')
@command_extra(name='socketstats', hidden=True, category='misc')
@commands.is_owner()
async def _socketstats(self, ctx):
delta = datetime.datetime.utcnow() - self.bot.uptime
minutes = delta.total_seconds() / 60
total = sum(self.bot.socket_stats.values())
cpm = total / minutes
await ctx.send(
f'{total} socket events observed ({cpm:.2f}/minute):\n{self.bot.socket_stats}')
@command_extra(name='uptime', category='misc')
async def _uptime(self, ctx):
uptime = humanize.naturaltime(
datetime.datetime.utcnow() - self.bot.uptime)
await ctx.send(f'Uptime: **{uptime}**')
async def on_error(self, event, *args):
e = discord.Embed(title='Event Error', colour=0xa32952)
e.add_field(name='Event', value=event)
e.description = f'```py\n{traceback.format_exc()}\n```'
e.timestamp = datetime.datetime.utcnow()
args_str = ['```py']
for index, arg in enumerate(args):
args_str.append(f'[{index}]: {arg!r}')
args_str.append('```')
e.add_field(name='Args', value='\n'.join(args_str), inline=False)
hook = self.get_cog('Logs').webhook
try:
await hook.send(embed=e)
except (discord.HTTPException, discord.NotFound,
discord.Forbidden, discord.InvalidArgument):
pass
def setup(bot: TuxBot):
cog = Logs(bot)
bot.add_cog(cog)
handler = GatewayHandler(cog)
logging.getLogger().addHandler(handler)
commands.AutoShardedBot.on_error = on_error

110
cogs/Monitoring.py Normal file
View file

@ -0,0 +1,110 @@
import logging
import urllib.request
from datetime import datetime
import discord
from aiohttp import web
from discord.ext import tasks, commands
from bot import TuxBot
log = logging.getLogger(__name__)
class Monitoring(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.site = web.TCPSite
self.ping_clusters.start()
app = web.Application()
app.add_routes([web.get('/', self.handle)])
self.runner = web.AppRunner(app)
self.bot.loop.create_task(self.start_HTTPMonitoring_server())
def cog_unload(self):
self.ping_clusters.stop()
@tasks.loop(seconds=10.0)
async def ping_clusters(self):
for cluster in self.bot.fallbacks:
if cluster == 'DEFAULT':
pass
else:
cluster = self.bot.fallbacks[cluster]
if not cluster.get('This', False):
host = cluster.get('Host')
port = cluster.get('Port')
try:
req = urllib.request.urlopen(
f"http://{host}:{port}",
timeout=2
)
except Exception:
global_channel = await self.bot.fetch_channel(
661347412463321098
)
e = discord.Embed(
title=f"Server `{cluster.get('Name')}`",
color=discord.colour.Color.red(),
description=f"Server **`{cluster.get('Name')}`** with address **`http://{host}:{port}`** is down ! ",
timestamp=datetime.now()
)
e.set_thumbnail(
url='https://upload.wikimedia.org/wikipedia/commons/7/75/Erroricon404.PNG'
)
await global_channel.send(embed=e)
else:
print(req.read().decode())
@ping_clusters.before_loop
async def before_pinging(self):
await self.bot.wait_until_ready()
cluster = self.bot.cluster
host = cluster.get('Host')
port = cluster.get('Port')
global_channel = await self.bot.fetch_channel(
661347412463321098
)
e = discord.Embed(
title=f"Server `{cluster.get('Name')}`",
color=discord.colour.Color.green(),
description=f"Server **`{cluster.get('Name')}`** with address **`http://{host}:{port}`** is started ! ",
timestamp=datetime.now()
)
e.set_thumbnail(
url='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d1/MW-Icon-CheckMark.svg/1024px-MW-Icon-CheckMark.svg.png'
)
await global_channel.send(embed=e)
async def start_HTTPMonitoring_server(self):
host = self.bot.cluster.get('WebPage')
port = self.bot.cluster.get('Port')
print(f"Starting HTTP Monitoring server on {host}:{port}")
await self.runner.setup()
self.site = web.TCPSite(self.runner, host, port)
await self.site.start()
async def handle(self, _):
return web.json_response(
{
'message': "I'm alive !",
'ws': self.bot.latency * 1000
}
)
def setup(bot: TuxBot):
bot.add_cog(Monitoring(bot))

222
cogs/Poll.py Normal file
View file

@ -0,0 +1,222 @@
import json
import logging
from typing import Union
import discord
from discord.ext import commands
from yarl import URL
from bot import TuxBot
from utils import PollModel, ResponsesModel
from utils import Texts
from utils.functions import emotes as utils_emotes
from utils import group_extra
log = logging.getLogger(__name__)
class Poll(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.icon = ":bar_chart:"
self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/233/bar-chart_1f4ca.png:"
def get_poll(self, pld) -> Union[bool, PollModel]:
if pld.user_id != self.bot.user.id:
poll = self.bot.database.session \
.query(PollModel) \
.filter(PollModel.message_id == pld.message_id)
if poll.count() > 0:
poll = poll.one()
emotes = utils_emotes.get(poll.available_choices)
if pld.emoji.name in emotes:
return poll
return False
async def remove_reaction(self, pld):
channel: discord.TextChannel = self.bot.get_channel(pld.channel_id)
message: discord.Message = await channel.fetch_message(pld.message_id)
user: discord.User = await self.bot.fetch_user(pld.user_id)
await message.remove_reaction(pld.emoji.name, user)
@commands.Cog.listener()
async def on_raw_reaction_add(self, pld: discord.RawReactionActionEvent):
poll = self.get_poll(pld)
if poll:
if poll.is_anonymous:
try:
await self.remove_reaction(pld)
except discord.errors.Forbidden:
pass
choice = utils_emotes.get_index(pld.emoji.name)
responses = self.bot.database.session.query(ResponsesModel) \
.filter(
ResponsesModel.poll_id == poll.id,
ResponsesModel.user == pld.user_id,
ResponsesModel.choice == choice
)
if responses.count() != 0:
response = responses.first()
self.bot.database.session.delete(response)
self.bot.database.session.commit()
else:
response = ResponsesModel(
user=pld.user_id,
poll_id=poll.id,
choice=choice
)
self.bot.database.session.add(response)
self.bot.database.session.commit()
await self.update_poll(poll.id)
@commands.Cog.listener()
async def on_raw_reaction_remove(self,
pld: discord.RawReactionActionEvent):
poll = self.get_poll(pld)
if poll:
choice = utils_emotes.get_index(pld.emoji.name)
responses = self.bot.database.session.query(ResponsesModel) \
.filter(
ResponsesModel.poll_id == poll.id,
ResponsesModel.user == pld.user_id,
ResponsesModel.choice == choice
)
if responses.count() != 0:
response = responses.first()
self.bot.database.session.delete(response)
self.bot.database.session.commit()
await self.update_poll(poll.id)
###########################################################################
async def create_poll(self, ctx: commands.Context, poll: str, anonymous):
question = (poll.split('|')[0]).strip()
responses = [response.strip() for response in poll.split('|')[1:]]
emotes = utils_emotes.get(len(responses))
stmt = await ctx.send(Texts('poll', ctx).get('**Preparation...**'))
poll_row = PollModel()
self.bot.database.session.add(poll_row)
self.bot.database.session.flush()
e = discord.Embed(description=f"**{question}**")
e.set_author(
name=ctx.author,
icon_url="https://cdn.gnous.eu/tuxbot/survey1.png"
)
for i, response in enumerate(responses):
e.add_field(
name=f"__{emotes[i]}` - {response.capitalize()}`__",
value="**0** vote"
)
e.set_footer(text=f"ID: #{poll_row.id}")
poll_row.channel_id = stmt.channel.id
poll_row.message_id = stmt.id
poll_row.content = e.to_dict()
poll_row.is_anonymous = anonymous
poll_row.available_choices = len(responses)
self.bot.database.session.commit()
await stmt.edit(content='', embed=e)
for emote in range(len(responses)):
await stmt.add_reaction(emotes[emote])
async def update_poll(self, poll_id: int):
poll = self.bot.database.session \
.query(PollModel) \
.filter(PollModel.id == poll_id) \
.one()
channel: discord.TextChannel = self.bot.get_channel(poll.channel_id)
message: discord.Message = await channel.fetch_message(poll.message_id)
chart_base_url = "https://quickchart.io/chart?backgroundColor=white&c="
chart_options = {
'type': 'pie',
'data': {
'labels': [],
'datasets': [
{
'data': []
}
]
}
}
content = json.loads(poll.content) \
if isinstance(poll.content, str) \
else poll.content
raw_responses = self.bot.database.session \
.query(ResponsesModel) \
.filter(ResponsesModel.poll_id == poll_id)
responses = {}
for response in raw_responses.all():
if responses.get(response.choice):
responses[response.choice] += 1
else:
responses[response.choice] = 1
for i, field in enumerate(content.get('fields')):
responders = responses.get(i, 0)
chart_options.get('data') \
.get('labels') \
.append(field.get('name')[5:].replace('__', ''))
chart_options.get('data') \
.get('datasets')[0] \
.get('data') \
.append(responders)
if responders <= 1:
field['value'] = f"**{responders}** vote"
else:
field['value'] = f"**{responders}** votes"
e = discord.Embed(description=content.get('description'))
e.set_author(
name=content.get('author').get('name'),
icon_url=content.get('author').get('icon_url')
)
chart_url = URL(chart_base_url + json.dumps(chart_options))
e.set_thumbnail(url=str(chart_url))
for field in content.get('fields'):
e.add_field(
name=field.get('name'),
value=field.get('value'),
inline=True
)
e.set_footer(text=content.get('footer').get('text'))
await message.edit(embed=e)
poll.content = json.dumps(content)
self.bot.database.session.commit()
@group_extra(name='poll', aliases=['sondage'], category='poll')
async def _poll(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('poll')
@_poll.group(name='create', aliases=['new', 'nouveau'])
async def _poll_create(self, ctx: commands.Context, *, poll: str):
is_anonymous = '--anonyme' in poll
poll = poll.replace('--anonyme', '')
await self.create_poll(ctx, poll, anonymous=is_anonymous)
def setup(bot: TuxBot):
bot.add_cog(Poll(bot))

410
cogs/Useful.py Normal file
View file

@ -0,0 +1,410 @@
# Created by romain at 04/01/2020
import logging
import os
import pathlib
import platform
import random
import re
import socket
import time
from socket import AF_INET6
from io import BytesIO
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
from PIL import ImageOps
import aiohttp
import discord
import humanize
import psutil
from discord.ext import commands
from tcp_latency import measure_latency
from bot import TuxBot
from utils import Texts
from utils import command_extra, group_extra
log = logging.getLogger(__name__)
class Useful(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.icon = ":toolbox:"
self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/233/toolbox_1f9f0.png"
@staticmethod
def _latest_commits():
cmd = 'git log -n 3 -s --format="[\`%h\`](https://git.gnous.eu/gnouseu/tuxbot-bot/commits/%H) %s (%cr)"'
return os.popen(cmd).read().strip()
@staticmethod
def fetch_info():
total_lines = 0
total_python_lines = 0
file_amount = 0
python_file_amount = 0
ENV = "env"
for path, _, files in os.walk("."):
for name in files:
file_dir = str(pathlib.PurePath(path, name))
if (
not name.endswith(".py")
and not name.endswith(".po")
and not name.endswith(".json")
) or ENV in file_dir:
continue
file_amount += 1
python_file_amount += 1 if name.endswith(".py") else 0
with open(file_dir, "r", encoding="utf-8") as file:
for line in file:
if not line.strip().startswith("#") \
or not line.strip():
total_lines += 1
total_python_lines += 1 if name.endswith(".py") \
else 0
return (file_amount, total_lines), (
python_file_amount, total_python_lines)
@staticmethod
def luhn_checker(number: int):
digits = [int(x) for x in reversed(str(number))]
for index, digit in enumerate(digits, start=1):
digit = digit * 2 if index % 2 == 0 else digit
if digit >= 10:
digit = sum(int(x) for x in list(str(digit)))
digits[index - 1] = digit
return sum(digits) % 10 == 0
###########################################################################
@command_extra(name='iplocalise', category='network')
async def _iplocalise(self, ctx: commands.Context, addr, ip_type=''):
addr = re.sub(r'http(s?)://', '', addr)
addr = addr[:-1] if addr.endswith('/') else addr
await ctx.trigger_typing()
try:
if 'v6' in ip_type:
try:
ip = socket.getaddrinfo(addr, None, AF_INET6)[1][4][0]
except socket.gaierror:
return await ctx.send(
Texts('useful', ctx).get('ipv6 not available'))
else:
ip = socket.gethostbyname(addr)
async with self.bot.session.get(f"http://ip-api.com/json/{ip}") \
as s:
response: dict = await s.json()
if response.get('status') == 'success':
e = discord.Embed(
title=f"{Texts('useful', ctx).get('Information for')}"
f" ``{addr}`` *`({response.get('query')})`*",
color=0x5858d7
)
e.add_field(
name=Texts('useful', ctx).get('Belongs to :'),
value=response['org'] if response['org'] else 'N/A',
inline=False
)
e.add_field(
name=Texts('useful', ctx).get('Is located at :'),
value=response['city'] if response['city'] else 'N/A',
inline=True
)
e.add_field(
name="Region :",
value=f"{response['regionName'] if response['regionName'] else 'N/A'} "
f"({response['country'] if response['country'] else 'N/A'})",
inline=True
)
e.set_thumbnail(
url=f"https://www.countryflags.io/"
f"{response.get('countryCode')}/flat/64.png")
await ctx.send(embed=e)
else:
await ctx.send(
content=f"{Texts('useful', ctx).get('info not available')}"
f"``{response['query'] if response['query'] else 'N/A'}``")
except Exception as e:
await ctx.send(e)
await ctx.send(
f"{Texts('useful', ctx).get('Cannot connect to host')} {addr}"
)
###########################################################################
@command_extra(name='getheaders', category='network')
async def _getheaders(self, ctx: commands.Context, addr: str):
if (addr.startswith('http') or addr.startswith('ftp')) is not True:
addr = f"http://{addr}"
await ctx.trigger_typing()
try:
async with self.bot.session.get(addr) as s:
e = discord.Embed(
title=f"{Texts('useful', ctx).get('Headers of')} {addr}",
color=0xd75858
)
e.add_field(name="Status", value=s.status, inline=True)
e.set_thumbnail(url=f"https://http.cat/{s.status}")
headers = dict(s.headers.items())
headers.pop('Set-Cookie', headers)
for key, value in headers.items():
e.add_field(name=key, value=value, inline=True)
await ctx.send(embed=e)
except aiohttp.ClientError:
await ctx.send(
f"{Texts('useful', ctx).get('Cannot connect to host')} {addr}"
)
###########################################################################
@command_extra(name='git', aliases=['sources', 'source', 'github'], category='misc')
async def _git(self, ctx):
e = discord.Embed(
title=Texts('useful', ctx).get('git repo'),
description=Texts('useful', ctx).get('git text'),
colour=0xE9D460
)
e.set_author(
name='Gnous',
icon_url="https://cdn.gnous.eu/logo1.png"
)
await ctx.send(embed=e)
###########################################################################
@command_extra(name='quote', category='misc')
async def _quote(self, ctx, message_id: discord.Message):
e = discord.Embed(
colour=message_id.author.colour,
description=message_id.clean_content,
timestamp=message_id.created_at
)
e.set_author(
name=message_id.author.display_name,
icon_url=message_id.author.avatar_url_as(format="jpg")
)
if len(message_id.attachments) >= 1:
e.set_image(url=message_id.attachments[0].url)
e.add_field(name="**Original**",
value=f"[Go!]({message_id.jump_url})")
e.set_footer(text="#" + message_id.channel.name)
await ctx.send(embed=e)
###########################################################################
@command_extra(name='ping', category='network')
async def _ping(self, ctx: commands.Context):
start = time.perf_counter()
await ctx.trigger_typing()
end = time.perf_counter()
latency = round(self.bot.latency * 1000, 2)
typing = round((end - start) * 1000, 2)
discordapp = measure_latency(host='discordapp.com', wait=0)[0]
e = discord.Embed(title='Ping', color=discord.Color.teal())
e.add_field(name='Websocket', value=f'{latency}ms')
e.add_field(name='Typing', value=f'{typing}ms')
e.add_field(name='discordapp.com', value=f'{discordapp}ms')
await ctx.send(embed=e)
###########################################################################
@command_extra(name='info', aliases=['about'], category='misc')
async def _info(self, ctx: commands.Context):
proc = psutil.Process()
total, python = self.fetch_info()
with proc.oneshot():
mem = proc.memory_full_info()
e = discord.Embed(
title=Texts('useful', ctx).get('Information about TuxBot'),
color=0x89C4F9)
e.add_field(
name=f"__:busts_in_silhouette: "
f"{Texts('useful', ctx).get('Development')}__",
value=f"**Romain#5117:** [git](https://git.gnous.eu/Romain)\n"
f"**Outout#4039:** [git](https://git.gnous.eu/mael)\n",
inline=True
)
e.add_field(
name="__<:python:596577462335307777> Python__",
value=f"**python** `{platform.python_version()}`\n"
f"**discord.py** `{discord.__version__}`",
inline=True
)
e.add_field(
name="__:gear: Usage__",
value=f"**{humanize.naturalsize(mem.rss)}** "
f"{Texts('useful', ctx).get('physical memory')}\n"
f"**{humanize.naturalsize(mem.vms)}** "
f"{Texts('useful', ctx).get('virtual memory')}\n",
inline=True
)
e.add_field(
name=f"__{Texts('useful', ctx).get('Servers count')}__",
value=str(len(self.bot.guilds)),
inline=True
)
e.add_field(
name=f"__{Texts('useful', ctx).get('Channels count')}__",
value=str(len([_ for _ in self.bot.get_all_channels()])),
inline=True
)
e.add_field(
name=f"__{Texts('useful', ctx).get('Members count')}__",
value=str(len([_ for _ in self.bot.get_all_members()])),
inline=True
)
e.add_field(
name=f"__:file_folder: {Texts('useful', ctx).get('Files')}__",
value=f"{total[0]} *({python[0]} <:python:596577462335307777>)*",
inline=True
)
e.add_field(
name=f"__¶ {Texts('useful', ctx).get('Lines')}__",
value=f"{total[1]} *({python[1]} <:python:596577462335307777>)*",
inline=True
)
e.add_field(
name=f"__{Texts('useful', ctx).get('Latest changes')}__",
value=self._latest_commits(),
inline=False
)
e.add_field(
name=f"__:link: {Texts('useful', ctx).get('Links')}__",
value="[tuxbot.gnous.eu](https://tuxbot.gnous.eu/) "
"| [gnous.eu](https://gnous.eu/) "
"| [git](https://git.gnous.eu/gnouseu/tuxbot-bot) "
"| [status](https://status.gnous.eu/check/154250) "
f"| [{Texts('useful', ctx).get('Invite')}](https://discordapp.com/oauth2/authorize?client_id=301062143942590465&scope=bot&permissions=268749888)",
inline=False
)
e.set_footer(text=f'version: {self.bot.version} '
f'• prefix: {ctx.prefix}')
await ctx.send(embed=e)
###########################################################################
@command_extra(name='credits', aliases=['contributors', 'authors'], category='misc')
async def _credits(self, ctx: commands.Context):
e = discord.Embed(
title=Texts('useful', ctx).get('Contributors'),
color=0x36393f
)
e.add_field(
name="**Outout#4039** ",
value="• https://git.gnous.eu/mael \n"
"• mael@gnous.eu\n"
"• [@outoutxyz](https://twitter.com/outouxyz)",
inline=True
)
e.add_field(
name="**Romain#5117** ",
value="• https://git.gnous.eu/Romain\n"
"• romain@gnous.eu",
inline=True
)
await ctx.send(embed=e)
###########################################################################
@group_extra(name='cb', aliases=['cc'], category='misc')
@commands.cooldown(1, 5, type=commands.BucketType.user)
async def _cb(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('cb')
@_cb.command(name='validate', aliases=['valid', 'correct'], category='misc')
@commands.cooldown(1, 5, type=commands.BucketType.user)
async def _cb_validate(self, ctx: commands.Context, *, number: int):
valid = self.luhn_checker(number)
await ctx.send(
Texts(
'useful', ctx
).get(
'valid_credit_card'
if valid
else 'invalid_credit_card'
)
)
@_cb.command(name='generate', aliases=['new', 'get'], category='misc')
@commands.cooldown(1, 5, type=commands.BucketType.user)
async def _cb_generate(self, ctx: commands.Context):
await ctx.channel.trigger_typing()
number = random.randint(4000_0000_0000_0000, 5999_9999_9999_9999)
while not self.luhn_checker(number):
number = random.randint(4000_0000_0000_0000, 5999_9999_9999_9999)
number = str(number)
cvv = ''.join(random.choice("abcdefghij") for _ in range(3))
with Image.open("utils/images/blank_credit_card.png") as blank:
cc_font = ImageFont.truetype('utils/fonts/credit_card.ttf', 26)
user_font = ImageFont.truetype('utils/fonts/credit_card.ttf', 20)
draw = ImageDraw.Draw(blank)
cvv_text = Image.new('L', (500, 50))
cvv_draw = ImageDraw.Draw(cvv_text)
cvv_draw.text((0, 0), cvv, font=user_font, fill=255)
cvv_rotated = cvv_text.rotate(23, expand=1)
draw.text(
(69, 510),
' '.join([number[i:i+4] for i in range(0, len(number), 4)]),
(210, 210, 210),
font=cc_font
)
draw.text(
(69, 550),
ctx.author.name.upper(),
(210, 210, 210),
font=user_font
)
blank.paste(ImageOps.colorize(cvv_rotated, (0, 0, 0), (0, 0, 0)), (470, 0), cvv_rotated)
output = BytesIO()
blank.save(output, 'png')
output.seek(0)
await ctx.send(file=discord.File(fp=output, filename="credit_card.png"))
def setup(bot: TuxBot):
bot.add_cog(Useful(bot))

64
cogs/User.py Normal file
View file

@ -0,0 +1,64 @@
import logging
from discord.ext import commands
from bot import TuxBot
from utils import AliasesModel
from utils import Texts
from utils import group_extra
log = logging.getLogger(__name__)
class User(commands.Cog):
def __init__(self, bot: TuxBot):
self.bot = bot
self.icon = ":bust_in_silhouette:"
self.big_icon = "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/twitter/233/bust-in-silhouette_1f464.png"
###########################################################################
@group_extra(name='alias', aliases=['aliases'], category='alias')
async def _alias(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send_help('alias')
@_alias.command(name='add', aliases=['set', 'new'])
async def _alias_add(self, ctx: commands.Context, *, user_alias: str):
is_global = False
if '--global' in user_alias:
is_global = True
user_alias.replace('--global', '')
user_alias = user_alias.split(' -> ')
if len(user_alias) != 2:
return await ctx.send_help('alias')
command = user_alias[1]
user_alias = user_alias[0]
if self.bot.get_command(command) is None:
return await ctx.send(Texts('user').get('Command not found'))
alias = AliasesModel(
user_id=ctx.author.id,
alias=user_alias,
command=command,
guild="global" if is_global else str(ctx.guild.id)
)
self.bot.database.session.add(alias)
self.bot.database.session.commit()
@_alias.command(name='remove', aliases=['drop', 'del', 'delete'])
async def _alias_remove(self, ctx: commands.Context, prefix: str):
...
@_alias.command(name='list', aliases=['show', 'all'])
async def _alias_list(self, ctx: commands.Context):
...
def setup(bot: TuxBot):
bot.add_cog(User(bot))

View file

@ -1,38 +0,0 @@
FROM python:3.9-slim-buster
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \
# dependencies for building Python packages
&& apt-get install -y build-essential \
# psycopg2 dependencies
&& apt-get install -y libpq-dev \
# Translations dependencies
&& apt-get install -y gettext \
# Git
&& apt-get install -y git \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
# Requirements are installed here to ensure they will be cached.
COPY ./dev.requirements.txt /app/dev.requirements.txt
COPY ./tuxbot /app/tuxbot
COPY ./data /app/data
COPY ./setup.cfg /app/setup.cfg
COPY ./setup.py /app/setup.py
RUN pip install -r /app/dev.requirements.txt
RUN pip install ./app
COPY ./compose/production/tuxbot/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY ./compose/local/tuxbot/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
WORKDIR /app
ENTRYPOINT ["/entrypoint"]

View file

@ -1,6 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset

View file

@ -1,6 +0,0 @@
FROM postgres:12.3
COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
RUN chmod +x /usr/local/bin/maintenance/*
RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
&& rmdir /usr/local/bin/maintenance

View file

@ -1,5 +0,0 @@
#!/usr/bin/env bash
BACKUP_DIR_PATH='/backups'
BACKUP_FILE_PREFIX='backup'

View file

@ -1,12 +0,0 @@
#!/usr/bin/env bash
countdown() {
declare desc="A simple countdown. Source: https://superuser.com/a/611582"
local seconds="${1}"
local d=$(($(date +%s) + "${seconds}"))
while [ "$d" -ge `date +%s` ]; do
echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r";
sleep 0.1
done
}

View file

@ -1,41 +0,0 @@
#!/usr/bin/env bash
message_newline() {
echo
}
message_debug()
{
echo -e "DEBUG: ${@}"
}
message_welcome()
{
echo -e "\e[1m${@}\e[0m"
}
message_warning()
{
echo -e "\e[33mWARNING\e[0m: ${@}"
}
message_error()
{
echo -e "\e[31mERROR\e[0m: ${@}"
}
message_info()
{
echo -e "\e[37mINFO\e[0m: ${@}"
}
message_suggestion()
{
echo -e "\e[33mSUGGESTION\e[0m: ${@}"
}
message_success()
{
echo -e "\e[32mSUCCESS\e[0m: ${@}"
}

View file

@ -1,16 +0,0 @@
#!/usr/bin/env bash
yes_no() {
declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message."
local arg1="${1}"
local response=
read -r -p "${arg1} (y/[n])? " response
if [[ "${response}" =~ ^[Yy]$ ]]
then
exit 0
else
exit 1
fi
}

View file

@ -1,38 +0,0 @@
#!/usr/bin/env bash
### Create a database backup.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backup
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
message_welcome "Backing up the '${POSTGRES_DB}' database..."
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi
export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"
backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"
message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."

View file

@ -1,22 +0,0 @@
#!/usr/bin/env bash
### View backups.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backups
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
message_welcome "These are the backups you have got:"
ls -lht "${BACKUP_DIR_PATH}"

View file

@ -1,55 +0,0 @@
#!/usr/bin/env bash
### Restore database from a backup.
###
### Parameters:
### <1> filename of an existing backup.
###
### Usage:
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres restore <1>
set -o errexit
set -o pipefail
set -o nounset
working_dir="$(dirname ${0})"
source "${working_dir}/_sourced/constants.sh"
source "${working_dir}/_sourced/messages.sh"
if [[ -z ${1+x} ]]; then
message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
exit 1
fi
backup_filename="${BACKUP_DIR_PATH}/${1}"
if [[ ! -f "${backup_filename}" ]]; then
message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
exit 1
fi
message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
exit 1
fi
export PGHOST="${POSTGRES_HOST}"
export PGPORT="${POSTGRES_PORT}"
export PGUSER="${POSTGRES_USER}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGDATABASE="${POSTGRES_DB}"
message_info "Dropping the database..."
dropdb "${PGDATABASE}"
message_info "Creating a new database..."
createdb --owner="${POSTGRES_USER}"
message_info "Applying the backup to the new database..."
gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."

View file

@ -1,40 +0,0 @@
FROM node:10-stretch-slim as client-builder
WORKDIR /app
# Python build stage
FROM python:3.9-slim-buster
ENV PYTHONUNBUFFERED 1
RUN apt-get update \
# dependencies for building Python packages
&& apt-get install -y build-essential \
# psycopg2 dependencies
&& apt-get install -y libpq-dev \
# Translations dependencies
&& apt-get install -y gettext \
# cleaning up unused files
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& rm -rf /var/lib/apt/lists/*
RUN addgroup --system tuxbot \
&& adduser --system --ingroup tuxbot tuxbot
# Requirements are installed here to ensure they will be cached.
COPY --chown=tuxbot:tuxbot ./compose/production/tuxbot/entrypoint /entrypoint
RUN sed -i 's/\r$//g' /entrypoint
RUN chmod +x /entrypoint
COPY --chown=tuxbot:tuxbot ./compose/production/tuxbot/start /start
RUN sed -i 's/\r$//g' /start
RUN chmod +x /start
COPY --from=client-builder --chown=tuxbot:tuxbot /app /app
USER tuxbot
WORKDIR /app
ENTRYPOINT ["/entrypoint"]

View file

@ -1,43 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
if [ -z "${POSTGRES_USER}" ]; then
base_postgres_image_default_user='postgres'
export POSTGRES_USER="${base_postgres_image_default_user}"
fi
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
echo "psql at: ${DATABASE_URL}"
postgres_ready() {
python << END
import sys
import asyncpg
import asyncio
async def main():
try:
conn = await asyncpg.connect('postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}')
except Exception:
sys.exit(-1)
await conn.close()
sys.exit(0)
asyncio.get_event_loop().run_until_complete(main())
END
}
until postgres_ready; do
>&2 echo 'Waiting for PostgreSQL to become available...'
sleep 1
done
>&2 echo 'PostgreSQL is available'
exec "$@"

View file

@ -1,8 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
tuxbot dev

View file

@ -0,0 +1,24 @@
[bot]
Token =
Tester =
Activity =
[postgresql]
Username =
Password =
Host =
DBName =
[permissions]
Owners =
[webhook]
ID =
Token =
[misc]
Separator =
[API]
Host =
Port =

View file

@ -0,0 +1,18 @@
[fr-srv01]
Host =
Name = fr-srv01
WebPage = 0.0.0.0
Port =
[rm-dev01]
This = True
Host = 127.0.0.1
Name = rm-dev01
WebPage = 0.0.0.0
Port = 3389
[rm-srv01]
Host = 127.0.0.1
Name = rm-srv01
WebPage = 0.0.0.0
Port = 3390

18
configs/prefixes.cfg Normal file
View file

@ -0,0 +1,18 @@
[280805240977227776]
prefixes = b1.[301062143942590465]*
[303633056944881686]
prefixes = b1.[301062143942590465]*
[373881878471770112]
prefixes = b1.
[336642139381301249]
prefixes = ba.
[274247231534792704]
prefixes = test.
[528679953399676938]
prefixes = test.

19
database.py Normal file
View file

@ -0,0 +1,19 @@
import sqlalchemy
from utils.models import database, metadata
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--migrate", action="store_true")
parser.add_argument("-s", "--seed", action="store_true")
args = parser.parse_args()
if args.migrate:
print("Migrate...")
engine = sqlalchemy.create_engine(str(database.url))
metadata.create_all(engine)
print("Done!")
if args.seed:
print('Seeding...')
# todo: add seeding
print("Done!")

View file

@ -1,3 +0,0 @@
pylint>=2.6.0
black>=20.8b1
mypy>=0.812

17
generate_locales.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
BASEDIR=$(pwd)
cd "$BASEDIR/utils/locales/en/LC_MESSAGES"
for i in *.po ; do
[[ -f "$i" ]] || continue
/usr/lib/python3.8/Tools/i18n/msgfmt.py -o "${i%.po}.mo" "${i%.po}"
done
cd "$BASEDIR/utils/locales/fr/LC_MESSAGES"
for i in *.po ; do
[[ -f "$i" ]] || continue
/usr/lib/python3.8/Tools/i18n/msgfmt.py -o "${i%.po}.mo" "${i%.po}"
done

View file

@ -1,34 +0,0 @@
version: '3'
volumes:
local_postgres_data: {}
local_postgres_data_backups: {}
services:
tuxbot:
build:
context: .
dockerfile: ./compose/local/tuxbot/Dockerfile
restart: always
image: tuxbot_bot_local_tuxbot
container_name: tuxbot
depends_on:
- postgres
volumes:
- .:/app:z
env_file:
- ./.envs/.local/.tuxbot
- ./.envs/.local/.postgres
command: /start
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: tuxbot_bot_production_postgres
container_name: postgres
volumes:
- local_postgres_data:/var/lib/postgresql/data:Z
- local_postgres_data_backups:/backups:z
env_file:
- ./.envs/.local/.postgres

View file

@ -1,30 +0,0 @@
version: '3'
volumes:
production_postgres_data: {}
production_postgres_data_backups: {}
production_traefik: {}
services:
tuxbot:
build:
context: .
dockerfile: ./compose/production/tuxbot/Dockerfile
image: tuxbot_bot_production_tuxbot
depends_on:
- postgres
env_file:
- ./.envs/.production/.tuxbot
- ./.envs/.production/.postgres
command: /start
postgres:
build:
context: .
dockerfile: ./compose/production/postgres/Dockerfile
image: tuxbot_bot_production_postgres
volumes:
- production_postgres_data:/var/lib/postgresql/data:Z
- production_postgres_data_backups:/backups:z
env_file:
- ./.envs/.production/.postgres

13
requirements.txt Normal file
View file

@ -0,0 +1,13 @@
requests
humanize
git+https://github.com/Rapptz/discord.py@master
jishaku
gitpython
orm
asyncpg
psycopg2
configparser
psutil
tcp_latency
yarl
pillow

View file

@ -1,51 +0,0 @@
[metadata]
name = Tuxbot-bot
version = attr: tuxbot.__version__
url = https://github.com/Rom1-J/tuxbot-bot/
author = Romain J.
author_email = romain@gnous.eu
maintainer = Romain J.
maintainer_email = romain@gnous.eu
description = A discord bot made for GnousEU's guild and OpenSource
long_description = file: README.rst
license = agplv3
platforms = linux
[options]
packages = find_namespace:
python_requires = >=3.8
install_requires =
aiocache>=0.11.1
asyncpg>=0.21.0
Babel>=2.8.0
beautifulsoup4>=4.9.3
discord.py @ git+https://github.com/Rapptz/discord.py
discord-ext-menus
humanize>=2.6.0
ipinfo>=4.1.0
ipwhois>=1.2.0
jishaku @ git+https://github.com/Gorialis/jishaku
psutil>=5.7.2
pydig>=0.3.0
; ralgo @ git+https://github.com/Rom1-J/ralgo
rich>=9.10.0
sentry_sdk>=0.20.2
structured_config>=4.12
tortoise-orm>=0.16.17
[options.entry_points]
console_scripts =
tuxbot=tuxbot.__main__:main
tuxbot-setup=tuxbot.setup:setup
[options.packages.find]
include =
tuxbot
tuxbot.*
[options.package_data]
* =
locales/*.po
**/locales/*.po
data/*
data/**/*

View file

@ -1,5 +0,0 @@
from setuptools import setup
setup(
python_requires=">=3.8",
)

2
todo Normal file
View file

@ -0,0 +1,2 @@
reconnaissance d'image
commande d'archivage pour les salons vocaux avec output mp4 dans lequel on voit le pseudo de celui qui parle

View file

@ -1,26 +0,0 @@
import os
from collections import namedtuple
build = os.popen("/usr/bin/git rev-parse --short HEAD").read().strip()
info = os.popen('/usr/bin/git log -n 3 -s --format="%s"').read().strip()
VersionInfo = namedtuple(
"VersionInfo", "major minor micro releaselevel build, info"
)
version_info = VersionInfo(
major=3, minor=0, micro=0, releaselevel="alpha", build=build, info=info
)
__version__ = "v{}.{}.{}-{}.{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.releaselevel,
version_info.build,
).replace("\n", "")
class ExitCodes:
CRITICAL = 1
SHUTDOWN = 0
RESTART = 42

View file

@ -1,28 +0,0 @@
import sys
from tuxbot import ExitCodes
from tuxbot.core.utils.console import console
def main() -> None:
try:
from .__run__ import run # pylint: disable=import-outside-toplevel
run()
except SystemExit as exc:
if exc.code == ExitCodes.RESTART:
sys.exit(exc.code)
else:
raise exc
except Exception:
console.print_exception(
show_locals=True, word_wrap=True, extra_lines=5
)
if __name__ == "__main__":
try:
main()
except Exception:
console.print_exception(
show_locals=True, word_wrap=True, extra_lines=5
)

View file

@ -1,243 +0,0 @@
import argparse
import asyncio
import logging
import signal
import sys
import os
from argparse import Namespace
import discord
import pip
from rich.columns import Columns
from rich.panel import Panel
from rich.table import Table, box
from rich import print as rprint
import tuxbot.logging
from tuxbot.core.bot import Tux
from tuxbot.core.utils import data_manager
from tuxbot.core.utils.console import console
from . import __version__, version_info, ExitCodes
log = logging.getLogger("tuxbot.main")
BORDER_STYLE = "not dim"
def debug_info() -> None:
"""Show debug info relatives to the bot"""
python_version = sys.version.replace("\n", "")
pip_version = pip.__version__
tuxbot_version = __version__
dpy_version = discord.__version__
uptime = os.popen("/usr/bin/uptime").read().strip().split()
console.print(
Panel("[bold blue]Debug Info", style="blue"), justify="center"
)
console.print()
columns = Columns(expand=True, padding=2, align="center")
table = Table(style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD)
table.add_column(
"Bot Info",
)
table.add_row(f"[u]Tuxbot version:[/u] {tuxbot_version}")
table.add_row(f"[u]Major:[/u] {version_info.major}")
table.add_row(f"[u]Minor:[/u] {version_info.minor}")
table.add_row(f"[u]Micro:[/u] {version_info.micro}")
table.add_row(f"[u]Level:[/u] {version_info.releaselevel}")
table.add_row(f"[u]Last change:[/u] {version_info.info}")
columns.add_renderable(table)
table = Table(style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD)
table.add_column(
"Python Info",
)
table.add_row(f"[u]Python version:[/u] {python_version}")
table.add_row(f"[u]Python executable path:[/u] {sys.executable}")
table.add_row(f"[u]Pip version:[/u] {pip_version}")
table.add_row(f"[u]Discord.py version:[/u] {dpy_version}")
columns.add_renderable(table)
table = Table(style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD)
table.add_column(
"Server Info",
)
table.add_row(f"[u]System:[/u] {os.uname().sysname}")
table.add_row(f"[u]System arch:[/u] {os.uname().machine}")
table.add_row(f"[u]Kernel:[/u] {os.uname().release}")
table.add_row(f"[u]User:[/u] {os.getlogin()}")
table.add_row(f"[u]Uptime:[/u] {uptime[2][:-1]}")
table.add_row(
f"[u]Load Average:[/u] {' '.join(map(str, os.getloadavg()))}"
)
columns.add_renderable(table)
console.print(columns)
console.print()
sys.exit(os.EX_OK)
def parse_cli_flags(args: list) -> Namespace:
"""Parser for cli values.
Parameters
----------
args:list
Is a list of all passed values.
Returns
-------
Namespace
"""
parser = argparse.ArgumentParser(
description="Tuxbot - OpenSource bot",
usage="tuxbot [arguments]",
)
parser.add_argument(
"--version",
"-V",
action="store_true",
help="Show tuxbot's used version",
)
parser.add_argument(
"--debug", action="store_true", help="Show debug information."
)
parser.add_argument(
"--token", "-T", type=str, help="Run Tuxbot with passed token"
)
return parser.parse_args(args)
async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> None:
"""Handler when the bot shutdown
It cancels all running task.
Parameters
----------
tux:Tux
Object for the bot.
signal_type:int, None
Exiting signal code.
exit_code:None|int
Code to show when exiting.
"""
if signal_type:
log.info("%s received. Quitting...", signal_type)
elif exit_code is None:
log.info("Shutting down from unhandled exception")
tux.shutdown_code = ExitCodes.CRITICAL
if exit_code is not None:
tux.shutdown_code = exit_code
await tux.shutdown()
async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
"""This run the bot.
Parameters
----------
tux:Tux
Object for the bot.
cli_flags:Namespace
All different flags passed in the console.
Returns
-------
None
When exiting, this function return None.
"""
data_path = data_manager.data_path
tuxbot.logging.init_logging(10, location=data_path / "logs")
log.debug("====Basic Config====")
log.debug("Data Path: %s", data_path)
if cli_flags.token:
token = cli_flags.token
else:
token = tux.config.Core.token
if not token:
log.critical("Token must be set if you want to login.")
sys.exit(ExitCodes.CRITICAL)
try:
await tux.load_packages()
console.print()
await tux.start(token=token)
except discord.LoginFailure:
log.critical("This token appears to be invalid.")
console.print()
console.print(
"[prompt.invalid]This token appears to be valid. [i]exiting...[/i]"
)
sys.exit(ExitCodes.CRITICAL)
except Exception as e:
log.critical(e)
raise e
return None
def run() -> None:
"""Main function"""
tux = None
cli_flags = parse_cli_flags(sys.argv[1:])
if cli_flags.debug:
debug_info()
elif cli_flags.version:
rprint(f"Tuxbot V{version_info.major}")
rprint(f"Complete Version: {__version__}")
sys.exit(os.EX_OK)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
tux = Tux(
cli_flags=cli_flags,
description="Tuxbot, made from and for OpenSource",
dm_help=None,
)
loop.run_until_complete(run_bot(tux, cli_flags))
except KeyboardInterrupt:
console.print(
" [red]Please use <prefix>quit instead of Ctrl+C to Shutdown!"
)
log.warning("Please use <prefix>quit instead of Ctrl+C to Shutdown!")
log.info("Received KeyboardInterrupt")
console.print("[i]Trying to shutdown...")
if tux is not None:
loop.run_until_complete(shutdown_handler(tux, signal.SIGINT))
except SystemExit as exc:
log.info("Shutting down with exit code: %s", exc.code)
if tux is not None:
loop.run_until_complete(shutdown_handler(tux, None, exc.code))
raise
except Exception as exc:
log.error("Unexpected exception (%s): ", type(exc))
console.print_exception(show_locals=True)
if tux is not None:
loop.run_until_complete(shutdown_handler(tux, None, 1))
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
log.info("Please wait, cleaning up a bit more")
loop.run_until_complete(asyncio.sleep(1))
asyncio.set_event_loop(None)
loop.stop()
loop.close()
exit_code = ExitCodes.CRITICAL if tux is None else tux.shutdown_code
sys.exit(exit_code)

View file

@ -1,19 +0,0 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .admin import Admin
from .config import AdminConfig, HAS_MODELS
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
version_info = VersionInfo(major=2, minor=0, micro=0, release_level="alpha")
__version__ = "v{}.{}.{}-{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.release_level,
).replace("\n", "")
def setup(bot: Tux):
bot.add_cog(Admin(bot))

View file

@ -1,57 +0,0 @@
import logging
from discord.ext import commands
from jishaku.models import copy_context_with
from tuxbot.core.utils import checks
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
from tuxbot.core.utils.functions.extra import (
command_extra,
ContextPlus,
)
log = logging.getLogger("tuxbot.cogs.Admin")
_ = Translator("Admin", __file__)
class Admin(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
# =========================================================================
# =========================================================================
@command_extra(name="quit", aliases=["shutdown"], deletable=False)
@checks.is_owner()
async def _quit(self, ctx: ContextPlus):
await ctx.send("*quit...*")
await self.bot.shutdown()
@command_extra(name="restart", deletable=False)
@checks.is_owner()
async def _restart(self, ctx: ContextPlus):
await ctx.send("*restart...*")
await self.bot.shutdown(restart=True)
@command_extra(name="update", deletable=False)
@checks.is_owner()
async def _update(self, ctx: ContextPlus):
sh = "jsk sh"
git = f"{sh} git pull"
update = f"{sh} make update"
git_command_ctx = await copy_context_with(
ctx, content=ctx.prefix + git
)
update_command_ctx = await copy_context_with(
ctx, content=ctx.prefix + update
)
await git_command_ctx.command.invoke(git_command_ctx)
await update_command_ctx.command.invoke(update_command_ctx)
await self._restart(ctx)

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class AdminConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -1,18 +0,0 @@
# English translations for Tuxbot-bot package.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-19 14:42+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

View file

@ -1,19 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-19 14:42+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

View file

@ -1,18 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-03-01 14:59+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

View file

@ -1,19 +0,0 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .custom import Custom
from .config import CustomConfig, HAS_MODELS
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
__version__ = "v{}.{}.{}-{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.release_level,
).replace("\n", "")
def setup(bot: Tux):
bot.add_cog(Custom(bot))

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class CustomConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -1,112 +0,0 @@
import logging
from typing import List
import discord
from discord.ext import commands
from tuxbot.cogs.Custom.functions.converters import AliasConvertor
from tuxbot.core.bot import Tux
from tuxbot.core.config import set_for_key, search_for, set_if_none
from tuxbot.core.config import Config
from tuxbot.core.i18n import (
Translator,
find_locale,
get_locale_name,
list_locales,
)
from tuxbot.core.utils.functions.extra import (
group_extra,
ContextPlus,
)
log = logging.getLogger("tuxbot.cogs.Custom")
_ = Translator("Custom", __file__)
class Custom(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
async def cog_command_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.send(_(str(error), ctx, self.bot.config))
# =========================================================================
# =========================================================================
async def _get_aliases(self, ctx: ContextPlus) -> dict:
return search_for(self.bot.config.Users, ctx.author.id, "aliases")
async def _save_lang(self, ctx: ContextPlus, lang: str) -> None:
set_for_key(
self.bot.config.Users, ctx.author.id, Config.User, locale=lang
)
async def _save_alias(self, ctx: ContextPlus, alias: dict) -> None:
set_for_key(
self.bot.config.Users, ctx.author.id, Config.User, alias=alias
)
# =========================================================================
# =========================================================================
@group_extra(name="custom", aliases=["perso"], deletable=True)
@commands.guild_only()
async def _custom(self, ctx: ContextPlus):
"""Manage custom settings."""
@_custom.command(name="locale", aliases=["langue", "lang"])
async def _custom_locale(self, ctx: ContextPlus, lang: str):
try:
await self._save_lang(ctx, find_locale(lang.lower()))
await ctx.send(
_(
"Locale changed for you to {lang} successfully",
ctx,
self.bot.config,
).format(lang=f"`{get_locale_name(lang).lower()}`")
)
except NotImplementedError:
e = discord.Embed(
title=_("List of available locales: ", ctx, self.bot.config),
description=list_locales,
color=0x36393E,
)
await ctx.send(embed=e)
@_custom.command(name="alias", aliases=["aliases"])
async def _custom_alias(self, ctx: ContextPlus, *, alias: AliasConvertor):
args: List[str] = str(alias).split(" | ")
command = args[0]
custom = args[1]
user_aliases = await self._get_aliases(ctx)
if not user_aliases:
set_if_none(self.bot.config.Users, ctx.author.id, Config.User)
user_aliases = await self._get_aliases(ctx)
if custom in user_aliases.keys():
return await ctx.send(
_(
"The alias `{alias}` is already defined "
"for the command `{command}`",
ctx,
self.bot.config,
).format(alias=custom, command=user_aliases.get(custom))
)
user_aliases[custom] = command
await self._save_alias(ctx, user_aliases)
await ctx.send(
_(
"The alias `{alias}` for the command `{command}` "
"was successfully created",
ctx,
self.bot.config,
).format(alias=custom, command=command)
)

View file

@ -1,29 +0,0 @@
from discord.ext import commands
from jishaku.models import copy_context_with
def _(x):
return x
class AliasConvertor(commands.Converter):
async def convert(self, ctx, argument):
args = argument.split(" | ")
if len(args) <= 1:
raise commands.BadArgument(
_("Alias must be like `[command] | [alias]`")
)
command_ctx = await copy_context_with(
ctx, content=ctx.prefix + args[0]
)
alias_ctx = await copy_context_with(ctx, content=ctx.prefix + args[1])
if command_ctx.command is None:
raise commands.BadArgument(_("Unknown command"))
if args[0] != args[1] and alias_ctx.command is not None:
raise commands.BadArgument(_("Command already exists"))
return argument

View file

@ -1,51 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-19 14:39+0100\n"
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/Custom/custom.py:69
#, python-brace-format
msgid "Locale changed for you to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Custom/custom.py:76
msgid "List of available locales: "
msgstr ""
#: tuxbot/cogs/Custom/custom.py:95
#, python-brace-format
msgid "The alias `{alias}` is already defined for the command `{command}`"
msgstr ""
#: tuxbot/cogs/Custom/custom.py:123
#, python-brace-format
msgid "The alias `{alias}` for the command `{command}` was successfully created"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:14
msgid "Alias must be like `[command] | [alias]`"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:23
#, python-brace-format
msgid "Unknown command"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:26
#, python-brace-format
msgid "Command already exists"
msgstr ""

View file

@ -1,52 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-19 14:39+0100\n"
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/Custom/custom.py:69
#, python-brace-format
msgid "Locale changed for you to {lang} successfully"
msgstr "Langue changée pour vous en {lang} avec succès"
#: tuxbot/cogs/Custom/custom.py:76
msgid "List of available locales: "
msgstr "Liste des langues disponibles: "
#: tuxbot/cogs/Custom/custom.py:95
#, python-brace-format
msgid "The alias `{alias}` is already defined for the command `{command}`"
msgstr "L'alias `{alias}` est déjà défini pour la commande `{command}`"
#: tuxbot/cogs/Custom/custom.py:123
#, python-brace-format
msgid "The alias `{alias}` for the command `{command}` was successfully created"
msgstr "L'alias `{alias}` pour la commande `{command}` a été créé avec succès"
#: tuxbot/cogs/Custom/functions/converters.py:14
msgid "Alias must be like `[command] | [alias]`"
msgstr "L'alias doit être comme `[command] | [alias"
#: tuxbot/cogs/Custom/functions/converters.py:23
#, python-brace-format
msgid "Unknown command"
msgstr "Commande inconnue"
#: tuxbot/cogs/Custom/functions/converters.py:26
#, python-brace-format
msgid "Command already exists"
msgstr "La commande existe déjà"

View file

@ -1,49 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: tuxbot/cogs/Custom/custom.py:64
#, python-brace-format
msgid "Locale changed for you to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Custom/custom.py:71
msgid "List of available locales: "
msgstr ""
#: tuxbot/cogs/Custom/custom.py:94
#, python-brace-format
msgid "The alias `{alias}` is already defined for the command `{command}`"
msgstr ""
#: tuxbot/cogs/Custom/custom.py:107
#, python-brace-format
msgid "The alias `{alias}` for the command `{command}` was successfully created"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:15
msgid "Alias must be like `[command] | [alias]`"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:24
msgid "Unknown command"
msgstr ""
#: tuxbot/cogs/Custom/functions/converters.py:27
msgid "Command already exists"
msgstr ""

View file

@ -1 +0,0 @@
# pylint: disable=cyclic-import

View file

@ -1,20 +0,0 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .dev import Dev
from .config import DevConfig, HAS_MODELS
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
version_info = VersionInfo(major=0, minor=1, micro=0, release_level="alpha")
__version__ = "v{}.{}.{}-{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.release_level,
).replace("\n", "")
def setup(bot: Tux):
cog = Dev(bot)
bot.add_cog(cog)

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class DevConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -1,142 +0,0 @@
import logging
import random
import string
import discord
from discord.enums import ButtonStyle
from discord import ui, SelectOption
from discord.ext import commands
from tuxbot.cogs.Dev.functions.utils import TicTacToe
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
from tuxbot.core.utils import checks
from tuxbot.core.utils.functions.extra import command_extra, ContextPlus
log = logging.getLogger("tuxbot.cogs.Dev")
_ = Translator("Dev", __file__)
class Test(ui.View):
@ui.button(label="label1", disabled=True, style=ButtonStyle.grey)
async def label1(self, button, interaction):
print("label1")
print(type(button), button)
print(type(interaction), interaction)
@ui.button(label="label2", style=ButtonStyle.danger)
async def label2(self, button, interaction):
print("label2")
print(type(button), button)
print(type(interaction), interaction)
class Test2(ui.View):
@ui.select(
placeholder="placeholder",
min_values=1,
max_values=3,
options=[
SelectOption(
label="label1",
value="value1",
description="description1",
),
SelectOption(
label="label2",
value="value2",
description="description2",
),
SelectOption(
label="label3",
value="value3",
description="description3",
),
SelectOption(
label="label4",
value="value4",
description="description4",
),
],
)
async def select1(self, *args, **kwargs):
print("select1")
print(args)
print(kwargs)
class Dev(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
# =========================================================================
# =========================================================================
@command_extra(name="crash", deletable=True)
@checks.is_owner()
async def _crash(self, ctx: ContextPlus, crash_type: str):
if crash_type == "ZeroDivisionError":
await ctx.send(str(5 / 0))
elif crash_type == "TypeError":
# noinspection PyTypeChecker
await ctx.send(str(int([]))) # type: ignore
elif crash_type == "IndexError":
await ctx.send(str([0][5]))
# =========================================================================
@command_extra(name="test", deletable=True)
@checks.is_owner()
async def _test(self, ctx: ContextPlus):
button = ui.Button(
style=ButtonStyle.primary,
label="test",
)
button2 = ui.Button(
style=ButtonStyle.secondary,
label="test2",
)
button3 = ui.Button(
style=ButtonStyle.green,
label="test3",
)
button4 = ui.Button(
style=ButtonStyle.blurple,
label="test4",
)
button5 = ui.Button(
style=ButtonStyle.danger,
label="test5",
)
view = ui.View()
view.add_item(button)
view.add_item(button2)
view.add_item(button3)
view.add_item(button4)
view.add_item(button5)
await ctx.send("test", view=view)
# =========================================================================
@command_extra(name="test2", deletable=True)
@checks.is_owner()
async def _test2(self, ctx: ContextPlus):
await ctx.send(view=Test2())
# =========================================================================
@command_extra(name="test3", deletable=False)
async def _test3(self, ctx: ContextPlus, opponent: discord.Member):
game = await ctx.send(f"Turn: {ctx.author}")
game_id = "".join(random.choices(string.ascii_letters, k=10))
view = TicTacToe(ctx.message.author, opponent, game, game_id=game_id)
await game.edit(content=f"Turn: {ctx.author}", view=view)

View file

@ -1,161 +0,0 @@
from typing import List, Optional, Dict
import discord
from discord import ui
from discord.enums import ButtonStyle
class TicTacToe(ui.View):
turn: int = 0
grid: Dict[str, List[List[Optional[int]]]] = {}
win: bool = False
def __init__(self, player: discord.Member, opponent: discord.Member,
game: discord.Message, game_id: str):
super().__init__()
self.player = player
self.opponent = opponent
self.game = game
self.game_id = game_id
self.init_grid()
def init_grid(self):
self.grid[self.game_id]: List[List[Optional[int]]] = [
[None for _ in range(3)]
for _ in range(3)
]
def get_grid(self):
return self.grid[self.game_id]
def get_turn(self):
return self.player if self.turn == 0 else self.opponent
def get_emoji(self):
return "" if self.turn == 0 else ""
def check_win(self):
wins = [
[self.get_grid()[0][0], self.get_grid()[0][1], self.get_grid()[0][2]],
[self.get_grid()[1][0], self.get_grid()[1][1], self.get_grid()[1][2]],
[self.get_grid()[2][0], self.get_grid()[2][1], self.get_grid()[2][2]],
[self.get_grid()[0][0], self.get_grid()[1][0], self.get_grid()[2][0]],
[self.get_grid()[0][1], self.get_grid()[1][1], self.get_grid()[2][1]],
[self.get_grid()[0][2], self.get_grid()[1][2], self.get_grid()[2][2]],
[self.get_grid()[0][0], self.get_grid()[1][1], self.get_grid()[2][2]],
[self.get_grid()[2][0], self.get_grid()[1][1], self.get_grid()[0][2]],
]
return [self.turn, self.turn, self.turn] in wins
async def congrats(self):
self.win = True
del self.grid[self.game_id]
await self.game.edit(
content=f"{self.get_turn()} wins!",
view=self
)
def set_pos(self, i, j):
self.get_grid()[i][j] = self.turn
async def next_turn(self, i, j):
if self.win:
return
self.set_pos(i, j)
if self.check_win():
return await self.congrats()
self.turn = 1 if self.turn == 0 else 0
await self.game.edit(
content=f"Turn {self.get_turn()}",
view=self
)
# =========================================================================
# =========================================================================
@ui.button(label="", style=ButtonStyle.grey, group=1)
async def button_1(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(0, 0)
@ui.button(label="", style=ButtonStyle.grey, group=1)
async def button_2(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(0, 1)
@ui.button(label="", style=ButtonStyle.grey, group=1)
async def button_3(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(0, 2)
# =========================================================================
# =========================================================================
@ui.button(label="", style=ButtonStyle.grey, group=2)
async def button_4(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(1, 0)
@ui.button(label="", style=ButtonStyle.grey, group=2)
async def button_5(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(1, 1)
@ui.button(label="", style=ButtonStyle.grey, group=2)
async def button_6(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(1, 2)
# =========================================================================
# =========================================================================
@ui.button(label="", style=ButtonStyle.grey, group=3)
async def button_7(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(2, 0)
@ui.button(label="", style=ButtonStyle.grey, group=3)
async def button_8(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(2, 1)
@ui.button(label="", style=ButtonStyle.grey, group=3)
async def button_9(self, button: ui.Button,
interaction: discord.Interaction):
if button.label == "" and interaction.user == self.get_turn():
button.label = ""
button.emoji = self.get_emoji()
await self.next_turn(2, 2)

View file

@ -1,18 +0,0 @@
# English translations for Tuxbot-bot package.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

View file

@ -1,19 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

View file

@ -1,18 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

View file

@ -1,19 +0,0 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .linux import Linux
from .config import LinuxConfig, HAS_MODELS
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
__version__ = "v{}.{}.{}-{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.release_level,
).replace("\n", "")
def setup(bot: Tux):
bot.add_cog(Linux(bot))

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class LinuxConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -1,77 +0,0 @@
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from tuxbot.cogs.Linux.functions.exceptions import CNFException
def _(x):
return x
class CNF:
_url = "https://command-not-found.com/{}"
_content: BeautifulSoup
command: str
description: str = ""
meta: dict = {}
distro: dict = {}
def __init__(self, command: str):
self.command = command
# =========================================================================
# =========================================================================
async def fetch(self):
try:
async with aiohttp.ClientSession() as cs:
async with cs.get(self._url.format(self.command)) as s:
if s.status == 200:
self._content = BeautifulSoup(
await s.text(), "html.parser"
)
return self.parse()
except (aiohttp.ClientError, asyncio.exceptions.TimeoutError):
pass
raise CNFException(_("Something went wrong ..."))
def parse(self):
info = self._content.find("div", class_="row-command-info")
distro = self._content.find_all("div", class_="command-install")
try:
self.description = info.find("p", class_="my-0").text.strip()
except AttributeError:
self.description = "N/A"
try:
for m in info.find("ul", class_="list-group").find_all("li"):
row = m.text.strip().split("\n")
self.meta[row[0].lower()[:-1]] = row[1]
except AttributeError:
self.meta = {}
try:
del distro[0] # unused row
for d in distro:
self.distro[
d.find("dt").text.strip().split("\n")[-1].strip()
] = d.find("code").text
except (AttributeError, IndexError):
self.distro = {}
def to_dict(self):
return {
"command": self.command,
"description": self.description,
"meta": self.meta,
"distro": self.distro,
}

View file

@ -1,9 +0,0 @@
from discord.ext import commands
class LinuxException(commands.BadArgument):
pass
class CNFException(LinuxException):
pass

View file

@ -1,17 +0,0 @@
from aiocache import cached, Cache
from aiocache.serializers import PickleSerializer
from tuxbot.cogs.Linux.functions.cnf import CNF
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="linux",
)
async def get_from_cnf(command: str) -> dict:
cnf = CNF(command)
await cnf.fetch()
return cnf.to_dict()

View file

@ -1,56 +0,0 @@
import logging
import discord
from discord.ext import commands
from tuxbot.cogs.Linux.functions.utils import get_from_cnf
from tuxbot.core.utils.functions.extra import command_extra, ContextPlus
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
log = logging.getLogger("tuxbot.cogs.Linux")
_ = Translator("Linux", __file__)
class Linux(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
async def cog_before_invoke(self, ctx: ContextPlus):
await ctx.trigger_typing()
# =========================================================================
# =========================================================================
@command_extra(name="cnf")
async def _cnf(self, ctx: ContextPlus, command: str):
cnf = await get_from_cnf(command)
if cnf["distro"]:
e = discord.Embed(title=f"{cnf['description']} ({cnf['command']})")
description = (
"__Maintainer:__ {maintainer}\n"
"__Homepage:__ [{homepage}]({homepage})\n"
"__Section:__ {section}".format(
maintainer=cnf["meta"].get("maintainer", "N/A"),
homepage=cnf["meta"].get("homepage", "N/A"),
section=cnf["meta"].get("section", "N/A"),
)
)
e.description = description
e.set_footer(
text="Powered by https://command-not-found.com/ "
"and with his authorization"
)
for k, v in cnf["distro"].items():
e.add_field(name=f"**__{k}__**", value=f"```{v}```")
return await ctx.send(embed=e)
await ctx.send(_("No result found", ctx, self.bot.config))

View file

@ -1,18 +0,0 @@
# English translations for Tuxbot-bot package.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-25 14:36+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

View file

@ -1,19 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-25 14:36+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

View file

@ -1,26 +0,0 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: tuxbot/cogs/Linux/functions/cnf.py:42
msgid "Something went wrong ..."
msgstr ""
#: tuxbot/cogs/Linux/linux.py:56
msgid "No result found"
msgstr ""

View file

@ -1,26 +0,0 @@
import logging
from collections import namedtuple
from discord.ext import commands
from tuxbot.core.bot import Tux
from .logs import Logs, GatewayHandler
from .config import LogsConfig, HAS_MODELS
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
__version__ = "v{}.{}.{}-{}".format(
version_info.major,
version_info.minor,
version_info.micro,
version_info.release_level,
).replace("\n", "")
def setup(bot: Tux):
cog = Logs(bot)
bot.add_cog(cog)
handler = GatewayHandler(cog)
logging.getLogger().addHandler(handler)

View file

@ -1,45 +0,0 @@
from typing import Dict
from structured_config import Structure, StrField
HAS_MODELS = False
class LogsConfig(Structure):
dm: str = StrField("")
mentions: str = StrField("")
guilds: str = StrField("")
errors: str = StrField("")
gateway: str = StrField("")
sentryKey: str = StrField("")
extra: Dict[str, Dict] = {
"dm": {
"type": str,
"description": "URL of the webhook used for send DMs "
"received and sent by the bot",
},
"mentions": {
"type": str,
"description": "URL of the webhook used for send Mentions "
"received by the bot",
},
"guilds": {
"type": str,
"description": "URL of the webhook used for send guilds where the "
"bot is added or removed",
},
"errors": {
"type": str,
"description": "URL of the webhook used for send errors in the bot",
},
"gateway": {
"type": str,
"description": "URL of the webhook used for send gateway information",
},
"sentryKey": {
"type": str,
"description": "Sentry KEY for error logging (https://sentry.io/)",
},
}

View file

@ -1,27 +0,0 @@
from collections import Counter
from typing import Dict
def sort_by(_events: Counter) -> Dict[str, dict]:
majors = (
"guild",
"channel",
"message",
"invite",
"integration",
"presence",
"voice",
"other",
)
sorted_events: Dict[str, Dict] = {m: {} for m in majors}
for event, count in _events:
done = False
for m in majors:
if event.lower().startswith(m):
sorted_events[m][event] = count
done = True
if not done:
sorted_events["other"][event] = count
return sorted_events

View file

@ -1,26 +0,0 @@
# English translations for Tuxbot-bot package.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2021-01-26 15:18+0100\n"
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tuxbot/cogs/Logs/logs.py:295
msgid "Sockets stats"
msgstr ""
#: tuxbot/cogs/Logs/logs.py:297
msgid "{} socket events observed ({:.2f}/minute):"
msgstr ""

View file

@ -1,27 +0,0 @@
# French translations for Tuxbot-bot package
# Traductions françaises du paquet Tuxbot-bot.
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
# This file is distributed under the same license as the Tuxbot-bot package.
# Automatically generated, 2020.
#
msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/Logs/logs.py:295
msgid "Sockets stats"
msgstr "Statistiques des évenements"
#: tuxbot/cogs/Logs/logs.py:297
msgid "{} socket events observed ({:.2f}/minute):"
msgstr "{} evenements ont été observés ({:.2f}/minute)"

Some files were not shown because too many files have changed in this diff Show more