Compare commits

...

160 commits

Author SHA1 Message Date
Romain J 834f071332 fix(PYL-W1401): Anomalous backslash detected 2020-11-12 00:07:51 +01:00
Romain J cfd59def74 feat(commands:polls|Polls): start dev of poll command 2020-11-12 00:03:01 +01:00
Romain J b4194dcadf feat(commands:locale|Utils): add translations for source info and credits 2020-11-11 02:46:48 +01:00
Romain J 42d7cad0e5 fix(commands|Utils>source): stupid of me, the +3 was 'cause of a diff in local and github 2020-11-11 01:29:53 +01:00
Romain J 5a65fe1a6c feat(commands|Utils): add source command 2020-11-11 01:27:51 +01:00
Romain J 3587a8f8a4 feat(commands|Utils): add credits and invite commands 2020-11-11 01:05:36 +01:00
Romain J 71576f48e4 fix(discord): add itents 2020-11-09 01:27:19 +01:00
Romain J d6e9cd6512 feat(commands|Utils): add info command 2020-11-09 01:18:55 +01:00
Romain J 7d588b2dbc remove(support): drop testing support for python 3.7 2020-11-08 01:09:40 +01:00
Romain J e38823e5be feat(database): add models loader in core 2020-11-08 01:07:27 +01:00
Romain J 71335de878 feat(logs): rewrite Logs cog 2020-10-22 00:00:48 +02:00
Romain J bdd77d1841 fix(PYL-C0201): 2020-10-21 00:26:40 +02:00
Romain J d7a2330fb6 fix(BAN-B607): 2020-10-21 00:11:53 +02:00
Romain J 969ff8c351 fix(PTC-W0031): 2020-10-21 00:09:47 +02:00
Romain J 4751a1b518 fix(deepsource): remove black from deepsource conf 2020-10-21 00:02:02 +02:00
Romain J 008ae76aca fix(PYL-R1705): 2020-10-20 23:56:02 +02:00
Romain J bef9060b78 fix(PTC-W0034): 2020-10-20 23:53:24 +02:00
Romain J 533ca6e3e7 fix(PYL-W1113): 2020-10-20 23:52:05 +02:00
Romain J 42e2d04a9e fix(make): add pylint to absolute path from venv 2020-10-20 23:49:46 +02:00
Romain J 7d67b8d581 update(deps): move to dpy 1.5.1 2020-10-20 23:45:51 +02:00
Romain J 6a926d717c Merge branch 'master' of https://github.com/Rom1-J/tuxbot-bot into master 2020-10-20 23:43:24 +02:00
Romain J bdc7afb1ef feat(readle): add black tag 2020-10-20 23:43:20 +02:00
DeepSource Bot 260ef9f41c Add .deepsource.toml 2020-10-20 21:39:32 +00:00
Romain J 10b7e4039c Merge branch 'master' into v3 2020-10-20 23:21:05 +02:00
Romain J 179c84b45a feat(instances): done -L active since 2020-10-19 22:17:19 +02:00
Romain J cc5df29e71 remove(debug): delete some console.log 2020-10-19 21:50:18 +02:00
Romain J 421ecbf6cb fix(config): change set_for to set_for_key 2020-10-19 21:44:29 +02:00
Romain J 554ec46413 feat(lang): done lang switcher 2020-10-19 15:04:10 +02:00
Romain J 888a7924be update(extra): remove yaml for token replacement 2020-10-19 01:37:12 +02:00
Romain J 1be4af8405 fix(config): update old config code 2020-10-19 00:53:26 +02:00
Romain J 3ca1a42cad fix(requirements): fix black module version 2020-10-19 00:22:58 +02:00
Romain J cebb1b0123 fix(linting): set the not to 10/10 on pylint 2020-10-19 00:20:58 +02:00
Romain J e0788137ff workingOn(conf): - 2020-09-02 00:08:06 +02:00
Romain J d68d54be44 update(requirements): clean up non useful requirement precision 2020-08-29 01:01:34 +02:00
Romain J 331599eb38 update(launcher): improve launcher UI & shutdown handling 2020-08-28 23:05:04 +02:00
Romain J 9a0786af7c update(main): improve launch UI 2020-08-28 01:06:57 +02:00
Romain J c1e253689d update(setup): improve setup UI 2020-08-26 17:15:38 +02:00
Romain J 1f88499d44 feat(i18n): finish persisting data for i18n 2020-06-11 19:43:00 +02:00
Romain J 5482429cba feat(i18n): finish i18n class, todo: persist data in DB 2020-06-11 01:06:30 +02:00
Mael G. 85506c8db4 'cause developper is doing SHIT 2020-06-08 00:49:41 +02:00
Romain J a42eb58be8 update(license): change again to a more more friendly license 2020-06-08 00:46:09 +02:00
Romain J 4efb707257 update(license): change to a more friendly license 2020-06-08 00:41:06 +02:00
Romain J 8e8a4b899e feat(license): add libre license 2020-06-08 00:09:41 +02:00
Romain J 85da8a34ab fix(doc): fix broken link in readme 2020-06-07 18:45:50 +02:00
Romain J a73d408462 update(doc|readme|installing): change installing steps 2020-06-07 18:29:55 +02:00
Romain J 5e8868b660 feat(style): add black 2020-06-07 17:41:34 +02:00
Romain J 9869312ee8 refactor(all): add black to all code 2020-06-06 18:51:47 +02:00
Romain J cdb891d435 feat(doc): add docstrings 2020-06-06 02:00:16 +02:00
Romain J bf6d961658 feat(i18n): start skeleton class for translations 2020-06-06 01:45:24 +02:00
Romain J dbf7f3ce8e fix(typo): fix typo in readme 2020-06-05 22:48:55 +02:00
Romain J 14f995550a update(doc): add more explication in readme file 2020-06-05 22:43:59 +02:00
Romain J fbafd03ea9 fix(setup): prevent case when user fill nothinf in multiple get_multiple 2020-06-05 00:36:20 +02:00
Romain J 7c75b0efad fix(gitignore): add workspace.xml to gitignore 2020-06-05 00:31:15 +02:00
Romain J 815709d68b tldr: core, warn's skeleton 2020-06-05 00:29:14 +02:00
Romain J b5b7f0c7ef feat(config): add Config object 2020-06-04 19:16:51 +02:00
Romain J ec68280519 change(naming_convention): remove unuseful prefix for data relatives functions 2020-06-04 16:36:22 +02:00
Romain J 50562059f9 feat(doc): add docstrings 2020-06-04 00:46:53 +02:00
Romain J 33fa6b7f1f fix(gitignore): add packages to gitignore 2020-06-04 00:17:36 +02:00
Romain J 335397554f feat(botCore|launcher): add features to the core launcher 2020-06-04 00:14:50 +02:00
Romain J cbe250f137 feat(botCore): start core template 2020-06-03 19:41:30 +02:00
Romain J 79ca4f95d6 feat(setup): add configurator step 2020-06-03 18:24:38 +02:00
Romain J 9020fe7201 fix(doc): fix broken link in readme 2020-06-03 01:12:19 +02:00
Romain J 9f8765e0a6 feat(doc): add readme file 2020-06-03 01:10:47 +02:00
Romain J 078dc075f2 refactor(all): start from new
feat(doc): add readme file
2020-06-03 01:07:43 +02:00
Romain J 28d1d71c5a feat(cli|ui): add ~~beautiful~~ useless UI when launching bot 2020-06-02 01:47:24 +02:00
Romain J 2e76379c87 feat(commands|Network): add iplocalise command 2020-05-31 22:49:04 +02:00
Romain J 45d61fc71d feat(automation): add possibility to delete command output 2020-05-27 00:58:53 +02:00
Romain J f9c31f4017 feat(doc): add github issue template 2020-05-24 01:18:05 +02:00
Romain J 04645ec639 first commit 2020-05-24 01:16:08 +02:00
Romain J 7eac932a4e 🤔 2020-04-01 16:38:34 +02:00
Romain J 833884bcb2 🤔 2020-04-01 16:37:03 +02:00
Romain J cce7bb4093 2020-02-04 18:47:11 +01:00
Romain J be1e6d24e4 breaking change !
update(database): change database ORM

