Compare commits

...

170 commits
v3 ... master

Author SHA1 Message Date
Romain J 616a067bc2 feat(commands:misc|Dev): add random tests things 2021-05-29 02:19:50 +02:00
Romain J 3240c61b20 update(commands:info|Utils): update git url 2021-05-17 00:27:21 +02:00
Romain J f06bfd7e24 update(commands:mute|Mod): add reason as optional 2021-05-17 00:20:56 +02:00
Romain J 7b50af0207 feat(commands:mute|Mod): add mute/unmute command 2021-05-17 00:08:16 +02:00
Romain J 2978706264 fix(doc): update version 2021-05-16 23:59:16 +02:00
Romain J ba53228d44 update(core): change to >=3.8 2021-05-16 23:21:27 +02:00
Romain J b75e5b8a8e update(commands:iplocalise|Network): update fail image 2021-05-16 18:09:51 +02:00
Romain J 3d8ea556d5 fix(commands:iplocalise|Network): fix map timeout 2021-05-16 18:03:33 +02:00
Romain J 82b8fb9814 feat(commands:update|Admin): add update command 2021-05-16 17:40:42 +02:00
Romain J fd0600b75d fix(commands:iplocalise|Network): close TUXBOT-BOT-4N 2021-05-16 17:28:49 +02:00
Romain J 09e69166ad fix(commands:iplocalise|Network): close TUXBOT-BOT-4T, close TUXBOT-BOT-4S 2021-05-16 17:07:26 +02:00
Romain J f8f56add97 fix(commands:iplocalise|Network): possible fix for timeout on map? 2021-05-16 15:46:52 +02:00
Romain J 06bcae81fe feat(commands:tag>all|Tag): feat way to see all tag for a guild
fix(core): close TUXBOT-BOT-4X
2021-05-16 15:27:27 +02:00
Romain J 614434aebf feat(commands:*|Tag): feat tag system 2021-05-16 00:32:14 +02:00
Romain J af3d742f68 fix typo 2021-05-15 21:44:46 +02:00
Romain J 4c72f07e8e feat(commands:rule>update|Mod): feat way to update rule messages 2021-05-15 21:36:30 +02:00
Romain J 5afadb0f25 fix(commands:rule>list|Mod): fix order
fix(commands:rule|Mod): fix when <prefix>rule <non digit>
2021-05-14 17:35:50 +02:00
Romain J 067e29a96a fix(commands:rule>list|Mod): fix when no rules 2021-05-13 23:14:13 +02:00
Romain J 8f62c2c4a1 feat(i18n:rule|Mod): feat rule command translations 2021-05-13 22:51:16 +02:00
Romain J ad443c9c48 feat(commands:rule|Mod): feat rule command 2021-05-13 22:19:27 +02:00
Romain J 4e3fbd7f4d feat(commands:*|Mod): first commit 2021-05-13 16:45:49 +02:00
Romain J 4678be191d update(deps): update deps versions 2021-05-13 16:45:04 +02:00
Romain J f00ff8d345 fix(core:i18n): fix crash for DMs 2021-05-13 16:44:18 +02:00
Romain J ba21cf859b fix(core): fix dependencies 2021-04-25 23:38:13 +02:00
Romain J 88f60690dd feat(commands:cnf|Linux): add new Command Not Found scrapper (Known issue: cache resolve on none results) 2021-04-25 23:33:30 +02:00
Romain J fcc23d87df update(commands:down?|Network): change api to improve results 2021-04-25 18:48:21 +02:00
Romain J 56e45b52b5 rage, part2 (hotfix from prod) 2021-04-25 01:22:39 +02:00
Romain J 96cfa17d2e rage 2021-04-25 00:59:57 +02:00
Romain J 3c5741e6c5 fix(commands:peeringdb|Network): totally remove cache for automated requests 2021-04-25 00:40:46 +02:00
Romain J 1693857864 fix(commands:peeringdb|Network): possible false positiv 2021-04-25 00:14:38 +02:00
Romain J 9362558a2e fix(commands:info|Utils): fetch files only in ./tuxbot 2021-04-24 23:13:53 +02:00
Romain J f7f5232e21 fix(commands:iplocalise|Network): prevent discord to not include :: to links 2021-04-24 23:01:10 +02:00
Romain J 32b6de0d0f fix(commands:iplocalise|Network): prevent discord to not include :: to links 2021-04-24 22:56:10 +02:00
Romain J 2a00d93023 feat(commands:iplocalise|Network): add way to ask for map 2021-04-24 22:34:53 +02:00
Romain J b9f6c6cb0a fix(commands:peeringdb|Network): fix false results 2021-04-23 23:47:43 +02:00
Romain J 5b7c905ac8 fix(commands:peeringdb|Network): fix timeout 2021-04-23 01:18:17 +02:00
Romain J 2e7934148e feat(commands:peeringdb|Network): add peeringdb command 2021-04-23 00:47:58 +02:00
Romain J 2afd3af540 fix(core): fix dependencies 2021-04-22 18:13:20 +02:00
Romain J c6c61a0886 update(commands:*|Network): speed optimisation 2021-04-22 18:11:55 +02:00
Romain J c6a5dc4ad6 feat(commands:isdown|Network): add isitdown api 2021-04-22 15:31:01 +02:00
Romain J 1f367fd2df fix(commands:getheaders|Network): prevent fails for missing [] on v6 2021-04-22 14:54:46 +02:00
Romain J 9172331927 fix(commands:info|Utils): undo testings changes 2021-04-22 00:22:09 +02:00
Romain J 561f56ca27 update(commands:iplocalise,getheaders|Network): speed optimisation 2021-04-22 00:16:37 +02:00
Romain J eca6e7b268 ram optimization 2021-04-21 18:28:50 +02:00
Romain J 4a508b1851 delete(commands:*|Crypto): remove unused cog 2021-04-21 18:00:43 +02:00
Romain J e63e939d77 feat(commands:cloudflare|Network): add workaround to iplocalise for cloudflare 2021-04-21 17:59:43 +02:00
Romain J 751c82909d style 2021-04-20 17:30:05 +02:00
Romain J 22c5ee57d4 fix(commands:getheaders|Network): fix timeout 2021-04-20 17:26:12 +02:00
Romain J 64fba7fec6 style 2021-04-20 17:12:38 +02:00
Romain J 7f9c202cc6 style 2021-04-20 15:51:03 +02:00
Romain J 1b7f153ec8 update(core): migrate to py3.10 & dpy2.0 2021-04-20 15:43:20 +02:00
Romain J 0eca877c1c fix(commands:dig|Network): fix async 2021-04-20 15:42:59 +02:00
Romain J 540dfd616a april fool 2021-03-31 21:57:37 +02:00
Romain J 1a10f64345 april fool 2021-03-31 18:09:10 +02:00
Romain J f0dc682047 style 2021-03-31 18:08:53 +02:00
Romain J 3525b9aa4b fix(commands:iplocalise|Network): remove crashing regex 2021-03-26 18:41:21 +01:00
Romain J 0ecc97518f fix(commands:iplocalise|Network): fix process crashing regex 2021-03-05 00:05:24 +01:00
Romain J 78a5ac9939 fix(commands:iplocalise|Network): change regex for domain validation 2021-03-02 19:00:08 +01:00
Romain J 34e32fdf68 fix(commands:ralgo|Crypto): set ralgo as async 2021-03-01 14:11:18 +01:00
Romain J edfeadb872 feat(commands:ralgo|Crypto): add ralgo api 2021-02-24 00:53:05 +01:00
Romain J 83723380e9 fix(commands|Network>iplocalise): improve domain regex 2021-02-16 19:38:42 +01:00
Romain J b5ca338d6c fix(commands|Custom>alias): close TUXBOT-BOT-1A, close TUXBOT-BOT-1H, close TUXBOT-BOT-1J 2021-02-16 19:28:30 +01:00
Romain J c7ddba1bae feat(host): start docker addon 2021-02-11 23:18:12 +01:00
Romain J 7423b40337 fix(core): close TUXBOT-BOT-11, close TUXBOT-BOT-19 2021-02-11 18:40:00 +01:00
Romain J fa98d67276 style 2021-02-11 18:27:26 +01:00
Romain J a0e67c1627 fix(commands|Network>iplocalise): close TUXBOT-BOT-10, close TUXBOT-BOT-16 2021-02-11 18:25:32 +01:00
Romain J 1e86abdf01 fix(core): close TUXBOT-BOT-Z 2021-02-11 18:11:19 +01:00
Romain J c566f775cd fix(deps): fix conflict version 2021-02-11 17:51:46 +01:00
Romain J fae56745bf fix(commands|Network>iplocalise): quickfix when no org is returned 2021-02-11 16:15:43 +01:00
Romain J f7176d917c fix(core): fix crash on DM message 2021-01-30 16:43:42 +01:00
Romain J 434021ecb9 feat(commands|Network>ping): feat ping command 2021-01-30 16:43:17 +01:00
Romain J 0c308727d2 fix(logs): exceptions must derive from BaseException 2021-01-30 14:21:20 +01:00
Romain J 5991ebfaf2 fix(readme|badges): update issue badge 2021-01-30 13:48:52 +01:00
Romain J 45d4aa1dc5 fix(commands|Network>iplocalise): close #6 2021-01-30 13:44:34 +01:00
Romain J 0687ee3f06 fix(import): add missing requirement 2021-01-28 10:39:54 +01:00
Romain J 287e4c1743
Merge pull request #5 from ebanDev/patch-3
Adding RHEL and derivatives readme section
2021-01-27 19:07:53 +01:00
Eban ce2b59b8d5
Update README.rst 2021-01-27 19:06:06 +01:00
Eban ae2538f99c
Update README.rst 2021-01-27 18:59:48 +01:00
Romain J da277e0d66 fix(import): add missing requirement 2021-01-27 15:55:57 +01:00
Romain J aeced979df fix(import): add missing requirement 2021-01-27 15:54:21 +01:00
Romain J bb87d77e33 fix(import): add missing file 2021-01-27 15:52:13 +01:00
Romain J 975a3b3d14 feat(doc): add missing instructions about postgresql 2021-01-27 15:51:10 +01:00
Romain J 33e09a9e02 feat(doc): add missing instructions about postgresql 2021-01-27 15:50:52 +01:00
Romain J c5c13506d7 fix(commands|Network>getheaders): fix timeout error 2021-01-27 15:14:53 +01:00
Romain J 647cc4bd64 feat(core|logs>sentry): feat sentry error handler 2021-01-27 15:14:05 +01:00
Romain J 554c0b52d5 feat(commands|Network>dig): feat dig command 2021-01-26 17:11:30 +01:00
Romain J 1d37dc1961 improve(core|replacement): improve not to show replacements 2021-01-26 17:10:31 +01:00
Romain J f88adec45b fix(commands|Utils>info): i18n 2021-01-26 15:45:44 +01:00
Romain J dd09a53c0e improve(commands|Network>iplocalise): rewrite 2021-01-26 15:43:16 +01:00
Romain J d66bec65ae feat(commands|Network>getheaders): feat getheaders command 2021-01-26 15:24:10 +01:00
Romain J fbb61c247d feat(secure): add ip to the list of things to not to display 2021-01-26 11:59:36 +01:00
Romain J d3ab384de0 improve(commands|Logs>socketstats): format output into an embed 2021-01-26 11:37:52 +01:00
Romain J 74307c755c fix(commands|Network>iplocalise): fix issues in 7962205d16 2021-01-26 10:21:39 +01:00
Romain J 18310f17a0 fix(PYL-W1401): Anomalous backslash detected
fix(PYL-W0613): Function contains unused argument
2021-01-26 09:43:25 +01:00
Romain J 7962205d16 feat(commands|Network>iplocalise): feat iplocalise command
todo:
- fix l'INET6 qui s'affiche comme 10 plutot que 6
- suppr le message Retrieving info si la commande n'a pas pu etre faite
- this shit https://canary.discord.com/channels/767804368233037886/768097484655689758/803299217279811634
2021-01-25 17:28:59 +01:00
Romain J fa3069244d feat(commands|Utils>info): feat cpu usage 2021-01-25 14:52:30 +01:00
Romain J 1fb3e035bd feat(commands|Polls>propose): feat poll proposition + acceptation 2021-01-22 10:22:39 +01:00
Romain J 37bbf0368e style 2021-01-21 16:13:46 +01:00
Romain J 01e0e5e27e style(deepsource): fix some analysis fails 2021-01-21 16:11:29 +01:00
Romain J 5d585bf218 feat(i18n): add translations for info command 2021-01-20 17:29:16 +01:00
Romain J 30cc3ecad2 feat(commands|Polls>propose): start dev for proposal 2021-01-20 17:28:37 +01:00
Romain J c3660aab8a style(import): change to absolute import for Tux 2021-01-20 15:08:16 +01:00
Romain J 0eaa53ffd5 feat(commands|Polls>create): feat poll creation 2021-01-20 14:51:04 +01:00
Romain J f00f0fd4c0 feat(commands|Custom>alias): finish alias interpretation 2021-01-19 16:05:20 +01:00
Romain J 1681c5abf5 Merge branch 'master' of https://github.com/Rom1-J/tuxbot-bot 2021-01-19 15:11:13 +01:00
Romain J 6757ce2ccc feat(commands|Custom>alias;locale): finish translations issues 2021-01-19 15:08:55 +01:00
Romain J 573ce3fb18 feat(commands|Custom>alias;locale): translations 2021-01-19 15:03:05 +01:00
Romain J 72fabf89b9 feat(commands|Custom>alias;locale): add alias registering and locale as custom for each user 2021-01-19 14:30:25 +01:00
Romain J 98b82e680e Update README.rst
feat(setup): add updater to setup
2021-01-19 11:40:49 +01:00
Romain J e5c3f1b8de Update README.rst 2021-01-19 11:16:50 +01:00
Romain J e537a59d8d
Update README.rst
Add wakatime badge
2020-12-08 15:28:13 +01:00
Romain J 834f071332 fix(PYL-W1401): Anomalous backslash detected 2020-11-12 00:07:51 +01:00
Romain J cfd59def74 feat(commands:polls|Polls): start dev of poll command 2020-11-12 00:03:01 +01:00
Romain J b4194dcadf feat(commands:locale|Utils): add translations for source info and credits 2020-11-11 02:46:48 +01:00
Romain J 42d7cad0e5 fix(commands|Utils>source): stupid of me, the +3 was 'cause of a diff in local and github 2020-11-11 01:29:53 +01:00
Romain J 5a65fe1a6c feat(commands|Utils): add source command 2020-11-11 01:27:51 +01:00
Romain J 3587a8f8a4 feat(commands|Utils): add credits and invite commands 2020-11-11 01:05:36 +01:00
Romain J 71576f48e4 fix(discord): add itents 2020-11-09 01:27:19 +01:00
Romain J d6e9cd6512 feat(commands|Utils): add info command 2020-11-09 01:18:55 +01:00
Romain J 7d588b2dbc remove(support): drop testing support for python 3.7 2020-11-08 01:09:40 +01:00
Romain J e38823e5be feat(database): add models loader in core 2020-11-08 01:07:27 +01:00
Romain J 71335de878 feat(logs): rewrite Logs cog 2020-10-22 00:00:48 +02:00
Romain J bdd77d1841 fix(PYL-C0201): 2020-10-21 00:26:40 +02:00
Romain J d7a2330fb6 fix(BAN-B607): 2020-10-21 00:11:53 +02:00
Romain J 969ff8c351 fix(PTC-W0031): 2020-10-21 00:09:47 +02:00
Romain J 4751a1b518 fix(deepsource): remove black from deepsource conf 2020-10-21 00:02:02 +02:00
Romain J 008ae76aca fix(PYL-R1705): 2020-10-20 23:56:02 +02:00
Romain J bef9060b78 fix(PTC-W0034): 2020-10-20 23:53:24 +02:00
Romain J 533ca6e3e7 fix(PYL-W1113): 2020-10-20 23:52:05 +02:00
Romain J 42e2d04a9e fix(make): add pylint to absolute path from venv 2020-10-20 23:49:46 +02:00
Romain J 7d67b8d581 update(deps): move to dpy 1.5.1 2020-10-20 23:45:51 +02:00
Romain J 6a926d717c Merge branch 'master' of https://github.com/Rom1-J/tuxbot-bot into master 2020-10-20 23:43:24 +02:00
Romain J bdc7afb1ef feat(readle): add black tag 2020-10-20 23:43:20 +02:00
DeepSource Bot 260ef9f41c Add .deepsource.toml 2020-10-20 21:39:32 +00:00
Romain J 10b7e4039c Merge branch 'master' into v3 2020-10-20 23:21:05 +02:00
Romain 951784718b update (cogs.basics>ping): optimisation
Remove unused parts
2020-08-24 21:08:55 +02:00
Mael G. 783f7507f0 Readme update 2020-08-24 13:25:02 -04:00
Mael G. 4b28ff0aea Fucking ecriture inclusive 2020-08-24 12:10:33 -04:00
Mael G. f155c7c27e Unused command text delete 2020-08-24 12:09:56 -04:00
Mael G. fa1ac02648 Help msg enchancement 2020-08-24 12:08:45 -04:00
Mael G. 5c51c15805 gitignore update 2020-08-24 12:02:10 -04:00
Mael G. fbda0e9414 add hardcoded alert for the filter message func 2020-08-24 12:01:16 -04:00
Mael G. 6b433970fe New URL of Discord for ping 2020-08-24 11:59:01 -04:00
Mael G. 032a49b08f remove cringe messages in afk 2020-08-24 11:57:51 -04:00
Mael G. f181f58735 Remove dead images for search embeds 2020-08-24 11:56:46 -04:00
Mael G. f7bfc25793 remove useless prints in utility cos 2020-08-24 11:54:06 -04:00
Mael G. 3dd17f44a1 shroute bug fix + enchancement 2020-08-24 11:52:45 -04:00
Mael G. 7e79ac7fab No hello file 2020-08-23 20:20:39 -04:00
Mael G. 3e3f6d42d6 Requirements update 2020-08-23 20:18:57 -04:00
Romain J 1e6f0b6eb0 repair dumb merge 2020-08-19 17:07:50 +02:00
Romain J 1f7da4fd14 improve avatar 2020-08-19 17:00:45 +02:00
Romain J db7dfd5c58 undo delete message when ban 2020-08-19 16:59:10 +02:00
Romain J f79074a97d improve info 2020-08-19 16:56:52 +02:00
Mael G. 6400d1da71 shroute 2020-08-19 10:35:32 -04:00
Mael G. bb6b25c5d9 shroute cmd 2020-08-19 10:34:25 -04:00
Mael G. d3683ed10d Shroute 2020-08-19 10:33:48 -04:00
Mael G. 5cc364480a DIG 2020-08-18 16:01:34 -04:00
Romain J 17b3e658fc remove debug things 2020-08-18 21:51:51 +02:00
Romain J 175174757b improve getheaders 2020-08-18 21:50:23 +02:00
Mael G. ecdde52ca3 iplocalise bug fix + bgp.he.net link 2020-08-18 15:39:23 -04:00
Mael G. 534a78e447 Iplocalise update 2020-04-27 11:21:56 -04:00
Romain J 083d14e056 fix(ci|show|render): fix inline issue 2020-04-17 14:32:25 +02:00
Romain J daed469994 fix(command): fix help spam 2020-04-08 00:26:26 +02:00
Romain J 68ca0cb2fc fix(ping): quick fix for ping result 2020-04-07 16:00:39 +02:00
Romain J 4d479f6516 update(rewrite): prepare commands to rewrite 2020-04-06 20:43:59 +02:00
Gitea afe76d00c1 Requirements 2020-02-01 01:53:03 +00:00
Mael G. fc06756363 feat(monitoring): add http server for monitoring 2019-12-29 22:19:26 +01:00
Mael G. 425ff79c8d add(jishaku) 2019-12-29 22:15:32 +01:00
Romain J 57ea780f2c add(doc): add issue template (better with git add . ...) 2019-09-01 16:19:03 +02:00
Romain J 78026ee88e add(doc): add issue template 2019-09-01 16:16:25 +02:00
161 changed files with 7233 additions and 672 deletions

8
.deepsource.toml Normal file
View 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
View 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
View file

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

15
.gitignore vendored
View file

@ -33,7 +33,20 @@ __pycache__/
__pypackages__/
venv
venv3.8
venv3.9
venv3.11
dist
build
*.egg
*.egg-info
*.egg-info
.ipython/
.env
.envs/*
!.envs/.local/
data/settings/
dump.rdb

View file

@ -1,44 +1,70 @@
<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>

View file

@ -3,5 +3,5 @@
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (tuxbot-bot-rewrite)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (tuxbot_bot)" project-jdk-type="Python SDK" />
</project>

View file

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tuxbot-bot-rewrite.iml" filepath="$PROJECT_DIR$/.idea/tuxbot-bot-rewrite.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/tuxbot_bot.iml" filepath="$PROJECT_DIR$/.idea/tuxbot_bot.iml" />
</modules>
</component>
</project>

View file

@ -5,8 +5,13 @@
<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="inheritedJdk" />
<orderEntry type="jdk" jdkName="Python 3.10 (tuxbot_bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">

14
.idea/webResources.xml Normal file
View 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
View file

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

View file

@ -4,12 +4,19 @@ good-names=
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)

View file

@ -1,31 +1,84 @@
PYTHON = python
VENV = venv
ifeq ($(ISPROD), 1)
DOCKER_LOCAL := docker-compose -f production.yml
else
DOCKER_LOCAL := docker-compose -f local.yml
endif
XGETTEXT_FLAGS = --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
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:
$(PYTHON) -m venv --clear $(VENV)
$(VENV)/bin/pip install -U pip setuptools
$(VIRTUAL_ENV)/bin/pip install -U pip setuptools
.PHONY: install
install:
$(VENV)/bin/pip install .
$(VIRTUAL_ENV)/bin/pip install .
.PHONY: install-dev
install-dev:
$(VIRTUAL_ENV)/bin/pip install -r dev.requirements.txt
.PHONY: update
update:
$(VENV)/bin/pip install -U .
$(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
reformat:
$(PYTHON) -m black `git ls-files "*.py"` --line-length=79 && pylint tuxbot
.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; \

View file

@ -1,4 +1,4 @@
|image0| |image1|
|image0| |image1| |image2| |image3|
.. role:: bash(code)
:language: bash
@ -14,7 +14,7 @@ Installing the pre-requirements
- The pre-requirements are:
- Python 3.7 or greater
- Python 3.8 or greater
- Pip
- Git
@ -26,9 +26,9 @@ Arch Linux
.. code-block:: bash
$ sudo pacman -Syu python python-pip python-virtualenv git
$ sudo pacman -Syu python python-pip python-virtualenv git make gcc postgresql
Continue to `create the venv <#creating-the-virtual-environment>`__.
Continue to `configure postgresql <#configure-postgresql>`__.
--------------
@ -38,9 +38,21 @@ Debian
.. code-block:: bash
$ sudo apt update
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git make gcc postgresql postgresql-client
Continue to `create the venv <#creating-the-virtual-environment>`__.
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>`__.
--------------
@ -49,6 +61,43 @@ 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
--------------------------------
@ -61,7 +110,7 @@ two commands:
$ make install
Now, switch your environment to the virtual one by run this single
command: :bash:`source ~/tuxvenv/bin/activate`
command: :bash:`source ~/venv/bin/activate`
Configuration
-------------
@ -69,12 +118,12 @@ Configuration
It's time to set up your first instance, to do this, you can simply
execute this command:
:bash:`tuxbot-setup [your instance name]`
:bash:`tuxbot-setup`
After following the instructions, you can run your instance by executing
this command:
:bash:`tuxbot [your instance name]`
:bash:`tuxbot`
Update
------
@ -85,5 +134,8 @@ To update the whole bot after a :bash:`git pull`, just execute
$ make update
.. |image0| image:: https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-%23007ec6
.. |image1| image:: https://img.shields.io/badge/dynamic/json?color=%23dfb317&label=issues&query=%24.open_issues_count&suffix=%20open&url=https%3A%2F%2Fgit.gnous.eu%2Fapi%2Fv1%2Frepos%2FGnousEU%2Ftuxbot-bot%2F
.. |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

View 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"]

View file

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

View 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

View file

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

View file

@ -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
}

View 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: ${@}"
}

View 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
}

View 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}'."

View 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}"

View 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."

View 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"]

View 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 "$@"

View file

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

3
dev.requirements.txt Normal file
View file

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

34
local.yml Normal file
View 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
View 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

View file

@ -1,7 +1,7 @@
[metadata]
name = Tuxbot-bot
version = attr: tuxbot.__version__
url = https://git.gnous.eu/gnouseu/tuxbot-bot/
url = https://github.com/Rom1-J/tuxbot-bot/
author = Romain J.
author_email = romain@gnous.eu
maintainer = Romain J.
@ -13,18 +13,25 @@ platforms = linux
[options]
packages = find_namespace:
python_requires = >=3.7
python_requires = >=3.8
install_requires =
appdirs>=1.4.4
aiocache>=0.11.1
asyncpg>=0.21.0
Babel>=2.8.0
black==20.8b1
discord.py==1.5.0
discord_flags==2.1.1
humanize==2.6.0
jishaku>=1.19.1.200
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
rich>=6.0.0
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 =
@ -41,4 +48,4 @@ include =
locales/*.po
**/locales/*.po
data/*
data/**/*
data/**/*

