wm: libirc

Download patch

ref: b37209a58eb37c8964c35755df501e4a29789935
author: Nima <nimaa@tuta.io>
date: Thu Oct 13 14:20:36 EDT 2022

Initial Commit

--- /dev/null
+++ b/Makefile
@@ -1,0 +1,2 @@
+all:
+	cc -I. net-posix.c irc.c utils.c examples/echo_bot.c 
--- /dev/null
+++ b/README.txt
@@ -1,0 +1,10 @@
+This "library" tries to make the process of writing irc client programs
+easier. it is not designed to be comperhensive, but rather designed to be
+easily expandable. it was originally written for an irc game bot and that 
+required very little and basic usage of the irc protocol. but you can easily 
+add more funcitonality to it, if you need to.
+
+see examples/echo_bot.c for details of how to use the library
+see irc.c and irc.h for details of how to expand the library 
+see net.h if you need to port libirc to other platforms/networking APIs
+
--- /dev/null
+++ b/examples/echo_bot.c
@@ -1,0 +1,53 @@
+#include <stdio.h>
+#include <string.h>
+#include "irc.h"
+#include "utils.h"
+
+int invite_handler(irc_session_t* s, irc_msg_t* msg) {
+	udebug("got invited to channel %s, joining the channel.", IRC_INVITE_CHANNEL(msg)); 
+	
+	irc_send_join(s, IRC_INVITE_CHANNEL(msg));
+	return 0;
+}
+
+int privmsg_handler(irc_session_t* s, irc_msg_t* msg) {
+	udebug("privmsg_handler(): message from %s : %s", IRC_PRIVMSG_ORIGIN(msg), IRC_PRIVMSG_MSG(msg));
+	
+	irc_send_privmsg(s, IRC_PRIVMSG_ORIGIN(msg), IRC_PRIVMSG_MSG(msg));
+	return 0;
+}
+
+int ping_handler(irc_session_t* s, irc_msg_t* msg) {
+	// libirc handles PINGs itself, but we can use this handler for debuugin purposes maybe
+	udebug("PING %s", IRC_PING_PARAMETER(msg));
+	return 0;
+}
+
+int main() {
+	irc_session_t sess;
+	irc_event_handler_set_t es;
+
+	memset(&es, 0, sizeof(es)); /* it is very important that all the unused event handlers are set to NULL */
+	es.invite_handler = invite_handler;
+	es.privmsg_handler = privmsg_handler;
+	es.ping_handler = ping_handler;
+
+	irc_init_session(&sess, /* session */
+			"bsdforall.org",  /* server */
+			"6667", /* port */
+			"mister_echo", /* nick name */
+			NULL,  /* password */
+			&es); /* event handlers */
+	if (irc_connect(&sess)) {
+		uerror("irc_connect() failed.");
+		return -1;
+	}
+
+	irc_main_loop(&sess); /* this blocks, libirc doesnt suppport multiple
+						  sessions, however, nothing's stopping you 
+						  from applying your own threading 
+						  mechanism to it. libirc has no global state and thus, you
+						  can have DIFFERENT IRC SESSIONS in different threads without 
+						  any syncrhonization.*/
+}
+
--- /dev/null
+++ b/irc.c
@@ -1,0 +1,329 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <assert.h>
+#include <string.h>
+#include <stdint.h>
+#include <memory.h>
+#include <stdarg.h>
+#include "irc.h"
+#include "utils.h"
+#include "net.h"
+
+void die(const char* msg) {
+	perror(msg);
+	exit(EXIT_FAILURE);
+}
+
+
+int irc_init_session(irc_session_t* s, const char* serv, const char* portno, const char* nick, const char* pass, irc_event_handler_set_t* es) {
+
+	memset(s, 0, sizeof(irc_session_t));
+
+	if (serv == NULL || portno == NULL || nick == NULL)
+		return -1;
+	s->server = strdup(serv);
+	s->portno = strdup(portno);
+	s->nick = strdup(nick);
+	s->password = pass? strdup(pass) : NULL;
+	s->buffer = (char *)malloc(IRC_MAX_BUFFER);
+	s->pos = 0;
+	s->event_handlers = *es;
+	assert(s->buffer);
+
+	s->connected = 0;
+	return 0;
+}
+ssize_t irc_send_raw(irc_session_t* s, const char* format, ...) {
+	char buffer[IRC_MAX_BUFFER];
+	va_list ap;
+	va_start(ap, format);
+
+	int n = vsnprintf(buffer, IRC_MAX_BUFFER, format, ap);
+	/* TODO: */
+	va_end(ap);
+	int nwrote = irc_socket_send(s->sockfd, buffer, n);
+#ifdef IRC_DEBUG
+	udbug("irc_send_raw(): wrote %d bytes into s->sockfd.", nwrote);
+#endif
+	return nwrote;
+}
+
+
+int irc_connect(irc_session_t* s) {
+#ifdef IRC_DEBUG
+	udebug("irc_connect(): connecting to %s:%s", s->server, s->portno);
+#endif
+	/* create/connect the socket, send the messages, see if it was a success */
+	// we have to use getaddrinfo and stuff, i think i wrote it somewhere right?
+	/* RFC 1459:
+	 * The recommended order for a client to register is as follows:
+	 * 1. Pass message
+	 * 2. Nick message
+	 * 3. User message */
+	s->sockfd = irc_socket_create(s->server, s->portno);
+	if (s->sockfd == -1) {
+		return -1; // FIXME: proper error messages
+	}
+#ifdef IRC_DEBUG
+	udebug("irc_connect() registering as NICK : %s USER: %s PASS: %s", s->nick, s->nick, s->password);
+#endif
+	if (s->password)
+		irc_send_raw(s, "PASS %s \r\n", s->password);
+	irc_send_raw(s, "NICK %s \r\n", s->nick);
+	irc_send_raw(s, "USER %s * * : AAAA\r\n", s->nick);
+#ifdef IRC_DEBUG
+	udebug("irc_connect(), sucessfully connected.");
+#endif
+	/* some irc servers require that we ping them first */
+	irc_send_raw(s, "PING : random\r\n");
+	return 0;
+}
+
+
+char* irc_str_skip_to(char *str, char c) {
+	while (*str) {
+		if (*str == c)
+			return str;
+		str++;
+	}
+	return NULL;
+}
+
+
+// returns the first cr(lf) in the string
+int irc_find_crlf(const char* str, int size) {
+	for (int i = 0; i < size; i++) {
+		if (str[i] == '\r' && str[i + 1] == '\n')
+			return i + 1;
+	}
+	return -1;
+}
+
+
+
+// buffer must be null-terminated, complete, raw irc message(excluding lf)
+int irc_parse_msg(char* buffer, irc_msg_t* msg, int lfpos) {
+	/*
+	 * From RFC 1459:
+	 *  <message>  ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
+	 *  <prefix>   ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
+	 *  <command>  ::= <letter> { <letter> } | <number> <number> <number>
+	 *  <SPACE>    ::= ' ' { ' ' }
+	 *  <params>   ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
+	 *  <middle>   ::= <Any *non-empty* sequence of octets not including SPACE
+	 *                 or NUL or CR or LF, the first of which may not be ':'>
+	 *  <trailing> ::= <Any, possibly *empty*, sequence of octets not including
+	 *                   NUL or CR or LF>
+	 */
+	memset(msg, 0, sizeof(irc_msg_t));
+
+	char *p = buffer;
+	char *prefix;
+	if (*p == ':') {
+		char *prefix_end = irc_str_skip_to(p, ' ');
+		if (!prefix_end) {
+#ifdef IRC_DEBUG_MODE
+			uerror("invalid message, parsing the prefix");
+#endif
+			goto invalid_msg;
+		}
+		*prefix_end = '\0';
+		prefix = p + 1;
+
+		msg->prefix_name = prefix;
+		char *n = irc_str_skip_to(prefix, '!');
+		if (n < prefix_end && n != NULL) {
+			// there's ['!' <user>] in the prefix.
+			*n = '\0';
+			msg->prefix_user = n + 1;
+		}
+
+		n = irc_str_skip_to(prefix, '@');
+		if (n < prefix_end && n != NULL) {
+			*n = '\0';
+			msg->prefix_host = n + 1;
+		}
+
+		*prefix_end = '\0';
+		p = prefix_end + 1;
+#ifdef IRC_DEBUG
+		udebug("msg->prefix_name = %s, msg->prefix_user = %s, msg->prefix_host = %s", msg->prefix_name, msg->prefix_user, msg->prefix_host);
+#endif
+	}
+
+	char *params_start = irc_str_skip_to(p, ' ');
+
+	if (!params_start) {
+#ifdef IRC_DEBUG_MOD
+		uerror("we don't have params_start.");
+#endif
+		goto invalid_msg;
+	}
+
+	*params_start = '\0';
+	msg->cmd = p;
+
+
+	p = params_start + 1;
+	char *params_end = irc_str_skip_to(p, ':');
+	if (!params_end) {
+#ifdef IRC_DEBUG_MODE
+		uwarn("we don't have trailing_start");
+#endif
+		params_end = buffer + lfpos; // apparently some servers don't have <trailing> with every msg
+	}
+	char *s = p;
+	int parameter_index = 0;
+	while (p != params_end && parameter_index < IRC_MAX_PARAMS - 1) {
+		if (*p == ' ') {
+			*p = '\0';
+			msg->parameters[parameter_index++] = s;
+			s = p + 1;
+		}
+		p++;
+	}
+	if(params_end != buffer + lfpos) { // if we had ':'
+		msg->parameters[parameter_index++] = params_end + 1; // it's already '\0' at the end
+	} else { // if we didn't have ':'
+		msg->parameters[parameter_index++] = s;
+	}
+	msg->parameters_size = parameter_index;
+
+#ifdef IRC_DEBUG_MODE
+	udebug("irc_parse_msg(); msg->cmd='%s'",  msg->cmd);
+
+	if (msg->parameters_size) {
+		udebug("irc_parse_msg(); msg->params are:");
+		for (int i = 0; i < msg->parameters_size; i++) {
+			printf("	[%d]: '%s'\n", i, msg->parameters[i]);
+		}
+	}
+#endif
+
+	return 0;
+invalid_msg:
+
+
+#ifdef IRC_DEBUG
+	uerror("invalid message(%s)", buffer);
+#endif
+
+
+	return -1;
+}
+
+int irc_process_msg(irc_session_t *s, irc_msg_t *m) { //call the right even handler
+	int32_t cmd = WORDL(m->cmd[0], m->cmd[1], m->cmd[2], m->cmd[3]);
+	int ret = 0;
+
+	/* FIXME: check m->parameters_size for validity of messages, if it doesnt have a sufficent number of
+	 * arguemtns, don't pass it to the handlers */
+	switch(cmd) {
+		case WORDL('P', 'I', 'N', 'G'):
+
+			/* apparently some IRC servers require PONG :<param> and some require PONG <param>,
+			 * and some require both! */
+			if (s->event_handlers.ping_handler)
+				ret = s->event_handlers.ping_handler(s, m);
+			if (m->parameters_size > 1)
+				irc_send_raw(s, "PONG %s :%s\r\n", m->parameters[0], m->parameters[1]);
+			else
+				irc_send_raw(s, "PONG :%s\r\n", m->parameters[0]);
+			break;
+
+
+		case WORDL('P', 'R', 'I', 'V'):
+			if (s->event_handlers.privmsg_handler)
+				ret = s->event_handlers.privmsg_handler(s, m);
+			break;
+
+		case WORDL('N', 'I', 'C', 'K'):
+			if (s->event_handlers.nick_handler)
+				ret = s->event_handlers.nick_handler(s, m);
+			break;
+
+		case WORDL('I', 'N', 'V', 'I'):
+			if (s->event_handlers.invite_handler)
+				ret = s->event_handlers.invite_handler(s, m);
+			break;
+		case WORDL('J', 'O', 'I', 'N'):
+			if (s->event_handlers.join_handler)
+				ret = s->event_handlers.join_handler(s, m);
+	}
+	return ret;
+}
+int irc_handle_incoming_data(irc_session_t* s, int lfpos) {
+
+	irc_msg_t msg;
+
+	s->buffer[lfpos - 1] = '\0';
+	if (irc_parse_msg(s->buffer, &msg, lfpos) == -1) {
+#ifdef IRC_DEBUG_MODE
+		uerror("irc_handle_incoming_adta(): invalid server command. \n");
+#endif
+		return -1;
+	}
+
+	int r = irc_process_msg(s, &msg); // call the right callbacks.
+	if (r != 0) {
+#ifdef IRC_DEBUG_MODE
+		uerror("irc_process_msg() wasn't sucessfull, ignoring the server message.");
+#endif
+		return -1;
+	}
+
+	return 0;
+}
+
+
+void irc_main_loop(irc_session_t* s) {
+	while (1) {	
+#ifdef IRC_DEBUG_MODE
+		udebug("irc_main_loop();");
+#endif
+		int nread = irc_socket_recv(s->sockfd, s->buffer + s->pos, IRC_MAX_BUFFER - s->pos);
+		if (nread <= 0) {
+#ifdef IRC_DEBUG_MODE
+			uerror("irc_socket_recv() problem in irc_main_loop(), exiting for now.\n");
+			// FIXME proper error messages
+#endif
+			exit(EXIT_FAILURE);
+		}
+
+		s->pos += nread;
+
+		int lfpos;
+		while ((lfpos = irc_find_crlf(s->buffer,s->pos)) != -1) {
+			irc_handle_incoming_data(s, lfpos);
+			memmove(s->buffer, s->buffer + lfpos + 1, s->pos - lfpos);
+			s->pos = s->pos - (lfpos + 1);
+		}
+
+	}
+}
+
+
+void irc_send_join(irc_session_t* s, const char *channel) {
+	irc_send_raw(s, "JOIN %s\r\n", channel);
+}
+
+void irc_send_privmsg(irc_session_t* s, const char *guy, const char *msg) {
+	irc_send_raw(s, "PRIVMSG %s :%s\r\n", guy, msg);
+}
+
+void irc_send_nick(irc_session_t* s, const char *nick) {
+	irc_send_raw(s, "NICK %s : \r\n", s, nick);
+}
+
+void irc_sendv_privmsg(irc_session_t* s, const char *guy, const char *format, ...) {
+	char buffer[IRC_MAX_MSG]; // FIXME 
+	va_list ap;
+
+	va_start(ap, format);
+
+	vsnprintf(buffer, sizeof(buffer), format, ap);
+	irc_send_privmsg(s, guy, buffer);
+
+	va_end(ap);
+}
--- /dev/null
+++ b/irc.h
@@ -1,0 +1,86 @@
+#pragma once
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <errno.h>
+#include <assert.h>
+#include <string.h>
+#include <stdint.h>
+#include <memory.h>
+#include <stdarg.h>
+#include <utils.h>
+#include <stdint.h>
+
+
+#define IRC_MAX_PARAMS 15 + 1 // one for the trailing
+#define IRC_MAX_BUFFER 1024 
+#define WORDL(c0, c1, c2, c3) (uint32_t)(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)) // shoutout to Hexchat 
+#define IRC_MAX_MSG 512
+
+typedef struct {
+	char *prefix_name; // <servername> | <nick>
+	char *prefix_user; // ['!' <user>]
+	char *prefix_host; // ['@' <host>]
+	char *cmd; // <cmd>
+	char *parameters[IRC_MAX_PARAMS]; //<params>
+	int parameters_size; // size of this guy ^
+} irc_msg_t;
+
+typedef struct irc_session irc_session_t;
+typedef int (*event_handler_t)(struct irc_session* , irc_msg_t*); /* event handlers must return 0 on success, and non-zero values on failure */
+
+typedef struct {
+	event_handler_t privmsg_handler;
+	event_handler_t join_handler;
+	event_handler_t nick_handler;
+	event_handler_t ping_handler;
+	event_handler_t invite_handler;
+} irc_event_handler_set_t;
+
+typedef struct irc_session{
+	char *server;
+	char *portno;
+	char *nick;
+	char *password;
+	
+	char *buffer;
+	int pos;
+
+	int sockfd;
+	int connected;
+	irc_event_handler_set_t event_handlers;
+} irc_session_t;
+
+
+/* instances of irc_msg_t should ideally only be used through these macros. */
+/* PRIVMSG handler helper macros */
+#define IRC_PRIVMSG_IS_ORIGIN_CHANNEL(m)  m->parameters[0][0] == '#'
+#define IRC_PRIVMSG_ORIGIN(m) IRC_PRIVMSG_IS_ORIGIN_CHANNEL(m) ? m->parameters[0] : m->prefix_name
+#define IRC_PRIVMSG_MSG(m) m->parameters[1]
+
+/* INVITE handler helper macros */
+#define IRC_INVITE_CHANNEL(m) m->parameters[1]
+
+/* PING handler helper macros */
+#define IRC_PING_PARAMETER(m) m->parameters[0]
+
+/* more can(and should) be added as the need for handling other events arises. */
+
+/* functions */
+int irc_init_session(irc_session_t* s, const char* serv, const char* portno, const char* nick, const char* pass, irc_event_handler_set_t* es);
+int irc_create_socket(char *host, char* portno);
+ssize_t irc_send_raw(irc_session_t* s, const char* format, ...);
+int irc_connect(irc_session_t* s);
+int irc_find_crlf(const char* str, int size);
+char* irc_str_skip_to(char *str, char c);
+int irc_parse_msg(char* buffer, irc_msg_t* msg, int lfpos);
+int irc_process_msg(irc_session_t *s, irc_msg_t *m); //call the right even handler
+int irc_handle_incoming_data(irc_session_t* s, int lfpos);
+void irc_main_loop(irc_session_t* s);
+void irc_send_join(irc_session_t* s, const char *channel);
+void irc_send_privmsg(irc_session_t* s, const char *guy, const char *msg);
+void irc_send_nick(irc_session_t* s, const char *nick);
+void irc_sendf_privmsg(irc_session_t* s, const char *guy, const char *format, ...);
--- /dev/null
+++ b/net-posix.c
@@ -1,0 +1,48 @@
+#include "net.h"
+
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <netdb.h>
+#include <string.h>
+
+irc_socket_t irc_socket_create(char* server, char* port) {
+	struct addrinfo hints;
+	struct addrinfo* result, *rp;
+	int sockfd, s;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags = 0;
+	hints.ai_canonname = NULL;
+	hints.ai_addr = NULL;
+	hints.ai_next = NULL;
+
+	s = getaddrinfo(server, port, &hints, &result);
+	if (s != 0)
+		return -1;
+	for (rp = result; rp != NULL; rp = rp->ai_next) {
+		sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+		if (sockfd == -1)
+			continue;
+		s = connect(sockfd, rp->ai_addr, rp->ai_addrlen);
+		if (s != 0) {
+			close(sockfd);
+			continue;
+		}
+		break;
+	}
+	freeaddrinfo(result);
+	if (rp == NULL) 
+		return -1;
+	return sockfd;
+}
+
+ssize_t irc_socket_recv(irc_socket_t sock, char* buffer, size_t max) {
+	return recv(sock, buffer, max, 0);
+}
+
+ssize_t irc_socket_send(irc_socket_t sock, char* buffer, size_t size) {
+	return write(sock, buffer, size);
+}
--- /dev/null
+++ b/net.h
@@ -1,0 +1,19 @@
+/* if you need to use any other networking api, all you need to do is to implement
+ * this interface */ 
+#pragma once
+
+#include <sys/types.h> // size_t 
+typedef int irc_socket_t; /* network handle type, change this to what is
+							appropriate for your api, we're using the berekeley sockets.*/
+
+/* connect to server:port, and return a "handle" that will passed to irc_sock_recv() and irc_irc_sock_send() functioons*/
+irc_socket_t irc_socket_create(char *server, char *port); 
+
+/* reads at most max bytes from the sock and puts into the buffer,
+ * returns the number of bytes read on success, and a negative value on failure */
+ssize_t irc_socket_recv(irc_socket_t sock, char* buffer, size_t max);
+
+/* writes size bytes from the buffer into irc_socket_t, 
+ * returns the number of bytes that were written into sock on sucess, and a negative
+ * value on failure.*/
+ssize_t irc_socket_send(irc_socket_t sock, char* buffer, size_t size); 
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,37 @@
+#include "utils.h"
+
+
+void udebug(const char *fmt, ...) {
+	va_list ap;
+	va_start(ap, fmt);
+	//SAGI_PRINT(fmt, "[DEBUG]", SAGI_COLOR_GREEN_FG , ap);
+	printf(SAGI_COLOR_BLUE_FG "[DEBUG]" SAGI_COLOR_RESET);
+	vprintf(fmt, ap);
+	printf("\n");
+	va_end(ap);
+}
+
+
+void uwarn(const char *fmt, ...) {
+	va_list ap;
+	va_start(ap, fmt);
+
+	printf(SAGI_COLOR_RED_FG "[WARN]" SAGI_COLOR_RESET);
+	vprintf(fmt, ap);
+	printf("\n");
+
+	va_end(ap);
+}
+
+
+
+void uerror(const char *fmt, ...) {
+	va_list ap;
+	va_start(ap, fmt);
+	printf(SAGI_COLOR_RED_FG "[ERROR]" SAGI_COLOR_RESET);
+	vprintf(fmt, ap);
+	printf("\n");
+	
+	va_end(ap);
+}
+
--- /dev/null
+++ b/utils.h
@@ -1,0 +1,36 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdarg.h>
+
+
+#define SAGI_COLOR_BLACK_FG	"\033[1;30m"         
+#define SAGI_COLOR_RED_FG 	   	"\033[1;31m"         
+#define SAGI_COLOR_GREEN_FG  	"\033[1;32m"         
+#define SAGI_COLOR_YELLOW_FG 	"\033[1;33m"         
+#define SAGI_COLOR_BLUE_FG    	"\033[1;34m"         
+#define SAGI_COLOR_MAGENTA_FG 	"\033[1;35m"         
+#define SAGI_COLOR_CYAN_FG    	"\033[1;36m"         
+#define SAGI_COLOR_WHITE_FG   	"\033[1;37m"         
+#define SAGI_COLOR_BLACK_BG  	"\033[1;40m"
+#define SAGI_COLOR_RED_BG 	  	"\033[1;41m"	
+#define SAGI_COLOR_GREEN_BG  	"\033[1;42m"
+#define SAGI_COLOR_YELLOW_BG 	"\033[1;43m"
+#define SAGI_COLOR_BLUE_BG   	"\033[1;44m"
+#define SAGI_COLOR_MAGENTA_BG 	"\033[1;45m"
+#define SAGI_COLOR_CYAN_BG   	"\033[1;46m"
+#define SAGI_COLOR_WHITE_BG  	"\033[1;47m"
+#define SAGI_COLOR_RESET           "\033[1;0m"
+#define SAGI_COLOR_BOLD_B		"\033[1;1m"  
+#define SAGI_COLOR_UNDERLINE       "\033[1;4m" 
+#define SAGI_COLOR_INVERSE         "\033[1;7m" 
+#define SAGI_COLOR_BOLD_O 		"\033[1;21m"  
+#define SAGI_COLOR_UNDERLINE_OFF   "\033[1;24m" 
+#define SAGI_COLOR_INVERSE_OFF "\033[1;27m" 
+
+
+void uprint(const char* fmt, const char *txt, const char *color, va_list ap);
+void udebug(const char *fmt, ...);
+void uwarn(const char *fmt, ...);
+void uerror(const char *fmt, ...);
+void udie(const char *msg);