Compare commits

...

33 Commits
master ... dev

Author SHA1 Message Date
Shadow15510 5a2def921f Cleaning irc_api folder and change for the Pypi package 2023-07-02 15:26:31 +02:00
Shadow15510 30073ceafe Duck bot incoming… 2023-06-14 11:54:00 +02:00
Shadow15510 89077db935 Add command aliases and better help 2023-06-12 23:28:52 +02:00
Shadow15510 59fb327773 Add timed commands 2023-06-12 21:58:51 +02:00
Shadow15510 31b0f29a9e factorization of auto-detected arguments 2023-06-12 18:29:58 +02:00
Shadow15510 490f6b7e8f Update fun_cmnds 2023-06-12 18:22:58 +02:00
Shadow15510 2643e7e944 Minor changes 2023-06-12 16:56:17 +02:00
Shadow15510 772b3387f6 Deleting tests files 2023-06-12 15:53:21 +02:00
Shadow15510 9ab3568e6a Auto detection of the arguments of commands 2023-06-12 15:52:51 +02:00
Shadow15510 9c4b6916be Add accents and specials caracters into arguments parsing 2023-06-12 11:10:29 +02:00
Shadow15510 c77a5db520 Split help module by module 2023-06-12 10:28:37 +02:00
Shadow15510 8f1553883f Add a decorator on user match 2023-06-12 10:17:47 +02:00
Shadow15510 8178a602fd new feature: add modules of commands instead of list of commands 2023-06-12 09:50:59 +02:00
Shadow15510 7e33a90fb2 Adapt parsing according to the command's type 2023-06-12 00:00:53 +02:00
Shadow15510 e90c98a75f Extend parsing to single quote and update auto_help with help command by command on demand 2023-06-11 23:51:29 +02:00
Shadow15510 637574f39d Add arguments parsing (with quote detection) 2023-06-11 23:35:46 +02:00
Shadow15510 a5f20b3113 Delete classes and add decorators 2023-06-11 18:25:08 +02:00
Shadow15510 b0d6fbac85 rename 'commands' into 'api' 2023-06-11 15:26:53 +02:00
Shadow15510 fe803f3bc4 Finishing cleaning repo 2023-06-11 14:53:09 +02:00
Shadow15510 4586775c6d Update .gitignore 2023-06-11 14:52:21 +02:00
Shadow15510 637ae90e91 Cleaning repo 2023-06-11 14:47:31 +02:00
Shadow15510 de1ab039be cleaning old folder 2023-06-11 14:30:46 +02:00
Shadow15510 afa3e1859f rename folder 2023-06-11 14:30:18 +02:00
Shadow15510 abaedc5f7f Finishing auto-generated help 2023-06-11 13:59:56 +02:00
Shadow15510 e2ac2fa527 Review the project archi 2023-06-11 13:45:42 +02:00
Shadow15510 efe477612b Starting an API 2023-06-11 13:45:17 +02:00
Shadow15510 20df81a28c Fix parsing error 2023-06-09 22:02:09 +02:00
Shadow15510 5f2b57d9ad Finishing doc on v5.py 2023-06-09 20:06:35 +02:00
Shadow15510 a01a09d23c Rollback to a python secret file 2023-06-09 20:04:19 +02:00
Shadow15510 ae6f2f9d0c Update .gitignore 2023-06-09 20:03:57 +02:00
Shadow15510 537c12b2b7 fix spaces in comment 2023-06-09 14:34:21 +02:00
Shadow15510 f421e756a8 Code documentation 2023-06-09 14:07:04 +02:00
Shadow15510 3c60008db2 Update .gitignore 2023-06-09 14:06:13 +02:00
8 changed files with 359 additions and 221 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
irc_api/__pycache__/*
__pycache__/*
secrets.py

27
bot.py
View File

@ -1,27 +0,0 @@
import socket
from threading import Thread
from irc import IRC
from v5 import V5
from secrets import USER, PASSWORD
class Bot(object):
def __init__(self, irc, v5, channels):
self.irc = IRC(*irc)
self.channels = channels
self.v5 = V5(v5, self.irc)
def start(self):
# Start IRC
self.irc.start(USER, PASSWORD)
# Join channels
for c in self.channels:
self.irc.join(c)
# Start v5 handler
self.v5.start()
# Run IRC
self.irc.run()

BIN
duck_hunting.db Normal file

Binary file not shown.

251
fun_cmnds.py Normal file
View File

@ -0,0 +1,251 @@
from irc_api import commands
from irc_api.commands import auto_help
from random import randint, choice
import sqlite3
GAME_CHANNEL = "#glados"
def get_hunter(name: str):
def get():
table = sqlite3.connect("duck_hunting.db")
c = table.cursor()
data = c.execute(f"""
SELECT * FROM hunter
WHERE name="{name}"
""").fetchall()
table.close()
return data
data = get()
if not data:
add_hunter(name)
data = get()
return list(data[0][1:])
def add_hunter(name: str):
table = sqlite3.connect("duck_hunting.db")
c = table.cursor()
c.execute(f"""
INSERT INTO hunter (name)
VALUES ("{name}")
""")
table.commit()
table.close()
def del_hunter(name: str):
table = sqlite3.connect("duck_hunting.db")
c = table.cursor()
c.execute(f"""
DELETE FROM hunter
WHERE name="{name}"
""")
table.commit()
table.close()
def modify_hunter(name: str, **kwargs):
args = ""
for var, value in kwargs.items():
args += f"{var}={value}, "
table = sqlite3.connect("duck_hunting.db")
c = table.cursor()
c.execute(f"""
UPDATE hunter
SET {args[:-2]}
WHERE name="{name}"
""")
table.commit()
table.close()
@commands.every(30, desc="Fait apparaître un canard de manière aléatoire.")
def pop_duck(bot):
if randint(1, 100) <= 20:
if bot.troll_duck > 0 and randint(1, 4) <= 3:
bot.troll_duck -= 1
bot.ducks_history.append(2)
ducks = ("crr… Coin coin ! 🦆", "Couac crr… ! 🦆")
elif randint(1, 10) == 1:
ducks = ("COIN COIN ! 🦆", "COUAC ! 🦆")
bot.ducks_history.append(1)
else:
ducks = ("Coin coin ! 🦆", "Couac ! 🦆")
bot.ducks_history.append(0)
bot.send(GAME_CHANNEL, choice(ducks))
if randint(1, 100) <= 15 and len(bot.ducks_history) > 1:
ducks = ("Le canard est parti…", "Le canard s'est envolé…")
bot.send(GAME_CHANNEL, "Le canard est parti…")
bot.ducks_history.pop(0)
@commands.channel(GAME_CHANNEL)
@commands.command("pan", desc="Tirer sur un canard.")
def fire(bot, msg):
money, ammo, ammo_in_chamber, _, has_scope, grease, is_jammed = get_hunter(msg.author)
if ammo_in_chamber <= 0:
bot.send(GAME_CHANNEL, "Chambre vide ! Il faut recharger. 😉")
return
if is_jammed:
bot.send(GAME_CHANNEL, "Votre fusil est enrayé !")
return
if grease > 0:
grease -= 1
elif grease == 0 and randint(1, 100) < 33:
is_jammed = 1
if len(bot.ducks_history) > 0:
is_hit = randint(1, 100)
if has_scope:
has_scope -= 1
is_hit = randint(1, 15)
if is_hit <= 80:
duck_type = bot.ducks_history.pop(-1)
money += 2
if duck_type == 2:
hit = ("Oh un canard en plastique", "Peeww… CLONG")
bot.send(GAME_CHANNEL, "Canard mécanique ! [munition -1]\n" + choice(hit))
elif duck_type == 1 or is_hit < 10:
money += 2
hit = ("C'était un super canard !", "Ouh la belle bête !", "ÇA c'est du canard.")
bot.send(GAME_CHANNEL, "Super canard touché ! [argent +4, munition -1]\n" + choice(hit))
elif duck_type == 0:
hit = ("Touché !", "Et un canard en moins !", "You dit it bro!", "Joli tir !", "Peeww !")
bot.send(GAME_CHANNEL, "Canard touché ! [argent +2, munition -1]\n" + choice(hit))
else:
fail = ("Et le canard est toujours vivant…", "Mais… Pourquoi tu tires à côté ?", "Il est pas passé loin de la vieille celui-là !")
bot.send(GAME_CHANNEL, "Tir raté… [munition -1]\n" + choice(fail))
else:
no_ducks = ("Euh, va faire vérifier ta vue s'il-te-plaît.", "Il est où le canard ? Il est bien caché ?", "Et une grand mère en moins ! Une ! Wait…", "Un canard ne hurle pas comme ça.", "Joli tir… Mais ce n'était pas un canard…")
bot.send(GAME_CHANNEL, "Aucun canard en vue. [munition -1]\n" + choice(no_ducks))
modify_hunter(msg.author, money=money, ammo=ammo, ammo_in_chamber=ammo_in_chamber - 1, has_scope=has_scope, grease=grease, is_jammed=is_jammed)
@commands.channel(GAME_CHANNEL)
@commands.command("recharge", alias=("reload",), desc="Fait monter une balle du chargeur dans la chambre")
def reload(bot, msg):
ammo, ammo_in_chamber, max_ammo = get_hunter(msg.author)[1: 4]
ammo_to_fill = max_ammo - ammo_in_chamber
if ammo_to_fill == 0:
bot.send(GAME_CHANNEL, "Ton arme est déjà chargée.")
elif ammo > 0:
if ammo >= ammo_to_fill:
ammo_filled = ammo_to_fill
else:
ammo_filled = ammo
modify_hunter(msg.author, ammo=ammo - ammo_filled, ammo_in_chamber=ammo_in_chamber + ammo_filled)
bot.send(GAME_CHANNEL, f"{msg.author} a rechargé son arme.")
else:
bot.send(GAME_CHANNEL, f"Plus de munitions ! Va voir le magasin pour en acheter. 😉")
@commands.channel(GAME_CHANNEL)
@commands.command("stat", alias=("info",), desc="Affiche les statistiques du joueur.")
def stat(bot, msg):
money, ammo, ammo_in_chamber, max_ammo, has_scope, grease, is_jammed = get_hunter(msg.author)
additionnal_infos = ""
if has_scope:
additionnal_infos += "[lunette installée] "
if is_jammed:
additionnal_infos += "[fusil enrayé] "
text = f"Statistiques de {msg.author} {additionnal_infos[:-1]}\n"
text += f" │ argent : {money}\n"
text += f" │ chargeur : {ammo_in_chamber} / {max_ammo}\n"
text += f" │ munitions : {ammo}\n"
text += f" │ graisse : {grease}"
bot.send(GAME_CHANNEL, text)
@commands.channel(GAME_CHANNEL)
@commands.command("nettoyer", alias=("clean",), desc="Nettoie le fusil.")
def clear(bot, msg):
grease, is_jammed = get_hunter(msg.author)[5: 7]
if is_jammed:
if grease:
bot.send(GAME_CHANNEL, f"{msg.author} a nettoyé son fusil !")
grease -= 1
is_jammed = 0
else:
bot.send(GAME_CHANNEL, "Vous devez acheter de la graisse pour pouvoir nettoyer votre fusil.")
else:
bot.send(GAME_CHANNEL, "Votre fusil est déjà en bon état.")
modify_hunter(msg.author, grease=grease, is_jammed=is_jammed)
@commands.channel("Duck")
@commands.command("duck", desc="Fait apparaître un canard mécanique.")
def fake_duck(bot, msg):
bot.troll_duck += 1
bot.send(msg.author, "Un canard mécanique apparaîtra… ou pas !")
@commands.channel(GAME_CHANNEL)
@commands.command("reset", desc="Réinitialise le profil du joueur")
def reset(bot, msg):
hunter = get_hunter(msg.author)[:3]
bot.send(GAME_CHANNEL, f"Le profil de {msg.author} a été réinitialisé.")
del_hunter(msg.author)
@commands.channel(GAME_CHANNEL)
@commands.command("achat", alias=("magasin", "shop"), desc="Acheter des munitions et accessoires.\nLancer la commande sans arguments pour voir le magasin.")
def achat(bot, msg, article: str=""):
def get_spec(spec):
fields = ("argent", "munition", "munition dans la chambre", "capacité chargeur", "lunette de visée", "graisse")
text = "["
for field_index, value in spec.items():
text += f"{fields[field_index]} {('-', '+')[abs(value) == value]}{abs(value)}, "
return text[:-2] + "]"
hunter = get_hunter(msg.author)
db_fields = ("money", "ammo", "ammo_in_chamber", "max_ammo", "has_scope", "grease")
availables_articles = {
"chargeur": ("Ajoute cinq balles à ton inventaire", "un chargeur", {0: -4, 1: 5}),
"balle": ("Ajoute une balle à ton inventaire", "une balle", {0: -1, 1: 1}),
"capacité": ("Augmente la capacité du chargeur d'une balle", "un chargeur haute capacité", {0: -5, 3: 1}),
"viseur": ("Votre prochain tir touchera forcément un canard.", "une lunette de visée", {0: -3, 4: 1}),
"graisse": ("Permet de nettoyer son arme et de limiter le risque d'enrayement.", "une boîte de graisse pour fusil", {0: -3, 5: 5})
}
if article:
article = article.lower()
if article in availables_articles.keys():
args = availables_articles[article]
# not enough money
if hunter[0] < abs(args[2][0]):
bot.send(GAME_CHANNEL, f"Vous n'avez pas assez d'argent pour acheter {args[1]} {get_spec(args[2])}.")
else:
bot.send(GAME_CHANNEL, f"Vous avez acheté {args[1]} {get_spec(args[2])}.")
for index, value in args[2].items():
hunter[index] += value
modify_hunter(msg.author, **{field: hunter[i] for i, field in enumerate(db_fields)})
else:
msg = "Liste des articles disponibles :\n"
msg += "".join([
f" | {article} : {args[0]} {get_spec(args[2])}\n"
for article, args in availables_articles.items()
])
bot.send(GAME_CHANNEL, msg)

72
glados_cmnds.py Normal file
View File

@ -0,0 +1,72 @@
from random import choice
import re
import requests
import time
from irc_api import commands
# from irc_api.commands import auto_help
@commands.on(lambda m: isinstance(re.match(r"(.*)(bonjour|coucou|salut|hey|hello|hi|yo) glados(.*)", m.text, re.IGNORECASE), re.Match))
def greetings_hello(bot, msg):
"""Dit bonjour à l'utilisateur"""
greetings = ("Bonjour", "Coucou", "Salut", "Hey", "Hello", "Hi", "Yo")
bot.send(msg.to, f"{choice(greetings)} {msg.author}.")
@commands.on(lambda m: isinstance(re.match(r"(.*)(au revoir|(à|a) plus|a\+|(à|a) toute|@\+|bye|see you|see you soon) glados(.*)", m.text, re.IGNORECASE), re.Match))
def greetings_bye(bot, msg):
"""Dit au revoir à l'utilisateur."""
greetings = ("Au revoir,", "À plus,", "Bye bye,", "See you soon,", "Bye,")
bot.send(msg.to, f"{choice(greetings)} {msg.author}.")
@commands.on(lambda m: isinstance(re.match(r"(.*)(merci|merci beaucoup|thx|thanks|thank you) glados(.*)", m.text, re.IGNORECASE), re.Match))
def thanks(bot, msg):
"""Répond aux remerciements de l'utilisateur."""
thanks_choice = ("Mais je vous en prie.", "Tout le plaisir est pour moi.", "C'est tout naturel.")
bot.send(msg.to, choice(thanks_choice))
@commands.on(lambda m: isinstance(re.match(r"(.*)quelle heure(.*)?", m.text, re.IGNORECASE), re.Match))
def hour(bot, msg):
"""Donne l'heure actuelle"""
now = time.strftime("%Hh%M", time.localtime())
bot.send(msg.to, f"Il est très exactement {now}.")
@commands.on(lambda m: "mec" in m.text.lower() and "glados" in m.text.lower())
def react_on_mec(bot, msg):
bot.send(msg.to, "Chuis pas ton mec, mon pote.")
@commands.on(lambda m: "pote" in m.text.lower() and "glados" in m.text.lower())
def react_on_pote(bot, msg):
bot.send(msg.to, "Chuis pas ton pote, mon gars.")
@commands.on(lambda m: "gars" in m.text.lower() and "glados" in m.text.lower())
def react_on_gars(bot, msg):
bot.send(msg.to, "Chuis pas ton gars, mec.")
@commands.command("wiki", desc="wiki <recherche> [limite=1]\nFait une recherche wikipedia.")
def wiki(bot, msg, text: str, limit: int=1):
if not (1 < limit <= 10):
limit = 1
session = requests.Session()
params = {
'action': 'opensearch',
'search': text,
'limit': limit,
'namespace': 0,
'redirects': 'resolve',
'format': 'json'
}
response = session.get(url="https://fr.wikipedia.org/w/api.php", params=params, timeout=(4, 12)).json()
if len(response[1]) == 0:
bot.send(msg.to, f"Aucun résultat trouvé pour la recherche : {text}.")
else:
bot.send(msg.to, f"{len(response[1])} résultat{('', 's')[limit > 1]} pour la recherche : '{text}'.")
for name, link in zip(response[1], response[3]):
bot.send(msg.to, f" {name} : {link}")

132
irc.py
View File

@ -1,132 +0,0 @@
# Manage the IRC layer of GLaDOS
import logging, re, socket, ssl
from functools import wraps
from queue import Queue
from threading import Thread
class IRC(object):
def __init__(self, host, port):
""" Initialize an IRC wrapper """
# Public attributes
self.connected = False # Simple lock
# Private attributes
self._socket = ssl.create_default_context().wrap_socket(
socket.create_connection((host, port)),
server_hostname=host)
self._inbox = Queue()
self._handler = Thread(target=self._handle)
self._callbacks = []
# Public methods
def start(self, nick, password):
""" Start the IRC layer. Manage authentication as well """
self._handler.start()
self._send(f"USER {nick} * * :{nick}")
self._send(f"NICK {nick}")
self._waitfor(lambda m: "NOTICE" in m and "/AUTH" in m)
self._send(f"AUTH {nick}:{password}")
self._waitfor(lambda m: "You are now logged in" in m)
self.connected = True
def run(self):
""" Handle new messages """
while True:
message = self.receive()
logging.info(f"received {message}")
if message is not None:
for event, callback in self._callbacks:
if event(message):
logging.info(f"matched {event.__name__}")
callback(message)
def send(self, target, message):
""" Send a message to the specified target (channel or user) """
self._send(f"PRIVMSG {target} :{message}")
def receive(self):
""" Receive a private message """
while True:
message = self._recv()
if " PRIVMSG " in message:
msg = Message(message)
if msg:
return msg
def join(self, channel):
""" Join a channel """
self._send(f"JOIN {channel}")
logging.info(f"joined {channel}")
def on(self, event):
""" Adds a callback to the IRC handler
Event is a function taking in parameter a Message and returning
True if the callback should be executed on the message """
def callback(func):
@wraps(func)
def wrapper(message):
func(message)
self._callbacks.append((event, wrapper))
logging.info(f"added callback {func.__name__}")
return wrapper
return callback
# Private methods
def _handle(self):
""" Handle raw messages from irc and manage ping """
while True:
# Get incoming messages
data = self._socket.recv(4096).decode()
# Split multiple lines
for m in data.split('\r\n'):
# Manage ping
if m.startswith("PING"):
self._send(m.replace("PING", "PONG"))
# Or add a new message to inbox
elif len(m):
self._inbox.put(m)
logging.debug(f"received {m}")
def _send(self, raw):
""" Wrap and encode raw message to send """
self._socket.send(f"{raw}\r\n".encode())
def _recv(self):
m = self._inbox.get()
return m
def _waitfor(self, condition):
""" Wait for a raw message that matches the condition """
msg = self._recv()
while not condition(msg):
msg = self._recv()
return msg
class Message(object):
r = re.compile("^:(?P<author>[\w.~|]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)")
def __init__(self, raw):
match = re.search(Message.r, raw)
if match:
self.author = match.group("author")
self.to = match.group("to")
self.text = match.group("text")
logging.debug(f"sucessfully parsed {raw} into {self}")
else:
self.author = ""
self.to = ""
self.text = ""
logging.warning(f"failed to parse {raw} into valid message")
def __str__(self):
return f"{self.author} to {self.to}: {self.text}"

50
main.py
View File

@ -1,25 +1,43 @@
#!/usr/bin/env python3
"""
main (GLaDOS)
=============
import logging, re
from bot import Bot
Description
-----------
Create a bot's instance and manages it.
"""
import logging
import re
from irc_api.bot import Bot
LOG_FORMAT = "%(asctime)s [%(levelname)s] <%(filename)s> %(funcName)s: %(message)s"
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
import glados_cmnds as gv4_cmnds
import fun_cmnds as fcmnds
from secrets import USER, PASSWORD
glados = Bot(
LOG_FORMAT = "[%(levelname)s] %(message)s"
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
class MyBot(Bot):
# ducks_history
# 0 : normal duck
# 1 : super duck
# 2 : mecanics duck
ducks_history = []
# nb_ducks = 0
troll_duck = 0
glados = MyBot(
('irc.planet-casio.com', 6697),
('127.0.0.1', 5555),
["#general", "#glados"]
gv4_cmnds, fcmnds,
auth=(USER, PASSWORD),
channels=["#glados"],
prefix="!"
)
@glados.irc.on(lambda m: re.match("bonjour glados", m.text, re.IGNORECASE))
def say_hello(msg):
glados.irc.send(msg.to, f"Heureuse de vous revoir, {msg.author}")
@glados.v5.on(lambda c, m: True)
def announce(channels, message):
for c in channels:
glados.irc.send(c, message)
glados.start()
glados.start("Duck")

46
v5.py
View File

@ -1,46 +0,0 @@
import logging, socket
from threading import Thread
from functools import wraps
class V5(object):
def __init__(self, v5, irc):
""" Initialize v5 handle
:irc <IRC>: an initialized IRC object """
self.irc = irc
self._sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
self._sock.bind(v5)
self._handler = Thread(target=self._handle)
self._callbacks = []
def start(self):
# Start v5 handler
self._handler.start()
logging.info("started")
def on(self, event):
""" Adds a callback to the v5 handler
Event is a function taking in parameter a list of channels and a
string, and return True if the callback should be executed """
def callback(func):
@wraps(func)
def wrapper(channels, message):
func(channels, message)
self._callbacks.append((event, wrapper))
return wrapper
return callback
def _handle(self):
while True:
data, addr = self._sock.recvfrom(4096)
data = data.decode()
logging.debug(f"received {data}")
channels, message = data.split(":", 1)
channels = channels.split(" ")
for event, callback in self._callbacks:
if event(channels, message):
logging.info(f"passed {event.__name__}")
callback(channels, message)