IRC bot v PHP - 3. časť

Michal Pirchala  /  16. 12. 2010, 21:00

Minule sme si naprogramovali základné pripojenie na IRC sieť. Po pripojení na server sme dostali od frigg-a (bot IRC siete) správu, ktorou si vyžadoval verziu nášho IRC klienta. Dnes doprogramujeme automatickú odpoveď na túto správu, odpovede na PING-y, ktoré sme nemuseli riešiť keďže sme neboli dlhšiu dobu neaktívni a moduly.

Najprv z jadra odstránime riadky, ktoré slúžili iba na testovacie účely:

if (empty($msg)){
    send("JOIN $channel");sleep(3);
    send("PRIVMSG $channel :text odoslaný do kanála");
    send("QUIT");
    die();
}

Správa VERSION

Túto správu nemusíme dostať iba po pripojení na IRC server od frigg-a, ale aj od ostatných užívateľov, ktorí požiadajú o verziu nášho klienta. Preto bude vhodné kontrolovať každú správu, ktorú dostaneme od servera. Teda overovať sa bude pri každom cykle. Pod riadok „if (!empty($msg)) echo $msg."\n"; „ pridáme toto:

//VERSION
if (preg_match("/^\:(.*)\!.* PRIVMSG $nick \:VERSION/", $msg, $matches)){
    Send("NOTICE {$matches[1]} :VERSION VERZIA KLIENTA\n\r");
}

Najprv si musíme vysvetliť v akej forme k nám budú chodiť správy:

:NICK!HOST PRIVMSG NÁŠ_NICK/KANÁL :OBSAH SPRÁVY

Podľa tohto vzoru máme zostavený regulárny výraz. Do premennej $matches si odchytíme nick odosielateľa správy. $nick v regulárnom výraze je náš nick, aby sme neodpovedali na version správy, ktoré napríklad niekto pošle do kanála. Odpovieme príkazom NOTICE. Namiesto „VERZIA KLIENTA“ si môžete doplniť niečo vlastné. Vo fantázii sa medze nekladú.

PING

Či nejde o ping, budeme tiež overovať pri každom cykle a hneď naň odpovieme. Parameter získame pomocou funkcie substr (namiesto preg_match).

//PING
if (substr($msg, 0, 6) == "PING :"){
    Send("PONG :".substr($msg, 6)."\n\r");
}

Moduly

Moduly vyriešime pomerne jednoducho. Vytvoríme si priečinok modules v priečinku kde máme jadro, kde budeme moduly ukladať. Pri každom cykle prejdeme celý tento priečinok a všetky moduly (skrtipy) vložíme a vykonáme pomocou funkcie include.

//MODULES
$d = dir("modules");
while (false !== ($entry = $d->read())) {
    if ($entry!='.' && $entry!='..') include('modules/'.$entry);     //ak sa prave includovany subor nerovna .. (nadradeny priecinok), alebo . (aktualny priecinok)
}
$d->close();

V moduloch budeme pravdepodobne využívať údaje ako nick odosielateľa, host odosielateľa, aktuálny kanál a správu. Aby sme získanie týchto údajov neriešili v každom module samostatne, spravíme to ešte pred vykonaním prvého modulu. Ešte raz vzor správ, podľa ktorého zostavíme regulárny výraz:

:NICK!HOST PRIVMSG NÁŠ_NICK/KANÁL :OBSAH SPRÁVY

//SPRAVA
if (preg_match("/^\:(.*)\!(.*) PRIVMSG (.*) \:(.*)$/", $msg, $matches)){
    $sender = $matches[1];
    $host = $matches[2];
    $act_chan = $matches[3];
    $message = $matches[4];
     
    if ($act_chan[0]!='#') $act_chan = $sender;
}

Do premennej $matches si uložíme subvýrazy. Prvý je nick odosielateľa ($sender), druhý host odosielaťeľa ($host), tretí aktuálny kanál ($act_chan) a štvrtý obsahuje samotnú správu ($message). Pozor na rozdiel medzi $message a $msg. $message obsahuje obsah správy (v príkaze PRIVMSG) a $msg obsahuje jeden riadok dát, ktorý sme dostali od servera. Všimnite si podmienku ktorá nasleduje. Tretí subvýraz ktorý dostaneme z nášho regulárneho výrazu obsahuje vždy kam sa má správa dostať. Ak má ísť do kanálu bude tam #kanál. Ale ak má ísť konkrétnemu užívateľovi, obsahuje jeho nick.  Keby sme dostali správu, obsahovala by premenná $act_chan náš nick. A keďže tam budeme odpovedať, písali by sme správy sami sebe. Táto podmienka zaistí, že ak sa nebude prvý znak premennej $act_chan rovnať „#“ (teda to nieje správa do kanála ale správa smerovaná nám), tak sa do $act_chan vloží nick odosielateľa. Teda pri odpovedi už budeme odpovedať jemu a nie sami sebe.

