Compare commits
No commits in common. "master" and "rewrite" have entirely different histories.
225 changed files with 4786 additions and 9463 deletions
|
@ -1,8 +0,0 @@
|
||||||
version = 1
|
|
||||||
|
|
||||||
[[analyzers]]
|
|
||||||
name = "python"
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[analyzers.meta]
|
|
||||||
runtime_version = "3.x.x"
|
|
|
@ -1,7 +0,0 @@
|
||||||
# PostgreSQL
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
POSTGRES_HOST=postgres
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_DB=tuxbot_bot
|
|
||||||
POSTGRES_USER=debug
|
|
||||||
POSTGRES_PASSWORD=debug
|
|
|
@ -1,4 +0,0 @@
|
||||||
# General
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
USE_DOCKER=yes
|
|
||||||
IPYTHONDIR=/app/.ipython
|
|
5
.github/issue_template.md
vendored
5
.github/issue_template.md
vendored
|
@ -1,6 +1,9 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
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]
|
- Python Version [e.g. 3.7.4]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
<-- Add any other context about the problem here. -->
|
Add any other context about the problem here.
|
158
.gitignore
vendored
158
.gitignore
vendored
|
@ -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__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
# C extensions
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
*.so
|
||||||
|
|
||||||
# User-specific stuff
|
# Distribution / packaging
|
||||||
.idea/**/workspace.xml
|
.Python
|
||||||
.idea/**/tasks.xml
|
build/
|
||||||
.idea/**/usage.statistics.xml
|
develop-eggs/
|
||||||
.idea/**/dictionaries
|
dist/
|
||||||
.idea/**/shelf
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
# Generated files
|
# PyInstaller
|
||||||
.idea/**/contentModel.xml
|
# 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
|
# Installer logs
|
||||||
.idea/**/dataSources/
|
pip-log.txt
|
||||||
.idea/**/dataSources.ids
|
pip-delete-this-directory.txt
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
|
|
||||||
|
# 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
|
*.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
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
venv
|
# Celery stuff
|
||||||
venv3.8
|
celerybeat-schedule
|
||||||
venv3.9
|
celerybeat.pid
|
||||||
venv3.11
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
.ipython/
|
# Environments
|
||||||
.env
|
.env
|
||||||
.envs/*
|
.venv
|
||||||
!.envs/.local/
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
data/settings/
|
# Rope project settings
|
||||||
dump.rdb
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
<component name="ProjectCodeStyleConfiguration">
|
|
||||||
<state>
|
|
||||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
|
||||||
</state>
|
|
||||||
</component>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -1,3 +0,0 @@
|
||||||
[mypy]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
exclude = venv
|
|
22
.pylintrc
22
.pylintrc
|
@ -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)
|
|
86
Makefile
86
Makefile
|
@ -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
86
README.md
Normal 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?)
|
141
README.rst
141
README.rst
|
@ -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
250
bot.py
Executable 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
56
cogs/API.py
Normal 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
534
cogs/Admin.py
Normal 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
227
cogs/Help.py
Normal 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
304
cogs/Logs.py
Normal 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
110
cogs/Monitoring.py
Normal 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
222
cogs/Poll.py
Normal 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
410
cogs/Useful.py
Normal 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
64
cogs/User.py
Normal 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))
|
|
@ -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"]
|
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
|
@ -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
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
BACKUP_DIR_PATH='/backups'
|
|
||||||
BACKUP_FILE_PREFIX='backup'
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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: ${@}"
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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}'."
|
|
|
@ -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}"
|
|
|
@ -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."
|
|
|
@ -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"]
|
|
|
@ -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 "$@"
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
tuxbot dev
|
|
24
configs/config.cfg.example
Normal file
24
configs/config.cfg.example
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[bot]
|
||||||
|
Token =
|
||||||
|
Tester =
|
||||||
|
Activity =
|
||||||
|
|
||||||
|
[postgresql]
|
||||||
|
Username =
|
||||||
|
Password =
|
||||||
|
Host =
|
||||||
|
DBName =
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
Owners =
|
||||||
|
|
||||||
|
[webhook]
|
||||||
|
ID =
|
||||||
|
Token =
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
Separator =
|
||||||
|
|
||||||
|
[API]
|
||||||
|
Host =
|
||||||
|
Port =
|
18
configs/fallbacks.cfg.example
Normal file
18
configs/fallbacks.cfg.example
Normal 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
18
configs/prefixes.cfg
Normal 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
19
database.py
Normal 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!")
|
|
@ -1,3 +0,0 @@
|
||||||
pylint>=2.6.0
|
|
||||||
black>=20.8b1
|
|
||||||
mypy>=0.812
|
|
17
generate_locales.sh
Executable file
17
generate_locales.sh
Executable 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
|
34
local.yml
34
local.yml
|
@ -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
|
|
|
@ -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
13
requirements.txt
Normal 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
|
51
setup.cfg
51
setup.cfg
|
@ -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/**/*
|
|
5
setup.py
5
setup.py
|
@ -1,5 +0,0 @@
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
python_requires=">=3.8",
|
|
||||||
)
|
|
2
todo
Normal file
2
todo
Normal 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
|
|
@ -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
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -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))
|
|
|
@ -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)
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class AdminConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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))
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -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)
|
|
||||||
)
|
|
|
@ -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
|
|
|
@ -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 ""
|
|
|
@ -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à"
|
|
|
@ -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 ""
|
|
|
@ -1 +0,0 @@
|
||||||
# pylint: disable=cyclic-import
|
|
|
@ -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)
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class DevConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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))
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxException(commands.BadArgument):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CNFException(LinuxException):
|
|
||||||
pass
|
|
|
@ -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()
|
|
|
@ -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))
|
|
|
@ -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"
|
|
|
@ -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"
|
|
|
@ -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 ""
|
|
|
@ -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)
|
|
|
@ -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/)",
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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 ""
|
|
|
@ -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
Loading…
Reference in a new issue