todo: update Admin, Poll and User cogs
2020-01-15 22:56:54 +01:00
Romain J 96618fa502 fix(token): fix all possible flaw of leaked token 2020-01-15 21:40:12 +01:00
Romain J caa98c44f4 update(image|blank_credit_card): add gnous logo 2020-01-15 19:03:03 +01:00
Romain J f03a54ca29 feat(command|Useful|cb): add cooldown 2020-01-15 02:00:57 +01:00
Romain J ad24e4ba86 feat(command|Useful|cb): add image generator for credit cards 2020-01-15 01:23:53 +01:00
Romain J b7ed8ffae7 some minor changes in few things 2020-01-15 00:08:53 +01:00
Romain J d187177908 feat(texts): add more texts 2020-01-12 20:43:05 +00:00
Romain J de4c8c549a texts(_help): add more text 2020-01-09 13:32:33 +01:00
Romain J a781e09bde feat(command|help): add usage and short desc 2020-01-05 19:49:02 +01:00
Romain J d9221002d2 fix(command|help): fix footer embed 2020-01-05 01:30:40 +01:00
Romain J 1bd73601f5 versionning: set version to release candidate 1 2020-01-05 01:03:34 +01:00
Romain J 248228408d feat(command|help): finish help command 2020-01-05 01:01:06 +01:00
Romain J 7b1fd7b463 refactor(cogs): prepare the env for new help command 2020-01-04 21:16:37 +01:00
Romain J 07e683e7cb Merge branch 'rewrite' of https://git.gnous.eu/gnouseu/tuxbot-bot into rewrite 2020-01-04 02:47:06 +01:00
Romain J fdf220cdfa refactor(cogs): prepare the env for help command 2020-01-04 02:46:12 +01:00
Romain J 7e8f0bbf12 fix(prefix): fix default prefix not working 2020-01-02 16:49:22 +01:00
Romain J c71c976111 change(name): set cluser_ as fallback_ 2020-01-01 23:44:46 +01:00
Romain J 6cd66ba407 change(refresh): set ping refresh to 2min 2020-01-01 20:40:17 +01:00
Romain J 92a0c257ca change(prefix): set default prefix to cluster name 2020-01-01 19:38:58 +01:00
Romain J ff878711bf remove(venv): remove venv files 2020-01-01 20:31:30 +01:00
Romain J 6d078e829d feat(HA): add cluster communication system 2019-12-31 00:52:17 +01:00
Romain J 6c18c3213e feat(config): add config file example 2019-12-30 15:33:02 +01:00
Romain J d94775e0e6 start development of alias command 2019-12-30 00:48:11 +01:00
Romain J 64b092dff2 feat(monitoring): add http server for monitoring 2019-12-29 23:18:10 +01:00
Romain J c442fd55fe feat(monitoring): add http server for monitoring 2019-12-29 21:38:59 +01:00
Romain J 97980e96d1 feat(command|prefix): add prefix commands (new, del, list)
known issue: `prefix list` return weird result when there is no custom prefix
2019-12-17 22:41:54 +01:00
Romain J d9427d1863 update(command|poll): first release 2019-12-16 23:06:25 +01:00
Romain J 348a78e4b3 update(git|gitignore): add venv to gitignore 2019-12-16 21:41:10 +01:00
Romain J d5afdcc60b update(command|info): add git to link's field 2019-12-16 19:36:55 +01:00
Romain J e7d4a9ee43 fix(logging): fix bot-log sending events 2019-12-16 18:19:32 +01:00
Romain J f42b2194cd tldr: refactoring 2019-12-16 18:12:10 +01:00
Romain J ba1122c07b fix(DM error): fix AttributeError: 'NoneType' on sending DM to the bot 2019-10-26 23:26:00 +02:00
Romain J c6114709ee flemme 2019-10-26 23:19:47 +02:00
Romain J fdc9c9196d add providers 2019-10-14 00:12:47 +02:00
Romain J d00aadd82f refactor(command/database|sondage): update poll model 2019-10-09 00:14:43 +02:00
Romain J 25f5c5e1f6 feat(command|sondage): add charts 2019-10-08 18:54:05 +02:00
Romain J ad4fb2fa89 refactor(command|sondage): continue rewrite of sondage" -m "known issues: more than 2 votes and remove are not updates 2019-10-07 23:51:06 +02:00
Romain J 98b241d51b refactor(command|sondage): continue rewrite of sondage
known issues: more than 2 votes are not peristed
2019-10-06 23:49:00 +02:00
Romain J 76e845e5be refactor(command|sondage): continue rewrite of sondage
known issues: datas are not commited in database on reaction on
2019-10-06 01:49:30 +02:00
Romain J d5f1f71a0a add(i18n): create a language switcher command for all guilds 2019-09-29 23:01:49 +02:00
Romain J 29808d41d6 refactor(database): migrate to sqlalchemy 2019-09-29 18:31:01 +02:00
Romain J 8f17085cf7 add(command|sondage): start dev of command sondage 2019-09-28 01:39:45 +02:00
Romain J 46b1daeae4 update(doc|readme): Finish cogs.utility'commands todo 2019-09-28 01:22:40 +02:00
Romain J 6429dc2e01 refactor(command|utility): rewrite quote 2019-09-28 01:20:44 +02:00
Romain J 3ae55bc92e refactor(command|utility): rewrite git 2019-09-28 01:14:59 +02:00
Romain J 60f68f245d refactor(command|utility): rewrite getheaders 2019-09-28 00:57:39 +02:00
Romain J 2d175b4453 update(cog|admin): add commands for warns 2019-09-22 04:18:28 +02:00
Romain J 0849c1bdff refactor(command|utility): rewrite iplocalise 2019-09-22 01:55:43 +02:00
Romain J e38fd5417f update(cog|logs): add commands 2019-09-21 23:17:45 +02:00
Romain J 4c48fdff6e add(cog|logs): add cog for logging 2019-09-21 00:11:29 +02:00
Romain J 9274895226 add(command|warn): start dev of command warn 2019-09-19 01:48:52 +02:00
Romain J 764068ef22 add(todo|command): add warn command to todo list 2019-09-18 12:51:19 +02:00
Romain J 6605115941 refactor(hierarchy): change first_run and locales path
Put first_run and locales dirs to extras
2019-09-18 12:30:49 +02:00
Romain J a3edb16528 feat(command|basics): create new command to get info on contributors (.credits/.contributors) 2019-09-15 02:56:41 +02:00
Romain J 68daf90938 update(command|info): add things to command 2019-09-15 02:31:31 +02:00
Mael G. 7c65c92084 Language fix : Stating -> Strating 2019-09-14 20:09:36 -04:00
Mael G. 23ec0f7e67 Language fix 2019-09-14 20:06:15 -04:00
Romain J 289fedb4b7 refactor(command|basics): rewrite info and ping commands 2019-09-15 01:25:32 +02:00
Romain J a9a8572aad update(doc|readme): Finish cogs.admin'commands todo 2019-09-13 11:38:09 +02:00
Romain J 582d254af5 refactor(command|admin|delete): rewrite delete command 2019-09-13 11:33:47 +02:00
Romain J d0a9e658a6 update(doc|readme): Add cogs.admin's commands todo
refactor(command|admin): rewrite some of all admin's command
2019-09-13 11:20:17 +02:00
Romain J d6f155e682 update(cogs|admin): add react commands 2019-09-12 17:21:00 +02:00
Romain J c0af425383 add(command|kick/ban): rewrite kick and ban commands 2019-09-12 15:43:57 +02:00
Romain J 806ddf0540 remove(cogs|all): remove all cogs and start dev of cogs.admin 2019-09-11 15:59:37 +02:00
Romain J 04f2c9feae update(doc|readme): add thing to do 2019-09-11 11:19:47 +02:00
Romain J 985f839d21 update(doc|readme): add thing to do 2019-09-11 11:14:52 +02:00
Romain J f6de008952 finish(support): support python 3.8 and discord.py 1.3.0 2019-09-10 23:52:39 +02:00
Romain J 7e5f6e6dbf finish(launcher): finish all launcher features 2019-09-10 23:48:50 +02:00
Romain J 537a905f85 better with add . 2019-09-10 23:44:44 +02:00
Romain J 02d279b6c4 add(laucnher|update): add bot.version and --update 2019-09-10 23:20:56 +02:00
Romain J 712339968c fuck: pep-8 79 chars 2019-09-10 18:37:38 +02:00
Romain J f6ba42a143 update(type-hint): add more type hint 2019-09-10 18:23:24 +02:00
Romain J 30e7906f3f update(application|bot): add prefixes, blacklist,... confs and command processing 2019-09-09 22:40:17 +02:00
Romain J cb0b9a2681 fix(logs): add logs/* to gitignore) 2019-09-08 23:14:31 +02:00
Romain J efc05f816e add(i18n): start i18n development
refactor(application|bot): ...
2019-09-08 23:05:43 +02:00
Romain J b03dc30c6c add(launcher): start launcher development 2019-09-01 23:01:43 +02:00
Romain J f4813702fd update(doc|readme): add more precision for skynet 2019-08-28 21:02:11 +02:00
Romain J 7bd7a5402e update(doc|readme): add thing to do 2019-08-28 11:54:10 +02:00
Romain J 4102f3e723 why not ? 2019-08-28 00:31:06 +02:00
Romain J 63938a6bf3 update(doc|readme): add backticks for markdown 2019-08-27 21:45:03 +02:00
Romain J 04ce551acb update(doc|readme): add new category for new commands 2019-08-27 21:17:19 +02:00
Romain J 108e2b8af3 add(doc|readme): add checklist for new features 2019-08-27 17:09:22 +02:00
Romain J 0a25ced905 delete(doc|readme): delete readme.md file
Encore desolé Mael mais je vais en faire un special pour cette branch
avec plein de checkbox pour voir quelles choses je dois encore ajouter
2019-08-27 12:02:02 +02:00
Romain J 31c6df0f88 delete(misc): delete random useless file 2019-08-24 18:25:12 +02:00
Romain J 36e03ad961 delete(fitter): delete init and autoinstall files
Desolé Mael, je sais que ces fichiers t'etaient chers mais ca aussi, ca
va sauter
2019-08-24 18:03:12 +02:00
Romain J 59423ae8a2 delete(dir): delete texts directory 2019-08-24 17:50:57 +02:00
Romain J d1afd8c7ec update(doc|markdown): add table to describe the template 2019-08-24 17:44:36 +02:00
Romain J 2157ae2899 add(doc): add issue template 2019-08-24 17:40:15 +02:00
119 changed files with 4635 additions and 4672 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"

View file

@ -25,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. -->

56
.gitignore vendored Executable file → Normal file
View file

@ -1,17 +1,39 @@
#Python
__pycache__/
*.pyc
.env
config.py
.DS_Store
private.py
#jetbrains
.idea/
#Ipinfo key
ipinfoio.key
#Bgp Graph
bgpgraph.png
bgpgraph
__pycache__/
*.py[cod]
*$py.class
# 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
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# 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
.idea/sonarlint/
*.log
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
venv
dist
build
*.egg
*.egg-info

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,52 @@
<component name="ProjectDictionaryState">
<dictionary name="romain">
<words>
<w>anglais</w>
<w>anonyme</w>
<w>appdirs</w>
<w>apres</w>
<w>asctime</w>
<w>commandstats</w>
<w>ctype</w>
<w>debian</w>
<w>découverte</w>
<w>fonction</w>
<w>francais</w>
<w>français</w>
<w>gnous</w>
<w>ipinfo</w>
<w>iplocalise</w>
<w>jishaku</w>
<w>langue</w>
<w>levelname</w>
<w>liste</w>
<w>localiseip</w>
<w>octobre</w>
<w>pacman</w>
<w>postgre</w>
<w>postgresql</w>
<w>pred</w>
<w>pylint</w>
<w>releaselevel</w>
<w>rprint</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>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.9 (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>

16
.idea/tuxbot-bot.iml Normal file
View file

@ -0,0 +1,16 @@
<?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" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9 (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>

19
.pylintrc Normal file
View file

@ -0,0 +1,19 @@
[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
W0703, # broad-except
R0801, # duplicate-code
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

35
Makefile Normal file
View file

@ -0,0 +1,35 @@
PYTHON = python
VENV = venv
XGETTEXT_FLAGS = --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
# Init
main:
$(PYTHON) -m venv --clear $(VENV)
$(VENV)/bin/pip install -U pip setuptools
install:
$(VENV)/bin/pip install .
install-dev:
$(VENV)/bin/pip install -r dev.requirements.txt
update:
$(VENV)/bin/pip install -U .
# Blackify code
reformat:
$(PYTHON) -m black `git ls-files "*.py"` --line-length=79 && $(PYTHON) -m pylint tuxbot
# Translations
xgettext:
for cog in tuxbot/cogs/*/; do \
xgettext `find $$cog -type f -name '*.py'` --output=$$cog/locales/messages.pot $(XGETTEXT_FLAGS); \
done
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
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,32 +0,0 @@
Tuxbot is a french speaking Discord bot made for the GnousEU's discord server. He has been maintened since 2016.
This code is not intended to reuse "as is". If you want to configure your own instance, you will need to modify some files in the source code (see below).
### Requirements
- A server with a modern GNU/Linux distribution with internet connectivity
- Python 3.7 or later with PIP
- Graphviz
### Installation
Install Python dependencies using ``pip3 install -r requirements.txt`` (make sure the pip executable match the correct python version)
Rename ``config.py.example`` to ``config.py`` and edit it with the required information.
You may want to edit the file ``cogs/filter_messages.py`` as well.
### Launch
Start the program using ``python3 bot.py`` (make sure you use the right Python executable)
### Additional features
#### Ipinfo.io API
Tuxbot can use the ipinfo.io API for more precises results for the ``iplocalise`` command. If you want to use it you should create a ``ipinfoio.key`` in the top tuxbot folder.
### Versions
Version list : [Click here to display](https://git.gnous.eu/gnouseu/tuxbot-bot/releases)
### Main contributors
* **Maël** _alias_ [@outoutxyz](https://twitter.com/outoutxyz)
* **Romain** _alias_ [Romain le malchanceux](https://github.com/Rom194)
### Licensing
This project is under the ``Creative Commons BY-NC-SA 4.0`` license - see [LICENSE.md](LICENSE.md) for more details

90
README.rst Normal file
View file

@ -0,0 +1,90 @@
|image0| |image1| |image2|
.. 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.7 or greater
- Pip
- Git
Operating systems
~~~~~~~~~~~~~~~~~
Arch Linux
^^^^^^^^^^
.. code-block:: bash
$ sudo pacman -Syu python python-pip python-virtualenv git
Continue to `create the venv <#creating-the-virtual-environment>`__.
--------------
Debian
^^^^^^
.. code-block:: bash
$ sudo apt update
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git
Continue to `create the venv <#creating-the-virtual-environment>`__.
--------------
Windows
^^^^^^^
*not for now and not for the future*
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 ~/tuxvenv/bin/activate`
Configuration
-------------
It's time to set up your first instance, to do this, you can simply
execute this command:
:bash:`tuxbot-setup [your instance name]`
After following the instructions, you can run your instance by executing
this command:
:bash:`tuxbot [your instance name]`
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/badge/dynamic/json?color=%23dfb317&label=issues&query=%24.open_issues_count&suffix=%20open&url=https%3A%2F%2Fgit.gnous.eu%2Fapi%2Fv1%2Frepos%2FGnousEU%2Ftuxbot-bot%2F
.. |image2| image:: https://img.shields.io/badge/code%20style-black-000000.svg

View file

@ -1,146 +0,0 @@
#!/bin/bash
# This script is meant for easy install Tuxbot using curl/wget
printf "Welcome to Tuxbot's installation guide.\n"
printf "\nLog file is in ~/.tuxinstall.log\n"
# Command checking
if (( $EUID != 0 )); then
printf "\n\nError : Please run this script as ROOT"
exit 0
fi
if ! [ -x "$(command -v git)" ]; then
printf "\n\nError : Git is not installed"
exit 0
fi
if ! [ -x "$(command -v pip3.7)" ]; then
printf "\n\nError : pip3.7 is not installed (using pip3.7 command)\nPlease install it to continue"
exit 0
fi
# Tuxbot directory answer
read -p "In which directory Tuxbot should be installed ? : [/srv/]" na
na=${na:-"/srv/"}
# Cloning tuxbot USING GNOUS'S GIT MIRROR
printf "Cloning git repository, please wait... \n" &
git clone https://git.gnous.eu/gnouseu/tuxbot-bot $na/tuxbot-bot &> ~/.tuxinstall.log
sleep 1
printf "Tuxbot has been cloned to $na.\n" 5 50
sleep 1
printf "Installing pip modules, please wait...\n" 5 50 &
sleep 1
# Downloading PIP modules using pip3.7 cmd
pip3.7 install -U discord.py[voice] &> ~/.tuxinstall.log
cd $na/tuxbot-bot
pip3.7 install -r requirements.txt &> ~/.tuxinstall.log
sleep 1
printf "Tuxbot's python dependencies have been downloaded\n"
sleep 1
# Answers to generate config
function generateConfig {
DATE=`date +%Y-%m-%d`
read -p "Enter your Discord API Token : " cToken
read -p "Enter the bot client ID : " cID
read -p "Enter the log channel ID : " cLogID
read -p "Enter the main channel of your server : " cSrvID
read -p "What game tuxbot should display as playing (eg : 'Eat potatoes') : " cGame
read -p "What is you're discord user ID (for admin cmd) : " cAdmin
echo "------------"
read -p "MySQL's tuxbot user : " mSQLuser
read -p "MySQL's tuxbot password : " mSQLpass
read -p "MySQL's tuxbot database name : " mSQLdb
echo """
#Generated by Tuxbot install script
#$DATE
token = \"$cToken\"
client_id = \"$cID\"
log_channel_id = \"$cLogID\"
main_server_id = \"$cSrvID\"
game = \"$cGame\"
authorized_id = [\"$cAdmin\"]
prefix = [\".\"]
description = '.'
mysql = {
\"host\": \"localhost\",
\"username\": \"$mSQLuser\",
\"password\": \"$mSQLpass\",
\"dbname\": \"$mSQLdb\"
}
fonts = {
\"normal\": \"NotoSansCJK-Regular.ttc\",
\"bold\": \"NotoSansCJK-Bold.ttc\"
}
""" &> $na/tuxbot-bot/config.py
}
printf "Do you want to generate config file ?\n1 - Yes (selected)\n2 - No\n"
read -p "(1-2) : " initConf
initConf=${initConf:-"1"}
case $initConf in
1) generateConfig;;
esac
#Non login user
echo "Adding tuxbot non-login user..."
useradd -M tuxbot
sleep 1
#Chown all perms to the non login user
echo "Fixing permissions..."
chown tuxbot:tuxbot -R $na/tuxbot-bot/
sleep 1
#Create the service file
echo "Adding Tuxbot service & start it..."
echo """[Unit]
Description=Tuxbot, a discord bot
#After=network.target
[Service]
Type=simple
User=tuxbot
Restart=on-failure
Restart=always
RestartSec=1
WorkingDirectory=$na/tuxbot-bot/
ExecStart=/usr/bin/env python3.7 $na/tuxbot-bot/bot.py
StandardOutput=file:/var/log/tuxbot.log
[Install]
WantedBy=multi-user.target
""" &> /lib/systemd/system/tuxbot.service
systemctl daemon-reload
systemctl start tuxbot
sleep 1
echo "Activation of tuxbot at startup..."
sleep 1
systemctl enable tuxbot
#End message
echo """
Tuxbot should be correctly installed.
Please check if all is good by execute :
systemctl status tuxbot
And .ping command in discord.
Configuration file is $na/tuxbot-bot/config.py
Main tuxbot directory is $na/tuxbot-bot/
Any question ? => Make an issue on github
https://git.gnous.eu/gnouseu/tuxbot-bot
https://github.com/outout14/tuxbot-bot
"""

141
bot.py
View file

@ -1,141 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = "Maël / Outout | Romain"
__licence__ = "WTFPL Licence 2.0"
import copy
import datetime
import os
import sys
import traceback
import aiohttp
import discord
from discord.ext import commands
import cogs.utils.cli_colors as colors
import config
from cogs.utils import checks
if sys.version_info[1] < 7 or sys.version_info[0] < 3:
print(f"{colors.text_colors.RED}[ERROR] Python 3.7 or + is required.{colors.ENDC}")
exit()
l_extensions = (
'cogs.admin',
'cogs.afk',
'cogs.atc',
'cogs.basics',
'cogs.ci',
'cogs.filter_messages',
'cogs.funs',
'cogs.role',
'cogs.search',
'cogs.send_logs',
'cogs.sondage',
'cogs.utility',
'cogs.vocal',
'cogs.private',
'cogs.monitoring',
'jishaku'
)
help_attrs = dict(hidden=True, in_help=True, name="DONOTUSE")
class TuxBot(commands.Bot):
def __init__(self):
self.uptime = datetime.datetime.utcnow()
self.config = config
super().__init__(command_prefix=self.config.prefix[0],
description=self.config.description,
pm_help=None,
help_command=None)
self.client_id = self.config.client_id
self.session = aiohttp.ClientSession(loop=self.loop)
self._events = []
self.add_command(self.do)
for extension in l_extensions:
try:
self.load_extension(extension)
print(f"{colors.text_colors.GREEN}\"{extension}\""
f" chargé !{colors.ENDC}")
except Exception as e:
print(f"{colors.text_colors.RED}"
f"Impossible de charger l'extension {extension}\n"
f"{type(e).__name__}: {e}{colors.ENDC}", file=sys.stderr)
async def is_owner(self, user: discord.User):
return str(user.id) in config.authorized_id
async def on_command_error(self, ctx, error):
if isinstance(error, commands.NoPrivateMessage):
await ctx.author.send('Cette commande ne peut pas être utilisee '
'en message privee.')
elif isinstance(error, commands.DisabledCommand):
await ctx.author.send('Desoler mais cette commande est desactive, '
'elle ne peut donc pas être utilisée.')
elif isinstance(error, commands.CommandInvokeError):
print(f'In {ctx.command.qualified_name}:', file=sys.stderr)
traceback.print_tb(error.original.__traceback__)
print(f'{error.original.__class__.__name__}: {error.original}',
file=sys.stderr)
async def on_ready(self):
log_channel_id = await self.fetch_channel(self.config.log_channel_id)
print('\n\n---------------------')
print('CONNECTÉ :')
print(f'Nom d\'utilisateur: {self.user} {colors.text_style.DIM}'
f'(ID: {self.user.id}){colors.ENDC}')
print(f'Channel de log: {log_channel_id} {colors.text_style.DIM}'
f'(ID: {log_channel_id.id}){colors.ENDC}')
print(f'Prefix: {self.config.prefix[0]}')
print('Merci d\'utiliser TuxBot')
print('---------------------\n\n')
await self.change_presence(status=discord.Status.dnd,
activity=discord.Game(
name=self.config.game)
)
@staticmethod
async def on_resumed():
print('resumed...')
async def on_message(self, message):
if message.author.bot:
return
try:
await self.process_commands(message)
except Exception as e:
print(f'{colors.text_colors.RED}Erreur rencontré : \n'
f' {type(e).__name__}: {e}{colors.ENDC} \n \n')
def run(self):
super().run(self.config.token, reconnect=True)
@checks.has_permissions(administrator=True)
@commands.command(pass_context=True, hidden=True)
async def do(self, ctx, times: int, *, command):
"""Repeats a command a specified number of times."""
msg = copy.copy(ctx.message)
msg.content = command
for i in range(times):
await self.process_commands(msg)
if __name__ == '__main__':
if os.path.exists('config.py') is not True:
print(f"{colors.text_colors.RED}"
f"Veuillez créer le fichier config.py{colors.ENDC}")
exit()
tuxbot = TuxBot()
tuxbot.run()

BIN
cogs/.DS_Store vendored

Binary file not shown.

View file

@ -1,407 +0,0 @@
import aiohttp
from discord.ext import commands
import discord
from .utils import checks
from .utils.checks import get_user
class Admin(commands.Cog):
"""Commandes secrètes d'administration."""
def __init__(self, bot):
self.bot = bot
self._last_result = None
self.sessions = set()
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name="upload", pass_context=True)
async def _upload(self, ctx, *, url=""):
if len(ctx.message.attachments) >= 1:
file = ctx.message.attachments[0].url
elif url != "":
file = url
else:
em = discord.Embed(title='Une erreur est survenue',
description="Fichier introuvable.",
colour=0xDC3546)
await ctx.send(embed=em)
return
async with aiohttp.ClientSession() as session:
async with session.get(file) as r:
image = await r.content.read()
with open(f"data/tmp/{str(ctx.author.id)}.png", 'wb') as f:
f.write(image)
f.close()
await ctx.send(file=discord.File(f"data/tmp/{str(ctx.author.id)}.png"))
@checks.has_permissions(administrator=True)
@commands.command(name="ban", pass_context=True)
async def _ban(self, ctx, user, *, reason=""):
"""Ban user"""
user = get_user(ctx.message, user)
if user and str(user.id) not in self.bot.config.unkickable_id:
try:
await user.ban(reason=reason, delete_message_days=0)
return_msg = f"`{user.mention}` a été banni\n"
if reason:
return_msg += f"raison : `{reason}`"
return_msg += "."
await ctx.send(return_msg)
except discord.Forbidden:
await ctx.send('Impossible de bannir cet user,'
' probleme de permission.')
else:
return await ctx.send('Impossible de trouver l\'user.')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name="kick", pass_context=True)
async def _kick(self, ctx, user, *, reason=""):
"""Kick a user"""
user = get_user(ctx.message, user)
if user and str(user.id) not in self.bot.config.unkickable_id:
try:
await user.kick(reason=reason)
return_msg = f"`{user.mention}` a été kické\n"
if reason:
return_msg += f"raison : `{reason}`"
return_msg += "."
await ctx.send(return_msg)
except discord.Forbidden:
await ctx.send('Impossible de kicker cet user,'
' probleme de permission.')
else:
return await ctx.send('Impossible de trouver l\'user.')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='clear', pass_context=True)
async def _clear(self, ctx, number: int, silent: str = True):
"""Clear <number> of message(s)"""
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
if number < 1000:
try:
await ctx.message.channel.purge(limit=number)
except Exception as e: # TODO : A virer dans l'event on_error
if silent is not True:
await ctx.send(f':sob: Une erreur est survenue : \n'
f' {type(e).__name__}: {e}')
if silent is not True:
await ctx.send("Hop voila j'ai viré des messages! Hello World")
print(f"{str(number)} messages ont été supprimés")
else:
await ctx.send('Trop de messages, entre un nombre < 1000')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='say', pass_context=True)
async def _say(self, ctx, *, tosay: str):
"""Say a message in the current channel"""
try:
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
await ctx.send(tosay)
except Exception as e: # TODO : A virer dans l'event on_error
await ctx.send(f':sob: Une erreur est survenue : \n'
f' {type(e).__name__}: {e}')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='sayto', pass_context=True)
async def _sayto(self, ctx, channel_id: int, *, tosay: str):
"""Say a message in the <channel_id> channel"""
try:
chan = self.bot.get_channel(channel_id)
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
try:
await chan.send(tosay)
except Exception:
print(f"Impossible d'envoyer le message dans {str(channel_id)}")
except Exception as e: # TODO : A virer dans l'event on_error
await ctx.send(f':sob: Une erreur est survenue : \n'
f' {type(e).__name__}: {e}')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='sayto_dm', pass_context=True)
async def _sayto_dm(self, ctx, user_id: int, *, tosay: str):
"""Say a message to the <user_id> user"""
try:
user = self.bot.get_user(user_id)
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
try:
await user.send(tosay)
except Exception:
print(f"Impossible d'envoyer le message dans {str(user_id)}")
except Exception as e: # TODO : A virer dans l'event on_error
await ctx.send(f':sob: Une erreur est survenue : \n'
f' {type(e).__name__}: {e}')
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='editsay', pass_context=True)
async def _editsay(self, ctx, message_id: int, *, new_content: str):
"""Edit a bot's message"""
try:
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
toedit = await ctx.channel.fetch_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{message_id}` sur ce salon")
return
try:
await toedit.edit(content=str(new_content))
except discord.errors.Forbidden:
await ctx.send("J'ai po les perms pour editer mes messages :(")
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='addreaction', pass_context=True)
async def _addreaction(self, ctx, message_id: int, reaction: str):
"""Add reactions to a message"""
try:
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
toadd = await ctx.channel.fetch_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{message_id}` sur ce salon")
return
try:
await toadd.add_reaction(reaction)
except discord.errors.Forbidden:
await ctx.send("J'ai po les perms pour ajouter des réactions :(")
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='delete', pass_context=True)
async def _delete(self, ctx, message_id: int):
"""Delete message in current channel"""
try:
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
todelete = await ctx.channel.fetch_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{message_id}` sur ce salon")
return
try:
await todelete.delete()
except discord.errors.Forbidden:
await ctx.send("J'ai po les perms pour supprimer des messages :(")
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='deletefrom', pass_context=True)
async def _deletefrom(self, ctx, chan_id: int, *, message_id: int):
"""Delete message in <chan_id> channel"""
try:
chan = self.bot.get_channel(chan_id)
try:
await ctx.message.delete()
except Exception:
print(f"Impossible de supprimer le message "
f"\"{str(ctx.message.content)}\"")
todelete = await chan.fetch_message(message_id)
except discord.errors.NotFound:
await ctx.send(f"Impossible de trouver le message avec l'id "
f"`{id}` sur le salon")
return
try:
await todelete.delete()
except discord.errors.Forbidden:
await ctx.send("J'ai po les perms pour supprimer le message :(")
"""---------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@commands.command(name='embed', pass_context=True)
async def _embed(self, ctx, *, msg: str = "help"):
"""Send an embed"""
if msg != "help":
ptext = title \
= description \
= image \
= thumbnail \
= color \
= footer \
= author \
= None
timestamp = discord.Embed.Empty
embed_values = msg.split('|')
for i in embed_values:
if i.strip().lower().startswith('ptext='):
ptext = i.strip()[6:].strip()
elif i.strip().lower().startswith('title='):
title = i.strip()[6:].strip()
elif i.strip().lower().startswith('description='):
description = i.strip()[12:].strip()
elif i.strip().lower().startswith('desc='):
description = i.strip()[5:].strip()
elif i.strip().lower().startswith('image='):
image = i.strip()[6:].strip()
elif i.strip().lower().startswith('thumbnail='):
thumbnail = i.strip()[10:].strip()
elif i.strip().lower().startswith('colour='):
color = i.strip()[7:].strip()
elif i.strip().lower().startswith('color='):
color = i.strip()[6:].strip()
elif i.strip().lower().startswith('footer='):
footer = i.strip()[7:].strip()
elif i.strip().lower().startswith('author='):
author = i.strip()[7:].strip()
elif i.strip().lower().startswith('timestamp'):
timestamp = ctx.message.created_at
else:
if description is None and not i.strip()\
.lower().startswith('field='):
description = i.strip()
if color:
if color.startswith('#'):
color = color[1:]
if not color.startswith('0x'):
color = '0x' + color
if ptext \
is title \
is description \
is image \
is thumbnail \
is color \
is footer \
is author \
is None \
and 'field=' not in msg:
try:
await ctx.message.delete()
except Exception:
print("Impossible de supprimer le message \"" + str(
ctx.message.content) + "\"")
return await ctx.send(content=None,
embed=discord.Embed(description=msg))
if color:
em = discord.Embed(timestamp=timestamp, title=title,
description=description,
color=int(color, 16))
else:
em = discord.Embed(timestamp=timestamp, title=title,
description=description)
for i in embed_values:
if i.strip().lower().startswith('field='):
field_inline = True
field = i.strip().lstrip('field=')
field_name, field_value = field.split('value=')
if 'inline=' in field_value:
field_value, field_inline = field_value.split(
'inline=')
if 'false' in field_inline.lower() \
or 'no' in field_inline.lower():
field_inline = False
field_name = field_name.strip().lstrip('name=')
em.add_field(name=field_name, value=field_value.strip(),
inline=field_inline)
if author:
if 'icon=' in author:
text, icon = author.split('icon=')
if 'url=' in icon:
em.set_author(name=text.strip()[5:],
icon_url=icon.split('url=')[0].strip(),
url=icon.split('url=')[1].strip())
else:
em.set_author(name=text.strip()[5:], icon_url=icon)
else:
if 'url=' in author:
em.set_author(name=author.split('url=')[0].strip()[5:],
url=author.split('url=')[1].strip())
else:
em.set_author(name=author)
if image:
em.set_image(url=image)
if thumbnail:
em.set_thumbnail(url=thumbnail)
if footer:
if 'icon=' in footer:
text, icon = footer.split('icon=')
em.set_footer(text=text.strip()[5:], icon_url=icon)
else:
em.set_footer(text=footer)
try:
await ctx.message.delete()
except Exception:
print("Impossible de supprimer le message \"" + str(
ctx.message.content) + "\"")
await ctx.send(content=ptext, embed=em)
else:
embed = discord.Embed(
title="Aide sur l'utilisation de la commande .embed:")
embed.add_field(name="Titre:", value="title=<le titre>",
inline=True)
embed.add_field(name="Description:",
value="description=<la description>", inline=True)
embed.add_field(name="Couleur:", value="color=<couleur en hexa>",
inline=True)
embed.add_field(name="Image:",
value="image=<url de l'image (en https)>",
inline=True)
embed.add_field(name="Thumbnail:",
value="thumbnail=<url de l'image>", inline=True)
embed.add_field(name="Auteur:", value="author=<nom de l'auteur>",
inline=True)
embed.add_field(name="Icon", value="icon=<url de l'image>",
inline=True)
embed.add_field(name="Footer", value="footer=<le footer>",
inline=True)
embed.set_footer(text="Exemple: .embed title=Un titre |"
" description=Une description |"
" color=3AB35E |"
" field=name=test value=test")
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Admin(bot))

View file

@ -1,65 +0,0 @@
from discord.ext import commands
import discord
import random
class AFK(commands.Cog):
"""Commandes utilitaires."""
def __init__(self, bot):
self.bot = bot
self.afk_users = []
"""---------------------------------------------------------------------"""
@commands.command(pass_context=True)
async def afk(self, ctx, action: str = ""):
if action.lower() == "list":
try:
await ctx.send(*self.afk_users)
except discord.HTTPException:
await ctx.send("Il n'y a personne d'afk...")
else:
user = ctx.author
self.afk_users.append(user)
msgs = ["s'absente de discord quelques instants",
"se casse de son pc",
"va sortir son chien",
"reviens bientôt",
"va nourrir son cochon",
"va manger des cookies",
"va manger de la poutine"]
await ctx.send(f"**{user.mention}** {random.choice(msgs)}...")
"""---------------------------------------------------------------------"""
@commands.Cog.listener()
async def on_message(self, message):
if message.author.bot \
or message.guild.id != int(self.bot.config.main_server_id):
return
user = message.author
if user in self.afk_users \
and message.content != self.bot.config.prefix[0] + "afk":
self.afk_users.remove(user)
msgs = ["a réssuscité",
"est de nouveau parmi nous",
"a fini d'uriner",
"n'est plus mort",
"est de nouveau sur son PC",
"a fini de manger sa poutine",
"a fini de danser",
"s'est réveillé",
"est de retour dans ce monde cruel"]
await message.channel.send(f"**{user.mention}**"
f" {random.choice(msgs)}...")
def setup(bot):
bot.add_cog(AFK(bot))

View file

@ -1,124 +0,0 @@
import asyncio
from bs4 import BeautifulSoup
import requests
import re
import discord
from discord.ext import commands
class ATC(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.playing = False
self.author = None
self.voice = None
@staticmethod
async def extra(self, ctx, icao):
if icao == "stop_playing":
if self.playing and (
ctx.author.id == self.author.id
or ctx.message.channel.permissions_for(ctx.message.author).administrator is True
):
await self.voice.disconnect()
self.playing = False
await ctx.send("Écoute terminée !")
return "quit"
else:
await ctx.send("Veuillez specifier un icao")
return "quit"
if icao == "info":
em = discord.Embed(title=f"Infos sur les services utilisés par {self.bot.config.prefix[0]}atc")
em.add_field(name="Service pour les communications:",
value="[liveatc.net](https://www.liveatc.net/)",
inline=False)
em.add_field(name="Service pour les plans des aéroports:",
value="[universalweather.com](http://www.universalweather.com/)",
inline=False)
await ctx.send(embed=em)
return "quit"
"""---------------------------------------------------------------------"""
@commands.command(name="atc", no_pm=True, pass_context=True)
async def _atc(self, ctx, icao="stop_playing"):
user = ctx.author
if not user.voice:
await ctx.send('Veuillez aller dans un channel vocal.')
return
if await self.extra(self, ctx, icao) == "quit":
return
if self.playing:
await ctx.send(f"Une écoute est déja en court, "
f"demandez à {self.author.mention} de faire "
f"`.atc stop_playing` pour l'arreter")
return
self.author = user
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.35 Safari/537.36',
}
req = requests.post("https://www.liveatc.net/search/",
data={'icao': icao},
headers=headers)
html = BeautifulSoup(req.text, features="lxml")
regex = r"(javascript: pageTracker\._trackPageview\('\/listen\/)(.*)(\'\)\;)"
possibilities = {}
emojis = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟',
'0⃣', '🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮']
to_react = []
idx = 0
for a in html.findAll("a", onclick=True):
val = a.get('onclick')
for match in re.finditer(regex, val):
possibilities[idx] = f"{emojis[idx]} - {match.groups()[1]}\n"
to_react.append(emojis[idx])
idx += 1
em = discord.Embed(title='Résultats pour : ' + icao,
description=str(''.join(possibilities.values())),
colour=0x4ECDC4)
em.set_image(
url=f"http://www.universalweather.com/regusers/mod-bin/uvtp_airport_image?icao={icao}")
poll_msg = await ctx.send(embed=em)
for emote in to_react:
await poll_msg.add_reaction(emote)
def check(reaction, user):
return user == ctx.author and reaction.emoji in to_react and \
reaction.message.id == poll_msg.id
async def waiter(future: asyncio.Future):
reaction, user = await self.bot.wait_for('reaction_add',
check=check)
future.set_result(emojis.index(reaction.emoji))
added_emoji = asyncio.Future()
self.bot.loop.create_task(waiter(added_emoji))
while not added_emoji.done():
await asyncio.sleep(0.1)
freq = possibilities[added_emoji.result()].split('- ')[1]
if possibilities:
self.playing = True
self.voice = await user.voice.channel.connect()
self.voice.play(
discord.FFmpegPCMAudio(f"http://yyz.liveatc.net/{freq}"))
await poll_msg.delete()
await ctx.send(f"Écoute de {freq}")
else:
await ctx.send(f"Aucun résultat trouvé pour {icao} {freq}")
def setup(bot):
bot.add_cog(ATC(bot))

View file

@ -1,68 +0,0 @@
import platform
import socket
import subprocess
import discord
from discord.ext import commands
from discord.http import Route
class Basics(commands.Cog):
"""Commandes générales."""
def __init__(self, bot):
self.bot = bot
@commands.command()
async def ping(self, ctx):
result = self.bot.latency * 1000
if float(result) >= 300:
em = discord.Embed(title="Ping : " + str(result) + "ms",
description="... c'est quoi ce ping !",
colour=0xFF1111)
await ctx.send(embed=em)
elif float(result) > 200:
em = discord.Embed(title="Ping : " + str(result) + "ms",
description="Ca va, ça peut aller, mais j'ai "
"l'impression d'avoir 40 ans !",
colour=0xFFA500)
await ctx.send(embed=em)
else:
em = discord.Embed(title="Ping : " + str(result) + "ms",
description="Wow c'te vitesse de réaction, "
"je m'épate moi-même !",
colour=0x11FF11)
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command()
async def info(self, ctx):
"""Affiches des informations sur le bot"""
text = open('texts/info.md').read()
os_info = str(platform.system()) + " / " + str(platform.release())
em = discord.Embed(title='Informations sur TuxBot',
description=text.format(os_info,
platform.python_version(),
socket.gethostname(),
discord.__version__,
Route.BASE),
colour=0x89C4F9)
em.set_footer(text="/home/****/bot.py")
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command()
async def help(self, ctx, page: int = 1):
"""Affiches l'aide du bot"""
page = int(page) if 0 < int(page) < 5 else 1
text = open('texts/help.md').read().split("[split]")
em = discord.Embed(title='Commandes de TuxBot', description=text[page - 1],
colour=0x89C4F9)
await ctx.send(content=f"page {page}/{len(text)}", embed=em)
def setup(bot):
bot.add_cog(Basics(bot))

View file

@ -1,311 +0,0 @@
import datetime
import random
import discord
import requests
from discord.ext import commands
from .utils import checks
from .utils import db
from .utils.checks import get_user, check_date
class Identity(commands.Cog):
"""Commandes des cartes d'identité ."""
def __init__(self, bot):
self.bot = bot
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SHOW TABLES LIKE 'users'""")
result = self.cursor.fetchone()
if not result:
# Creation table Utilisateur si premiere fois
sql = "CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, userid TEXT null, username TEXT null, os TEXT null, config TEXT null, useravatar TEXT null, userbirth TEXT null, pays TEXT null, cidate TEXT null, cibureau TEXT null);"
self.cursor.execute(sql)
"""--------------------------------------------------------------------------------------------------------------------------"""
@commands.group(name="ci", no_pm=True, pass_context=True)
async def _ci(self, ctx):
"""Cartes d'identité"""
if ctx.invoked_subcommand is None:
text = open('texts/ci-info.md').read()
em = discord.Embed(title='Commandes de carte d\'identité de TuxBot', description=text, colour=0x89C4F9)
await ctx.send(embed=em)
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="show")
async def ci_show(self, ctx, args: str = None):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
if args == None:
user = get_user(ctx.message, ctx.author.name)
else:
user = get_user(ctx.message, args)
if user:
self.cursor.execute("""SELECT userid, username, useravatar, userbirth, cidate, cibureau, os, config, pays, id FROM users WHERE userid=%s""",(str(user.id)))
result = self.cursor.fetchone()
def isexist(var):
if not var:
return "Non renseigné."
else:
return var
if not result:
await ctx.send(f"{ctx.author.mention}> :x: Désolé mais {user.mention} est sans papier !")
else:
try:
user_birth = datetime.datetime.fromisoformat(result[3])
user_birth_day = check_date(str(user_birth.day))
user_birth_month = check_date(str(user_birth.month))
formated_user_birth = str(user_birth_day) + "/" + str(user_birth_month) + "/" + str(user_birth.year)
try: ## a virer une fois le patch appliqué pour tout le monde
cidate = datetime.datetime.fromisoformat(result[4])
cidate_day = check_date(str(cidate.day)) ## a garder
cidate_month = check_date(str(cidate.month)) ## a garder
formated_cidate = str(cidate_day) + "/" + str(cidate_month) + "/" + str(cidate.year) ## a garder
except ValueError: ## a virer une fois le patch appliqué pour tout le monde
formated_cidate = str(result[4]).replace('-', '/') ## a virer une fois le patch appliqué pour tout le monde
await ctx.send(f"{user.mention} vous êtes prié(e) de faire la commande `.ci update` afin de regler un probleme de date coté bdd") ## a virer une fois le patch appliqué pour tout le monde
embed=discord.Embed(title="Carte d'identité | Communisme Linuxien")
embed.set_author(name=result[1], icon_url=result[2])
embed.set_thumbnail(url = result[2])
embed.add_field(name="Nom :", value=result[1], inline=True)
embed.add_field(name="Système d'exploitation :", value=isexist(result[6]), inline=True)
embed.add_field(name="Configuration Système : ", value=isexist(result[7]), inline=False)
embed.add_field(name="Date de naissance sur discord : ", value=formated_user_birth, inline=True)
embed.add_field(name="Pays : ", value=isexist(result[8]), inline=True)
embed.add_field(name="Profil sur le web : ", value="*indisponible*") # value=f"https://tuxbot.gnous.eu/users/{result[9]}", inline=True)
embed.set_footer(text=f"Enregistré dans le bureau {result[5]} le {formated_cidate}.")
await ctx.send(embed=embed)
except Exception as e:
await ctx.send(f"{ctx.author.mention}> :x: Désolé mais la carte d'identité de {user.mention} est trop longue de ce fait je ne peux te l'envoyer, essaye de l'aléger, {user.mention} :wink: !")
await ctx.send(f':sob: Une erreur est survenue : \n {type(e).__name__}: {e}')
else:
return await ctx.send('Impossible de trouver l\'user.')
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="register")
async def ci_register(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
await ctx.send("Mais tu as déja une carte d'identité ! u_u")
else:
now = datetime.datetime.now()
self.cursor.execute("""INSERT INTO users(userid, username, useravatar, userbirth, cidate, cibureau) VALUES(%s, %s, %s, %s, %s, %s)""", (str(ctx.author.id), str(ctx.author), str(ctx.author.avatar_url_as(format="jpg", size=512)), str(ctx.author.created_at), now, str(ctx.message.guild.name)))
self.conn.commit()
await ctx.send(f":clap: Bievenue à toi {ctx.author.name} dans le communisme {ctx.message.guild.name} ! Fait ``.ci`` pour plus d'informations !")
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="delete")
async def ci_delete(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""DELETE FROM users WHERE userid =%s""", (str(ctx.author.id)))
self.conn.commit()
await ctx.send("Tu es maintenant sans papiers !")
else:
await ctx.send("Déja enregistre ta carte d'identité avant de la supprimer u_u (après c'est pas logique...)")
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="update")
async def ci_update(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
try:
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""SELECT cidate FROM users WHERE userid=%s""",(str(ctx.author.id)))
old_ci_date = self.cursor.fetchone()
try:
new_ci_date = datetime.datetime.fromisoformat(old_ci_date[0])
except ValueError:
old_ci_date = datetime.datetime.strptime(old_ci_date[0].replace('/', '-'), '%d-%m-%Y')
old_ci_date_day = check_date(str(old_ci_date.day))
old_ci_date_month = check_date(str(old_ci_date.month))
new_ci_date = f"{str(old_ci_date.year)}-{str(old_ci_date_month)}-{str(old_ci_date_day)} 00:00:00.000000"
await ctx.send("succes update")
self.cursor.execute("""UPDATE users SET cidate = %s WHERE userid = %s""", (str(new_ci_date), str(ctx.author.id)))
self.conn.commit()
self.cursor.execute("""UPDATE users SET useravatar = %s, username = %s, cibureau = %s WHERE userid = %s""", (str(ctx.author.avatar_url_as(format="jpg", size=512)), str(ctx.author), str(ctx.message.guild), str(ctx.author.id)))
self.conn.commit()
await ctx.send(f"{ctx.author.mention}> Tu viens, en quelques sortes, de renaitre !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !")
except Exception as e: #TODO : A virer dans l'event on_error
await ctx.send(':( Erreur veuillez contacter votre administrateur :')
await ctx.send(f'{type(e).__name__}: {e}')
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="setconfig")
async def ci_setconfig(self, ctx, *, conf: str = None):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
if conf:
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""UPDATE users SET config = %s WHERE userid = %s""", (str(conf), str(ctx.author.id)))
self.conn.commit()
await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !")
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="setos")
async def ci_setos(self, ctx, *, conf: str = None):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
if conf:
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""UPDATE users SET os = %s WHERE userid = %s""", (str(conf), str(ctx.author.id)))
self.conn.commit()
await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !")
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="setcountry")
async def ci_setcountry(self, ctx, *, country: str = None):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
if country:
self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id)))
result = self.cursor.fetchone()
if result:
self.cursor.execute("""UPDATE users SET pays = %s WHERE userid = %s""", (str(country), str(ctx.author.id)))
self.conn.commit()
await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !")
else:
await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !")
"""--------------------------------------------------------------------------------------------------------------------------"""
@_ci.command(pass_context=True, name="online_edit")
async def ci_online_edit(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SELECT id FROM users WHERE userid=%s""",(str(ctx.author.id)))
result = self.cursor.fetchone()
if not result:
return await ctx.send(f"Déja enregistre ta carte d'identité avant de l'éditer u_u (après c'est pas logique...)")
dm = await ctx.author.create_dm()
try:
def is_exist(key, value):
self.cursor.execute("""SELECT * FROM sessions WHERE {}=%s""".format(str(key)), (str(value)))
return self.cursor.fetchone()
user_id = result[0]
is_admin = '1' if str(ctx.author.id) in self.bot.config.authorized_id else '0'
token = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'*25, 25))
created_at = datetime.datetime.utcnow()
while is_exist('token', token):
token = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'*25, 25))
if is_exist('user_id', user_id):
self.cursor.execute("""UPDATE sessions SET is_admin = %s, token = %s, updated_at = %s WHERE user_id = %s""", (str(is_admin), str(token), str(created_at), str(user_id)))
self.conn.commit()
else:
self.cursor.execute("""INSERT INTO sessions(user_id, is_admin, token, created_at, updated_at) VALUES(%s, %s, %s, %s, %s)""", (str(user_id), str(is_admin), str(token), str(created_at), str(created_at)))
self.conn.commit()
embed=discord.Embed(title="Clé d'édition pour tuxweb", description=f"Voici ta clé d'édition, vas sur [https://tuxbot.gnous.eu/fr/users/{user_id}](https://tuxbot.gnous.eu/fr/users/{user_id}) puis cliques sur `editer` et entre la clé afin de pouvoir modifier ta ci", colour=0x89C4F9)
embed.set_footer(text=f"Cette clé sera valide durant les 10 prochaines minutes, ne la communiques à personne !")
await dm.send(embed=embed)
await dm.send(token)
await ctx.send(f"{ctx.author.mention} ta clé d'édition t'a été envoyée en message privé")
except Exception as e:
await ctx.send(f"{ctx.author.mention}, je ne peux pas t'envoyer de message privé :(. Penses à autoriser les messages privés provenant des membres du serveur pour que je puisse te donner ta clef d'édition")
"""--------------------------------------------------------------------------------------------------------------------------"""
@checks.has_permissions(administrator=True)
@_ci.command(pass_context=True, name="list")
async def ci_list(self, ctx):
self.conn = db.connect_to_db(self)
self.cursor = self.conn.cursor()
self.cursor.execute("""SELECT id, username FROM users""")
rows = self.cursor.fetchall()
msg = ""
try:
for row in rows:
row_id = row[0]
row_name = row[1].encode('utf-8')
msg += f"{str(row_id)} : {str(row_name)} \n"
post = requests.post("https://hastebin.com/documents", data=msg)
await ctx.send(f"{ctx.author.mention} liste posté avec succès sur :\nhttps://hastebin.com/{post.json()['key']}.txt")
with open('ci_list.txt', 'w', encoding='utf-8') as fp:
for row in rows:
row_id = row[0]
row_name = row[1]
fp.write(f"{str(row_id)} : {str(row_name)} \n")
except Exception as e:
await ctx.send(f':sob: Une erreur est survenue : \n {type(e).__name__}: {e}')
def setup(bot):
bot.add_cog(Identity(bot))

View file

@ -1,64 +0,0 @@
from discord.ext import commands
import re
class FilterMessages(commands.Cog):
"""Flitre des messages"""
def __init__(self, bot):
self.bot = bot
@commands.Cog.listener()
async def on_message(self, message):
#
# These are hard-coded ID that you should change in production to adapt your requirements
#
no_pub_guild = [280805240977227776, 303633056944881686,
274247231534792704]
lien_channel = [280805783795662848, 508794201509593088,
516017286948061204]
sondage_channel = [394146769107419146, 477147964393914388]
if message.author.bot \
or str(message.author.id) in self.bot.config.authorized_id \
or message.channel.permissions_for(message.author).administrator is True:
return
discord_invite_regex = re.compile(r"(discord\.(gg|io|me|li)|discordapp\.com\/invite)\/[0-9A-Za-z]*", re.IGNORECASE)
invalid_link_regex = re.compile(r"^(\[[^\]]+\]|<\:[a-z0-9]+\:[0-9]+>) .+ https?:\/\/\S*$", re.IGNORECASE)
try:
if message.guild.id in no_pub_guild:
if isinstance(discord_invite_regex.search(message.content), re.Match):
author = self.bot.get_user(message.author.id)
await message.delete()
await author.send("La pub pour les serveurs discord n'est pas autorisée ici")
if message.channel.id in lien_channel \
and not isinstance(invalid_link_regex.search(message.content), re.Match):
author = self.bot.get_user(message.author.id)
await message.delete()
await author.send(f"Votre message `{message.content}` a été "
f"supprimé du channel `liens` ou `projets` "
f"car il ne respecte pas la structure "
f"définie. Pour partager un lien veuillez "
f"suivre la structure suivante :"
f" ` [Sujet] Descirption http(s)://....`")
await author.send("Si vous voulez commenter ou discuter à "
"propos d'un lien ou d'un projet, veuillez "
"le faire dans le channel"
" `#discussion-des-liens` ou"
" `#discussion-projets`.")
if message.channel.id in sondage_channel:
prefix_lenght = len(await self.bot.get_prefix(message))
command = (message.content.split()[0])[prefix_lenght:]
if command != "sondage":
await message.delete()
except AttributeError:
pass
def setup(bot):
bot.add_cog(FilterMessages(bot))

View file

@ -1,191 +0,0 @@
from discord.ext import commands
from typing import Union
import asyncio
import discord
import urllib.request
import json
import random
import requests
class Funs(commands.Cog):
"""Commandes funs."""
def __init__(self, bot):
self.bot = bot
"""---------------------------------------------------------------------"""
@commands.command()
async def avatar(self, ctx, user: Union[discord.Member, discord.User] = None):
"""Récuperer l'avatar de ..."""
if user is None:
user = ctx.message.author
embed = discord.Embed(title="Avatar de " + user.name,
description=f"[Ouvrir dans mon navigateur]"
f"({user.avatar_url_as(format='png')})")
embed.set_image(url=user.avatar_url_as(format='png'))
await ctx.send(embed=embed)
"""---------------------------------------------------------------------"""
@commands.command(pass_context=True)
async def poke(self, ctx, user: discord.Member):
"""Poke quelqu'un"""
await ctx.send(f":clap: Hey {user.mention} tu t'es fait poker par"
f" {ctx.message.author} !")
await ctx.message.delete()
"""---------------------------------------------------------------------"""
@commands.command()
async def btcprice(self, ctx):
"""Le prix du BTC"""
loading = await ctx.send("_réfléchis..._")
try:
url = urllib.request.urlopen("https://blockchain.info/fr/ticker")
btc = json.loads(url.read().decode())
except KeyError:
btc = 1
if btc == 1:
await loading.edit(content="Impossible d'accèder à l'API"
" blockchain.info, veuillez réessayer"
" ultérieurment ! :c")
else:
frbtc = str(btc["EUR"]["last"]).replace(".", ",")
usbtc = str(btc["USD"]["last"]).replace(".", ",")
await loading.edit(content=f"Un bitcoin est égal à :"
f" {usbtc}$US soit {frbtc}€.")
"""---------------------------------------------------------------------"""
@commands.command()
async def joke(self, ctx, number: str = 0):
"""Print a random joke in a json file"""
with open('texts/jokes.json') as js:
jk = json.load(js)
try:
if 15 >= int(number) > 0:
clef = str(number)
else:
clef = str(random.randint(1, 15))
except Exception:
clef = str(random.randint(1, 15))
joke = jk["{}".format(clef)]
embed = discord.Embed(title="Blague _{}_ : ".format(clef),
description=joke['content'], colour=0x03C9A9)
embed.set_footer(text="Par " + joke['author'])
embed.set_thumbnail(url='https://outout.tech/tuxbot/blobjoy.png')
await ctx.send(embed=embed)
"""---------------------------------------------------------------------"""
@commands.command()
async def ethylotest(self, ctx):
"""Ethylotest simulator 2018"""
results_poulet = ["Désolé mais mon ethylotest est sous Windows Vista, "
"merci de patienter...",
"_(ethylotest)_ : Une erreur est survenue. Windows "
"cherche une solution à se problème.",
"Mais j'l'ai foutu où ce p\\*\\*\\* d'ethylotest de m\\*\\*\\* "
"bordel fait ch\\*\\*\\*",
"C'est pas possible z'avez cassé l'ethylotest !"]
results_client = ["D'accord, il n'y a pas de problème à cela je suis "
"complètement clean",
"Bien sur si c'est votre devoir !", "Suce bi\\*e !",
"J'ai l'air d'être bourré ?",
"_laissez moi prendre un bonbon à la menthe..._"]
result_p = random.choice(results_poulet)
result_c = random.choice(results_client)
await ctx.send(":oncoming_police_car: Bonjour bonjour, contrôle "
"d'alcoolémie !")
await asyncio.sleep(0.5)
await ctx.send(":man: " + result_c)
await asyncio.sleep(1)
await ctx.send(":police_car: " + result_p)
"""---------------------------------------------------------------------"""
@commands.command()
async def coin(self, ctx):
"""Coin flip simulator 2025"""
starts_msg = ["Je lance la pièce !", "C'est parti !", "C'est une pièce"
" d'un cent faut"
" pas la perdre",
"C'est une pièce d'un euro faut pas la perdre",
"Je lance !"]
results_coin = ["{0} pile", "{0} face", "{1} Heu c'est quoi pile c'est"
" quoi face enfaite ?",
"{1} Oh shit, je crois que je l'ai perdue",
"{1} Et bim je te vol ta pièce !",
"{0} Oh une erreur d'impression il n'y a ni pile ni"
" face !"]
start = random.choice(starts_msg)
result = random.choice(results_coin)
await ctx.send(start)
await asyncio.sleep(0.6)
await ctx.send(result.format(":moneybag: Et la pièce retombe sur ...",
":robot:"))
"""---------------------------------------------------------------------"""
@commands.command()
async def pokemon(self, ctx):
"""Random pokemon fight"""
with open('texts/pokemons.json') as js:
jk = json.load(js)
poke1 = jk[random.randint(1, 150)]
poke2 = jk[random.randint(1, 150)]
try:
if poke1['MaxHP'] > poke2['MaxHP']:
winer = poke1
else:
winer = poke2
except KeyError:
winer = poke1
await ctx.send(":flag_white: **Le combat commence !**")
await asyncio.sleep(1)
await ctx.send(":loudspeaker: Les concurants sont {} contre {} ! Bonne"
" chance à eux !".format(poke1["Name"], poke2["Name"]))
await asyncio.sleep(0.5)
await ctx.send(":boom: {} commence et utilise {}".format(
poke1["Name"], poke1["Fast Attack(s)"][0]["Name"]))
await asyncio.sleep(1)
await ctx.send(":dash: {} réplique avec {}".format(
poke2["Name"], poke2["Fast Attack(s)"][0]["Name"]))
await asyncio.sleep(1.2)
await ctx.send("_le combat continue de se dérouler..._")
await asyncio.sleep(1.5)
await ctx.send(":trophy: Le gagnant est **{}** !".format(
winer["Name"]))
"""---------------------------------------------------------------------"""
@commands.command()
async def randomcat(self, ctx):
"""Display a random cat"""
r = requests.get('http://aws.random.cat/meow')
cat = str(r.json()['file'])
embed = discord.Embed(title="Meow",
description="[Voir le chat plus grand]({})".
format(cat), colour=0x03C9A9)
embed.set_thumbnail(url=cat)
embed.set_author(name="Random.cat", url='https://random.cat/')
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Funs(bot))

