Reprise de GoNib mon bot IRC en Golang

Ouai ça fait longtemps que je vous ai pas écrit. Une part de flemme mais aussi de pas trop savoir quoi écrire mais j'ai une autre excuse ! En ce moment je me suis beaucoup replongé dans Gonib mon bot IRC écrit en Golang.

Je l'ai commencé il y a deux ans avec une ptite série de poste à son propos. Et je n'y ai plus touché. Il a tourné parfaitement tout ce temps.

Il y a qu'un ptit truc qui me gonflait : les commandes sont case-sensitive et mon clavier android envoie une majuscule en début de phrase ce qui fait chier. Je voulais donc juste virer ça, histoire de plus me faire chier. Et puis au final, une fois ouvert le code : aoutch. Je ne me souvenais pas que c'était aussi long ce code source.

Et j'ai pris plaisir à lire les sources et recomprendre ce que j'avais fait. Et direct j'ai eu d'autres idées d'amélioration et voilà j'étais lancé.

debugPrint

Bon premier truc faire en sorte de pouvoir afficher différentes informations en fonction du niveau de débug. C'est tout con mais ultra pratique et plutôt que d'avoir à manuellement enlever/foutre des fmt.Println de partout pourquoi ne pas se faire une ptite commande perso pour ça ?

La commande est ultra simple, elle prend une string et un int en entrée pour juste le message à afficher ainsi que le niveau de débug nécessaire pour l'afficher. Bon au début j'ai fait le truc à l'envers puis je me suis ressaisi et maintenant plus le niveau est élevé plus j'affiche des trucs basiques.

Maintenant je suis pas sûr que le nom de la fonction soit très judicieux mais bon… c'est fait.

J'ai donc viré la quasi-totalité des fmt.Println qui ont été donc remplacé par des debugPrint.

Des variables globales de status

J'ai ajouté quelques variables globales afin de savoir si je suis connecté ou non, est-ce que je suis sur le salon, la connexion TCP éest-elle établie ? Quel est mon pseudo ? Quel est mon nom complet ? Tout ce genre de ptit truc.

Et en fonction de ce qui est reçu du serveur adapter leurs valeurs. J'en suis cependant au stade où j'ai des souvenirs de l'école où l'on m'a répété qu'il fallait éviter les variables globales, c'est pas beau, c'est dangeureux. Sauf que j'ai pas le souvenir du "pourquoi ?" et surtout "c'est pas bien, mais forcément pour telle utilisation c'est normale".

Du coup je sais pas si j'en ai une utilisation légitime ou non.

Et je me fais encore plus peur depuis que j'ai découvert que le compilo pouvait détecter comme un grand les race-conditions en ajoutant simplement -race dans la ligne d'éxécution. Et là … bha … j'en ai.

En gros il détecte que je lis et écris une même variable depuis plusieurs fonctions/routines différentes. Et bha oui et c'est chouette, non ? Comment faire autrement ? J'ai pas trop la solution actuellement.

Visiblement il faut passer par des channels et du coup les passer à toutes les fonctions qui en ont besoin. Et surtout j'ai l'impression que c'est bien pour envoyer les infos dans un sens mais dans l'autre sens… j'ai l'impression que c'est moins simple. C'est le gros point que j'aimerai améliorer prochainement mais j'ai pas encore trouvé la doc bien à ce sujet.

Parser

Bon la syntaxe du protocole IRC est simple. Donc faire un parser ça va être du gâteau, non ?

Et bhé en fait mouai. Je m'y suis repris à trois fois afin de faire un parser à peu près solide. Et tant qu'à faire j'ai séparé la partie parser de la partie action à réaliser.

Après avoir un peu mieux observé un peu tout ce qui provient du serveur je suis arrivé à ça :

[@tag] :source TYPE [destination] [option] [:contenu]

[@tag]

En gros le tag est la grande nouveauté d'IRCv3 et c'est donc parfaitement optionnel. Ça peut contenir des infos assez variées que je détaille plus loin. Ça commence par un "@" et ça contient en vérité plusieurs tags. Exemple : @msgid=UXY5o0fonahKTQI3EiEL3I;time=2021-07-15T14:35:45.173Z

:source

La source indique qui est à l'origine d'un message. Elle y est toujours et commence par ":" . Ça peut être le nom du serveur s'il s'agit d'un message serveur ou bien être un utilisateur. Exemple : Lord!Lord@geeknode.fuckyeah

TYPE

Le type de message reçu. Il y a plein de types de message, les PRIVMSG (les messages classiques), les NOTICE, les KICK, les TOPIC, bref il y a de quoi faire. D'ailleurs à ce propos, sur IRC il n'y a pas de différence entre un message pour un salon et un message personnel, juste la destination va changer. Dans les TYPE on retrouve également tous les numerics, qui sont des informations renvoyées par le serveur. On trouve une liste pas mal du tout sur cette page. Exemple : PRIVMSG

[destination]

Il s'agit bha … ouai du destinataire d'un message. C'est donc souvent un nom de salon, le nom de quelqu'un ou parfois directement le nom du serveur. Ce champ est presque tout le temps rempli mais il existe de rares messages (biensûr quand j'écris je ne me souviens plus desquels) qui n'en ont pas. Il faut donc prévoir le cas où c'est vide. Exemple : #testage

[option]

J'ai appelé ce champ option car bha au début je pensais que c'était le seul optionnel… En gros ce champ complémente parfois le champ TYPE. C'est notamment le cas pour les CAP qui ont des sous-commandes qui sont alors présentes dans ce champs. Exemple : LS

[:contenu]

Le dernier champ est le contenu. Il commence par un ":" et contrairement à ce que je pensais n'est même pas obligatoire (source d'emmerde). Certains NUMERICS n'ont pas de contenu (ce qui est illogique ma foi). C'est la partie qui contient les messages à proprement écrit. Pour les messages de TYPE "fonctionnels" ça contient souvent la raison, ou du facultatif. Exemple : :asv ?

