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