View file

@ -1,41 +0,0 @@
import asyncio
import threading
from aiohttp import web
from discord.ext import commands
from bot import TuxBot
class Monitoring(commands.Cog):
def __init__(self):
self.app = web.Application()
t = threading.Thread(
target=self.run_server,
args=(self.aiohttp_server(),)
)
t.start()
def aiohttp_server(self):
async def hi(request):
return web.Response(text="I'm alive !")
self.app.add_routes([web.get('/', hi)])
runner = web.AppRunner(self.app)
return runner
@staticmethod
def run_server(runner):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(runner.setup())
site = web.TCPSite(runner, '0.0.0.0', 3389)
loop.run_until_complete(site.start())
loop.run_forever()
def setup(bot: TuxBot):
bot.add_cog(Monitoring())

View file

@ -1,152 +0,0 @@
from discord.ext import commands
import discord
class Role(commands.Cog):
"""Commandes role."""
def __init__(self, bot):
self.bot = bot
self.ARCH_ROLE = 393077257826205706
self.DEBIAN_ROLE = 393077933209550859
self.RHEL_ROLE = 393078333245751296
self.ANDROID_ROLE = 393087862972612627
self.BSD_ROLE = 401791543708745738
@commands.group(name="role", no_pm=True, pass_context=True,
case_insensitive=True)
async def _role(self, ctx):
"""Affiche l'aide sur la commande role"""
if ctx.message.guild.id != 280805240977227776:
return
if ctx.invoked_subcommand is None:
text = open('texts/roles.md').read()
em = discord.Embed(title='Gestionnaires de rôles',
description=text, colour=0x89C4F9)
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@_role.command(name="arch", aliases=["archlinux", "arch_linux"],
pass_context=True)
async def role_arch(self, ctx):
"""Ajoute/retire le role 'Arch user'"""
roles = ctx.message.author.roles
role_id = []
for role in roles:
role_id.append(role.id)
user = ctx.message.author
if self.ARCH_ROLE in role_id:
await user.remove_roles(discord.Object(id=self.ARCH_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Pourquoi tu viens "
f"de supprimer Arch Linux, c'était trop compliqué "
f"pour toi ? <:sad:343723037331292170>")
else:
await user.add_roles(discord.Object(id=self.ARCH_ROLE))
await ctx.send(f"{ctx.message.author.mention} > How un "
f"ArchLinuxien, c'est bon les ``yaourt`` ? "
f"<:hap:354275645574086656>")
"""---------------------------------------------------------------------"""
@_role.command(name="debian", pass_context=True)
async def role_debian(self, ctx):
"""Ajoute/retire le role 'debian user'"""
roles = ctx.message.author.roles
role_id = []
for role in roles:
role_id.append(role.id)
user = ctx.message.author
if self.DEBIAN_ROLE in role_id:
await user.remove_roles(discord.Object(id=self.DEBIAN_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Adieu ! Tu verras, "
f"APT te manquera ! ")
else:
await user.add_roles(discord.Object(id=self.DEBIAN_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Un utilisateur de "
f"Debian, encore et encore ! "
f"<:stuck_out_tongue:343723077412323339>")
"""---------------------------------------------------------------------"""
@_role.command(name="rhel", pass_context=True)
async def role_rhel(self, ctx):
"""Ajoute/retire le role 'rhel user'"""
roles = ctx.message.author.roles
role_id = []
for role in roles:
role_id.append(role.id)
user = ctx.message.author
if self.RHEL_ROLE in role_id:
await user.remove_roles(discord.Object(id=self.RHEL_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Pourquoi tu t'en "
f"vas, il sont déjà assez seul là-bas "
f"<:sad:343723037331292170>")
else:
await user.add_roles(discord.Object(id=self.RHEL_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Mais, voila "
f"quelqu'un qui porte des chapeaux ! "
f"<:hap:354275645574086656>")
"""---------------------------------------------------------------------"""
@_role.command(name="android", pass_context=True)
async def role_android(self, ctx):
"""Ajoute/retire le role 'android user'"""
roles = ctx.message.author.roles
role_id = []
for role in roles:
role_id.append(role.id)
user = ctx.message.author
if self.ANDROID_ROLE in role_id:
await user.remove_roles(discord.Object(id=self.ANDROID_ROLE))
await ctx.send(f"{ctx.message.author.mention} >How, me dit pas "
f"que tu as compris que les Android's allaient "
f"exterminer le monde ? "
f"<:trollface:375327667160875008>")
else:
await user.add_roles(discord.Object(id=self.ANDROID_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Hey, un utilisateur"
f" d'Android, prêt à continuer l'extermination de "
f"WP et iOS ? "
f"<:stuck_out_tongue:343723077412323339>")
"""---------------------------------------------------------------------"""
@_role.command(name="bsd", pass_context=True)
async def role_bsd(self, ctx):
"""Ajoute/retire le role 'BSD user'"""
roles = ctx.message.author.roles
role_id = []
for role in roles:
role_id.append(role.id)
user = ctx.message.author
if self.BSD_ROLE in role_id:
await user.remove_roles(discord.Object(id=self.BSD_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Ohhhh fait gaffe "
f"ou le démon va te piquer")
else:
await user.add_roles(discord.Object(id=self.BSD_ROLE))
await ctx.send(f"{ctx.message.author.mention} > Quelqu'un sous "
f"BSD ! Au moins il a pas besoin de mettre GNU "
f"devant son OS à chaque fois :d")
"""---------------------------------------------------------------------"""
@_role.command(name="staff", pass_context=True, hidden=True)
async def role_staff(self, ctx):
"""Easter egg"""
await ctx.send(f"{ctx.message.author.mention} > Vous n'avez pas "
f"le rôle staff, tu crois quoi :joy:")
def setup(bot):
bot.add_cog(Role(bot))

View file

@ -1,152 +0,0 @@
from discord.ext import commands
import discord
import asyncio
import urllib.request
import wikipedia
wikipedia.set_lang("fr")
class Search(commands.Cog):
"""Commandes de WWW."""
def __init__(self, bot):
self.bot = bot
@commands.group(name="search", no_pm=True, pass_context=True)
async def _search(self, ctx):
"""Rechercher sur le world wide web"""
if ctx.invoked_subcommand is None:
text = open('texts/search.md').read()
em = discord.Embed(title='Commandes de search TuxBot',
description=text,
colour=0x89C4F9)
await ctx.send(embed=em)
@_search.command(pass_context=True, name="docubuntu")
async def search_docubuntu(self, ctx, args):
attends = await ctx.send("_Je te cherche ça {} !_".format(
ctx.message.author.mention))
html = urllib.request.urlopen("https://doc.ubuntu-fr.org/" +
args).read()
if "avez suivi un lien" in str(html):
await attends.edit(content=":sob: Nooooon ! Cette page n'existe "
"pas, mais tu peux toujours la créer : "
"https://doc.ubuntu-fr.org/" + args)
else:
await attends.delete()
embed = discord.Embed(description="Voila j'ai trouvé ! Voici la "
"page ramenant à votre recherche,"
" toujours aussi bien rédigée "
":wink: : https://doc.ubuntu-fr."
"org/" + args,
url='http://doc.ubuntu-fr.org/')
embed.set_author(name="DocUbuntu-Fr",
url='http://doc.ubuntu-fr.org/')
embed.set_footer(text="Merci à ceux qui ont pris le temps d'écrire "
"cette documentation")
await ctx.send(embed=embed)
@_search.command(pass_context=True, name="docarch")
async def search_docarch(self, ctx, args):
attends = await ctx.send("_Je te cherche ça {} !_".format(
ctx.message.author.mention))
html = urllib.request.urlopen("https://wiki.archlinux.org/index.php/" +
args).read()
if "There is currently no text in this page" in str(html):
await attends.edit(content=":sob: Nooooon ! Cette page n'existe "
"pas.")
else:
await attends.delete()
embed = discord.Embed(description="Voila j'ai trouvé ! Voici la "
"page ramenant à votre recherche,"
" toujours aussi bien rédigée "
":wink: : https://wiki.archlinux."
"org/index.php/" + args,
url='https://wiki.archlinux.org/index.php/')
embed.set_author(name="Doc ArchLinux")
embed.set_footer(text="Merci à ceux qui ont pris le temps d'écrire "
"cette documentation")
await ctx.send(embed=embed)
@_search.command(pass_context=True, name="wikipedia")
async def search_wikipedia(self, ctx: commands.Context, args):
"""Fait une recherche sur wikipd"""
wait = await ctx.send("_Je cherche..._")
results = wikipedia.search(args)
nbmr = 0
mmssgg = ""
for value in results:
nbmr = nbmr + 1
mmssgg = mmssgg + "**{}**: {} \n".format(str(nbmr), value)
em = discord.Embed(title='Résultats de : ' + args,
description = mmssgg,
colour=0x4ECDC4)
em.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/commons/"
"2/26/Paullusmagnus-logo_%28large%29.png")
await wait.delete()
sending = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"]
def check(reaction, user):
return user == ctx.author and reaction.emoji in sending and \
reaction.message.id == msg.id
async def waiter(future: asyncio.Future):
reaction, user = await self.bot.wait_for('reaction_add',
check=check)
future.set_result(reaction.emoji)
emoji = asyncio.Future()
self.bot.loop.create_task(waiter(emoji))
msg = await ctx.send(embed=em)
for e in sending:
await msg.add_reaction(e)
if emoji.done():
break
while not emoji.done():
await asyncio.sleep(0.1)
page = int(sending.index(emoji.result()))
args_ = results[page]
try:
await msg.delete()
await ctx.trigger_typing()
wait = await ctx.send(ctx.message.author.mention +
" ah ok sympa cette recherche, je l'effectue de suite !")
wp = wikipedia.page(args_)
wp_contenu = wp.summary[:200] + "..."
em = discord.Embed(title='Wikipedia : ' + wp.title,
description = "{} \n_Lien_ : {} ".format(
wp_contenu, wp.url),
colour=0x9B59B6)
em.set_author(name="Wikipedia",
url='http://wikipedia.org',
icon_url='https://upload.wikimedia.org/wikipedia/'
'commons/2/26/Paullusmagnus-logo_%28large'
'%29.png')
em.set_thumbnail(url = "https://upload.wikimedia.org/wikipedia/"
"commons/2/26/Paullusmagnus-logo_%28large"
"%29.png")
em.set_footer(text="Merci à eux de nous fournir une encyclopédie "
"libre !")
await wait.delete()
await ctx.send(embed=em)
except wikipedia.exceptions.PageError:
# TODO : A virer dans l'event on_error
await ctx.send(":open_mouth: Une **erreur interne** est survenue,"
" si cela ce reproduit contactez votre"
" administrateur ou faites une Issue sur"
" ``gitea`` !")
def setup(bot):
bot.add_cog(Search(bot))

View file

@ -1,105 +0,0 @@
import datetime
import socket
import discord
from discord.ext import commands
class SendLogs(commands.Cog):
"""Send logs to a specific channel"""
def __init__(self, bot):
self.bot = bot
self.log_channel = None
self.main_server_id = int(self.bot.config.main_server_id)
@commands.Cog.listener()
async def on_resumed(self):
em = discord.Embed(title="Et hop je me reconnecte à l'api 😃",
colour=0x5cb85c)
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
@commands.Cog.listener()
async def on_ready(self):
self.log_channel = await self.bot.fetch_channel(int(self.bot.config.log_channel_id))
em = discord.Embed(title="Je suis opérationnel 😃",
description=f"*Instance lancée sur "
f"{socket.gethostname()}*", colour=0x5cb85c)
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.Cog.listener()
async def on_guild_join(self, guild: discord.Guild):
em = discord.Embed(title=f"On m'a ajouté à : {str(guild.name)} 😃",
colour=0x51A351)
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
@commands.Cog.listener()
async def on_guild_remove(self, guild: discord.Guild):
em = discord.Embed(title=f"On m'a viré de : {str(guild.name)} 😦",
colour=0xBD362F)
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.Cog.listener()
async def on_member_join(self, member):
if member.guild.id == self.main_server_id:
em = discord.Embed(title=f"{str(member)} *`({str(member.id)})`* "
f"nous a rejoint 😃", colour=0x51A351)
em.set_footer(text=f"Compte crée le {member.created_at}")
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
@commands.Cog.listener()
async def on_member_remove(self, member):
if member.guild.id == self.main_server_id:
em = discord.Embed(title=f"{str(member)} *`({str(member.id)})`* "
f"nous a quitté 😦", colour=0xBD362F)
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.Cog.listener()
async def on_message_delete(self, message):
if message.guild.id == self.main_server_id and not message.author.bot:
async def is_a_command(message):
prefix_lenght = len(await self.bot.get_prefix(message))
command = (message.content.split()[0])[prefix_lenght:]
if command == '':
command = "not_a_command"
return self.bot.get_command(str(command))
if await is_a_command(message) is None:
em = discord.Embed(title=f"Message supprimé dans :"
f" {str(message.channel.name)}",
colour=0xBD362F)
em.add_field(name=f"{str(message.author)} "
f"*`({str(message.author.id)})`* "
f"a supprimé :", value=str(message.content))
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
@commands.Cog.listener()
async def on_message_edit(self, before, after):
if before.guild.id == self.main_server_id and not before.author.bot:
em = discord.Embed(title=f"Message edité dans : "
f"{before.channel.name}", colour=0x0088CC)
em.add_field(name=f"{str(before.author)} "
f"*`({str(before.author.id)})`* a"
f" edité :", value=str(before.content))
em.add_field(name="Pour remplacer par :", value=str(after.content))
em.timestamp = datetime.datetime.utcnow()
await self.log_channel.send(embed=em)
def setup(bot):
bot.add_cog(SendLogs(bot))

View file

@ -1,99 +0,0 @@
import asyncio
import discord
from discord.ext import commands
class Sondage(commands.Cog):
"""Commandes sondage."""
def __init__(self, bot):
self.bot = bot
@commands.command(pass_context=True)
async def sondage(self, ctx, *, msg="help"):
if msg != "help":
await ctx.message.delete()
options = msg.split(" | ")
times = [x for x in options if x.startswith("time=")]
if times:
time = int(times[0].strip("time="))
options.remove(times[0])
else:
time = 0
if len(options) <= 1:
raise commands.errors.MissingRequiredArgument
if len(options) >= 22:
return await ctx.send(f"{ctx.message.author.mention}> "
f":octagonal_sign: Vous ne pouvez pas "
f"mettre plus de 20 réponses !")
emoji = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟', '0⃣',
'🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮']
to_react = []
confirmation_msg = f"**{options[0].rstrip('?')}?**:\n\n"
for idx, option in enumerate(options[1:]):
confirmation_msg += f"{emoji[idx]} - {option}\n"
to_react.append(emoji[idx])
confirmation_msg += "*Sondage proposé par* " + \
str(ctx.message.author.mention)
if time == 0:
confirmation_msg += ""
else:
confirmation_msg += f"\n\nVous avez {time} secondes pour voter!"
poll_msg = await ctx.send(confirmation_msg)
for emote in to_react:
await poll_msg.add_reaction(emote)
if time != 0:
await asyncio.sleep(time)
async for message in ctx.message.channel.history():
if message.id == poll_msg.id:
poll_msg = message
results = {}
for reaction in poll_msg.reactions:
if reaction.emoji in to_react:
results[reaction.emoji] = reaction.count - 1
end_msg = "Le sondage est términé. Les résultats sont:\n\n"
for result in results:
end_msg += "{} {} - {} votes\n". \
format(result,
options[emoji.index(result)+1],
results[result])
top_result = max(results, key=lambda key: results[key])
if len([x for x in results
if results[x] == results[top_result]]) > 1:
top_results = []
for key, value in results.items():
if value == results[top_result]:
top_results.append(options[emoji.index(key)+1])
end_msg += "\nLes gagnants sont : {}". \
format(", ".join(top_results))
else:
top_result = options[emoji.index(top_result)+1]
end_msg += "\n\"{}\" est le gagnant!".format(top_result)
await ctx.send(end_msg)
else:
await ctx.send("please use `@tuxbot poll` (this is rewrite version in beta")
await ctx.message.delete()
text = open('texts/rpoll.md').read()
em = discord.Embed(title='Aide sur le sondage',
description=text,
colour=0xEEEEEE)
await ctx.send(embed=em)
def setup(bot):
bot.add_cog(Sondage(bot))

View file

@ -1,603 +0,0 @@
import datetime
import json
import pytz
import random
import urllib
import aiohttp
import ipinfo as ipinfoio
import pydig
import telnetlib
from graphviz import Digraph
from ipwhois.net import Net
from ipwhois.asn import IPASN
import ipwhois
import discord
import requests, re
from discord.ext import commands
import socket
class Utility(commands.Cog):
"""Commandes utilitaires."""
def __init__(self, bot):
self.bot = bot
@commands.group(name="clock", pass_context=True, case_insensitive=True)
async def clock(self, ctx):
"""Display hour in a country"""
if ctx.invoked_subcommand is None:
text = open('texts/clocks.md').read()
em = discord.Embed(title='Liste des Horloges', description=text, colour=0xEEEEEE)
await ctx.send(embed=em)
@clock.command(name="montréal", aliases=["mtl", "montreal"], pass_context=True)
async def clock_montreal(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('America/Montreal'))
site = "http://ville.montreal.qc.ca/"
img = "https://upload.wikimedia.org/wikipedia/commons/e/e0/Rentier_fws_1.jpg"
country = "au Canada, Québec"
description = "Montréal est la deuxième ville la plus peuplée du Canada. Elle se situe dans la région du Québec"
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Montréal', description=f"A [Montréal]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="vancouver", pass_context=True)
async def clock_vancouver(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('America/Vancouver'))
site = "http://vancouver.ca/"
img = "https://upload.wikimedia.org/wikipedia/commons/f/fe/Dock_Vancouver.JPG"
country = "au Canada"
description = "Vancouver, officiellement City of Vancouver, est une cité portuaire au Canada"
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Vancouver', description=f"A [Vancouver]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="new-york",aliases=["ny", "n-y", "new york"], pass_context=True)
async def clock_new_york(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('America/New_York'))
site = "http://www1.nyc.gov/"
img = "https://upload.wikimedia.org/wikipedia/commons/e/e3/NewYork_LibertyStatue.jpg"
country = "aux U.S.A."
description = "New York, est la plus grande ville des États-Unis en termes d'habitants et l'une des plus importantes du continent américain. "
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à New York', description=f"A [str(New York]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="la", aliases=["los-angeles", "losangeles", "l-a", "los angeles"], pass_context=True)
async def clock_la(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('America/Los_Angeles'))
site = "https://www.lacity.org/"
img = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/LA_Skyline_Mountains2.jpg/800px-LA_Skyline_Mountains2.jpg"
country = "aux U.S.A."
description = "Los Angeles est la deuxième ville la plus peuplée des États-Unis après New York. Elle est située dans le sud de l'État de Californie, sur la côte pacifique."
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Los Angeles', description=f"A [Los Angeles]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="paris", aliases=["baguette"],pass_context=True)
async def clock_paris(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('Europe/Paris'))
site = "http://www.paris.fr/"
img = "https://upload.wikimedia.org/wikipedia/commons/a/af/Tour_eiffel_at_sunrise_from_the_trocadero.jpg"
country = "en France"
description = "Paris est la capitale de la France. Elle se situe au cœur d'un vaste bassin sédimentaire aux sols fertiles et au climat tempéré, le bassin parisien."
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Paris', description=f"A [Paris]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="berlin", pass_context=True)
async def clock_berlin(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('Europe/Berlin'))
site = "http://www.berlin.de/"
img = "https://upload.wikimedia.org/wikipedia/commons/9/91/Eduard_Gaertner_Schlossfreiheit.jpg"
country = "en Allemagne"
description = "Berlin est la capitale et la plus grande ville d'Allemagne. Située dans le nord-est du pays, elle compte environ 3,5 millions d'habitants. "
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Berlin', description=f"A [Berlin]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="berne", aliases=["zurich", "bern"], pass_context=True)
async def clock_berne(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('Europe/Zurich'))
site = "http://www.berne.ch/"
img = "https://upload.wikimedia.org/wikipedia/commons/d/db/Justitia_Statue_02.jpg"
country = "en Suisse"
description = "Berne est la cinquième plus grande ville de Suisse et la capitale du canton homonyme. Depuis 1848, Berne est la « ville fédérale »."
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Berne', description=f"A [Berne]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="tokyo", pass_context=True)
async def clock_tokyo(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('Asia/Tokyo'))
site = "http://www.gotokyo.org/"
img = "https://upload.wikimedia.org/wikipedia/commons/3/37/TaroTokyo20110213-TokyoTower-01.jpg"
country = "au Japon"
description = "Tokyo, anciennement Edo, officiellement la préfecture métropolitaine de Tokyo, est la capitale du Japon."
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Tokyo', description=f"A [Tokyo]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
@clock.command(name="moscou", aliases=["moscow", "moskova"], pass_context=True)
async def clock_moscou(self, ctx):
then = datetime.datetime.now(pytz.utc)
utc = then.astimezone(pytz.timezone('Europe/Moscow'))
site = "https://www.mos.ru/"
img = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Andreyevsky_Zal.jpg"
country = "en Russie"
description = "Moscou est la capitale de la Fédération de Russie et la plus grande ville d'Europe. Moscou est situé sur la rivière Moskova. "
form = '%H heures %M'
tt = utc.strftime(form)
em = discord.Embed(title='Heure à Moscou', description=f"A [Moscou]({site}) {country}, Il est **{str(tt)}** ! \n {description} \n _source des images et du texte : [Wikimedia foundation](http://commons.wikimedia.org/)_", colour=0xEEEEEE)
em.set_thumbnail(url = img)
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command()
async def ytdiscover(self, ctx):
"""Random youtube channel"""
with open('texts/ytb.json') as js:
ytb = json.load(js)
clef = str(random.randint(0,12))
chaine = ytb["{}".format(clef)]
embed = discord.Embed(title=chaine['name'],
url=chaine['url'],
description=f"**{chaine['name']}**, {chaine['desc']} \n[Je veux voir ça]({chaine['url']})")
embed.set_thumbnail(url='https://outout.tech/tuxbot/yt.png')
await ctx.send(embed=embed)
"""---------------------------------------------------------------------"""
@commands.command(name='iplocalise', pass_context=True)
async def _iplocalise(self, ctx, ipaddress, iptype=""):
realipaddress = ipaddress
"""Getting headers."""
if ipaddress.startswith("http://"):
if ipaddress[-1:] == '/':
ipaddress = ipaddress[:-1]
ipaddress = ipaddress.split("http://")[1]
if ipaddress.startswith("https://"):
if ipaddress[-1:] == '/':
ipaddress = ipaddress[:-1]
ipaddress = ipaddress.split("https://")[1]
if(iptype=="ipv6" or iptype=="v6" or iptype=="-6"):
try:
ipaddress = socket.getaddrinfo(ipaddress, None, socket.AF_INET6)[1][4][0]
except Exception as e:
await ctx.send("Erreur, cette adresse n'est pas disponible en IPv6.")
return
elif(iptype=="ipv4" or iptype=="v4" or iptype=="-4"):
try:
ipaddress = socket.getaddrinfo(ipaddress, None, socket.AF_INET)[1][4][0]
except Exception as e:
await ctx.send("Erreur, cette adresse n'est pas disponible en IPv4.")
return
else:
try:
ipaddress = socket.getaddrinfo(ipaddress, None)[1][4][0]
except Exception as e:
await ctx.send("Erreur, cette adresse n'est pas disponible.")
return
iploading = await ctx.send("_Récupération des informations..._")
try:
net = Net(ipaddress)
obj = IPASN(net)
ipinfo = obj.lookup()
except ipwhois.exceptions.IPDefinedError:
await ctx.send("Cette IP est reservée à un usage local selon la RFC 1918. Impossible d'avoir des informations supplémentaires à son propos.")
await iploading.delete()
return
try:
iphostname = socket.gethostbyaddr(ipaddress)[0]
except:
iphostname = "N/A"
# IPINFO api
api_result = True
try:
with open('ipinfoio.key') as k:
access_token = k.read().replace("\n", "")
handler = ipinfoio.getHandler(access_token)
details = handler.getDetails(ipaddress)
except Exception as e:
api_result = False
try:
embed = discord.Embed(title=f"Informations pour ``{realipaddress} ({ipaddress})``", color=0x5858d7)
if(api_result):
asn = details.org.split(" ")[0]
embed.add_field(name="Appartient à :", value=f"[{details.org}](https://bgp.he.net/{asn})")
else:
embed.add_field(name="Appartient à :", value=f"{ipinfo['asn_description']} ([AS{ipinfo['asn']}](https://bgp.he.net/{ipinfo['asn']}))", inline = False)
embed.add_field(name="RIR :", value=f"{ipinfo['asn_registry']}", inline = True)
if(api_result):
embed.add_field(name="Région :", value=f"{details.city} - {details.region} ({details.country})")
else:
embed.add_field(name="Région :", value=f"{ipinfo['asn_country_code']}")
embed.add_field(name="Nom de l'hôte :", value=f"{iphostname}")
# Adding country flag
if(api_result):
embed.set_thumbnail(url=f"https://www.countryflags.io/{details.country}/shiny/64.png")
else:
embed.set_thumbnail(url=f"https://www.countryflags.io/{ipinfo['asn_country_code']}/shiny/64.png")
await ctx.send(embed=embed)
except:
await ctx.send(content=f"Erreur, impossible d'avoir des informations sur l'adresse IP ``{realipaddress}``")
await iploading.delete()
"""---------------------------------------------------------------------"""
@commands.command(name='dig', pass_context=True)
async def _dig(self, ctx, domain, querytype="abc", dnssec="no"):
if not querytype in ['A', 'AAAA', 'CNAME', 'NS', 'DS', 'DNSKEY', 'SOA', 'TXT', 'PTR', 'MX']:
await ctx.send("Requêtes supportées : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX")
return
if(dnssec == "no"):
resolver = pydig.Resolver(
nameservers=[
'80.67.169.40',
'80.67.169.12',
]
)
else:
resolver = pydig.Resolver(
nameservers=[
'80.67.169.40',
'80.67.169.12',
],
additional_args=[
'+dnssec',
]
)
resquery = resolver.query(domain, querytype)
embed = discord.Embed(title=f"Requête DIG sur {domain} pour une entrée {querytype}", color=0x5858d7)
champ_id = 1
for champ in resquery:
embed.add_field(name=f"Champ {champ_id} :", value=champ)
champ_id = champ_id + 1
if champ_id == 1:
embed.add_field(name="Ooops", value="Pas de résultat")
await ctx.send(embed=embed)
"""---------------------------------------------------------------------"""
@commands.command(name='getheaders')
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"Headers : {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"Cannot connect to host {addr}"
)
"""---------------------------------------------------------------------"""
@commands.command(name='peeringdb', pass_context=True)
async def _peeringdb(self, ctx, *, asn):
def notEmptyField(embed, name, value):
if(value != ""):
embed.add_field(name=name, value=value)
if asn.startswith("AS"):
asn = asn[2:]
loadingmsg = await ctx.send("_Récupération des informations..._")
"""Getting the ASN id in the peeringdb database"""
try:
asnid = urllib.request.urlopen("https://www.peeringdb.com/api/as_set/" + asn)
asnid = json.loads(asnid.read().decode())
pdbid = asnid["data"][0][asn]
asinfo = urllib.request.urlopen("https://www.peeringdb.com/api/net?irr_as_set=" + pdbid)
asinfo = json.loads(asinfo.read().decode())["data"]
for asndata in asinfo:
if(asndata['asn'] == int(asn)):
asinfo = asndata
asproto = ""
if(asinfo["info_ipv6"]):
asproto = asproto + "IPv6 "
if(asinfo["info_unicast"]):
asproto = asproto + "Unicast "
if(asinfo["info_multicast"]):
asproto = asproto + "Multicast "
if(asinfo["info_never_via_route_servers"]):
asproto = asproto + "Never via Route servers"
embed = discord.Embed(title=f"Informations pour {asinfo['name']} ``AS{asn}``", color=0x5858d7)
notEmptyField(embed, name="Nom :", value=asinfo['name'])
notEmptyField(embed, name="Aka :", value=asinfo['aka'])
notEmptyField(embed, name="Site :", value=asinfo['website'])
notEmptyField(embed, name="Looking Glass :", value=asinfo['looking_glass'])
notEmptyField(embed, name="Traffic :", value=asinfo['info_traffic'])
notEmptyField(embed, name="Ratio du traffic :", value=asinfo['info_ratio'])
notEmptyField(embed, name="Prefixes IPv4 :", value=asinfo['info_prefixes4'])
notEmptyField(embed, name="Prefixes IPv6 :", value=asinfo['info_prefixes6'])
notEmptyField(embed, name="Politique de Peering :", value=f"[{asinfo['policy_general']}]({asinfo['policy_url']})")
notEmptyField(embed, name="Protocoles supportés :", value=asproto)
embed.set_footer(text=f"https://www.peeringdb.com/")
await ctx.send(embed=embed)
await loadingmsg.delete()
except IndexError:
await ctx.send(f"Impossible d'avoir des informations sur l'AS AS{asn}")
await loadingmsg.delete()
except urllib.error.HTTPError:
await ctx.send(f"L'AS{asn} est introuvable dans la base de données de PeeringDB.")
await loadingmsg.delete()
"""---------------------------------------------------------------------"""
@commands.command(name='shroute', pass_context=True)
async def _shroute(self, ctx, srv, ipaddress):
"""Show as path graph to an IP via data from a Route Server using graphviz"""
if not srv in ["opentransit", 'he', 'att', "oregonuniv", "warian", 'csaholdigs', 'iamageeknz']:
await ctx.send("Requêtes supportées : opentransit (Orange), he (Huricanne Electric), att (AT&T), oregonuniv, warian, csaholdigs, iamageeknz")
return
#List of RS
if srv == "opentransit":
host = "route-server.opentransit.net"
user = "rviews"
password = "Rviews"
lg_asn = "5511"
cmd = "show bgp {}"
elif srv == "oregonuniv":
host = "route-views.routeviews.org"
user = "rviews"
password = "none"
lg_asn = "3582"
cmd = "show bgp {}"
elif srv == "warian":
host = "route-server.warian.net"
user = "none"
password = "rviews"
lg_asn = "56911"
cmd = "show bgp ipv4 unicast {}"
elif srv == "csaholdigs": #Blacklist sometime
host = "route-views.sg.routeviews.org"
user = "none"
password = "none"
lg_asn = "45494"
cmd = "show bgp ipv4 unicast {}"
elif srv == "he": #Blacklist sometime
host = "route-server.he.net"
user = "none"
password = "none"
lg_asn = "6939"
cmd = "show bgp ipv4 unicast {}"
elif srv == "iamageeknz": #Blacklist sometime
host = "rs.as45186.net"
user = "none"
password = "none"
lg_asn = "45186"
cmd = "show bgp ipv4 unicast {}"
elif srv == "att":
host = "route-server.ip.att.net"
user = "rviews"
password = "rviews"
lg_asn = "7018"
cmd = "show route {}"
ip = ipaddress
await ctx.send("Connexion en cours au route server...")
tn = telnetlib.Telnet(host)
#Login to the RS via Telnet
if user != "none":
if(srv == "att"):
tn.read_until("login: ".encode())
tn.write((user + "\n").encode())
else:
tn.read_until("Username: ".encode())
tn.write((user + "\n").encode())
if password != "none":
if(srv == "att"):
tn.read_until("Password:".encode())
tn.write((password + "\n").encode())
else:
tn.read_until("Password: ".encode())
tn.write((password + "\n").encode())
await ctx.send("Connecté ! Récupération des données...")
#Sending show route via telnet to the RS
tn.write((cmd + "\n").format(ip).encode())
tn.write(chr(25).encode())
tn.write(chr(25).encode())
tn.write(chr(25).encode())
tn.write("q\n".encode())
tn.write("exit\n".encode())
await ctx.send("Données récupérées ! Traitement en cours")
#Parsing data
data = tn.read_all().decode("utf-8")
paths = {}
#Parsing as paths
paths["as_list"] = re.findall(r" ([0-9][0-9 ]+),", data)
if(paths["as_list"] == []):
paths["as_list"] = re.findall(r" ([0-9][0-9 ]+)[^0-9.]", data)
#Custom parsing for AT&T
if(srv == "att"):
paths["as_list"] = re.findall(r"(?<=AS path: 7018 )[0-9][0-9 ]+[^ I]", data)
#Graphviz diagram
g = Digraph('G', filename='bgpgraph', format='png', graph_attr={'rankdir':'LR', 'concentrate': 'true'})
#Diagram paths generation
as_path_count = 0
for as_path in paths['as_list']:
as_path = as_path.split(" ")
as_path.reverse()
original_asn = as_path[0]
border_asn = as_path[-1]
precedent_asn = original_asn
for asn in as_path:
if asn != "2001": #Cause HE got a default or something weird to this asn
if asn != original_asn:
g.edge("AS" + asn, "AS" + precedent_asn)
precedent_asn = asn
if asn == border_asn:
g.edge("AS" + lg_asn, "AS" + asn)
as_path_count += 1
#If empty as_path
if as_path_count == 0:
await ctx.send("Pas de route trouvée vers l'IP demandée depuis le route server choisi.")
return
#Render the graph
g.render()
#Send it
with open('bgpgraph.png', 'rb') as fp:
await ctx.send(file=discord.File(fp, 'bgpgraph.png'))
"""---------------------------------------------------------------------"""
@commands.command(name='git', pass_context=True)
async def _git(self, ctx):
"""Pour voir mon code"""
text = "How tu veux voir mon repos Gitea pour me disséquer ? " \
"Pas de soucis ! Je suis un Bot, je ne ressens pas la " \
"douleur !\n https://git.gnous.eu/gnouseu/tuxbot-bot"
em = discord.Embed(title='Repos TuxBot-Bot', description=text, colour=0xE9D460)
em.set_author(name='Gnous', icon_url="https://cdn.discordapp.com/"
"icons/280805240977227776/"
"9ba1f756c9d9bfcf27989d0d0abb3862"
".png")
await ctx.send(embed=em)
"""---------------------------------------------------------------------"""
@commands.command(name='quote', pass_context=True)
async def _quote(self, ctx, quote_id):
global quoted_message
async def get_message(message_id: int):
for channel in ctx.message.guild.channels:
if isinstance(channel, discord.TextChannel):
test_chan = await self.bot.fetch_channel(channel.id)
try:
return await test_chan.fetch_message(message_id)
except (discord.NotFound, discord.Forbidden):
pass
return None
quoted_message = await get_message(int(quote_id))
if quoted_message is not None:
embed = discord.Embed(colour=quoted_message.author.colour,
description=quoted_message.clean_content,
timestamp=quoted_message.created_at)
embed.set_author(name=quoted_message.author.display_name,
icon_url=quoted_message.author.avatar_url_as(
format="jpg"))
if len(quoted_message.attachments) >= 1:
embed.set_image(url=quoted_message.attachments[0].url)
embed.add_field(name="**Original**",
value=f"[Go!]({quoted_message.jump_url})")
embed.set_footer(text="#" + quoted_message.channel.name)
await ctx.send(embed=embed)
else:
await ctx.send("Impossible de trouver le message.")
def setup(bot):
bot.add_cog(Utility(bot))

View file

@ -1,124 +0,0 @@
from discord.ext import commands
def is_owner_check(message):
return str(message.author.id) in ['171685542553976832',
'269156684155453451']
def is_owner(warn=True):
def check(ctx, log):
owner = is_owner_check(ctx.message)
if not owner and log:
print(ctx.message.author.name + " à essayer d'executer " + ctx.message.content + " sur le serveur " + ctx.message.guild.name)
return owner
owner = commands.check(lambda ctx: check(ctx, warn))
return owner
"""-------------------------------------------------------------------------"""
async def check_permissions(ctx, perms, *, check=all):
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner or is_owner_check(ctx.message) is True:
return True
resolved = ctx.channel.permissions_for(ctx.author)
return check(getattr(resolved, name, None) == value for name, value in
perms.items())
def has_permissions(*, check=all, **perms):
async def pred(ctx):
return await check_permissions(ctx, perms, check=check)
return commands.check(pred)
async def check_guild_permissions(ctx, perms, *, check=all):
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner:
return True
if ctx.guild is None:
return False
resolved = ctx.author.guild_permissions
return check(getattr(resolved, name, None) == value for name, value in
perms.items())
def has_guild_permissions(*, check=all, **perms):
async def pred(ctx):
return await check_guild_permissions(ctx, perms, check=check)
return commands.check(pred)
# These do not take channel overrides into account
def is_mod():
async def pred(ctx):
return await check_guild_permissions(ctx, {'manage_guild': True})
return commands.check(pred)
def is_admin():
async def pred(ctx):
return await check_guild_permissions(ctx, {'administrator': True})
return commands.check(pred)
def mod_or_permissions(**perms):
perms['manage_guild'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
def admin_or_permissions(**perms):
perms['administrator'] = True
async def predicate(ctx):
return await check_guild_permissions(ctx, perms, check=any)
return commands.check(predicate)
def is_in_guilds(*guild_ids):
def predicate(ctx):
guild = ctx.guild
if guild is None:
return False
return guild.id in guild_ids
return commands.check(predicate)
def get_user(message, user):
try:
member = message.mentions[0]
except:
member = message.guild.get_member_named(user)
if not member:
try:
member = message.guild.get_member(int(user))
except ValueError:
pass
if not member:
return None
return member
def check_date(date: str):
if len(date) == 1:
return f"0{date}"
else:
return date

View file

@ -1,46 +0,0 @@
class text_colors:
BLACK = '\033[30m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
LIGHT_GRAY = '\033[37m'
DARK_GRAY = '\033[90m'
LIGHT_RED = '\033[91m'
LIGHT_GREEN = '\033[92m'
LIGHT_YELLOW = '\033[93m'
LIGHT_BLUE = '\033[94m'
LIGHT_MAGENTA = '\033[95m'
LIGHT_CYAN = '\033[96m'
WHITE = '\033[97m'
class bg_colors:
BLACK = '\033[40m'
RED = '\033[41m'
GREEN = '\033[42m'
YELLOW = '\033[43m'
BLUE = '\033[44m'
MAGENTA = '\033[45m'
CYAN = '\033[46m'
LIGHT_GRAY = '\033[47m'
DARK_GRAY = '\033[100m'
LIGHT_RED = '\033[101m'
LIGHT_GREEN = '\033[102m'
LIGHT_YELLOW = '\033[103m'
LIGHT_BLUE = '\033[104m'
LIGHT_MAGENTA = '\033[105m'
LIGHT_CYAN = '\033[106m'
WHITE = '\033[107m'
class text_style:
BOLD = '\033[1m'
DIM = '\033[2m'
UNDERLINE = '\033[4m'
BLINK = '\033[5m'
ENDC = '\033[0m'

View file

@ -1,29 +0,0 @@
import pymysql
def connect_to_db(self):
mysqlHost = self.bot.config.mysql["host"]
mysqlUser = self.bot.config.mysql["username"]
mysqlPass = self.bot.config.mysql["password"]
mysqlDB = self.bot.config.mysql["dbname"]
try:
return pymysql.connect(host=mysqlHost, user=mysqlUser,
passwd=mysqlPass, db=mysqlDB, charset='utf8')
except KeyError:
print(
"Rest in peperoni, Impossible de se connecter a la base de données.")
print(str(KeyError))
return
def reconnect_to_db(self):
if not self.conn:
mysqlHost = self.bot.config.mysql["host"]
mysqlUser = self.bot.config.mysql["username"]
mysqlPass = self.bot.config.mysql["password"]
mysqlDB = self.bot.config.mysql["dbname"]
return pymysql.connect(host=mysqlHost, user=mysqlUser,
passwd=mysqlPass, db=mysqlDB, charset='utf8')
return self.conn

View file

@ -1,75 +0,0 @@
async def entry_to_code(bot, entries):
width = max(map(lambda t: len(t[0]), entries))
output = ['```']
fmt = '{0:<{width}}: {1}'
for name, entry in entries:
output.append(fmt.format(name, entry, width=width))
output.append('```')
await bot.say('\n'.join(output))
import datetime
async def indented_entry_to_code(bot, entries):
width = max(map(lambda t: len(t[0]), entries))
output = ['```']
fmt = '\u200b{0:>{width}}: {1}'
for name, entry in entries:
output.append(fmt.format(name, entry, width=width))
output.append('```')
await bot.say('\n'.join(output))
async def too_many_matches(bot, msg, matches, entry):
check = lambda m: m.content.isdigit()
await bot.say('There are too many matches... Which one did you mean? **Only say the number**.')
await bot.say('\n'.join(map(entry, enumerate(matches, 1))))
# only give them 3 tries.
for i in range(3):
message = await bot.wait_for_message(author=msg.author, channel=msg.channel, check=check)
index = int(message.content)
try:
return matches[index - 1]
except:
await bot.say('Please give me a valid number. {} tries remaining...'.format(2 - i))
raise ValueError('Too many tries. Goodbye.')
class Plural:
def __init__(self, **attr):
iterator = attr.items()
self.name, self.value = next(iter(iterator))
def __str__(self):
v = self.value
if v > 1:
return '%s %ss' % (v, self.name)
return '%s %s' % (v, self.name)
def human_timedelta(dt):
now = datetime.datetime.utcnow()
delta = now - dt
hours, remainder = divmod(int(delta.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
days, hours = divmod(hours, 24)
years, days = divmod(days, 365)
if years:
if days:
return '%s and %s ago' % (Plural(year=years), Plural(day=days))
return '%s ago' % Plural(year=years)
if days:
if hours:
return '%s and %s ago' % (Plural(day=days), Plural(hour=hours))
return '%s ago' % Plural(day=days)
if hours:
if minutes:
return '%s and %s ago' % (Plural(hour=hours), Plural(minute=minutes))
return '%s ago' % Plural(hour=hours)
if minutes:
if seconds:
return '%s and %s ago' % (Plural(minute=minutes), Plural(second=seconds))
return '%s ago' % Plural(minute=minutes)
return '%s ago' % Plural(second=seconds)

View file

@ -1,147 +0,0 @@
#!/bin/env python
# With credit to DanielKO
from lxml import etree
import datetime, re
import asyncio, aiohttp
NINTENDO_LOGIN_PAGE = "https://id.nintendo.net/oauth/authorize"
SPLATNET_CALLBACK_URL = "https://splatoon.nintendo.net/users/auth/nintendo/callback"
SPLATNET_CLIENT_ID = "12af3d0a3a1f441eb900411bb50a835a"
SPLATNET_SCHEDULE_URL = "https://splatoon.nintendo.net/schedule"
class Rotation(object):
def __init__(self):
self.start = None
self.end = None
self.turf_maps = []
self.ranked_mode = None
self.ranked_maps = []
@property
def is_over(self):
return self.end < datetime.datetime.utcnow()
def __str__(self):
now = datetime.datetime.utcnow()
prefix = ''
if self.start > now:
minutes_delta = int((self.start - now) / datetime.timedelta(minutes=1))
hours = int(minutes_delta / 60)
minutes = minutes_delta % 60
prefix = '**In {0} hours and {1} minutes**:\n'.format(hours, minutes)
else:
prefix = '**Current Rotation**:\n'
fmt = 'Turf War is {0[0]} and {0[1]}\n{1} is {2[0]} and {2[1]}'
return prefix + fmt.format(self.turf_maps, self.ranked_mode, self.ranked_maps)
# based on https://github.com/Wiwiweb/SakuraiBot/blob/master/src/sakuraibot.py
async def get_new_splatnet_cookie(username, password):
parameters = {'client_id': SPLATNET_CLIENT_ID,
'response_type': 'code',
'redirect_uri': SPLATNET_CALLBACK_URL,
'username': username,
'password': password}
async with aiohttp.post(NINTENDO_LOGIN_PAGE, data=parameters) as response:
cookie = response.history[-1].cookies.get('_wag_session')
if cookie is None:
print(req)
raise Exception("Couldn't retrieve cookie")
return cookie
def parse_splatnet_time(timestr):
# time is given as "MM/DD at H:MM [p|a].m. (PDT|PST)"
# there is a case where it goes over the year, e.g. 12/31 at ... and then 1/1 at ...
# this case is kind of weird though and is currently unexpected
# it could even end up being e.g. 12/31/2015 ... and then 1/1/2016 ...
# we'll never know
regex = r'(?P<month>\d+)\/(?P<day>\d+)\s*at\s*(?P<hour>\d+)\:(?P<minutes>\d+)\s*(?P<p>a\.m\.|p\.m\.)\s*\((?P<tz>.+)\)'
m = re.match(regex, timestr.strip())
if m is None:
raise RuntimeError('Apparently the timestamp "{}" does not match the regex.'.format(timestr))
matches = m.groupdict()
tz = matches['tz'].strip().upper()
offset = None
if tz == 'PDT':
# EDT is UTC - 4, PDT is UTC - 7, so you need +7 to make it UTC
offset = +7
elif tz == 'PST':
# EST is UTC - 5, PST is UTC - 8, so you need +8 to make it UTC
offset = +8
else:
raise RuntimeError('Unknown timezone found: {}'.format(tz))
pm = matches['p'].replace('.', '') # a.m. -> am
current_time = datetime.datetime.utcnow()
# Kind of hacky.
fmt = "{2}/{0[month]}/{0[day]} {0[hour]}:{0[minutes]} {1}".format(matches, pm, current_time.year)
splatoon_time = datetime.datetime.strptime(fmt, '%Y/%m/%d %I:%M %p') + datetime.timedelta(hours=offset)
# check for new year
if current_time.month == 12 and splatoon_time.month == 1:
splatoon_time.replace(current_time.year + 1)
return splatoon_time
async def get_splatnet_schedule(splatnet_cookie):
cookies = {'_wag_session': splatnet_cookie}
"""
This is repeated 3 times:
<span class"stage-schedule"> ... </span> <--- figure out how to parse this
<div class="stage-list">
<div class="match-type">
<span class="icon-regular-match"></span> <--- turf war
</div>
... <span class="map-name"> ... </span>
... <span class="map-name"> ... </span>
</div>
<div class="stage-list">
<div class="match-type">
<span class="icon-earnest-match"></span> <--- ranked
</div>
... <span class="rule-description"> ... </span> <--- Splat Zones, Rainmaker, Tower Control
... <span class="map-name"> ... </span>
... <span class="map-name"> ... </span>
</div>
"""
schedule = []
async with aiohttp.get(SPLATNET_SCHEDULE_URL, cookies=cookies, data={'locale':"en"}) as response:
text = await response.text()
root = etree.fromstring(text, etree.HTMLParser())
stage_schedule_nodes = root.xpath("//*[@class='stage-schedule']")
stage_list_nodes = root.xpath("//*[@class='stage-list']")
if len(stage_schedule_nodes)*2 != len(stage_list_nodes):
raise RuntimeError("SplatNet changed, need to update the parsing!")
for sched_node in stage_schedule_nodes:
r = Rotation()
start_time, end_time = sched_node.text.split("~")
r.start = parse_splatnet_time(start_time)
r.end = parse_splatnet_time(end_time)
tw_list_node = stage_list_nodes.pop(0)
r.turf_maps = tw_list_node.xpath(".//*[@class='map-name']/text()")
ranked_list_node = stage_list_nodes.pop(0)
r.ranked_maps = ranked_list_node.xpath(".//*[@class='map-name']/text()")
r.ranked_mode = ranked_list_node.xpath(".//*[@class='rule-description']/text()")[0]
schedule.append(r)
return schedule

View file

@ -1,140 +0,0 @@
import asyncio
class Menu:
"""An interactive menu class for Discord."""
class Submenu:
"""A metaclass of the Menu class."""
def __init__(self, name, content):
self.content = content
self.leads_to = []
self.name = name
def get_text(self):
text = ""
for idx, menu in enumerate(self.leads_to):
text += "[{}] {}\n".format(idx+1, menu.name)
return text
def get_child(self, child_idx):
try:
return self.leads_to[child_idx]
except IndexError:
raise IndexError("child index out of range")
def add_child(self, child):
self.leads_to.append(child)
class InputSubmenu:
"""A metaclass of the Menu class for submenu options that take input, instead of prompting the user to pick an option."""
def __init__(self, name, content, input_function, leads_to):
self.content = content
self.name = name
self.input_function = input_function
self.leads_to = leads_to
def next_child(self):
return self.leads_to
class ChoiceSubmenu:
"""A metaclass of the Menu class for submenu options for choosing an option from a list."""
def __init__(self, name, content, options, input_function, leads_to):
self.content = content
self.name = name
self.options = options
self.input_function = input_function
self.leads_to = leads_to
def next_child(self):
return self.leads_to
def __init__(self, main_page):
self.children = []
self.main = self.Submenu("main", main_page)
def add_child(self, child):
self.main.add_child(child)
async def start(self, ctx):
current = self.main
menu_msg = None
while True:
output = ""
if type(current) == self.Submenu:
if type(current.content) == str:
output += current.content + "\n"
elif callable(current.content):
current.content()
else:
raise TypeError("submenu body is not a str or function")
if not current.leads_to:
if not menu_msg:
menu_msg = await ctx.send("```" + output + "```")
else:
await menu_msg.edit(content="```" + output + "```")
break
output += "\n" + current.get_text() + "\n"
output += "Enter a number."
if not menu_msg:
menu_msg = await ctx.send("```" + output + "```")
else:
await menu_msg.edit(content="```" + output + "```")
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
await reply.delete()
try:
current = current.get_child(int(reply.content) - 1)
except IndexError:
print("Invalid number.")
break
elif type(current) == self.InputSubmenu:
if type(current.content) == list:
answers = []
for question in current.content:
await menu_msg.edit(content="```" + question + "\n\nEnter a value." + "```")
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
await reply.delete()
answers.append(reply)
current.input_function(*answers)
else:
await menu_msg.edit(content="```" + current.content + "\n\nEnter a value." + "```")
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel)
await reply.delete()
current.input_function(reply)
if not current.leads_to:
break
current = current.leads_to
elif type(current) == self.ChoiceSubmenu:
result = "```" + current.content + "\n\n"
if type(current.options) == dict:
indexes = {}
for idx, option in enumerate(current.options):
result += "[{}] {}: {}\n".format(idx+1, option, current.options[option])
indexes[idx] = option
else:
for idx, option in current.options:
result += "[{}] {}\n".format(idx+1, option)
await menu_msg.edit(content=result + "\nPick an option.```")
reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.content.isdigit() and m.channel == ctx.message.channel)
await reply.delete()
if type(current.options) == dict:
current.input_function(reply, indexes[int(reply.content)-1])
else:
current.input_function(reply, current.options[reply-1])
if not current.leads_to:
break
current = current.leads_to

View file

@ -1,503 +0,0 @@
# Help paginator by Rapptz
# Edited by F4stZ4p
import asyncio
import discord
class CannotPaginate(Exception):
pass
class Pages:
"""Implements a paginator that queries the user for the
pagination interface.
Pages are 1-index based, not 0-index based.
If the user does not reply within 2 minutes then the pagination
interface exits automatically.
Parameters
------------
ctx: Context
The context of the command.
entries: List[str]
A list of entries to paginate.
per_page: int
How many entries show up per page.
show_entry_count: bool
Whether to show an entry count in the footer.
Attributes
-----------
embed: discord.Embed
The embed object that is being used to send pagination info.
Feel free to modify this externally. Only the description,
footer fields, and colour are internally modified.
permissions: discord.Permissions
Our permissions for the channel.
"""
def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True):
self.bot = ctx.bot
self.entries = entries
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.per_page = per_page
pages, left_over = divmod(len(self.entries), self.per_page)
if left_over:
pages += 1
self.maximum_pages = pages
self.embed = discord.Embed(colour=discord.Color.green())
self.paginating = len(entries) > per_page
self.show_entry_count = show_entry_count
self.reaction_emojis = [
('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page),
('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page),
('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page),
('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page),
('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ),
('\N{BLACK SQUARE FOR STOP}', self.stop_pages),
('\N{INFORMATION SOURCE}', self.show_help),
]
if ctx.guild is not None:
self.permissions = self.channel.permissions_for(ctx.guild.me)
else:
self.permissions = self.channel.permissions_for(ctx.bot.user)
if not self.permissions.embed_links:
raise CannotPaginate('Bot does not have embed links permission.')
if not self.permissions.send_messages:
raise CannotPaginate('Bot cannot send messages.')
if self.paginating:
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CannotPaginate('Bot does not have add reactions permission.')
if not self.permissions.read_message_history:
raise CannotPaginate('Bot does not have Read Message History permission.')
def get_page(self, page):
base = (page - 1) * self.per_page
return self.entries[base:base + self.per_page]
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
p = []
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
p.append(f'{index}. {entry}')
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
self.embed.description = '\n'.join(p)
return await self.channel.send(embed=self.embed)
if not first:
self.embed.description = '\n'.join(p)
await self.message.edit(embed=self.embed)
return
p.append('')
p.append('Confused? React with \N{INFORMATION SOURCE} for more info.')
self.embed.description = '\n'.join(p)
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def checked_show_page(self, page):
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
async def first_page(self):
"""goes to the first page"""
await self.show_page(1)
async def last_page(self):
"""goes to the last page"""
await self.show_page(self.maximum_pages)
async def next_page(self):
"""goes to the next page"""
await self.checked_show_page(self.current_page + 1)
async def previous_page(self):
"""goes to the previous page"""
await self.checked_show_page(self.current_page - 1)
async def show_current_page(self):
if self.paginating:
await self.show_page(self.current_page)
async def numbered_page(self):
"""lets you type a page number to go to"""
to_delete = []
to_delete.append(await self.channel.send('What page do you want to go to?'))
def message_check(m):
return m.author == self.author and \
self.channel == m.channel and \
m.content.isdigit()
try:
msg = await self.bot.wait_for('message', check=message_check, timeout=30.0)
except asyncio.TimeoutError:
to_delete.append(await self.channel.send('Took too long.'))
await asyncio.sleep(5)
else:
page = int(msg.content)
to_delete.append(msg)
if page != 0 and page <= self.maximum_pages:
await self.show_page(page)
else:
to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})'))
await asyncio.sleep(5)
try:
await self.channel.delete_messages(to_delete)
except Exception:
pass
async def show_help(self):
"""shows this message"""
messages = ['Welcome to the interactive paginator!\n']
messages.append('This interactively allows you to see pages of text by navigating with ' \
'reactions. They are as follows:\n')
for (emoji, func) in self.reaction_emojis:
messages.append(f'{emoji} {func.__doc__}')
self.embed.description = '\n'.join(messages)
self.embed.clear_fields()
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(60.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def stop_pages(self):
"""stops the interactive pagination session"""
await self.message.delete()
self.paginating = False
def react_check(self, reaction, user):
if user is None or user.id != self.author.id:
return False
if reaction.message.id != self.message.id:
return False
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def paginate(self):
"""Actually paginate the entries and run the interactive loop if necessary."""
first_page = self.show_page(1, first=True)
if not self.paginating:
await first_page
else:
# allow us to react to reactions right away if we're paginating
self.bot.loop.create_task(first_page)
while self.paginating:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0)
except asyncio.TimeoutError:
self.paginating = False
try:
await self.message.clear_reactions()
except:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except:
pass # can't remove it so don't bother doing so
await self.match()
class FieldPages(Pages):
"""Similar to Pages except entries should be a list of
tuples having (key, value) to show as embed fields instead.
"""
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = discord.Embed.Empty
for key, value in entries:
self.embed.add_field(name=key, value=value, inline=False)
if self.maximum_pages > 1:
if self.show_entry_count:
text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)'
else:
text = f'Page {page}/{self.maximum_pages}'
self.embed.set_footer(text=text)
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
import itertools
import inspect
import re
# ?help
# ?help Cog
# ?help command
# -> could be a subcommand
_mention = re.compile(r'<@\!?([0-9]{1,19})>')
def cleanup_prefix(bot, prefix):
m = _mention.match(prefix)
if m:
user = bot.get_user(int(m.group(1)))
if user:
return f'@{user.name} '
return prefix
async def _can_run(cmd, ctx):
try:
return await cmd.can_run(ctx)
except:
return False
def _command_signature(cmd):
# this is modified from discord.py source
# which I wrote myself lmao
result = [cmd.qualified_name]
if cmd.usage:
result.append(cmd.usage)
return ' '.join(result)
params = cmd.clean_params
if not params:
return ' '.join(result)
for name, param in params.items():
if param.default is not param.empty:
# We don't want None or '' to trigger the [name=value] case and instead it should
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append(f'[{name}={param.default!r}]')
else:
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
result.append(f'[{name}...]')
else:
result.append(f'<{name}>')
return ' '.join(result)
class HelpPaginator(Pages):
def __init__(self, ctx, entries, *, per_page=4):
super().__init__(ctx, entries=entries, per_page=per_page)
self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help))
self.total = len(entries)
@classmethod
async def from_cog(cls, ctx, cog):
cog_name = cog.__class__.__name__
# get the commands
entries = sorted(ctx.bot.get_cog(cog_name).get_commands(), key=lambda c: c.name)
# remove the ones we can't run
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
self = cls(ctx, entries)
self.title = f'{cog_name} Commands'
self.description = inspect.getdoc(cog)
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_command(cls, ctx, command):
try:
entries = sorted(command.commands, key=lambda c: c.name)
except AttributeError:
entries = []
else:
entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden]
self = cls(ctx, entries)
self.title = command.signature
if command.description:
self.description = f'{command.description}\n\n{command.help}'
else:
self.description = command.help or 'No help given.'
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
return self
@classmethod
async def from_bot(cls, ctx):
def key(c):
return c.cog_name or '\u200bMisc'
entries = sorted(ctx.bot.commands, key=key)
nested_pages = []
per_page = 9
# 0: (cog, desc, commands) (max len == 9)
# 1: (cog, desc, commands) (max len == 9)
# ...
for cog, commands in itertools.groupby(entries, key=key):
plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden]
if len(plausible) == 0:
continue
description = ctx.bot.get_cog(cog)
if description is None:
description = discord.Embed.Empty
else:
description = inspect.getdoc(description) or discord.Embed.Empty
nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page))
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
# swap the get_page implementation with one that supports our style of pagination
self.get_page = self.get_bot_page
self._is_bot = True
# replace the actual total
self.total = sum(len(o) for _, _, o in nested_pages)
return self
def get_bot_page(self, page):
cog, description, commands = self.entries[page - 1]
self.title = f'{cog} Commands'
self.description = description
return commands
async def show_page(self, page, *, first=False):
self.current_page = page
entries = self.get_page(page)
self.embed.clear_fields()
self.embed.description = self.description
self.embed.title = self.title
if hasattr(self, '_is_bot'):
value ='Check the bot source: **[GitHub Link](https://github.com/F4stZ4p/DJ5n4k3/)**'
self.embed.add_field(name='**GitHub**', value=value, inline=False)
self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.')
signature = _command_signature
for entry in entries:
self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False)
if self.maximum_pages:
self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)')
if not self.paginating:
return await self.channel.send(embed=self.embed)
if not first:
await self.message.edit(embed=self.embed)
return
self.message = await self.channel.send(embed=self.embed)
for (reaction, _) in self.reaction_emojis:
if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'):
# no |<< or >>| buttons if we only have two pages
# we can't forbid it if someone ends up using it but remove
# it from the default set
continue
await self.message.add_reaction(reaction)
async def show_help(self):
"""shows this message"""
self.embed.title = 'Paginator help'
self.embed.description = 'Hello! Welcome to the help page.'
messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis]
self.embed.clear_fields()
self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())
async def show_bot_help(self):
"""shows how to use the bot"""
self.embed.title = 'Using the bot'
self.embed.description = 'Hello! Welcome to the help page.'
self.embed.clear_fields()
entries = (
('<argument>', 'This means the argument is __**required**__.'),
('[argument]', 'This means the argument is __**optional**__.'),
('[A|B]', 'This means the it can be __**either A or B**__.'),
('[argument...]', 'This means you can have multiple arguments.\n' \
'Now that you know the basics, it should be noted that...\n' \
'__**You do not type in the brackets!**__')
)
self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.')
for name, value in entries:
self.embed.add_field(name=name, value=value, inline=False)
self.embed.set_footer(text=f'We were on page {self.current_page} before this message.')
await self.message.edit(embed=self.embed)
async def go_back_to_current_page():
await asyncio.sleep(30.0)
await self.show_current_page()
self.bot.loop.create_task(go_back_to_current_page())