Et du coup ?

Bha je me suis fait une ptite fonction à qui j'envoie une string, un séparateur et qui me renvoie deux string : la partie de la string avant séparateur et la partie de la string après le séparateur. Si le séparateur n'est pas trouvé, ça me renvoie dans la première string toute la string d'entrée. Étrangement je n'ai pas trouvé de fonction dans la libs strings qui fasse ça.

Il ne me reste plus qu'à appeler cette fonction plusieurs fois afin d'extraire chaque morceau de ma ligne. Au final le parser ne fait plus que dix lignes et semble s'accomoder de tout ce que lui envoie le serveur.

Le parser créer ensuite une struct ParsedMessage qui contient toutes les strings des différents morceaux. Cette struct est envoyée à la fonction processMessage qui s'occupera de traiter tout ça :-)

J'en suis plutôt content parcequ'au bout de quatre versions j'ai le sentiment d'être parvenu au résultat nickel.

Je ne résiste pas au plaisir de vous montrer cette partie
if msg[:1] == "@" { msg , message_tag = extractUntil(msg," ") }
if msg[:1] == ":" {
    msg , message_source = extractUntil(msg," ")
    message_source = message_source[1:]
}
msg , message_type = extractUntil(msg," ")
if msg[:1] != ":" { msg , message_destination = extractUntil(msg," ") }
if msg[:1] != ":" { msg , message_option = extractUntil(msg," :") }
if msg[:1] == ":" { message_content = strings.TrimSuffix(msg[1:]," ") }

return ParsedMessage{msg_tag: message_tag, msg_source: message_source, msg_type: message_type, msg_destination: message_destination, msg_content: message_content, msg_option: message_option}

Go fmt

Ouai je l'avais pas fait avant. Mais maintenant c'est bon, j'essaye d'y penser régulièrement.

Cette commande permet de s'occuper du formattage des fichiers sources. Ça met les bons espace, ça indente tout comme il faut… bref c'est cool.

Je me retrouvais avec des fichiers avec des tabs et des espaces vu que les éditeurs de texte s'évertuent à ajouter l'un ou l'autre par défaut ou à ne pas afficher l'indentation présente mais la leur. Bref, en faisant un cat sur le fichier c'était le gros bordel.

Hop maintenant c'est propre et cohérent.

Module Détection de SHUN

Sur IRC, les ircop (ceux qui gèrent le serveur) ont une commande géniale qui permet de faire en sorte d'ignorer tout ce qu'envoie un client sauf les PING/PONG. Ça a pour effet d'isoler un relou qui emmerde tout le monde. Il ne se rend pas compte que ce qu'il envoie ne parvient pas aux autres personnes.

J'ai donc ajouté un ptit moyen de détecter ça sur mon bot au cas où… Bon en vrai c'était juste pour voir la faisabilité. Moralité : c'est ultra simple.

Il suffit de s'envoyer régulièrement un message à soi-même et de vérifier qu'on le reçoit. Voilà j'ai mis ça en place à côté de la détection de ping timeout.

Module quizz

L'été est là et ce sont des vacances pour pas mal de monde. Qui n'aime pas un ptit quizz entre amis ?

À une époque IRC débordait de ces bots de jeux et je suis triste de ne plus en avoir. Donc j'ai fait le mien \o/

Le principe est super simple : Un fichier CSV avec sur chaque ligne la réponse, la question, un indice, un second indice.

Lorsque quelqu'un demande un quizz, on choppe une ligne aléatoire du fichier et on pose la question, on démarre un timer. Tous les messages qui arrivent pendant le timer sont testées pour voir si ça correspond à la réponse. Au bout d'un premier timer on balance un indice, puis au second timer, un deuxième indice puis au troisième c'est perdu. Bien entendu une bonne réponse met fin à tout.

En cas de bonne réponse, on lit le fichier de score, on cherche si une ligne commence par le pseudo si c'est le cas incrémente l'int des points, si c'est pas le cas on rajoute une ligne. Voilà c'est tout bête.

J'ai pas envie de me faire chier avec l'affichage des points pour le moment et encore moins pour le tri. Pas envie de faire d'algo, si vraiment je m'y attèle ça sera hors du bot via une filouterie à base de commandes shell :-°

Module TODO

Bon, mon module TODO avait un ptit bug foireux. Il m'ajoutait parfois des lignes vides (et donc des todo vides).

J'ai donc jeté et refait de 0 la gestion de fichier. Et c'est bon ça marche.

En gros je lis la todo en entier et je la fous en mémoire. Je fais les modifs dans la mémoire et je réécris intégralement le fichier.

C'est pas optimisé mais franchement vu la taille de la todo, ça ira largement. Ce bug traînait depuis deux ans et je ne comprenais pas comment il pouvait se produire et en bossant sur le module de quizz j'ai eu le même truc et j'ai compris ma connerie. Comme quoi si vous avez un souci sur bug, faites deux ans de pause et pouf vous aurez l'illumination directe.

Support IRCv3

Quitte à faire un bot IRC en 2021, autant qu'il soit compatible avec une partie des specs de IRCv3. Ces specs ajoutent pas mal de trucs plus ou moins utiles.

