Commit 867b569c authored by louiz’'s avatar louiz’

Introduce the XMPP component, handle BASIC presence, message and subscription

parent e1006919
#include <xmpp/vaporo_component.hpp>
#include <steam/steam_client.hpp>
#include <network/poller.hpp>
#include <utils/timed_events.hpp>
#include <logger/logger.hpp>
#include <config/config.hpp>
int main()
/**
* Provide an helpful message to help the user write a minimal working
* configuration file.
*/
int config_help(const std::string& missing_option)
{
if (!missing_option.empty())
std::cerr << "Error: empty value for option " << missing_option << "." << std::endl;
std::cerr <<
"Please provide a configuration file filled like this:\n\n"
"hostname=irc.example.com\npassword=S3CR3T\nsteam_login=example\nsteam_password=yoyo\nauthorized_jid=example@example.com"
<< std::endl;
return 1;
}
int main(int ac, char** av)
{
if (ac > 1)
Config::filename = av[1];
else
Config::filename = "vaporo.cfg";
Config::file_must_exist = true;
std::cerr << "Using configuration file: " << Config::filename << std::endl;
std::string password;
try { // The file must exist
password = Config::get("password", "");
}
catch (const std::ios::failure& e) {
return config_help("");
}
const std::string hostname = Config::get("hostname", "");
const std::string login = Config::get("steam_login", "");
const std::string steam_pass = Config::get("steam_password", "");
const std::string authorized_jid = Config::get("authorized_jid", "");
if (password.empty())
return config_help("password");
if (hostname.empty())
return config_help("hostname");
if (login.empty())
return config_help("login");
if (steam_pass.empty())
return config_help("steam_password");
if (authorized_jid.empty())
return config_help("authorized_jid");
auto p = std::make_shared<Poller>();
SteamClient client(p);
client.start();
auto xmpp_component =
std::make_shared<VaporoComponent>(p, hostname, password,
authorized_jid, login, steam_pass);
xmpp_component->start();
auto timeout = TimedEventsManager::instance().get_timeout();
while (p->poll(timeout) != -1)
{
TimedEventsManager::instance().execute_expired_events();
log_debug("poll");
timeout = TimedEventsManager::instance().get_timeout();
log_debug("Timeout: " << timeout.count());
}
return 0;
}
......@@ -2,14 +2,92 @@
#include <logger/logger.hpp>
#include <network/poller.hpp>
#include <utils/timed_events.hpp>
#include <xmpp/vaporo_component.hpp>
#include <cstring>
#include <functional>
#include <fstream>
SteamClient::SteamClient(std::shared_ptr<Poller> poller):
using namespace std::string_literals;
static const char* error_messages[] = {
"",
"",
"generic failure",
"no/failed network connection",
"unknown error",
"password/ticket is invalid",
"same user logged in elsewhere",
"protocol version is incorrect",
"a parameter is incorrect",
"file was not found",
"called method busy - action not taken",
"called object was in an invalid state",
"name is invalid",
"email is invalid",
"name is not unique",
"access is denied",
"operation timed out",
"VAC2 banned",
"account not found",
"steamID is invalid",
"The requested service is currently unavailable",
"The user is not logged on",
"Request is pending (may be in process, or waiting on third party)",
"Encryption or Decryption failed",
"Insufficient privilege",
"Too much of a good thing",
"Access has been revoked (used for revoked guest passes)",
"License/Guest pass the user is trying to access is expired",
"Guest pass has already been redeemed by account, cannot be acked again",
"The request is a duplicate and the action has already occurred in the past, ignored this time",
"All the games in this guest pass redemption request are already owned by the user",
"IP address not found",
"failed to write change to the data store",
"failed to acquire access lock for this operation",
"unknown error",
"unknown error",
"unknown error",
"unknown error",
"unknown error",
"failed to find the shopping cart requested",
"a user didn't allow it",
"target is ignoring sender",
"nothing matching the request found",
"unknown error",
"this service is not accepting content changes right now",
"account doesn't have value, so this feature isn't available",
"allowed to take this action, but only because requester is admin",
"A Version mismatch in content transmitted within the Steam protocol.",
"The current CM can't service the user making a request, user should try another.",
"You are already logged in elsewhere, this cached credential login has failed.",
"You are already logged in elsewhere, you must wait",
"unknown error",
"unknown error",
"unknown error",
"unknown error",
"unknown error",
};
static const char* steam_state_to_xmpp_show[] = {
// See EPersonaState
"",
"",
"dnd",
"away",
"xa",
"chat",
"chat"
};
SteamClient::SteamClient(std::shared_ptr<Poller> poller,
const std::string& login,
const std::string& password):
TCPSocketHandler(poller),
sentry{}
login(login),
password(password),
sentry{},
xmpp(nullptr)
{
this->load_sentry();
this->steam = std::make_unique<SteamPPClient>(
......@@ -45,11 +123,13 @@ SteamClient::SteamClient(std::shared_ptr<Poller> poller):
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6);
this->steam->onPrivateMsg = std::bind(&SteamClient::on_private_msg, this,
std::placeholders::_1, std::placeholders::_2);
}
void SteamClient::start()
{
this->connect("146.66.152.12", "27017", false);
this->connect("72.165.61.174", "27017", false);
}
void SteamClient::on_connected()
......@@ -88,17 +168,34 @@ void SteamClient::parse_in_buffer(const size_t size)
void SteamClient::on_handshake()
{
log_debug("onHandshake");
// TODO read from the config file
if (this->sentry[0] != '\0')
this->steam->LogOn("", "", reinterpret_cast<const unsigned char*>(this->sentry));
{
log_debug("using the sentry");
this->steam->LogOn(this->login.data(), this->password.data(), reinterpret_cast<const unsigned char*>(this->sentry));
}
else
this->steam->LogOn("", "", nullptr, "2BP2N");
{
log_debug("Not using the sentry");
this->steam->LogOn(this->login.data(), this->password.data(), nullptr, "2BP2N");
}
log_debug("LogOn() called");
}
void SteamClient::on_log_on(Steam::EResult result, Steam::SteamID steam_id)
{
log_debug("on_log_on: " << static_cast<std::size_t>(result) << " steamid: " << steam_id.steamID64);
if (result == Steam::EResult::OK)
{
// TODO: handle busy, away, etc
this->roster.clear();
this->steam->SetPersonaState(Steam::EPersonaState::Online);
this->xmpp->send_presence({}, {}, {}, {}, {});
}
else
{
this->xmpp->send_presence({}, "unavailable", {}, {}, {});
this->xmpp->send_information_message("Login failed: "s + error_messages[static_cast<std::size_t>(result)]);
}
}
void SteamClient::on_sentry(const unsigned char* hash)
......@@ -128,6 +225,8 @@ void SteamClient::on_relationships(bool incremental,
log_debug("Requesting user info for " << i << " friends");
this->steam->RequestUserInfo(i, users_info);
return ;
// TODO, or remove
log_debug("-- Groups --");
for (auto it = groups.begin(); it != groups.end(); ++it)
{
......@@ -141,21 +240,43 @@ void SteamClient::on_user_info(Steam::SteamID user, Steam::SteamID* source, cons
Steam::EPersonaState* state, const unsigned char avatar_hash[20],
const char* game_name)
{
log_debug("on_user_info: " << name);
if (state)
const std::string id(std::to_string(user.steamID64));
auto item = this->roster.get_item(id);
if (!item)
{
log_debug("status: " << static_cast<int>(*state));
std::string str_name;
if (name)
str_name = name;
std::vector<std::string> groups;
item = this->roster.add_item(id, str_name, groups);
}
else
{
log_debug("offline");
if (name)
item->name = name;
}
log_debug("on_user_info: " << name << ": " << user.steamID64);
this->xmpp->on_steam_roster_item_changed(item);
if (!state || *state == Steam::EPersonaState::Offline)
this->xmpp->send_presence(id, "unavailable", {}, {}, {});
else
this->xmpp->send_presence(id, {}, {}, {},
steam_state_to_xmpp_show[static_cast<std::size_t>(*state)]);
// TODO gaming PEP
if (game_name)
{
log_debug("Game name: " << game_name);
}
}
void SteamClient::on_private_msg(Steam::SteamID user, const char* message)
{
log_debug("on_private_msg: " << user.steamID64 << " [" << message << "]");
const std::string id = std::to_string(user.steamID64);
this->xmpp->send_message_from_steam(id, message);
}
void SteamClient::save_sentry()
{
......@@ -177,3 +298,9 @@ void SteamClient::load_sentry()
}
}
void SteamClient::send_message(const std::string& str_id, const std::string& body)
{
Steam::SteamID id(std::stoll(str_id));
log_debug("sending steam message: " << id << " == " << id.steamID64 << " body: " << body);
this->steam->SendPrivateMessage(id, body.data());
}
......@@ -2,26 +2,35 @@
#define STEAM_CLIENT_HPP_INCLUDED
#include <network/tcp_socket_handler.hpp>
#include <xmpp/roster.hpp>
#include <steam++.h>
#include <memory>
class Poller;
class VaporoComponent;
class SteamClient: public TCPSocketHandler
{
using SteamPPClient = Steam::SteamClient;
public:
SteamClient(std::shared_ptr<Poller> poller);
SteamClient(std::shared_ptr<Poller> poller, const std::string& login,
const std::string& password);
~SteamClient() = default;
void set_xmpp(VaporoComponent* xmpp)
{
this->xmpp = xmpp;
}
void start();
void on_connected() override final;
void on_connection_failed(const std::string& reason) override final;
void on_connection_close(const std::string& error) override final;
void parse_in_buffer(const size_t size) override final;
void send_message(const std::string& id, const std::string& body);
/**
* Callback called by the steam object on some events
......@@ -37,14 +46,19 @@ public:
void on_user_info(Steam::SteamID user, Steam::SteamID* source, const char* name,
Steam::EPersonaState* state, const unsigned char avatar_hash[20],
const char* game_name);
void on_private_msg(Steam::SteamID user, const char* message);
private:
std::unique_ptr<SteamPPClient> steam;
const std::string login;
const std::string password;
/**
* The size wanted by steam in the next readable() call
*/
std::size_t wanted_size;
unsigned char sentry[20];
VaporoComponent* xmpp;
Roster roster;
SteamClient(const SteamClient&) = delete;
SteamClient(SteamClient&&) = delete;
......
#include <xmpp/vaporo_component.hpp>
#include <network/poller.hpp>
#include <logger/logger.hpp>
#include <xmpp/jid.hpp>
#include <utils/scopeguard.hpp>
VaporoComponent::VaporoComponent(std::shared_ptr<Poller> poller,
const std::string& hostname,
const std::string& secret,
const std::string& authorized_jid,
const std::string& steam_login,
const std::string& steam_password):
XmppComponent(poller, hostname, secret),
steam(poller, steam_login, steam_password),
authorized_jid(authorized_jid)
{
this->steam.set_xmpp(this);
this->stanza_handlers.emplace("presence",
std::bind(&VaporoComponent::handle_presence, this,std::placeholders::_1));
this->stanza_handlers.emplace("message",
std::bind(&VaporoComponent::handle_message, this,std::placeholders::_1));
this->stanza_handlers.emplace("iq",
std::bind(&VaporoComponent::handle_iq, this,std::placeholders::_1));
}
void VaporoComponent::handle_presence(const Stanza& stanza)
{
const std::string from_str = stanza.get_tag("from");
const Jid from(stanza.get_tag("from"));
const std::string type = stanza.get_tag("type");
if (type == "subscribe")
{ // User wants to add us in its roster
if (from_str == this->authorized_jid)
{ // Auto-accept
this->send_presence({}, "subscribed", {}, from_str, {});
this->send_presence({}, "subscribe", {}, from_str, {});
}
else
{ // Auto-deny
this->send_presence({}, "unsubscribed", {}, from_str, {});
}
}
else if (type == "unavailable")
{
// TODO log-off from steam
}
else
{
// TODO: Log-in to steam
this->steam.start();
}
}
void VaporoComponent::handle_message(const Stanza& stanza)
{
std::string from = stanza.get_tag("from");
std::string id = stanza.get_tag("id");
std::string to_str = stanza.get_tag("to");
std::string type = stanza.get_tag("type");
if (from.empty())
return;
if (type.empty())
type = "normal";
XmlNode* body = stanza.get_child("body", COMPONENT_NS);
Jid to(to_str);
if (body && !body->get_inner().empty())
this->steam.send_message(to.local, body->get_inner());
}
void VaporoComponent::handle_iq(const Stanza& stanza)
{
std::string id = stanza.get_tag("id");
std::string from = stanza.get_tag("from");
std::string to_str = stanza.get_tag("to");
std::string type = stanza.get_tag("type");
if (from.empty())
return;
if (id.empty() || to_str.empty() || type.empty())
{
this->send_stanza_error("iq", from, this->served_hostname, id,
"modify", "bad-request", "");
return;
}
Jid to(to_str);
// These two values will be used in the error iq sent if we don't disable
// the scopeguard.
std::string error_type("cancel");
std::string error_name("internal-server-error");
utils::ScopeGuard stanza_error([&](){
this->send_stanza_error("iq", from, to_str, id,
error_type, error_name, "");
});
if (type == "result")
{
XmlNode* query;
if ((query = stanza.get_child("query", "jabber:iq:roster")))
{ // We received the user's current roster
this->on_roster_items_received(query);
}
}
stanza_error.disable();
}
void VaporoComponent::send_presence(const std::string& from,
const std::string& type,
const std::string& status_msg,
const std::string& to,
const std::string& show)
{
Stanza presence("presence");
if (from.empty())
presence["from"] = this->served_hostname;
else
presence["from"] = from + "@" + this->served_hostname;
if (to.empty())
presence["to"] = this->authorized_jid;
else
presence["to"] = to + "@" + this->served_hostname;
if (!type.empty())
presence["type"] = type;
if (!status_msg.empty())
{
XmlNode status("status");
status.set_inner(status_msg);
status.close();
presence.add_child(std::move(status));
}
if (!show.empty())
{
XmlNode show_elem("show");
show_elem.set_inner(show);
show_elem.close();
presence.add_child(std::move(show_elem));
}
presence.close();
this->send_stanza(presence);
}
void VaporoComponent::send_information_message(const std::string& txt)
{
Stanza message("message");
message["to"] = this->authorized_jid;
message["type"] = "chat";
XmlNode body("body");
body.set_inner(txt);
body.close();
message.add_child(std::move(body));
message.close();
this->send_stanza(message);
}
void VaporoComponent::after_handshake()
{
// Empty our internal roster
this->xmpp_roster.clear();
// Get the user's roster
Stanza iq("iq");
iq["to"] = this->authorized_jid;
iq["type"] = "get";
XmlNode query("jabber:iq:roster:query");
query.close();
iq.add_child(std::move(query));
iq["id"] = this->next_id();
iq.close();
this->send_stanza(iq);
}
void VaporoComponent::on_roster_items_received(const XmlNode* node)
{
log_debug("on_roster_items_received");
auto items = node->get_children("item", "jabber:iq:roster");
for (const auto item: items)
{
auto it = this->xmpp_roster.get_item(item->get_tag("jid"));
if (!it)
this->xmpp_roster.add_item(item->get_tag("jid"), item->get_tag("name"));
else
it->name = item->get_tag("name");
}
}
void VaporoComponent::on_steam_roster_item_changed(const RosterItem* item)
{
auto it = this->xmpp_roster.get_item(item->jid + "@" + this->served_hostname);
if (!it || it->name != item->name)
// The steam contact changed its name or it's a new contact, update the
// item on the server roster
this->send_roster_push(item);
}
void VaporoComponent::send_roster_push(const RosterItem* roster_item)
{
Stanza iq("iq");
iq["to"] = this->authorized_jid;
iq["id"] = this->next_id();
iq["type"] = "set";
XmlNode query("jabber:iq:roster:query");
XmlNode item("item");
item["jid"] = roster_item->jid + "@" + this->served_hostname;
item["name"] = roster_item->name;
// TODO subscription
item["subscription"] = "both";
// TODO groups
item.close();
query.add_child(std::move(item));
query.close();
iq.add_child(std::move(query));
iq.close();
this->send_stanza(iq);
}
void VaporoComponent::send_message_from_steam(const std::string& from, const std::string& body)
{
this->send_message(from, std::make_tuple(body, nullptr), this->authorized_jid,
"chat", false);
}
void VaporoComponent::shutdown()
{
// Send an unavailable presence for each contact
for (const auto& item: this->xmpp_roster.get_items())
{
Jid jid(item.jid);
this->send_presence(jid.local, "unavailable", "Gateway shutdown", {}, {});
}
this->send_presence({}, "unavailable", "Gateway shutdown", {}, {});
}
#ifndef VAPORO_COMPONENT_HPP_INCLUDED
#define VAPORO_COMPONENT_HPP_INCLUDED
#include <xmpp/xmpp_component.hpp>
#include <xmpp/roster.hpp>
#include <steam/steam_client.hpp>
#include <map>
class Poller;
class VaporoComponent: public XmppComponent
{
public:
VaporoComponent(std::shared_ptr<Poller> poller,
const std::string& hostname,
const std::string& secret,
const std::string& authorized_jid,
const std::string& steam_login,
const std::string& steam_password);
~VaporoComponent() = default;
void on_steam_roster_item_changed(const RosterItem* item);
void send_roster_push(const RosterItem* item);
/**
* Send a basic presence with a type and an optional status. From contains
* the local part of the from jid, if it's empty it's just the gateway JID.
* Show is optional as well.
*/
void send_presence(const std::string& from, const std::string& type,
const std::string& status_msg, const std::string& to,
const std::string& show);
void send_message_from_steam(const std::string& from, const std::string& body);
/**
* Send a simple message from the gateway itself, to indicate an error, or
* some other useful information to the user.
*/
void send_information_message(const std::string& message);
void on_roster_items_received(const XmlNode* node);
/**
* Handle the various stanza types
*/
void handle_presence(const Stanza& stanza);
void handle_message(const Stanza& stanza);
void handle_iq(const Stanza& stanza);
void after_handshake() override final;
void shutdown();