├── cogs ├── utils │ ├── __init__.py │ ├── db.py │ ├── cli_colors.py │ ├── formats.py │ ├── checks.py │ ├── maps.py │ ├── menu.py │ └── paginator.py ├── .DS_Store ├── afk.py ├── filter_messages.py ├── basics.py ├── sondage.py ├── vocal.py ├── send_logs.py ├── atc.py ├── role.py ├── search.py ├── funs.py ├── ci.py ├── admin.py └── utility.py ├── requirements.txt ├── .gitignore ├── init.sh ├── README.md ├── texts ├── clocks.md ├── search.md ├── rpoll.md ├── roles.md ├── ci-info.md ├── passport-info.md ├── info.md ├── ytb.json ├── jokes.json └── help.md ├── config.py.example ├── bot.py └── LICENSE /cogs/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cogs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outout14/tuxbot-bot/HEAD/cogs/.DS_Store -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymysql 2 | gtts 3 | beautifulsoup4 4 | lxml==4.2.4 5 | bs4 6 | pytz 7 | requests 8 | wikipedia 9 | pillow -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Python 2 | __pycache__/ 3 | *.pyc 4 | .env 5 | config.py 6 | .DS_Store 7 | private.py 8 | 9 | #jetbrains 10 | .idea/ -------------------------------------------------------------------------------- /init.sh: -------------------------------------------------------------------------------- 1 | #pip install -U "https://github.com/Rapptz/discord.py/archive/rewrite.zip#egg=discord.py[voice]" 2 | python3 -m pip install -U discord.py[voice] 3 | pip install -r requirements.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # /!\ Ancien repos /!\ 2 | 3 | **Le repos de tuxbot est désormais sur [https://git.gnous.eu/gnouseu/tuxbot-bot](https://git.gnous.eu/gnouseu/tuxbot-bot), une infrastructure indépendante appartenant à son projet mère, Gnous.eu** 4 | -------------------------------------------------------------------------------- /texts/clocks.md: -------------------------------------------------------------------------------- 1 | 2 | _Pour utiliser les horloges utilisez la commande : **clock ** ville_ 3 | -> Montreal (Canada, QC) 4 | -> Vancouver (Canada, BC) 5 | -> New-York / N-Y (U.S.A.) 6 | -> LosAngeles / L-A (U.S.A.) 7 | -> Berlin (Allemagne) 8 | -> Bern / Zurich (Suisse) 9 | -> Paris (France) 10 | -> Tokyo (Japon) 11 | -> Moscou (Russie) 12 | -------------------------------------------------------------------------------- /texts/search.md: -------------------------------------------------------------------------------- 1 | 2 | _Attention ! entrez vos termes de recherche sans espaces !_ 3 | Pour effectuer une recherche utilisez la commande ``.search {site_de_recherche} {termes_recherche}`` 4 | -> [**docubuntu**](https://doc.ubuntu-fr.org) : Effectuer une recherche sur un paquet dans la Documentation du site ubuntu-fr.org. 5 | -> [**wikipedia**](https://fr.wikipedia.org) : Effectuer une recherche sur l'encyclopédie libre Wikipedia en Français ! 6 | -> [**docaur**](https://doc.archlinux.org) : Effectuer une recherche sur la doc ArchLinux ! 7 | -------------------------------------------------------------------------------- /config.py.example: -------------------------------------------------------------------------------- 1 | token = "INSERT TOKEN HERE" 2 | client_id = 3 | log_channel_id = 4 | main_server_id = 5 | 6 | game = "PLAYING_GAME_HERE" 7 | prefix = ["."] 8 | description = """ 9 | Je suis TuxBot, le bot qui vit de l'OpenSource ! ;) 10 | """ 11 | 12 | mysql = { 13 | "host": "localhost", 14 | "username": "msqlusername", 15 | "password": "msqlpasswd", 16 | "dbname": "mysqldb" 17 | } 18 | 19 | authorized_id = ['admin ids here'] 20 | unkickable_id = ['unkickable ids here'] 21 | -------------------------------------------------------------------------------- /texts/rpoll.md: -------------------------------------------------------------------------------- 1 | **Créez un sondage avec les réactions !** 2 | 3 | **Usage** : 4 | ``.sondage | | | `` Vous pouvez utiliser autant de réponses que vous le souhaitez, en plaçant un symbole | entre chaque choix. 5 | 6 | **Exemple**: 7 | ``.sondage Quelle est votre couleur préférée ? | Rouge | Vert | Bleu | Autre`` 8 | 9 | 10 | **Definir un temps limite** : 11 | Vous pouvez également utiliser l'option "time" pour définir le temps en secondes pendant lequel le sondage durera. 12 | 13 | **Exemple**: 14 | ``.sondage Utilisez vous twitteur ? | Oui | Non | Pas souvent | time=10``. 15 | -------------------------------------------------------------------------------- /texts/roles.md: -------------------------------------------------------------------------------- 1 | 2 | Pour améliorer l'expérience utilisateur de tout le monde, vous pouvez spécifier la·les distribution·s que vous utilisez via les rôles Discord. 3 | 4 | **Liste des rôles** 5 | └> Arch : pour <:archlinux:282214818486812673> 6 | └> Debian : pour Debian <:debian:375326392482791424> et ses dérivés (Ubuntu, Kali, etc.) 7 | └> Rhel : pour Red Hat Entreprise Linux et ses dérivés (Fedora, CentOS, etc.) 8 | └> Android : pour Android <:android:282214818486812673> 9 | └> BSD : pour BSD <:bsd:401797313657569280> 10 | 11 | **Commandes** 12 | └> Pour ajouter un rôle : ``.role Nomdurole`` 13 | └> Pour retirer un rôle : ``.role Nomdurole`` -------------------------------------------------------------------------------- /texts/ci-info.md: -------------------------------------------------------------------------------- 1 | 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 ! 2 | 3 | **Liste des commandes : ** 4 | -> .ci : Affiche l'aide sur les cartes d'identité 5 | -> .ci show _pseudo_ : Affiche la carte d'identité de _pseudo_ 6 | -> .ci register : Vous enregistre dans la base de donnée des cartes d'identité 7 | -> .ci setos _nom de l'os_ : Défini votre système d'exploitation 8 | -> .ci setconfig _votre configuration pc_ : Défini la configuration de votre ordinateur 9 | -> .ci setcountry : Défini votre pays 10 | -> .ci update : Met à jour votre image si vous l'avez changé 11 | -> .ci delete : Supprime votre carte d'identité 12 | -------------------------------------------------------------------------------- /texts/passport-info.md: -------------------------------------------------------------------------------- 1 | Le passeport est un petit système dans tuxbot permetant de vous démarquer de vos amis en ayant la possibilité d'y renseigner plusieurs informations ! 2 | 3 | **Liste des commandes : ** 4 | -> .passeport : Affiche l'aide sur les cartes d'identité 5 | -> .passeport show _pseudo_ : Affiche le passeport de _pseudo_ 6 | -> .passeport config : Vous envois un message privé afin de configurer votre passeport 7 | -> .passeport background _url_ : Défini _url_ comme étant le fond d'écran de votre passeport 8 | -> .passeport thème : Definie le theme de votre passeport (`.passeport preview` envoi une image avec les 2 thèmes pour comparer) 9 | -> .passeport delete : Supprime les informations de votre carte passeport 10 | -------------------------------------------------------------------------------- /texts/info.md: -------------------------------------------------------------------------------- 1 | 2 | :tools: **Développement** : 3 | └> Outout : [outout.xyz](https://outout.xyz/) 4 | └> Romain : [son github](http://git.gnous.eu/Romain) 5 | └> Langage : [Python3](http://www.python.org/) 6 | └> Api : [discord.py {3}](https://github.com/Rapptz/discord.py) 7 | └> Discord Api : [{4}]({4}) 8 | └> En se basant sur : [RobotDanny](https://github.com/Rapptz/RoboDanny) 9 | 10 | <:server:577916597239283742> **Hébergé sur "{2}"**: 11 | └> <:tux:282212977627758593> OS : {0} 12 | └> Version : {1} 13 | 14 | :telephone: **Contact** : 15 | └> <:DiscordLogoColor:577915413573402624> : Outout#8406 16 | └> <:twitter:577915119032336387> : [@outoutxyz](https://twitter.com/outouxyz) 17 | └> :mailbox: : [mael@gnous.eu](mailto:mael@gnous.eu) 18 | └> <:DiscordLogoColor:577915413573402624> : Romain#5117 19 | 20 | 21 | <:DiscordLogoColor:577915413573402624> **Serveur** : 22 | └> Serveur GnousEU : [rejoindre](https://discord.gg/NFW3EeS) 23 | 24 | 25 | -------------------------------------------------------------------------------- /cogs/utils/db.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | 4 | def connect_to_db(self): 5 | mysqlHost = self.bot.config.mysql["host"] 6 | mysqlUser = self.bot.config.mysql["username"] 7 | mysqlPass = self.bot.config.mysql["password"] 8 | mysqlDB = self.bot.config.mysql["dbname"] 9 | 10 | try: 11 | return pymysql.connect(host=mysqlHost, user=mysqlUser, 12 | passwd=mysqlPass, db=mysqlDB, charset='utf8') 13 | except KeyError: 14 | print( 15 | "Rest in peperoni, Impossible de se connecter a la base de données.") 16 | print(str(KeyError)) 17 | return 18 | 19 | 20 | def reconnect_to_db(self): 21 | if not self.conn: 22 | mysqlHost = self.bot.config.mysql["host"] 23 | mysqlUser = self.bot.config.mysql["username"] 24 | mysqlPass = self.bot.config.mysql["password"] 25 | mysqlDB = self.bot.config.mysql["dbname"] 26 | 27 | return pymysql.connect(host=mysqlHost, user=mysqlUser, 28 | passwd=mysqlPass, db=mysqlDB, charset='utf8') 29 | return self.conn 30 | -------------------------------------------------------------------------------- /cogs/utils/cli_colors.py: -------------------------------------------------------------------------------- 1 | class text_colors: 2 | BLACK = '\033[30m' 3 | RED = '\033[31m' 4 | GREEN = '\033[32m' 5 | YELLOW = '\033[33m' 6 | BLUE = '\033[34m' 7 | MAGENTA = '\033[35m' 8 | CYAN = '\033[36m' 9 | LIGHT_GRAY = '\033[37m' 10 | DARK_GRAY = '\033[90m' 11 | LIGHT_RED = '\033[91m' 12 | LIGHT_GREEN = '\033[92m' 13 | LIGHT_YELLOW = '\033[93m' 14 | LIGHT_BLUE = '\033[94m' 15 | LIGHT_MAGENTA = '\033[95m' 16 | LIGHT_CYAN = '\033[96m' 17 | WHITE = '\033[97m' 18 | 19 | 20 | class bg_colors: 21 | BLACK = '\033[40m' 22 | RED = '\033[41m' 23 | GREEN = '\033[42m' 24 | YELLOW = '\033[43m' 25 | BLUE = '\033[44m' 26 | MAGENTA = '\033[45m' 27 | CYAN = '\033[46m' 28 | LIGHT_GRAY = '\033[47m' 29 | DARK_GRAY = '\033[100m' 30 | LIGHT_RED = '\033[101m' 31 | LIGHT_GREEN = '\033[102m' 32 | LIGHT_YELLOW = '\033[103m' 33 | LIGHT_BLUE = '\033[104m' 34 | LIGHT_MAGENTA = '\033[105m' 35 | LIGHT_CYAN = '\033[106m' 36 | WHITE = '\033[107m' 37 | 38 | 39 | class text_style: 40 | BOLD = '\033[1m' 41 | DIM = '\033[2m' 42 | UNDERLINE = '\033[4m' 43 | BLINK = '\033[5m' 44 | 45 | 46 | ENDC = '\033[0m' 47 | -------------------------------------------------------------------------------- /texts/ytb.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": {"name": "KickSama", "desc": "Des dessins annimés sympatiques par un jeune !", "url": "https://www.youtube.com/user/TheKickGuy"}, 3 | "2": {"name": "U=RI", "desc": "Des vidéos interessantes sur l'électricité dont des tutoriels !", "url": "https://www.youtube.com/channel/UCVqx3vXNghSqUcVg2nmegYA"}, 4 | "3": {"name": "Outout", "desc": "Outout, chaine vraiment nul et peu alimenté par mon créateur...", "url": "https://www.youtube.com/channel/UC2XpYyT5X5tq9UQpXdc1JaQ"}, 5 | "4": {"name": "SuperJDay64", "desc": "Des LetsPlay sur Nintendo64 avec beaucoup de plombiers moustachus !", "url": "https://www.youtube.com/channel/UCjkQgODdmhR9I2TatJZtGSQ/"}, 6 | "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"}, 7 | "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"}, 8 | "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"}, 9 | "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"}, 10 | "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"}, 11 | "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"}, 12 | "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"}, 13 | "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"} 14 | } 15 | 16 | -------------------------------------------------------------------------------- /texts/jokes.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": {"content": "Les hyperboles sa sert à manger des hyper-soupes :3 (Lawl!)", "author": "Crumble14 (bukkit.fr)"}, 3 | "2": {"content": "Le comble de Windows, c’est que pour l’arrêter, il faut cliquer sur démarrer.", "author": "Keke142 (bukkit.fr)"}, 4 | "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"}, 5 | "4": {"content": "Il y a 10 types de personnes dans le monde, ceux qui comprennent le binaire et les autres.", "author": "Dartasen (bukkit.fr)"}, 6 | "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)"}, 7 | "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)"}, 8 | "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)"}, 9 | "8": {"content": "Pourquoi y'a pas d'adresse windows ou linux ? Si y'a l'addresse mac !", "author": "Antho"}, 10 | "9": {"content": "Les appareils apple ont ils une adresse personnalisée ?", "author": "Outout"}, 11 | "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"}, 12 | "11": {"content": "Pourquoi est-ce que les girafes aiment magasiner à bas prix? Tout est une question de cou.", "author": "Maxx_Qc (bukkit.fr)"}, 13 | "12": {"content": "``Même éteint le hackeur peut pirater l'ordi`` \"Le SuperGeek tournant sous Ubuntu (ou Windows)\"", "author": "Outout"}, 14 | "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 L’ingé Microsoft : non, on remonte dans le bus, on ferme toutes les fenêtres, et logiquement ça devrait redémarrer.", "author": "Internet"} 15 | } -------------------------------------------------------------------------------- /texts/help.md: -------------------------------------------------------------------------------- 1 | **Commandes utilitaires** 2 | -> .afk : Signaler son absence *(commande désactivée)* 3 | -> .back : Signaler son retour *(commande désactivée)* 4 | -> .clock _ville_: Affiche l'heure et quelques infos sur la ville en question 5 | -> .ytdiscover : Découvrir des chaines youtube 6 | -> .search _site_ _contenu_ : Fait une recherche sur un site (.search pour plus d'infos) 7 | -> .avatar _@pseudo_ : Récupère l'avatar de _@pseudo_ 8 | -> .poke _@pseudo_ : Poke _@pseudo_ 9 | -> .hastebin _code_ : Poste du code sur hastebin 10 | -> .sondage _question_ | _reponse_ | _reponse_ | _option_ : Créer un sondage avec des réactions 11 | -> .sondage : Affiche l'aide pour la commande sondage 12 | -> .role _nomdurole_ : Ajoute/Retire le rôle en question 13 | -> .iplocalise _IP ou NDD_ : affiche la localisation et le propriétaire de l'ip (ou de l'IP lié a un nom de domaine) 14 | -> .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) 15 | -> .getheaders _IP ou NDD_ : affiche les en-têtes (headers) d'une IP/Nom de domaine via HTTP/HTTPS/FTP 16 | -> .btcprice : Affiche le prix du bitcoin 17 | [split] 18 | **Commandes Funs** 19 | -> .joke : Affiche une blague aléatoire 20 | -> .ethylotest : Simule un ethylotest détraqué 21 | -> .pokemon : Lance un combat de pokémons 22 | -> .coin : Simule un pile ou face 23 | -> .randomcat : Affiche des image de chats :3 24 | [split] 25 | **Commandes Carte d'Identité** 26 | -> .ci : Affiche l'aide sur les cartes d'identité 27 | -> .ci show _pseudo_ : Affiche la carte d'identité de _pseudo_ 28 | -> .ci register : Vous enregistre dans la base de donnée des cartes d'identité 29 | -> .ci setos _nom de l'os_ : Défini le système d'exploitation 30 | -> .ci setconfig _votre configuration pc_ : Défini la configuration de votre ordinateur 31 | -> .ci setcountry : Défini votre pays 32 | -> .ci update : Met à jour votre image si vous l'avez changé :wink: 33 | -> .ci delete : Supprime votre carte d'identité **a tous jamais** 34 | [split] 35 | **Commandes diverses** : 36 | -> .info : Affiche des informations sur le bot 37 | -> .help : Affiche ce message 38 | -> .clock : Affiche la liste des horloges des villes 39 | -> .ping : Ping le bot 40 | -> .git : Affiche le repos Gitea du Bot :heart: 41 | -------------------------------------------------------------------------------- /cogs/afk.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | import random 4 | 5 | 6 | class AFK(commands.Cog): 7 | """Commandes utilitaires.""" 8 | 9 | def __init__(self, bot): 10 | self.bot = bot 11 | self.afk_users = [] 12 | 13 | """---------------------------------------------------------------------""" 14 | 15 | @commands.command(pass_context=True) 16 | async def afk(self, ctx, action: str = ""): 17 | 18 | if action.lower() == "list": 19 | try: 20 | await ctx.send(*self.afk_users) 21 | except discord.HTTPException: 22 | await ctx.send("Il n'y a personne d'afk...") 23 | else: 24 | user = ctx.author 25 | self.afk_users.append(user) 26 | msgs = ["s'absente de discord quelques instants", 27 | "se casse de son pc", 28 | "va sortir son chien", 29 | "reviens bientôt", 30 | "va nourrir son cochon", 31 | "va manger des cookies", 32 | "va manger de la poutine", 33 | "va faire caca", 34 | "va faire pipi"] 35 | 36 | await ctx.send(f"**{user.mention}** {random.choice(msgs)}...") 37 | 38 | """---------------------------------------------------------------------""" 39 | 40 | @commands.Cog.listener() 41 | async def on_message(self, message): 42 | if message.author.bot \ 43 | or message.guild.id != int(self.bot.config.main_server_id): 44 | return 45 | 46 | user = message.author 47 | 48 | if user in self.afk_users \ 49 | and message.content != self.bot.config.prefix[0] + "afk": 50 | self.afk_users.remove(user) 51 | 52 | msgs = ["a réssuscité", 53 | "est de nouveau parmi nous", 54 | "a fini de faire caca", 55 | "a fini d'uriner", 56 | "n'est plus mort", 57 | "est de nouveau sur son PC", 58 | "a fini de manger sa poutine", 59 | "a fini de danser", 60 | "s'est réveillé", 61 | "est de retour dans ce monde cruel"] 62 | 63 | await message.channel.send(f"**{user.mention}**" 64 | f" {random.choice(msgs)}...") 65 | 66 | 67 | def setup(bot): 68 | bot.add_cog(AFK(bot)) 69 | -------------------------------------------------------------------------------- /cogs/utils/formats.py: -------------------------------------------------------------------------------- 1 | async def entry_to_code(bot, entries): 2 | width = max(map(lambda t: len(t[0]), entries)) 3 | output = ['```'] 4 | fmt = '{0:<{width}}: {1}' 5 | for name, entry in entries: 6 | output.append(fmt.format(name, entry, width=width)) 7 | output.append('```') 8 | await bot.say('\n'.join(output)) 9 | 10 | import datetime 11 | 12 | async def indented_entry_to_code(bot, entries): 13 | width = max(map(lambda t: len(t[0]), entries)) 14 | output = ['```'] 15 | fmt = '\u200b{0:>{width}}: {1}' 16 | for name, entry in entries: 17 | output.append(fmt.format(name, entry, width=width)) 18 | output.append('```') 19 | await bot.say('\n'.join(output)) 20 | 21 | async def too_many_matches(bot, msg, matches, entry): 22 | check = lambda m: m.content.isdigit() 23 | await bot.say('There are too many matches... Which one did you mean? **Only say the number**.') 24 | await bot.say('\n'.join(map(entry, enumerate(matches, 1)))) 25 | 26 | # only give them 3 tries. 27 | for i in range(3): 28 | message = await bot.wait_for_message(author=msg.author, channel=msg.channel, check=check) 29 | index = int(message.content) 30 | try: 31 | return matches[index - 1] 32 | except: 33 | await bot.say('Please give me a valid number. {} tries remaining...'.format(2 - i)) 34 | 35 | raise ValueError('Too many tries. Goodbye.') 36 | 37 | class Plural: 38 | def __init__(self, **attr): 39 | iterator = attr.items() 40 | self.name, self.value = next(iter(iterator)) 41 | 42 | def __str__(self): 43 | v = self.value 44 | if v > 1: 45 | return '%s %ss' % (v, self.name) 46 | return '%s %s' % (v, self.name) 47 | 48 | def human_timedelta(dt): 49 | now = datetime.datetime.utcnow() 50 | delta = now - dt 51 | hours, remainder = divmod(int(delta.total_seconds()), 3600) 52 | minutes, seconds = divmod(remainder, 60) 53 | days, hours = divmod(hours, 24) 54 | years, days = divmod(days, 365) 55 | 56 | if years: 57 | if days: 58 | return '%s and %s ago' % (Plural(year=years), Plural(day=days)) 59 | return '%s ago' % Plural(year=years) 60 | 61 | if days: 62 | if hours: 63 | return '%s and %s ago' % (Plural(day=days), Plural(hour=hours)) 64 | return '%s ago' % Plural(day=days) 65 | 66 | if hours: 67 | if minutes: 68 | return '%s and %s ago' % (Plural(hour=hours), Plural(minute=minutes)) 69 | return '%s ago' % Plural(hour=hours) 70 | 71 | if minutes: 72 | if seconds: 73 | return '%s and %s ago' % (Plural(minute=minutes), Plural(second=seconds)) 74 | return '%s ago' % Plural(minute=minutes) 75 | return '%s ago' % Plural(second=seconds) 76 | -------------------------------------------------------------------------------- /cogs/filter_messages.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import re 3 | 4 | 5 | class FilterMessages(commands.Cog): 6 | """Flitre des messages""" 7 | 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | @commands.Cog.listener() 12 | async def on_message(self, message): 13 | no_pub_guild = [280805240977227776, 303633056944881686, 14 | 274247231534792704] 15 | lien_channel = [280805783795662848, 508794201509593088, 16 | 516017286948061204] 17 | sondage_channel = [394146769107419146, 477147964393914388] 18 | 19 | if message.author.bot \ 20 | or str(message.author.id) in self.bot.config.authorized_id \ 21 | or message.channel.permissions_for(message.author).administrator is True: 22 | return 23 | 24 | discord_invite_regex = re.compile(r"(discord\.(gg|io|me|li)|discordapp\.com\/invite)\/[0-9A-Za-z]*", re.IGNORECASE) 25 | invalid_link_regex = re.compile(r"^(\[[^\]]+\]|<\:[a-z0-9]+\:[0-9]+>) .+ https?:\/\/\S*$", re.IGNORECASE) 26 | 27 | try: 28 | if message.guild.id in no_pub_guild: 29 | if isinstance(discord_invite_regex.search(message.content), re.Match): 30 | author = self.bot.get_user(message.author.id) 31 | await message.delete() 32 | await author.send("La pub pour les serveurs discord n'est pas autorisée ici") 33 | 34 | if message.channel.id in lien_channel \ 35 | and not isinstance(invalid_link_regex.search(message.content), re.Match): 36 | author = self.bot.get_user(message.author.id) 37 | await message.delete() 38 | await author.send(f"Votre message `{message.content}` a été " 39 | f"supprimé du channel `liens` ou `projets` " 40 | f"car il ne respecte pas la structure " 41 | f"définie. Pour partager un lien veuillez " 42 | f"suivre la structure suivante :" 43 | f" ` [Sujet] Descirption http(s)://....`") 44 | await author.send("Si vous voulez commenter ou discuter à " 45 | "propos d'un lien ou d'un projet, veuillez " 46 | "le faire dans le channel" 47 | " `#discussion-des-liens` ou" 48 | " `#discussion-projets`.") 49 | 50 | if message.channel.id in sondage_channel: 51 | prefix_lenght = len(await self.bot.get_prefix(message)) 52 | command = (message.content.split()[0])[prefix_lenght:] 53 | if command != "sondage": 54 | await message.delete() 55 | except AttributeError: 56 | pass 57 | 58 | 59 | def setup(bot): 60 | bot.add_cog(FilterMessages(bot)) 61 | -------------------------------------------------------------------------------- /cogs/basics.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import socket 3 | import subprocess 4 | 5 | import discord 6 | from discord.ext import commands 7 | from discord.http import Route 8 | 9 | 10 | class Basics(commands.Cog): 11 | """Commandes générales.""" 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | @commands.command() 17 | async def ping(self, ctx): 18 | ping_res = str(subprocess.Popen(["/bin/ping", "-c1", "discordapp.com"], 19 | stdout=subprocess.PIPE).stdout.read()) 20 | formated_res = [item for item in ping_res.split() if 'time=' in item] 21 | result = str(formated_res[0])[5:] 22 | 23 | if float(result) >= 200: 24 | em = discord.Embed(title="Ping : " + str(result) + "ms", 25 | description="... c'est quoi ce ping !", 26 | colour=0xFF1111) 27 | await ctx.send(embed=em) 28 | elif float(result) > 100 < 200: 29 | em = discord.Embed(title="Ping : " + str(result) + "ms", 30 | description="Ca va, ça peut aller, mais j'ai " 31 | "l'impression d'avoir 40 ans !", 32 | colour=0xFFA500) 33 | await ctx.send(embed=em) 34 | else: 35 | em = discord.Embed(title="Ping : " + str(result) + "ms", 36 | description="Wow c'te vitesse de réaction, " 37 | "je m'épate moi-même !", 38 | colour=0x11FF11) 39 | await ctx.send(embed=em) 40 | 41 | """---------------------------------------------------------------------""" 42 | 43 | @commands.command() 44 | async def info(self, ctx): 45 | """Affiches des informations sur le bot""" 46 | text = open('texts/info.md').read() 47 | os_info = str(platform.system()) + " / " + str(platform.release()) 48 | em = discord.Embed(title='Informations sur TuxBot', 49 | description=text.format(os_info, 50 | platform.python_version(), 51 | socket.gethostname(), 52 | discord.__version__, 53 | Route.BASE), 54 | colour=0x89C4F9) 55 | em.set_footer(text="/home/****/bot.py") 56 | await ctx.send(embed=em) 57 | 58 | """---------------------------------------------------------------------""" 59 | 60 | @commands.command() 61 | async def help(self, ctx): 62 | """Affiches l'aide du bot""" 63 | text = open('texts/help.md').read() 64 | em = discord.Embed(title='Commandes de TuxBot', description=text, 65 | colour=0x89C4F9) 66 | await ctx.send(embed=em) 67 | 68 | 69 | def setup(bot): 70 | bot.add_cog(Basics(bot)) 71 | -------------------------------------------------------------------------------- /cogs/utils/checks.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | 4 | def is_owner_check(message): 5 | return str(message.author.id) in ['171685542553976832', 6 | '269156684155453451'] 7 | 8 | 9 | def is_owner(warn=True): 10 | def check(ctx, log): 11 | owner = is_owner_check(ctx.message) 12 | if not owner and log: 13 | print(ctx.message.author.name + " à essayer d'executer " + ctx.message.content + " sur le serveur " + ctx.message.guild.name) 14 | return owner 15 | 16 | owner = commands.check(lambda ctx: check(ctx, warn)) 17 | return owner 18 | 19 | 20 | """-------------------------------------------------------------------------""" 21 | 22 | 23 | async def check_permissions(ctx, perms, *, check=all): 24 | is_owner = await ctx.bot.is_owner(ctx.author) 25 | if is_owner or is_owner_check(ctx.message) is True: 26 | return True 27 | 28 | resolved = ctx.channel.permissions_for(ctx.author) 29 | return check(getattr(resolved, name, None) == value for name, value in 30 | perms.items()) 31 | 32 | 33 | def has_permissions(*, check=all, **perms): 34 | async def pred(ctx): 35 | return await check_permissions(ctx, perms, check=check) 36 | 37 | return commands.check(pred) 38 | 39 | 40 | async def check_guild_permissions(ctx, perms, *, check=all): 41 | is_owner = await ctx.bot.is_owner(ctx.author) 42 | if is_owner: 43 | return True 44 | 45 | if ctx.guild is None: 46 | return False 47 | 48 | resolved = ctx.author.guild_permissions 49 | return check(getattr(resolved, name, None) == value for name, value in 50 | perms.items()) 51 | 52 | 53 | def has_guild_permissions(*, check=all, **perms): 54 | async def pred(ctx): 55 | return await check_guild_permissions(ctx, perms, check=check) 56 | 57 | return commands.check(pred) 58 | 59 | 60 | # These do not take channel overrides into account 61 | 62 | 63 | def is_mod(): 64 | async def pred(ctx): 65 | return await check_guild_permissions(ctx, {'manage_guild': True}) 66 | 67 | return commands.check(pred) 68 | 69 | 70 | def is_admin(): 71 | async def pred(ctx): 72 | return await check_guild_permissions(ctx, {'administrator': True}) 73 | 74 | return commands.check(pred) 75 | 76 | 77 | def mod_or_permissions(**perms): 78 | perms['manage_guild'] = True 79 | 80 | async def predicate(ctx): 81 | return await check_guild_permissions(ctx, perms, check=any) 82 | 83 | return commands.check(predicate) 84 | 85 | 86 | def admin_or_permissions(**perms): 87 | perms['administrator'] = True 88 | 89 | async def predicate(ctx): 90 | return await check_guild_permissions(ctx, perms, check=any) 91 | 92 | return commands.check(predicate) 93 | 94 | 95 | def is_in_guilds(*guild_ids): 96 | def predicate(ctx): 97 | guild = ctx.guild 98 | if guild is None: 99 | return False 100 | return guild.id in guild_ids 101 | 102 | return commands.check(predicate) 103 | 104 | 105 | def get_user(message, user): 106 | try: 107 | member = message.mentions[0] 108 | except: 109 | member = message.guild.get_member_named(user) 110 | if not member: 111 | try: 112 | member = message.guild.get_member(int(user)) 113 | except ValueError: 114 | pass 115 | if not member: 116 | return None 117 | return member 118 | 119 | 120 | def check_date(date: str): 121 | if len(date) == 1: 122 | return f"0{date}" 123 | else: 124 | return date 125 | -------------------------------------------------------------------------------- /cogs/sondage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import discord 4 | from discord.ext import commands 5 | 6 | 7 | class Sondage(commands.Cog): 8 | """Commandes sondage.""" 9 | 10 | def __init__(self, bot): 11 | self.bot = bot 12 | 13 | @commands.command(pass_context=True) 14 | async def sondage(self, ctx, *, msg="help"): 15 | if msg != "help": 16 | await ctx.message.delete() 17 | options = msg.split(" | ") 18 | 19 | times = [x for x in options if x.startswith("time=")] 20 | 21 | if times: 22 | time = int(times[0].strip("time=")) 23 | options.remove(times[0]) 24 | else: 25 | time = 0 26 | 27 | if len(options) <= 1: 28 | raise commands.errors.MissingRequiredArgument 29 | if len(options) >= 22: 30 | return await ctx.send(f"{ctx.message.author.mention}> " 31 | f":octagonal_sign: Vous ne pouvez pas " 32 | f"mettre plus de 20 réponses !") 33 | 34 | emoji = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟', '0⃣', 35 | '🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮'] 36 | to_react = [] 37 | confirmation_msg = f"**{options[0].rstrip('?')}?**:\n\n" 38 | 39 | for idx, option in enumerate(options[1:]): 40 | confirmation_msg += f"{emoji[idx]} - {option}\n" 41 | to_react.append(emoji[idx]) 42 | 43 | confirmation_msg += "*Sondage proposé par* " + \ 44 | str(ctx.message.author.mention) 45 | if time == 0: 46 | confirmation_msg += "" 47 | else: 48 | confirmation_msg += f"\n\nVous avez {time} secondes pour voter!" 49 | 50 | poll_msg = await ctx.send(confirmation_msg) 51 | for emote in to_react: 52 | await poll_msg.add_reaction(emote) 53 | 54 | if time != 0: 55 | await asyncio.sleep(time) 56 | async for message in ctx.message.channel.history(): 57 | if message.id == poll_msg.id: 58 | poll_msg = message 59 | 60 | results = {} 61 | 62 | for reaction in poll_msg.reactions: 63 | if reaction.emoji in to_react: 64 | results[reaction.emoji] = reaction.count - 1 65 | end_msg = "Le sondage est términé. Les résultats sont:\n\n" 66 | 67 | for result in results: 68 | end_msg += "{} {} - {} votes\n". \ 69 | format(result, 70 | options[emoji.index(result)+1], 71 | results[result]) 72 | 73 | top_result = max(results, key=lambda key: results[key]) 74 | 75 | if len([x for x in results 76 | if results[x] == results[top_result]]) > 1: 77 | top_results = [] 78 | for key, value in results.items(): 79 | if value == results[top_result]: 80 | top_results.append(options[emoji.index(key)+1]) 81 | end_msg += "\nLes gagnants sont : {}". \ 82 | format(", ".join(top_results)) 83 | else: 84 | top_result = options[emoji.index(top_result)+1] 85 | end_msg += "\n\"{}\" est le gagnant!".format(top_result) 86 | await ctx.send(end_msg) 87 | else: 88 | await ctx.message.delete() 89 | 90 | text = open('texts/rpoll.md').read() 91 | em = discord.Embed(title='Aide sur le sondage', 92 | description=text, 93 | colour=0xEEEEEE) 94 | await ctx.send(embed=em) 95 | 96 | 97 | def setup(bot): 98 | bot.add_cog(Sondage(bot)) 99 | -------------------------------------------------------------------------------- /cogs/vocal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import re 4 | import subprocess 5 | import uuid 6 | 7 | import discord 8 | from discord.ext import commands 9 | from gtts import gTTS 10 | 11 | 12 | class Vocal(commands.Cog): 13 | 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.playing = False 17 | self.author = None 18 | self.voice = None 19 | 20 | """---------------------------------------------------------------------""" 21 | 22 | @staticmethod 23 | def get_duration(file): 24 | popen = subprocess.Popen(("ffprobe", 25 | "-show_entries", 26 | "format=duration", 27 | "-i", file), 28 | stdout=subprocess.PIPE, 29 | stderr=subprocess.PIPE) 30 | output, err = popen.communicate() 31 | match = re.search(r"[-+]?\d*\.\d+|\d+", str(output)) 32 | return float(match.group()) 33 | 34 | @commands.command(name="voc", no_pm=True, pass_context=True) 35 | async def _voc(self, ctx, *, message=""): 36 | if message == "": 37 | await ctx.send("Veuillez écrire un message...") 38 | return 39 | if message == "stop_playing" \ 40 | and ( 41 | ctx.author.id == self.author.id 42 | or ctx.message.channel.permissions_for( 43 | ctx.message.author 44 | ).administrator is True 45 | ) \ 46 | and self.playing is True: 47 | 48 | await ctx.send('stop') 49 | await self.voice.disconnect() 50 | self.playing = False 51 | return 52 | 53 | if self.playing is True: 54 | await ctx.send("Je suis déja en train de parler," 55 | " merci de réenvoyer ton message" 56 | " quand j'aurais fini.") 57 | return 58 | 59 | user = ctx.author 60 | self.author = user 61 | 62 | if user.voice: 63 | self.playing = True 64 | filename = f"data/tmp/voc/{uuid.uuid1()}.mp3" 65 | lang = [x for x in message.split(" ") if x.startswith("lang=")] 66 | 67 | loading = await ctx.send("*Chargement du message en cours...*") 68 | 69 | if lang: 70 | choice_lang = (lang[0])[5:] 71 | message = f"{user.display_name} à dit: {message.strip(lang[0])}" if len(ctx.author.voice.channel.members) >= 4 else message.strip(lang[0]) 72 | 73 | try: 74 | tts = gTTS( 75 | text=message, 76 | lang=str(choice_lang)) 77 | except ValueError: 78 | tts = gTTS( 79 | text=message, 80 | lang="fr") 81 | await ctx.send("La langue n'est pas supportée," 82 | " le francais a donc été choisi") 83 | else: 84 | message = f"{user.display_name} à dit: {message}" if len(ctx.author.voice.channel.members) >= 4 else message 85 | tts = gTTS(text=message, 86 | lang="fr") 87 | 88 | tts.save(filename) 89 | 90 | self.voice = await user.voice.channel.connect() 91 | self.voice.play(discord.FFmpegPCMAudio(filename)) 92 | counter = 0 93 | duration = self.get_duration(filename) 94 | while not counter >= duration: 95 | if self.playing: 96 | await loading.edit( 97 | content=f"Lecture du message de {self.author.display_name} en cours : {counter}sec/{duration}sec") 98 | await asyncio.sleep(1) 99 | counter += 1 100 | else: 101 | break 102 | await self.voice.disconnect() 103 | 104 | await loading.edit(content="Lecture terminée") 105 | self.voice = None 106 | os.remove(filename) 107 | self.playing = False 108 | else: 109 | await ctx.send('Veuillez aller dans un channel vocal.') 110 | 111 | 112 | def setup(bot): 113 | bot.add_cog(Vocal(bot)) 114 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Maël / Outout | Romain" 5 | __licence__ = "WTFPL Licence 2.0" 6 | 7 | import copy 8 | import datetime 9 | import os 10 | import sys 11 | import traceback 12 | 13 | import aiohttp 14 | import discord 15 | from discord.ext import commands 16 | 17 | import cogs.utils.cli_colors as colors 18 | import config 19 | from cogs.utils import checks 20 | 21 | l_extensions = ( 22 | 'cogs.admin', 23 | 'cogs.afk', 24 | 'cogs.atc', 25 | 'cogs.basics', 26 | 'cogs.ci', 27 | 'cogs.filter_messages', 28 | 'cogs.funs', 29 | 'cogs.role', 30 | 'cogs.search', 31 | 'cogs.send_logs', 32 | 'cogs.sondage', 33 | 'cogs.utility', 34 | 'cogs.vocal', 35 | ) 36 | 37 | help_attrs = dict(hidden=True, in_help=True, name="DONOTUSE") 38 | 39 | 40 | class TuxBot(commands.Bot): 41 | def __init__(self): 42 | self.uptime = datetime.datetime.utcnow() 43 | self.config = config 44 | super().__init__(command_prefix=self.config.prefix[0], 45 | description=self.config.description, 46 | pm_help=None, 47 | help_command=None) 48 | 49 | self.client_id = self.config.client_id 50 | self.session = aiohttp.ClientSession(loop=self.loop) 51 | self._events = [] 52 | 53 | self.add_command(self.do) 54 | 55 | for extension in l_extensions: 56 | try: 57 | self.load_extension(extension) 58 | print(f"{colors.text_colors.GREEN}\"{extension}\"" 59 | f" chargé !{colors.ENDC}") 60 | except Exception as e: 61 | print(f"{colors.text_colors.RED}" 62 | f"Impossible de charger l'extension {extension}\n" 63 | f"{type(e).__name__}: {e}{colors.ENDC}", file=sys.stderr) 64 | 65 | async def on_command_error(self, ctx, error): 66 | if isinstance(error, commands.NoPrivateMessage): 67 | await ctx.author.send('Cette commande ne peut pas être utilisee ' 68 | 'en message privee.') 69 | elif isinstance(error, commands.DisabledCommand): 70 | await ctx.author.send('Desoler mais cette commande est desactive, ' 71 | 'elle ne peut donc pas être utilisée.') 72 | elif isinstance(error, commands.CommandInvokeError): 73 | print(f'In {ctx.command.qualified_name}:', file=sys.stderr) 74 | traceback.print_tb(error.original.__traceback__) 75 | print(f'{error.original.__class__.__name__}: {error.original}', 76 | file=sys.stderr) 77 | 78 | async def on_ready(self): 79 | log_channel_id = await self.fetch_channel(self.config.log_channel_id) 80 | 81 | print('\n\n---------------------') 82 | print('CONNECTÉ :') 83 | print(f'Nom d\'utilisateur: {self.user} {colors.text_style.DIM}' 84 | f'(ID: {self.user.id}){colors.ENDC}') 85 | print(f'Channel de log: {log_channel_id} {colors.text_style.DIM}' 86 | f'(ID: {log_channel_id.id}){colors.ENDC}') 87 | print(f'Prefix: {self.config.prefix[0]}') 88 | print('Merci d\'utiliser TuxBot') 89 | print('---------------------\n\n') 90 | 91 | await self.change_presence(status=discord.Status.dnd, 92 | activity=discord.Game( 93 | name=self.config.game) 94 | ) 95 | 96 | @staticmethod 97 | async def on_resumed(): 98 | print('resumed...') 99 | 100 | async def on_message(self, message): 101 | if message.author.bot: 102 | return 103 | 104 | try: 105 | await self.process_commands(message) 106 | except Exception as e: 107 | print(f'{colors.text_colors.RED}Erreur rencontré : \n' 108 | f' {type(e).__name__}: {e}{colors.ENDC} \n \n') 109 | 110 | def run(self): 111 | super().run(self.config.token, reconnect=True) 112 | 113 | @checks.has_permissions(administrator=True) 114 | @commands.command(pass_context=True, hidden=True) 115 | async def do(self, ctx, times: int, *, command): 116 | """Repeats a command a specified number of times.""" 117 | msg = copy.copy(ctx.message) 118 | msg.content = command 119 | for i in range(times): 120 | await self.process_commands(msg) 121 | 122 | 123 | if __name__ == '__main__': 124 | if os.path.exists('config.py') is not True: 125 | print(f"{colors.text_colors.RED}" 126 | f"Veuillez créer le fichier config.py{colors.ENDC}") 127 | exit() 128 | 129 | tuxbot = TuxBot() 130 | tuxbot.run() 131 | -------------------------------------------------------------------------------- /cogs/send_logs.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import socket 3 | 4 | import discord 5 | from discord.ext import commands 6 | 7 | 8 | class SendLogs(commands.Cog): 9 | """Send logs to a specific channel""" 10 | 11 | def __init__(self, bot): 12 | 13 | self.bot = bot 14 | self.log_channel = None 15 | self.main_server_id = int(self.bot.config.main_server_id) 16 | 17 | @commands.Cog.listener() 18 | async def on_resumed(self): 19 | em = discord.Embed(title="Et hop je me reconnecte à l'api 😃", 20 | colour=0x5cb85c) 21 | em.timestamp = datetime.datetime.utcnow() 22 | await self.log_channel.send(embed=em) 23 | 24 | @commands.Cog.listener() 25 | async def on_ready(self): 26 | self.log_channel = await self.bot.fetch_channel(int(self.bot.config.log_channel_id)) 27 | em = discord.Embed(title="Je suis opérationnel 😃", 28 | description=f"*Instance lancée sur " 29 | f"{socket.gethostname()}*", colour=0x5cb85c) 30 | em.timestamp = datetime.datetime.utcnow() 31 | await self.log_channel.send(embed=em) 32 | 33 | """---------------------------------------------------------------------""" 34 | 35 | @commands.Cog.listener() 36 | async def on_guild_join(self, guild: discord.Guild): 37 | em = discord.Embed(title=f"On m'a ajouté à : {str(guild.name)} 😃", 38 | colour=0x51A351) 39 | em.timestamp = datetime.datetime.utcnow() 40 | await self.log_channel.send(embed=em) 41 | 42 | @commands.Cog.listener() 43 | async def on_guild_remove(self, guild: discord.Guild): 44 | em = discord.Embed(title=f"On m'a viré de : {str(guild.name)} 😦", 45 | colour=0xBD362F) 46 | em.timestamp = datetime.datetime.utcnow() 47 | await self.log_channel.send(embed=em) 48 | 49 | """---------------------------------------------------------------------""" 50 | 51 | @commands.Cog.listener() 52 | async def on_member_join(self, member): 53 | if member.guild.id == self.main_server_id: 54 | em = discord.Embed(title=f"{str(member)} *`({str(member.id)})`* " 55 | f"nous a rejoint 😃", colour=0x51A351) 56 | em.set_footer(text=f"Compte crée le {member.created_at}") 57 | em.timestamp = datetime.datetime.utcnow() 58 | await self.log_channel.send(embed=em) 59 | 60 | @commands.Cog.listener() 61 | async def on_member_remove(self, member): 62 | if member.guild.id == self.main_server_id: 63 | em = discord.Embed(title=f"{str(member)} *`({str(member.id)})`* " 64 | f"nous a quitté 😦", colour=0xBD362F) 65 | em.timestamp = datetime.datetime.utcnow() 66 | await self.log_channel.send(embed=em) 67 | 68 | """---------------------------------------------------------------------""" 69 | 70 | @commands.Cog.listener() 71 | async def on_message_delete(self, message): 72 | if message.guild.id == self.main_server_id and not message.author.bot: 73 | async def is_a_command(message): 74 | prefix_lenght = len(await self.bot.get_prefix(message)) 75 | command = (message.content.split()[0])[prefix_lenght:] 76 | if command == '': 77 | command = "not_a_command" 78 | 79 | return self.bot.get_command(str(command)) 80 | 81 | if await is_a_command(message) is None: 82 | em = discord.Embed(title=f"Message supprimé dans :" 83 | f" {str(message.channel.name)}", 84 | colour=0xBD362F) 85 | em.add_field(name=f"{str(message.author)} " 86 | f"*`({str(message.author.id)})`* " 87 | f"a supprimé :", value=str(message.content)) 88 | em.timestamp = datetime.datetime.utcnow() 89 | await self.log_channel.send(embed=em) 90 | 91 | @commands.Cog.listener() 92 | async def on_message_edit(self, before, after): 93 | if before.guild.id == self.main_server_id and not before.author.bot: 94 | em = discord.Embed(title=f"Message edité dans : " 95 | f"{before.channel.name}", colour=0x0088CC) 96 | em.add_field(name=f"{str(before.author)} " 97 | f"*`({str(before.author.id)})`* a" 98 | f" edité :", value=str(before.content)) 99 | em.add_field(name="Pour remplacer par :", value=str(after.content)) 100 | em.timestamp = datetime.datetime.utcnow() 101 | await self.log_channel.send(embed=em) 102 | 103 | 104 | def setup(bot): 105 | bot.add_cog(SendLogs(bot)) 106 | -------------------------------------------------------------------------------- /cogs/atc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from bs4 import BeautifulSoup 4 | import requests 5 | import re 6 | 7 | import discord 8 | from discord.ext import commands 9 | 10 | 11 | class ATC(commands.Cog): 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | self.playing = False 16 | self.author = None 17 | self.voice = None 18 | 19 | @staticmethod 20 | async def extra(self, ctx, icao): 21 | if icao == "stop_playing": 22 | if self.playing and ( 23 | ctx.author.id == self.author.id 24 | or ctx.message.channel.permissions_for(ctx.message.author).administrator is True 25 | ): 26 | await self.voice.disconnect() 27 | self.playing = False 28 | await ctx.send("Écoute terminée !") 29 | return "quit" 30 | else: 31 | await ctx.send("Veuillez specifier un icao") 32 | return "quit" 33 | if icao == "info": 34 | em = discord.Embed(title=f"Infos sur les services utilisés par {self.bot.config.prefix[0]}atc") 35 | em.add_field(name="Service pour les communications:", 36 | value="[liveatc.net](https://www.liveatc.net/)", 37 | inline=False) 38 | em.add_field(name="Service pour les plans des aéroports:", 39 | value="[universalweather.com](http://www.universalweather.com/)", 40 | inline=False) 41 | await ctx.send(embed=em) 42 | return "quit" 43 | 44 | """---------------------------------------------------------------------""" 45 | 46 | @commands.command(name="atc", no_pm=True, pass_context=True) 47 | async def _atc(self, ctx, icao="stop_playing"): 48 | user = ctx.author 49 | if not user.voice: 50 | await ctx.send('Veuillez aller dans un channel vocal.') 51 | return 52 | 53 | if await self.extra(self, ctx, icao) == "quit": 54 | return 55 | 56 | if self.playing: 57 | await ctx.send(f"Une écoute est déja en court, " 58 | f"demandez à {self.author.mention} de faire " 59 | f"`.atc stop_playing` pour l'arreter") 60 | return 61 | self.author = user 62 | headers = { 63 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.35 Safari/537.36', 64 | } 65 | req = requests.post("https://www.liveatc.net/search/", 66 | data={'icao': icao}, 67 | headers=headers) 68 | html = BeautifulSoup(req.text, features="lxml") 69 | regex = r"(javascript: pageTracker\._trackPageview\('\/listen\/)(.*)(\'\)\;)" 70 | 71 | possibilities = {} 72 | emojis = ['1⃣', '2⃣', '3⃣', '4⃣', '5⃣', '6⃣', '7⃣', '8⃣', '9⃣', '🔟', 73 | '0⃣', '🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮'] 74 | to_react = [] 75 | 76 | idx = 0 77 | for a in html.findAll("a", onclick=True): 78 | val = a.get('onclick') 79 | for match in re.finditer(regex, val): 80 | possibilities[idx] = f"{emojis[idx]} - {match.groups()[1]}\n" 81 | to_react.append(emojis[idx]) 82 | idx += 1 83 | 84 | em = discord.Embed(title='Résultats pour : ' + icao, 85 | description=str(''.join(possibilities.values())), 86 | colour=0x4ECDC4) 87 | em.set_image( 88 | url=f"http://www.universalweather.com/regusers/mod-bin/uvtp_airport_image?icao={icao}") 89 | 90 | poll_msg = await ctx.send(embed=em) 91 | for emote in to_react: 92 | await poll_msg.add_reaction(emote) 93 | 94 | def check(reaction, user): 95 | return user == ctx.author and reaction.emoji in to_react and \ 96 | reaction.message.id == poll_msg.id 97 | 98 | async def waiter(future: asyncio.Future): 99 | reaction, user = await self.bot.wait_for('reaction_add', 100 | check=check) 101 | 102 | future.set_result(emojis.index(reaction.emoji)) 103 | 104 | added_emoji = asyncio.Future() 105 | self.bot.loop.create_task(waiter(added_emoji)) 106 | 107 | while not added_emoji.done(): 108 | await asyncio.sleep(0.1) 109 | 110 | freq = possibilities[added_emoji.result()].split('- ')[1] 111 | 112 | if possibilities: 113 | self.playing = True 114 | self.voice = await user.voice.channel.connect() 115 | self.voice.play( 116 | discord.FFmpegPCMAudio(f"http://yyz.liveatc.net/{freq}")) 117 | await poll_msg.delete() 118 | await ctx.send(f"Écoute de {freq}") 119 | else: 120 | await ctx.send(f"Aucun résultat trouvé pour {icao} {freq}") 121 | 122 | 123 | def setup(bot): 124 | bot.add_cog(ATC(bot)) 125 | -------------------------------------------------------------------------------- /cogs/utils/maps.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | # With credit to DanielKO 4 | 5 | from lxml import etree 6 | import datetime, re 7 | import asyncio, aiohttp 8 | 9 | NINTENDO_LOGIN_PAGE = "https://id.nintendo.net/oauth/authorize" 10 | SPLATNET_CALLBACK_URL = "https://splatoon.nintendo.net/users/auth/nintendo/callback" 11 | SPLATNET_CLIENT_ID = "12af3d0a3a1f441eb900411bb50a835a" 12 | SPLATNET_SCHEDULE_URL = "https://splatoon.nintendo.net/schedule" 13 | 14 | class Rotation(object): 15 | def __init__(self): 16 | self.start = None 17 | self.end = None 18 | self.turf_maps = [] 19 | self.ranked_mode = None 20 | self.ranked_maps = [] 21 | 22 | 23 | @property 24 | def is_over(self): 25 | return self.end < datetime.datetime.utcnow() 26 | 27 | def __str__(self): 28 | now = datetime.datetime.utcnow() 29 | prefix = '' 30 | if self.start > now: 31 | minutes_delta = int((self.start - now) / datetime.timedelta(minutes=1)) 32 | hours = int(minutes_delta / 60) 33 | minutes = minutes_delta % 60 34 | prefix = '**In {0} hours and {1} minutes**:\n'.format(hours, minutes) 35 | else: 36 | prefix = '**Current Rotation**:\n' 37 | 38 | fmt = 'Turf War is {0[0]} and {0[1]}\n{1} is {2[0]} and {2[1]}' 39 | return prefix + fmt.format(self.turf_maps, self.ranked_mode, self.ranked_maps) 40 | 41 | # based on https://github.com/Wiwiweb/SakuraiBot/blob/master/src/sakuraibot.py 42 | async def get_new_splatnet_cookie(username, password): 43 | parameters = {'client_id': SPLATNET_CLIENT_ID, 44 | 'response_type': 'code', 45 | 'redirect_uri': SPLATNET_CALLBACK_URL, 46 | 'username': username, 47 | 'password': password} 48 | 49 | async with aiohttp.post(NINTENDO_LOGIN_PAGE, data=parameters) as response: 50 | cookie = response.history[-1].cookies.get('_wag_session') 51 | if cookie is None: 52 | print(req) 53 | raise Exception("Couldn't retrieve cookie") 54 | return cookie 55 | 56 | def parse_splatnet_time(timestr): 57 | # time is given as "MM/DD at H:MM [p|a].m. (PDT|PST)" 58 | # there is a case where it goes over the year, e.g. 12/31 at ... and then 1/1 at ... 59 | # this case is kind of weird though and is currently unexpected 60 | # it could even end up being e.g. 12/31/2015 ... and then 1/1/2016 ... 61 | # we'll never know 62 | 63 | regex = r'(?P\d+)\/(?P\d+)\s*at\s*(?P\d+)\:(?P\d+)\s*(?P

