Compare commits

..

215 commits

Author SHA1 Message Date
616a067bc2 feat(commands:misc|Dev): add random tests things 2021-05-29 02:19:50 +02:00
3240c61b20 update(commands:info|Utils): update git url 2021-05-17 00:27:21 +02:00
f06bfd7e24 update(commands:mute|Mod): add reason as optional 2021-05-17 00:20:56 +02:00
7b50af0207 feat(commands:mute|Mod): add mute/unmute command 2021-05-17 00:08:16 +02:00
2978706264 fix(doc): update version 2021-05-16 23:59:16 +02:00
ba53228d44 update(core): change to >=3.8 2021-05-16 23:21:27 +02:00
b75e5b8a8e update(commands:iplocalise|Network): update fail image 2021-05-16 18:09:51 +02:00
3d8ea556d5 fix(commands:iplocalise|Network): fix map timeout 2021-05-16 18:03:33 +02:00
82b8fb9814 feat(commands:update|Admin): add update command 2021-05-16 17:40:42 +02:00
fd0600b75d fix(commands:iplocalise|Network): close TUXBOT-BOT-4N 2021-05-16 17:28:49 +02:00
09e69166ad fix(commands:iplocalise|Network): close TUXBOT-BOT-4T, close TUXBOT-BOT-4S 2021-05-16 17:07:26 +02:00
f8f56add97 fix(commands:iplocalise|Network): possible fix for timeout on map? 2021-05-16 15:46:52 +02:00
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
614434aebf feat(commands:*|Tag): feat tag system 2021-05-16 00:32:14 +02:00
af3d742f68 fix typo 2021-05-15 21:44:46 +02:00
4c72f07e8e feat(commands:rule>update|Mod): feat way to update rule messages 2021-05-15 21:36:30 +02:00
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
067e29a96a fix(commands:rule>list|Mod): fix when no rules 2021-05-13 23:14:13 +02:00
8f62c2c4a1 feat(i18n:rule|Mod): feat rule command translations 2021-05-13 22:51:16 +02:00
ad443c9c48 feat(commands:rule|Mod): feat rule command 2021-05-13 22:19:27 +02:00
4e3fbd7f4d feat(commands:*|Mod): first commit 2021-05-13 16:45:49 +02:00
4678be191d update(deps): update deps versions 2021-05-13 16:45:04 +02:00
f00ff8d345 fix(core:i18n): fix crash for DMs 2021-05-13 16:44:18 +02:00
ba21cf859b fix(core): fix dependencies 2021-04-25 23:38:13 +02:00
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
fcc23d87df update(commands:down?|Network): change api to improve results 2021-04-25 18:48:21 +02:00
56e45b52b5 rage, part2 (hotfix from prod) 2021-04-25 01:22:39 +02:00
96cfa17d2e rage 2021-04-25 00:59:57 +02:00
3c5741e6c5 fix(commands:peeringdb|Network): totally remove cache for automated requests 2021-04-25 00:40:46 +02:00
1693857864 fix(commands:peeringdb|Network): possible false positiv 2021-04-25 00:14:38 +02:00
9362558a2e fix(commands:info|Utils): fetch files only in ./tuxbot 2021-04-24 23:13:53 +02:00
f7f5232e21 fix(commands:iplocalise|Network): prevent discord to not include :: to links 2021-04-24 23:01:10 +02:00
32b6de0d0f fix(commands:iplocalise|Network): prevent discord to not include :: to links 2021-04-24 22:56:10 +02:00
2a00d93023 feat(commands:iplocalise|Network): add way to ask for map 2021-04-24 22:34:53 +02:00
b9f6c6cb0a fix(commands:peeringdb|Network): fix false results 2021-04-23 23:47:43 +02:00
5b7c905ac8 fix(commands:peeringdb|Network): fix timeout 2021-04-23 01:18:17 +02:00
2e7934148e feat(commands:peeringdb|Network): add peeringdb command 2021-04-23 00:47:58 +02:00
2afd3af540 fix(core): fix dependencies 2021-04-22 18:13:20 +02:00
c6c61a0886 update(commands:*|Network): speed optimisation 2021-04-22 18:11:55 +02:00
c6a5dc4ad6 feat(commands:isdown|Network): add isitdown api 2021-04-22 15:31:01 +02:00
1f367fd2df fix(commands:getheaders|Network): prevent fails for missing [] on v6 2021-04-22 14:54:46 +02:00
9172331927 fix(commands:info|Utils): undo testings changes 2021-04-22 00:22:09 +02:00
561f56ca27 update(commands:iplocalise,getheaders|Network): speed optimisation 2021-04-22 00:16:37 +02:00
eca6e7b268 ram optimization 2021-04-21 18:28:50 +02:00
4a508b1851 delete(commands:*|Crypto): remove unused cog 2021-04-21 18:00:43 +02:00
e63e939d77 feat(commands:cloudflare|Network): add workaround to iplocalise for cloudflare 2021-04-21 17:59:43 +02:00
751c82909d style 2021-04-20 17:30:05 +02:00
22c5ee57d4 fix(commands:getheaders|Network): fix timeout 2021-04-20 17:26:12 +02:00
64fba7fec6 style 2021-04-20 17:12:38 +02:00
7f9c202cc6 style 2021-04-20 15:51:03 +02:00
1b7f153ec8 update(core): migrate to py3.10 & dpy2.0 2021-04-20 15:43:20 +02:00
0eca877c1c fix(commands:dig|Network): fix async 2021-04-20 15:42:59 +02:00
540dfd616a april fool 2021-03-31 21:57:37 +02:00
1a10f64345 april fool 2021-03-31 18:09:10 +02:00
f0dc682047 style 2021-03-31 18:08:53 +02:00
3525b9aa4b fix(commands:iplocalise|Network): remove crashing regex 2021-03-26 18:41:21 +01:00
0ecc97518f fix(commands:iplocalise|Network): fix process crashing regex 2021-03-05 00:05:24 +01:00
78a5ac9939 fix(commands:iplocalise|Network): change regex for domain validation 2021-03-02 19:00:08 +01:00
34e32fdf68 fix(commands:ralgo|Crypto): set ralgo as async 2021-03-01 14:11:18 +01:00
edfeadb872 feat(commands:ralgo|Crypto): add ralgo api 2021-02-24 00:53:05 +01:00
83723380e9 fix(commands|Network>iplocalise): improve domain regex 2021-02-16 19:38:42 +01:00
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
c7ddba1bae feat(host): start docker addon 2021-02-11 23:18:12 +01:00
7423b40337 fix(core): close TUXBOT-BOT-11, close TUXBOT-BOT-19 2021-02-11 18:40:00 +01:00
fa98d67276 style 2021-02-11 18:27:26 +01:00
a0e67c1627 fix(commands|Network>iplocalise): close TUXBOT-BOT-10, close TUXBOT-BOT-16 2021-02-11 18:25:32 +01:00
1e86abdf01 fix(core): close TUXBOT-BOT-Z 2021-02-11 18:11:19 +01:00
c566f775cd fix(deps): fix conflict version 2021-02-11 17:51:46 +01:00
fae56745bf fix(commands|Network>iplocalise): quickfix when no org is returned 2021-02-11 16:15:43 +01:00
f7176d917c fix(core): fix crash on DM message 2021-01-30 16:43:42 +01:00
434021ecb9 feat(commands|Network>ping): feat ping command 2021-01-30 16:43:17 +01:00
0c308727d2 fix(logs): exceptions must derive from BaseException 2021-01-30 14:21:20 +01:00
5991ebfaf2 fix(readme|badges): update issue badge 2021-01-30 13:48:52 +01:00
45d4aa1dc5 fix(commands|Network>iplocalise): close #6 2021-01-30 13:44:34 +01:00
0687ee3f06 fix(import): add missing requirement 2021-01-28 10:39:54 +01:00
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
da277e0d66 fix(import): add missing requirement 2021-01-27 15:55:57 +01:00
aeced979df fix(import): add missing requirement 2021-01-27 15:54:21 +01:00
bb87d77e33 fix(import): add missing file 2021-01-27 15:52:13 +01:00
975a3b3d14 feat(doc): add missing instructions about postgresql 2021-01-27 15:51:10 +01:00
33e09a9e02 feat(doc): add missing instructions about postgresql 2021-01-27 15:50:52 +01:00
c5c13506d7 fix(commands|Network>getheaders): fix timeout error 2021-01-27 15:14:53 +01:00
647cc4bd64 feat(core|logs>sentry): feat sentry error handler 2021-01-27 15:14:05 +01:00
554c0b52d5 feat(commands|Network>dig): feat dig command 2021-01-26 17:11:30 +01:00
1d37dc1961 improve(core|replacement): improve not to show replacements 2021-01-26 17:10:31 +01:00
f88adec45b fix(commands|Utils>info): i18n 2021-01-26 15:45:44 +01:00
dd09a53c0e improve(commands|Network>iplocalise): rewrite 2021-01-26 15:43:16 +01:00
d66bec65ae feat(commands|Network>getheaders): feat getheaders command 2021-01-26 15:24:10 +01:00
fbb61c247d feat(secure): add ip to the list of things to not to display 2021-01-26 11:59:36 +01:00
d3ab384de0 improve(commands|Logs>socketstats): format output into an embed 2021-01-26 11:37:52 +01:00
74307c755c fix(commands|Network>iplocalise): fix issues in 7962205d16 2021-01-26 10:21:39 +01:00
18310f17a0 fix(PYL-W1401): Anomalous backslash detected
fix(PYL-W0613): Function contains unused argument
2021-01-26 09:43:25 +01:00
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
fa3069244d feat(commands|Utils>info): feat cpu usage 2021-01-25 14:52:30 +01:00
1fb3e035bd feat(commands|Polls>propose): feat poll proposition + acceptation 2021-01-22 10:22:39 +01:00
37bbf0368e style 2021-01-21 16:13:46 +01:00
01e0e5e27e style(deepsource): fix some analysis fails 2021-01-21 16:11:29 +01:00
5d585bf218 feat(i18n): add translations for info command 2021-01-20 17:29:16 +01:00
30cc3ecad2 feat(commands|Polls>propose): start dev for proposal 2021-01-20 17:28:37 +01:00
c3660aab8a style(import): change to absolute import for Tux 2021-01-20 15:08:16 +01:00
0eaa53ffd5 feat(commands|Polls>create): feat poll creation 2021-01-20 14:51:04 +01:00
f00f0fd4c0 feat(commands|Custom>alias): finish alias interpretation 2021-01-19 16:05:20 +01:00
1681c5abf5 Merge branch 'master' of https://github.com/Rom1-J/tuxbot-bot 2021-01-19 15:11:13 +01:00
6757ce2ccc feat(commands|Custom>alias;locale): finish translations issues 2021-01-19 15:08:55 +01:00
573ce3fb18 feat(commands|Custom>alias;locale): translations 2021-01-19 15:03:05 +01:00
72fabf89b9 feat(commands|Custom>alias;locale): add alias registering and locale as custom for each user 2021-01-19 14:30:25 +01:00
98b82e680e Update README.rst
feat(setup): add updater to setup
2021-01-19 11:40:49 +01:00
e5c3f1b8de Update README.rst 2021-01-19 11:16:50 +01:00
e537a59d8d
Update README.rst
Add wakatime badge
2020-12-08 15:28:13 +01:00
834f071332 fix(PYL-W1401): Anomalous backslash detected 2020-11-12 00:07:51 +01:00
cfd59def74 feat(commands:polls|Polls): start dev of poll command 2020-11-12 00:03:01 +01:00
b4194dcadf feat(commands:locale|Utils): add translations for source info and credits 2020-11-11 02:46:48 +01:00
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
5a65fe1a6c feat(commands|Utils): add source command 2020-11-11 01:27:51 +01:00
3587a8f8a4 feat(commands|Utils): add credits and invite commands 2020-11-11 01:05:36 +01:00
71576f48e4 fix(discord): add itents 2020-11-09 01:27:19 +01:00
d6e9cd6512 feat(commands|Utils): add info command 2020-11-09 01:18:55 +01:00
7d588b2dbc remove(support): drop testing support for python 3.7 2020-11-08 01:09:40 +01:00
e38823e5be feat(database): add models loader in core 2020-11-08 01:07:27 +01:00
71335de878 feat(logs): rewrite Logs cog 2020-10-22 00:00:48 +02:00
bdd77d1841 fix(PYL-C0201): 2020-10-21 00:26:40 +02:00
d7a2330fb6 fix(BAN-B607): 2020-10-21 00:11:53 +02:00
969ff8c351 fix(PTC-W0031): 2020-10-21 00:09:47 +02:00
4751a1b518 fix(deepsource): remove black from deepsource conf 2020-10-21 00:02:02 +02:00
008ae76aca fix(PYL-R1705): 2020-10-20 23:56:02 +02:00
bef9060b78 fix(PTC-W0034): 2020-10-20 23:53:24 +02:00
533ca6e3e7 fix(PYL-W1113): 2020-10-20 23:52:05 +02:00
42e2d04a9e fix(make): add pylint to absolute path from venv 2020-10-20 23:49:46 +02:00
7d67b8d581 update(deps): move to dpy 1.5.1 2020-10-20 23:45:51 +02:00
6a926d717c Merge branch 'master' of https://github.com/Rom1-J/tuxbot-bot into master 2020-10-20 23:43:24 +02:00
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
10b7e4039c Merge branch 'master' into v3 2020-10-20 23:21:05 +02:00
179c84b45a feat(instances): done -L active since 2020-10-19 22:17:19 +02:00
cc5df29e71 remove(debug): delete some console.log 2020-10-19 21:50:18 +02:00
421ecbf6cb fix(config): change set_for to set_for_key 2020-10-19 21:44:29 +02:00
554ec46413 feat(lang): done lang switcher 2020-10-19 15:04:10 +02:00
888a7924be update(extra): remove yaml for token replacement 2020-10-19 01:37:12 +02:00
1be4af8405 fix(config): update old config code 2020-10-19 00:53:26 +02:00
3ca1a42cad fix(requirements): fix black module version 2020-10-19 00:22:58 +02:00
cebb1b0123 fix(linting): set the not to 10/10 on pylint 2020-10-19 00:20:58 +02:00
e0788137ff workingOn(conf): - 2020-09-02 00:08:06 +02:00
d68d54be44 update(requirements): clean up non useful requirement precision 2020-08-29 01:01:34 +02:00
331599eb38 update(launcher): improve launcher UI & shutdown handling 2020-08-28 23:05:04 +02:00
9a0786af7c update(main): improve launch UI 2020-08-28 01:06:57 +02:00
c1e253689d update(setup): improve setup UI 2020-08-26 17:15:38 +02:00
Romain
951784718b update (cogs.basics>ping): optimisation
Remove unused parts
2020-08-24 21:08:55 +02:00
783f7507f0 Readme update 2020-08-24 13:25:02 -04:00
4b28ff0aea Fucking ecriture inclusive 2020-08-24 12:10:33 -04:00
f155c7c27e Unused command text delete 2020-08-24 12:09:56 -04:00
fa1ac02648 Help msg enchancement 2020-08-24 12:08:45 -04:00
5c51c15805 gitignore update 2020-08-24 12:02:10 -04:00
fbda0e9414 add hardcoded alert for the filter message func 2020-08-24 12:01:16 -04:00
6b433970fe New URL of Discord for ping 2020-08-24 11:59:01 -04:00
032a49b08f remove cringe messages in afk 2020-08-24 11:57:51 -04:00
f181f58735 Remove dead images for search embeds 2020-08-24 11:56:46 -04:00
f7bfc25793 remove useless prints in utility cos 2020-08-24 11:54:06 -04:00
3dd17f44a1 shroute bug fix + enchancement 2020-08-24 11:52:45 -04:00
7e79ac7fab No hello file 2020-08-23 20:20:39 -04:00
3e3f6d42d6 Requirements update 2020-08-23 20:18:57 -04:00
1e6f0b6eb0 repair dumb merge 2020-08-19 17:07:50 +02:00
1f7da4fd14 improve avatar 2020-08-19 17:00:45 +02:00
db7dfd5c58 undo delete message when ban 2020-08-19 16:59:10 +02:00
f79074a97d improve info 2020-08-19 16:56:52 +02:00
6400d1da71 shroute 2020-08-19 10:35:32 -04:00
bb6b25c5d9 shroute cmd 2020-08-19 10:34:25 -04:00
d3683ed10d Shroute 2020-08-19 10:33:48 -04:00
5cc364480a DIG 2020-08-18 16:01:34 -04:00
17b3e658fc remove debug things 2020-08-18 21:51:51 +02:00
175174757b improve getheaders 2020-08-18 21:50:23 +02:00
ecdde52ca3 iplocalise bug fix + bgp.he.net link 2020-08-18 15:39:23 -04:00
1f88499d44 feat(i18n): finish persisting data for i18n 2020-06-11 19:43:00 +02:00
5482429cba feat(i18n): finish i18n class, todo: persist data in DB 2020-06-11 01:06:30 +02:00
85506c8db4 'cause developper is doing SHIT 2020-06-08 00:49:41 +02:00
a42eb58be8 update(license): change again to a more more friendly license 2020-06-08 00:46:09 +02:00
4efb707257 update(license): change to a more friendly license 2020-06-08 00:41:06 +02:00
8e8a4b899e feat(license): add libre license 2020-06-08 00:09:41 +02:00
85da8a34ab fix(doc): fix broken link in readme 2020-06-07 18:45:50 +02:00
a73d408462 update(doc|readme|installing): change installing steps 2020-06-07 18:29:55 +02:00
5e8868b660 feat(style): add black 2020-06-07 17:41:34 +02:00
9869312ee8 refactor(all): add black to all code 2020-06-06 18:51:47 +02:00
cdb891d435 feat(doc): add docstrings 2020-06-06 02:00:16 +02:00
bf6d961658 feat(i18n): start skeleton class for translations 2020-06-06 01:45:24 +02:00
dbf7f3ce8e fix(typo): fix typo in readme 2020-06-05 22:48:55 +02:00
14f995550a update(doc): add more explication in readme file 2020-06-05 22:43:59 +02:00
fbafd03ea9 fix(setup): prevent case when user fill nothinf in multiple get_multiple 2020-06-05 00:36:20 +02:00
7c75b0efad fix(gitignore): add workspace.xml to gitignore 2020-06-05 00:31:15 +02:00
815709d68b tldr: core, warn's skeleton 2020-06-05 00:29:14 +02:00
b5b7f0c7ef feat(config): add Config object 2020-06-04 19:16:51 +02:00
ec68280519 change(naming_convention): remove unuseful prefix for data relatives functions 2020-06-04 16:36:22 +02:00
50562059f9 feat(doc): add docstrings 2020-06-04 00:46:53 +02:00
33fa6b7f1f fix(gitignore): add packages to gitignore 2020-06-04 00:17:36 +02:00
335397554f feat(botCore|launcher): add features to the core launcher 2020-06-04 00:14:50 +02:00
cbe250f137 feat(botCore): start core template 2020-06-03 19:41:30 +02:00
79ca4f95d6 feat(setup): add configurator step 2020-06-03 18:24:38 +02:00
9020fe7201 fix(doc): fix broken link in readme 2020-06-03 01:12:19 +02:00
9f8765e0a6 feat(doc): add readme file 2020-06-03 01:10:47 +02:00
078dc075f2 refactor(all): start from new
feat(doc): add readme file
2020-06-03 01:07:43 +02:00
28d1d71c5a feat(cli|ui): add ~~beautiful~~ useless UI when launching bot 2020-06-02 01:47:24 +02:00
2e76379c87 feat(commands|Network): add iplocalise command 2020-05-31 22:49:04 +02:00
45d61fc71d feat(automation): add possibility to delete command output 2020-05-27 00:58:53 +02:00
f9c31f4017 feat(doc): add github issue template 2020-05-24 01:18:05 +02:00
04645ec639 first commit 2020-05-24 01:16:08 +02:00
534a78e447 Iplocalise update 2020-04-27 11:21:56 -04:00
083d14e056 fix(ci|show|render): fix inline issue 2020-04-17 14:32:25 +02:00
daed469994 fix(command): fix help spam 2020-04-08 00:26:26 +02:00
68ca0cb2fc fix(ping): quick fix for ping result 2020-04-07 16:00:39 +02:00
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
fc06756363 feat(monitoring): add http server for monitoring 2019-12-29 22:19:26 +01:00
425ff79c8d add(jishaku) 2019-12-29 22:15:32 +01:00
57ea780f2c add(doc): add issue template (better with git add . ...) 2019-09-01 16:19:03 +02:00
78026ee88e add(doc): add issue template 2019-09-01 16:16:25 +02:00
225 changed files with 9463 additions and 4786 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

