OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-sensory / opencog / atoms / irc-v0 / IRChatStream.cc

517 lines · cc ref:
/*
 * opencog/atoms/sensory-v0/IRChatStream.cc
 *
 * Copyright (C) 2020 Linas Vepstas
 * All Rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <opencog/util/exceptions.h>
#include <opencog/util/oc_assert.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include <opencog/sensory-v0/types/atom_types.h>
#include "IRChatStream.h"

#include "IRC.h"

#include <errno.h>
#include <string.h> // for strerror()
#include <unistd.h> // for sleep()

using namespace opencog;

IRChatStream::IRChatStream(Type t, const std::string& str)
	: OutputStream(t)
{
	OC_ASSERT(nameserver().isA(_type, I_R_CHAT_STREAM),
		"Bad IRChatStream constructor!");
	init(str);
}

IRChatStream::IRChatStream(const std::string& str)
	: OutputStream(I_R_CHAT_STREAM)
{
	init(str);
}

IRChatStream::IRChatStream(const Handle& senso)
	: OutputStream(I_R_CHAT_STREAM)
{
	if (SENSORY_NODE != senso->get_type())
		throw RuntimeException(TRACE_INFO,
			"Expecting SensoryNode, got %s\n", senso->to_string().c_str());

	init(senso->get_name());
}

IRChatStream::~IRChatStream()
{
	if (nullptr == _conn) return;
	_cancel = true;

	// We can send a quit, but then we never actually
	// wait for the quit reply. So .. whatever.
	_conn->quit("Adios");
	_conn->disconnect();

	_loop->join();
	delete _loop;
}

// ==================================================================
/// The URL format is described in
/// https://en.wikipedia.org/wiki/IRC
///
/// According to wikipedia, URI formats are:
/// irc://<host>[:<port>]/[<channel>[?<channel_keyword>]]
/// ircs://<host>[:<port>]/[<channel>[?<channel_keyword>]]
/// irc6://<host>[:<port>]/[<channel>[?<channel_keyword>]]
///
/// IRC URL format is described here:
/// https://datatracker.ietf.org/doc/html/draft-butcher-irc-url
///
/// See also: https://ircv3.net/irc/index.html
///
void IRChatStream::init(const std::string& url)
{
	_conn = nullptr;
	_cancel = false;

	if (0 != url.compare(0, 6, "irc://"))
		throw RuntimeException(TRACE_INFO,
			"Unsupported URL \"%s\"\n", url.c_str());

	// Make a copy, for debugging purposes.
	_uri = url;

	// Ignore the first 6 chars "irc://"
	size_t base = 6;
	size_t nck = url.find('@', base);

	if (std::string::npos == nck)
		throw RuntimeException(TRACE_INFO,
			"Invalid IRC URL \"%s\" expecting irc://nick@host[:port]\n",
			url.c_str());
	_nick = url.substr(base, nck-base);

	// There may or may not be a port number.
	size_t sls = url.find('/', nck);
	size_t col = url.find(':', nck);

	_port = 6667;
	nck ++;
	if (std::string::npos == col)
	{
		if (std::string::npos == sls)
			_host = url.substr(nck);
		else
			_host = url.substr(nck, sls-nck);
	}
	else
	{
		_host = url.substr(nck, col-nck);
		_port = atoi(url.substr(col+1, sls-col-1).c_str());
	}

	_conn = new IRC;
	_conn->context = this;

	// Hooks run in order. Privmsg will be most common.
	_conn->hook_irc_command("PRIVMSG", &xgot_privmsg);

	// 001 and 002 are server greetings.
	_conn->hook_irc_command("001", &xgot_privmsg);
	_conn->hook_irc_command("002", &xgot_privmsg);
	_conn->hook_irc_command("433", &xgot_privmsg);
	_conn->hook_irc_command("403", &xgot_privmsg);

	// Bypass the message I/O for some of this stuff.
	_conn->hook_irc_command("376", &xend_of_motd);
	_conn->hook_irc_command("KICK", &xgot_kick);

	// Misc debug printers
	_conn->hook_irc_command("JOIN", &xgot_misc);
	_conn->hook_irc_command("NOTICE", &xgot_misc);
	_conn->hook_irc_command("322", &xgot_misc);
	_conn->hook_irc_command("353", &xgot_misc);
	_conn->hook_irc_command("421", &xgot_misc);
	_conn->hook_irc_command("705", &xgot_misc);
	_conn->hook_irc_command("706", &xgot_misc);

	// Other messages:
	// JOIN - channel join messages
	// NOTICE - server notices
	// 001-005 - server info
	// 250-265 - channel info
	// 321 - start of channel listing
	// 322 - channel name and info
	// 323 - end of channel list
	// 328 - channel title?
	// 353 - channel : list of nicks in channel
	// 366 - end of /NAMES list.
	// 375 - start of motd
	// 372 - motd
	// 353 - channel members
	// 366 - end of list of channel members
	// 403 - channel not found
	// 421 - unknown command
	// 433 - :Nickname is already in use.
	// 705 - reply to HELP command
	// 706 - end of reply to HELP command
	// MODE - read-write modes

	// Run I/O loop in it's own thread.
	_loop = new std::thread(&IRChatStream::looper, this);
}

// ==================================================================

ValuePtr IRChatStream::describe(AtomSpace* as, bool silent)
{
	throw RuntimeException(TRACE_INFO, "Not implemeneted");
	return Handle::UNDEFINED;
}

// ==================================================================

// Infinite loop.
// XXX Needs major redesign. Works for now.
void IRChatStream::looper(void)
{
	// Defaults
	const char* user = "botski";
	const char* name = "Atomese Sensory Stream";
	const char* pass = "";

	// Loop forever. When the IRC network burps and closes our
	// connection, just log in again.
	while (true)
	{
		printf("Joining network=%s port=%d nick=%s user=%s\n",
			_host.c_str(), _port, _nick.c_str(), user);

		int rc = _conn->start(_host.c_str(), _port, _nick.c_str(),
		                      user, name, pass);
		if (rc)
			throw RuntimeException(TRACE_INFO,
				"Unable to connect (%d) to URL \"%s\"\n", rc, _uri.c_str());

		rc = _conn->message_loop();
		if (_cancel) return;

		if (rc) perror("IRChatStream Error: Socket error");

		// For now, assume we got kicked and want to spawn again.
		_conn->disconnect();
		sleep(20);
	}
}

// ==================================================================
// thunks

int IRChatStream::xend_of_motd(const char* params, irc_reply_data* ird,
                               void* data)
{
	IRC* conn = static_cast<IRC*>(data);
	IRChatStream* that = static_cast<IRChatStream*>(conn->context);
	return that->end_of_motd(params, ird);
}

int IRChatStream::xgot_privmsg(const char* params, irc_reply_data* ird,
                               void* data)
{
	IRC* conn = static_cast<IRC*>(data);
	IRChatStream* that = static_cast<IRChatStream*>(conn->context);
	return that->got_privmsg(params, ird);
}

int IRChatStream::xgot_kick(const char* params, irc_reply_data* ird,
                               void* data)
{
	IRC* conn = static_cast<IRC*>(data);
	IRChatStream* that = static_cast<IRChatStream*>(conn->context);
	return that->got_kick(params, ird);
}

int IRChatStream::xgot_misc(const char* params, irc_reply_data* ird,
                               void* data)
{
	IRC* conn = static_cast<IRC*>(data);
	IRChatStream* that = static_cast<IRChatStream*>(conn->context);
	return that->got_misc(params, ird);
}

// ==================================================================

/* printf can puke if these fields are NULL */
static void fixup_reply(irc_reply_data* ird)
{
	ird->nick = (NULL == ird->nick) ? (char *) "" : ird->nick;
	ird->ident = (NULL == ird->ident) ? (char *) "" : ird->ident;
	ird->host = (NULL == ird->host) ? (char *) "" : ird->host;
	ird->target = (NULL == ird->target) ? (char *) "" : ird->target;

#if DEBUG
	printf(">>> IRC nick=%s ident=%s host=%s target=%s\n",
		ird->nick, ird->ident, ird->host, ird->target);
	printf(">>> IRC reply params=%s\n", params);
#endif
}