a\.m\.|p\.m\.)\s*\((?P.+)\)' 64 | m = re.match(regex, timestr.strip()) 65 | 66 | if m is None: 67 | raise RuntimeError('Apparently the timestamp "{}" does not match the regex.'.format(timestr)) 68 | 69 | matches = m.groupdict() 70 | tz = matches['tz'].strip().upper() 71 | offset = None 72 | if tz == 'PDT': 73 | # EDT is UTC - 4, PDT is UTC - 7, so you need +7 to make it UTC 74 | offset = +7 75 | elif tz == 'PST': 76 | # EST is UTC - 5, PST is UTC - 8, so you need +8 to make it UTC 77 | offset = +8 78 | else: 79 | raise RuntimeError('Unknown timezone found: {}'.format(tz)) 80 | 81 | pm = matches['p'].replace('.', '') # a.m. -> am 82 | 83 | current_time = datetime.datetime.utcnow() 84 | 85 | # Kind of hacky. 86 | fmt = "{2}/{0[month]}/{0[day]} {0[hour]}:{0[minutes]} {1}".format(matches, pm, current_time.year) 87 | splatoon_time = datetime.datetime.strptime(fmt, '%Y/%m/%d %I:%M %p') + datetime.timedelta(hours=offset) 88 | 89 | # check for new year 90 | if current_time.month == 12 and splatoon_time.month == 1: 91 | splatoon_time.replace(current_time.year + 1) 92 | 93 | return splatoon_time 94 | 95 | 96 | async def get_splatnet_schedule(splatnet_cookie): 97 | cookies = {'_wag_session': splatnet_cookie} 98 | 99 | 100 | """ 101 | This is repeated 3 times: 102 | 103 | ... <--- figure out how to parse this 104 |