View file

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

158
.gitignore vendored
View file

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

View file

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

View file

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

9
.idea/discord.xml Normal file
View file

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

View file

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

View file

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

7
.idea/misc.xml Normal file
View file

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

8
.idea/modules.xml Normal file
View file

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

11
.idea/statistic.xml Normal file
View file

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

21
.idea/tuxbot_bot.iml Normal file
View file

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

11
.idea/vcs.xml Normal file
View file

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

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

22
.pylintrc Normal file
View file

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

1098
LICENSE Executable file → Normal file

File diff suppressed because it is too large Load diff

86
Makefile Normal file
View file

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

View file

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

141
README.rst Normal file
View file

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

250
bot.py
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

3
dev.requirements.txt Normal file
View file

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

View file

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

34
local.yml Normal file
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,13 +0,0 @@
requests
humanize
git+https://github.com/Rapptz/discord.py@master
jishaku
gitpython
orm
asyncpg
psycopg2
configparser
psutil
tcp_latency
yarl
pillow

51
setup.cfg Normal file
View file

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

5
setup.py Normal file
View file

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

2
todo
View file

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

26
tuxbot/__init__.py Normal file
View file

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

28
tuxbot/__main__.py Normal file
View file

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

243
tuxbot/__run__.py Normal file
View file

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

View file

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

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

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

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: 2021-01-19 14:42+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

View file

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

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

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

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

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: 2021-01-25 14:36+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

View file

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

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

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

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

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