# HG changeset patch
# Node ID a0a9701fa441
# Parent  0079467afee4
diff --git a/libpurple/protocols/irc/cmds.c b/libpurple/protocols/irc/cmds.c
--- a/libpurple/protocols/irc/cmds.c
+++ b/libpurple/protocols/irc/cmds.c
@@ -390,7 +390,7 @@
 		buf = irc_format(irc, "vc:", "PART", args[0] ? args[0] : target, args[1]);
 	else
 		buf = irc_format(irc, "vc", "PART", args[0] ? args[0] : target);
-	irc_send(irc, buf);
+	irc_priority_send(irc, buf, strlen(buf));
 	g_free(buf);
 
 	return 0;
@@ -479,7 +479,7 @@
 		 * decide we want custom quit messages.
 		 */
 		buf = irc_format(irc, "v:", "QUIT", (args && args[0]) ? args[0] : IRC_DEFAULT_QUIT);
-		irc_send(irc, buf);
+		irc_priority_send(irc, buf, strlen(buf));
 		g_free(buf);
 
 		irc->quitting = TRUE;
diff --git a/libpurple/protocols/irc/irc.h b/libpurple/protocols/irc/irc.h
--- a/libpurple/protocols/irc/irc.h
+++ b/libpurple/protocols/irc/irc.h
@@ -29,7 +29,6 @@
 #include <sasl/sasl.h>
 #endif
 
-#include "circbuffer.h"
 #include "ft.h"
 #include "roomlist.h"
 #include "sslconn.h"
@@ -44,6 +43,13 @@
 
 #define IRC_DEFAULT_QUIT "Leaving."
 
+/* By default set the command send interval to 2 seconds and allow bursting of
+ * 5 commands at once.  This means, if we haven't sent a command in 10 seconds
+ * we can send 5 commands immediately with no penalty.
+ */
+#define IRC_DEFAULT_COMMAND_INTERVAL 2
+#define IRC_DEFAULT_COMMAND_MAX_BURST 5
+
 #define IRC_BUFSIZE_INCREMENT 1024
 #define IRC_MAX_BUFSIZE 16384
 
@@ -93,10 +99,12 @@
 
 	gboolean quitting;
 
-	PurpleCircBuffer *outbuf;
-	guint writeh;
+	time_t recv_time;
 
-	time_t recv_time;
+	GQueue *send_queue;
+	time_t send_time;
+	guint send_handler;
+	gboolean sent_partial;
 
 	char *mode_chars;
 	char *reqnick;
@@ -120,8 +128,9 @@
 
 typedef int (*IRCCmdCallback) (struct irc_conn *irc, const char *cmd, const char *target, const char **args);
 
-int irc_send(struct irc_conn *irc, const char *buf);
-int irc_send_len(struct irc_conn *irc, const char *buf, int len);
+void irc_send(struct irc_conn *irc, const char *buf);
+void irc_send_len(struct irc_conn *irc, const char *buf, int len);
+void irc_priority_send(struct irc_conn *irc, const char *buf, gsize len);
 gboolean irc_blist_timeout(struct irc_conn *irc);
 gboolean irc_who_channel_timeout(struct irc_conn *irc);
 void irc_buddy_query(struct irc_conn *irc);
diff --git a/libpurple/protocols/irc/irc.c b/libpurple/protocols/irc/irc.c
--- a/libpurple/protocols/irc/irc.c
+++ b/libpurple/protocols/irc/irc.c
@@ -29,6 +29,7 @@
 #include "blist.h"
 #include "conversation.h"
 #include "debug.h"
+#include "glibcompat.h"
 #include "notify.h"
 #include "prpl.h"
 #include "plugin.h"
@@ -95,9 +96,21 @@
 		ret = write(irc->fd, buf, len);
 	}
 
+	irc->send_time = time(NULL);
+
 	return ret;
 }
 
+void irc_priority_send(struct irc_conn *irc, const char *buf, gsize len)
+{
+	if(irc->sent_partial) {
+		g_queue_insert_after(irc->send_queue, irc->send_queue->head,
+		                     g_strdup(buf));
+	} else {
+		do_send(irc, buf, len);
+	}
+}
+
 static int irc_send_raw(PurpleConnection *gc, const char *buf, int len)
 {
 	struct irc_conn *irc = (struct irc_conn*)gc->proto_data;
@@ -108,99 +121,123 @@
 	return len;
 }
 
-static void
-irc_send_cb(gpointer data, gint source, PurpleInputCondition cond)
-{
-	struct irc_conn *irc = data;
-	int ret, writelen;
+static gboolean
+irc_send_handler_cb(gpointer data) {
+	struct irc_conn *irc = (struct irc_conn *)data;
+	gint available = 0;
+	gint interval = 0;
+
+	interval = purple_account_get_int(irc->account, "ratelimit-interval",
+	                                  IRC_DEFAULT_COMMAND_INTERVAL);
 
-	writelen = purple_circ_buffer_get_max_read(irc->outbuf);
-
-	if (writelen == 0) {
-		purple_input_remove(irc->writeh);
-		irc->writeh = 0;
-		return;
+	/* Check if we're enabled. */
+	if(interval < 1) {
+		available = G_MAXINT;
+	} else {
+		gint burst = purple_account_get_int(irc->account, "ratelimit-burst",
+		                                    IRC_DEFAULT_COMMAND_MAX_BURST);
+		available = (time(NULL) - irc->send_time) / interval;
+		if(available > burst) {
+			available = burst;
+		}
 	}
 
-	ret = do_send(irc, irc->outbuf->outptr, writelen);
+	while(available > 0) {
+		gchar *msg = NULL;
+		gpointer raw = NULL;
+		gint length = 0, ret = 0;
+
+		/* No messages in the queue should be NULL, so a NULL value means the
+		 * queue is empty.
+		 */
+		raw = g_queue_pop_head(irc->send_queue);
+		if(raw == NULL) {
+			break;
+		}
+
+		msg = (gchar *)raw;
+		length = strlen(msg);
+
+		ret = do_send(irc, msg, length);
+		if(ret <= 0 && errno != EAGAIN) {
+			PurpleConnection *gc = purple_account_get_connection(irc->account);
+			gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
+			                             g_strerror(errno));
+
+			purple_connection_error_reason(gc,
+			                               PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
+			                               tmp);
+			g_free(tmp);
+
+			g_free(msg);
+
+			return G_SOURCE_REMOVE;
+		} else if(ret < length) {
+			gchar *partial = NULL;
 
-	if (ret < 0 && errno == EAGAIN)
-		return;
-	else if (ret <= 0) {
-		PurpleConnection *gc = purple_account_get_connection(irc->account);
-		gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
-			g_strerror(errno));
-		purple_connection_error_reason (gc,
-			PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
-		g_free(tmp);
-		return;
+			/* The preceeding conditional allows EAGAIN to fall through to
+			 * here so that we can retransmit it.  There shouldn't even be a
+			 * case where ret < 0 and != EAGAIN, which is why we have the
+			 * assert.
+			 */
+			if(ret < 0) {
+				if(ret == EAGAIN) {
+					ret = 0;
+				} else {
+					g_assert_not_reached();
+				}
+			}
+
+			/* We need to move to passed the characters we already wrote and
+			 * requeue the rest of the string. We know ret is less than length,
+			 * so the starting address of msg plus ret can never get outside of
+			 * the string, and likewise, length minus ret will always be <
+			 * length because ret is less than length and if it was somehow
+			 * negative, it's been reset to zero.
+			 */
+			partial = g_strndup(msg + ret, length - ret);
+
+			/* requeue the item to the start of the queue */
+			g_queue_push_head(irc->send_queue, partial);
+			irc->sent_partial = TRUE;
+		} else {
+			/* We successfully sent a message so decrement the counter. */
+			available -= 1;
+			irc->sent_partial = FALSE;
+		}
+
+		/* Message was processed successfully or a partial message was
+		 * allocated and requeued so we can free the one we popped off.
+		 */
+		g_free(msg);
 	}
 
-	purple_circ_buffer_mark_read(irc->outbuf, ret);
-
-#if 0
-	/* We *could* try to write more if we wrote it all */
-	if (ret == write_len) {
-		irc_send_cb(data, source, cond);
-	}
-#endif
+	return G_SOURCE_CONTINUE;
 }
 
-int irc_send(struct irc_conn *irc, const char *buf)
+void irc_send(struct irc_conn *irc, const char *buf)
 {
     return irc_send_len(irc, buf, strlen(buf));
 }
 
-int irc_send_len(struct irc_conn *irc, const char *buf, int buflen)
-{
-	int ret;
- 	char *tosend = g_strdup(buf);
+void
+irc_send_len(struct irc_conn *irc, const char *buf, int buflen) {
+	char *tosend = g_strdup(buf);
 
 	purple_signal_emit(_irc_plugin, "irc-sending-text", purple_account_get_connection(irc->account), &tosend);
 
-	if (tosend == NULL)
-		return 0;
-
-	if (!purple_strequal(tosend, buf)) {
-		buflen = strlen(tosend);
+	if(tosend == NULL) {
+		return;
 	}
 
-	if (purple_debug_is_verbose()) {
+	if(purple_debug_is_verbose()) {
 		char *clean = purple_utf8_salvage(tosend);
 		clean = g_strstrip(clean);
 		purple_debug_misc("irc", "<< %s\n", clean);
 		g_free(clean);
 	}
 
-	/* If we're not buffering writes, try to send immediately */
-	if (!irc->writeh)
-		ret = do_send(irc, tosend, buflen);
-	else {
-		ret = -1;
-		errno = EAGAIN;
-	}
-
-	/* purple_debug(PURPLE_DEBUG_MISC, "irc", "sent%s: %s",
-		irc->gsc ? " (ssl)" : "", tosend); */
-	if (ret <= 0 && errno != EAGAIN) {
-		PurpleConnection *gc = purple_account_get_connection(irc->account);
-		gchar *tmp = g_strdup_printf(_("Lost connection with server: %s"),
-			g_strerror(errno));
-		purple_connection_error_reason (gc,
-			PURPLE_CONNECTION_ERROR_NETWORK_ERROR, tmp);
-		g_free(tmp);
-	} else if (ret < buflen) {
-		if (ret < 0)
-			ret = 0;
-		if (!irc->writeh)
-			irc->writeh = purple_input_add(
-				irc->gsc ? irc->gsc->fd : irc->fd,
-				PURPLE_INPUT_WRITE, irc_send_cb, irc);
-		purple_circ_buffer_append(irc->outbuf, tosend + ret,
-			buflen - ret);
-	}
-	g_free(tosend);
-	return ret;
+	g_queue_push_tail(irc->send_queue, tosend);
 }
 
 /* XXX I don't like messing directly with these buddies */
@@ -357,7 +394,9 @@
 	gc->proto_data = irc = g_new0(struct irc_conn, 1);
 	irc->fd = -1;
 	irc->account = account;
-	irc->outbuf = purple_circ_buffer_new(512);
+
+	irc->send_queue = g_queue_new();
+	irc->sent_partial = FALSE;
 
 	userparts = g_strsplit(username, "@", 2);
 	purple_connection_set_display_name(gc, userparts[0]);
@@ -406,6 +445,7 @@
 	const char *nickname, *identname, *realname;
 	struct irc_conn *irc = gc->proto_data;
 	const char *pass = purple_connection_get_password(gc);
+	gint interval, burst;
 #ifdef HAVE_CYRUS_SASL
 	const gboolean use_sasl = purple_account_get_bool(irc->account, "sasl", FALSE);
 #endif
@@ -417,7 +457,7 @@
 		else /* intended to fall through */
 #endif
 			buf = irc_format(irc, "v:", "PASS", pass);
-		if (irc_send(irc, buf) < 0) {
+		if (do_send(irc, buf, strlen(buf)) < 0) {
 			g_free(buf);
 			return FALSE;
 		}
@@ -449,7 +489,7 @@
 	                 strlen(realname) ? realname : IRC_DEFAULT_ALIAS);
 	g_free(tmp);
 	g_free(server);
-	if (irc_send(irc, buf) < 0) {
+	if (do_send(irc, buf, strlen(buf)) < 0) {
 		g_free(buf);
 		return FALSE;
 	}
@@ -458,7 +498,7 @@
 	buf = irc_format(irc, "vn", "NICK", nickname);
 	irc->reqnick = g_strdup(nickname);
 	irc->nickused = FALSE;
-	if (irc_send(irc, buf) < 0) {
+	if (do_send(irc, buf, strlen(buf)) < 0) {
 		g_free(buf);
 		return FALSE;
 	}
@@ -466,6 +506,15 @@
 
 	irc->recv_time = time(NULL);
 
+	/* Give ourselves on full burst for startup commands. */
+	interval = purple_account_get_int(irc->account, "ratelimit-interval",
+	                                  IRC_DEFAULT_COMMAND_INTERVAL);
+	burst = purple_account_get_int(irc->account, "ratelimit-burst",
+	                               IRC_DEFAULT_COMMAND_MAX_BURST);
+
+	irc->send_time = time(NULL) - (interval * burst);
+	irc->send_handler = g_timeout_add_seconds(1, irc_send_handler_cb, irc);
+
 	return TRUE;
 }
 
@@ -540,10 +589,10 @@
 		g_string_free(irc->motd, TRUE);
 	g_free(irc->server);
 
-	if (irc->writeh)
-		purple_input_remove(irc->writeh);
-
-	purple_circ_buffer_destroy(irc->outbuf);
+	g_queue_free_full(irc->send_queue, g_free);
+	if(irc->send_handler != 0) {
+		g_source_remove(irc->send_handler);
+	}
 
 	g_free(irc->mode_chars);
 	g_free(irc->reqnick);
@@ -1104,6 +1153,16 @@
 	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
 #endif
 
+	option = purple_account_option_int_new(_("Seconds between sending messages"),
+	                                       "ratelimit-interval",
+	                                       IRC_DEFAULT_COMMAND_INTERVAL);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = purple_account_option_int_new(_("Maximum messages to send at once"),
+	                                       "ratelimit-burst",
+	                                       IRC_DEFAULT_COMMAND_MAX_BURST);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
 	_irc_plugin = plugin;
 
 	purple_prefs_remove("/plugins/prpl/irc/quitmsg");
diff --git a/libpurple/protocols/irc/msgs.c b/libpurple/protocols/irc/msgs.c
--- a/libpurple/protocols/irc/msgs.c
+++ b/libpurple/protocols/irc/msgs.c
@@ -1256,7 +1256,7 @@
 	char *buf;
 
 	buf = irc_format(irc, "v:", "PONG", args[0]);
-	irc_send(irc, buf);
+	irc_priority_send(irc, buf, strlen(buf));
 	g_free(buf);
 }
 