Ak by sa do týchto premenných uložili nejaké hodnoty, ubehol by ďalší cyklus a od servera by sme nedostali žiadnu správu, tieto hodnoty by sa nezmenili a bot by napríklad vykonával stále ten istý príkaz. Preto ich na konci každého cyklu vyprázdnime.

$msg='';
$message='';
$host='';
$sender='';

Automatické pripojenie do kanálu

Ak chceme, aby sa náš bot pripojil do vopred určeného kanálu, pridáme do jadra tento kód:

if (preg_match("/^\:.* 376 $NICK/", $msg, $matches)){
    Send("JOIN $channel $pass\n\r");
}

Teda po ukončení správy MOTD (ako sa môžeme dočítať v dokumentácii, táto správa má čislo 376) sa pripojíme do kanála uloženého v premennej $channel. Ak by sme do jadra nepridali túto funkciu, náš bot by po pripojení na server nevstúpil do žiadneho kanála a mohol by vykonávať iba príkazy poslané cez súkromné správy. V niektorej z ďalších častí si ukážeme, ako prinútiť bota, aby sa pripojil do ďalších kanálov.

Jadro

//Pripojenie k MySQL databaze
define('SQL_NAME', 'meno');
define('SQL_PASS', 'heslo');
define('SQL_HOST', 'server');
define('SQL_DBNM', 'databaza');
$dbc = mysql_connect(SQL_HOST, SQL_NAME, SQL_PASS);
if (!$dbc) die('Nedá sa pripojiť k databázovému serveru');
mysql_select_db(SQL_DBNM) or die ('Nedá sa vybrať databáza.');
mysql_query('SET NAMES UTF8');     //kodovanie v UTF-8

//Neobmedzeny casovy limit na vykonanie skriptu
set_time_limit(0);

//Nastavenie
$channel = '#naskanal';
$nick = 'mojbot';

//Funkcie
function Send ($cmd){
    $cmd = $cmd."\n\r";
    global $server;
    fwrite($server['SOCKET'], $cmd, strlen($cmd));
    echo "< $cmd";
}

//Pripojenie na server
$server = array();
$server['SOCKET'] = fsockopen("irc.freenode.net", 6667, $errno, $errstr);

if ($server['SOCKET']){

    Send("NICK $nick");
    Send("USER $nick $nick irc.freenode.net :$nick");
    stream_set_timeout($server['SOCKET'], 1);     //na spravu od servera budeme cakat maximalne jednu sekundu

    while(true){
        $msg = trim(fgets($server['SOCKET'], 1024));
        if (!empty($msg)) echo ">".$msg."\n";
     
        //VERSION
        if (preg_match("/^\:(.*)\!.* PRIVMSG $nick \:VERSION/", $msg, $matches)){
            Send("NOTICE {$matches[1]} :VERSION VERZIA KLIENTA\n\r");
        }

        //AUTOMATICKE PRIPOJENIE NA KANAL
        if (preg_match("/^\:.* 376 $NICK/", $msg, $matches)){
            Send("JOIN $channel $pass\n\r");
        }

        //PING
        if (substr($msg, 0, 6) == "PING :"){
            Send("PONG :".substr($msg, 6)."\n\r");
        }

        //SPRAVA
        if (preg_match("/^\:(.*)\!(.*) PRIVMSG (.*) \:(.*)$/", $msg, $matches)){
            $sender = $matches[1];
            $host = $matches[2];
            $act_chan = $matches[3];
            $message = $matches[4];
     
            if ($act_chan[0]!='#') $act_chan = $sender;
        }

        //MODULES
        $d = dir("modules");
        while (false !== ($entry = $d->read())) {
            if ($entry!='.' && $entry!='..') include('modules/'.$entry);     //ak sa prave includovany subor nerovna .. (nadradeny priecinok) alebo . (aktualny priecinok)
        }
        $d->close();

        $msg='';
        $message='';
        $host='';
        $sender='';
    }

} else echo "Nemôžem sa pripojiť na server";
?>

Záver

Momentálne je bot trocha nepraktický, keďže ho nemôžeme nijako ukončiť. Odporúčam ho preto spúšťať zo shellu, kde ho kedykoľvek môžete vypnúť stlačením Ctrl+C (v Linuxe). Nabudúce doprogramujeme modul na vykonavánie príkazov a nejaké príkazy.

Neprehliadnite: