Compare commits
215 commits
Author | SHA1 | Date | |
---|---|---|---|
616a067bc2 | |||
3240c61b20 | |||
f06bfd7e24 | |||
7b50af0207 | |||
2978706264 | |||
ba53228d44 | |||
b75e5b8a8e | |||
3d8ea556d5 | |||
82b8fb9814 | |||
fd0600b75d | |||
09e69166ad | |||
f8f56add97 | |||
06bcae81fe | |||
614434aebf | |||
af3d742f68 | |||
4c72f07e8e | |||
5afadb0f25 | |||
067e29a96a | |||
8f62c2c4a1 | |||
ad443c9c48 | |||
4e3fbd7f4d | |||
4678be191d | |||
f00ff8d345 | |||
ba21cf859b | |||
88f60690dd | |||
fcc23d87df | |||
56e45b52b5 | |||
96cfa17d2e | |||
3c5741e6c5 | |||
1693857864 | |||
9362558a2e | |||
f7f5232e21 | |||
32b6de0d0f | |||
2a00d93023 | |||
b9f6c6cb0a | |||
5b7c905ac8 | |||
2e7934148e | |||
2afd3af540 | |||
c6c61a0886 | |||
c6a5dc4ad6 | |||
1f367fd2df | |||
9172331927 | |||
561f56ca27 | |||
eca6e7b268 | |||
4a508b1851 | |||
e63e939d77 | |||
751c82909d | |||
22c5ee57d4 | |||
64fba7fec6 | |||
7f9c202cc6 | |||
1b7f153ec8 | |||
0eca877c1c | |||
540dfd616a | |||
1a10f64345 | |||
f0dc682047 | |||
3525b9aa4b | |||
0ecc97518f | |||
78a5ac9939 | |||
34e32fdf68 | |||
edfeadb872 | |||
83723380e9 | |||
b5ca338d6c | |||
c7ddba1bae | |||
7423b40337 | |||
fa98d67276 | |||
a0e67c1627 | |||
1e86abdf01 | |||
c566f775cd | |||
fae56745bf | |||
f7176d917c | |||
434021ecb9 | |||
0c308727d2 | |||
5991ebfaf2 | |||
45d4aa1dc5 | |||
0687ee3f06 | |||
287e4c1743 | |||
|
ce2b59b8d5 | ||
|
ae2538f99c | ||
da277e0d66 | |||
aeced979df | |||
bb87d77e33 | |||
975a3b3d14 | |||
33e09a9e02 | |||
c5c13506d7 | |||
647cc4bd64 | |||
554c0b52d5 | |||
1d37dc1961 | |||
f88adec45b | |||
dd09a53c0e | |||
d66bec65ae | |||
fbb61c247d | |||
d3ab384de0 | |||
74307c755c | |||
18310f17a0 | |||
7962205d16 | |||
fa3069244d | |||
1fb3e035bd | |||
37bbf0368e | |||
01e0e5e27e | |||
5d585bf218 | |||
30cc3ecad2 | |||
c3660aab8a | |||
0eaa53ffd5 | |||
f00f0fd4c0 | |||
1681c5abf5 | |||
6757ce2ccc | |||
573ce3fb18 | |||
72fabf89b9 | |||
98b82e680e | |||
e5c3f1b8de | |||
e537a59d8d | |||
834f071332 | |||
cfd59def74 | |||
b4194dcadf | |||
42d7cad0e5 | |||
5a65fe1a6c | |||
3587a8f8a4 | |||
71576f48e4 | |||
d6e9cd6512 | |||
7d588b2dbc | |||
e38823e5be | |||
71335de878 | |||
bdd77d1841 | |||
d7a2330fb6 | |||
969ff8c351 | |||
4751a1b518 | |||
008ae76aca | |||
bef9060b78 | |||
533ca6e3e7 | |||
42e2d04a9e | |||
7d67b8d581 | |||
6a926d717c | |||
bdc7afb1ef | |||
|
260ef9f41c | ||
10b7e4039c | |||
179c84b45a | |||
cc5df29e71 | |||
421ecbf6cb | |||
554ec46413 | |||
888a7924be | |||
1be4af8405 | |||
3ca1a42cad | |||
cebb1b0123 | |||
e0788137ff | |||
d68d54be44 | |||
331599eb38 | |||
9a0786af7c | |||
c1e253689d | |||
|
951784718b | ||
783f7507f0 | |||
4b28ff0aea | |||
f155c7c27e | |||
fa1ac02648 | |||
5c51c15805 | |||
fbda0e9414 | |||
6b433970fe | |||
032a49b08f | |||
f181f58735 | |||
f7bfc25793 | |||
3dd17f44a1 | |||
7e79ac7fab | |||
3e3f6d42d6 | |||
1e6f0b6eb0 | |||
1f7da4fd14 | |||
db7dfd5c58 | |||
f79074a97d | |||
6400d1da71 | |||
bb6b25c5d9 | |||
d3683ed10d | |||
5cc364480a | |||
17b3e658fc | |||
175174757b | |||
ecdde52ca3 | |||
1f88499d44 | |||
5482429cba | |||
85506c8db4 | |||
a42eb58be8 | |||
4efb707257 | |||
8e8a4b899e | |||
85da8a34ab | |||
a73d408462 | |||
5e8868b660 | |||
9869312ee8 | |||
cdb891d435 | |||
bf6d961658 | |||
dbf7f3ce8e | |||
14f995550a | |||
fbafd03ea9 | |||
7c75b0efad | |||
815709d68b | |||
b5b7f0c7ef | |||
ec68280519 | |||
50562059f9 | |||
33fa6b7f1f | |||
335397554f | |||
cbe250f137 | |||
79ca4f95d6 | |||
9020fe7201 | |||
9f8765e0a6 | |||
078dc075f2 | |||
28d1d71c5a | |||
2e76379c87 | |||
45d61fc71d | |||
f9c31f4017 | |||
04645ec639 | |||
534a78e447 | |||
083d14e056 | |||
daed469994 | |||
68ca0cb2fc | |||
4d479f6516 | |||
|
afe76d00c1 | ||
fc06756363 | |||
425ff79c8d | |||
57ea780f2c | |||
78026ee88e |
225 changed files with 9463 additions and 4786 deletions
8
.deepsource.toml
Normal file
8
.deepsource.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "python"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
runtime_version = "3.x.x"
|
7
.envs/.local/.postgres
Normal file
7
.envs/.local/.postgres
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# PostgreSQL
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_DB=tuxbot_bot
|
||||||
|
POSTGRES_USER=debug
|
||||||
|
POSTGRES_PASSWORD=debug
|
4
.envs/.local/.tuxbot
Normal file
4
.envs/.local/.tuxbot
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# General
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
USE_DOCKER=yes
|
||||||
|
IPYTHONDIR=/app/.ipython
|
5
.github/issue_template.md
vendored
5
.github/issue_template.md
vendored
|
@ -1,9 +1,6 @@
|
||||||
---
|
---
|
||||||
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: ''
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -28,4 +25,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,146 +1,52 @@
|
||||||
#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
|
||||||
|
|
||||||
# C extensions
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
*.so
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
# Distribution / packaging
|
# User-specific stuff
|
||||||
.Python
|
.idea/**/workspace.xml
|
||||||
build/
|
.idea/**/tasks.xml
|
||||||
develop-eggs/
|
.idea/**/usage.statistics.xml
|
||||||
dist/
|
.idea/**/dictionaries
|
||||||
downloads/
|
.idea/**/shelf
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
# Generated files
|
||||||
# Usually these files are written by a python script from a template
|
.idea/**/contentModel.xml
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
# Sensitive or high-churn files
|
||||||
pip-log.txt
|
.idea/**/dataSources/
|
||||||
pip-delete-this-directory.txt
|
.idea/**/dataSources.ids
|
||||||
|
.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/
|
|
||||||
|
|
||||||
# Translations
|
.idea/sonarlint/
|
||||||
*.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__/
|
||||||
|
|
||||||
# Celery stuff
|
venv
|
||||||
celerybeat-schedule
|
venv3.8
|
||||||
celerybeat.pid
|
venv3.9
|
||||||
|
venv3.11
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
.ipython/
|
||||||
.env
|
.env
|
||||||
.venv
|
.envs/*
|
||||||
env/
|
!.envs/.local/
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
data/settings/
|
||||||
.ropeproject
|
dump.rdb
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
71
.idea/dictionaries/romain.xml
Normal file
71
.idea/dictionaries/romain.xml
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<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>
|
9
.idea/discord.xml
Normal file
9
.idea/discord.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?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>
|
15
.idea/inspectionProfiles/Project_Default.xml
Normal file
15
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
7
.idea/misc.xml
Normal file
7
.idea/misc.xml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?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>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?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>
|
11
.idea/statistic.xml
Normal file
11
.idea/statistic.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?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>
|
21
.idea/tuxbot_bot.iml
Normal file
21
.idea/tuxbot_bot.iml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?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>
|
11
.idea/vcs.xml
Normal file
11
.idea/vcs.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?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>
|
14
.idea/webResources.xml
Normal file
14
.idea/webResources.xml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<?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>
|
3
.mypy.ini
Normal file
3
.mypy.ini
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[mypy]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
exclude = venv
|
22
.pylintrc
Normal file
22
.pylintrc
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[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
Normal file
86
Makefile
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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
86
README.md
|
@ -1,86 +0,0 @@
|
||||||
# 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
Normal file
141
README.rst
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
|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
250
bot.py
|
@ -1,250 +0,0 @@
|
||||||
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
56
cogs/API.py
|
@ -1,56 +0,0 @@
|
||||||
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
534
cogs/Admin.py
|
@ -1,534 +0,0 @@
|
||||||
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
227
cogs/Help.py
|
@ -1,227 +0,0 @@
|
||||||
# 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
304
cogs/Logs.py
|
@ -1,304 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,110 +0,0 @@
|
||||||
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
222
cogs/Poll.py
|
@ -1,222 +0,0 @@
|
||||||
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
410
cogs/Useful.py
|
@ -1,410 +0,0 @@
|
||||||
# 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
64
cogs/User.py
|
@ -1,64 +0,0 @@
|
||||||
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))
|
|
38
compose/local/tuxbot/Dockerfile
Normal file
38
compose/local/tuxbot/Dockerfile
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
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"]
|
6
compose/local/tuxbot/start
Normal file
6
compose/local/tuxbot/start
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
set -o nounset
|
||||||
|
|
6
compose/production/postgres/Dockerfile
Normal file
6
compose/production/postgres/Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
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
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
|
||||||
|
BACKUP_DIR_PATH='/backups'
|
||||||
|
BACKUP_FILE_PREFIX='backup'
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/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
|
||||||
|
}
|
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal file
41
compose/production/postgres/maintenance/_sourced/messages.sh
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#!/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: ${@}"
|
||||||
|
}
|
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal file
16
compose/production/postgres/maintenance/_sourced/yes_no.sh
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/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
|
||||||
|
}
|
38
compose/production/postgres/maintenance/backup
Normal file
38
compose/production/postgres/maintenance/backup
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#!/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}'."
|
22
compose/production/postgres/maintenance/backups
Normal file
22
compose/production/postgres/maintenance/backups
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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}"
|
55
compose/production/postgres/maintenance/restore
Normal file
55
compose/production/postgres/maintenance/restore
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#!/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."
|
40
compose/production/tuxbot/Dockerfile
Normal file
40
compose/production/tuxbot/Dockerfile
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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"]
|
43
compose/production/tuxbot/entrypoint
Normal file
43
compose/production/tuxbot/entrypoint
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/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 "$@"
|
8
compose/production/tuxbot/start
Normal file
8
compose/production/tuxbot/start
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
set -o nounset
|
||||||
|
|
||||||
|
|
||||||
|
tuxbot dev
|
|
@ -1,24 +0,0 @@
|
||||||
[bot]
|
|
||||||
Token =
|
|
||||||
Tester =
|
|
||||||
Activity =
|
|
||||||
|
|
||||||
[postgresql]
|
|
||||||
Username =
|
|
||||||
Password =
|
|
||||||
Host =
|
|
||||||
DBName =
|
|
||||||
|
|
||||||
[permissions]
|
|
||||||
Owners =
|
|
||||||
|
|
||||||
[webhook]
|
|
||||||
ID =
|
|
||||||
Token =
|
|
||||||
|
|
||||||
[misc]
|
|
||||||
Separator =
|
|
||||||
|
|
||||||
[API]
|
|
||||||
Host =
|
|
||||||
Port =
|
|
|
@ -1,18 +0,0 @@
|
||||||
[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
|
|
|
@ -1,18 +0,0 @@
|
||||||
[280805240977227776]
|
|
||||||
prefixes = b1.[301062143942590465]*
|
|
||||||
|
|
||||||
[303633056944881686]
|
|
||||||
prefixes = b1.[301062143942590465]*
|
|
||||||
|
|
||||||
[373881878471770112]
|
|
||||||
prefixes = b1.
|
|
||||||
|
|
||||||
[336642139381301249]
|
|
||||||
prefixes = ba.
|
|
||||||
|
|
||||||
[274247231534792704]
|
|
||||||
prefixes = test.
|
|
||||||
|
|
||||||
[528679953399676938]
|
|
||||||
prefixes = test.
|
|
||||||
|
|
19
database.py
19
database.py
|
@ -1,19 +0,0 @@
|
||||||
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!")
|
|
3
dev.requirements.txt
Normal file
3
dev.requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pylint>=2.6.0
|
||||||
|
black>=20.8b1
|
||||||
|
mypy>=0.812
|
|
@ -1,17 +0,0 @@
|
||||||
#!/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
Normal file
34
local.yml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
30
production.yml
Normal file
30
production.yml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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
|
|
@ -1,13 +0,0 @@
|
||||||
requests
|
|
||||||
humanize
|
|
||||||
git+https://github.com/Rapptz/discord.py@master
|
|
||||||
jishaku
|
|
||||||
gitpython
|
|
||||||
orm
|
|
||||||
asyncpg
|
|
||||||
psycopg2
|
|
||||||
configparser
|
|
||||||
psutil
|
|
||||||
tcp_latency
|
|
||||||
yarl
|
|
||||||
pillow
|
|
51
setup.cfg
Normal file
51
setup.cfg
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
[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
Normal file
5
setup.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
python_requires=">=3.8",
|
||||||
|
)
|
2
todo
2
todo
|
@ -1,2 +0,0 @@
|
||||||
reconnaissance d'image
|
|
||||||
commande d'archivage pour les salons vocaux avec output mp4 dans lequel on voit le pseudo de celui qui parle
|
|
26
tuxbot/__init__.py
Normal file
26
tuxbot/__init__.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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
|
28
tuxbot/__main__.py
Normal file
28
tuxbot/__main__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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
|
||||||
|
)
|
243
tuxbot/__run__.py
Normal file
243
tuxbot/__run__.py
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
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)
|
19
tuxbot/cogs/Admin/__init__.py
Normal file
19
tuxbot/cogs/Admin/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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))
|
57
tuxbot/cogs/Admin/admin.py
Normal file
57
tuxbot/cogs/Admin/admin.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
12
tuxbot/cogs/Admin/config.py
Normal file
12
tuxbot/cogs/Admin/config.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from structured_config import Structure
|
||||||
|
|
||||||
|
HAS_MODELS = False
|
||||||
|
|
||||||
|
|
||||||
|
class AdminConfig(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
extra: Dict[str, Dict] = {}
|
18
tuxbot/cogs/Admin/locales/en-US.po
Normal file
18
tuxbot/cogs/Admin/locales/en-US.po
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
19
tuxbot/cogs/Admin/locales/fr-FR.po
Normal file
19
tuxbot/cogs/Admin/locales/fr-FR.po
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# 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"
|
18
tuxbot/cogs/Admin/locales/messages.pot
Normal file
18
tuxbot/cogs/Admin/locales/messages.pot
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
19
tuxbot/cogs/Custom/__init__.py
Normal file
19
tuxbot/cogs/Custom/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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))
|
12
tuxbot/cogs/Custom/config.py
Normal file
12
tuxbot/cogs/Custom/config.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from structured_config import Structure
|
||||||
|
|
||||||
|
HAS_MODELS = False
|
||||||
|
|
||||||
|
|
||||||
|
class CustomConfig(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
extra: Dict[str, Dict] = {}
|
112
tuxbot/cogs/Custom/custom.py
Normal file
112
tuxbot/cogs/Custom/custom.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
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)
|
||||||
|
)
|
29
tuxbot/cogs/Custom/functions/converters.py
Normal file
29
tuxbot/cogs/Custom/functions/converters.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
51
tuxbot/cogs/Custom/locales/en-US.po
Normal file
51
tuxbot/cogs/Custom/locales/en-US.po
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# 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 ""
|
52
tuxbot/cogs/Custom/locales/fr-FR.po
Normal file
52
tuxbot/cogs/Custom/locales/fr-FR.po
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# 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à"
|
49
tuxbot/cogs/Custom/locales/messages.pot
Normal file
49
tuxbot/cogs/Custom/locales/messages.pot
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
# 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
tuxbot/cogs/Custom/models/__init__.py
Normal file
1
tuxbot/cogs/Custom/models/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# pylint: disable=cyclic-import
|
20
tuxbot/cogs/Dev/__init__.py
Normal file
20
tuxbot/cogs/Dev/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
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)
|
12
tuxbot/cogs/Dev/config.py
Normal file
12
tuxbot/cogs/Dev/config.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from structured_config import Structure
|
||||||
|
|
||||||
|
HAS_MODELS = False
|
||||||
|
|
||||||
|
|
||||||
|
class DevConfig(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
extra: Dict[str, Dict] = {}
|
142
tuxbot/cogs/Dev/dev.py
Normal file
142
tuxbot/cogs/Dev/dev.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
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)
|
0
tuxbot/cogs/Dev/functions/__init__.py
Normal file
0
tuxbot/cogs/Dev/functions/__init__.py
Normal file
161
tuxbot/cogs/Dev/functions/utils.py
Normal file
161
tuxbot/cogs/Dev/functions/utils.py
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
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)
|
18
tuxbot/cogs/Dev/locales/en-US.po
Normal file
18
tuxbot/cogs/Dev/locales/en-US.po
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
19
tuxbot/cogs/Dev/locales/fr-FR.po
Normal file
19
tuxbot/cogs/Dev/locales/fr-FR.po
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# 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"
|
18
tuxbot/cogs/Dev/locales/messages.pot
Normal file
18
tuxbot/cogs/Dev/locales/messages.pot
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
0
tuxbot/cogs/Dev/models/__init__.py
Normal file
0
tuxbot/cogs/Dev/models/__init__.py
Normal file
19
tuxbot/cogs/Linux/__init__.py
Normal file
19
tuxbot/cogs/Linux/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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))
|
12
tuxbot/cogs/Linux/config.py
Normal file
12
tuxbot/cogs/Linux/config.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from structured_config import Structure
|
||||||
|
|
||||||
|
HAS_MODELS = False
|
||||||
|
|
||||||
|
|
||||||
|
class LinuxConfig(Structure):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
extra: Dict[str, Dict] = {}
|
0
tuxbot/cogs/Linux/functions/__init__.py
Normal file
0
tuxbot/cogs/Linux/functions/__init__.py
Normal file
77
tuxbot/cogs/Linux/functions/cnf.py
Normal file
77
tuxbot/cogs/Linux/functions/cnf.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
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,
|
||||||
|
}
|
9
tuxbot/cogs/Linux/functions/exceptions.py
Normal file
9
tuxbot/cogs/Linux/functions/exceptions.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
class LinuxException(commands.BadArgument):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CNFException(LinuxException):
|
||||||
|
pass
|
17
tuxbot/cogs/Linux/functions/utils.py
Normal file
17
tuxbot/cogs/Linux/functions/utils.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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()
|
56
tuxbot/cogs/Linux/linux.py
Normal file
56
tuxbot/cogs/Linux/linux.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
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))
|
18
tuxbot/cogs/Linux/locales/en-US.po
Normal file
18
tuxbot/cogs/Linux/locales/en-US.po
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# 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"
|
19
tuxbot/cogs/Linux/locales/fr-FR.po
Normal file
19
tuxbot/cogs/Linux/locales/fr-FR.po
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# 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"
|
26
tuxbot/cogs/Linux/locales/messages.pot
Normal file
26
tuxbot/cogs/Linux/locales/messages.pot
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# 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 ""
|
0
tuxbot/cogs/Linux/models/__init__.py
Normal file
0
tuxbot/cogs/Linux/models/__init__.py
Normal file
26
tuxbot/cogs/Logs/__init__.py
Normal file
26
tuxbot/cogs/Logs/__init__.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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)
|
45
tuxbot/cogs/Logs/config.py
Normal file
45
tuxbot/cogs/Logs/config.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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/)",
|
||||||
|
},
|
||||||
|
}
|
0
tuxbot/cogs/Logs/functions/__init__.py
Normal file
0
tuxbot/cogs/Logs/functions/__init__.py
Normal file
27
tuxbot/cogs/Logs/functions/utils.py
Normal file
27
tuxbot/cogs/Logs/functions/utils.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue