Creando un bot IRC en PHP con Feed RSS

Creando un bot IRC en PHP con Feed RSS

$index[0][0] : Introducción


$index[0][1]. ¿Por qué esta guía?

Esta guía va destinada a un nivel bajo de PHP, explicando cada función y su funcionamiento, revisando el código línea por línea y ofreciendo soporte en www.bl4ckp0rtal.org/foro para cualquier tipo de problema o duda.
También viene originada por la poca documentación útil (se pueden encontrar entre 10 y 20 tutoriales de bots en PHP y ninguno de ellos funciona o está bien explicado) que se pueda encontrar en la red, sin ir más lejos mi primer bot se ha basado en un código PHP no funcional, en un código python y con la ayuda de mi amigo Frozzenfew en este lenguaje.

$index[0][2]. ¿Qué necesito saber?

Yo creo que no hay nada que no se pueda conseguir sin ganas y la motivación adecuada, pero te recomiendo que tengas una noción aceptable de PHP (sobretodo arrays y creación de funciones).
Todas las funciones que voy a explicar se pueden encontrar en php.net (Bendita página) y siempre que tengamos algún problema es sin duda al primer sitio donde debemos ir, de todos os iré dejado un link a su respectiva página en php.net.


$index[1][0] : Los primeros pasos de tu bot


$index[1][1]. Las Clases (Class) de PHP