View file

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

View file

@ -1,8 +1,8 @@
import os
from collections import namedtuple
build = os.popen("git rev-parse --short HEAD").read().strip()
info = os.popen('git log -n 1 -s --format="%s"').read().strip()
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"

View file

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

View file

@ -4,80 +4,34 @@ import logging
import signal
import sys
import os
import tracemalloc
from argparse import Namespace
from typing import NoReturn
from datetime import datetime
import discord
import humanize
import pip
from rich.columns import Columns
from rich.console import Console
from rich.panel import Panel
from rich.traceback import install
from rich.table import Table, box
from rich.text import Text
from rich import print as rprint
import tuxbot.logging
from tuxbot.core.bot import Tux
from tuxbot.core import data_manager
from tuxbot.core import config
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")
console = Console()
install(console=console)
tracemalloc.start()
BORDER_STYLE = "not dim"
def list_instances() -> NoReturn:
"""List all available instances"""
app_config = config.ConfigFile(
data_manager.config_dir / "config.yaml", config.AppConfig
).config
console.print(
Panel("[bold green]Instances", style="green"), justify="center"
)
console.print()
columns = Columns(expand=True, padding=2, align="center")
for instance, details in app_config.Instances.items():
active = details["active"]
last_run = (
humanize.naturaltime(
datetime.now() - datetime.fromtimestamp(details["last_run"])
)
or "[i]unknown"
)
table = Table(
style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD
)
table.add_column("Name")
table.add_column(("Running since" if active else "Last run"))
table.add_row(instance, last_run)
table.title = Text(instance, style="green" if active else "red")
columns.add_renderable(table)
console.print(columns)
console.print()
sys.exit(os.EX_OK)
def debug_info() -> NoReturn:
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("uptime").read().strip().split()
uptime = os.popen("/usr/bin/uptime").read().strip().split()
console.print(
Panel("[bold blue]Debug Info", style="blue"), justify="center"
@ -141,7 +95,7 @@ def parse_cli_flags(args: list) -> Namespace:
"""
parser = argparse.ArgumentParser(
description="Tuxbot - OpenSource bot",
usage="tuxbot <instance_name> [arguments]",
usage="tuxbot [arguments]",
)
parser.add_argument(
"--version",
@ -152,27 +106,14 @@ def parse_cli_flags(args: list) -> Namespace:
parser.add_argument(
"--debug", action="store_true", help="Show debug information."
)
parser.add_argument(
"--list-instances",
"-L",
action="store_true",
help="List all instance names",
)
parser.add_argument(
"--token", "-T", type=str, help="Run Tuxbot with passed token"
)
parser.add_argument(
"instance_name",
nargs="?",
help="Name of the bot instance created during `tuxbot-setup`.",
)
args = parser.parse_args(args)
return args
return parser.parse_args(args)
async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> NoReturn:
async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> None:
"""Handler when the bot shutdown
It cancels all running task.
@ -213,7 +154,7 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
None
When exiting, this function return None.
"""
data_path = data_manager.data_path(tux.instance_name)
data_path = data_manager.data_path
tuxbot.logging.init_logging(10, location=data_path / "logs")
@ -232,9 +173,9 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
try:
await tux.load_packages()
console.print()
await tux.start(token=token, bot=True)
await tux.start(token=token)
except discord.LoginFailure:
log.critical("This token appears to be valid.")
log.critical("This token appears to be invalid.")
console.print()
console.print(
"[prompt.invalid]This token appears to be valid. [i]exiting...[/i]"
@ -247,14 +188,12 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
return None
def run() -> NoReturn:
def run() -> None:
"""Main function"""
tux = None
cli_flags = parse_cli_flags(sys.argv[1:])
if cli_flags.list_instances:
list_instances()
elif cli_flags.debug:
if cli_flags.debug:
debug_info()
elif cli_flags.version:
rprint(f"Tuxbot V{version_info.major}")
@ -266,13 +205,6 @@ def run() -> NoReturn:
asyncio.set_event_loop(loop)
try:
if not cli_flags.instance_name:
console.print(
"[red]No instance provided ! "
"You can use 'tuxbot -L' to list all available instances"
)
sys.exit(ExitCodes.CRITICAL)
tux = Tux(
cli_flags=cli_flags,
description="Tuxbot, made from and for OpenSource",
@ -296,7 +228,7 @@ def run() -> NoReturn:
raise
except Exception as exc:
log.error("Unexpected exception (%s): ", type(exc))
console.print_exception()
console.print_exception(show_locals=True)
if tux is not None:
loop.run_until_complete(shutdown_handler(tux, None, 1))
finally:

View file

@ -1,8 +1,8 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .admin import Admin
from .config import AdminConfig
from ...core.bot import Tux
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")

View 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)

View 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] = {}

View file

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-06-11 19:07+0200\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"
@ -16,12 +16,3 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tuxbot/cogs/admin/admin.py:33
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/admin/admin.py:43
msgid "List of available locales: "
msgstr ""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-06-11 19:07+0200\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"
@ -17,12 +17,3 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/admin/admin.py:33
#, fuzzy, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr "Langue changée pour {lang} avec succès"
#: tuxbot/cogs/admin/admin.py:43
msgid "List of available locales: "
msgstr "Liste des langues disponibles : "

View 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"

View file

View 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))

View 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] = {}

View 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)
)

View file

View 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

View 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 ""

View 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à"

View 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 ""

View file

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

View 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
View 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
View 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)

View file

View 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)

View 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"

View 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"

View 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"

View file

View 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))

View 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] = {}

View file

View 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,
}

View file

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

View 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()

View 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))

View file

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-06-11 18:24+0200\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"
@ -16,12 +16,3 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tuxbot/cogs/admin/admin.py:38
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/admin/admin.py:47
msgid "List of available locales: "
msgstr ""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-06-11 18:24+0200\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"
@ -17,12 +17,3 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/admin/admin.py:38
#, fuzzy, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr "Langue changée pour {lang} avec succès"
#: tuxbot/cogs/admin/admin.py:47
msgid "List of available locales: "
msgstr "Liste des langues disponibles : "

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Tuxbot-bot\n"
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
"POT-Creation-Date: 2020-06-11 19:07+0200\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"
@ -17,11 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: tuxbot/cogs/admin/admin.py:33
#, python-brace-format
msgid "Locale changed to {lang} successfully"
#: tuxbot/cogs/Linux/functions/cnf.py:42
msgid "Something went wrong ..."
msgstr ""
#: tuxbot/cogs/admin/admin.py:43
msgid "List of available locales: "
#: tuxbot/cogs/Linux/linux.py:56
msgid "No result found"
msgstr ""

View file

View 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)

View file

@ -1,15 +1,20 @@
from typing import Dict
from structured_config import Structure, StrField
HAS_MODELS = False
class AdminConfig(Structure):
class LogsConfig(Structure):
dm: str = StrField("")
mentions: str = StrField("")
guilds: str = StrField("")
errors: str = StrField("")
gateway: str = StrField("")
sentryKey: str = StrField("")
extra = {
extra: Dict[str, Dict] = {
"dm": {
"type": str,
"description": "URL of the webhook used for send DMs "
@ -33,4 +38,8 @@ extra = {
"type": str,
"description": "URL of the webhook used for send gateway information",
},
"sentryKey": {
"type": str,
"description": "Sentry KEY for error logging (https://sentry.io/)",
},
}

View file

View 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

View file

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

View file

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

View file

@ -0,0 +1,30 @@
# 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/Logs/logs.py:274
msgid "```An error occurred, the bot owner has been advertised...```"
msgstr ""
#: tuxbot/cogs/Logs/logs.py:334
msgid "Sockets stats"
msgstr ""
#: tuxbot/cogs/Logs/logs.py:336
msgid "{} socket events observed ({:.2f}/minute):"
msgstr ""

359
tuxbot/cogs/Logs/logs.py Normal file
View file

@ -0,0 +1,359 @@
import asyncio
import datetime
import json
import logging
import textwrap
import traceback
from collections import defaultdict
from logging import LogRecord
from typing import Any, Dict, List, DefaultDict
import discord
import humanize
import psutil
import sentry_sdk
from discord.ext import commands, tasks
from structured_config import ConfigFile
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
from tuxbot.core.utils.functions.extra import (
command_extra,
ContextPlus,
)
from tuxbot.core.utils.data_manager import cogs_data_path
from .config import LogsConfig
from .functions.utils import sort_by
from ...core.utils.functions.utils import shorten
log = logging.getLogger("tuxbot.cogs.Logs")
_ = Translator("Logs", __file__)
class GatewayHandler(logging.Handler):
def __init__(self, cog):
self.cog = cog
super().__init__(logging.INFO)
def filter(self, record: LogRecord):
return (
record.name == "discord.gateway"
or "Shard ID" in record.msg
or "Websocket closed " in record.msg
)
def emit(self, record: LogRecord):
self.cog.add_record(record)
class Logs(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
self.process = psutil.Process()
self._batch_lock = asyncio.Lock()
self._data_batch: List[Dict[str, Any]] = []
self._gateway_queue: asyncio.Queue = asyncio.Queue()
self.gateway_worker.start() # pylint: disable=no-member
self.__config: LogsConfig = ConfigFile(
str(cogs_data_path("Logs") / "config.yaml"),
LogsConfig,
).config
self._resumes: List[datetime.datetime] = []
self._identifies: DefaultDict[Any, list] = defaultdict(list)
self.old_on_error = bot.on_error
bot.on_error = self.on_error
if self.bot.instance_name != "dev":
# pylint: disable=abstract-class-instantiated
sentry_sdk.init(
dsn=self.__config.sentryKey,
traces_sample_rate=1.0,
environment=self.bot.instance_name,
debug=False,
attach_stacktrace=True,
)
def cog_unload(self):
self.bot.on_error = self.old_on_error
async def on_error(self, event, *args, **kwargs):
raise # pylint: disable=misplaced-bare-raise
# =========================================================================
# =========================================================================
def webhook(self, log_type):
webhook = discord.Webhook.from_url(
getattr(self.__config, log_type),
session=self.bot.session,
)
return webhook
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("guilds").send(embed=e)
def add_record(self, record: LogRecord):
self._gateway_queue.put_nowait(record)
async def notify_gateway_status(self, record: LogRecord):
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}] "
f"{await shorten(record.msg, 1500)}`"
)
await self.webhook("gateway").send(msg)
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 _, 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]
async def register_command(self, ctx: ContextPlus):
if ctx.command is None:
return
command = ctx.command.qualified_name
self.bot.stats["commands"][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(
"%s: %s in %s > %s",
message.created_at,
message.author,
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,
}
)
# =========================================================================
# =========================================================================
@tasks.loop()
async def gateway_worker(self):
record = await self._gateway_queue.get()
await self.notify_gateway_status(record)
@commands.Cog.listener()
async def on_command_completion(self, ctx: ContextPlus):
await self.register_command(ctx)
@commands.Cog.listener()
async def on_socket_response(self, msg):
self.bot.stats["socket"][msg.get("t")] += 1
@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,
)
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("dm").send(embed=e)
@commands.Cog.listener()
async def on_command_error(
self, ctx: ContextPlus, error: commands.CommandError
):
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
self.bot.console.log(
"Command Error, check sentry or discord error channel"
)
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),
)
e.add_field(
name="Bot Instance",
value=self.bot.instance_name,
)
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("errors").send(embed=e)
e.description = _(
"```An error occurred, the bot owner has been advertised...```",
ctx,
self.bot.config,
)
e.remove_field(0)
e.remove_field(1)
e.remove_field(1)
if self.bot.instance_name != "dev":
sentry_sdk.capture_exception(error)
e.set_footer(text=sentry_sdk.last_event_id())
await ctx.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()
# =========================================================================
# =========================================================================
@command_extra(name="commandstats", hidden=True, deletable=True)
@commands.is_owner()
async def _commandstats(self, ctx: ContextPlus, limit=20):
counter = self.bot.stats["commands"]
width = len(max(counter, key=len)) + 1
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, deletable=True)
async def _socketstats(self, ctx: ContextPlus):
delta = datetime.datetime.now() - self.bot.uptime
minutes = delta.total_seconds() / 60
counter = self.bot.stats["socket"]
if None in counter:
counter.pop(None)
total = sum(self.bot.stats["socket"].values())
cpm = total / minutes
e = discord.Embed(
title=_("Sockets stats", ctx, self.bot.config),
description=_(
"{} socket events observed ({:.2f}/minute):",
ctx,
self.bot.config,
).format(total, cpm),
color=discord.colour.Color.green(),
)
for major, events in sort_by(counter.most_common()).items():
if events:
output = "\n".join(f"{k}: {v}" for k, v in events.items())
e.add_field(
name=major.capitalize(),
value=f"```\n{output}\n```",
inline=False,
)
await ctx.send(embed=e)
@command_extra(name="uptime")
async def _uptime(self, ctx: ContextPlus):
uptime = humanize.naturaltime(
datetime.datetime.now() - self.bot.uptime
)
await ctx.send(f"Uptime: **{uptime}**")

View file

View file

@ -0,0 +1,19 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .mod import Mod
from .config import ModConfig, 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(Mod(bot))

12
tuxbot/cogs/Mod/config.py Normal file
View file

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

View file

View file

@ -0,0 +1,68 @@
from discord.ext import commands
from discord.ext.commands import Context
from tuxbot.cogs.Mod.functions.exceptions import (
RuleTooLongException,
UnknownRuleException,
NonMessageException,
NonBotMessageException,
ReasonTooLongException,
)
from tuxbot.cogs.Mod.models import Rule
def _(x):
return x
class RuleIDConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
if not argument.isdigit():
raise UnknownRuleException(_("Unknown rule"))
arg = int(argument)
rule_row = await Rule.get_or_none(server_id=ctx.guild.id, rule_id=arg)
if not rule_row:
raise UnknownRuleException(_("Unknown rule"))
return arg
class RuleConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
if len(argument) > 300:
raise RuleTooLongException(
_("Rule length must be 300 characters or lower.")
)
return argument
class BotMessageConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
try:
m = await commands.MessageConverter().convert(ctx, argument)
if m.author == ctx.me:
return m
raise NonBotMessageException(_("Please provide one of my message"))
except commands.BadArgument:
raise NonMessageException(
_("Please provide a message in this guild")
)
class ReasonConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
if argument is None:
return f"{ctx.author.display_name} (ID: {ctx.author.id})"
if len(argument) > 300:
raise ReasonTooLongException(
_("Reason length must be 300 characters or lower.")
)
return argument

View file

@ -0,0 +1,25 @@
from discord.ext import commands
class ModException(commands.BadArgument):
pass
class RuleTooLongException(ModException):
pass
class UnknownRuleException(ModException):
pass
class NonMessageException(ModException):
pass
class NonBotMessageException(ModException):
pass
class ReasonTooLongException(ModException):
pass

View file

@ -0,0 +1,52 @@
from typing import Optional, List
from tuxbot.cogs.Mod.models import MuteRole
from tuxbot.cogs.Mod.models.rules import Rule
from tuxbot.core.config import set_for_key
from tuxbot.core.config import Config
from tuxbot.core.bot import Tux
from tuxbot.core.utils.functions.extra import ContextPlus
async def save_lang(bot: Tux, ctx: ContextPlus, lang: str) -> None:
set_for_key(bot.config.Servers, ctx.guild.id, Config.Server, locale=lang)
async def get_server_rules(guild_id: int) -> List[Rule]:
return await Rule.filter(server_id=guild_id).all().order_by("rule_id")
def get_most_recent_server_rules(rules: List[Rule]) -> Rule:
return sorted(rules, key=lambda r: r.updated_at, reverse=True)[0]
def paginate_server_rules(rules: List[Rule]) -> List[str]:
body = [""]
for rule in rules:
if len(body[-1] + format_rule(rule)) > 2000:
body.append(format_rule(rule) + "\n")
else:
body[-1] += format_rule(rule) + "\n"
return body
def format_rule(rule: Rule) -> str:
return f"**{rule.rule_id}** - {rule.content}"
async def get_mute_role(guild_id: int) -> Optional[MuteRole]:
return await MuteRole.get_or_none(server_id=guild_id)
async def create_mute_role(guild_id: int, role_id: int) -> MuteRole:
role_row = await MuteRole()
role_row.server_id = guild_id # type: ignore
role_row.role_id = role_id # type: ignore
await role_row.save()
return role_row

View file

@ -0,0 +1,100 @@
# 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"
#: tuxbot/cogs/Mod/mod.py:67
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:78
msgid "List of available locales: "
msgstr ""
#: tuxbot/cogs/Mod/mod.py:103
msgid ""
"{}please read the following rule: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:121
msgid "No rules found for this server"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:120
msgid "Rules for {}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:126
msgid "Latest change: {}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:140
msgid "Rules for {} ({}/{})"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:159
msgid ""
"Following rule added: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:182
msgid ""
"Following rule updated: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:200
msgid ""
"Following rule deleted: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
msgid "Missing members"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
#: tuxbot/cogs/Mod/mod.py:390
msgid "No mute role has been specified for this guild"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:346
msgid "Mute role successfully defined"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:22
msgid "Unknown rule"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:31
msgid "Rule length must be 300 characters or lower."
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:50
msgid "Please provide one of my message"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:53
msgid "Please provide a message in this guild"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:62
msgid "Reason length must be 300 characters or lower."
msgstr ""

View file

@ -0,0 +1,109 @@
# 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"
#: tuxbot/cogs/Mod/mod.py:67
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr "Langue changée pour {lang} avec succès"
#: tuxbot/cogs/Mod/mod.py:78
msgid "List of available locales: "
msgstr "Liste des langues disponibles : "
#: tuxbot/cogs/Mod/mod.py:103
msgid ""
"{}please read the following rule: \n"
"{}"
msgstr ""
"{}merci de lire la règle suivante : \n"
"{}"
#: tuxbot/cogs/Mod/mod.py:121
msgid "No rules found for this server"
msgstr "Aucune règle trouvée pour ce serveur"
#: tuxbot/cogs/Mod/mod.py:120
msgid "Rules for {}"
msgstr "Règles pour {}"
#: tuxbot/cogs/Mod/mod.py:126
msgid "Latest change: {}"
msgstr "Dernières modifications : {}"
#: tuxbot/cogs/Mod/mod.py:140
msgid "Rules for {} ({}/{})"
msgstr "Règles pour {} ({}/{})"
#: tuxbot/cogs/Mod/mod.py:159
msgid ""
"Following rule added: \n"
"{}"
msgstr ""
"La règle suivante a été ajoutée: \n"
"{}"
#: tuxbot/cogs/Mod/mod.py:182
msgid ""
"Following rule updated: \n"
"{}"
msgstr ""
"La règle suivante a été modifiée: \n"
"{}"
#: tuxbot/cogs/Mod/mod.py:200
msgid ""
"Following rule deleted: \n"
"{}"
msgstr ""
"La règle suivante a été supprimée: \n"
"{}"
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
msgid "Missing members"
msgstr "Membres inexistants"
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
#: tuxbot/cogs/Mod/mod.py:390
msgid "No mute role has been specified for this guild"
msgstr "Aucun rôle mute n'a été spécifié pour ce serveur"
#: tuxbot/cogs/Mod/mod.py:346
msgid "Mute role successfully defined"
msgstr "Rôle mute défini avec succès"
#: tuxbot/cogs/Mod/functions/converters.py:22
msgid "Unknown rule"
msgstr "Règle inconnue"
#: tuxbot/cogs/Mod/functions/converters.py:31
msgid "Rule length must be 300 characters or lower."
msgstr "La règle doit faire 300 characters ou moins"
#: tuxbot/cogs/Mod/functions/converters.py:50
msgid "Please provide one of my message"
msgstr "Merci de donner un de mes messages"
#: tuxbot/cogs/Mod/functions/converters.py:53
msgid "Please provide a message in this guild"
msgstr "Merci de donner un message dans ce serveur"
#: tuxbot/cogs/Mod/functions/converters.py:62
msgid "Reason length must be 300 characters or lower."
msgstr "La raison doit faire 300 characters ou moins"

View file

@ -0,0 +1,101 @@
# 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/Mod/mod.py:81
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:92
msgid "List of available locales: "
msgstr ""
#: tuxbot/cogs/Mod/mod.py:117
msgid ""
"{}please read the following rule: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:135 tuxbot/cogs/Mod/mod.py:237
msgid "No rules found for this server"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:139 tuxbot/cogs/Mod/mod.py:241
msgid "Rules for {}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:145 tuxbot/cogs/Mod/mod.py:247
msgid "Latest change: {}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:159 tuxbot/cogs/Mod/mod.py:264
msgid "Rules for {} ({}/{})"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:180
msgid ""
"Following rule added: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:203
msgid ""
"Following rule updated: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:221
msgid ""
"Following rule deleted: \n"
"{}"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
msgid "Missing members"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
#: tuxbot/cogs/Mod/mod.py:390
msgid "No mute role has been specified for this guild"
msgstr ""
#: tuxbot/cogs/Mod/mod.py:346
msgid "Mute role successfully defined"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:21
#: tuxbot/cogs/Mod/functions/converters.py:28
msgid "Unknown rule"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:37
msgid "Rule length must be 300 characters or lower."
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:51
msgid "Please provide one of my message"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:54
msgid "Please provide a message in this guild"
msgstr ""
#: tuxbot/cogs/Mod/functions/converters.py:62
msgid "Reason length must be 300 characters or lower."
msgstr ""

402
tuxbot/cogs/Mod/mod.py Normal file
View file

@ -0,0 +1,402 @@
import logging
from datetime import datetime
from typing import Optional
import discord
from discord.ext import commands
from tuxbot.cogs.Mod.functions.converters import (
RuleConverter,
RuleIDConverter,
BotMessageConverter,
ReasonConverter,
)
from tuxbot.cogs.Mod.functions.exceptions import (
RuleTooLongException,
UnknownRuleException,
NonMessageException,
NonBotMessageException,
ReasonTooLongException,
)
from tuxbot.cogs.Mod.functions.utils import (
save_lang,
get_server_rules,
format_rule,
get_most_recent_server_rules,
paginate_server_rules,
get_mute_role,
create_mute_role,
)
from tuxbot.cogs.Mod.models.rules import Rule
from tuxbot.core.utils import checks
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
find_locale,
get_locale_name,
list_locales,
)
from tuxbot.core.utils.functions.extra import (
group_extra,
ContextPlus,
command_extra,
)
log = logging.getLogger("tuxbot.cogs.Mod")
_ = Translator("Mod", __file__)
class Mod(commands.Cog):
def __init__(self, bot: Tux):
self.bot = bot
async def cog_command_error(self, ctx: ContextPlus, error):
if isinstance(
error,
(
RuleTooLongException,
UnknownRuleException,
NonMessageException,
NonBotMessageException,
ReasonTooLongException,
),
):
return await ctx.send(_(str(error), ctx, self.bot.config))
# =========================================================================
# =========================================================================
@group_extra(name="lang", aliases=["locale", "langue"], deletable=True)
@commands.guild_only()
@checks.is_admin()
async def _lang(self, ctx: ContextPlus):
"""Manage lang settings."""
@_lang.command(name="set", aliases=["define", "choice"])
async def _lang_set(self, ctx: ContextPlus, lang: str):
try:
await save_lang(self.bot, ctx, find_locale(lang.lower()))
await ctx.send(
_(
"Locale changed to {lang} successfully",
ctx,
self.bot.config,
).format(lang=f"`{get_locale_name(lang).lower()}`")
)
except NotImplementedError:
await self._lang_list(ctx)
@_lang.command(name="list", aliases=["liste", "all", "view"])
async def _lang_list(self, ctx: ContextPlus):
e = discord.Embed(
title=_("List of available locales: ", ctx, self.bot.config),
description=list_locales(),
color=0x36393E,
)
await ctx.send(embed=e)
# =========================================================================
@group_extra(
name="rule",
aliases=["rules", "regle", "regles"],
deletable=False,
invoke_without_command=True,
)
@commands.guild_only()
async def _rule(
self,
ctx: ContextPlus,
rule: RuleIDConverter,
members: commands.Greedy[discord.Member],
):
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
message = _(
"{}please read the following rule: \n{}", ctx, self.bot.config
)
authors = ""
for member in members:
if member in ctx.message.mentions:
authors += f"{member.name}#{member.discriminator}, "
else:
authors += f"{member.mention}, "
await ctx.send(message.format(authors, format_rule(rule_row)))
@_rule.command(name="list", aliases=["show", "all"])
async def _rule_list(self, ctx: ContextPlus):
rules = await get_server_rules(ctx.guild.id)
if not rules:
return await ctx.send(
_("No rules found for this server", ctx, self.bot.config)
)
embed = discord.Embed(
title=_("Rules for {}", ctx, self.bot.config).format(
ctx.guild.name
),
color=discord.Color.blue(),
)
embed.set_footer(
text=_("Latest change: {}", ctx, self.bot.config).format(
get_most_recent_server_rules(rules).updated_at.ctime()
)
)
pages = paginate_server_rules(rules)
if len(pages) == 1:
embed.description = pages[0]
await ctx.send(embed=embed)
else:
for i, page in enumerate(pages):
embed.title = _(
"Rules for {} ({}/{})", ctx, self.bot.config
).format(ctx.guild.name, str(i + 1), str(len(pages)))
embed.description = page
await ctx.send(embed=embed)
@checks.is_admin()
@_rule.command(name="add")
async def _rule_add(self, ctx: ContextPlus, *, rule: RuleConverter):
rule_row = await Rule()
rule_row.server_id = ctx.guild.id
rule_row.author_id = ctx.message.author.id
rule_row.rule_id = (
len(await get_server_rules(ctx.guild.id)) + 1 # type: ignore
)
rule_row.content = str(rule) # type: ignore
await rule_row.save()
await ctx.send(
_("Following rule added: \n{}", ctx, self.bot.config).format(
format_rule(rule_row)
)
)
@checks.is_admin()
@_rule.command(name="edit")
async def _rule_edit(
self,
ctx: ContextPlus,
rule: RuleIDConverter,
*,
content: RuleConverter,
):
# noinspection PyTypeChecker
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
rule_row.content = str(content) # type: ignore
rule_row.updated_at = datetime.now() # type: ignore
await rule_row.save()
await ctx.send(
_("Following rule updated: \n{}", ctx, self.bot.config).format(
format_rule(rule_row)
)
)
@checks.is_admin()
@_rule.command(name="delete")
async def _rule_delete(
self,
ctx: ContextPlus,
rule: RuleIDConverter,
):
# noinspection PyTypeChecker
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
await rule_row.delete()
await ctx.send(
_("Following rule deleted: \n{}", ctx, self.bot.config).format(
format_rule(rule_row)
)
)
@checks.is_admin()
@_rule.command(name="update")
async def _rule_update(
self,
ctx: ContextPlus,
message: BotMessageConverter,
):
rules = await get_server_rules(ctx.guild.id)
if not rules:
return await ctx.send(
_("No rules found for this server", ctx, self.bot.config)
)
embed = discord.Embed(
title=_("Rules for {}", ctx, self.bot.config).format(
ctx.guild.name
),
color=discord.Color.blue(),
)
embed.set_footer(
text=_("Latest change: {}", ctx, self.bot.config).format(
get_most_recent_server_rules(rules).updated_at.ctime()
)
)
pages = paginate_server_rules(rules)
# noinspection PyTypeChecker
to_edit: discord.Message = message
if len(pages) == 1:
embed.description = pages[0]
await to_edit.edit(content="", embed=embed)
else:
for i, page in enumerate(pages):
embed.title = _(
"Rules for {} ({}/{})", ctx, self.bot.config
).format(ctx.guild.name, str(i + 1), str(len(pages)))
embed.description = page
await to_edit.edit(content="", embed=embed)
# =========================================================================
@group_extra(
name="mute",
deletable=True,
invoke_without_command=True,
)
@commands.guild_only()
@checks.is_admin()
async def _mute(
self,
ctx: ContextPlus,
members: commands.Greedy[discord.Member],
*,
reason: Optional[ReasonConverter],
):
if not members:
return await ctx.send(_("Missing members", ctx, self.bot.config))
role_row = await get_mute_role(ctx.guild.id)
if role_row is None:
return await ctx.send(
_(
"No mute role has been specified for this guild",
ctx,
self.bot.config,
)
)
for member in members:
await member.add_roles(
discord.Object(id=int(role_row.role_id)), reason=reason
)
await ctx.send("\N{THUMBS UP SIGN}")
@_mute.command(name="show", aliases=["role"])
async def _mute_show(
self,
ctx: ContextPlus,
):
role_row = await get_mute_role(ctx.guild.id)
if (
role_row is None
or (role := ctx.guild.get_role(int(role_row.role_id))) is None
):
return await ctx.send(
_(
"No mute role has been specified for this guild",
ctx,
self.bot.config,
)
)
muted_members = [m for m in ctx.guild.members if role in m.roles]
e = discord.Embed(
title=f"Role: {role.name} (ID: {role.id})", color=role.color
)
e.add_field(name="Total mute:", value=len(muted_members))
await ctx.send(embed=e)
@_mute.command(name="set", aliases=["define"])
async def _mute_set(self, ctx: ContextPlus, role: discord.Role):
role_row = await get_mute_role(ctx.guild.id)
if role_row is None:
await create_mute_role(ctx.guild.id, role.id)
else:
role_row.role_id = role.id # type: ignore
await role_row.save()
await ctx.send(
_("Mute role successfully defined", ctx, self.bot.config)
)
# =========================================================================
@command_extra(
name="tempmute",
deletable=True,
)
@commands.guild_only()
@checks.is_admin()
async def _tempmute(
self,
ctx: ContextPlus,
time,
members: discord.Member,
*,
reason: Optional[ReasonConverter],
):
_, _, _, _ = ctx, time, members, reason
# =========================================================================
@command_extra(
name="unmute",
deletable=True,
)
@commands.guild_only()
@checks.is_admin()
async def _unmute(
self,
ctx: ContextPlus,
members: commands.Greedy[discord.Member],
*,
reason: Optional[ReasonConverter],
):
if not members:
return await ctx.send(_("Missing members", ctx, self.bot.config))
role_row = await get_mute_role(ctx.guild.id)
if role_row is None:
return await ctx.send(
_(
"No mute role has been specified for this guild",
ctx,
self.bot.config,
)
)
for member in members:
await member.remove_roles(
discord.Object(id=int(role_row.role_id)), reason=reason
)
await ctx.send("\N{THUMBS UP SIGN}")

View file

@ -0,0 +1,3 @@
from .rules import *
from .warns import *
from .mutes import *

View file

@ -0,0 +1,46 @@
import tortoise
from tortoise import fields
class MuteRole(tortoise.Model):
id = fields.BigIntField(pk=True)
server_id = fields.BigIntField()
role_id = fields.BigIntField()
class Meta:
table = "mute_role"
def __str__(self):
return (
f"<MuteRole id={self.id} "
f"server_id={self.server_id} "
f"role_id={self.role_id}>"
)
__repr__ = __str__
class Mute(tortoise.Model):
id = fields.BigIntField(pk=True)
server_id = fields.BigIntField()
author_id = fields.BigIntField()
reason = fields.TextField(max_length=300)
member_id = fields.BigIntField()
created_at = fields.DatetimeField(auto_now_add=True)
expire_at = fields.DatetimeField(null=True)
class Meta:
table = "mutes"
def __str__(self):
return (
f"<Mute id={self.id} "
f"server_id={self.server_id} "
f"author_id={self.author_id} "
f"reason='{self.reason}' "
f"member_id={self.member_id} "
f"created_at={self.created_at} "
f"expire_at={self.expire_at}>"
)
__repr__ = __str__

View file

@ -0,0 +1,28 @@
import tortoise
from tortoise import fields
class Rule(tortoise.Model):
id = fields.BigIntField(pk=True)
server_id = fields.BigIntField()
author_id = fields.BigIntField()
rule_id = fields.IntField()
content = fields.TextField(max_length=300)
created_at = fields.DatetimeField(auto_now_add=True)
updated_at = fields.DatetimeField(auto_now_add=True)
class Meta:
table = "rules"
def __str__(self):
return (
f"<Rule id={self.id} "
f"server_id={self.server_id} "
f"author_id={self.author_id} "
f"rule_id={self.rule_id} "
f"content='{self.content}' "
f"created_at={self.created_at} "
f"updated_at={self.updated_at}>"
)
__repr__ = __str__

View file

@ -0,0 +1,24 @@
import tortoise
from tortoise import fields
class Warn(tortoise.Model):
id = fields.BigIntField(pk=True)
server_id = fields.BigIntField()
user_id = fields.BigIntField()
reason = fields.TextField(max_length=255)
created_at = fields.DatetimeField()
class Meta:
table = "warns"
def __str__(self):
return (
f"<Warn id={self.id} "
f"server_id={self.server_id} "
f"user_id={self.user_id} "
f"reason='{self.reason}' "
f"created_at={self.created_at}>"
)
__repr__ = __str__

View file

@ -0,0 +1,19 @@
from collections import namedtuple
from tuxbot.core.bot import Tux
from .network import Network
from .config import NetworkConfig, 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(Network(bot))

View file

@ -0,0 +1,22 @@
from typing import Dict
from structured_config import Structure, StrField
HAS_MODELS = False
class NetworkConfig(Structure):
ipinfoKey: str = StrField("")
geoapifyKey: str = StrField("")
extra: Dict[str, Dict] = {
"ipinfoKey": {
"type": str,
"description": "API Key for ipinfo.io (.iplocalise command)",
},
"geoapifyKey": {
"type": str,
"description": "API Key for geoapify.com (.iplocalise command)",
},
}

View file

@ -0,0 +1,55 @@
from discord.ext import commands
from discord.ext.commands import Context
def _(x):
return x
class IPConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
argument = argument.replace("http://", "").replace("https://", "")
argument = argument.rstrip("/")
if argument.startswith("`") and argument.endswith("`"):
argument = argument.lstrip("`").rstrip("`")
return argument.lower()
class DomainConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
if not argument.startswith("http"):
return f"http://{argument}"
return argument
class QueryTypeConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
return argument.lower()
class IPParamsConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
if not argument:
return None
params = {
"inet": "",
"map": "map" in argument.lower(),
}
if "4" in argument:
params["inet"] = "4"
elif "6" in argument:
params["inet"] = "6"
elif len(arg := argument.split(" ")) >= 2:
params["inet"] = arg[0]
return params
class ASConverter(commands.Converter):
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
return argument.lower().lstrip("as")

View file

@ -0,0 +1,29 @@
from discord.ext import commands
class NetworkException(commands.BadArgument):
pass
class RFC18(NetworkException):
pass
class InvalidIp(NetworkException):
pass
class InvalidDomain(NetworkException):
pass
class InvalidQueryType(NetworkException):
pass
class VersionNotFound(NetworkException):
pass
class InvalidAsn(NetworkException):
pass

View file

@ -0,0 +1,315 @@
import io
import socket
from typing import NoReturn, Optional, Union
import asyncio
import re
import ipinfo
import ipwhois
import pydig
import aiohttp
from ipinfo.exceptions import RequestQuotaExceededError
from ipwhois import Net
from ipwhois.asn import IPASN
from aiocache import cached, Cache
from aiocache.serializers import PickleSerializer
from tuxbot.cogs.Network.functions.exceptions import (
VersionNotFound,
RFC18,
InvalidIp,
InvalidQueryType,
InvalidAsn,
)
def _(x):
return x
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_ip(loop, ip: str, inet: Optional[dict]) -> str:
_inet: Union[socket.AddressFamily, int] = 0 # pylint: disable=no-member
if inet:
if inet["inet"] == "6":
_inet = socket.AF_INET6
elif inet["inet"] == "4":
_inet = socket.AF_INET
def _get_ip(_ip: str):
try:
return socket.getaddrinfo(_ip, None, _inet)[1][4][0]
except socket.gaierror as e:
raise VersionNotFound(
_(
"Unable to collect information on this in the given "
"version",
)
) from e
return await loop.run_in_executor(None, _get_ip, str(ip))
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_hostname(loop, ip: str) -> str:
def _get_hostname(_ip: str):
try:
return socket.gethostbyaddr(ip)[0]
except socket.herror:
return "N/A"
try:
return await asyncio.wait_for(
loop.run_in_executor(None, _get_hostname, str(ip)),
timeout=0.200,
)
# assuming that if the hostname isn't retrieved in first .3sec,
# it doesn't exists
except asyncio.exceptions.TimeoutError:
return "N/A"
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_ipwhois_result(loop, ip: str) -> Union[NoReturn, dict]:
def _get_ipwhois_result(_ip: str) -> Union[NoReturn, dict]:
try:
net = Net(ip)
obj = IPASN(net)
return obj.lookup()
except ipwhois.exceptions.ASNRegistryError:
return {}
except ipwhois.exceptions.IPDefinedError as e:
raise RFC18(
_(
"IP address {ip_address} is already defined as Private-Use"
" Networks via RFC 1918."
)
) from e
try:
return await asyncio.wait_for(
loop.run_in_executor(None, _get_ipwhois_result, str(ip)),
timeout=0.200,
)
except asyncio.exceptions.TimeoutError:
return {}
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_ipinfo_result(loop, apikey: str, ip: str) -> dict:
def _get_ipinfo_result(_ip: str) -> Union[NoReturn, dict]:
"""
Q. Why no getHandlerAsync ?
A. Use of this return "Unclosed client session" and "Unclosed connector"
"""
try:
handler = ipinfo.getHandler(apikey, request_options={"timeout": 7})
return (handler.getDetails(ip)).all
except RequestQuotaExceededError:
return {}
try:
return await asyncio.wait_for(
loop.run_in_executor(None, _get_ipinfo_result, str(ip)),
timeout=8,
)
except asyncio.exceptions.TimeoutError:
return {}
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_crimeflare_result(ip: str) -> Optional[str]:
try:
async with aiohttp.ClientSession() as cs:
async with cs.post(
"http://www.crimeflare.org:82/cgi-bin/cfsearch.cgi",
data=f"cfS={ip}",
timeout=aiohttp.ClientTimeout(total=21),
) as s:
result = re.search(r"(\d*\.\d*\.\d*\.\d*)", await s.text())
if result:
return result.group()
except (aiohttp.ClientError, asyncio.exceptions.TimeoutError):
pass
return None
def merge_ipinfo_ipwhois(ipinfo_result: dict, ipwhois_result: dict) -> dict:
output = {
"belongs": "N/A",
"rir": "N/A",
"region": "N/A",
"flag": "N/A",
"map": "N/A",
}
if ipinfo_result:
org = ipinfo_result.get("org", "N/A")
asn = org.split()[0] if len(org.split()) > 1 else "N/A"
output["belongs"] = f"[{org}](https://bgp.he.net/{asn})"
output["rir"] = f"```{ipwhois_result.get('asn_registry', 'N/A')}```"
output["region"] = (
f"```{ipinfo_result.get('city', 'N/A')} - "
f"{ipinfo_result.get('region', 'N/A')} "
f"({ipinfo_result.get('country', 'N/A')})```"
)
output[
"flag"
] = f"https://flagcdn.com/144x108/{ipinfo_result['country'].lower()}.png"
output["map"] = ipinfo_result["loc"]
elif ipwhois_result:
org = ipwhois_result.get("asn_description", "N/A")
asn = ipwhois_result.get("asn", "N/A")
asn_country = ipwhois_result.get("asn_country_code", "N/A")
output["belongs"] = f"{org} ([AS{asn}](https://bgp.he.net/{asn}))"
output["rir"] = f"```{ipwhois_result['asn_registry']}```"
output["region"] = f"```{asn_country}```"
output[
"flag"
] = f"https://flagcdn.com/144x108/{asn_country.lower()}.png"
return output
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_map_bytes(apikey: str, latlon: str) -> Optional[io.BytesIO]:
if latlon == "N/A":
return None
url = (
"https://maps.geoapify.com/v1/staticmap"
"?style=osm-carto"
"&width=333"
"&height=250"
"&center=lonlat:{lonlat}"
"&zoom=12"
"&marker=lonlat:{lonlat};color:%23ff0000;size:small"
"&apiKey={apikey}"
)
lonlat = ",".join(latlon.split(",")[::-1])
url = url.format(lonlat=lonlat, apikey=apikey)
try:
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=5)
) as cs:
async with cs.get(url) as s:
if s.status != 200:
return None
return io.BytesIO(await s.read())
except asyncio.exceptions.TimeoutError:
from ..images.load_fail import value
return io.BytesIO(value)
@cached(
ttl=24 * 3600,
serializer=PickleSerializer(),
cache=Cache.MEMORY,
namespace="network",
)
async def get_pydig_result(
loop, domain: str, query_type: str, dnssec: Union[str, bool]
) -> list:
additional_args = [] if dnssec is False else ["+dnssec"]
def _get_pydig_result(_domain: str) -> Union[NoReturn, dict]:
resolver = pydig.Resolver(
nameservers=[
"80.67.169.40",
"80.67.169.12",
],
additional_args=additional_args,
)
return resolver.query(_domain, query_type)
try:
return await asyncio.wait_for(
loop.run_in_executor(None, _get_pydig_result, str(domain)),
timeout=0.500,
)
except asyncio.exceptions.TimeoutError:
return []
def check_ip_version_or_raise(
version: Optional[dict],
) -> Union[bool, NoReturn]:
if version is None or version["inet"] in ("4", "6", ""):
return True
raise InvalidIp(_("Invalid ip version"))
def check_query_type_or_raise(query_type: str) -> Union[bool, NoReturn]:
query_types = (
"a",
"aaaa",
"cname",
"ns",
"ds",
"dnskey",
"soa",
"txt",
"ptr",
"mx",
)
if query_type in query_types:
return True
raise InvalidQueryType(
_(
"Supported queries : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
)
)
def check_asn_or_raise(asn: str) -> Union[bool, NoReturn]:
if asn.isdigit() and int(asn) < 4_294_967_295:
return True
raise InvalidAsn(_("Invalid ASN provided"))

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