/// Do not attempt to join any channels, until MOTD has arrived.
/// XXX Implement me as a semaphore or mutex or something.
int IRChatStream::end_of_motd(const char* params, irc_reply_data* ird)
{
	return 0;
}

int IRChatStream::got_misc(const char* params, irc_reply_data* ird)
{
	fixup_reply(ird);

	// ident is the "true identity" of the nick.
	// host is the hostname that the nick is sending from,
	// target is who the message is for.
	printf("Misc reply: nick=%s nick-ident=%s nick-host=%s msg-target=%s\n",
		ird->nick, ird->ident, ird->host, ird->target);
	printf("Misc params=%s\n", params);

	return 0;
}

// ==================================================================

int IRChatStream::got_privmsg(const char* params, irc_reply_data* ird)
{
	fixup_reply(ird);
printf(">>> IRC msg from %s to %s =%s\n", ird->nick, ird->target, params);

	// Skip over leading colon.
	const char * start = params;
	if (':' == *start) start++;

	// ird->nick is who the message is from.
	// ird->target is who the message is to. It typically has one
	// of two values: the channel name, and so a public message, or
	// my nick, in which case its a private message to me.
	ValueSeq msg;
	msg.push_back(createStringValue(ird->nick));
	msg.push_back(createStringValue(ird->target));

	// Does the message start with the _nick? If so, special case it.
	// This is special handling to make message processing easier.
	if (0 == _nick.compare(0, _nick.size(), start, _nick.size()))
		msg.push_back(createStringValue(
			std::vector<std::string>({_nick, start + _nick.size()})));
	else
		msg.push_back(createStringValue(start));

	ValuePtr svp(createLinkValue(msg));
	push(svp); // concurrent_queue<ValutePtr>::push(svp);
	return 0;
}

int IRChatStream::got_kick(const char* params, irc_reply_data* ird)
{
	fixup_reply(ird);
	printf("got kicked -- input=%s\n", params);
	printf("nick=%s ident=%s host=%s target=%s\n",
	       ird->nick, ird->ident, ird->host, ird->target);
	return 0;
}

// ==============================================================

// This will read one line from the file stream, and return that line.
// So, a line-oriented, buffered interface. For now.
void IRChatStream::update() const
{
	if (nullptr == _conn) { _value.clear(); return; }

	// XXX When is it ever closed ???
	if (is_closed() and 0 == concurrent_queue<ValuePtr>::size()) return;

	// Read one at a time. (???)
	// This will hang, until there's something to read.
	try
	{
		ValuePtr val;
		const_cast<IRChatStream*>(this) -> pop(val);
		_value.resize(1);
		_value[0] = val;
		return;
	}
	catch (typename concurrent_queue<ValuePtr>::Canceled& e)
	{}

	// If we are here, the queue closed up. Should never happen...
}

// ==============================================================

#define CHKNARG(NUM,MSG) \
	if (cmdstrs.size() < NUM) \
		throw RuntimeException(TRACE_INFO, MSG);

/// Deal with IRC commands
/// Expecting structured values:
/// command-name + arguments
void IRChatStream::run_cmd(const std::vector<std::string>& cmdstrs)
{
	if (0 == cmdstrs.size()) return;

	const std::string& cmd = cmdstrs[0];
	if (0 == cmd.compare("PRIVMSG"))
	{
		CHKNARG(3, "Expecting at least three arguments");
		const char* msg_target = cmdstrs[1].c_str();

		if (cmdstrs.size() == 3)
		{
			_conn->privmsg(msg_target, cmdstrs[2].c_str());
			return;
		}

		// Concatenate the rest into a single line.
		std::string msg;
		for (size_t i=2; i< cmdstrs.size(); i++)
			msg += cmdstrs[i];
		_conn->privmsg(msg_target, msg.c_str());
		return;
	}

	if (0 == cmd.compare("JOIN"))
	{
		CHKNARG(2, "Expecting at least two arguments");
		// Channels must always start with hash mark.
		const char* channel = cmdstrs[1].c_str();
		// if ('#' != _channel[0]) _channel = "#" + _channel;
		_conn->join(channel);
		return;
	}

	if (0 == cmd.compare("PART"))
	{
		CHKNARG(2, "Expecting at least two arguments");
		const char* channel = cmdstrs[1].c_str();
		_conn->part(channel);
		return;
	}

	// Anything goes. Raw commands. Server will bitch if we do
	// something wrong.
	_conn->raw(cmd.c_str());
}

