From 27d21b371f24272466a2d6a5bf2e2b717ee2d9fc Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Sun, 27 Feb 2011 22:45:32 +0000 Subject: A start on Swiftob, a Swiften-based chatbot. diff --git a/.project b/.project index 8a42eaa..fcdfcdc 100644 --- a/.project +++ b/.project @@ -18,7 +18,7 @@ org.eclipse.cdt.make.core.autoBuildTarget - Sluift + org.eclipse.cdt.make.core.buildArguments @@ -29,10 +29,6 @@ python - org.eclipse.cdt.make.core.buildLocation - - - org.eclipse.cdt.make.core.cleanBuildTarget -c @@ -54,7 +50,7 @@ org.eclipse.cdt.make.core.fullBuildTarget - Sluift + org.eclipse.cdt.make.core.stopOnError diff --git a/Swiften/MUC/MUC.cpp b/Swiften/MUC/MUC.cpp index b8c23cd..68a5a86 100644 --- a/Swiften/MUC/MUC.cpp +++ b/Swiften/MUC/MUC.cpp @@ -208,6 +208,14 @@ void MUC::handleCreationConfigResponse(MUCOwnerPayload::ref /*unused*/, ErrorPay } } +bool MUC::hasOccupant(const std::string& nick) { + return occupants.find(nick) != occupants.end(); +} + +MUCOccupant MUC::getOccupant(const std::string& nick) { + return occupants.find(nick)->second; +} + //FIXME: Recognise Topic changes //TODO: Invites(direct/mediated) diff --git a/Swiften/MUC/MUC.h b/Swiften/MUC/MUC.h index ef76a6a..278ef95 100644 --- a/Swiften/MUC/MUC.h +++ b/Swiften/MUC/MUC.h @@ -51,7 +51,9 @@ namespace Swift { void handleIncomingMessage(Message::ref message); /** Expose public so it can be called when e.g. user goes offline */ void handleUserLeft(LeavingType); - + /** Get occupant information*/ + MUCOccupant getOccupant(const std::string& nick); + bool hasOccupant(const std::string& nick); public: boost::signal onJoinComplete; boost::signal onJoinFailed; diff --git a/Swiftob/Commands.cpp b/Swiftob/Commands.cpp new file mode 100644 index 0000000..e39d23e --- /dev/null +++ b/Swiftob/Commands.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/Commands.h" + +#include +#include + +#include + +typedef std::pair NamedCommand; + +Commands::Commands(Users* users, Swift::Client* client, Storage* storage, MUCs* mucs) { + users_ = users; + client_ = client; + mucs_ = mucs; + storage_ = storage; + resetCommands(); +} + +void Commands::resetCommands() { + foreach (NamedCommand command, commands_) { + delete command.second; + } + commands_.clear(); + registerCommand("quit", Owner, "Quit the bot", boost::bind(&Commands::handleQuitCommand, this, _1, _2, _3)); + registerCommand("help", Anyone, "Get help", boost::bind(&Commands::handleHelpCommand, this, _1, _2, _3)); + registerCommand("join", Owner, "Join a MUC", boost::bind(&Commands::handleJoinCommand, this, _1, _2, _3)); + registerCommand("part", Owner, "Leave a MUC", boost::bind(&Commands::handlePartCommand, this, _1, _2, _3)); + registerCommand("rehash", Owner, "Reload scripts", boost::bind(&Commands::handleRehashCommand, this, _1, _2, _3)); + onReset(); +} + +void Commands::registerCommand(const std::string& name, RoleList roles, const std::string& description, boost::function callback) { + Command* command = new Command(roles, description); + commands_[name] = command; + command->onReceived.connect(callback); +} + +bool Commands::hasCommand(const std::string& name) { + return commands_.find(name) != commands_.end(); +} + +bool Commands::runCommand(const std::string& name, const std::string& params, Swift::Message::ref message) { + Users::User::Role userRole = users_->getRoleForSender(message); + Command* command = commands_[name]; + if (roleIn(userRole, command->getAllowedBy())) { + command->onReceived(name, params, message); + return true; + } else { + replyTo(message, "You may not run this command", true); + } + return false; +} + +bool Commands::roleIn(const Users::User::Role userRole, RoleList roleList) { + switch (roleList) { + case Owner : return userRole == Users::User::Owner; + case Anyone : return true; + } + std::cerr << "Unrecognised role list" << std::endl; + return false; +} + +void Commands::handleQuitCommand(const std::string& /*command*/, const std::string& /*params*/, Swift::Message::ref message) { + replyTo(message, "Shutting down"); + std::cout << "Quitting at the behest of " << message->getFrom().toString() << std::endl; + exit(0); +} + +void Commands::setRehashError(const std::string& error) { + if (!rehashError_.empty()) { + rehashError_ += "; "; + } + rehashError_ += error; +} + +void Commands::handleRehashCommand(const std::string& /*command*/, const std::string& /*params*/, Swift::Message::ref message) { + rehashError_ = ""; + replyTo(message, "Rehashing now."); + std::cout << "Rehashing at the behest of " << message->getFrom().toString() << std::endl; + resetCommands(); + if (rehashError_.empty()) { + replyTo(message, "Rehash complete"); + } else { + replyTo(message, "I have suffered a tremendous failure: " + rehashError_); + } +} + +void Commands::handleJoinCommand(const std::string& /*command*/, const std::string& params, Swift::Message::ref message) { + Swift::JID room(params); + if (!room.isValid() || !room.getResource().empty() || room.getNode().empty()) { + replyTo(message, "Can't join " + room.toString() + ", not a valid room JID."); + return; + } + if (mucs_->contains(room)) { + replyTo(message, "I'm already (trying to be?) in " + room.toString() + "."); + return; + } + replyTo(message, "Trying to join " + room.toString() + "."); + mucs_->join(room, boost::bind(&Commands::handleJoinCommandSuccess, this, room, message), boost::bind(&Commands::handleJoinCommandFailure, this, room, _1, message)); +} + +void Commands::handlePartCommand(const std::string& /*command*/, const std::string& params, Swift::Message::ref message) { + Swift::JID room(params); + if (!room.isValid() || !room.getResource().empty() || room.getNode().empty()) { + replyTo(message, "Can't leave " + room.toString() + ", not a valid room JID."); + return; + } + if (mucs_->contains(room)) { + replyTo(message, "I'm not in " + room.toString() + "."); + return; + } + replyTo(message, "Leaving " + room.toString() + "."); + mucs_->part(room); +} + +void Commands::handleJoinCommandSuccess(const Swift::JID& room, Swift::Message::ref message) { + replyTo(message, "Joined " + room.toString()); +} + +void Commands::handleJoinCommandFailure(const Swift::JID& room, const std::string& error, Swift::Message::ref message) { + replyTo(message, "Join to " + room.toString() + "failed. " + error); +} + +void Commands::handleHelpCommand(const std::string& /*command*/, const std::string& /*params*/, Swift::Message::ref message) { + Users::User::Role userRole = users_->getRoleForSender(message); + std::string result("Available commands:"); + std::string bang = message->getType() == Swift::Message::Groupchat ? "\n!" : "\n"; + foreach (NamedCommand pair, commands_) { + if (roleIn(userRole, pair.second->getAllowedBy())) { + result += bang + pair.first + " - " + pair.second->getDescription(); + } + } + replyTo(message, result, true); +} + +/** + * \param outOfMUC Reply to the sender directly, don't spam MUCs with the reply + */ +void Commands::replyTo(Swift::Message::ref source, std::string replyBody, bool outOfMUC) { + Swift::Message::ref reply(new Swift::Message()); + Swift::Message::Type type = source->getType(); + reply->setType(type); + reply->setBody(type == Swift::Message::Groupchat ? source->getFrom().getResource() + ": " + replyBody : replyBody); + Swift::JID to = source->getFrom(); + if (type == Swift::Message::Groupchat) { + if (outOfMUC) { + reply->setType(Swift::Message::Chat); + } else { + to = to.toBare(); + } + } + reply->setTo(to); + if (client_->isAvailable()) { + client_->sendMessage(reply); + } else { + std::cout << "Dropping '" + reply->getBody() + "' -> " + reply->getTo().toString() + " on the floor due to missing connection." << std::endl; + } +} diff --git a/Swiftob/Commands.h b/Swiftob/Commands.h new file mode 100644 index 0000000..e11078d --- /dev/null +++ b/Swiftob/Commands.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +#include +#include + +#include "Swiftob/Users.h" +#include "Swiftob/MUCs.h" + +namespace Swift { + class Client; +} + +class Storage; +class Commands { + public: + enum RoleList {Anyone, Owner}; + public: + class Command { + public: + Command(RoleList allowedBy, const std::string& description) : allowedBy_(allowedBy), description_(description) { + } + + virtual ~Command() {}; + + boost::signal onReceived; + RoleList getAllowedBy() {return allowedBy_;} + std::string getDescription() {return description_;} + private: + RoleList allowedBy_; + std::string description_; + }; + + public: + Commands(Users* users, Swift::Client* client, Storage* storage, MUCs* mucs); + bool hasCommand(const std::string&); + bool runCommand(const std::string& command, const std::string& params, Swift::Message::ref message); + void replyTo(Swift::Message::ref source, std::string replyBody, bool outOfMUC = false); + void registerCommand(const std::string& name, RoleList roles, const std::string& description, boost::function callback); + void resetCommands(); + void setRehashError(const std::string& error); + + public: + boost::signal onReset; + private: + bool roleIn(const Users::User::Role userRole, RoleList roles); + void handleQuitCommand(const std::string& command, const std::string& params, Swift::Message::ref message); + void handleHelpCommand(const std::string& command, const std::string& params, Swift::Message::ref message); + void handleJoinCommand(const std::string& /*command*/, const std::string& params, Swift::Message::ref message); + void handleJoinCommandSuccess(const Swift::JID& room, Swift::Message::ref message); + void handleJoinCommandFailure(const Swift::JID& room, const std::string& error, Swift::Message::ref message); + void handlePartCommand(const std::string& /*command*/, const std::string& params, Swift::Message::ref message); + void handleRehashCommand(const std::string& command, const std::string& params, Swift::Message::ref message); + private: + std::map commands_; + Users* users_; + Swift::Client* client_; + Storage* storage_; + MUCs* mucs_; + std::string rehashError_; +}; + + diff --git a/Swiftob/LuaCommands.cpp b/Swiftob/LuaCommands.cpp new file mode 100644 index 0000000..7be818e --- /dev/null +++ b/Swiftob/LuaCommands.cpp @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/LuaCommands.h" + +#include +#include +#include + +#include +#include + +#include "Swiftob/Commands.h" + +#define LUA_COMMANDS "__Lua_Commands" +#define STORAGE "__Storage" + +LuaCommands::LuaCommands(Commands* commands, const std::string& path, Client* client, TimerFactory* timerFactory, MUCs* mucs) : path_(path), scriptsPath_(boost::filesystem::path(path_) / "scripts") { + commands_ = commands; + client_ = client; + timerFactory_ = timerFactory; + mucs_ = mucs; + commands_->onReset.connect(boost::bind(&LuaCommands::registerCommands, this)); + registerCommands(); +} + +void LuaCommands::registerCommands() { + std::cout << "Trying to load all scripts in " << scriptsPath_ << std::endl; + if (boost::filesystem::exists(scriptsPath_) && boost::filesystem::is_directory(scriptsPath_)) { + std::vector files; + copy(boost::filesystem::directory_iterator(scriptsPath_), boost::filesystem::directory_iterator(), std::back_inserter(files)); + foreach (boost::filesystem::path file, files) { + if (boost::filesystem::is_regular_file(file) && file.extension() == ".lua") { + loadScript(file); + } + } + } +} + +static int l_register_command(lua_State *L) { + LuaCommands* commands = NULL; + lua_getfield(L, LUA_REGISTRYINDEX, LUA_COMMANDS); + commands = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + if (!lua_isfunction(L, 4)) { + return luaL_error(L, "register_command callback parameter must be a function"); + } + //luaL_ref callback(lua_to(L, 4)); + lua_pushvalue(L, 4); + int callbackIndex = luaL_ref(L, LUA_REGISTRYINDEX); + lua_pop(L, 1); + + if (!lua_isstring(L, 3)) { + return luaL_error(L, "register_command description parameter must be a string"); + } + std::string description(lua_tostring(L, 3)); + lua_pop(L, 1); + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "register_command allowed roles parameter must be a string"); + } + std::string roleString(lua_tostring(L, 2)); + lua_pop(L, 1); + Commands::RoleList roleList = Commands::Owner; + if (roleString == "Owner") { + roleList = Commands::Owner; + } else if (roleString == "Anyone") { + roleList = Commands::Anyone; + } else { + return luaL_error(L, "register_command allowed roles parameter has illegal value"); + } + if (!lua_isstring(L, 1)) { + return luaL_error(L, "register_command command name parameter must be a string"); + } + std::string name(lua_tostring(L, 1)); + lua_pop(L, 1); + std::cout << "Registering lua command '" << name << "' for '" << roleString << "' with callback index " << callbackIndex << std::endl; + commands->getCommands()->registerCommand(name, roleList, description, boost::bind(&LuaCommands::handleLuaCommand, commands, callbackIndex, L, _1, _2, _3)); + + return 0; +} + +static std::string luatable_asstring(lua_State *L, const char* key) { + lua_getfield(L, -1, key); + const char* valueChars = lua_tostring(L, -1); + std::string value(valueChars != NULL ? valueChars : ""); + lua_pop(L, 1); + return value; +} + +static int luatable_asint(lua_State *L, const char* key) { + lua_getfield(L, -1, key); + int value = lua_tointeger(L, -1); + lua_pop(L, 1); + return value; +} + +static int luatable_asfunction(lua_State *L, const char* key) { + lua_getfield(L, -1, key); + int callbackIndex = luaL_ref(L, LUA_REGISTRYINDEX); + return callbackIndex; +} + +static Message::ref messageFromTable(lua_State *L) { + Message::ref message(new Message()); + message->setFrom(JID(luatable_asstring(L, "from"))); + message->setBody(luatable_asstring(L, "body")); + Message::Type type = Message::Normal; + std::string typeString(luatable_asstring(L, "type")); + if (typeString == "normal") { + type = Message::Normal; + } else if (typeString == "chat") { + type = Message::Chat; + } else if (typeString == "groupchat") { + type = Message::Groupchat; + } else if (typeString == "error") { + type = Message::Error; + } else if (typeString == "headline") { + type = Message::Headline; + } else { + return Message::ref(); + } + message->setType(type); + return message; +} + +LuaCommands* LuaCommands::commandsFromLua(lua_State *L) { + LuaCommands* commands = NULL; + lua_getfield(L, LUA_REGISTRYINDEX, LUA_COMMANDS); + commands = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return commands; +} + +Storage* LuaCommands::storageFromLua(lua_State *L) { + Storage* storage = NULL; + lua_getfield(L, LUA_REGISTRYINDEX, STORAGE); + storage = static_cast(lua_touserdata(L, -1)); + lua_pop(L, 1); + return storage; +} + +static int l_reply_to(lua_State *L) { + LuaCommands* commands = LuaCommands::commandsFromLua(L); + + if (!lua_isboolean(L, 3) && lua_gettop(L) > 2) { + return luaL_error(L, "reply_to parameter 3 must be boolean if present"); + } + bool outOfMUC = lua_toboolean(L, 3); + if (lua_gettop(L) == 3) { + lua_pop(L, 1); + } + + if (!lua_isstring(L, 2)) { + return luaL_error(L, "reply_to body parameter must be a string"); + } + std::string body(lua_tostring(L, 2)); + lua_pop(L, 1); + + if (!lua_istable(L, 1)) { + return luaL_error(L, "reply_to message parameter must be a table"); + } + lua_pushvalue(L, 1); + Message::ref message(messageFromTable(L)); + if (!message) { + return luaL_error(L, "message parameter invalid"); + } + commands->getCommands()->replyTo(message, body, outOfMUC); + lua_pop(L, 1); + + return 0; +} + +static int l_muc_input_to_jid(lua_State *L) { + LuaCommands* commands = LuaCommands::commandsFromLua(L); + return commands->muc_input_to_jid(L); +} + +int LuaCommands::muc_input_to_jid(lua_State *L) { + if (!lua_isstring(L, 2)) { + return luaL_error(L, "must pass a string to muc_input_to_jid p2"); + } + std::string source = lua_tostring(L, 2); + JID sourceJID(source); + lua_pop(L, 1); + if (!lua_isstring(L, 1)) { + return luaL_error(L, "must pass a string to muc_input_to_jid p1"); + } + std::string input = lua_tostring(L, 1); + lua_pop(L, 1); + JID result(input); + if (mucs_->contains(sourceJID.toBare())) { + if (result.isBare() && result.getNode().empty()) { + if (mucs_->getMUC(sourceJID.toBare())->hasOccupant(input)) { + result = JID(sourceJID.getNode(), sourceJID.getDomain(), input); + } + } + } + + lua_pushstring(L, result.isValid() ? result.toString().c_str() : ""); + return 1; +} + +void LuaCommands::handleSoftwareVersionResponse(boost::shared_ptr version, ErrorPayload::ref error, bool timeout, GetSoftwareVersionRequest::ref request, Timer::ref timer, lua_State* L, Callbacks callbacks) { + request->onResponse.disconnect_all_slots(); + timer->onTick.disconnect_all_slots(); + timer->stop(); + int callback = callbacks.failure; + int stackCount = 0; + if (timeout) { + callback = callbacks.timeout; + } else if (version) { + callback = callbacks.success; + } + lua_rawgeti(L, LUA_REGISTRYINDEX, callback); + if (error) { + lua_pushstring(L, error->getText().empty() ? "No error text" : error->getText().c_str()); + stackCount++; + } + else if (version) { + lua_createtable(L, 0, 3); + lua_pushstring(L, version->getName().c_str()); + lua_setfield(L, -2, "name"); + lua_pushstring(L, version->getVersion().c_str()); + lua_setfield(L, -2, "version"); + lua_pushstring(L, version->getOS().c_str()); + lua_setfield(L, -2, "os"); + stackCount++; + } + else { + lua_pushliteral(L, "Missing payload"); + stackCount++; + } + int result = lua_pcall(L, stackCount, 0, 0); + if (result != 0) { + std::string error(lua_tostring(L, -1)); + lua_pop(L, 1); + std::cout << error << std::endl; + callbacks.erase(L); + luaL_error(L, error.c_str()); + } else { + callbacks.erase(L); + } + +} + +static int l_get_software_version(lua_State *L) { + LuaCommands* commands = LuaCommands::commandsFromLua(L); + return commands->get_software_version(L); +} + +int LuaCommands::get_software_version(lua_State *L) { + if (!lua_istable(L, 1)) { + return luaL_error(L, "get_software_version requires a table parameter."); + } + lua_pushvalue(L, 1); + JID to(luatable_asstring(L, "to")); + if (!to.isValid()) { + return luaL_error(L, "invalid JID."); + } + int timeout = luatable_asint(L, "timeout"); + if (timeout == 0) { + return luaL_error(L, "invalid timeout."); + } + + int successCallback = luatable_asfunction(L, "success_callback"); + int failureCallback = luatable_asfunction(L, "failure_callback"); + int timeoutCallback = luatable_asfunction(L, "timeout_callback"); + GetSoftwareVersionRequest::ref request = GetSoftwareVersionRequest::create(to, client_->getIQRouter()); + Timer::ref timer = timerFactory_->createTimer(timeout * 1000); + Callbacks callbacks(successCallback, failureCallback, timeoutCallback); + request->onResponse.connect(boost::bind(&LuaCommands::handleSoftwareVersionResponse, this, _1, _2, false, request, timer, L, callbacks)); + boost::shared_ptr fakePayload; + ErrorPayload::ref fakeError; + timer->onTick.connect(boost::bind(&LuaCommands::handleSoftwareVersionResponse, this, fakePayload, fakeError, true, request, timer, L, callbacks)); + timer->start(); + request->send(); + return 1; +} + +static int l_store_setting(lua_State *L) { + return LuaCommands::commandsFromLua(L)->store_setting(L); +} + +static int l_get_setting(lua_State *L) { + return LuaCommands::commandsFromLua(L)->get_setting(L); +} + +int LuaCommands::store_setting(lua_State *L) { + if (!lua_isstring(L, 2) || !lua_isstring(L, 1)) { + return luaL_error(L, "both setting and key must be strings"); + } + std::string value(lua_tostring(L, 2)); + std::string key(lua_tostring(L, 1)); + lua_pop(L, 2); + storageFromLua(L)->saveSetting(key, value); + return 0; +} + +int LuaCommands::get_setting(lua_State *L) { + if (!lua_isstring(L, 1)) { + return luaL_error(L, "key must be a string"); + } + std::string key(lua_tostring(L, 1)); + lua_pop(L, 1); + lua_pushstring(L, storageFromLua(L)->getSetting(key).c_str()); + return 1; + +} + +void LuaCommands::handleLuaCommand(int callbackIndex, lua_State* L, const std::string& command, const std::string& params, Swift::Message::ref message) { + lua_rawgeti(L, LUA_REGISTRYINDEX, callbackIndex); + lua_pushstring(L, command.c_str()); + lua_pushstring(L, params.c_str()); + messageOntoStack(message, L); + int result = lua_pcall(L, 3, 0, 0); + if (result != 0) { + std::string error(lua_tostring(L, -1)); + lua_pop(L, 1); + error = "Command '" + command + "' failed: " + error; + std::cout << error << std::endl; + commands_->replyTo(message, error, false); + } +} + +void LuaCommands::messageOntoStack(Swift::Message::ref message, lua_State* L) { + lua_createtable(L, 0, 4); + std::string typeString; + switch (message->getType()) { + case Message::Chat : typeString = "chat";break; + case Message::Groupchat : typeString = "groupchat";break; + case Message::Normal : typeString = "normal";break; + case Message::Error : typeString = "error";break; + case Message::Headline : typeString = "headline";break; + } + lua_pushstring(L, typeString.c_str()); + lua_setfield(L, -2, "type"); + lua_pushstring(L, message->getFrom().toString().c_str()); + lua_setfield(L, -2, "from"); + lua_pushstring(L, message->getFrom().toBare().toString().c_str()); + lua_setfield(L, -2, "frombare"); + lua_pushstring(L, message->getTo().toString().c_str()); + lua_setfield(L, -2, "to"); + lua_pushstring(L, message->getBody().c_str()); + lua_setfield(L, -2, "body"); +} + +void LuaCommands::loadScript(boost::filesystem::path filePath) { + std::cout << "Trying to load file from " << filePath << std::endl; + lua_State* lua = lua_open(); + luaL_openlibs(lua); + lua_pushlightuserdata(lua, this); + lua_setfield(lua, LUA_REGISTRYINDEX, LUA_COMMANDS); + std::string filename(filePath.filename()); + filename += ".storage"; + boost::filesystem::path storagePath(boost::filesystem::path(path_) / filename); + Storage* storage = new Storage(storagePath); + lua_pushlightuserdata(lua, storage); + lua_setfield(lua, LUA_REGISTRYINDEX, STORAGE); + lua_register(lua, "swiftob_register_command", &l_register_command); + lua_register(lua, "swiftob_reply_to", &l_reply_to); + lua_register(lua, "swiftob_get_software_version", &l_get_software_version); + lua_register(lua, "swiftob_muc_input_to_jid", &l_muc_input_to_jid); + lua_register(lua, "swiftob_store_setting", &l_store_setting); + lua_register(lua, "swiftob_get_setting", &l_get_setting); + int fileLoaded = luaL_dofile(lua, filePath.string().c_str()); + if (fileLoaded == 0 ) { + std::cout << "Loaded" << std::endl; + } else { + const char* error = lua_tostring(lua, -1); + std::cout << "Error: " << error << std::endl; + lua_pop(lua, -1); + } +} diff --git a/Swiftob/LuaCommands.h b/Swiftob/LuaCommands.h new file mode 100644 index 0000000..dc8e36e --- /dev/null +++ b/Swiftob/LuaCommands.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +extern "C" { +#include +#include +#include +} + +#include +#include +#include "Swiften/Network/NetworkFactories.h" +#include +#include +#include + +#include "Swiftob/Commands.h" +#include "Swiftob/Storage.h" + +using namespace Swift; +/** + * Yes, there's an odd naming scheme going on here for methods. + * normalCamelCase methods are methods called from C++ + * lower_case_methods are exposed to Lua through wrappers and + * l_lower_case_functions are functions directly exposed (often calling the lower_case_methods). + */ +class LuaCommands { + public: + class LuaCommand : public Commands::Command, boost::noncopyable { + public: + /** Takes ownership of lua and storage.*/ + LuaCommand(Commands::RoleList allowedBy, const std::string& description, lua_State* lua, Storage* storage) : Command(allowedBy, description), lua_(lua), storage_(storage) { + } + + virtual ~LuaCommand() { + lua_close(lua_); + delete storage_; + }; + + private: + lua_State* lua_; + Storage* storage_; + }; + + class Callbacks { + public: + Callbacks(int success, int failure, int timeout) : success(success), failure(failure), timeout(timeout) {}; + int success; + int failure; + int timeout; + void erase(lua_State *L) { + lua_pushnil(L); + lua_rawseti(L, LUA_REGISTRYINDEX, success); + lua_pushnil(L); + lua_rawseti(L, LUA_REGISTRYINDEX, failure); + lua_pushnil(L); + lua_rawseti(L, LUA_REGISTRYINDEX, timeout); + } + }; + + LuaCommands(Commands* commands, const std::string& path, Client* client, TimerFactory* timerFactory, MUCs* mucs); + /* Public but isn't really part of the API */ + void handleLuaCommand(int callbackIndex, lua_State* L, const std::string& command, const std::string& params, Message::ref message); + Commands* getCommands() {return commands_;} + int get_software_version(lua_State *L); + int muc_input_to_jid(lua_State *L); + int store_setting(lua_State *L); + int get_setting(lua_State *L); + static LuaCommands* commandsFromLua(lua_State *L); + static Storage* storageFromLua(lua_State *L); + private: + void registerCommands(); + void loadScript(boost::filesystem::path filePath); + void messageOntoStack(Message::ref message, lua_State* L); + void handleSoftwareVersionResponse(boost::shared_ptr version, ErrorPayload::ref error, bool timeout, GetSoftwareVersionRequest::ref request, Timer::ref timer, lua_State* L, Callbacks callbacks); + private: + std::string path_; + boost::filesystem::path scriptsPath_; + Commands* commands_; + MUCs* mucs_; + Client* client_; + TimerFactory* timerFactory_; +}; + + diff --git a/Swiftob/MUCs.cpp b/Swiftob/MUCs.cpp new file mode 100644 index 0000000..55bf313 --- /dev/null +++ b/Swiftob/MUCs.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/MUCs.h" + +#include +#include +#include +#include + +#include "Swiftob/Storage.h" + +#define MUC_LIST_SETTING "muc_list" + +typedef std::pair JIDMUCPair; + +MUCs::MUCs(Client* client, Storage* storage) : defaultNick_("Kanchil+") { + client_ = client; + storage_ = storage; + client_->onConnected.connect(boost::bind(&MUCs::handleConnected, this)); +} + +void MUCs::handleConnected() { + foreach (std::string room, String::split(storage_->getSetting(MUC_LIST_SETTING), ' ')) { + join(JID(room), boost::bind(&MUCs::handleInitialJoinSuccess, this), boost::bind(&MUCs::handleInitialJoinFailure, this, _1)); + } +} + +void MUCs::handleInitialJoinSuccess() { + +} + +void MUCs::handleInitialJoinFailure(const std::string&) { + +} + +void MUCs::join(const JID& room, boost::signal::slot_type successCallback, boost::function failureCallback) { + if (contains(room)) { + failureCallback("Already in room"); + } + mucs_[room] = client_->getMUCManager()->createMUC(room); + mucs_[room]->onJoinComplete.connect(successCallback); + mucs_[room]->onJoinFailed.connect(boost::bind(&MUCs::handleJoinFailed, this, room, _1, failureCallback)); + mucs_[room]->joinWithContextSince(defaultNick_, boost::posix_time::microsec_clock::universal_time()); + save(); +} + +void MUCs::part(const JID& room) { + if (!contains(room)) { + return; + } + mucs_[room]->part(); +} + +bool MUCs::contains(const JID& room) { + return mucs_.find(room) != mucs_.end(); +} + +void MUCs::handleJoinFailed(const JID& muc, ErrorPayload::ref error, boost::function failureCallback) { + std::string errorMessage("Couldn't join room"); + if (error) { + switch (error->getCondition()) { + case ErrorPayload::Conflict: + // rejoinNick = nick_ + "_"; + errorMessage += ": Nickname in use"; + // errorMessage = str(format("Unable to enter this room as %1%, retrying as %2%") % nick_ % rejoinNick); + break; + case ErrorPayload::JIDMalformed: + errorMessage += ": "; + errorMessage += "No nickname specified"; + break; + case ErrorPayload::NotAuthorized: + errorMessage += ": "; + errorMessage += "A password needed"; + break; + case ErrorPayload::RegistrationRequired: + errorMessage += ": "; + errorMessage += "Only members may enter"; + break; + case ErrorPayload::Forbidden: + errorMessage += ": "; + errorMessage += "You are banned from the room"; + break; + case ErrorPayload::ServiceUnavailable: + errorMessage += ": "; + errorMessage += "The room is full"; + break; + case ErrorPayload::ItemNotFound: + errorMessage += ": "; + errorMessage += "The room does not exist"; + break; + default: + break; + } + if (!error->getText().empty()) { + errorMessage += " - " + error->getText(); + } + } + mucs_.erase(muc); + failureCallback(errorMessage); +} + +void MUCs::save() { + std::string concat; + foreach (JIDMUCPair pair, mucs_) { + concat += pair.first.toString() + " "; + } + storage_->saveSetting(MUC_LIST_SETTING, concat); +} + +MUC::ref MUCs::getMUC(const JID& room) { + return (mucs_.find(room) != mucs_.end()) ? mucs_[room] : MUC::ref(); +} diff --git a/Swiftob/MUCs.h b/Swiftob/MUCs.h new file mode 100644 index 0000000..e727ec2 --- /dev/null +++ b/Swiftob/MUCs.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace Swift { +class Client; +class MUC; +} + +class Storage; + +using namespace Swift; + +class MUCs { + public: + MUCs(Client* client, Storage* storage); + void join(const JID& room, boost::signal::slot_type successCallback, boost::function failureCallback); + void part(const JID& room); + bool contains(const JID& room); + MUC::ref getMUC(const JID& room); + private: + void handleConnected(); + void handleJoinFailed(const JID& room, ErrorPayload::ref error, boost::function failureCallback); + void handleInitialJoinSuccess(); + void handleInitialJoinFailure(const std::string&); + void save(); + private: + MUCRegistry registry_; + std::map mucs_; + Client* client_; + Storage* storage_; + std::string defaultNick_; +}; + + diff --git a/Swiftob/SConscript b/Swiftob/SConscript new file mode 100644 index 0000000..2139822 --- /dev/null +++ b/Swiftob/SConscript @@ -0,0 +1,26 @@ +Import("env") + + +if env["SCONS_STAGE"] == "build": + myenv = env.Clone() + myenv.MergeFlags(myenv["SWIFTEN_FLAGS"]) + myenv.MergeFlags(myenv["LIBIDN_FLAGS"]) + myenv.MergeFlags(myenv["BOOST_FLAGS"]) + myenv.MergeFlags(myenv["ZLIB_FLAGS"]) + myenv.MergeFlags(myenv["OPENSSL_FLAGS"]) + myenv.MergeFlags(myenv.get("SQLITE_FLAGS", {})) + myenv.MergeFlags(myenv.get("LIBXML_FLAGS", "")) + myenv.MergeFlags(myenv.get("EXPAT_FLAGS", "")) + myenv.MergeFlags(myenv["PLATFORM_FLAGS"]) + myenv.MergeFlags(myenv["LUA_FLAGS"]) + sources = [ + "linit.c", # This is horrible! + "Swiftob.cpp", + "Users.cpp", + "Commands.cpp", + "MUCs.cpp", + "Storage.cpp", + "LuaCommands.cpp", + "main.cpp" + ] + swiftob = myenv.Program("swiftob", sources) \ No newline at end of file diff --git a/Swiftob/Storage.cpp b/Swiftob/Storage.cpp new file mode 100644 index 0000000..0cf16d7 --- /dev/null +++ b/Swiftob/Storage.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/Storage.h" + +#include +#include +#include + +typedef std::pair Strings; + +Storage::Storage(const std::string& path) : settingsPath_(boost::filesystem::path(path)) { + load(); +} + +Storage::Storage(const boost::filesystem::path& path) : settingsPath_(path) { + load(); +} + +void Storage::load() { + if (boost::filesystem::exists(settingsPath_)) { + Swift::ByteArray data; + data.readFromFile(settingsPath_.string()); + foreach (std::string line, Swift::String::split(data.toString(), '\n')) { + std::pair pair = Swift::String::getSplittedAtFirst(line, '\t'); + settings_[pair.first] = pair.second; + } + } +} + +void Storage::saveSetting(const std::string& setting, const std::string& value) { + settings_[setting] = value; + std::string settingsString; + foreach(Strings pair, settings_) { + settingsString += pair.first + '\t' + pair.second + '\n'; + } + boost::filesystem::ofstream file(settingsPath_); + file << settingsString; + file.close(); +} + +std::string Storage::getSetting(const std::string& setting) { + return settings_[setting]; +} diff --git a/Swiftob/Storage.h b/Swiftob/Storage.h new file mode 100644 index 0000000..c4181d8 --- /dev/null +++ b/Swiftob/Storage.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +#include +#include + +class Storage { + public: + Storage(const std::string& path); + Storage(const boost::filesystem::path& path); + void saveSetting(const std::string& setting, const std::string& value); + std::string getSetting(const std::string& setting); + private: + void load(); + std::map settings_; + std::string path_; + boost::filesystem::path settingsPath_; +}; + + diff --git a/Swiftob/Swiftob.cpp b/Swiftob/Swiftob.cpp new file mode 100644 index 0000000..6c6dad0 --- /dev/null +++ b/Swiftob/Swiftob.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/Swiftob.h" + +#include +#include +#include + +#include +#include +#include + +#include "Swiftob/Users.h" +#include "Swiftob/Storage.h" + + +po::options_description Swiftob::getOptionsDescription() { + po::options_description result("Options"); + result.add_options() + ("path", po::value(), "Configuration folder") + ("help", "produce help message") + ("init", "Reset everything (Really, everything, be careful, you only want to use this on first run).") + ("jid", po::value(), "JID to use") + ("password", po::value(), "password") + ("initial-owner", po::value(), "Initial bot owner (JID)") + ; + return result; +} + +Swiftob::Swiftob(const po::variables_map& options) : options_(options), networkFactories_(&eventLoop_), quitting_(false) { + std::string path; + path = options["path"].as(); + client_ = new Swift::Client(Swift::JID(options["jid"].as()), options["password"].as(), &networkFactories_); + storage_ = new Storage(boost::filesystem::path(path) / "settings.txt"); + mucs_ = new MUCs(client_, storage_); + users_ = new Users(client_, mucs_); + commands_ = new Commands(users_, client_, storage_, mucs_); + lua_ = new LuaCommands(commands_, path, client_, networkFactories_.getTimerFactory(), mucs_); + client_->onConnected.connect(boost::bind(&Swiftob::handleConnected, this)); + client_->onDisconnected.connect(boost::bind(&Swiftob::handleDisconnected, this, _1)); + client_->onMessageReceived.connect(boost::bind(&Swiftob::handleMessageReceived, this, _1)); + if (options_.count("init") > 0) { + + } else if (options_.count("jid") > 0 || options_.count("password") > 0 || options_.count("initial-owner") == 0) { + std::cout << "Ignoring initial config options without --initial" << std::endl; + } + client_->setAlwaysTrustCertificates(); + client_->setSoftwareVersion("Swiftob", "pregit", ""); + client_->connect(); + eventLoop_.run(); +} + +void Swiftob::handleConnected() { + std::cout << "Connected" << std::endl; + if (options_.count("init") > 0) {}{ /* FIXME: Not ready for persistence yet*/ + users_->clearAll(); + users_->addUser(Users::User(Swift::JID(options_["initial-owner"].as()), Users::User::Owner)); + } + Swift::Presence::ref presence(new Swift::Presence()); + presence->setStatus("Online and botty"); + client_->getPresenceSender()->sendPresence(presence); +} + +void Swiftob::handleDisconnected(const boost::optional& /*error*/) { + std::cout << "Disconnected" << std::endl; + /* FIXME: check if last connect was more than a minute ago. If so, go ahead and connect, if not then wait a minute before connecting.*/ + if (quitting_) { + eventLoop_.stop(); + } else { + client_->connect(); + } +} + +void Swiftob::handleMessageReceived(Swift::Message::ref message) { + Swift::Message::Type type = message->getType(); + if (type == Swift::Message::Error || type == Swift::Message::Headline) { + std::cout << "Ignoring typed message" << std::endl; + return; + } + std::string body = message->getBody(); + std::cout << "Got message with body " << body << std::endl; + if (body.size() == 0) { + std::cout << "Not handling empty body" << std::endl; + return; + } + /*Convert body into !command if it's not a MUC, and it misses the bang*/ + std::string bangBody(body); + if (type != Swift::Message::Groupchat && body[0] != '!') { + bangBody = "!" + body; + } + std::cout << "After banging, body is " << bangBody << std::endl; + std::pair split = Swift::String::getSplittedAtFirst(bangBody, ' '); + std::string commandName(split.first); + commandName = Swift::String::getSplittedAtFirst(commandName, '!').second; + /*FIXME: remove leading bang in commandName*/ + if (commands_->hasCommand(commandName)) { + std::cout << "Matched command " << commandName << std::endl; + commands_->runCommand(commandName, split.second, message); + } +} + +Swiftob::~Swiftob() { + delete lua_; + delete commands_; + delete storage_; + delete users_; + delete client_; +} + +int Swiftob::exec() { + return 0; +} + diff --git a/Swiftob/Swiftob.h b/Swiftob/Swiftob.h new file mode 100644 index 0000000..45061d0 --- /dev/null +++ b/Swiftob/Swiftob.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "Swiftob/Commands.h" +#include "Swiftob/LuaCommands.h" + +namespace po = boost::program_options; + +class Users; +class Storage; +class Swiftob { + public: + Swiftob(const po::variables_map& options); + static po::options_description getOptionsDescription(); + int exec(); + ~Swiftob(); + private: + void handleConnected(); + void handleDisconnected(const boost::optional&); + void handleMessageReceived(Swift::Message::ref); + private: + const po::variables_map options_; + Swift::SimpleEventLoop eventLoop_; + Swift::BoostNetworkFactories networkFactories_; + Commands* commands_; + LuaCommands* lua_; + Storage* storage_; + MUCs* mucs_; + bool quitting_; + Users* users_; + Swift::Client* client_; +}; diff --git a/Swiftob/Users.cpp b/Swiftob/Users.cpp new file mode 100644 index 0000000..55ba4eb --- /dev/null +++ b/Swiftob/Users.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include "Swiftob/Users.h" + +#include + +#include + +#include "Swiftob/MUCs.h" + +Users::Users(Client* client, MUCs* mucs) { + client_ = client; + mucs_ = mucs; +} + +/* TODO: Store in roster */ +void Users::clearAll() { + users_.clear(); +} + +void Users::addUser(const User& user) { + users_.push_back(user); +} + +Users::User::Role Users::getRoleForSender(Message::ref message) { + JID jid = message->getFrom(); + MUC::ref muc = mucs_->getMUC(message->getFrom().toBare()); + if (muc && muc->hasOccupant(message->getFrom().getResource())) { + MUCOccupant occupant = muc->getOccupant(message->getFrom().getResource()); + if (occupant.getRealJID()) { + jid = occupant.getRealJID().get(); + } + } + foreach (User user, users_) { + if (user.getJID().equals(jid.toBare(), JID::WithoutResource)) { + return user.getRole(); + } + } + return User::Unknown; +} + diff --git a/Swiftob/Users.h b/Swiftob/Users.h new file mode 100644 index 0000000..0acc330 --- /dev/null +++ b/Swiftob/Users.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace Swift { +class Client; +} + +class MUCs; + +using namespace Swift; + +class Users { + public: + class User { + public: + /* If you add a role here, edit the role lists in Commands.cpp*/ + enum Role {Unknown, Owner}; + User(const JID& jid, Role role) : jid_(jid), role_(role) {} + Role getRole() {return role_;} + Swift::JID getJID() {return jid_;} + private: + Swift::JID jid_; + Role role_; + }; + + public: + Users(Client* client, MUCs* mucs); + void clearAll(); + void addUser(const User& user); + User::Role getRoleForSender(Message::ref message); + + private: + std::vector users_; + Client* client_; + MUCs* mucs_; +}; + + diff --git a/Swiftob/linit.c b/Swiftob/linit.c new file mode 100644 index 0000000..13c5b09 --- /dev/null +++ b/Swiftob/linit.c @@ -0,0 +1 @@ +#include "../3rdParty/Lua/src/linit.c" diff --git a/Swiftob/main.cpp b/Swiftob/main.cpp new file mode 100644 index 0000000..9908b45 --- /dev/null +++ b/Swiftob/main.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2011 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include +#include +#include +#include +#include + +#include "Swiftob.h" + +int main(int argc, char* argv[]) { + boost::program_options::options_description desc = Swiftob::getOptionsDescription(); + boost::program_options::variables_map vm; + try { + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm); + } catch (boost::program_options::unknown_option option) { +#if BOOST_VERSION >= 104200 + std::cout << "Ignoring unknown option " << option.get_option_name() << " but continuing." << std::endl; +#else + std::cout << "Error: " << option.what() << " (continuing)" << std::endl; +#endif + } + boost::program_options::notify(vm); + if (vm.count("help") > 0) { + std::cout << desc << "\n"; + return 1; + } + + Swiftob bot(vm); + int result = bot.exec(); + + return result; +} diff --git a/Swiftob/scripts/agenda.lua b/Swiftob/scripts/agenda.lua new file mode 100644 index 0000000..897b89c --- /dev/null +++ b/Swiftob/scripts/agenda.lua @@ -0,0 +1,94 @@ +agendas = {} +currents = {} + +function full_agenda(from) + fullagenda = {} + fullagenda[1] = "Roll call" + fullagenda[2] = "Agenda bashing" + for i, v in ipairs(agendas[from]) do + table.insert(fullagenda, v) + end + table.insert(fullagenda, "Date of next meeting") + table.insert(fullagenda, "Any other business") + return fullagenda +end + +function agenda_full_command(command, params, message) + from = message['frombare'] + ensure_loaded(from) + agenda = agendas[from] + fullagenda = full_agenda(from) + reply = "" + for i, v in ipairs(fullagenda) do + reply = reply..i..") "..v.."\n" + end + reply = reply.."Fini" + swiftob_reply_to(message, reply) +end + +function agenda_append_command(command, params, message) + from = message['frombare'] + agenda_append(from, params) + agenda_save(from) + swiftob_reply_to(message, "Done.") +end + +function agenda_up_command(command, params, message) + from = message['frombare'] + ensure_loaded(from) + up = tonumber(params) + if up == nil then up = 1 end + currents[from] = currents[from] + up + if currents[from] <= 0 then currents[from] = 1 end + item = full_agenda(from)[currents[from]] + if item == nil then item = "Fini." end + reply = currents[from]..") "..item + swiftob_reply_to(message, reply) +end + + +function agenda_clear_command(command, params, message) + from = message['frombare'] + agendas[from] = {} + agenda_save(from) + swiftob_reply_to(message, "Done.") +end + +function agenda_save(from) + agenda = agendas[from] + swiftob_store_setting("count@@@"..from, #agenda) + for i, v in ipairs(agenda) do + swiftob_store_setting(i.."@@@"..from, v) + end +end + +function ensure_loaded(from) + if agendas[from] == nil then + agenda_load(from) + end +end + +function agenda_load(from) + agendas[from] = {} + currents[from] = 0 + num_items = tonumber(swiftob_get_setting("count@@@"..from)) + if num_items == nil then num_items = 0 end + for i = 1, num_items do + agenda_append(from, swiftob_get_setting(i.."@@@"..from)) + end +end + +function agenda_append(from, item) + ensure_loaded(from) + agenda = agendas[from] + table.insert(agenda, item) + agendas[from] = agenda +end + +swiftob_register_command("agenda", "Anyone", "print the full agenda", agenda_full_command) +swiftob_register_command("agendaappend", "Owner", "append an item to the agenda", agenda_append_command) +swiftob_register_command("agendaclear", "Owner", "clear the agenda", agenda_clear_command) +swiftob_register_command("agendaup", "Owner", "Moves the current counter by n, and returns the current agenda item", agenda_up_command) + + + diff --git a/Swiftob/scripts/echo.lua b/Swiftob/scripts/echo.lua new file mode 100644 index 0000000..7adc2b3 --- /dev/null +++ b/Swiftob/scripts/echo.lua @@ -0,0 +1,5 @@ +function echo_message(command, params, message) + swiftob_reply_to(message, params) +end + +swiftob_register_command("echo", "Anyone", "What did you say?", echo_message) \ No newline at end of file diff --git a/Swiftob/scripts/eval.lua b/Swiftob/scripts/eval.lua new file mode 100644 index 0000000..c9840dc --- /dev/null +++ b/Swiftob/scripts/eval.lua @@ -0,0 +1,14 @@ + +function eval_command(command, params, message) + assert(loadstring(params))() + swiftob_reply_to(message, "done") +end + +function evalr_command(command, params, message) + result = assert(loadstring(params))() + swiftob_reply_to(message, "" .. result) +end + +swiftob_register_command("eval", "Owner", "Evaluate an expression", eval_command) +swiftob_register_command("evalr", "Owner", "Evaluate an expression and return the result", evalr_command) + diff --git a/Swiftob/scripts/version.lua b/Swiftob/scripts/version.lua new file mode 100644 index 0000000..40b4e4d --- /dev/null +++ b/Swiftob/scripts/version.lua @@ -0,0 +1,32 @@ +function friendly_version(version) + result = version['name'] + if version['version'] ~= nil and version['version'] ~= "" then + result = result.." version "..version['version'] + end + if version['os'] ~= nil and version['os'] ~= "" then + result = result .." on "..version['os'] + end + return result +end + +function version_command(command, params, message) + jid = swiftob_muc_input_to_jid(params, message['from']) + if jid == nil then + + else + swiftob_get_software_version({ + to=jid, + timeout=10, + success_callback=function(result) + swiftob_reply_to(message, params.." is running "..friendly_version(result)) + end, + failure_callback=function(error) + swiftob_reply_to(message, "Error getting version from "..params..": "..error) + end, + timeout_callback=function() + swiftob_reply_to(message, "Timeout waiting for version from "..params) + end}) + end +end + +swiftob_register_command("version", "Anyone", "Ask for someone's version", version_command) \ No newline at end of file -- cgit v0.10.2-6-g49f6