105 |
106 | <--- turf war 107 |
108 | ... ... 109 | ... ... 110 |
111 |
112 |
113 | <--- ranked 114 |
115 | ... ... <--- Splat Zones, Rainmaker, Tower Control 116 | ... ... 117 | ... ... 118 |
119 | """ 120 | 121 | schedule = [] 122 | async with aiohttp.get(SPLATNET_SCHEDULE_URL, cookies=cookies, data={'locale':"en"}) as response: 123 | text = await response.text() 124 | root = etree.fromstring(text, etree.HTMLParser()) 125 | stage_schedule_nodes = root.xpath("//*[@class='stage-schedule']") 126 | stage_list_nodes = root.xpath("//*[@class='stage-list']") 127 | 128 | if len(stage_schedule_nodes)*2 != len(stage_list_nodes): 129 | raise RuntimeError("SplatNet changed, need to update the parsing!") 130 | 131 | for sched_node in stage_schedule_nodes: 132 | r = Rotation() 133 | 134 | start_time, end_time = sched_node.text.split("~") 135 | r.start = parse_splatnet_time(start_time) 136 | r.end = parse_splatnet_time(end_time) 137 | 138 | tw_list_node = stage_list_nodes.pop(0) 139 | r.turf_maps = tw_list_node.xpath(".//*[@class='map-name']/text()") 140 | 141 | ranked_list_node = stage_list_nodes.pop(0) 142 | r.ranked_maps = ranked_list_node.xpath(".//*[@class='map-name']/text()") 143 | r.ranked_mode = ranked_list_node.xpath(".//*[@class='rule-description']/text()")[0] 144 | 145 | schedule.append(r) 146 | 147 | return schedule 148 | -------------------------------------------------------------------------------- /cogs/utils/menu.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | class Menu: 4 | """An interactive menu class for Discord.""" 5 | 6 | 7 | class Submenu: 8 | """A metaclass of the Menu class.""" 9 | def __init__(self, name, content): 10 | self.content = content 11 | self.leads_to = [] 12 | self.name = name 13 | 14 | def get_text(self): 15 | text = "" 16 | for idx, menu in enumerate(self.leads_to): 17 | text += "[{}] {}\n".format(idx+1, menu.name) 18 | return text 19 | 20 | def get_child(self, child_idx): 21 | try: 22 | return self.leads_to[child_idx] 23 | except IndexError: 24 | raise IndexError("child index out of range") 25 | 26 | def add_child(self, child): 27 | self.leads_to.append(child) 28 | 29 | class InputSubmenu: 30 | """A metaclass of the Menu class for submenu options that take input, instead of prompting the user to pick an option.""" 31 | def __init__(self, name, content, input_function, leads_to): 32 | self.content = content 33 | self.name = name 34 | self.input_function = input_function 35 | self.leads_to = leads_to 36 | 37 | def next_child(self): 38 | return self.leads_to 39 | 40 | class ChoiceSubmenu: 41 | """A metaclass of the Menu class for submenu options for choosing an option from a list.""" 42 | def __init__(self, name, content, options, input_function, leads_to): 43 | self.content = content 44 | self.name = name 45 | self.options = options 46 | self.input_function = input_function 47 | self.leads_to = leads_to 48 | 49 | def next_child(self): 50 | return self.leads_to 51 | 52 | 53 | def __init__(self, main_page): 54 | self.children = [] 55 | self.main = self.Submenu("main", main_page) 56 | 57 | def add_child(self, child): 58 | self.main.add_child(child) 59 | 60 | async def start(self, ctx): 61 | current = self.main 62 | menu_msg = None 63 | while True: 64 | output = "" 65 | 66 | if type(current) == self.Submenu: 67 | if type(current.content) == str: 68 | output += current.content + "\n" 69 | elif callable(current.content): 70 | current.content() 71 | else: 72 | raise TypeError("submenu body is not a str or function") 73 | 74 | if not current.leads_to: 75 | if not menu_msg: 76 | menu_msg = await ctx.send("```" + output + "```") 77 | else: 78 | await menu_msg.edit(content="```" + output + "```") 79 | break 80 | 81 | output += "\n" + current.get_text() + "\n" 82 | output += "Enter a number." 83 | 84 | if not menu_msg: 85 | menu_msg = await ctx.send("```" + output + "```") 86 | else: 87 | await menu_msg.edit(content="```" + output + "```") 88 | 89 | 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) 90 | await reply.delete() 91 | 92 | try: 93 | current = current.get_child(int(reply.content) - 1) 94 | except IndexError: 95 | print("Invalid number.") 96 | break 97 | 98 | elif type(current) == self.InputSubmenu: 99 | if type(current.content) == list: 100 | answers = [] 101 | for question in current.content: 102 | await menu_msg.edit(content="```" + question + "\n\nEnter a value." + "```") 103 | reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel) 104 | await reply.delete() 105 | answers.append(reply) 106 | current.input_function(*answers) 107 | else: 108 | await menu_msg.edit(content="```" + current.content + "\n\nEnter a value." + "```") 109 | reply = await ctx.bot.wait_for("message", check=lambda m: m.author == ctx.bot.user and m.channel == ctx.message.channel) 110 | await reply.delete() 111 | current.input_function(reply) 112 | 113 | if not current.leads_to: 114 | break 115 | 116 | current = current.leads_to 117 | 118 | elif type(current) == self.ChoiceSubmenu: 119 | result = "```" + current.content + "\n\n" 120 | if type(current.options) == dict: 121 | indexes = {} 122 | for idx, option in enumerate(current.options): 123 | result += "[{}] {}: {}\n".format(idx+1, option, current.options[option]) 124 | indexes[idx] = option 125 | else: 126 | for idx, option in current.options: 127 | result += "[{}] {}\n".format(idx+1, option) 128 | await menu_msg.edit(content=result + "\nPick an option.```") 129 | 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) 130 | await reply.delete() 131 | if type(current.options) == dict: 132 | current.input_function(reply, indexes[int(reply.content)-1]) 133 | else: 134 | current.input_function(reply, current.options[reply-1]) 135 | 136 | if not current.leads_to: 137 | break 138 | 139 | current = current.leads_to 140 | -------------------------------------------------------------------------------- /cogs/role.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | 4 | 5 | class Role(commands.Cog): 6 | """Commandes role.""" 7 | 8 | def __init__(self, bot): 9 | self.bot = bot 10 | 11 | self.ARCH_ROLE = 393077257826205706 12 | self.DEBIAN_ROLE = 393077933209550859 13 | self.RHEL_ROLE = 393078333245751296 14 | self.ANDROID_ROLE = 393087862972612627 15 | self.BSD_ROLE = 401791543708745738 16 | 17 | @commands.group(name="role", no_pm=True, pass_context=True, 18 | case_insensitive=True) 19 | async def _role(self, ctx): 20 | """Affiche l'aide sur la commande role""" 21 | if ctx.message.guild.id != 280805240977227776: 22 | return 23 | 24 | if ctx.invoked_subcommand is None: 25 | text = open('texts/roles.md').read() 26 | em = discord.Embed(title='Gestionnaires de rôles', 27 | description=text, colour=0x89C4F9) 28 | await ctx.send(embed=em) 29 | 30 | """---------------------------------------------------------------------""" 31 | 32 | @_role.command(name="arch", aliases=["archlinux", "arch_linux"], 33 | pass_context=True) 34 | async def role_arch(self, ctx): 35 | """Ajoute/retire le role 'Arch user'""" 36 | roles = ctx.message.author.roles 37 | role_id = [] 38 | for role in roles: 39 | role_id.append(role.id) 40 | 41 | user = ctx.message.author 42 | if self.ARCH_ROLE in role_id: 43 | await user.remove_roles(discord.Object(id=self.ARCH_ROLE)) 44 | await ctx.send(f"{ctx.message.author.mention} > Pourquoi tu viens " 45 | f"de supprimer Arch Linux, c'était trop compliqué " 46 | f"pour toi ? <:sad:343723037331292170>") 47 | else: 48 | await user.add_roles(discord.Object(id=self.ARCH_ROLE)) 49 | await ctx.send(f"{ctx.message.author.mention} > How un " 50 | f"ArchLinuxien, c'est bon les ``yaourt`` ? " 51 | f"<:hap:354275645574086656>") 52 | 53 | """---------------------------------------------------------------------""" 54 | 55 | @_role.command(name="debian", pass_context=True) 56 | async def role_debian(self, ctx): 57 | """Ajoute/retire le role 'debian user'""" 58 | roles = ctx.message.author.roles 59 | role_id = [] 60 | for role in roles: 61 | role_id.append(role.id) 62 | 63 | user = ctx.message.author 64 | if self.DEBIAN_ROLE in role_id: 65 | await user.remove_roles(discord.Object(id=self.DEBIAN_ROLE)) 66 | await ctx.send(f"{ctx.message.author.mention} > Adieu ! Tu verras, " 67 | f"APT te manquera ! ") 68 | else: 69 | await user.add_roles(discord.Object(id=self.DEBIAN_ROLE)) 70 | await ctx.send(f"{ctx.message.author.mention} > Un utilisateur de " 71 | f"Debian, encore et encore ! " 72 | f"<:stuck_out_tongue:343723077412323339>") 73 | 74 | """---------------------------------------------------------------------""" 75 | 76 | @_role.command(name="rhel", pass_context=True) 77 | async def role_rhel(self, ctx): 78 | """Ajoute/retire le role 'rhel user'""" 79 | roles = ctx.message.author.roles 80 | role_id = [] 81 | for role in roles: 82 | role_id.append(role.id) 83 | 84 | user = ctx.message.author 85 | if self.RHEL_ROLE in role_id: 86 | await user.remove_roles(discord.Object(id=self.RHEL_ROLE)) 87 | await ctx.send(f"{ctx.message.author.mention} > Pourquoi tu t'en " 88 | f"vas, il sont déjà assez seul là-bas " 89 | f"<:sad:343723037331292170>") 90 | else: 91 | await user.add_roles(discord.Object(id=self.RHEL_ROLE)) 92 | await ctx.send(f"{ctx.message.author.mention} > Mais, voila " 93 | f"quelqu'un qui porte des chapeaux ! " 94 | f"<:hap:354275645574086656>") 95 | 96 | """---------------------------------------------------------------------""" 97 | 98 | @_role.command(name="android", pass_context=True) 99 | async def role_android(self, ctx): 100 | """Ajoute/retire le role 'android user'""" 101 | roles = ctx.message.author.roles 102 | role_id = [] 103 | for role in roles: 104 | role_id.append(role.id) 105 | 106 | user = ctx.message.author 107 | if self.ANDROID_ROLE in role_id: 108 | await user.remove_roles(discord.Object(id=self.ANDROID_ROLE)) 109 | await ctx.send(f"{ctx.message.author.mention} >How, me dit pas " 110 | f"que tu as compris que les Android's allaient " 111 | f"exterminer le monde ? " 112 | f"<:trollface:375327667160875008>") 113 | else: 114 | await user.add_roles(discord.Object(id=self.ANDROID_ROLE)) 115 | await ctx.send(f"{ctx.message.author.mention} > Hey, un utilisateur" 116 | f" d'Android, prêt à continuer l'extermination de " 117 | f"WP et iOS ? " 118 | f"<:stuck_out_tongue:343723077412323339>") 119 | 120 | """---------------------------------------------------------------------""" 121 | 122 | @_role.command(name="bsd", pass_context=True) 123 | async def role_bsd(self, ctx): 124 | """Ajoute/retire le role 'BSD user'""" 125 | roles = ctx.message.author.roles 126 | role_id = [] 127 | for role in roles: 128 | role_id.append(role.id) 129 | 130 | user = ctx.message.author 131 | if self.BSD_ROLE in role_id: 132 | await user.remove_roles(discord.Object(id=self.BSD_ROLE)) 133 | await ctx.send(f"{ctx.message.author.mention} > Ohhhh fait gaffe " 134 | f"ou le démon va te piquer") 135 | else: 136 | await user.add_roles(discord.Object(id=self.BSD_ROLE)) 137 | await ctx.send(f"{ctx.message.author.mention} > Quelqu'un sous " 138 | f"BSD ! Au moins il a pas besoin de mettre GNU " 139 | f"devant son OS à chaque fois :d") 140 | 141 | """---------------------------------------------------------------------""" 142 | 143 | @_role.command(name="staff", pass_context=True, hidden=True) 144 | async def role_staff(self, ctx): 145 | """Easter egg""" 146 | 147 | await ctx.send(f"{ctx.message.author.mention} > Vous n'avez pas " 148 | f"le rôle staff, tu crois quoi :joy:") 149 | 150 | 151 | def setup(bot): 152 | bot.add_cog(Role(bot)) 153 | -------------------------------------------------------------------------------- /cogs/search.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import discord 3 | import asyncio 4 | import urllib.request 5 | import wikipedia 6 | 7 | wikipedia.set_lang("fr") 8 | 9 | 10 | class Search(commands.Cog): 11 | """Commandes de WWW.""" 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | @commands.group(name="search", no_pm=True, pass_context=True) 17 | async def _search(self, ctx): 18 | """Rechercher sur le world wide web""" 19 | if ctx.invoked_subcommand is None: 20 | text = open('texts/search.md').read() 21 | em = discord.Embed(title='Commandes de search TuxBot', 22 | description=text, 23 | colour=0x89C4F9) 24 | await ctx.send(embed=em) 25 | 26 | @_search.command(pass_context=True, name="docubuntu") 27 | async def search_docubuntu(self, ctx, args): 28 | attends = await ctx.send("_Je te cherche ça {} !_".format( 29 | ctx.message.author.mention)) 30 | html = urllib.request.urlopen("https://doc.ubuntu-fr.org/" + 31 | args).read() 32 | if "avez suivi un lien" in str(html): 33 | await attends.edit(content=":sob: Nooooon ! Cette page n'existe " 34 | "pas, mais tu peux toujours la créer : " 35 | "https://doc.ubuntu-fr.org/" + args) 36 | else: 37 | await attends.delete() 38 | embed = discord.Embed(description="Voila j'ai trouvé ! Voici la " 39 | "page ramenant à votre recherche," 40 | " toujours aussi bien rédigée " 41 | ":wink: : https://doc.ubuntu-fr." 42 | "org/" + args, 43 | url='http://doc.ubuntu-fr.org/') 44 | embed.set_author(name="DocUbuntu-Fr", 45 | url='http://doc.ubuntu-fr.org/', 46 | icon_url='https://tuxbot.outout.xyz/data/ubuntu.png') 47 | embed.set_thumbnail(url='https://tuxbot.outout.xyz/data/ubuntu.png') 48 | embed.set_footer(text="Merci à ceux qui ont pris le temps d'écrire " 49 | "cette documentation") 50 | await ctx.send(embed=embed) 51 | 52 | @_search.command(pass_context=True, name="docarch") 53 | async def search_docarch(self, ctx, args): 54 | attends = await ctx.send("_Je te cherche ça {} !_".format( 55 | ctx.message.author.mention)) 56 | html = urllib.request.urlopen("https://wiki.archlinux.org/index.php/" + 57 | args).read() 58 | if "There is currently no text in this page" in str(html): 59 | await attends.edit(content=":sob: Nooooon ! Cette page n'existe " 60 | "pas.") 61 | else: 62 | await attends.delete() 63 | embed = discord.Embed(description="Voila j'ai trouvé ! Voici la " 64 | "page ramenant à votre recherche," 65 | " toujours aussi bien rédigée " 66 | ":wink: : https://wiki.archlinux." 67 | "org/index.php/" + args, 68 | url='https://wiki.archlinux.org/index.php/') 69 | embed.set_author(name="Doc ArchLinux", 70 | url='https://wiki.archlinux.org/index.php/', 71 | icon_url='https://tuxbot.outout.xyz/data/arch.png') 72 | embed.set_thumbnail(url='https://tuxbot.outout.xyz/data/arch.png') 73 | embed.set_footer(text="Merci à ceux qui ont pris le temps d'écrire " 74 | "cette documentation") 75 | await ctx.send(embed=embed) 76 | 77 | @_search.command(pass_context=True, name="wikipedia") 78 | async def search_wikipedia(self, ctx: commands.Context, args): 79 | """Fait une recherche sur wikipd""" 80 | 81 | wait = await ctx.send("_Je cherche..._") 82 | results = wikipedia.search(args) 83 | nbmr = 0 84 | mmssgg = "" 85 | 86 | for value in results: 87 | nbmr = nbmr + 1 88 | mmssgg = mmssgg + "**{}**: {} \n".format(str(nbmr), value) 89 | 90 | em = discord.Embed(title='Résultats de : ' + args, 91 | description = mmssgg, 92 | colour=0x4ECDC4) 93 | em.set_thumbnail(url="https://upload.wikimedia.org/wikipedia/commons/" 94 | "2/26/Paullusmagnus-logo_%28large%29.png") 95 | await wait.delete() 96 | 97 | sending = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"] 98 | 99 | def check(reaction, user): 100 | return user == ctx.author and reaction.emoji in sending and \ 101 | reaction.message.id == msg.id 102 | 103 | async def waiter(future: asyncio.Future): 104 | reaction, user = await self.bot.wait_for('reaction_add', 105 | check=check) 106 | future.set_result(reaction.emoji) 107 | 108 | emoji = asyncio.Future() 109 | self.bot.loop.create_task(waiter(emoji)) 110 | 111 | msg = await ctx.send(embed=em) 112 | for e in sending: 113 | await msg.add_reaction(e) 114 | if emoji.done(): 115 | break 116 | 117 | while not emoji.done(): 118 | await asyncio.sleep(0.1) 119 | 120 | page = int(sending.index(emoji.result())) 121 | 122 | args_ = results[page] 123 | 124 | try: 125 | await msg.delete() 126 | await ctx.trigger_typing() 127 | wait = await ctx.send(ctx.message.author.mention + 128 | " ah ok sympa cette recherche, je l'effectue de suite !") 129 | wp = wikipedia.page(args_) 130 | wp_contenu = wp.summary[:200] + "..." 131 | em = discord.Embed(title='Wikipedia : ' + wp.title, 132 | description = "{} \n_Lien_ : {} ".format( 133 | wp_contenu, wp.url), 134 | colour=0x9B59B6) 135 | em.set_author(name="Wikipedia", 136 | url='http://wikipedia.org', 137 | icon_url='https://upload.wikimedia.org/wikipedia/' 138 | 'commons/2/26/Paullusmagnus-logo_%28large' 139 | '%29.png') 140 | em.set_thumbnail(url = "https://upload.wikimedia.org/wikipedia/" 141 | "commons/2/26/Paullusmagnus-logo_%28large" 142 | "%29.png") 143 | em.set_footer(text="Merci à eux de nous fournir une encyclopédie " 144 | "libre !") 145 | await wait.delete() 146 | await ctx.send(embed=em) 147 | 148 | except wikipedia.exceptions.PageError: 149 | # TODO : A virer dans l'event on_error 150 | await ctx.send(":open_mouth: Une **erreur interne** est survenue," 151 | " si cela ce reproduit contactez votre" 152 | " administrateur ou faites une Issue sur" 153 | " ``gitea`` !") 154 | 155 | 156 | def setup(bot): 157 | bot.add_cog(Search(bot)) 158 | -------------------------------------------------------------------------------- /cogs/funs.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | import asyncio 3 | import discord 4 | import urllib.request 5 | import json 6 | import random 7 | import requests 8 | 9 | 10 | class Funs(commands.Cog): 11 | """Commandes funs.""" 12 | 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | """---------------------------------------------------------------------""" 17 | 18 | @commands.command() 19 | async def avatar(self, ctx, user: discord.Member = None): 20 | """Récuperer l'avatar de ...""" 21 | 22 | if user is None: 23 | user = ctx.message.author 24 | 25 | embed = discord.Embed(title="Avatar de : " + user.name, 26 | url=user.avatar_url_as(format="png"), 27 | description=f"[Voir en plus grand]" 28 | f"({user.avatar_url_as(format='png')})") 29 | embed.set_thumbnail(url=user.user.avatar_url_as(format="png")) 30 | await ctx.send(embed=embed) 31 | 32 | """---------------------------------------------------------------------""" 33 | 34 | @commands.command(pass_context=True) 35 | async def poke(self, ctx, user: discord.Member): 36 | """Poke quelqu'un""" 37 | await ctx.send(f":clap: Hey {user.mention} tu t'es fait poker par" 38 | f" {ctx.message.author} !") 39 | await ctx.message.delete() 40 | 41 | """---------------------------------------------------------------------""" 42 | 43 | @commands.command() 44 | async def btcprice(self, ctx): 45 | """Le prix du BTC""" 46 | loading = await ctx.send("_réfléchis..._") 47 | try: 48 | url = urllib.request.urlopen("https://blockchain.info/fr/ticker") 49 | btc = json.loads(url.read().decode()) 50 | except KeyError: 51 | btc = 1 52 | 53 | if btc == 1: 54 | await loading.edit(content="Impossible d'accèder à l'API" 55 | " blockchain.info, veuillez réessayer" 56 | " ultérieurment ! :c") 57 | else: 58 | frbtc = str(btc["EUR"]["last"]).replace(".", ",") 59 | usbtc = str(btc["USD"]["last"]).replace(".", ",") 60 | await loading.edit(content=f"Un bitcoin est égal à :" 61 | f" {usbtc}$US soit {frbtc}€.") 62 | 63 | """---------------------------------------------------------------------""" 64 | 65 | @commands.command() 66 | async def joke(self, ctx, number: str = 0): 67 | """Print a random joke in a json file""" 68 | with open('texts/jokes.json') as js: 69 | jk = json.load(js) 70 | 71 | try: 72 | if 15 >= int(number) > 0: 73 | clef = str(number) 74 | else: 75 | clef = str(random.randint(1, 15)) 76 | except Exception: 77 | clef = str(random.randint(1, 15)) 78 | 79 | joke = jk["{}".format(clef)] 80 | 81 | embed = discord.Embed(title="Blague _{}_ : ".format(clef), 82 | description=joke['content'], colour=0x03C9A9) 83 | embed.set_footer(text="Par " + joke['author']) 84 | embed.set_thumbnail(url='https://outout.tech/tuxbot/blobjoy.png') 85 | await ctx.send(embed=embed) 86 | 87 | """---------------------------------------------------------------------""" 88 | 89 | @commands.command() 90 | async def ethylotest(self, ctx): 91 | """Ethylotest simulator 2018""" 92 | results_poulet = ["Désolé mais mon ethylotest est sous Windows Vista, " 93 | "merci de patienter...", 94 | "_(ethylotest)_ : Une erreur est survenue. Windows " 95 | "cherche une solution à se problème.", 96 | "Mais j'l'ai foutu où ce p\\*\\*\\* d'ethylotest de m\\*\\*\\* " 97 | "bordel fait ch\\*\\*\\*", 98 | "C'est pas possible z'avez cassé l'ethylotest !"] 99 | results_client = ["D'accord, il n'y a pas de problème à cela je suis " 100 | "complètement clean", 101 | "Bien sur si c'est votre devoir !", "Suce bi\\*e !", 102 | "J'ai l'air d'être bourré ?", 103 | "_laissez moi prendre un bonbon à la menthe..._"] 104 | 105 | result_p = random.choice(results_poulet) 106 | result_c = random.choice(results_client) 107 | 108 | await ctx.send(":oncoming_police_car: Bonjour bonjour, contrôle " 109 | "d'alcoolémie !") 110 | await asyncio.sleep(0.5) 111 | await ctx.send(":man: " + result_c) 112 | await asyncio.sleep(1) 113 | await ctx.send(":police_car: " + result_p) 114 | 115 | """---------------------------------------------------------------------""" 116 | 117 | @commands.command() 118 | async def coin(self, ctx): 119 | """Coin flip simulator 2025""" 120 | starts_msg = ["Je lance la pièce !", "C'est parti !", "C'est une pièce" 121 | " d'un cent faut" 122 | " pas la perdre", 123 | "C'est une pièce d'un euro faut pas la perdre", 124 | "Je lance !"] 125 | results_coin = ["{0} pile", "{0} face", "{1} Heu c'est quoi pile c'est" 126 | " quoi face enfaite ?", 127 | "{1} Oh shit, je crois que je l'ai perdue", 128 | "{1} Et bim je te vol ta pièce !", 129 | "{0} Oh une erreur d'impression il n'y a ni pile ni" 130 | " face !"] 131 | 132 | start = random.choice(starts_msg) 133 | result = random.choice(results_coin) 134 | 135 | await ctx.send(start) 136 | await asyncio.sleep(0.6) 137 | await ctx.send(result.format(":moneybag: Et la pièce retombe sur ...", 138 | ":robot:")) 139 | 140 | """---------------------------------------------------------------------""" 141 | 142 | @commands.command() 143 | async def pokemon(self, ctx): 144 | """Random pokemon fight""" 145 | with open('texts/pokemons.json') as js: 146 | jk = json.load(js) 147 | 148 | poke1 = jk[random.randint(1, 150)] 149 | poke2 = jk[random.randint(1, 150)] 150 | 151 | try: 152 | if poke1['MaxHP'] > poke2['MaxHP']: 153 | winer = poke1 154 | else: 155 | winer = poke2 156 | except KeyError: 157 | winer = poke1 158 | 159 | await ctx.send(":flag_white: **Le combat commence !**") 160 | await asyncio.sleep(1) 161 | await ctx.send(":loudspeaker: Les concurants sont {} contre {} ! Bonne" 162 | " chance à eux !".format(poke1["Name"], poke2["Name"])) 163 | await asyncio.sleep(0.5) 164 | await ctx.send(":boom: {} commence et utilise {}".format( 165 | poke1["Name"], poke1["Fast Attack(s)"][0]["Name"])) 166 | await asyncio.sleep(1) 167 | await ctx.send(":dash: {} réplique avec {}".format( 168 | poke2["Name"], poke2["Fast Attack(s)"][0]["Name"])) 169 | await asyncio.sleep(1.2) 170 | await ctx.send("_le combat continue de se dérouler..._") 171 | await asyncio.sleep(1.5) 172 | await ctx.send(":trophy: Le gagnant est **{}** !".format( 173 | winer["Name"])) 174 | 175 | """---------------------------------------------------------------------""" 176 | 177 | @commands.command() 178 | async def randomcat(self, ctx): 179 | """Display a random cat""" 180 | r = requests.get('http://aws.random.cat/meow') 181 | cat = str(r.json()['file']) 182 | embed = discord.Embed(title="Meow", 183 | description="[Voir le chat plus grand]({})". 184 | format(cat), colour=0x03C9A9) 185 | embed.set_thumbnail(url=cat) 186 | embed.set_author(name="Random.cat", url='https://random.cat/') 187 | await ctx.send(embed=embed) 188 | 189 | 190 | def setup(bot): 191 | bot.add_cog(Funs(bot)) 192 | -------------------------------------------------------------------------------- /cogs/ci.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | 4 | import discord 5 | import requests 6 | from discord.ext import commands 7 | 8 | from .utils import checks 9 | from .utils import db 10 | from .utils.checks import get_user, check_date 11 | 12 | 13 | class Identity(commands.Cog): 14 | """Commandes des cartes d'identité .""" 15 | 16 | def __init__(self, bot): 17 | self.bot = bot 18 | 19 | self.conn = db.connect_to_db(self) 20 | self.cursor = self.conn.cursor() 21 | 22 | self.cursor.execute("""SHOW TABLES LIKE 'users'""") 23 | result = self.cursor.fetchone() 24 | 25 | if not result: 26 | # Creation table Utilisateur si premiere fois 27 | 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);" 28 | self.cursor.execute(sql) 29 | 30 | """--------------------------------------------------------------------------------------------------------------------------""" 31 | 32 | @commands.group(name="ci", no_pm=True, pass_context=True) 33 | async def _ci(self, ctx): 34 | """Cartes d'identité""" 35 | 36 | if ctx.invoked_subcommand is None: 37 | text = open('texts/ci-info.md').read() 38 | em = discord.Embed(title='Commandes de carte d\'identité de TuxBot', description=text, colour=0x89C4F9) 39 | await ctx.send(embed=em) 40 | 41 | """--------------------------------------------------------------------------------------------------------------------------""" 42 | 43 | @_ci.command(pass_context=True, name="show") 44 | async def ci_show(self, ctx, args: str = None): 45 | self.conn = db.connect_to_db(self) 46 | self.cursor = self.conn.cursor() 47 | 48 | if args == None: 49 | user = get_user(ctx.message, ctx.author.name) 50 | else: 51 | user = get_user(ctx.message, args) 52 | 53 | if user: 54 | self.cursor.execute("""SELECT userid, username, useravatar, userbirth, cidate, cibureau, os, config, pays, id FROM users WHERE userid=%s""",(str(user.id))) 55 | result = self.cursor.fetchone() 56 | 57 | def isexist(var): 58 | if not var: 59 | return "Non renseigné." 60 | else: 61 | return var 62 | 63 | if not result: 64 | await ctx.send(f"{ctx.author.mention}> :x: Désolé mais {user.mention} est sans papier !") 65 | else: 66 | try: 67 | user_birth = datetime.datetime.fromisoformat(result[3]) 68 | user_birth_day = check_date(str(user_birth.day)) 69 | user_birth_month = check_date(str(user_birth.month)) 70 | 71 | formated_user_birth = str(user_birth_day) + "/" + str(user_birth_month) + "/" + str(user_birth.year) 72 | 73 | try: ## a virer une fois le patch appliqué pour tout le monde 74 | cidate = datetime.datetime.fromisoformat(result[4]) 75 | cidate_day = check_date(str(cidate.day)) ## a garder 76 | cidate_month = check_date(str(cidate.month)) ## a garder 77 | 78 | formated_cidate = str(cidate_day) + "/" + str(cidate_month) + "/" + str(cidate.year) ## a garder 79 | except ValueError: ## a virer une fois le patch appliqué pour tout le monde 80 | formated_cidate = str(result[4]).replace('-', '/') ## a virer une fois le patch appliqué pour tout le monde 81 | 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 82 | 83 | embed=discord.Embed(title="Carte d'identité | Communisme Linuxien") 84 | embed.set_author(name=result[1], icon_url=result[2]) 85 | embed.set_thumbnail(url = result[2]) 86 | embed.add_field(name="Nom :", value=result[1], inline=True) 87 | embed.add_field(name="Système d'exploitation :", value=isexist(result[6]), inline=True) 88 | embed.add_field(name="Configuration Système : ", value=isexist(result[7]), inline=True) 89 | embed.add_field(name="Date de naissance sur discord : ", value=formated_user_birth, inline=True) 90 | embed.add_field(name="Pays : ", value=isexist(result[8]), inline=True) 91 | embed.add_field(name="Profil sur le web : ", value=f"https://tuxbot.gnous.eu/users/{result[9]}", inline=True) 92 | embed.set_footer(text=f"Enregistré dans le bureau {result[5]} le {formated_cidate}.") 93 | await ctx.send(embed=embed) 94 | except Exception as e: 95 | 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: !") 96 | await ctx.send(f':sob: Une erreur est survenue : \n {type(e).__name__}: {e}') 97 | else: 98 | return await ctx.send('Impossible de trouver l\'user.') 99 | 100 | """--------------------------------------------------------------------------------------------------------------------------""" 101 | 102 | @_ci.command(pass_context=True, name="register") 103 | async def ci_register(self, ctx): 104 | self.conn = db.connect_to_db(self) 105 | self.cursor = self.conn.cursor() 106 | 107 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 108 | result = self.cursor.fetchone() 109 | 110 | if result: 111 | await ctx.send("Mais tu as déja une carte d'identité ! u_u") 112 | else: 113 | now = datetime.datetime.now() 114 | 115 | 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))) 116 | self.conn.commit() 117 | await ctx.send(f":clap: Bievenue à toi {ctx.author.name} dans le communisme {ctx.message.guild.name} ! Fait ``.ci`` pour plus d'informations !") 118 | 119 | """--------------------------------------------------------------------------------------------------------------------------""" 120 | 121 | @_ci.command(pass_context=True, name="delete") 122 | async def ci_delete(self, ctx): 123 | self.conn = db.connect_to_db(self) 124 | self.cursor = self.conn.cursor() 125 | 126 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 127 | result = self.cursor.fetchone() 128 | 129 | if result: 130 | self.cursor.execute("""DELETE FROM users WHERE userid =%s""", (str(ctx.author.id))) 131 | self.conn.commit() 132 | await ctx.send("Tu es maintenant sans papiers !") 133 | else: 134 | await ctx.send("Déja enregistre ta carte d'identité avant de la supprimer u_u (après c'est pas logique...)") 135 | 136 | """--------------------------------------------------------------------------------------------------------------------------""" 137 | 138 | @_ci.command(pass_context=True, name="update") 139 | async def ci_update(self, ctx): 140 | self.conn = db.connect_to_db(self) 141 | self.cursor = self.conn.cursor() 142 | 143 | try: 144 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 145 | result = self.cursor.fetchone() 146 | 147 | if result: 148 | self.cursor.execute("""SELECT cidate FROM users WHERE userid=%s""",(str(ctx.author.id))) 149 | old_ci_date = self.cursor.fetchone() 150 | 151 | try: 152 | new_ci_date = datetime.datetime.fromisoformat(old_ci_date[0]) 153 | except ValueError: 154 | old_ci_date = datetime.datetime.strptime(old_ci_date[0].replace('/', '-'), '%d-%m-%Y') 155 | 156 | old_ci_date_day = check_date(str(old_ci_date.day)) 157 | old_ci_date_month = check_date(str(old_ci_date.month)) 158 | 159 | new_ci_date = f"{str(old_ci_date.year)}-{str(old_ci_date_month)}-{str(old_ci_date_day)} 00:00:00.000000" 160 | 161 | await ctx.send("succes update") 162 | 163 | self.cursor.execute("""UPDATE users SET cidate = %s WHERE userid = %s""", (str(new_ci_date), str(ctx.author.id))) 164 | self.conn.commit() 165 | 166 | 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))) 167 | self.conn.commit() 168 | await ctx.send(f"{ctx.author.mention}> Tu viens, en quelques sortes, de renaitre !") 169 | else: 170 | await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !") 171 | 172 | except Exception as e: #TODO : A virer dans l'event on_error 173 | await ctx.send(':( Erreur veuillez contacter votre administrateur :') 174 | await ctx.send(f'{type(e).__name__}: {e}') 175 | 176 | """--------------------------------------------------------------------------------------------------------------------------""" 177 | 178 | @_ci.command(pass_context=True, name="setconfig") 179 | async def ci_setconfig(self, ctx, *, conf: str = None): 180 | self.conn = db.connect_to_db(self) 181 | self.cursor = self.conn.cursor() 182 | 183 | if conf: 184 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 185 | result = self.cursor.fetchone() 186 | 187 | if result: 188 | self.cursor.execute("""UPDATE users SET config = %s WHERE userid = %s""", (str(conf), str(ctx.author.id))) 189 | self.conn.commit() 190 | await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !") 191 | else: 192 | await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !") 193 | else: 194 | await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !") 195 | 196 | """--------------------------------------------------------------------------------------------------------------------------""" 197 | 198 | @_ci.command(pass_context=True, name="setos") 199 | async def ci_setos(self, ctx, *, conf: str = None): 200 | self.conn = db.connect_to_db(self) 201 | self.cursor = self.conn.cursor() 202 | 203 | if conf: 204 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 205 | result = self.cursor.fetchone() 206 | 207 | if result: 208 | self.cursor.execute("""UPDATE users SET os = %s WHERE userid = %s""", (str(conf), str(ctx.author.id))) 209 | self.conn.commit() 210 | await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !") 211 | else: 212 | await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !") 213 | else: 214 | await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !") 215 | 216 | """--------------------------------------------------------------------------------------------------------------------------""" 217 | 218 | @_ci.command(pass_context=True, name="setcountry") 219 | async def ci_setcountry(self, ctx, *, country: str = None): 220 | self.conn = db.connect_to_db(self) 221 | self.cursor = self.conn.cursor() 222 | 223 | if country: 224 | self.cursor.execute("""SELECT id, userid FROM users WHERE userid=%s""", (str(ctx.author.id))) 225 | result = self.cursor.fetchone() 226 | 227 | if result: 228 | self.cursor.execute("""UPDATE users SET pays = %s WHERE userid = %s""", (str(country), str(ctx.author.id))) 229 | self.conn.commit() 230 | await ctx.send(f"{ctx.author.mention}> :ok_hand: Carte d'identité mise à jour !") 231 | else: 232 | await ctx.send(f"{ctx.author.mention}> :x: Veuillez enregistrer votre carte d'identité pour commencer !") 233 | else: 234 | await ctx.send(f"{ctx.author.mention}> :x: Il manque un paramètre !") 235 | 236 | """--------------------------------------------------------------------------------------------------------------------------""" 237 | 238 | @_ci.command(pass_context=True, name="online_edit") 239 | async def ci_online_edit(self, ctx): 240 | self.conn = db.connect_to_db(self) 241 | self.cursor = self.conn.cursor() 242 | 243 | self.cursor.execute("""SELECT id FROM users WHERE userid=%s""",(str(ctx.author.id))) 244 | result = self.cursor.fetchone() 245 | 246 | if not result: 247 | return await ctx.send(f"Déja enregistre ta carte d'identité avant de l'éditer u_u (après c'est pas logique...)") 248 | 249 | dm = await ctx.author.create_dm() 250 | 251 | try: 252 | def is_exist(key, value): 253 | self.cursor.execute("""SELECT * FROM sessions WHERE {}=%s""".format(str(key)), (str(value))) 254 | return self.cursor.fetchone() 255 | 256 | user_id = result[0] 257 | is_admin = '1' if str(ctx.author.id) in self.bot.config.authorized_id else '0' 258 | token = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'*25, 25)) 259 | created_at = datetime.datetime.utcnow() 260 | 261 | while is_exist('token', token): 262 | token = ''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'*25, 25)) 263 | 264 | if is_exist('user_id', user_id): 265 | 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))) 266 | self.conn.commit() 267 | else: 268 | 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))) 269 | self.conn.commit() 270 | 271 | 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) 272 | embed.set_footer(text=f"Cette clé sera valide durant les 10 prochaines minutes, ne la communiques à personne !") 273 | await dm.send(embed=embed) 274 | await dm.send(token) 275 | 276 | await ctx.send(f"{ctx.author.mention} ta clé d'édition t'a été envoyée en message privé") 277 | 278 | except Exception as e: 279 | 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") 280 | 281 | """--------------------------------------------------------------------------------------------------------------------------""" 282 | 283 | @checks.has_permissions(administrator=True) 284 | @_ci.command(pass_context=True, name="list") 285 | async def ci_list(self, ctx): 286 | self.conn = db.connect_to_db(self) 287 | self.cursor = self.conn.cursor() 288 | 289 | self.cursor.execute("""SELECT id, username FROM users""") 290 | rows = self.cursor.fetchall() 291 | msg = "" 292 | try: 293 | for row in rows: 294 | row_id = row[0] 295 | row_name = row[1].encode('utf-8') 296 | msg += f"{str(row_id)} : {str(row_name)} \n" 297 | post = requests.post("https://hastebin.com/documents", data=msg) 298 | await ctx.send(f"{ctx.author.mention} liste posté avec succès sur :\nhttps://hastebin.com/{post.json()['key']}.txt") 299 | 300 | with open('ci_list.txt', 'w', encoding='utf-8') as fp: 301 | for row in rows: 302 | row_id = row[0] 303 | row_name = row[1] 304 | 305 | fp.write(f"{str(row_id)} : {str(row_name)} \n") 306 | 307 | except Exception as e: 308 | await ctx.send(f':sob: Une erreur est survenue : \n {type(e).__name__}: {e}') 309 | 310 | def setup(bot): 311 | bot.add_cog(Identity(bot)) 312 | -------------------------------------------------------------------------------- /cogs/admin.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from discord.ext import commands 3 | import discord 4 | from .utils import checks 5 | 6 | from .utils.checks import get_user 7 | 8 | 9 | class Admin(commands.Cog): 10 | """Commandes secrètes d'administration.""" 11 | 12 | def __init__(self, bot): 13 | self.bot = bot 14 | self._last_result = None 15 | self.sessions = set() 16 | 17 | """---------------------------------------------------------------------""" 18 | 19 | @checks.has_permissions(administrator=True) 20 | @commands.command(name="upload", pass_context=True) 21 | async def _upload(self, ctx, *, url=""): 22 | if len(ctx.message.attachments) >= 1: 23 | file = ctx.message.attachments[0].url 24 | elif url != "": 25 | file = url 26 | else: 27 | em = discord.Embed(title='Une erreur est survenue', 28 | description="Fichier introuvable.", 29 | colour=0xDC3546) 30 | await ctx.send(embed=em) 31 | return 32 | 33 | async with aiohttp.ClientSession() as session: 34 | async with session.get(file) as r: 35 | image = await r.content.read() 36 | 37 | with open(f"data/tmp/{str(ctx.author.id)}.png", 'wb') as f: 38 | f.write(image) 39 | f.close() 40 | await ctx.send(file=discord.File(f"data/tmp/{str(ctx.author.id)}.png")) 41 | 42 | @checks.has_permissions(administrator=True) 43 | @commands.command(name="ban", pass_context=True) 44 | async def _ban(self, ctx, user, *, reason=""): 45 | """Ban user""" 46 | user = get_user(ctx.message, user) 47 | if user and str(user.id) not in self.bot.config.unkickable_id: 48 | try: 49 | await user.ban(reason=reason) 50 | return_msg = f"`{user.mention}` a été banni\n" 51 | if reason: 52 | return_msg += f"raison : `{reason}`" 53 | return_msg += "." 54 | await ctx.send(return_msg) 55 | except discord.Forbidden: 56 | await ctx.send('Impossible de bannir cet user,' 57 | ' probleme de permission.') 58 | else: 59 | return await ctx.send('Impossible de trouver l\'user.') 60 | 61 | """---------------------------------------------------------------------""" 62 | 63 | @checks.has_permissions(administrator=True) 64 | @commands.command(name="kick", pass_context=True) 65 | async def _kick(self, ctx, user, *, reason=""): 66 | """Kick a user""" 67 | user = get_user(ctx.message, user) 68 | if user and str(user.id) not in self.bot.config.unkickable_id: 69 | try: 70 | await user.kick(reason=reason) 71 | return_msg = f"`{user.mention}` a été kické\n" 72 | if reason: 73 | return_msg += f"raison : `{reason}`" 74 | return_msg += "." 75 | await ctx.send(return_msg) 76 | except discord.Forbidden: 77 | await ctx.send('Impossible de kicker cet user,' 78 | ' probleme de permission.') 79 | else: 80 | return await ctx.send('Impossible de trouver l\'user.') 81 | 82 | """---------------------------------------------------------------------""" 83 | 84 | @checks.has_permissions(administrator=True) 85 | @commands.command(name='clear', pass_context=True) 86 | async def _clear(self, ctx, number: int, silent: str = True): 87 | """Clear of message(s)""" 88 | try: 89 | await ctx.message.delete() 90 | except Exception: 91 | print(f"Impossible de supprimer le message " 92 | f"\"{str(ctx.message.content)}\"") 93 | if number < 1000: 94 | try: 95 | await ctx.message.channel.purge(limit=number) 96 | except Exception as e: # TODO : A virer dans l'event on_error 97 | if silent is not True: 98 | await ctx.send(f':sob: Une erreur est survenue : \n' 99 | f' {type(e).__name__}: {e}') 100 | if silent is not True: 101 | await ctx.send("Hop voila j'ai viré des messages! Hello World") 102 | print(f"{str(number)} messages ont été supprimés") 103 | else: 104 | await ctx.send('Trop de messages, entre un nombre < 1000') 105 | 106 | """---------------------------------------------------------------------""" 107 | 108 | @checks.has_permissions(administrator=True) 109 | @commands.command(name='say', pass_context=True) 110 | async def _say(self, ctx, *, tosay: str): 111 | """Say a message in the current channel""" 112 | try: 113 | try: 114 | await ctx.message.delete() 115 | except Exception: 116 | print(f"Impossible de supprimer le message " 117 | f"\"{str(ctx.message.content)}\"") 118 | await ctx.send(tosay) 119 | except Exception as e: # TODO : A virer dans l'event on_error 120 | await ctx.send(f':sob: Une erreur est survenue : \n' 121 | f' {type(e).__name__}: {e}') 122 | 123 | """---------------------------------------------------------------------""" 124 | 125 | @checks.has_permissions(administrator=True) 126 | @commands.command(name='sayto', pass_context=True) 127 | async def _sayto(self, ctx, channel_id: int, *, tosay: str): 128 | """Say a message in the channel""" 129 | try: 130 | chan = self.bot.get_channel(channel_id) 131 | try: 132 | await ctx.message.delete() 133 | except Exception: 134 | print(f"Impossible de supprimer le message " 135 | f"\"{str(ctx.message.content)}\"") 136 | try: 137 | await chan.send(tosay) 138 | except Exception: 139 | print(f"Impossible d'envoyer le message dans {str(channel_id)}") 140 | except Exception as e: # TODO : A virer dans l'event on_error 141 | await ctx.send(f':sob: Une erreur est survenue : \n' 142 | f' {type(e).__name__}: {e}') 143 | 144 | """---------------------------------------------------------------------""" 145 | 146 | @checks.has_permissions(administrator=True) 147 | @commands.command(name='sayto_dm', pass_context=True) 148 | async def _sayto_dm(self, ctx, user_id: int, *, tosay: str): 149 | """Say a message to the user""" 150 | try: 151 | user = self.bot.get_user(user_id) 152 | try: 153 | await ctx.message.delete() 154 | except Exception: 155 | print(f"Impossible de supprimer le message " 156 | f"\"{str(ctx.message.content)}\"") 157 | try: 158 | await user.send(tosay) 159 | except Exception: 160 | print(f"Impossible d'envoyer le message dans {str(user_id)}") 161 | except Exception as e: # TODO : A virer dans l'event on_error 162 | await ctx.send(f':sob: Une erreur est survenue : \n' 163 | f' {type(e).__name__}: {e}') 164 | 165 | """---------------------------------------------------------------------""" 166 | 167 | @checks.has_permissions(administrator=True) 168 | @commands.command(name='editsay', pass_context=True) 169 | async def _editsay(self, ctx, message_id: int, *, new_content: str): 170 | """Edit a bot's message""" 171 | try: 172 | try: 173 | await ctx.message.delete() 174 | except Exception: 175 | print(f"Impossible de supprimer le message " 176 | f"\"{str(ctx.message.content)}\"") 177 | toedit = await ctx.channel.get_message(message_id) 178 | except discord.errors.NotFound: 179 | await ctx.send(f"Impossible de trouver le message avec l'id " 180 | f"`{message_id}` sur ce salon") 181 | return 182 | try: 183 | await toedit.edit(content=str(new_content)) 184 | except discord.errors.Forbidden: 185 | await ctx.send("J'ai po les perms pour editer mes messages :(") 186 | 187 | """---------------------------------------------------------------------""" 188 | 189 | @checks.has_permissions(administrator=True) 190 | @commands.command(name='addreaction', pass_context=True) 191 | async def _addreaction(self, ctx, message_id: int, reaction: str): 192 | """Add reactions to a message""" 193 | try: 194 | try: 195 | await ctx.message.delete() 196 | except Exception: 197 | print(f"Impossible de supprimer le message " 198 | f"\"{str(ctx.message.content)}\"") 199 | toadd = await ctx.channel.get_message(message_id) 200 | except discord.errors.NotFound: 201 | await ctx.send(f"Impossible de trouver le message avec l'id " 202 | f"`{message_id}` sur ce salon") 203 | return 204 | try: 205 | await toadd.add_reaction(reaction) 206 | except discord.errors.Forbidden: 207 | await ctx.send("J'ai po les perms pour ajouter des réactions :(") 208 | 209 | """---------------------------------------------------------------------""" 210 | 211 | @checks.has_permissions(administrator=True) 212 | @commands.command(name='delete', pass_context=True) 213 | async def _delete(self, ctx, message_id: int): 214 | """Delete message in current channel""" 215 | try: 216 | try: 217 | await ctx.message.delete() 218 | except Exception: 219 | print(f"Impossible de supprimer le message " 220 | f"\"{str(ctx.message.content)}\"") 221 | todelete = await ctx.channel.get_message(message_id) 222 | except discord.errors.NotFound: 223 | await ctx.send(f"Impossible de trouver le message avec l'id " 224 | f"`{message_id}` sur ce salon") 225 | return 226 | try: 227 | await todelete.delete() 228 | except discord.errors.Forbidden: 229 | await ctx.send("J'ai po les perms pour supprimer des messages :(") 230 | 231 | """---------------------------------------------------------------------""" 232 | 233 | @checks.has_permissions(administrator=True) 234 | @commands.command(name='deletefrom', pass_context=True) 235 | async def _deletefrom(self, ctx, chan_id: int, *, message_id: int): 236 | """Delete message in channel""" 237 | try: 238 | chan = self.bot.get_channel(chan_id) 239 | try: 240 | await ctx.message.delete() 241 | except Exception: 242 | print(f"Impossible de supprimer le message " 243 | f"\"{str(ctx.message.content)}\"") 244 | todelete = await chan.get_message(message_id) 245 | except discord.errors.NotFound: 246 | await ctx.send(f"Impossible de trouver le message avec l'id " 247 | f"`{id}` sur le salon") 248 | return 249 | try: 250 | await todelete.delete() 251 | except discord.errors.Forbidden: 252 | await ctx.send("J'ai po les perms pour supprimer le message :(") 253 | 254 | """---------------------------------------------------------------------""" 255 | 256 | @checks.has_permissions(administrator=True) 257 | @commands.command(name='embed', pass_context=True) 258 | async def _embed(self, ctx, *, msg: str = "help"): 259 | """Send an embed""" 260 | if msg != "help": 261 | ptext = title \ 262 | = description \ 263 | = image \ 264 | = thumbnail \ 265 | = color \ 266 | = footer \ 267 | = author \ 268 | = None 269 | timestamp = discord.Embed.Empty 270 | embed_values = msg.split('|') 271 | for i in embed_values: 272 | if i.strip().lower().startswith('ptext='): 273 | ptext = i.strip()[6:].strip() 274 | elif i.strip().lower().startswith('title='): 275 | title = i.strip()[6:].strip() 276 | elif i.strip().lower().startswith('description='): 277 | description = i.strip()[12:].strip() 278 | elif i.strip().lower().startswith('desc='): 279 | description = i.strip()[5:].strip() 280 | elif i.strip().lower().startswith('image='): 281 | image = i.strip()[6:].strip() 282 | elif i.strip().lower().startswith('thumbnail='): 283 | thumbnail = i.strip()[10:].strip() 284 | elif i.strip().lower().startswith('colour='): 285 | color = i.strip()[7:].strip() 286 | elif i.strip().lower().startswith('color='): 287 | color = i.strip()[6:].strip() 288 | elif i.strip().lower().startswith('footer='): 289 | footer = i.strip()[7:].strip() 290 | elif i.strip().lower().startswith('author='): 291 | author = i.strip()[7:].strip() 292 | elif i.strip().lower().startswith('timestamp'): 293 | timestamp = ctx.message.created_at 294 | else: 295 | if description is None and not i.strip()\ 296 | .lower().startswith('field='): 297 | description = i.strip() 298 | 299 | if color: 300 | if color.startswith('#'): 301 | color = color[1:] 302 | if not color.startswith('0x'): 303 | color = '0x' + color 304 | 305 | if ptext \ 306 | is title \ 307 | is description \ 308 | is image \ 309 | is thumbnail \ 310 | is color \ 311 | is footer \ 312 | is author \ 313 | is None \ 314 | and 'field=' not in msg: 315 | try: 316 | await ctx.message.delete() 317 | except Exception: 318 | print("Impossible de supprimer le message \"" + str( 319 | ctx.message.content) + "\"") 320 | return await ctx.send(content=None, 321 | embed=discord.Embed(description=msg)) 322 | 323 | if color: 324 | em = discord.Embed(timestamp=timestamp, title=title, 325 | description=description, 326 | color=int(color, 16)) 327 | else: 328 | em = discord.Embed(timestamp=timestamp, title=title, 329 | description=description) 330 | for i in embed_values: 331 | if i.strip().lower().startswith('field='): 332 | field_inline = True 333 | field = i.strip().lstrip('field=') 334 | field_name, field_value = field.split('value=') 335 | if 'inline=' in field_value: 336 | field_value, field_inline = field_value.split( 337 | 'inline=') 338 | if 'false' in field_inline.lower() \ 339 | or 'no' in field_inline.lower(): 340 | field_inline = False 341 | field_name = field_name.strip().lstrip('name=') 342 | em.add_field(name=field_name, value=field_value.strip(), 343 | inline=field_inline) 344 | if author: 345 | if 'icon=' in author: 346 | text, icon = author.split('icon=') 347 | if 'url=' in icon: 348 | em.set_author(name=text.strip()[5:], 349 | icon_url=icon.split('url=')[0].strip(), 350 | url=icon.split('url=')[1].strip()) 351 | else: 352 | em.set_author(name=text.strip()[5:], icon_url=icon) 353 | else: 354 | if 'url=' in author: 355 | em.set_author(name=author.split('url=')[0].strip()[5:], 356 | url=author.split('url=')[1].strip()) 357 | else: 358 | em.set_author(name=author) 359 | 360 | if image: 361 | em.set_image(url=image) 362 | if thumbnail: 363 | em.set_thumbnail(url=thumbnail) 364 | if footer: 365 | if 'icon=' in footer: 366 | text, icon = footer.split('icon=') 367 | em.set_footer(text=text.strip()[5:], icon_url=icon) 368 | else: 369 | em.set_footer(text=footer) 370 | 371 | try: 372 | await ctx.message.delete() 373 | except Exception: 374 | print("Impossible de supprimer le message \"" + str( 375 | ctx.message.content) + "\"") 376 | await ctx.send(content=ptext, embed=em) 377 | 378 | else: 379 | embed = discord.Embed( 380 | title="Aide sur l'utilisation de la commande .embed:") 381 | embed.add_field(name="Titre:", value="title=", 382 | inline=True) 383 | embed.add_field(name="Description:", 384 | value="description=", inline=True) 385 | embed.add_field(name="Couleur:", value="color=", 386 | inline=True) 387 | embed.add_field(name="Image:", 388 | value="image=", 389 | inline=True) 390 | embed.add_field(name="Thumbnail:", 391 | value="thumbnail=", inline=True) 392 | embed.add_field(name="Auteur:", value="author=", 393 | inline=True) 394 | embed.add_field(name="Icon", value="icon=", 395 | inline=True) 396 | embed.add_field(name="Footer", value="footer=", 397 | inline=True) 398 | embed.set_footer(text="Exemple: .embed title=Un titre |" 399 | " description=Une description |" 400 | " color=3AB35E |" 401 | " field=name=test value=test") 402 | 403 | await ctx.send(embed=embed) 404 | 405 | 406 | def setup(bot): 407 | bot.add_cog(Admin(bot)) 408 | -------------------------------------------------------------------------------- /cogs/utility.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import pytz 4 | import random 5 | import urllib 6 | 7 | import discord 8 | import requests 9 | from discord.ext import commands 10 | 11 | 12 | class Utility(commands.Cog): 13 | """Commandes utilitaires.""" 14 | 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | @commands.group(name="clock", pass_context=True, case_insensitive=True) 19 | async def clock(self, ctx): 20 | """Display hour in a country""" 21 | 22 | if ctx.invoked_subcommand is None: 23 | text = open('texts/clocks.md').read() 24 | em = discord.Embed(title='Liste des Horloges', description=text, colour=0xEEEEEE) 25 | await ctx.send(embed=em) 26 | 27 | @clock.command(name="montréal", aliases=["mtl", "montreal"], pass_context=True) 28 | async def clock_montreal(self, ctx): 29 | then = datetime.datetime.now(pytz.utc) 30 | 31 | utc = then.astimezone(pytz.timezone('America/Montreal')) 32 | site = "http://ville.montreal.qc.ca/" 33 | img = "https://upload.wikimedia.org/wikipedia/commons/e/e0/Rentier_fws_1.jpg" 34 | country = "au Canada, Québec" 35 | description = "Montréal est la deuxième ville la plus peuplée du Canada. Elle se situe dans la région du Québec" 36 | 37 | form = '%H heures %M' 38 | tt = utc.strftime(form) 39 | 40 | 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) 41 | em.set_thumbnail(url = img) 42 | await ctx.send(embed=em) 43 | 44 | @clock.command(name="vancouver", pass_context=True) 45 | async def clock_vancouver(self, ctx): 46 | then = datetime.datetime.now(pytz.utc) 47 | 48 | utc = then.astimezone(pytz.timezone('America/Vancouver')) 49 | site = "http://vancouver.ca/" 50 | img = "https://upload.wikimedia.org/wikipedia/commons/f/fe/Dock_Vancouver.JPG" 51 | country = "au Canada" 52 | description = "Vancouver, officiellement City of Vancouver, est une cité portuaire au Canada" 53 | 54 | form = '%H heures %M' 55 | tt = utc.strftime(form) 56 | 57 | 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) 58 | em.set_thumbnail(url = img) 59 | await ctx.send(embed=em) 60 | 61 | @clock.command(name="new-york",aliases=["ny", "n-y", "new york"], pass_context=True) 62 | async def clock_new_york(self, ctx): 63 | then = datetime.datetime.now(pytz.utc) 64 | 65 | utc = then.astimezone(pytz.timezone('America/New_York')) 66 | site = "http://www1.nyc.gov/" 67 | img = "https://upload.wikimedia.org/wikipedia/commons/e/e3/NewYork_LibertyStatue.jpg" 68 | country = "aux U.S.A." 69 | 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. " 70 | 71 | form = '%H heures %M' 72 | tt = utc.strftime(form) 73 | 74 | 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) 75 | em.set_thumbnail(url = img) 76 | await ctx.send(embed=em) 77 | 78 | @clock.command(name="la", aliases=["los-angeles", "losangeles", "l-a", "los angeles"], pass_context=True) 79 | async def clock_la(self, ctx): 80 | then = datetime.datetime.now(pytz.utc) 81 | 82 | utc = then.astimezone(pytz.timezone('America/Los_Angeles')) 83 | site = "https://www.lacity.org/" 84 | img = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/LA_Skyline_Mountains2.jpg/800px-LA_Skyline_Mountains2.jpg" 85 | country = "aux U.S.A." 86 | 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." 87 | 88 | form = '%H heures %M' 89 | tt = utc.strftime(form) 90 | 91 | 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) 92 | em.set_thumbnail(url = img) 93 | await ctx.send(embed=em) 94 | 95 | @clock.command(name="paris", aliases=["baguette"],pass_context=True) 96 | async def clock_paris(self, ctx): 97 | then = datetime.datetime.now(pytz.utc) 98 | 99 | utc = then.astimezone(pytz.timezone('Europe/Paris')) 100 | site = "http://www.paris.fr/" 101 | img = "https://upload.wikimedia.org/wikipedia/commons/a/af/Tour_eiffel_at_sunrise_from_the_trocadero.jpg" 102 | country = "en France" 103 | 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." 104 | 105 | form = '%H heures %M' 106 | tt = utc.strftime(form) 107 | 108 | 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) 109 | em.set_thumbnail(url = img) 110 | await ctx.send(embed=em) 111 | 112 | @clock.command(name="berlin", pass_context=True) 113 | async def clock_berlin(self, ctx): 114 | then = datetime.datetime.now(pytz.utc) 115 | 116 | utc = then.astimezone(pytz.timezone('Europe/Berlin')) 117 | site = "http://www.berlin.de/" 118 | img = "https://upload.wikimedia.org/wikipedia/commons/9/91/Eduard_Gaertner_Schlossfreiheit.jpg" 119 | country = "en Allemagne" 120 | 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. " 121 | 122 | form = '%H heures %M' 123 | tt = utc.strftime(form) 124 | 125 | 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) 126 | em.set_thumbnail(url = img) 127 | await ctx.send(embed=em) 128 | 129 | @clock.command(name="berne", aliases=["zurich", "bern"], pass_context=True) 130 | async def clock_berne(self, ctx): 131 | then = datetime.datetime.now(pytz.utc) 132 | 133 | utc = then.astimezone(pytz.timezone('Europe/Zurich')) 134 | site = "http://www.berne.ch/" 135 | img = "https://upload.wikimedia.org/wikipedia/commons/d/db/Justitia_Statue_02.jpg" 136 | country = "en Suisse" 137 | 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 »." 138 | 139 | form = '%H heures %M' 140 | tt = utc.strftime(form) 141 | 142 | 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) 143 | em.set_thumbnail(url = img) 144 | await ctx.send(embed=em) 145 | 146 | @clock.command(name="tokyo", pass_context=True) 147 | async def clock_tokyo(self, ctx): 148 | then = datetime.datetime.now(pytz.utc) 149 | 150 | utc = then.astimezone(pytz.timezone('Asia/Tokyo')) 151 | site = "http://www.gotokyo.org/" 152 | img = "https://upload.wikimedia.org/wikipedia/commons/3/37/TaroTokyo20110213-TokyoTower-01.jpg" 153 | country = "au Japon" 154 | description = "Tokyo, anciennement Edo, officiellement la préfecture métropolitaine de Tokyo, est la capitale du Japon." 155 | 156 | form = '%H heures %M' 157 | tt = utc.strftime(form) 158 | 159 | 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) 160 | em.set_thumbnail(url = img) 161 | await ctx.send(embed=em) 162 | 163 | @clock.command(name="moscou", aliases=["moscow", "moskova"], pass_context=True) 164 | async def clock_moscou(self, ctx): 165 | then = datetime.datetime.now(pytz.utc) 166 | 167 | utc = then.astimezone(pytz.timezone('Europe/Moscow')) 168 | site = "https://www.mos.ru/" 169 | img = "https://upload.wikimedia.org/wikipedia/commons/f/f7/Andreyevsky_Zal.jpg" 170 | country = "en Russie" 171 | 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. " 172 | 173 | form = '%H heures %M' 174 | tt = utc.strftime(form) 175 | 176 | 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) 177 | em.set_thumbnail(url = img) 178 | await ctx.send(embed=em) 179 | 180 | """---------------------------------------------------------------------""" 181 | 182 | @commands.command() 183 | async def ytdiscover(self, ctx): 184 | """Random youtube channel""" 185 | with open('texts/ytb.json') as js: 186 | ytb = json.load(js) 187 | 188 | clef = str(random.randint(0,12)) 189 | chaine = ytb["{}".format(clef)] 190 | 191 | embed = discord.Embed(title=chaine['name'], 192 | url=chaine['url'], 193 | description=f"**{chaine['name']}**, {chaine['desc']} \n[Je veux voir ça]({chaine['url']})") 194 | embed.set_thumbnail(url='https://outout.tech/tuxbot/yt.png') 195 | await ctx.send(embed=embed) 196 | 197 | """---------------------------------------------------------------------""" 198 | 199 | @commands.command(name='hastebin', pass_context=True) 200 | async def _hastebin(self, ctx, *, data): 201 | """Poster sur Hastebin.""" 202 | await ctx.message.delete() 203 | 204 | post = requests.post("https://hastebin.com/documents", data=data) 205 | 206 | try: 207 | await ctx.send(f"{ctx.author.mention} message posté avec succès sur :\nhttps://hastebin.com/{post.json()['key']}.txt") 208 | except json.JSONDecodeError: 209 | await ctx.send("Impossible de poster ce message. L'API doit être HS.") 210 | 211 | """---------------------------------------------------------------------""" 212 | 213 | @commands.command(name='iplocalise', pass_context=True) 214 | async def _iplocalise(self, ctx, ipaddress): 215 | """Recup headers.""" 216 | if ipaddress.startswith("http://"): 217 | if ipaddress[-1:] == '/': 218 | ipaddress = ipaddress[:-1] 219 | ipaddress = ipaddress.split("http://")[1] 220 | if ipaddress.startswith("https://"): 221 | if ipaddress[-1:] == '/': 222 | ipaddress = ipaddress[:-1] 223 | ipaddress = ipaddress.split("https://")[1] 224 | 225 | iploading = await ctx.send("_réfléchis..._") 226 | ipapi = urllib.request.urlopen("http://ip-api.com/json/" + ipaddress) 227 | ipinfo = json.loads(ipapi.read().decode()) 228 | 229 | if ipinfo["status"] != "fail": 230 | if ipinfo['org']: 231 | org = ipinfo['org'] 232 | else: 233 | org = 'n/a' 234 | 235 | if ipinfo['query']: 236 | ip = ipinfo['query'] 237 | else: 238 | ip = 'n/a' 239 | 240 | if ipinfo['city']: 241 | city = ipinfo['city'] 242 | else: 243 | city = 'n/a' 244 | 245 | if ipinfo['regionName']: 246 | regionName = ipinfo['regionName'] 247 | else: 248 | regionName = 'n/a' 249 | 250 | if ipinfo['country']: 251 | country = ipinfo['country'] 252 | else: 253 | country = 'n/a' 254 | 255 | embed = discord.Embed(title=f"Informations pour {ipaddress} *`({ip})`*", color=0x5858d7) 256 | embed.add_field(name="Appartient à :", value=org, inline = False) 257 | embed.add_field(name="Se situe à :", value=city, inline = True) 258 | embed.add_field(name="Region :", value=f"{regionName} ({country})", inline = True) 259 | embed.set_thumbnail(url=f"https://www.countryflags.io/{ipinfo['countryCode']}/flat/64.png") 260 | await ctx.send(embed=embed) 261 | else: 262 | await ctx.send(content=f"Erreur, impossible d'avoir des informations sur l'adresse IP {ipinfo['query']}") 263 | await iploading.delete() 264 | 265 | """---------------------------------------------------------------------""" 266 | @commands.command(name='getheaders', pass_context=True) 267 | async def _getheaders(self, ctx, *, adresse): 268 | """Recuperer les HEADERS :d""" 269 | print("Loaded") 270 | if adresse.startswith("http://") != True and adresse.startswith("https://") != True: 271 | adresse = "http://" + adresse 272 | if len(adresse) > 200: 273 | await ctx.send("{0} Essaye d'entrer une adresse de moins de 200 caractères plutôt.".format(ctx.author.mention)) 274 | elif adresse.startswith("http://") or adresse.startswith("https://") or adresse.startswith("ftp://"): 275 | try: 276 | get = urllib.request.urlopen(adresse, timeout = 1) 277 | embed = discord.Embed(title="Entêtes de {0}".format(adresse), color=0xd75858) 278 | embed.add_field(name="Code Réponse", value=get.getcode(), inline = True) 279 | embed.set_thumbnail(url="https://http.cat/{}".format(str(get.getcode()))) 280 | if get.getheader('location'): 281 | embed.add_field(name="Redirection vers", value=get.getheader('location'), inline=True) 282 | if get.getheader('server'): 283 | embed.add_field(name="Serveur", value=get.getheader('server'), inline=True) 284 | if get.getheader('content-type'): 285 | embed.add_field(name="Type de contenu", value = get.getheader('content-type'), inline = True) 286 | if get.getheader('x-content-type-options'): 287 | embed.add_field(name="x-content-type", value= get.getheader('x-content-type-options'), inline=True) 288 | if get.getheader('x-frame-options'): 289 | embed.add_field(name="x-frame-options", value= get.getheader('x-frame-options'), inline=True) 290 | if get.getheader('cache-control'): 291 | embed.add_field(name="Controle du cache", value = get.getheader('cache-control'), inline = True) 292 | await ctx.send(embed=embed) 293 | except urllib.error.HTTPError as e: 294 | embed = discord.Embed(title="Entêtes de {0}".format(adresse), color=0xd75858) 295 | embed.add_field(name="Code Réponse", value=e.getcode(), inline = True) 296 | embed.set_thumbnail(url="https://http.cat/{}".format(str(e.getcode()))) 297 | await ctx.send(embed=embed) 298 | print('''An error occurred: {} The response code was {}'''.format(e, e.getcode())) 299 | except urllib.error.URLError as e: 300 | print("ERROR @ getheaders @ urlerror : {} - adress {}".format(e, adresse)) 301 | await ctx.send('[CONTACTER ADMIN] URLError: {}'.format(e.reason)) 302 | except Exception as e: 303 | print("ERROR @ getheaders @ Exception : {} - adress {}".format(e, adresse)) 304 | await ctx.send("{0} Impossible d'accèder à {1}, es-tu sur que l'adresse {1} est correcte et que le serveur est allumé ?".format(ctx.author.mention, adresse)) 305 | else: 306 | await ctx.send("{0} Merci de faire commencer {1} par ``https://``, ``http://`` ou ``ftp://``.".format(ctx.author.mention, adresse)) 307 | 308 | """---------------------------------------------------------------------""" 309 | 310 | @commands.command(name='git', pass_context=True) 311 | async def _git(self, ctx): 312 | """Pour voir mon code""" 313 | text = "How tu veux voir mon repos Gitea pour me disséquer ? " \ 314 | "Pas de soucis ! Je suis un Bot, je ne ressens pas la " \ 315 | "douleur !\n https://git.gnous.eu/gnouseu/tuxbot-bot" 316 | em = discord.Embed(title='Repos TuxBot-Bot', description=text, colour=0xE9D460) 317 | em.set_author(name='Gnous', icon_url="https://cdn.discordapp.com/" 318 | "icons/280805240977227776/" 319 | "9ba1f756c9d9bfcf27989d0d0abb3862" 320 | ".png") 321 | await ctx.send(embed=em) 322 | 323 | """---------------------------------------------------------------------""" 324 | 325 | @commands.command(name='quote', pass_context=True) 326 | async def _quote(self, ctx, quote_id): 327 | global quoted_message 328 | 329 | async def get_message(message_id: int): 330 | for channel in ctx.message.guild.channels: 331 | if isinstance(channel, discord.TextChannel): 332 | test_chan = await self.bot.fetch_channel(channel.id) 333 | try: 334 | return await test_chan.fetch_message(message_id) 335 | except discord.NotFound: 336 | pass 337 | return None 338 | 339 | quoted_message = await get_message(int(quote_id)) 340 | 341 | if quoted_message is not None: 342 | embed = discord.Embed(colour=quoted_message.author.colour, 343 | description=quoted_message.clean_content, 344 | timestamp=quoted_message.created_at) 345 | embed.set_author(name=quoted_message.author.display_name, 346 | icon_url=quoted_message.author.avatar_url_as( 347 | format="jpg")) 348 | if len(quoted_message.attachments) >= 1: 349 | embed.set_image(url=quoted_message.attachments[0].url) 350 | embed.add_field(name="**Original**", 351 | value=f"[Go!]({quoted_message.jump_url})") 352 | embed.set_footer(text="#" + quoted_message.channel.name) 353 | 354 | await ctx.send(embed=embed) 355 | else: 356 | await ctx.send("Impossible de trouver le message.") 357 | 358 | 359 | def setup(bot): 360 | bot.add_cog(Utility(bot)) 361 | -------------------------------------------------------------------------------- /cogs/utils/paginator.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import discord 3 | 4 | class CannotPaginate(Exception): 5 | pass 6 | 7 | class Pages: 8 | """Implements a paginator that queries the user for the 9 | pagination interface. 10 | 11 | Pages are 1-index based, not 0-index based. 12 | 13 | If the user does not reply within 2 minutes then the pagination 14 | interface exits automatically. 15 | 16 | Parameters 17 | ------------ 18 | ctx: Context 19 | The context of the command. 20 | entries: List[str] 21 | A list of entries to paginate. 22 | per_page: int 23 | How many entries show up per page. 24 | show_entry_count: bool 25 | Whether to show an entry count in the footer. 26 | 27 | Attributes 28 | ----------- 29 | embed: discord.Embed 30 | The embed object that is being used to send pagination info. 31 | Feel free to modify this externally. Only the description, 32 | footer fields, and colour are internally modified. 33 | permissions: discord.Permissions 34 | Our permissions for the channel. 35 | """ 36 | def __init__(self, ctx, *, entries, per_page=12, show_entry_count=True): 37 | self.bot = ctx.bot 38 | self.entries = entries 39 | self.message = ctx.message 40 | self.channel = ctx.channel 41 | self.author = ctx.author 42 | self.per_page = per_page 43 | pages, left_over = divmod(len(self.entries), self.per_page) 44 | if left_over: 45 | pages += 1 46 | self.maximum_pages = pages 47 | self.embed = discord.Embed(colour=discord.Colour.blurple()) 48 | self.paginating = len(entries) > per_page 49 | self.show_entry_count = show_entry_count 50 | self.reaction_emojis = [ 51 | ('\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.first_page), 52 | ('\N{BLACK LEFT-POINTING TRIANGLE}', self.previous_page), 53 | ('\N{BLACK RIGHT-POINTING TRIANGLE}', self.next_page), 54 | ('\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}', self.last_page), 55 | ('\N{INPUT SYMBOL FOR NUMBERS}', self.numbered_page ), 56 | ('\N{BLACK SQUARE FOR STOP}', self.stop_pages), 57 | ('\N{INFORMATION SOURCE}', self.show_help), 58 | ] 59 | 60 | if ctx.guild is not None: 61 | self.permissions = self.channel.permissions_for(ctx.guild.me) 62 | else: 63 | self.permissions = self.channel.permissions_for(ctx.bot.user) 64 | 65 | if not self.permissions.embed_links: 66 | raise CannotPaginate('Bot does not have embed links permission.') 67 | 68 | if not self.permissions.send_messages: 69 | raise CannotPaginate('Bot cannot send messages.') 70 | 71 | if self.paginating: 72 | # verify we can actually use the pagination session 73 | if not self.permissions.add_reactions: 74 | raise CannotPaginate('Bot does not have add reactions permission.') 75 | 76 | if not self.permissions.read_message_history: 77 | raise CannotPaginate('Bot does not have Read Message History permission.') 78 | 79 | def get_page(self, page): 80 | base = (page - 1) * self.per_page 81 | return self.entries[base:base + self.per_page] 82 | 83 | async def show_page(self, page, *, first=False): 84 | self.current_page = page 85 | entries = self.get_page(page) 86 | p = [] 87 | for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)): 88 | p.append(f'{index}. {entry}') 89 | 90 | if self.maximum_pages > 1: 91 | if self.show_entry_count: 92 | text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)' 93 | else: 94 | text = f'Page {page}/{self.maximum_pages}' 95 | 96 | self.embed.set_footer(text=text) 97 | 98 | if not self.paginating: 99 | self.embed.description = '\n'.join(p) 100 | return await self.channel.send(embed=self.embed) 101 | 102 | if not first: 103 | self.embed.description = '\n'.join(p) 104 | await self.message.edit(embed=self.embed) 105 | return 106 | 107 | p.append('') 108 | p.append('Confused? React with \N{INFORMATION SOURCE} for more info.') 109 | self.embed.description = '\n'.join(p) 110 | self.message = await self.channel.send(embed=self.embed) 111 | for (reaction, _) in self.reaction_emojis: 112 | if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'): 113 | # no |<< or >>| buttons if we only have two pages 114 | # we can't forbid it if someone ends up using it but remove 115 | # it from the default set 116 | continue 117 | 118 | await self.message.add_reaction(reaction) 119 | 120 | async def checked_show_page(self, page): 121 | if page != 0 and page <= self.maximum_pages: 122 | await self.show_page(page) 123 | 124 | async def first_page(self): 125 | """goes to the first page""" 126 | await self.show_page(1) 127 | 128 | async def last_page(self): 129 | """goes to the last page""" 130 | await self.show_page(self.maximum_pages) 131 | 132 | async def next_page(self): 133 | """goes to the next page""" 134 | await self.checked_show_page(self.current_page + 1) 135 | 136 | async def previous_page(self): 137 | """goes to the previous page""" 138 | await self.checked_show_page(self.current_page - 1) 139 | 140 | async def show_current_page(self): 141 | if self.paginating: 142 | await self.show_page(self.current_page) 143 | 144 | async def numbered_page(self): 145 | """lets you type a page number to go to""" 146 | to_delete = [] 147 | to_delete.append(await self.channel.send('What page do you want to go to?')) 148 | 149 | def message_check(m): 150 | return m.author == self.author and \ 151 | self.channel == m.channel and \ 152 | m.content.isdigit() 153 | 154 | try: 155 | msg = await self.bot.wait_for('message', check=message_check, timeout=30.0) 156 | except asyncio.TimeoutError: 157 | to_delete.append(await self.channel.send('Took too long.')) 158 | await asyncio.sleep(5) 159 | else: 160 | page = int(msg.content) 161 | to_delete.append(msg) 162 | if page != 0 and page <= self.maximum_pages: 163 | await self.show_page(page) 164 | else: 165 | to_delete.append(await self.channel.send(f'Invalid page given. ({page}/{self.maximum_pages})')) 166 | await asyncio.sleep(5) 167 | 168 | try: 169 | await self.channel.delete_messages(to_delete) 170 | except Exception: 171 | pass 172 | 173 | async def show_help(self): 174 | """shows this message""" 175 | messages = ['Welcome to the interactive paginator!\n'] 176 | messages.append('This interactively allows you to see pages of text by navigating with ' \ 177 | 'reactions. They are as follows:\n') 178 | 179 | for (emoji, func) in self.reaction_emojis: 180 | messages.append(f'{emoji} {func.__doc__}') 181 | 182 | self.embed.description = '\n'.join(messages) 183 | self.embed.clear_fields() 184 | self.embed.set_footer(text=f'We were on page {self.current_page} before this message.') 185 | await self.message.edit(embed=self.embed) 186 | 187 | async def go_back_to_current_page(): 188 | await asyncio.sleep(60.0) 189 | await self.show_current_page() 190 | 191 | self.bot.loop.create_task(go_back_to_current_page()) 192 | 193 | async def stop_pages(self): 194 | """stops the interactive pagination session""" 195 | await self.message.delete() 196 | self.paginating = False 197 | 198 | def react_check(self, reaction, user): 199 | if user is None or user.id != self.author.id: 200 | return False 201 | 202 | if reaction.message.id != self.message.id: 203 | return False 204 | 205 | for (emoji, func) in self.reaction_emojis: 206 | if reaction.emoji == emoji: 207 | self.match = func 208 | return True 209 | return False 210 | 211 | async def paginate(self): 212 | """Actually paginate the entries and run the interactive loop if necessary.""" 213 | first_page = self.show_page(1, first=True) 214 | if not self.paginating: 215 | await first_page 216 | else: 217 | # allow us to react to reactions right away if we're paginating 218 | self.bot.loop.create_task(first_page) 219 | 220 | while self.paginating: 221 | try: 222 | reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=120.0) 223 | except asyncio.TimeoutError: 224 | self.paginating = False 225 | try: 226 | await self.message.clear_reactions() 227 | except: 228 | pass 229 | finally: 230 | break 231 | 232 | try: 233 | await self.message.remove_reaction(reaction, user) 234 | except: 235 | pass # can't remove it so don't bother doing so 236 | 237 | await self.match() 238 | 239 | class FieldPages(Pages): 240 | """Similar to Pages except entries should be a list of 241 | tuples having (key, value) to show as embed fields instead. 242 | """ 243 | async def show_page(self, page, *, first=False): 244 | self.current_page = page 245 | entries = self.get_page(page) 246 | 247 | self.embed.clear_fields() 248 | self.embed.description = discord.Embed.Empty 249 | 250 | for key, value in entries: 251 | self.embed.add_field(name=key, value=value, inline=False) 252 | 253 | if self.maximum_pages > 1: 254 | if self.show_entry_count: 255 | text = f'Page {page}/{self.maximum_pages} ({len(self.entries)} entries)' 256 | else: 257 | text = f'Page {page}/{self.maximum_pages}' 258 | 259 | self.embed.set_footer(text=text) 260 | 261 | if not self.paginating: 262 | return await self.channel.send(embed=self.embed) 263 | 264 | if not first: 265 | await self.message.edit(embed=self.embed) 266 | return 267 | 268 | self.message = await self.channel.send(embed=self.embed) 269 | for (reaction, _) in self.reaction_emojis: 270 | if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'): 271 | # no |<< or >>| buttons if we only have two pages 272 | # we can't forbid it if someone ends up using it but remove 273 | # it from the default set 274 | continue 275 | 276 | await self.message.add_reaction(reaction) 277 | 278 | import itertools 279 | import inspect 280 | import re 281 | 282 | # ?help 283 | # ?help Cog 284 | # ?help command 285 | # -> could be a subcommand 286 | 287 | _mention = re.compile(r'<@\!?([0-9]{1,19})>') 288 | 289 | def cleanup_prefix(bot, prefix): 290 | m = _mention.match(prefix) 291 | if m: 292 | user = bot.get_user(int(m.group(1))) 293 | if user: 294 | return f'@{user.name} ' 295 | return prefix 296 | 297 | async def _can_run(cmd, ctx): 298 | try: 299 | return await cmd.can_run(ctx) 300 | except: 301 | return False 302 | 303 | def _command_signature(cmd): 304 | # this is modified from discord.py source 305 | # which I wrote myself lmao 306 | 307 | result = [cmd.qualified_name] 308 | if cmd.usage: 309 | result.append(cmd.usage) 310 | return ' '.join(result) 311 | 312 | params = cmd.clean_params 313 | if not params: 314 | return ' '.join(result) 315 | 316 | for name, param in params.items(): 317 | if param.default is not param.empty: 318 | # We don't want None or '' to trigger the [name=value] case and instead it should 319 | # do [name] since [name=None] or [name=] are not exactly useful for the user. 320 | should_print = param.default if isinstance(param.default, str) else param.default is not None 321 | if should_print: 322 | result.append(f'[{name}={param.default!r}]') 323 | else: 324 | result.append(f'[{name}]') 325 | elif param.kind == param.VAR_POSITIONAL: 326 | result.append(f'[{name}...]') 327 | else: 328 | result.append(f'<{name}>') 329 | 330 | return ' '.join(result) 331 | 332 | class HelpPaginator(Pages): 333 | def __init__(self, ctx, entries, *, per_page=4): 334 | super().__init__(ctx, entries=entries, per_page=per_page) 335 | self.reaction_emojis.append(('\N{WHITE QUESTION MARK ORNAMENT}', self.show_bot_help)) 336 | self.total = len(entries) 337 | 338 | @classmethod 339 | async def from_cog(cls, ctx, cog): 340 | cog_name = cog.__class__.__name__ 341 | 342 | # get the commands 343 | entries = sorted(ctx.bot.get_cog_commands(cog_name), key=lambda c: c.name) 344 | 345 | # remove the ones we can't run 346 | entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden] 347 | 348 | self = cls(ctx, entries) 349 | self.title = f'{cog_name} Commands' 350 | self.description = inspect.getdoc(cog) 351 | self.prefix = cleanup_prefix(ctx.bot, ctx.prefix) 352 | 353 | # no longer need the database 354 | #await ctx.release() 355 | 356 | return self 357 | 358 | @classmethod 359 | async def from_command(cls, ctx, command): 360 | try: 361 | entries = sorted(command.commands, key=lambda c: c.name) 362 | except AttributeError: 363 | entries = [] 364 | else: 365 | entries = [cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden] 366 | 367 | self = cls(ctx, entries) 368 | self.title = command.signature 369 | 370 | if command.description: 371 | self.description = f'{command.description}\n\n{command.help}' 372 | else: 373 | self.description = command.help or 'No help given.' 374 | 375 | self.prefix = cleanup_prefix(ctx.bot, ctx.prefix) 376 | #await ctx.release() 377 | return self 378 | 379 | @classmethod 380 | async def from_bot(cls, ctx): 381 | def key(c): 382 | return c.cog_name or '\u200bMisc' 383 | 384 | entries = sorted(ctx.bot.commands, key=key) 385 | nested_pages = [] 386 | per_page = 9 387 | 388 | # 0: (cog, desc, commands) (max len == 9) 389 | # 1: (cog, desc, commands) (max len == 9) 390 | # ... 391 | 392 | for cog, commands in itertools.groupby(entries, key=key): 393 | plausible = [cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden] 394 | if len(plausible) == 0: 395 | continue 396 | 397 | description = ctx.bot.get_cog(cog) 398 | if description is None: 399 | description = discord.Embed.Empty 400 | else: 401 | description = inspect.getdoc(description) or discord.Embed.Empty 402 | 403 | nested_pages.extend((cog, description, plausible[i:i + per_page]) for i in range(0, len(plausible), per_page)) 404 | 405 | self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session 406 | self.prefix = cleanup_prefix(ctx.bot, ctx.prefix) 407 | #await ctx.release() 408 | 409 | # swap the get_page implementation with one that supports our style of pagination 410 | self.get_page = self.get_bot_page 411 | self._is_bot = True 412 | 413 | # replace the actual total 414 | self.total = sum(len(o) for _, _, o in nested_pages) 415 | return self 416 | 417 | def get_bot_page(self, page): 418 | cog, description, commands = self.entries[page - 1] 419 | self.title = f'{cog} Commands' 420 | self.description = description 421 | return commands 422 | 423 | async def show_page(self, page, *, first=False): 424 | self.current_page = page 425 | entries = self.get_page(page) 426 | 427 | self.embed.clear_fields() 428 | self.embed.description = self.description 429 | self.embed.title = self.title 430 | 431 | if hasattr(self, '_is_bot'): 432 | value ='For more help, join the official bot support server: https://discord.gg/pYuKF2Z' 433 | self.embed.add_field(name='Support', value=value, inline=False) 434 | 435 | self.embed.set_footer(text=f'Use "{self.prefix}help command" for more info on a command.') 436 | 437 | signature = _command_signature 438 | 439 | for entry in entries: 440 | self.embed.add_field(name=signature(entry), value=entry.short_doc or "No help given", inline=False) 441 | 442 | if self.maximum_pages: 443 | self.embed.set_author(name=f'Page {page}/{self.maximum_pages} ({self.total} commands)') 444 | 445 | if not self.paginating: 446 | return await self.channel.send(embed=self.embed) 447 | 448 | if not first: 449 | await self.message.edit(embed=self.embed) 450 | return 451 | 452 | self.message = await self.channel.send(embed=self.embed) 453 | for (reaction, _) in self.reaction_emojis: 454 | if self.maximum_pages == 2 and reaction in ('\u23ed', '\u23ee'): 455 | # no |<< or >>| buttons if we only have two pages 456 | # we can't forbid it if someone ends up using it but remove 457 | # it from the default set 458 | continue 459 | 460 | await self.message.add_reaction(reaction) 461 | 462 | async def show_help(self): 463 | """shows this message""" 464 | 465 | self.embed.title = 'Paginator help' 466 | self.embed.description = 'Hello! Welcome to the help page.' 467 | 468 | messages = [f'{emoji} {func.__doc__}' for emoji, func in self.reaction_emojis] 469 | self.embed.clear_fields() 470 | self.embed.add_field(name='What are these reactions for?', value='\n'.join(messages), inline=False) 471 | 472 | self.embed.set_footer(text=f'We were on page {self.current_page} before this message.') 473 | await self.message.edit(embed=self.embed) 474 | 475 | async def go_back_to_current_page(): 476 | await asyncio.sleep(30.0) 477 | await self.show_current_page() 478 | 479 | self.bot.loop.create_task(go_back_to_current_page()) 480 | 481 | async def show_bot_help(self): 482 | """shows how to use the bot""" 483 | 484 | self.embed.title = 'Using the bot' 485 | self.embed.description = 'Hello! Welcome to the help page.' 486 | self.embed.clear_fields() 487 | 488 | entries = ( 489 | ('', 'This means the argument is __**required**__.'), 490 | ('[argument]', 'This means the argument is __**optional**__.'), 491 | ('[A|B]', 'This means the it can be __**either A or B**__.'), 492 | ('[argument...]', 'This means you can have multiple arguments.\n' \ 493 | 'Now that you know the basics, it should be noted that...\n' \ 494 | '__**You do not type in the brackets!**__') 495 | ) 496 | 497 | self.embed.add_field(name='How do I use this bot?', value='Reading the bot signature is pretty simple.') 498 | 499 | for name, value in entries: 500 | self.embed.add_field(name=name, value=value, inline=False) 501 | 502 | self.embed.set_footer(text=f'We were on page {self.current_page} before this message.') 503 | await self.message.edit(embed=self.embed) 504 | 505 | async def go_back_to_current_page(): 506 | await asyncio.sleep(30.0) 507 | await self.show_current_page() 508 | 509 | self.bot.loop.create_task(go_back_to_current_page()) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | --------------------------------------------------------------------------------