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}