View file

@ -1,113 +0,0 @@
import asyncio
import os
import re
import subprocess
import uuid
import discord
from discord.ext import commands
from gtts import gTTS
class Vocal(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.playing = False
self.author = None
self.voice = None
"""---------------------------------------------------------------------"""
@staticmethod
def get_duration(file):
popen = subprocess.Popen(("ffprobe",
"-show_entries",
"format=duration",
"-i", file),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = popen.communicate()
match = re.search(r"[-+]?\d*\.\d+|\d+", str(output))
return float(match.group())
@commands.command(name="voc", no_pm=True, pass_context=True)
async def _voc(self, ctx, *, message=""):
if message == "":
await ctx.send("Veuillez écrire un message...")
return
if message == "stop_playing" \
and (
ctx.author.id == self.author.id
or ctx.message.channel.permissions_for(
ctx.message.author
).administrator is True
) \
and self.playing is True:
await ctx.send('stop')
await self.voice.disconnect()
self.playing = False
return
if self.playing is True:
await ctx.send("Je suis déja en train de parler,"
" merci de réenvoyer ton message"
" quand j'aurais fini.")
return
user = ctx.author
self.author = user
if user.voice:
self.playing = True
filename = f"data/tmp/voc/{uuid.uuid1()}.mp3"
lang = [x for x in message.split(" ") if x.startswith("lang=")]
loading = await ctx.send("*Chargement du message en cours...*")
if lang:
choice_lang = (lang[0])[5:]
message = f"{user.display_name} à dit: {message.strip(lang[0])}" if len(ctx.author.voice.channel.members) >= 4 else message.strip(lang[0])
try:
tts = gTTS(
text=message,
lang=str(choice_lang))
except ValueError:
tts = gTTS(
text=message,
lang="fr")
await ctx.send("La langue n'est pas supportée,"
" le francais a donc été choisi")
else:
message = f"{user.display_name} à dit: {message}" if len(ctx.author.voice.channel.members) >= 4 else message
tts = gTTS(text=message,
lang="fr")
tts.save(filename)
self.voice = await user.voice.channel.connect()
self.voice.play(discord.FFmpegPCMAudio(filename))
counter = 0
duration = self.get_duration(filename)
while not counter >= duration:
if self.playing:
await loading.edit(
content=f"Lecture du message de {self.author.display_name} en cours : {counter}sec/{duration}sec")
await asyncio.sleep(1)
counter += 1
else:
break
await self.voice.disconnect()
await loading.edit(content="Lecture terminée")
self.voice = None
os.remove(filename)
self.playing = False
else:
await ctx.send('Veuillez aller dans un channel vocal.')
def setup(bot):
bot.add_cog(Vocal(bot))

View file

@ -1,20 +0,0 @@
token = "INSERT TOKEN HERE"
client_id = <INSERT_CLIENT_ID_HERE (in int)>
log_channel_id = <INSERT_LOG_CHANNEL_HERE (in int)>
main_server_id = <INSERT_MAIN_CHANNEL_ID_HERE (in int)>
game = "PLAYING_GAME_HERE"
prefix = ["."]
description = """
Je suis TuxBot, le bot qui vit de l'OpenSource ! ;)
"""
mysql = {
"host": "localhost",
"username": "msqlusername",
"password": "msqlpasswd",
"dbname": "mysqldb"
}
authorized_id = ['admin ids here']
unkickable_id = ['unkickable ids here']

3
dev.requirements.txt Normal file
View file

@ -0,0 +1,3 @@
youtrack
pylint==2.6.0
black==20.8b1

View file

@ -1,3 +0,0 @@
#pip install -U "https://github.com/Rapptz/discord.py/archive/rewrite.zip#egg=discord.py[voice]"
python3 -m pip install -U discord.py[voice]
python3 -m pip install -r requirements.txt

View file

@ -1,15 +0,0 @@
pymysql
gtts
beautifulsoup4
lxml==4.2.4
bs4
pytz
requests
wikipedia
pillow
ipwhois
ipinfo
pydig
networkx
graphviz
jishaku

45
setup.cfg Normal file
View file

@ -0,0 +1,45 @@
[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.7
install_requires =
appdirs>=1.4.4
asyncpg>=0.21.0
Babel>=2.8.0
discord.py>=1.5.1
discord_flags>=2.1.1
humanize>=2.6.0
jishaku>=1.19.1.200
psutil>=5.7.2
rich>=6.0.0
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.7",
)

117
test.py
View file

@ -1,117 +0,0 @@
import telnetlib
import re
from graphviz import Digraph
srv = "he"
if srv == "opentransit":
host = "route-server.opentransit.net"
user = "rviews"
password = "Rviews"
lg_asn = "5511"
cmd = "show bgp {}"
elif srv == "oregonuniv":
host = "route-views.routeviews.org"
user = "rviews"
password = "none"
lg_asn = "3582"
cmd = "show bgp {}"
elif srv == "warian":
host = "route-server.warian.net"
user = "none"
password = "rviews"
lg_asn = "56911"
cmd = "show bgp ipv4 unicast {}"
elif srv == "csaholdigs": #Blacklist des fois
host = "route-views.sg.routeviews.org"
user = "none"
password = "none"
lg_asn = "45494"
cmd = "show bgp ipv4 unicast {}"
elif srv == "he": #Blacklist des fois
host = "route-server.he.net"
user = "none"
password = "none"
lg_asn = "6939"
cmd = "show bgp ipv4 unicast {}"
elif srv == "iamageeknz": #Blacklist des fois
host = "rs.as45186.net"
user = "none"
password = "none"
lg_asn = "45186"
cmd = "show bgp ipv4 unicast {}"
elif srv == "att":
host = "route-server.ip.att.net"
user = "rviews"
password = "rviews"
lg_asn = "7018"
cmd = "show route {}"
# Récupération de l'IP dont on veux connaitre plus d'informations
ip = input("IP : ")
# Connexion à OpenTransit
tn = telnetlib.Telnet(host)
if user != "none":
if(srv == "att"):
tn.read_until("login: ".encode())
tn.write((user + "\n").encode())
print("ok")
else:
tn.read_until("Username: ".encode())
tn.write((user + "\n").encode())
if password != "none":
if(srv == "att"):
tn.read_until("Password:".encode())
tn.write((password + "\n").encode())
print("ok")
else:
tn.read_until("Password: ".encode())
tn.write((password + "\n").encode())
# Execution d'une commande d'information BGP
tn.write((cmd + "\n").format(ip).encode())
tn.write(chr(25).encode())
tn.write(chr(25).encode())
tn.write(chr(25).encode())
tn.write("q\n".encode())
tn.write("exit\n".encode())
# Decodage des données pour les adaptées à python
data = tn.read_all().decode("utf-8")
print(data)
# Récupération des données grâce à l'utilisation d'expression régulière (module re)
paths = {}
paths["as_list"] = re.findall(r" ([0-9][0-9 ]+),", data)
if(paths["as_list"] == []):
paths["as_list"] = re.findall(r" ([0-9][0-9 ]+)[^0-9.]", data)
if(srv == "att"):
paths["as_list"] = re.findall(r"(?<=AS path: 7018 )[0-9][0-9 ]+[^ I]", data)
as_list = paths['as_list']
g = Digraph('G', filename='hello', format='png', graph_attr={'rankdir':'LR', 'concentrate': 'true'})
for as_path in as_list:
as_path = as_path.split(" ")
as_path.reverse()
original_asn = as_path[0]
border_asn = as_path[-1]
precedent_asn = original_asn
for asn in as_path:
if asn != original_asn:
g.edge("AS" + asn, "AS" + precedent_asn)
precedent_asn = asn
if asn == border_asn:
g.edge("AS" + lg_asn, "AS" + asn)
g.render()

View file

@ -1,30 +0,0 @@
import networkx as nx
import matplotlib.pyplot as plt
from graphviz import Digraph
import ipinfo as ipinfoio
as_list = ['701 2914 395747', '3267 1299 395747', '3257 395747']
g = Digraph('G', filename='hello', format='png', graph_attr={'rankdir':'LR'})
lg_asn = "5511"
for as_path in as_list:
as_path = as_path.split(" ")
as_path.reverse()
original_asn = as_path[0]
border_asn = as_path[-1]
precedent_asn = original_asn
for asn in as_path:
if asn != original_asn:
g.edge("AS" + asn, "AS" + precedent_asn)
precedent_asn = asn
if asn == border_asn:
g.edge("AS" + lg_asn, "AS" + asn)
print(as_path)
print("\n")
g.render()

View file

@ -1,11 +0,0 @@
La carte d'identité est un petit système dans tuxbot permetant de vous démarquer de vos amis en ayant la possibilité d'y renseigner plusieurs informations !
**Liste des commandes : **
-> .ci : Affiche l'aide sur les cartes d'identité
-> .ci show _pseudo_ : Affiche la carte d'identité de _pseudo_
-> .ci register : Vous enregistre dans la base de donnée des cartes d'identité
-> .ci setos _nom de l'os_ : Défini votre système d'exploitation
-> .ci setconfig _votre configuration pc_ : Défini la configuration de votre ordinateur
-> .ci setcountry : Défini votre pays
-> .ci update : Met à jour votre image si vous l'avez changé
-> .ci delete : Supprime votre carte d'identité

View file

@ -1,11 +0,0 @@
_Pour utiliser les horloges utilisez la commande : **clock ** ville_
-> Montreal (Canada, QC)
-> Vancouver (Canada, BC)
-> New-York / N-Y (U.S.A.)
-> LosAngeles / L-A (U.S.A.)
-> Berlin (Allemagne)
-> Bern / Zurich (Suisse)
-> Paris (France)
-> Tokyo (Japon)
-> Moscou (Russie)

View file

@ -1,43 +0,0 @@
**Commandes utilitaires**
-> .afk : Signaler son absence
-> .back : Signaler son retour
-> .clock _ville_: Affiche l'heure et quelques infos sur la ville en question
-> .ytdiscover : Découvrir des chaînes YouTube
-> .search _site_ _contenu_ : Fait une recherche sur un site (.search pour plus d'infos)
-> .avatar _@pseudo_ : Récupère l'avatar de _@pseudo_
-> .poke _@pseudo_ : Poke _@pseudo_
-> .sondage _question_ | _reponse_ | _reponse_ | _option_ : Créer un sondage avec des réactions
-> .role _nomdurole_ : Ajoute/Retire le rôle _nomdurole_
-> .btcprice : Affiche le prix du bitcoin
[split]
**Commandes Réseau**
-> .iplocalise _IP ou NDD_ : affiche la localisation et le propriétaire de l'ip (ou de l'IP lié a un nom de domaine)
-> .iplocalise _IP ou NDD_ **ipv6**: affiche la localisation et le propriétaire de l'ip en forçant sur l'IPv6 (ou de l'IP lié a un nom de domaine)
-> .getheaders _IP ou NDD_ : affiche les en-têtes (headers) d'une IP/Nom de domaine via HTTP/HTTPS/FTP
-> .shroute _source_ _IPv4_ : envoie un graphique contenant la route pour aller vers _IPv4_ depuis _source_ (ex: _he_)
-> .peeringdb _ASN_ : affiche les informations PeeringDB de _ASN_
[split]
**Commandes Funs**
-> .joke : Affiche une blague aléatoire
-> .ethylotest : Simule un ethylotest détraqué
-> .pokemon : Lance un combat de pokémons
-> .coin : Simule un pile ou face
-> .randomcat : Affiche des image de chats miaou
[split]
**Commandes Carte d'Identité**
-> .ci : Affiche l'aide sur les cartes d'identité
-> .ci show _pseudo_ : Affiche la carte d'identité de _pseudo_
-> .ci register : Vous enregistre dans la base de donnée des cartes d'identité
-> .ci setos _nom de l'os_ : Défini le système d'exploitation
-> .ci setconfig _votre configuration pc_ : Défini la configuration de votre ordinateur
-> .ci setcountry : Défini votre pays
-> .ci update : Met à jour votre image si vous l'avez changé :wink:
-> .ci delete : Supprime votre carte d'identité **a tous jamais**
[split]
**Commandes diverses** :
-> .info : Affiche des informations sur le bot
-> .help : Affiche ce message
-> .clock : Affiche la liste des horloges des villes
-> .sondage : Affiche l'aide pour la commande sondage
-> .ping : Ping le bot
-> .git : Affiche le repos Gitea du Bot :heart:

View file

@ -1,25 +0,0 @@
:tools: **Développement** :
└> Outout : [jesuis.enpls.org](https://jesuis.enpls.org/)
└> Romain : [son github](http://git.gnous.eu/Romain)
└> Langage : [Python3](http://www.python.org/)
└> Api : [discord.py {3}](https://github.com/Rapptz/discord.py)
└> Discord Api : [{4}]({4})
└> En se basant sur : [RobotDanny](https://github.com/Rapptz/RoboDanny)
:desktop: **Hébergé sur "{2}"**:
└> OS : {0}
└> Version : {1}
:telephone: **Contact** :
└> Discord : Outout#8406
└> Twitter : [@outoutxyz](https://twitter.com/outouxyz)
└> Courriel : [mael@gnous.eu](mailto:mael@gnous.eu)
└> Discord : Romain#5117
└> Courriel : [romain@gnous.eu](mailto:romain@gnous.eu)
:link: **Serveur** :
└> Serveur GnousEU : [rejoindre](https://discord.gg/URKy7yd)

View file

@ -1,15 +0,0 @@
{
"1": {"content": "Les hyperboles sa sert à manger des hyper-soupes :3 (Lawl!)", "author": "Crumble14 (bukkit.fr)"},
"2": {"content": "Le comble de Windows, cest que pour larrêter, il faut cliquer sur démarrer.", "author": "Keke142 (bukkit.fr)"},
"3": {"content": "Chrome: On est le 8 avril 2016 13h02 \n Safari: On est le 8 avril 2016 13h02 \n Internet Explorer: On est le... **[Internet Explorer a cessé de fonctionner, veuillez redémarrer votre machine]**", "author": "NyoSan"},
"4": {"content": "Il y a 10 types de personnes dans le monde, ceux qui comprennent le binaire et les autres.", "author": "Dartasen (bukkit.fr)"},
"5": {"content": "C'est une requête SQL qui rentre dans un bar et qui s'adresse à deux tables \"Puis-je vous joindre ?\".\"", "author": "Dartasen (bukkit.fr)"},
"6": {"content": "Combien de développeurs faut-il pour remplacer une ampoule grillée ? Aucun, c'est un problème Hardware.", "author": "Dartasen (bukkit.fr)"},
"7": {"content": "Tu sais que tu as affaire à un développeur quand ça ne le gêne pas d'avoir un String dans l'Array.", "author": "Dartasen (bukkit.fr)"},
"8": {"content": "Pourquoi y'a pas d'adresse windows ou linux ? Si y'a l'addresse mac !", "author": "Antho"},
"9": {"content": "Les appareils apple ont ils une adresse personnalisée ?", "author": "Outout"},
"10": {"content": "Le 1er janvier 1970 c'est le jour où il y a eu le plus de plantages. (cf : http://bit.ly/2rArLVe)", "author": "NyoSan"},
"11": {"content": "Pourquoi est-ce que les girafes aiment magasiner à bas prix? Tout est une question de cou.", "author": "Maxx_Qc (bukkit.fr)"},
"12": {"content": "``Même éteint le hackeur peut pirater l'ordi`` \"Le SuperGeek tournant sous Ubuntu (ou Windows)\"", "author": "Outout"},
"13": {"content": "Trois ingénieurs (1 chimiste, 1 électronicien, 1 Microsoft) dans un bus roulant dans un désert. \n\n Le bus « tombe en panne » sans raison apparente, et voila les 3 gars à discuter. \n Lélectronicien : je pourrais regarder les circuits et voir si quelque chose cloche. \n Le chimiste : on devrait vérifier l'essence avant. \n Lingé Microsoft : non, on remonte dans le bus, on ferme toutes les fenêtres, et logiquement ça devrait redémarrer.", "author": "Internet"}
}

File diff suppressed because one or more lines are too long

View file

@ -1,13 +0,0 @@
Pour améliorer l'expérience utilisateur de tout le monde, vous pouvez spécifier la ou les distributions que vous utilisez via les rôles Discord.
**Liste des rôles**
└> Arch : pour les utilisateurs de Arch, Manjaro et TuxNVape
└> Debian : pour Debian et ses dérivés (Ubuntu, Kali, etc.)
└> Rhel : pour Red Hat Entreprise Linux et ses dérivés (Fedora, CentOS, etc.)
└> Android : pour Android
└> BSD : pour les systèmes basés sur BSD
**Commandes**
└> Pour ajouter un rôle : ``.role Nomdurole``
└> Pour retirer un rôle : ``.role Nomdurole``

View file

@ -1,14 +0,0 @@
**Créez un sondage avec les réactions !**
**Usage** :
``.sondage <question> | <reponse> | <reponse> | <reponse>`` Vous pouvez utiliser autant de réponses que vous le souhaitez, en plaçant un symbole | entre chaque choix.
**Exemple**:
``.sondage Quelle est votre couleur préférée ? | Rouge | Vert | Bleu | Autre``
**Definir un temps limite** :
Vous pouvez également utiliser l'option "time" pour définir le temps en secondes pendant lequel le sondage durera.
**Exemple**:
``.sondage Utilisez vous twitteur ? | Oui | Non | Pas souvent | time=10``.

View file

@ -1,6 +0,0 @@
_Attention ! entrez vos termes de recherche sans espaces !_
Pour effectuer une recherche utilisez la commande ``.search {site_de_recherche} {termes_recherche}``
-> [**docubuntu**](https://doc.ubuntu-fr.org) : Effectuer une recherche sur un paquet dans la Documentation du site ubuntu-fr.org.
-> [**wikipedia**](https://fr.wikipedia.org) : Effectuer une recherche sur l'encyclopédie libre Wikipedia en Français !
-> [**docaur**](https://doc.archlinux.org) : Effectuer une recherche sur la doc ArchLinux !

View file

@ -1,15 +0,0 @@
{
"1": {"name": "KickSama", "desc": "Des dessins annimés sympatiques par un jeune !", "url": "https://www.youtube.com/user/TheKickGuy"},
"2": {"name": "U=RI", "desc": "Des vidéos interessantes sur l'électricité dont des tutoriels !", "url": "https://www.youtube.com/channel/UCVqx3vXNghSqUcVg2nmegYA"},
"3": {"name": "Outout", "desc": "Outout, chaine vraiment nul et peu alimenté par mon créateur...", "url": "https://www.youtube.com/channel/UC2XpYyT5X5tq9UQpXdc1JaQ"},
"4": {"name": "SuperJDay64", "desc": "Des LetsPlay sur Nintendo64 avec beaucoup de plombiers moustachus !", "url": "https://www.youtube.com/channel/UCjkQgODdmhR9I2TatJZtGSQ/"},
"5": {"name": "Monsieur Plouf", "desc": "Vidéos comiques de critiques de jeux AAA avec un décors assez spécial !", "url": "https://www.youtube.com/channel/UCrt_PUTF9LdJyuDfXweHwuQ"},
"6": {"name": "MaxEstLa", "desc": "Petite chaîne bien _sympatique_ sur la réaction de vidéos malsaine ! Très éducative x)", "url": "https://www.youtube.com/channel/UCsk9XguwTfgbenCZ4AlIcYQ"},
"7": {"name": "Met-Hardware", "desc": "Chaine youtube sur l'hardware et des let's play bien sypatique !", "url": "https://www.youtube.com/channel/UC7rse81OttysA1m1yn_f-OA"},
"8": {"name": "ElectronikHeart", "desc": "~~Test de produits de merde ~~ L'informatique sous un angle différent et agréable", "url": "https://www.youtube.com/user/ElectronikHeart"},
"9": {"name": "Caljbeut", "desc": "Cartoon Trash ! Dessins annimés par un ancien de l'armée sur la politique et d'autre sujets ! **On est pas la pour rigoler**", "url": "https://www.youtube.com/channel/UCNM-UkIP1BL5jv9ZrN5JMCA"},
"10": {"name": "Autodisciple", "desc": "Defis, Bitcoins, Geek, la vie quoi ! Sans oublier des défis de 30 Jours !", "url": "https://www.youtube.com/channel/UCDMxcev7u9Nf7KMJuyIm-BA"},
"11": {"name": "CineAstuces", "desc": "Techniques, metiers du cinema, reportages et autres en rapport avec la cinématographie !", "url": "https://www.youtube.com/channel/UC--84qgkrqqqYivuuXuQIQg"},
"12": {"name": "Epic Teaching of the History", "desc": "L'Histoire c'est hyper méga giga ultra _(j'ai pas été payé)_ drôle avec RaAak le renard ! ", "url": "https://www.youtube.com/channel/UCHwd4qMCzN4A2r6piZxTl4A"}
}

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

30
tuxbot/__main__.py Normal file
View file

@ -0,0 +1,30 @@
from rich.console import Console
from rich.traceback import install
from tuxbot import ExitCodes
console = Console()
install(console=console, show_locals=True)
def main() -> None:
try:
from .__run__ import run # pylint: disable=import-outside-toplevel
run()
except SystemExit as exc:
if exc.code == ExitCodes.RESTART:
# reimport to load changes
from .__run__ import run # pylint: disable=import-outside-toplevel
run()
else:
raise exc
except Exception:
console.print_exception(show_locals=True)
if __name__ == "__main__":
try:
main()
except Exception:
console.print_exception(show_locals=True)

308
tuxbot/__run__.py Normal file
View file

@ -0,0 +1,308 @@
import argparse
import asyncio
import logging
import signal
import sys
import os
from argparse import Namespace
from datetime import datetime
import discord
import humanize
import pip
from rich.columns import Columns
from rich.console import Console
from rich.panel import Panel
from rich.traceback import install
from rich.table import Table, box
from rich.text import Text
from rich import print as rprint
import tuxbot.logging
from tuxbot.core.bot import Tux
from tuxbot.core import config
from .core.utils import data_manager
from . import __version__, version_info, ExitCodes
log = logging.getLogger("tuxbot.main")
console = Console()
install(console=console, show_locals=True)
BORDER_STYLE = "not dim"
def list_instances() -> None:
"""List all available instances"""
app_config = config.ConfigFile(
data_manager.config_dir / "config.yaml", config.AppConfig
).config
console.print(
Panel("[bold green]Instances", style="green"), justify="center"
)
console.print()
columns = Columns(expand=True, padding=2, align="center")
for instance, details in app_config.Instances.items():
active = details["active"]
last_run = (
humanize.naturaltime(
datetime.now() - datetime.fromtimestamp(details["last_run"])
)
or "[i]unknown"
)
table = Table(
style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD
)
table.add_column("Name")
table.add_column(("Running since" if active else "Last run"))
table.add_row(instance, last_run)
table.title = Text(instance, style="green" if active else "red")
columns.add_renderable(table)
console.print(columns)
console.print()
sys.exit(os.EX_OK)
def debug_info() -> 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 <instance_name> [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(
"--list-instances",
"-L",
action="store_true",
help="List all instance names",
)
parser.add_argument(
"--token", "-T", type=str, help="Run Tuxbot with passed token"
)
parser.add_argument(
"instance_name",
nargs="?",
help="Name of the bot instance created during `tuxbot-setup`.",
)
args = parser.parse_args(args)
return args
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(tux.instance_name)
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, bot=True)
except discord.LoginFailure:
log.critical("This token appears to be valid.")
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.list_instances:
list_instances()
elif 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:
if not cli_flags.instance_name:
console.print(
"[red]No instance provided ! "
"You can use 'tuxbot -L' to list all available instances"
)
sys.exit(ExitCodes.CRITICAL)
tux = Tux(
cli_flags=cli_flags,
description="Tuxbot, made from and for OpenSource",
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 .admin import Admin
from .config import AdminConfig, HAS_MODELS
from ...core.bot import Tux
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,86 @@
import logging
import discord
from discord.ext import commands
from tuxbot.core.utils import checks
from tuxbot.core.bot import Tux
from tuxbot.core.config import set_for_key
from tuxbot.core.config import Config
from tuxbot.core.i18n import (
Translator,
find_locale,
get_locale_name,
available_locales,
)
from tuxbot.core.utils.functions.extra import (
group_extra,
command_extra,
ContextPlus,
)
log = logging.getLogger("tuxbot.cogs.Admin")
_ = Translator("Admin", __file__)
class Admin(commands.Cog, name="Admin"):
def __init__(self, bot: Tux):
self.bot = bot
async def _save_lang(self, ctx: ContextPlus, lang: str):
set_for_key(
self.bot.config.Servers, ctx.guild.id, Config.Server, locale=lang
)
# =========================================================================
# =========================================================================
@group_extra(name="lang", aliases=["locale", "langue"], deletable=True)
@commands.guild_only()
@checks.is_admin()
async def _lang(self, ctx: ContextPlus):
"""Manage lang settings."""
@_lang.command(name="set", aliases=["define", "choice"])
async def _lang_set(self, ctx: ContextPlus, lang: str):
try:
await self._save_lang(ctx, find_locale(lang.lower()))
await ctx.send(
_(
"Locale changed to {lang} successfully",
ctx,
self.bot.config,
).format(lang=f"`{get_locale_name(lang).lower()}`")
)
except NotImplementedError:
await self._lang_list(ctx)
@_lang.command(name="list", aliases=["liste", "all", "view"])
async def _lang_list(self, ctx: ContextPlus):
description = ""
for key, value in available_locales.items():
description += f":flag_{key[-2:].lower()}: {value[0]}\n"
e = discord.Embed(
title=_("List of available locales: ", ctx, self.bot.config),
description=description,
color=0x36393E,
)
await ctx.send(embed=e)
# =========================================================================
@command_extra(name="quit", aliases=["shutdown"], deletable=False)
@commands.guild_only()
@checks.is_owner()
async def _quit(self, ctx: ContextPlus):
await ctx.send("*quit...*")
await self.bot.shutdown()
@command_extra(name="restart", deletable=False)
@commands.guild_only()
@checks.is_owner()
async def _restart(self, ctx: ContextPlus):
await ctx.send("*restart...*")
await self.bot.shutdown(restart=True)

View file

@ -0,0 +1,10 @@
from structured_config import Structure
HAS_MODELS = True
class AdminConfig(Structure):
pass
extra = {}

View file

@ -0,0 +1,27 @@
# 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-11-11 02:40+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tuxbot/cogs/Admin/admin.py:50
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Admin/admin.py:65
msgid "List of available locales: "
msgstr ""

View file

@ -0,0 +1,27 @@
# 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:13+0200\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tuxbot/cogs/admin/admin.py:47
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/admin/admin.py:62
msgid "List of available locales: "
msgstr ""

View file

@ -0,0 +1,28 @@
# 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-11-11 02:40+0100\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/Admin/admin.py:50
#, fuzzy, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr "Langue changée pour {lang} avec succès"
#: tuxbot/cogs/Admin/admin.py:65
msgid "List of available locales: "
msgstr "Liste des langues disponibles : "

View file

@ -0,0 +1,28 @@
# 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:13+0200\n"
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: tuxbot/cogs/admin/admin.py:47
#, fuzzy, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr "Langue changée pour {lang} avec succès"
#: tuxbot/cogs/admin/admin.py:62
msgid "List of available locales: "
msgstr "Liste des langues disponibles : "

View file

@ -0,0 +1,27 @@
# 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-11-11 16:42+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"
#: tuxbot/cogs/Admin/admin.py:50
#, python-brace-format
msgid "Locale changed to {lang} successfully"
msgstr ""
#: tuxbot/cogs/Admin/admin.py:65
msgid "List of available locales: "
msgstr ""

View file

@ -0,0 +1,2 @@
from .alias import *
from .warns import *

View file

@ -0,0 +1,24 @@
import tortoise
from tortoise import fields
class Alias(tortoise.Model):
id = fields.BigIntField(pk=True)
user_id = fields.BigIntField()
alias = fields.TextField(max_length=255)
command = fields.TextField(max_length=255)
guild = fields.BigIntField()
class Meta:
table = "aliases"
def __str__(self):
return (
f"<Alias id={self.id} "
f"user_id={self.user_id} "
f"alias='{self.alias}' "
f"command='{self.command}' "
f"guild={self.guild}>"
)
__repr__ = __str__

View file

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

View file

@ -0,0 +1,20 @@
from collections import namedtuple
from .dev import Dev
from .config import DevConfig, HAS_MODELS
from ...core.bot import Tux
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)

22
tuxbot/cogs/Dev/config.py Normal file
View file

@ -0,0 +1,22 @@
from structured_config import Structure, StrField
HAS_MODELS = False
class DevConfig(Structure):
url: str = StrField("")
login: str = StrField("")
password: str = StrField("")
extra = {
"url": {
"type": str,
"description": "URL of the YouTrack instance (without /youtrack/)",
},
"login": {"type": str, "description": "Login for YouTrack instance"},
"password": {
"type": str,
"description": "Password for YouTrack instance",
},
}

46
tuxbot/cogs/Dev/dev.py Normal file
View file

@ -0,0 +1,46 @@
import logging
from discord.ext import commands
from youtrack.connection import Connection as YouTrack
from structured_config import ConfigFile
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
from tuxbot.core.utils.data_manager import cogs_data_path
from .config import DevConfig
from ...core.utils import checks
from ...core.utils.functions.extra import group_extra, ContextPlus
log = logging.getLogger("tuxbot.cogs.Dev")
_ = Translator("Dev", __file__)
class Dev(commands.Cog, name="Dev"):
yt: YouTrack # pylint: disable=invalid-name
def __init__(self, bot: Tux):
self.bot = bot
self.config: DevConfig = ConfigFile(
str(cogs_data_path(self.bot.instance_name, "Dev") / "config.yaml"),
DevConfig,
).config
# pylint: disable=invalid-name
self.yt = YouTrack(
self.config.url.rstrip("/") + "/youtrack/",
login=self.config.login,
password=self.config.password,
)
# =========================================================================
# =========================================================================
@group_extra(name="issue", aliases=["issues"], deletable=True)
@checks.is_owner()
async def _issue(self, ctx: ContextPlus):
"""Manage bot issues."""
@_issue.command(name="list", aliases=["liste", "all", "view"])
async def _lang_list(self, ctx: ContextPlus):
pass

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,27 @@
import logging
from collections import namedtuple
from discord.ext import commands
from .logs import Logs, on_error, GatewayHandler
from .config import LogsConfig, HAS_MODELS
from ...core.bot import Tux
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)
commands.AutoShardedBot.on_error = on_error

View file

@ -0,0 +1,38 @@
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("")
extra = {
"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",
},
}

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"

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

@ -0,0 +1,332 @@
import asyncio
import datetime
import json
import logging
import textwrap
import traceback
from collections import defaultdict
from logging import LogRecord
import discord
import humanize
import psutil
from discord.ext import commands, tasks
from structured_config import ConfigFile
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
from tuxbot.core.utils.functions.extra import (
command_extra,
ContextPlus,
)
from tuxbot.core.utils.data_manager import cogs_data_path
from .config import LogsConfig
log = logging.getLogger("tuxbot.cogs.Logs")
_ = Translator("Logs", __file__)
class GatewayHandler(logging.Handler):
def __init__(self, cog):
self.cog = cog
super().__init__(logging.INFO)
def filter(self, record: LogRecord):
return (
record.name == "discord.gateway"
or "Shard ID" in record.msg
or "Websocket closed " in record.msg
)
def emit(self, record: LogRecord):
self.cog.add_record(record)
class Logs(commands.Cog, name="Logs"):
def __init__(self, bot: Tux):
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() # pylint: disable=no-member
self.config: LogsConfig = ConfigFile(
str(
cogs_data_path(self.bot.instance_name, "Logs") / "config.yaml"
),
LogsConfig,
).config
self._resumes = []
self._identifies = defaultdict(list)
def _clear_gateway_data(self):
one_week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7)
to_remove = [
index
for index, dt in enumerate(self._resumes)
if dt < one_week_ago
]
for index in reversed(to_remove):
del self._resumes[index]
for _, dates in self._identifies.items():
to_remove = [
index for index, dt in enumerate(dates) if dt < one_week_ago
]
for index in reversed(to_remove):
del dates[index]
@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: ContextPlus):
if ctx.command is None:
return
command = ctx.command.qualified_name
self.bot.stats["commands"][command] += 1
message = ctx.message
if ctx.guild is None:
destination = "Private Message"
guild_id = None
else:
destination = f"#{message.channel} ({message.guild})"
guild_id = ctx.guild.id
log.info(
"%s: %s in %s > %s",
message.created_at,
message.author,
destination,
message.content,
)
async with self._batch_lock:
self._data_batch.append(
{
"guild": guild_id,
"channel": ctx.channel.id,
"author": ctx.author.id,
"used": message.created_at.isoformat(),
"prefix": ctx.prefix,
"command": command,
"failed": ctx.command_failed,
}
)
@commands.Cog.listener()
async def on_command_completion(self, ctx: ContextPlus):
await self.register_command(ctx)
@commands.Cog.listener()
async def on_socket_response(self, msg):
self.bot.stats["socket"][msg.get("t")] += 1
def webhook(self, log_type):
webhook = discord.Webhook.from_url(
getattr(self.config, log_type),
adapter=discord.AsyncWebhookAdapter(self.bot.session),
)
return webhook
async def log_error(self, *, ctx: ContextPlus = 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("errors").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("guilds").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("dm").send(embed=e)
@commands.Cog.listener()
async def on_command_error(self, ctx: ContextPlus, 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("errors").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: LogRecord):
self._gateway_queue.put_nowait(record)
async def notify_gateway_status(self, record: LogRecord):
types = {"INFO": ":information_source:", "WARNING": ":warning:"}
emoji = types.get(record.levelname, ":heavy_multiplication_x:")
dt = datetime.datetime.utcfromtimestamp(record.created)
msg = f"{emoji} `[{dt:%Y-%m-%d %H:%M:%S}] {record.message}`"
await self.webhook("gateway").send(msg)
@command_extra(name="commandstats", hidden=True, deletable=True)
@commands.is_owner()
async def _commandstats(self, ctx: ContextPlus, limit=20):
counter = self.bot.stats["commands"]
width = len(max(counter, key=len)) + 1
if limit > 0:
common = counter.most_common(limit)
else:
common = counter.most_common()[limit:]
output = "\n".join(f"{k:<{width}}: {c}" for k, c in common)
await ctx.send(f"```\n{output}\n```")
@command_extra(name="socketstats", hidden=True, deletable=True)
@commands.is_owner()
async def _socketstats(self, ctx: ContextPlus):
delta = datetime.datetime.now() - self.bot.uptime
minutes = delta.total_seconds() / 60
counter = self.bot.stats["socket"]
if None in counter:
counter.pop(None)
width = len(max(counter, key=len)) + 1
common = counter.most_common()
total = sum(self.bot.stats["socket"].values())
cpm = total / minutes
output = "\n".join(f"{k:<{width}}: {c}" for k, c in common)
await ctx.send(
f"{total} socket events observed ({cpm:.2f}/minute):"
f"```\n{output}\n```"
)
@command_extra(name="uptime")
async def _uptime(self, ctx: ContextPlus):
uptime = humanize.naturaltime(
datetime.datetime.now() - self.bot.uptime
)
await ctx.send(f"Uptime: **{uptime}**")
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("errors")
try:
await hook.send(embed=e)
except (
discord.HTTPException,
discord.NotFound,
discord.Forbidden,
discord.InvalidArgument,
):
pass

View file

View file

@ -0,0 +1,19 @@
from collections import namedtuple
from .polls import Polls
from .config import PollsConfig, HAS_MODELS
from ...core.bot import Tux
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(Polls(bot))

View file

@ -0,0 +1,10 @@
from structured_config import Structure
HAS_MODELS = True
class PollsConfig(Structure):
pass
extra = {}

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

@ -0,0 +1,2 @@
from .polls import *
from .responses import *

View file

@ -0,0 +1,35 @@
import tortoise
from tortoise import fields
class Poll(tortoise.Model):
id = fields.BigIntField(pk=True)
channel_id = fields.BigIntField()
message_id = fields.BigIntField()
author_id = fields.BigIntField()
content = fields.JSONField()
is_anonymous = fields.BooleanField()
available_choices = fields.IntField()
choices: fields.ManyToManyRelation["Response"] = fields.ManyToManyField(
"models.Response", related_name="choices"
)
class Meta:
table = "polls"
def __str__(self):
return (
f"<Poll id={self.id} "
f"channel_id={self.channel_id} "
f"message_id={self.message_id} "
f"author_id={self.author_id} "
f"content={self.content} "
f"is_anonymous={self.is_anonymous} "
f"available_choices={self.available_choices} "
f"choices={self.choices}>"
)
__repr__ = __str__

View file

@ -0,0 +1,22 @@
import tortoise
from tortoise import fields
class Response(tortoise.Model):
response_id = fields.BigIntField(pk=True)
poll = fields.ForeignKeyField("models.Poll")
user_id = fields.BigIntField()
choice = fields.IntField()
class Meta:
table = "responses"
def __str__(self):
return (
f"<Response poll={self.poll} "
f"user_id={self.user_id} "
f"choice={self.choice}>"
)
__repr__ = __str__

View file

@ -0,0 +1,51 @@
import logging
from discord.ext import commands
from tuxbot.core.utils.functions.extra import ContextPlus, group_extra
from tuxbot.core.bot import Tux
from tuxbot.core.i18n import (
Translator,
)
log = logging.getLogger("tuxbot.cogs.Polls")
_ = Translator("Polls", __file__)
class Polls(commands.Cog, name="Polls"):
def __init__(self, bot: Tux):
self.bot = bot
# =========================================================================
# =========================================================================
@group_extra(name="polls", aliases=["poll", "sondages", "sondage"])
async def _polls(self, ctx: ContextPlus, *, message):
if ctx.invoked_subcommand is None:
args: list = message.lower().split()
is_anonymous = False
if "--anonymous" in args:
is_anonymous = True
args.remove("--anonymous")
elif "--anonyme" in args:
is_anonymous = True
args.remove("--anonyme")
if args[-1] != "|":
args.append("|")
delimiters = [i for i, val in enumerate(args) if val == "|"]
question = " ".join(args[0 : delimiters[0]]).capitalize()
answers = []
for i in range(len(delimiters) - 1):
start = delimiters[i] + 1
end = delimiters[i + 1]
answers.append(" ".join(args[start:end]).capitalize())
await ctx.send(
f"{message=}\n{question=}\n{answers=}\n{is_anonymous=}"
)

View file

@ -0,0 +1,19 @@
from collections import namedtuple
from .utils import Utils
from .config import UtilsConfig, HAS_MODELS
from ...core.bot import Tux
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(Utils(bot))

View file

@ -0,0 +1,10 @@
from structured_config import Structure
HAS_MODELS = False
class UtilsConfig(Structure):
pass
extra = {}

View file

@ -0,0 +1,50 @@
import os
import pathlib
def fetch_info():
total_lines = 0
total_python_class = 0
total_python_functions = 0
total_python_coroutines = 0
total_python_comments = 0
file_amount = 0
python_file_amount = 0
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.readlines():
line = line.strip()
if line.startswith("class"):
total_python_class += 1
if line.startswith("def"):
total_python_functions += 1
if line.startswith("async def"):
total_python_coroutines += 1
if "#" in line:
total_python_comments += 1
total_lines += 1
return {
"total_lines": total_lines,
"total_python_class": total_python_class,
"total_python_functions": total_python_functions,
"total_python_coroutines": total_python_coroutines,
"total_python_comments": total_python_comments,
"file_amount": file_amount,
"python_file_amount": python_file_amount,
}

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