Un gros morceau sont donc les message-tags.

Faut bien voir qu'IRCv3 reste pleinement compatible avec le protocole d'antan. Il faut donc que les deux versions du protocole puissent coexister. Ce qui a été décidé est assez simple c'est lors de la connexion, un client en IRCv3 peut négocier des CAPS qui vont activer ou non certaines extensions du protocole.

Comme ça un client non compatible reste comme d'hab alors qu'un nouveau aura droit à quelques variations dans le protocole.

message-tags

Une grande partie de ces extensions utilisent donc les tags qui sont un morceau de texte que l'on retrouve en début de ligne avec donc différentes infos supplémentaires. Ça peut être juste l'horodatage des messages (ouai initialement, les messages ne le sont pas, c'est au client de le faire de son côté ce qui fait que selon l'horloge des clients un même message peut être affiché comme étant arrivé à une heure complètement différente). On a également des id permettant d'avoir un identifiant unique sur un message (pratique si on le combine à d'autres extensions comme par exemple des réactions). Il est possible d'y ajouter des tags custom par les clients ce qui peut amener à plein de ptites joyeusetés complémentaires (certains clients envisagent d'envoyer une url vers une image pour servir d'avatar). Dans les tags on retrouve également de la notification de personne en train d'écrire. Bref tout un tas de trucs divers et variés.

Dans mon cas pour l'instant j'ai juste fait en sorte que le parser puisse recevoir ces tags sans planter ce qui est déjà pas mal à mon niveau.*

Négociation des CAPS

Pour activer ces nouveautés, il faut les demander au serveur lors de la phase de connexion. J'en demande un paquet et je stock le résultat dans une map de booléen. Ça me permet d'avoir un tableau avec tout ça.

J'ai découvert la joie des mutex afin de pouvoir remplir ce tableau en écriture et le consulter en lecture sans causer de souci. Bon j'ai rien fait de bien poussé, juste suffisamment pour que ça m'explose plus à la gueule.

Au début ça fonctionnait sans rien faire avec le niveau de débug max, mais lorsque je l'ai baissé, le programme tournant un chouilla plus vite, j'ai eu de la lecture en même temps que de l'écriture ce qui m'a planté le truc.

Par chance le message d'erreur est plutôt explicite avec le numéro de ligne et tout qui va bien. Un coup de moteur de recherche plus tard et hop j'avais une solution à appliquer.

extended-join

Une CAP en particulier est l'extended-join qui permet d'avoir quelques infos en plus quand quelqu'un rejoint un salon. Le souci c'est que du coup la syntaxe de la commande JOIN est quelque peu chamboulée (trois fois rien, hein) du coup j'ai rajouté une condition en fonction du tableau précédent afin de savoir si la CAP est activée ou non et en fonction de ça processer différemment le JOIN.

C'est mon premier cas où il a fallu que j'adapte mon process en fonction des CAPS. Je suis content que ça fonctionne comme sur des roulettes. Je suis cependant juste un peu déçu quant à la syntaxe : je n'ai pas réussi à récupérer la valeur pour l'utiliser directement dans le if, j'ai été obligé de récupérer la valeur dans une variable qui est ensuite utilisé dans le if…

Un peu dommage.


Bon bha voilà, j'ai encore pas mal de trucs à faire dessus. Le bot culmine à 750 lignes de codes (bon avec des commentaires et des lignes vides, hein).

J'aimerai vraiment m'atteler au souci de race-conditions et mieux comprendre la bonne façon de gérer des variables globales (qui ne devraient probablement pas être globales). Voilà voilà.