Vamos a hacer uso de la OOP de PHP (más info: http://php.net/manual/en/language.oop5.php) para la programación de nuestro bot.
Lo básico que debemos saber:

Class ClassName //Declaramos la Class como si se tratase de una función
{
Var
variable = valor; //a estas variables accederemos así: $this->variable
Function __construct($variable) //el () funciona igual que en function
{ //esta función se llama cuando usamos new ClassName
//Aquí aremos las operaciones correspondientes al crear el nuevo objeto (bot)
}
}
$var = new ClassName($variable) //Llamamos a la clase (creándola) y le pasamos una variable para que trabaje con ella.
?>
$index[1][2]. Configuración del Bot

Ya que tenemos claro cómo será el esqueleto de nuestro bot, vamos a empezar por codear algunas configuraciones previas. (la parte anterior era solo un ejemplo, si hay alguna duda en el orden del code abajo dejaré el code completo)

set_time_limit(0)
ini_set(‘display_errors’, ‘on’)
$config = array(//Config del server:
'server' => 'server’,
'
port' => 6667,
'
nick' => nick,
'
pass' => pass,
'
name' => 'nombre',
'
owner' => 'tunick',
'
channel' => 'canal
);
?>

El time limit es el tiempo que un script tiene permitido ejecutarse, si el tiempo se supera el script se cierra, esto es bueno para evitar ciertos bucles pero en nuestro caso no queremos que se cierre por lo tanto quitamos la opción asignándole el valor 0.
Con ini_set habilitamos que se muestren los errores. No creo que haga falta documentar más ambas partes.

La variable $config la usaremos como array, es más simple así, de entre toda la configuración solo cabe resaltar:
Owner: es la persona que controlará el bot, aquellos más experimentados y que tengan la necesidad de asignar el Owner a varias personas siéntanse libres de crearse un array en owner.
Pass: Antes de poder usar esto hemos de registrar el Nick de nuestro bot, que quede claro para que luego no haya errores en el funcionamiento.

$index[1][3]. Función __Construct()

Para que no haya dudas el nombre de construct lleva 2 ‘_’.
Cuando declaramos una variable fuera de una función podemos usar GLOBAL para acceder a ella o pasarla por parámetro, este último método usaremos, así que la función __construct requerirá la variable $config.
Dejo los comentarios originales del code porque os pueden ayudar para entender una primera lectura del code.

class IRCBot
{
var $socket;//Definimos la conexión
var $ex = array();//Definimos el array de datos

function __construct($config)
{
$this->socket = fsockopen($config['server'], $config['port']); //conectamos...
if (!$this->socket) //comprobemos que conecta...
{ echo "Unable to connect to ".$config['server'];
} else {
$this->send('USER', $config['nick'].' bl4ckp0rtal.org '.$config['nick'].' :'.$config['name']); //Log User
$this->send('NICK', $config['nick']); //Change Nick
$this->main($config); //A jugar!
}
}
?>
Creamos la Class con el nombre IRCBot y le definimos 2 variables $socket para la conexión y $ex como un array para cuando separemos los datos (esto ya se verá mas adelante.
Analizemos __construct mas a fondo, primero que nada __construct requiere una variable como argumento que llamará $config.
Usamos la variable para abrir una conexión mediante el comando fsockopen(dirección,puerto) (info: http://es2.php.net/manual/en/function.fsockopen.php).
Una vez que hemos intentado conectar comprobaremos que se haya conectado, si es así con la función send(comando,mensaje) (esta función la explicaré más abajo al detalle, de momento quedaros con que envía datos al servidor.), cambiamos nuestro Nick y llamamos a la función main($config).


$index[1][4]. Función main()


En esta función también necesitamos trabajar con los datos de la configuración por eso requerirá que se le pase una variable por parámetro.

Pd. A partir de aquí hay muchísimas formas de plantear el funcionamiento. Así que espero que el lector se tome este manual como una simple orientación para comprender el cómo y luego pueda aplicar su propia lógica de programación. Esta en ningún momento se dice que sea la única ni la mejor manera de hacerlo, simplemente es la forma que a mí me gusta hacer.

function main($config)
{
$on = 1; // funciona asi que on es 1
while($on) //Aca vamos con el bucle, solo salimos si on es 0
{
$data = fgets($this->socket,1024); //recibimos datos
echo nl2br($data);
flush();
$this->ex = explode(' ',$data); //Separamos los datos
?>
Aquí no acaba el code pero vamos a ir por partes, la función main es la principal, el cerebro de la aplicación. Se encarga de recolectar los datos y de dar las respuestas correspondientes.
Primero de todo ponemos una variable llamada $on en 1 o true que nos servirá para controlar el bucle “infinito”. A continuación creamos ese espantoso bucle infinito que nos puede dar varios problemas. Si en algún momento vuestro bot falla y se desconecta sin razón aparente o intenta entrar a un canal ya conectado recomiendo ir al Administrador de tareas y matar el proceso httpd.exe pues con varios fallos podría llegar a sobrecargar nuestro pc.

Bien, primero vamos a capturar los datos que nos envían y guardarlo en la variable $data con fgets(conexión,tamaño de datos), hasta hoy tenía el tamaño a 128 pero recomiendo subirlo a 1024 para poder enviarle más datos, mostramos los datos por pantalla y usamos flush(); (http://es2.php.net/manual/en/function.flush.php).

Ahora viene la parte que mas confusiones crea, primero hemos de tener clara como será el formato de $data:

:nax!nax@bl4ckp0rtal.org PRIVMSG #bl4ckp0rtal :palabra1 palabar2 etc

Ese es el formato básico de un mensaje, podemos analizar 4 partes esenciales:
$this->Ex[0] :nax!nax@bl4ckp0rtal.org: en esta parte sacamos 3 partes más, el Nick, el user y el host.
$this->Ex[1] PRIVMSG: el comando.
$this->Ex[2] #bl4ckp0rtal : el canal en los mensajes privados saldrá el nombre de nuestro bot y no el canal…
$this->Ex[3,4,5,etc..] :palabra1 palabar2 etc: Las considero un conjunto pero en realidad en el explode las estamos separando.
Ese es el formato básico que tendrá un mensaje que nos envíen por medio de un canal. Aconsejo miremos el log frecuentemente para ver los cambios entre un mensaje un cambio de poderes (MODE) , una expulsión (KICK) un ban, etc.
Con esto nuestro bot ya es capaz de entrar a nuestro canal y registrar la conversación, pero aun tiene muchas carencias, la más importante es el PING.
El servidor nos enviará cada X tiempo un comando PING seguido de un código al cual deberemos responder PONG y el mismo código. Así que procederemos con nuestra obra:


if(strlen(strstr($data,'PING :'))>0) //Jugando al Ping Pong
{
$pong = str_replace('PING','PONG',$data);
$this->send($pong);
} else if (strlen(strstr($data,'for Open Proxies'))>0)
{//Si acabó de entrar a deepspace o nos echan del canal conectamos
$this->send('JOIN', $config['channel']);
$this->send('IDENTIFY', $config['pass']);
}

?>
Lo que hacemos es básicamente buscar la palabra PING, el strlen() es por el caso de que el resultado de 0 y así no evaluaría como FALSE no es para nada necesario en este caso pero me gusta tener buenas costumbres de programación, así que solo lo remplazamos por PONG y lo enviamos.
También agregué un else if con el último data que envía mi servidor para saber que ya terminó de recibir todo los data (me vi obligado a ello porque sinó no hacia el JOIN correctamente), así que solo hemos de entrar a nuestro servidor IRC con el bot y ver cuál es el ultimo data que envía.
En mi caso:
:opsb2!opsb2@stats.deepspace.org NOTICE botiboti :Your Host is being Scanned for Open Proxies
Entonces solo nos queda entrar al canal e identificar el nick.

$index[1][4]. Función send()


Ahora voy a hacer un pequeño salto a la función send() para que podamos entender más claramente el código que tenemos en las manos y sobretodo el código que nos queda.
El formato al enviar datos a send es: send($comando, [$mensaje]), donde el mensaje es un valor opcional.


function send($cmd, $msg=null) // Funcion para enviar comandos
{
if($msg == null)
{
fputs($this->socket, $cmd."\r\n");
echo ''.$cmd.'
'
;
} else {
fputs($this->socket, $cmd.' '.$msg."\r\n");
echo ''.$cmd.' '.$msg.'
';
}
}
?>

Usamos la función fputs($conexion, $mensaje a enviar) (info http://es2.php.net/manual/en/function.fputs.php). El \r y \n es para indicar a IRC que se acabó el comando (es como enviar un enter)
No hay mucho más que explicar, pero creo que ahora se hace más claro el código.


$index[2][0] Dándole un cerebro al bot


$index[2][1] Función main(), evaluando y tomando decisiones.

Quiero aclarar que esta parte del code (de la versión 1.1.5) está pendiente de ser reescrita, funciona perfectamente pero da una sensación abstracta y está poco organizada, pero se arreglará para la versión 1.2.0 y muy posiblemente editaré estos puntos para que el manual sea aún más claro y eficaz.

if(preg_match('/^:+'.$config['owner'].'/',$data)) //solo si es Owner
{
unset($arg);//si el comando tiene argumentos los metemos en $ar
for($i = 4; $i <= count($this->ex)-1; $i++) {$arg = $arg.' '.$this->ex[$i]; }

$command = str_replace(array(chr(10), chr(13)), '', $this->ex[3]);
$arg = str_replace(array(chr(10), chr(13)), '', $arg);

?>
Con el if simplemente nos aseguramos que el que nos envía los comandos es el Owner.
Borramos la variable argumentos y el for siguiente es un proceso lógico.
Lo que hace es a partir del 4to valor del array y mientras $i sea menor o igual que la totalidad de valores -1 (el -1 es porque el primer valor es 0 y no 1) agregue ese valor a la variable $arg, así podríamos enviar mensajes con el comando .say por ejemplo.
Asignamos el $this->ex[3] al command, si tenemos dudas con esto revisemos varias veces la cadena $data y no olvidemos que la primera parte es 0 y no 1
Ahora quitamos \r y \n a $command y $arg, dicen que es recomendable, no e probado que pasa si no se quitan, si tenéis curiosidad probad y comentadme…

$index[2][1] Primeras palabras del bot
A partir de aquí empiezan los comandos, voy a poner los más básicos, porque los demás es cuestión de probarlo y ver que formato tiene el $data y realizar las comprobaciones necesarias



$index[2][2] Primers palabras del bot

A partir de aquí empiezan los comandos, voy a poner los más básicos, porque los demás es cuestión de probarlo y ver que formato tiene el $data y realizar las comprobaciones necesarias


if($command == ':.q')
{
if($arg) { $this->send('QUIT', $arg); } else { $this->send('QUIT', 'Bye!'); } // QUIT
$on = 0;
}else if($command == ':.join' && isset($this->ex[4])) {
for($i = 4; $i <= count($this->ex)-1; $i++ ) { $this->send('JOIN', $this->ex[$i]); } // JOIN
}else if($command == ':.leave') { $this->send('PART', $this->ex[2]); // LEAVE
?>


La comprobación la hacemos con if y no switch por el simple hecho de poder agregar varias condiciones, los ‘:’ del principio es porque no merece la pena una línea para quitar los ‘:’, todos estos comandos se le indican al bot por el canal y no pro privado.
Como podemos ver todo se basa en el comando SEND y la sintaxis correcta del $data, para salir enviamos un QUIT, el argumento es un mensaje opcional que si no se pone se envía Bye!, el $on=0 es para salir del while indicando que se va a cerrar la conexión.
El Join conecta con todos los canales que le pasemos y el LEAVE sale del canal en el que mandamos la acción.
}else if($this->ex[2] == $config['nick'] && isset($this->ex[3])) {
$this->ex[3] = str_replace(':','',$this->ex[3]);
$this->send('PRIVMSG '.$this->ex[3], $arg); // SAY - ONLY PRIVATE
?>


Este comando es un tanto más especial y es la base para la versión 1.2.0, comprueba que en vez de enviarse a un canal se envíe a él (es decir un mensaje privado) y comprueba que se le haya dado valor a $this->ex[3] (que es donde indicaremos el canal), si estas condiciones se cumplen substituye los ‘:’ por nada y envía un PRIVMSG al canal con el texto que hemos asignado.

$index[2][3] Primeros brotes de autonomía.

}else if ($this->ex[1] == 'KICK' && $this->ex[3] == $config['nick']) { //Kick? I Rejoin!
$this->send('JOIN', $this->ex[2]);
}
?>
Esta parte se encarga de detectar que ha sido kickeado y en caso de ser así entrar otra vez al canal.
Esta parte se puede configurar para que solo lo haga en los canales de “confianza” (los de $config), en mi caso lo he dejado en todos.

$index[2][4] Feed RSS.

Ahora vamos a ver su función más importante hasta el momento, .rss
En el mismo if que venimos modificando hace rato agregamos

}else if($command == ':.rss') { $this->FeedRss($this->ex[2]); //Show feed
?>

Y vamos a crear la función

function FeedRss($chann)
{
$feedurl='http://feeds.feedburner.com/Bl4ckp0rtal?format=xml';
$raw = file_get_contents($feedurl);
$rawfeed = str_replace('feedburner:origLink','origlink',$raw);
$xml = new SimpleXMLElement($rawfeed);
foreach($xml->channel->item as $item)
{
$this->send('PRIVMSG '.$chann, $item->title.' '.$item->origlink);
}
}
?>


Como requerimiento tenemos el $chann, para saber el canal.
En $feedurl guardamos la url del feed.
En $raw descargamos el código del feed en xml
Substituimos feedburner:origLink por origlink (sino el php da problemas al trabajar con un ítem con : en el nombre.
Ahora solo resta mostrarlos, el raw nos devuelve 5 items así que no hará falta limitar el bucle. (no hagan spam ¬¬”)

Usamos la librería SimpleXMLElements (info: http://es2.php.net/manual/en/book.simplexml.php), a mi parecer está muy pobremente explicada, pero con algo de maña, estudiando el code y algo de tutos en internet, entendemos lo siguiente:
El xml es algo asi

Bl4ck-P0rtal
http://www.bl4ck-p0rtal.org/foro/index.php
…bla bla bla…
<ítem>rss1link rss (no el original)
…blab la bla…

<ítem...
pd: la estructura del feed puede variar según el servicio pero suele ser similar,esta es de feed burner

y así 5 veces
Las partes que nos interesa son title y link y son las que usamos precisamente en el code, y como vemos están dentro de <ítem>

$index[2][5] Cerrándolo todo

Para finalizar cerramos todos los if sueltos y ponemos esto

fclose($this->socket);
?>
La línea antes de cerrar el while para que cierre la conexión.

$index[2][6] todo el code


El código entero se puede encontrar aqui: http://www.bl4ck-p0rtal.org/foro/index.php?topic=8212.0

no lo publico porque vamos a crear un post demasiado largo...


$index[3][0] Final


$index[3][1] Puteadas:

- A Microsoft Office por pasarse por el forro el formato de mis codes
- A todos esos lamos que se creen lo que no son por hacer cosas que mi hermana de 12 años hace mejor y luego se ocultan tras una pared para que nadie lo sepa ;)

$index[3][2] Gr33ts:

- A todo los users y administración de Bl4ck-P0rtal.org que siempre me enseñan algo nuevo
- A aquellos de indetectables que se nos meten en el IRC a hablar a ratos ;)
- A Leesiem y Frozenfew por su interés en el code y a este ultimo por su ayuda en python (gracias bro ;) )


Nax