// ==============================================================
/// Deal with different kinds of stream formats.
void IRChatStream::write_one(AtomSpace* as, bool silent,
                             const ValuePtr& command_data)
{
	if (command_data->is_type(STRING_VALUE))
	{
		StringValuePtr svp(StringValueCast(command_data));
		run_cmd(svp->value());
		return;
	}

	if (command_data->is_type(LINK_VALUE))
	{
		// Syntactic sugar: unwrap a doubly-wrapped LinkValue
		// These arise "naturally" as a result of filtering,
		// and are a pain in the next to deal with. So we'll
		// just do it "for free" here.
		LinkValuePtr lvp(LinkValueCast(command_data));
		if (lvp->size() == 1 and lvp->value()[0]->is_type(LINK_VALUE))
			lvp = LinkValueCast(lvp->value()[0]);

		// Do it again. Sometimes get double-wrapped! Ugh! WTF.
		if (lvp->size() == 1 and lvp->value()[0]->is_type(LINK_VALUE))
			lvp = LinkValueCast(lvp->value()[0]);

		std::vector<std::string> cmd;
		for (const ValuePtr& vp : lvp->value())
		{
			if (vp->is_type(STRING_VALUE))
			{
				const StringValuePtr& svp(StringValueCast(vp));
				for (const std::string& str : svp->value())
					cmd.push_back(str);
			}
			else
			if (vp->is_node())
				cmd.push_back(HandleCast(vp)->get_name());

			// FalseLink is the result of a no-op. So do nothing.
			else if (FALSE_LINK == vp->get_type())
				return;

			else
				throw RuntimeException(TRACE_INFO,
					"Expecting node or string; got %s\n", vp->to_string().c_str());
		}
		run_cmd(cmd);
		return;
	}

	if (LIST_LINK == command_data->get_type())
	{
		std::vector<std::string> cmd;
		for (const Handle& ho : HandleCast(command_data)->getOutgoingSet())
		{
			if (not ho->is_node())
				throw RuntimeException(TRACE_INFO,
					"Expecting node; got %s\n", ho->to_string().c_str());
			cmd.push_back(ho->get_name());
		}
		run_cmd(cmd);
		return;
	}

	throw RuntimeException(TRACE_INFO,
		"Unsupported data %s\n", command_data->to_string().c_str());
}

// Write stuff to a file.
ValuePtr IRChatStream::write_out(AtomSpace* as, bool silent,
                                 const Handle& cref)
{
	if (nullptr == _conn)
		throw RuntimeException(TRACE_INFO,
			"IRC stream not open: URI \"%s\"\n", _uri.c_str());

	return do_write_out(as, silent, cref);
}

// ==============================================================

// Adds factory when library is loaded.
DEFINE_VALUE_FACTORY(I_R_CHAT_STREAM, createIRChatStream, std::string)
DEFINE_VALUE_FACTORY(I_R_CHAT_STREAM, createIRChatStream, Handle)

// ====================================================================

void opencog_sensory_v0_irc_init(void)
{
	// Force shared lib ctors to run
};