gonib.go
  1package main
  2
  3import (
  4	"bufio"
  5	"flag"
  6	"fmt"
  7	"io"
  8	"io/ioutil"
  9	"math/rand"
 10	"net"
 11	"os"
 12	"strconv"
 13	"strings"
 14	"sync"
 15	"time"
 16)
 17
 18var bleu string = "\033[1;34m"
 19var rouge string = "\033[1;31m"
 20var vert string = "\033[1;32m"
 21var jaune string = "\033[1;33m"
 22var violet string = "\033[1;35m"
 23var cyan string = "\033[1;36m"
 24var normal string = "\033[0m"
 25var dim string = "\033[2m"
 26var me string
 27var server string
 28var port string
 29var nick string
 30var wanted_nick string
 31var channel string
 32var lport string
 33var debug int = 0
 34var todofile string = "/home/nib/todo"
 35var datelayout string = "02/01/06"
 36var tdatelayout string = "02/01/06 - 15:03:05"
 37var onchan bool = false
 38var connected bool = false
 39var cap_messagetags int = 0 //sert comme booléen mais permet de shifter le parsing
 40var quizz_on bool = false
 41var quizz_reponse chan ParsedMessage
 42var enabled_caps = make(map[string]bool)
 43var enabled_caps_mutex = sync.RWMutex{}
 44
 45var sender chan Message
 46var rawsender chan string
 47var AliveLoop chan bool
 48var pong chan bool
 49
 50type Ircconnection struct {
 51	Server   string
 52	Port     string
 53	Timeouts int
 54	Conn     net.Conn
 55	Receiver chan string
 56	//	Pong              chan bool
 57	counter           int
 58	Struct            chan Message
 59	StopHandleCounter chan bool
 60	StopInteract      chan bool
 61}
 62
 63type ParsedMessage struct {
 64	msg_tag         string
 65	msg_source      string
 66	msg_type        string
 67	msg_destination string
 68	msg_content     string
 69	msg_option      string
 70}
 71type Message struct {
 72	msg   string
 73	dest  string
 74	level int // le Level correspond à l'importance du message. 10 ce sont les messages normaux. En dessous ça sera plus du debug (par exemple les Pings), en fonction de la couleur on peut décider de ne pas afficher, ou d'afficher avec une certaine couleur
 75}
 76
 77func (connection *Ircconnection) Connect() {
 78	var TcpConnected bool = false
 79	for !TcpConnected {
 80		var err error
 81		connection.Conn, err = net.Dial("tcp", server+":"+port)
 82		fmt.Println(rouge + "Connection to " + server + ":" + port + normal)
 83		if err != nil {
 84			fmt.Println(err)
 85			time.Sleep(time.Duration(connection.Timeouts) * time.Second)
 86		} else {
 87			TcpConnected = true
 88		}
 89	}
 90	connection.Receiver = make(chan string)
 91	connection.Struct = make(chan Message)
 92	connection.StopHandleCounter = make(chan bool)
 93	connection.StopInteract = make(chan bool)
 94	connection.Timeouts = 0
 95	counter_updater := make(chan bool)
 96	go io.Copy(connection.Conn, os.Stdin)
 97	go connection.handleIncoming()
 98	go connection.handleCounter(counter_updater)
 99	go connection.Interact(counter_updater)
100	rawsender <- "CAP LS 302"
101	rawsender <- "NICK " + wanted_nick
102	rawsender <- "USER " + wanted_nick + " 0.0.0.0 " + wanted_nick + " :" + wanted_nick + " bot"
103	rawsender <- "CAP REQ :message-tags"
104	rawsender <- "CAP REQ :batch"
105	rawsender <- "CAP REQ :extended-join"
106	rawsender <- "CAP REQ :chghost"
107	rawsender <- "CAP REQ :cap-notify"
108	rawsender <- "CAP REQ :userhost-in-names"
109	rawsender <- "CAP REQ :multi-prefix"
110	rawsender <- "CAP REQ :away-notify"
111	rawsender <- "CAP REQ :account-notify"
112	rawsender <- "CAP REQ :server-time"
113	rawsender <- "CAP REQ :echo-message"
114	rawsender <- "CAP REQ :labeled-response"
115	rawsender <- "CAP END"
116
117	for !connected {
118		time.Sleep(200 * time.Millisecond)
119	}
120	rawsender <- "JOIN :" + channel
121	go connection.StatusCheck()
122}
123func (connection *Ircconnection) Disconnect() {
124	onchan = false
125	connected = false
126	cap_messagetags = 0
127	defer debugPrint("Fermeture Disconnect()", 0)
128	fmt.Println(vert + "On coupe" + rouge + " la connexion !" + normal)
129	connection.StopHandleCounter <- true
130	connection.StopInteract <- true
131	connection.Conn.Close()
132	go connection.Connect()
133}
134func NewIrcconnection() Ircconnection {
135	return Ircconnection{Server: server, Port: port, Timeouts: 5}
136}
137func (connection *Ircconnection) handleIncoming() {
138	defer debugPrint("Fermeture connection.handleIncoming()", 0)
139	scanner := bufio.NewScanner(connection.Conn)
140	for scanner.Scan() {
141		ln := scanner.Text()
142		connection.Receiver <- ln
143	}
144}
145
146func (connection *Ircconnection) Interact(counter_updater chan bool) {
147	defer debugPrint("Fermeture connection.Interact()", 0)
148	for {
149		select {
150		case writer := <-sender:
151			if connection.counter < 5000 {
152				counter_updater <- true
153			}
154			fmt.Println("\t"+jaune+">>> PRIVMSG "+normal+writer.dest+" :"+writer.msg+" //"+jaune+"[", connection.counter, "]"+normal)
155			io.WriteString(connection.Conn, "PRIVMSG "+writer.dest+" :"+writer.msg+"\n")
156			time.Sleep(time.Duration(connection.counter) * time.Millisecond)
157		case rawwriter := <-rawsender:
158			if connection.counter < 5000 {
159				//counter_updater <- true
160			}
161			if debug > 0 {
162				fmt.Println("\t"+rouge+">>> "+normal+rawwriter+" //"+jaune+"[", connection.counter, "]"+normal)
163			}
164			io.WriteString(connection.Conn, rawwriter+"\n")
165			time.Sleep(time.Duration(connection.counter) * time.Millisecond)
166		case reader := <-connection.Receiver:
167			parsedMessage := parseIrc(connection, reader)
168			go processMessage(parsedMessage)
169		case <-connection.StopInteract:
170			return
171
172		}
173	}
174}
175
176func (connection *Ircconnection) StatusCheck() {
177	defer connection.Disconnect()
178	defer debugPrint("Fermeture connection.StatusCheck()", 0)
179	var ShunTimeouts int = 0
180	for {
181		time.Sleep(60 * time.Second)
182		// Vérif de la disponibilité du pseudo
183		if nick != wanted_nick {
184			debugPrint("Pseudo "+nick+" différent de "+wanted_nick, 2)
185			rawsender <- "NICK " + wanted_nick
186		}
187		// Vérif de la connexion au salon désiré
188		if !onchan {
189			rawsender <- "JOIN " + channel
190		}
191		// Vérif du PING du serveur
192		debugPrint("\t"+jaune+"CHECK: "+bleu+"PING "+normal+me, 2)
193		rawsender <- "PING " + me
194		select {
195		case <-pong:
196			connection.Timeouts = 0
197			//time.Sleep(10 * time.Second)
198		case <-time.After(2 * time.Second):
199			connection.Timeouts++
200			fmt.Println(rouge+"Timeout ", connection.Timeouts)
201			if connection.Timeouts > 5 {
202				fmt.Println(rouge + "Ping Timeout du serveur !" + normal)
203				return
204				connection.Disconnect()
205			}
206		}
207		// Vérif shun
208		debugPrint("\t"+jaune+"CHECK: "+bleu+"PRIVMSG "+normal+nick+":AUTOCHECK", 2)
209		sender <- Message{msg: "AUTOCHECK", dest: nick, level: 12}
210		select {
211		case <-AliveLoop:
212			ShunTimeouts = 0
213		case <-time.After(2 * time.Second):
214			ShunTimeouts++
215			fmt.Println(rouge+"ShunTimeout ", ShunTimeouts)
216			if ShunTimeouts > 2 {
217				fmt.Println(rouge + "Shun Timeout du serveur !" + normal)
218				connection.Disconnect()
219				return
220			}
221		}
222	}
223}
224
225// Futur système anti-flood. tout pourri pour le moment.
226func (connection *Ircconnection) handleCounter(counter_updater <-chan bool) {
227	defer debugPrint("Fermeture de connection.handleCounter()", 0)
228	for {
229		select {
230		case <-connection.StopHandleCounter:
231			return
232		case <-counter_updater:
233			connection.counter = connection.counter + 100
234		default:
235			time.Sleep(time.Duration(connection.counter+500) * time.Millisecond)
236			if connection.counter > 0 {
237				connection.counter -= 50
238			} else {
239				connection.counter = 0
240			}
241		}
242	}
243}
244
245/////////////////// MAIN //////////////////////////////////////
246func main() {
247	sender = make(chan Message, 3)
248	rawsender = make(chan string)
249	AliveLoop = make(chan bool)
250	pong = make(chan bool)
251	quizz_reponse = make(chan ParsedMessage)
252
253	flag.StringVar(&server, "server", "localhost", "Server hostname to connect to")
254	flag.StringVar(&port, "port", "6667", "Which port to connect to")
255	flag.StringVar(&wanted_nick, "nick", "bab", "Which nickname you want to use")
256	flag.StringVar(&channel, "channel", "#lms", "Which channel to join")
257	flag.StringVar(&lport, "lport", "4321", "Which port to listen incoming connections")
258	flag.IntVar(&debug, "debug", 0, "Enable debug messages")
259	flag.Parse()
260
261	connection := NewIrcconnection()
262	connection.Connect()
263
264	in, err := net.Listen("tcp", ":"+lport)
265	check(err)
266	defer in.Close()
267
268	for {
269		inconn, err := in.Accept()
270		if err != nil {
271			fmt.Println(err)
272			continue
273		}
274		go handleIncoming(inconn, connection)
275	}
276
277}
278
279// ------------------
280// Côté IRC
281// ------------------
282
283func parseIrc(connection *Ircconnection, msg string) ParsedMessage {
284	debugPrint("\t\t"+cyan+"<< "+normal+msg, 3)
285
286	var message_tag string = ""
287	var message_source string = ""
288	var message_type string = ""
289	var message_destination string = ""
290	var message_option string = ""
291	var message_content string = ""
292	// PARSER 3
293	if msg[:1] == "@" {
294		msg, message_tag = extractUntil(msg, " ")
295	}
296	if msg[:1] == ":" {
297		msg, message_source = extractUntil(msg, " ")
298		message_source = message_source[1:]
299	}
300	msg, message_type = extractUntil(msg, " ")
301	if msg[:1] != ":" {
302		msg, message_destination = extractUntil(msg, " ")
303	}
304	if msg[:1] != ":" {
305		msg, message_option = extractUntil(msg, " :")
306	}
307	if msg[:1] == ":" {
308		message_content = strings.TrimSuffix(msg[1:], " ")
309	}
310
311	//fmt.Println("tag ["+bleu+message_tag+normal+"] src ["+bleu+ message_source+normal+"] type ["+bleu+ message_type+normal+"] dst ["+bleu+ message_destination+normal+"] option ["+bleu+ message_option+normal+"] content ["+bleu+ message_content+normal+"]")
312	return ParsedMessage{msg_tag: message_tag, msg_source: message_source, msg_type: message_type, msg_destination: message_destination, msg_content: message_content, msg_option: message_option}
313
314}
315
316func processMessage(parsedMessage ParsedMessage) {
317	debugPrint(violet+"Message_tag ["+normal+parsedMessage.msg_tag+violet+"] source ["+normal+parsedMessage.msg_source+violet+"] type ["+normal+parsedMessage.msg_type+violet+"] destination ["+normal+parsedMessage.msg_destination+violet+"] option ["+normal+parsedMessage.msg_option+violet+"] content ["+normal+parsedMessage.msg_content+violet+"]"+normal, 5)
318
319	if parsedMessage.msg_type == "PING" {
320		rawsender <- "PONG :" + parsedMessage.msg_content
321		return
322	}
323	switch parsedMessage.msg_type {
324	case "CAP":
325		if parsedMessage.msg_option == "ACK" {
326			enabled_caps_mutex.Lock()
327			enabled_caps[parsedMessage.msg_content] = true
328			enabled_caps_mutex.Unlock()
329		}
330		return
331
332	case "001":
333		connected = true
334		nick = parsedMessage.msg_destination
335		me = parsedMessage.msg_content[strings.LastIndex(parsedMessage.msg_content, " ")+1:]
336		return
337	case "NOTICE":
338		// Auto Invite on Knock
339		if parsedMessage.msg_content == ":[Knock]" {
340			//io.WriteString(connection.Conn, "invite "+elements[5][0:strings.IndexAny(elements[5], "!")]+" "+channel+"\n")
341		}
342		return
343	case "JOIN":
344		// @msgid=sBqscFhVMaV9uQMOxJsAcb-uxRnr6kSWCSBVDMvd22ufA;time=2021-07-13T23:07:17.218Z :gonib!gonib@2a01:cb1d:8c37:7f00:7285:c2ff:fe62:b714 JOIN #testage * :gonib bot
345		// @msgid=B7Bk1vWoztQeTBWRQZeyAU-uxRnr6kSWCSBVDMvd22ufA;time=2021-07-13T23:09:06.420Z :gonib!gonib@2a01:cb1d:8c37:7f00:7285:c2ff:fe62:b714 JOIN :#testage
346		var new_chan string
347		enabled_caps_mutex.RLock()
348		extended_join := enabled_caps["extended-join"]
349		enabled_caps_mutex.RUnlock()
350		if extended_join {
351			new_chan = parsedMessage.msg_destination
352		} else {
353			new_chan = parsedMessage.msg_content
354		}
355		if Nick(parsedMessage.msg_source) == nick {
356			debugPrint("On join "+new_chan, 0)
357			sender <- Message{msg: "Bonjour " + new_chan + " :-)", dest: new_chan, level: 10}
358		} else {
359			sender <- Message{msg: "Bienvenue " + Nick(parsedMessage.msg_source), dest: new_chan, level: 10}
360		}
361		return
362	case "PONG":
363		pong <- true
364		//FIXME !! connection.Pong <- true
365	case "PART":
366		if Nick(parsedMessage.msg_source) == nick && parsedMessage.msg_content == channel {
367			onchan = false
368			debugPrint(rouge+"On vient de quitter "+channel+normal, 0)
369		}
370		return
371	case "MODE":
372		return
373	case "TOPIC":
374		return
375	case "NICK":
376		if parsedMessage.msg_source == nick {
377			nick = parsedMessage.msg_content
378		}
379		return
380	case "KICK":
381		if parsedMessage.msg_content == nick {
382			debugPrint(rouge+"On s'est fait kicker de "+parsedMessage.msg_destination+" par "+parsedMessage.msg_source+normal, 0)
383			onchan = false
384		}
385		return
386	case "QUIT":
387		if Nick(parsedMessage.msg_source) == nick {
388			debugPrint(rouge+"On vient de se déconnecter"+normal, 0)
389			onchan = false
390			connected = false
391		}
392		return
393	case "ERROR":
394		debugPrint(rouge+parsedMessage.msg_content+normal, 0)
395		connected = false
396		onchan = false
397		return
398	case "319":
399		if !strings.Contains(parsedMessage.msg_content, channel) {
400			onchan = false
401			debugPrint(rouge+"On n'est pas connecté au salon"+normal, 0)
402		} else {
403			onchan = true
404		}
405		return
406	case "433":
407		if !connected { //pseudo déjà utilisé au moment de la connexion
408			debugPrint(rouge+"PSEUDO OCCUPÉ LORS DE LA CONNEXION"+normal+" on passe à "+wanted_nick+"_", 0)
409			rawsender <- "NICK " + wanted_nick + "_"
410			//io.WriteString(connection.Conn, "NICK "+wanted_nick+"_ \n")
411		} else {
412			fmt.Println(vert + "le pseudo " + parsedMessage.msg_content + " est déjà utilisé, on reste sur " + nick + normal)
413		}
414		return
415	case "PRIVMSG":
416		if parsedMessage.msg_source == me && parsedMessage.msg_destination == nick && parsedMessage.msg_content == "AUTOCHECK" {
417			AliveLoop <- true
418			return
419		}
420		fmt.Println("\t\t" + bleu + "<< " + normal + parsedMessage.msg_destination + " " + Nick(parsedMessage.msg_source) + " |" + parsedMessage.msg_content)
421
422		if quizz_on {
423			quizz_reponse <- ParsedMessage{msg_source: parsedMessage.msg_source, msg_content: parsedMessage.msg_content, msg_type: parsedMessage.msg_type, msg_tag: ""}
424		}
425
426		if parsedMessage.msg_content == "quizz" {
427			if quizz_on {
428				return
429			}
430			quizz_on = true
431			go quizz()
432		}
433		if len(parsedMessage.msg_content) > 1 {
434			var elements []string = strings.Fields(parsedMessage.msg_content)
435			switch elements[0] {
436			case "dig":
437				if len(elements) > 1 {
438					ip, err := net.LookupIP(elements[1])
439					if err != nil {
440						sender <- Message{msg: "Erreur de résolution DNS.", dest: parsedMessage.msg_destination, level: 11}
441						return
442					}
443					for i := 0; i < len(ip); i++ {
444						sender <- Message{msg: ip[i].String(), dest: parsedMessage.msg_destination, level: 10}
445					}
446				} else {
447					sender <- Message{msg: "Il faut un argument supplémentaire", dest: parsedMessage.msg_destination, level: 10}
448				}
449				return
450			case "up":
451				if len(elements) > 2 {
452					_, err := net.Dial("tcp", elements[1]+":"+elements[2])
453					if err != nil {
454						sender <- Message{msg: elements[1] + ":" + elements[2] + " est non joignable", dest: parsedMessage.msg_destination, level: 10}
455					} else {
456						sender <- Message{msg: elements[1] + ":" + elements[2] + " : OK", dest: parsedMessage.msg_destination, level: 10}
457					}
458				} else {
459					sender <- Message{msg: "Syntaxe : up nom port", dest: parsedMessage.msg_destination, level: 10}
460				}
461				return
462			}
463		}
464		return
465	default:
466		//debugPrint(rouge+"PAS COMPRIS : "+parsedMessage.msg_content+normal)
467		return
468	}
469}
470
471func aReimplementer(connection *Ircconnection, msg string, elements []string) {
472	if len(elements) > 3 {
473		switch elements[3] {
474		case ":heure":
475			sender <- Message{msg: "Paies-toi une montre vaut rien!", dest: channel, level: 5}
476		case ":todo":
477			if len(elements) <= 4 {
478				todoList()
479			} else {
480				switch elements[4] {
481				case "add":
482					item := ""
483					for i := 5; i < len(elements); i++ {
484						item = item + elements[i] + " "
485					}
486					todoAdd(item)
487					sender <- Message{msg: "Ajout à la todo", dest: channel, level: 10}
488				case "del":
489					item, err := strconv.ParseInt(elements[5], 10, 32)
490					if err != nil {
491						sender <- Message{msg: err.Error(), dest: channel, level: 11}
492					}
493					todoDel(int(item) - 1)
494					sender <- Message{msg: "Suppression de l'item", dest: channel, level: 10}
495				}
496			}
497		case ":plot", ":Plot":
498			if len(elements) == 8 {
499				plotArgs := elements[4] + " " + elements[5] + " " + elements[6] + " " + elements[7]
500				plotAdd(plotArgs)
501			} else {
502				sender <- Message{msg: "conso gaz elec eau", dest: channel, level: 10}
503				sender <- Message{msg: "poids peupeu bab lord", dest: channel, level: 10}
504			}
505		}
506	}
507}
508
509// -----------------
510// Commandes du bot
511// -----------------
512
513func todoList() {
514	file, err := os.Open(todofile)
515	if err != nil {
516		file, e := os.Create(todofile)
517		if e != nil {
518			sender <- Message{msg: e.Error(), dest: channel, level: 11}
519		}
520		defer file.Close()
521	}
522	defer file.Close()
523	scanner := bufio.NewScanner(file)
524	var index int = 0
525	for scanner.Scan() {
526		index++
527		sender <- Message{msg: strconv.Itoa(index) + ": " + scanner.Text(), dest: channel, level: 10}
528	}
529
530}
531
532func todoAdd(item string) {
533	file, err := os.OpenFile(todofile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
534	if err != nil {
535		sender <- Message{msg: err.Error(), dest: channel, level: 12}
536	}
537	defer file.Close()
538	writer := bufio.NewWriter(file)
539	defer writer.Flush()
540	sender <- Message{msg: "Ajout : " + item, dest: channel, level: 10}
541	now := time.Now().Format(datelayout)
542	fmt.Fprint(file, now, " ", item, "\n")
543}
544
545func todoDel(item int) {
546	input, err := ioutil.ReadFile(todofile)
547	if err != nil {
548		sender <- Message{msg: err.Error(), dest: channel, level: 11}
549	}
550	lines := strings.Split(string(input), "\n")
551	linesout := make([]string, 0)
552	for i, todo := range lines {
553		if i != item && string(todo) != "" {
554			linesout = append(linesout, todo)
555		}
556	}
557	output := strings.Join(linesout, "\n")
558	err = ioutil.WriteFile(todofile, []byte(output), 0644)
559	if err != nil {
560		sender <- Message{msg: err.Error(), dest: channel, level: 11}
561	}
562}
563
564func plotAdd(incoming string) {
565	var args []string = strings.Fields(incoming)
566	if args[0] != "conso" && args[0] != "poids" {
567		sender <- Message{msg: "Seul 'poids' ou 'conso' sont acceptés", dest: channel, level: 12}
568		return
569	}
570	var filePath string = "/var/www/lord.re/graph/" + args[0] + ".csv"
571	fmt.Println("le fichier est " + filePath)
572	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
573	if err != nil {
574		sender <- Message{msg: err.Error(), dest: channel, level: 12}
575	}
576	defer file.Close()
577	writer := bufio.NewWriter(file)
578	defer writer.Flush()
579	now := time.Now().Format(datelayout)
580	fmt.Fprint(file, now, ",", args[1], ",", args[2], ",", args[3], "\n")
581	sender <- Message{msg: "C'est plotté ! https://lord.re/graph", dest: channel, level: 10}
582}
583
584func quizz() {
585	defer func() {
586		sender <- Message{msg: "C'est fini pour cette passionnante partie de quizz. À une prochaine ;-)", dest: channel, level: 10}
587		quizz_on = false
588	}()
589	sender <- Message{msg: "C'est parti !", dest: channel, level: 10}
590	var max_indice int = 3
591	var nbr_indice int = 0
592
593	bonne_reponse, question, indice1, indice2 := randomQuizz()
594	sender <- Message{msg: question, dest: channel, level: 10}
595	for {
596		select {
597		case reponse := <-quizz_reponse:
598			debugPrint("Quizz réponse : "+reponse.msg_content+" | emetteur :"+reponse.msg_source, 5)
599			if reponse.msg_content == bonne_reponse {
600				sender <- Message{msg: "C'est gagné " + Nick(reponse.msg_source), dest: channel, level: 10}
601				quizzUpScore(Nick(reponse.msg_source))
602				return
603			}
604		case <-time.After(30 * time.Second):
605			nbr_indice++
606			debugPrint("Nbr_indice : "+strconv.Itoa(nbr_indice), 5)
607			if nbr_indice == 1 {
608				sender <- Message{msg: "Indice 1 : " + indice1, dest: channel, level: 10}
609			}
610			if nbr_indice == 2 {
611				sender <- Message{msg: "Indice 2 : " + indice2, dest: channel, level: 10}
612			}
613			if nbr_indice > max_indice {
614				debugPrint("C'est perdu", 4)
615				sender <- Message{msg: "Et c'est perdu :-( Nous ne saurons jamais la réponse !", dest: channel, level: 10}
616				return
617			}
618		}
619	}
620}
621
622// ------------------
623// Serveur en écoute
624// ------------------
625
626func incoming(connection Ircconnection) {
627	in, err := net.Listen("tcp", ":4321")
628	defer in.Close()
629	if err != nil {
630		fmt.Println(err)
631		os.Exit(1)
632	}
633
634	for {
635		inconn, err := in.Accept()
636		if err != nil {
637			fmt.Println(err)
638			continue
639		}
640		go handleIncoming(inconn, connection)
641	}
642}
643
644func handleIncoming(in net.Conn, connection Ircconnection) {
645	defer func() {
646		fmt.Println(bleu+"Déconnexion de ", in.RemoteAddr(), normal)
647		in.Close()
648		recover()
649	}()
650	fmt.Println(bleu+"Incoming from ", in.RemoteAddr(), normal)
651	inbuf := bufio.NewReader(in)
652	for {
653		inmsg, err := inbuf.ReadString('\n')
654		if err != nil || inmsg == "\n" {
655			break
656		}
657		fmt.Print(vert + "<<]] " + inmsg + normal)
658		//   		connection.SendMsg(inmsg,3)
659		sender <- Message{msg: inmsg, dest: channel, level: 10}
660		time.Sleep(500 * time.Millisecond)
661	}
662}
663
664// ------------
665//  Génériques
666// ------------
667
668func debugPrint(msg string, level int) {
669	if debug >= level {
670		fmt.Println(cyan + msg + normal)
671	}
672}
673func quizzUpScore(gagnant string) {
674	debugPrint("Entrée dans quizzUpScore pour "+gagnant, 4)
675
676	scoreData, err := ioutil.ReadFile("quizz_score.txt")
677	check(err)
678	scorelines := strings.Split(string(scoreData), "\n")
679	var nouveau_gagnant bool = true
680	for i, line := range scorelines {
681		if strings.Contains(line, gagnant+",") {
682			nouveau_gagnant = false
683			linedata := strings.Split(line, ",")
684			new_score, _ := strconv.Atoi(linedata[1])
685			new_score++
686			scorelines[i] = linedata[0] + "," + strconv.Itoa(new_score)
687			debugPrint("joueur :"+linedata[0]+" ancien score :"+linedata[1]+" nouveau score : "+strconv.Itoa(new_score), 4)
688		}
689	}
690	if nouveau_gagnant {
691		//au lieu d'append on édite la dernière ligne qui par défaut ne contient qu'un retour à la ligne. Ça évite les lignes vides qui se rajoutent.
692		scorelines[len(scorelines)-1] = gagnant + ",1\n"
693	}
694	newScoreData := strings.Join(scorelines, "\n")
695	err = ioutil.WriteFile("quizz_score.txt", []byte(newScoreData), 0644)
696	check(err)
697}
698func randomQuizz() (string, string, string, string) {
699	debugPrint("Entrée dans randomQuizz()", 4)
700	file, err := os.Open("quizz.txt")
701	check(err)
702	defer file.Close()
703	var quizzAssets []string
704	var nbr_line int = 0
705	scanner := bufio.NewScanner(file)
706	for scanner.Scan() {
707		quizzAssets = append(quizzAssets, scanner.Text())
708		nbr_line++
709	}
710	random_seed := rand.NewSource(time.Now().UnixNano())
711	random_source := rand.New(random_seed)
712	var retourQuizz []string = strings.Split(quizzAssets[random_source.Intn(nbr_line)], ",")
713	debugPrint(retourQuizz[0]+retourQuizz[1]+retourQuizz[2]+retourQuizz[3], 4)
714	return retourQuizz[0], retourQuizz[1], retourQuizz[2], retourQuizz[3]
715
716}
717
718func check(e error) {
719	if e != nil {
720		panic(e)
721	}
722}
723
724func Nick(utilisateur_complet string) string {
725	var nick_extrait string = utilisateur_complet[0:strings.IndexAny(utilisateur_complet, "!")]
726	return nick_extrait
727}
728
729func extractUntil(input string, pattern string) (string, string) {
730	var output1 string
731	var output2 string
732	if strings.Contains(input, pattern) {
733		output2 = strings.TrimPrefix(input[:strings.Index(input, pattern)], " ")
734		output1 = strings.TrimPrefix(strings.TrimPrefix(input, output2), " ")
735		//		fmt.Println(violet+"[input:"+normal+input+violet+"] [output1:"+normal+output1+violet+"] [output2:"+normal+output2+violet+"]"+normal)
736		return output1, output2
737	} else {
738		//		fmt.Println(violet+"[input:"+normal+input+violet+"] [output1:"+normal+input+violet+"] [output2:"+normal+""+violet+"]"+normal)
739		return input, ""
740
741	}
742}