Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
697339dbfa
25 changed files with 936 additions and 673 deletions
|
@ -38,7 +38,7 @@ set(Boost_USE_STATIC_LIBS ON)
|
|||
set(Boost_USE_MULTITHREADED ON)
|
||||
set(Boost_USE_STATIC_RUNTIME OFF)
|
||||
endif()
|
||||
find_package(Boost COMPONENTS program_options date_time system filesystem regex thread signals REQUIRED)
|
||||
find_package(Boost COMPONENTS program_options date_time system filesystem regex signals REQUIRED)
|
||||
message( STATUS "Found Boost: ${Boost_LIBRARIES}, ${Boost_INCLUDE_DIR}")
|
||||
|
||||
set(Protobuf_DIR "${CMAKE_SOURCE_DIR}/cmake_modules")
|
||||
|
@ -56,6 +56,9 @@ find_package(event)
|
|||
set(pqxx_DIR "${CMAKE_SOURCE_DIR}/cmake_modules")
|
||||
find_package(pqxx)
|
||||
|
||||
set(dbus_DIR "${CMAKE_SOURCE_DIR}/cmake_modules")
|
||||
find_package(dbus)
|
||||
|
||||
find_package(Doxygen)
|
||||
|
||||
INCLUDE(FindQt4)
|
||||
|
@ -146,12 +149,21 @@ if (PROTOBUF_FOUND)
|
|||
endif()
|
||||
|
||||
message("Frotz plugin : yes")
|
||||
message("SMSTools3 plugin : yes")
|
||||
|
||||
if(${LIBDBUSGLIB_FOUND})
|
||||
message("Skype plugin : yes")
|
||||
include_directories(${LIBDBUSGLIB_INCLUDE_DIRS})
|
||||
else()
|
||||
message("Skype plugin : no (install dbus-glib-devel)")
|
||||
endif()
|
||||
|
||||
else()
|
||||
message("Network plugins : no (install libprotobuf-dev)")
|
||||
message("Libpurple plugin : no (install libpurple and libprotobuf-dev)")
|
||||
message("IRC plugin : no (install libircclient-qt and libprotobuf-dev)")
|
||||
message("Frotz plugin : no (install libprotobuf-dev)")
|
||||
message("SMSTools3 plugin : no (install libprotobuf-dev)")
|
||||
endif()
|
||||
|
||||
if (LOG4CXX_FOUND)
|
||||
|
|
16
ChangeLog
16
ChangeLog
|
@ -1,11 +1,27 @@
|
|||
Version 2.0.0-beta (X-X-X):
|
||||
General:
|
||||
* Added PostreSQL support (thanks to Jadestorm).
|
||||
* Send presences only "from" bare JID (fixed bug with buddies appearing
|
||||
twice in the roster and potential unregistering issues).
|
||||
* Fixed potential MySQL/SQLite3 deadlocks.
|
||||
* Fixed disconnecting in server-mode when client does not send unavailable
|
||||
presence before disconnection.
|
||||
* Fixed crash in server-mode when client send its custom jabber:iq:storage
|
||||
payload.
|
||||
* Fixed registration from Pidgin.
|
||||
* Unsubscribe presence sent to some buddy doesn't disconnect the account.
|
||||
* Remote Roster requests are not sent to resources, but to bare JID.
|
||||
* Added automatic reconnection in case of non-fatal error.
|
||||
* Added more error messages.
|
||||
|
||||
Skype:
|
||||
* Initial support for Skype added, read more on
|
||||
http://spectrum.im/projects/spectrum/wiki/Spectrum_2_Admin_-_Skype_backend
|
||||
|
||||
SMSTools3:
|
||||
* Initial support for SMSTools3, read more on
|
||||
http://spectrum.im/projects/spectrum/wiki/Spectrum_2_Admin_-_SMSTools3_backend
|
||||
|
||||
version 2.0.0 alpha (2011-12-06):
|
||||
General:
|
||||
* First Spectrum 2.0.0 alpha release, check more on
|
||||
|
|
|
@ -7,9 +7,13 @@ if (PROTOBUF_FOUND)
|
|||
ADD_SUBDIRECTORY(libcommuni)
|
||||
endif()
|
||||
|
||||
ADD_SUBDIRECTORY(smstools3)
|
||||
|
||||
if (NOT WIN32)
|
||||
ADD_SUBDIRECTORY(frotz)
|
||||
# ADD_SUBDIRECTORY(skype)
|
||||
if (${LIBDBUSGLIB_FOUND})
|
||||
ADD_SUBDIRECTORY(skype)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
|
|
@ -6,7 +6,7 @@ FILE(GLOB SRC *.c *.cpp)
|
|||
|
||||
ADD_EXECUTABLE(spectrum2_frotz_backend ${SRC})
|
||||
|
||||
target_link_libraries(spectrum2_frotz_backend transport pthread transport-plugin ${Boost_LIBRARIES} ${SWIFTEN_LIBRARY} ${LOG4CXX_LIBRARIES})
|
||||
target_link_libraries(spectrum2_frotz_backend transport pthread ${Boost_LIBRARIES} ${SWIFTEN_LIBRARY} ${LOG4CXX_LIBRARIES})
|
||||
|
||||
INSTALL(TARGETS spectrum2_frotz_backend RUNTIME DESTINATION bin)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ FILE(GLOB HEADERS *.h)
|
|||
QT4_WRAP_CPP(SRC ${HEADERS})
|
||||
ADD_EXECUTABLE(spectrum2_libcommuni_backend ${SRC})
|
||||
|
||||
target_link_libraries(spectrum2_libcommuni_backend ${IRC_LIBRARY} ${QT_LIBRARIES} transport-plugin transport pthread)
|
||||
target_link_libraries(spectrum2_libcommuni_backend ${IRC_LIBRARY} ${QT_LIBRARIES} transport pthread)
|
||||
|
||||
INSTALL(TARGETS spectrum2_libcommuni_backend RUNTIME DESTINATION bin)
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
cmake_minimum_required(VERSION 2.6)
|
||||
FILE(GLOB SRC *.cpp)
|
||||
|
||||
include_directories(/usr/include/dbus-1.0/)
|
||||
include_directories(/usr/lib/dbus-1.0/include/)
|
||||
include_directories(/usr/lib64/dbus-1.0/include/)
|
||||
|
||||
ADD_EXECUTABLE(spectrum2_skype_backend ${SRC})
|
||||
|
||||
target_link_libraries(spectrum2_skype_backend ${GLIB2_LIBRARIES} ${EVENT_LIBRARIES} transport pthread dbus-glib-1 dbus-1 gobject-2.0 transport-plugin)
|
||||
target_link_libraries(spectrum2_skype_backend ${GLIB2_LIBRARIES} ${EVENT_LIBRARIES} transport pthread ${LIBDBUSGLIB_LIBRARIES})
|
||||
|
||||
INSTALL(TARGETS spectrum2_skype_backend RUNTIME DESTINATION bin)
|
||||
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
/**
|
||||
* XMPP - libpurple transport
|
||||
*
|
||||
* Copyright (C) 2009, Jan Kaluza <hanzz@soc.pidgin.im>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
|
||||
*/
|
||||
|
||||
#include "geventloop.h"
|
||||
#ifdef _WIN32
|
||||
#include "win32/win32dep.h"
|
||||
#endif
|
||||
#ifdef WITH_LIBEVENT
|
||||
#include "event.h"
|
||||
#endif
|
||||
|
||||
typedef struct _PurpleIOClosure {
|
||||
PurpleInputFunction function;
|
||||
guint result;
|
||||
gpointer data;
|
||||
#ifdef WITH_LIBEVENT
|
||||
GSourceFunc function2;
|
||||
struct timeval timeout;
|
||||
struct event evfifo;
|
||||
#endif
|
||||
} PurpleIOClosure;
|
||||
|
||||
static gboolean io_invoke(GIOChannel *source,
|
||||
GIOCondition condition,
|
||||
gpointer data)
|
||||
{
|
||||
PurpleIOClosure *closure = (PurpleIOClosure* )data;
|
||||
PurpleInputCondition purple_cond = (PurpleInputCondition)0;
|
||||
|
||||
int tmp = 0;
|
||||
if (condition & READ_COND)
|
||||
{
|
||||
tmp |= PURPLE_INPUT_READ;
|
||||
purple_cond = (PurpleInputCondition)tmp;
|
||||
}
|
||||
if (condition & WRITE_COND)
|
||||
{
|
||||
tmp |= PURPLE_INPUT_WRITE;
|
||||
purple_cond = (PurpleInputCondition)tmp;
|
||||
}
|
||||
|
||||
closure->function(closure->data, g_io_channel_unix_get_fd(source), purple_cond);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void io_destroy(gpointer data)
|
||||
{
|
||||
g_free(data);
|
||||
}
|
||||
|
||||
static guint input_add(gint fd,
|
||||
PurpleInputCondition condition,
|
||||
PurpleInputFunction function,
|
||||
gpointer data)
|
||||
{
|
||||
PurpleIOClosure *closure = g_new0(PurpleIOClosure, 1);
|
||||
GIOChannel *channel;
|
||||
GIOCondition cond = (GIOCondition)0;
|
||||
closure->function = function;
|
||||
closure->data = data;
|
||||
|
||||
int tmp = 0;
|
||||
if (condition & PURPLE_INPUT_READ)
|
||||
{
|
||||
tmp |= READ_COND;
|
||||
cond = (GIOCondition)tmp;
|
||||
}
|
||||
if (condition & PURPLE_INPUT_WRITE)
|
||||
{
|
||||
tmp |= WRITE_COND;
|
||||
cond = (GIOCondition)tmp;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
channel = wpurple_g_io_channel_win32_new_socket(fd);
|
||||
#else
|
||||
channel = g_io_channel_unix_new(fd);
|
||||
#endif
|
||||
closure->result = g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, cond,
|
||||
io_invoke, closure, io_destroy);
|
||||
|
||||
g_io_channel_unref(channel);
|
||||
return closure->result;
|
||||
}
|
||||
|
||||
static PurpleEventLoopUiOps eventLoopOps =
|
||||
{
|
||||
g_timeout_add,
|
||||
g_source_remove,
|
||||
input_add,
|
||||
g_source_remove,
|
||||
NULL,
|
||||
#if GLIB_CHECK_VERSION(2,14,0)
|
||||
g_timeout_add_seconds,
|
||||
#else
|
||||
NULL,
|
||||
#endif
|
||||
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
|
||||
#ifdef WITH_LIBEVENT
|
||||
|
||||
static GHashTable *events = NULL;
|
||||
static unsigned long id = 0;
|
||||
|
||||
static void event_io_destroy(gpointer data)
|
||||
{
|
||||
PurpleIOClosure *closure = (PurpleIOClosure* )data;
|
||||
event_del(&closure->evfifo);
|
||||
g_free(data);
|
||||
}
|
||||
|
||||
static void event_io_invoke(int fd, short event, void *data)
|
||||
{
|
||||
PurpleIOClosure *closure = (PurpleIOClosure* )data;
|
||||
PurpleInputCondition purple_cond = (PurpleInputCondition)0;
|
||||
int tmp = 0;
|
||||
if (event & EV_READ)
|
||||
{
|
||||
tmp |= PURPLE_INPUT_READ;
|
||||
purple_cond = (PurpleInputCondition)tmp;
|
||||
}
|
||||
if (event & EV_WRITE)
|
||||
{
|
||||
tmp |= PURPLE_INPUT_WRITE;
|
||||
purple_cond = (PurpleInputCondition)tmp;
|
||||
}
|
||||
if (event & EV_TIMEOUT)
|
||||
{
|
||||
// tmp |= PURPLE_INPUT_WRITE;
|
||||
// purple_cond = (PurpleInputCondition)tmp;
|
||||
if (closure->function2(closure->data))
|
||||
evtimer_add(&closure->evfifo, &closure->timeout);
|
||||
// else
|
||||
// event_io_destroy(data);
|
||||
return;
|
||||
}
|
||||
|
||||
closure->function(closure->data, fd, purple_cond);
|
||||
}
|
||||
|
||||
static gboolean event_input_remove(guint handle)
|
||||
{
|
||||
PurpleIOClosure *closure = (PurpleIOClosure *) g_hash_table_lookup(events, &handle);
|
||||
if (closure)
|
||||
event_io_destroy(closure);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static guint event_input_add(gint fd,
|
||||
PurpleInputCondition condition,
|
||||
PurpleInputFunction function,
|
||||
gpointer data)
|
||||
{
|
||||
PurpleIOClosure *closure = g_new0(PurpleIOClosure, 1);
|
||||
GIOChannel *channel;
|
||||
GIOCondition cond = (GIOCondition)0;
|
||||
closure->function = function;
|
||||
closure->data = data;
|
||||
|
||||
int tmp = EV_PERSIST;
|
||||
if (condition & PURPLE_INPUT_READ)
|
||||
{
|
||||
tmp |= EV_READ;
|
||||
}
|
||||
if (condition & PURPLE_INPUT_WRITE)
|
||||
{
|
||||
tmp |= EV_WRITE;
|
||||
}
|
||||
|
||||
event_set(&closure->evfifo, fd, tmp, event_io_invoke, closure);
|
||||
event_add(&closure->evfifo, NULL);
|
||||
|
||||
int *f = (int *) g_malloc(sizeof(int));
|
||||
*f = id;
|
||||
id++;
|
||||
g_hash_table_replace(events, f, closure);
|
||||
|
||||
return *f;
|
||||
}
|
||||
|
||||
static guint event_timeout_add (guint interval, GSourceFunc function, gpointer data) {
|
||||
struct timeval timeout;
|
||||
PurpleIOClosure *closure = g_new0(PurpleIOClosure, 1);
|
||||
closure->function2 = function;
|
||||
closure->data = data;
|
||||
|
||||
timeout.tv_sec = interval/1000;
|
||||
timeout.tv_usec = (interval%1000)*1000;
|
||||
evtimer_set(&closure->evfifo, event_io_invoke, closure);
|
||||
evtimer_add(&closure->evfifo, &timeout);
|
||||
closure->timeout = timeout;
|
||||
|
||||
guint *f = (guint *) g_malloc(sizeof(guint));
|
||||
*f = id;
|
||||
id++;
|
||||
g_hash_table_replace(events, f, closure);
|
||||
return *f;
|
||||
}
|
||||
|
||||
static PurpleEventLoopUiOps libEventLoopOps =
|
||||
{
|
||||
event_timeout_add,
|
||||
event_input_remove,
|
||||
event_input_add,
|
||||
event_input_remove,
|
||||
NULL,
|
||||
// #if GLIB_CHECK_VERSION(2,14,0)
|
||||
// g_timeout_add_seconds,
|
||||
// #else
|
||||
NULL,
|
||||
// #endif
|
||||
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
|
||||
#endif /* WITH_LIBEVENT*/
|
||||
|
||||
PurpleEventLoopUiOps * getEventLoopUiOps(void){
|
||||
return &eventLoopOps;
|
||||
#ifdef WITH_LIBEVENT
|
||||
events = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, NULL);
|
||||
return &libEventLoopOps;
|
||||
#endif
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* XMPP - libpurple transport
|
||||
*
|
||||
* Copyright (C) 2009, Jan Kaluza <hanzz@soc.pidgin.im>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef _HI_EVENTLOOP_H
|
||||
#define _HI_EVENTLOOP_H
|
||||
|
||||
#include <glib.h>
|
||||
#include "purple.h"
|
||||
#include "eventloop.h"
|
||||
|
||||
#define READ_COND (G_IO_IN | G_IO_HUP | G_IO_ERR)
|
||||
#define WRITE_COND (G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL)
|
||||
|
||||
PurpleEventLoopUiOps * getEventLoopUiOps(void);
|
||||
|
||||
#endif
|
|
@ -12,9 +12,7 @@
|
|||
#include "transport/rostermanager.h"
|
||||
#include "transport/conversation.h"
|
||||
#include "transport/networkplugin.h"
|
||||
#include "spectrumeventloop.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "geventloop.h"
|
||||
#include "log4cxx/logger.h"
|
||||
#include "log4cxx/consoleappender.h"
|
||||
#include "log4cxx/patternlayout.h"
|
||||
|
@ -96,6 +94,9 @@ class Skype {
|
|||
return m_username;
|
||||
}
|
||||
|
||||
bool createDBusProxy();
|
||||
bool loadSkypeBuddies();
|
||||
|
||||
private:
|
||||
std::string m_username;
|
||||
std::string m_password;
|
||||
|
@ -103,12 +104,16 @@ class Skype {
|
|||
DBusGConnection *m_connection;
|
||||
DBusGProxy *m_proxy;
|
||||
std::string m_user;
|
||||
int m_timer;
|
||||
int m_counter;
|
||||
int fd_output;
|
||||
};
|
||||
|
||||
class SpectrumNetworkPlugin : public NetworkPlugin {
|
||||
public:
|
||||
SpectrumNetworkPlugin(Config *config, const std::string &host, int port) : NetworkPlugin() {
|
||||
this->config = config;
|
||||
LOG4CXX_INFO(logger, "Starting the backend.");
|
||||
}
|
||||
|
||||
~SpectrumNetworkPlugin() {
|
||||
|
@ -120,7 +125,7 @@ class SpectrumNetworkPlugin : public NetworkPlugin {
|
|||
void handleLoginRequest(const std::string &user, const std::string &legacyName, const std::string &password) {
|
||||
std::string name = legacyName;
|
||||
name = name.substr(name.find(".") + 1);
|
||||
LOG4CXX_INFO(logger, "Creating account with name '" << name);
|
||||
LOG4CXX_INFO(logger, "Creating account with name '" << name << "'");
|
||||
|
||||
Skype *skype = new Skype(user, name, password);
|
||||
m_sessions[user] = skype;
|
||||
|
@ -297,237 +302,288 @@ class SpectrumNetworkPlugin : public NetworkPlugin {
|
|||
|
||||
};
|
||||
|
||||
Skype::Skype(const std::string &user, const std::string &username, const std::string &password) {
|
||||
m_username = username;
|
||||
m_user = user;
|
||||
m_password = password;
|
||||
m_pid = 0;
|
||||
m_connection = 0;
|
||||
m_proxy = 0;
|
||||
}
|
||||
|
||||
void Skype::login() {
|
||||
boost::filesystem::path path(std::string("/tmp/skype/") + m_username);
|
||||
if (!boost::filesystem::exists(path)) {
|
||||
boost::filesystem::create_directories(path);
|
||||
boost::filesystem::path path2(std::string("/tmp/skype/") + m_username + "/" + m_username );
|
||||
boost::filesystem::create_directories(path2);
|
||||
}
|
||||
Skype::Skype(const std::string &user, const std::string &username, const std::string &password) {
|
||||
m_username = username;
|
||||
m_user = user;
|
||||
m_password = password;
|
||||
m_pid = 0;
|
||||
m_connection = 0;
|
||||
m_proxy = 0;
|
||||
m_timer = -1;
|
||||
m_counter = 0;
|
||||
}
|
||||
|
||||
std::string shared_xml = "<?xml version=\"1.0\"?>\n"
|
||||
"<config version=\"1.0\" serial=\"28\" timestamp=\"" + boost::lexical_cast<std::string>(time(NULL)) + ".0\">\n"
|
||||
"<UI>\n"
|
||||
"<Installed>2</Installed>\n"
|
||||
"<Language>en</Language>\n"
|
||||
"</UI>\n"
|
||||
"</config>\n";
|
||||
g_file_set_contents(std::string(std::string("/tmp/skype/") + m_username + "/shared.xml").c_str(), shared_xml.c_str(), -1, NULL);
|
||||
|
||||
std::string config_xml = "<?xml version=\"1.0\"?>\n"
|
||||
"<config version=\"1.0\" serial=\"7\" timestamp=\"" + boost::lexical_cast<std::string>(time(NULL)) + ".0\">\n"
|
||||
"<Lib>\n"
|
||||
"<Account>\n"
|
||||
"<IdleTimeForAway>30000000</IdleTimeForAway>\n"
|
||||
"<IdleTimeForNA>300000000</IdleTimeForNA>\n"
|
||||
"<LastUsed>" + boost::lexical_cast<std::string>(time(NULL)) + "</LastUsed>\n"
|
||||
"</Account>\n"
|
||||
"</Lib>\n"
|
||||
"<UI>\n"
|
||||
"<API>\n"
|
||||
"<Authorizations>Spectrum</Authorizations>\n"
|
||||
"<BlockedPrograms></BlockedPrograms>\n"
|
||||
"</API>\n"
|
||||
"</UI>\n"
|
||||
"</config>\n";
|
||||
g_file_set_contents(std::string(std::string("/tmp/skype/") + m_username + "/" + m_username +"/config.xml").c_str(), config_xml.c_str(), -1, NULL);
|
||||
std::string db_path = std::string("/tmp/skype/") + m_username;
|
||||
char *db = (char *) malloc(db_path.size() + 1);
|
||||
strcpy(db, db_path.c_str());
|
||||
LOG4CXX_INFO(logger, m_username << ": Spawning new Skype instance dbpath=" << db);
|
||||
gchar* argv[6] = {"skype", "--disable-cleanlooks", "--pipelogin", "--dbpath", db, 0};
|
||||
static gboolean load_skype_buddies(gpointer data) {
|
||||
Skype *skype = (Skype *) data;
|
||||
return skype->loadSkypeBuddies();
|
||||
}
|
||||
|
||||
int fd;
|
||||
int fd_output;
|
||||
g_spawn_async_with_pipes(NULL,
|
||||
argv,
|
||||
NULL /*envp*/,
|
||||
G_SPAWN_SEARCH_PATH,
|
||||
NULL /*child_setup*/,
|
||||
NULL /*user_data*/,
|
||||
&m_pid /*child_pid*/,
|
||||
&fd,
|
||||
NULL,
|
||||
&fd_output,
|
||||
NULL /*error*/);
|
||||
std::string login_data = std::string(m_username + " " + m_password + "\n");
|
||||
LOG4CXX_INFO(logger, m_username << ": Login data=" << login_data);
|
||||
write(fd, login_data.c_str(), login_data.size());
|
||||
close(fd);
|
||||
|
||||
fcntl (fd_output, F_SETFL, O_NONBLOCK);
|
||||
bool Skype::createDBusProxy() {
|
||||
if (m_proxy == NULL) {
|
||||
LOG4CXX_INFO(logger, "Creating DBus proxy for com.Skype.Api.");
|
||||
m_counter++;
|
||||
|
||||
free(db);
|
||||
GError *error = NULL;
|
||||
m_proxy = dbus_g_proxy_new_for_name_owner (m_connection, "com.Skype.API", "/com/Skype", "com.Skype.API", &error);
|
||||
if (m_proxy == NULL && error != NULL) {
|
||||
LOG4CXX_INFO(logger, m_username << ":" << error->message);
|
||||
|
||||
sleep(2);
|
||||
|
||||
GError *error = NULL;
|
||||
DBusObjectPathVTable vtable;
|
||||
|
||||
//Initialise threading
|
||||
dbus_threads_init_default();
|
||||
|
||||
if (m_connection == NULL)
|
||||
{
|
||||
m_connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
|
||||
if (m_connection == NULL && error != NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS Error: " << error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_proxy == NULL)
|
||||
{
|
||||
m_proxy = dbus_g_proxy_new_for_name_owner (m_connection,
|
||||
"com.Skype.API",
|
||||
"/com/Skype",
|
||||
"com.Skype.API",
|
||||
&error);
|
||||
if (m_proxy == NULL && error != NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ":" << error->message);
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
vtable.message_function = &skype_notify_handler;
|
||||
dbus_connection_register_object_path(dbus_g_connection_get_connection(m_connection), "/com/Skype/Client", &vtable, this);
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
std::string re = "CONNSTATUS OFFLINE";
|
||||
while (re == "CONNSTATUS OFFLINE" || re.empty()) {
|
||||
sleep(1);
|
||||
gchar buffer[1024];
|
||||
int bytes_read;
|
||||
bytes_read = read (fd_output, buffer, 1023);
|
||||
if (bytes_read > 0) {
|
||||
buffer[bytes_read] = 0;
|
||||
np->handleDisconnected(m_user, 0, buffer);
|
||||
close(fd_output);
|
||||
logout();
|
||||
return;
|
||||
}
|
||||
re = send_command("NAME Spectrum");
|
||||
if (counter++ > 15)
|
||||
break;
|
||||
}
|
||||
|
||||
close(fd_output);
|
||||
|
||||
if (send_command("PROTOCOL 7") != "PROTOCOL 7") {
|
||||
np->handleDisconnected(m_user, 0, "Skype is not ready");
|
||||
if (m_counter == 15) {
|
||||
np->handleDisconnected(m_user, 0, error->message);
|
||||
logout();
|
||||
return;
|
||||
g_error_free(error);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
np->handleConnected(m_user);
|
||||
|
||||
std::map<std::string, std::string> group_map;
|
||||
std::string groups = send_command("SEARCH GROUPS CUSTOM");
|
||||
groups = groups.substr(groups.find(' ') + 1);
|
||||
std::vector<std::string> grps;
|
||||
boost::split(grps, groups, boost::is_any_of(","));
|
||||
BOOST_FOREACH(std::string grp, grps) {
|
||||
std::vector<std::string> data;
|
||||
std::string name = send_command("GET GROUP " + grp + " DISPLAYNAME");
|
||||
boost::split(data, name, boost::is_any_of(" "));
|
||||
name = name.substr(name.find("DISPLAYNAME") + 12);
|
||||
|
||||
std::string users = send_command("GET GROUP " + data[1] + " USERS");
|
||||
users = name.substr(name.find("USERS") + 6);
|
||||
boost::split(data, users, boost::is_any_of(","));
|
||||
BOOST_FOREACH(std::string u, data) {
|
||||
group_map[u] = grp;
|
||||
}
|
||||
}
|
||||
|
||||
std::string friends = send_command("GET AUTH_CONTACTS_PROFILES");
|
||||
|
||||
char **full_friends_list = g_strsplit((strchr(friends.c_str(), ' ')+1), ";", 0);
|
||||
if (full_friends_list && full_friends_list[0])
|
||||
{
|
||||
//in the format of: username;full name;phone;office phone;mobile phone;
|
||||
// online status;friendly name;voicemail;mood
|
||||
// (comma-seperated lines, usernames can have comma's)
|
||||
|
||||
for (int i=0; full_friends_list[i] && *full_friends_list[i] != '\0'; i+=8)
|
||||
{
|
||||
std::string buddy = full_friends_list[i];
|
||||
|
||||
if (buddy[0] == ',') {
|
||||
buddy.erase(buddy.begin());
|
||||
}
|
||||
std::cout << "BUDDY '" << buddy << "'\n";
|
||||
std::string st = full_friends_list[i + 5];
|
||||
|
||||
pbnetwork::StatusType status = getStatus(st);
|
||||
|
||||
std::string alias = full_friends_list[i + 6];
|
||||
|
||||
std::string mood_text = "";
|
||||
if (full_friends_list[i + 8] && *full_friends_list[i + 8] != '\0' && *full_friends_list[i + 8] != ',') {
|
||||
mood_text = full_friends_list[i + 8];
|
||||
i++;
|
||||
}
|
||||
|
||||
std::vector<std::string> groups;
|
||||
groups.push_back(group_map[buddy]);
|
||||
np->handleBuddyChanged(m_user, buddy, alias, groups, status, mood_text);
|
||||
}
|
||||
}
|
||||
g_strfreev(full_friends_list);
|
||||
|
||||
send_command("SET AUTOAWAY OFF");
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
void Skype::logout() {
|
||||
if (m_pid != 0) {
|
||||
send_command("SET USERSTATUS INVISIBLE");
|
||||
send_command("SET USERSTATUS OFFLINE");
|
||||
sleep(2);
|
||||
g_object_unref(m_proxy);
|
||||
LOG4CXX_INFO(logger, m_username << ": Killing Skype instance");
|
||||
kill((int) m_pid, SIGTERM);
|
||||
m_pid = 0;
|
||||
}
|
||||
}
|
||||
if (m_proxy) {
|
||||
LOG4CXX_INFO(logger, "Proxy created.");
|
||||
DBusObjectPathVTable vtable;
|
||||
vtable.message_function = &skype_notify_handler;
|
||||
dbus_connection_register_object_path(dbus_g_connection_get_connection(m_connection), "/com/Skype/Client", &vtable, this);
|
||||
|
||||
std::string Skype::send_command(const std::string &message) {
|
||||
GError *error = NULL;
|
||||
gchar *str = NULL;
|
||||
m_counter = 0;
|
||||
m_timer = g_timeout_add_seconds(1, load_skype_buddies, this);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean create_dbus_proxy(gpointer data) {
|
||||
Skype *skype = (Skype *) data;
|
||||
return skype->createDBusProxy();
|
||||
}
|
||||
|
||||
void Skype::login() {
|
||||
boost::filesystem::path path(std::string("/tmp/skype/") + m_username);
|
||||
if (!boost::filesystem::exists(path)) {
|
||||
boost::filesystem::create_directories(path);
|
||||
boost::filesystem::path path2(std::string("/tmp/skype/") + m_username + "/" + m_username );
|
||||
boost::filesystem::create_directories(path2);
|
||||
}
|
||||
|
||||
std::string shared_xml = "<?xml version=\"1.0\"?>\n"
|
||||
"<config version=\"1.0\" serial=\"28\" timestamp=\"" + boost::lexical_cast<std::string>(time(NULL)) + ".0\">\n"
|
||||
"<UI>\n"
|
||||
"<Installed>2</Installed>\n"
|
||||
"<Language>en</Language>\n"
|
||||
"</UI>\n"
|
||||
"</config>\n";
|
||||
g_file_set_contents(std::string(std::string("/tmp/skype/") + m_username + "/shared.xml").c_str(), shared_xml.c_str(), -1, NULL);
|
||||
|
||||
std::string config_xml = "<?xml version=\"1.0\"?>\n"
|
||||
"<config version=\"1.0\" serial=\"7\" timestamp=\"" + boost::lexical_cast<std::string>(time(NULL)) + ".0\">\n"
|
||||
"<Lib>\n"
|
||||
"<Account>\n"
|
||||
"<IdleTimeForAway>30000000</IdleTimeForAway>\n"
|
||||
"<IdleTimeForNA>300000000</IdleTimeForNA>\n"
|
||||
"<LastUsed>" + boost::lexical_cast<std::string>(time(NULL)) + "</LastUsed>\n"
|
||||
"</Account>\n"
|
||||
"</Lib>\n"
|
||||
"<UI>\n"
|
||||
"<API>\n"
|
||||
"<Authorizations>Spectrum</Authorizations>\n"
|
||||
"<BlockedPrograms></BlockedPrograms>\n"
|
||||
"</API>\n"
|
||||
"</UI>\n"
|
||||
"</config>\n";
|
||||
g_file_set_contents(std::string(std::string("/tmp/skype/") + m_username + "/" + m_username +"/config.xml").c_str(), config_xml.c_str(), -1, NULL);
|
||||
|
||||
std::string db_path = std::string("/tmp/skype/") + m_username;
|
||||
char *db = (char *) malloc(db_path.size() + 1);
|
||||
strcpy(db, db_path.c_str());
|
||||
LOG4CXX_INFO(logger, m_username << ": Spawning new Skype instance dbpath=" << db);
|
||||
gchar* argv[6] = {"skype", "--disable-cleanlooks", "--pipelogin", "--dbpath", db, 0};
|
||||
|
||||
int fd;
|
||||
g_spawn_async_with_pipes(NULL,
|
||||
argv,
|
||||
NULL /*envp*/,
|
||||
G_SPAWN_SEARCH_PATH,
|
||||
NULL /*child_setup*/,
|
||||
NULL /*user_data*/,
|
||||
&m_pid /*child_pid*/,
|
||||
&fd,
|
||||
NULL,
|
||||
&fd_output,
|
||||
NULL /*error*/);
|
||||
std::string login_data = std::string(m_username + " " + m_password + "\n");
|
||||
LOG4CXX_INFO(logger, m_username << ": Login data=" << login_data);
|
||||
write(fd, login_data.c_str(), login_data.size());
|
||||
close(fd);
|
||||
|
||||
fcntl (fd_output, F_SETFL, O_NONBLOCK);
|
||||
|
||||
free(db);
|
||||
|
||||
//Initialise threading
|
||||
dbus_threads_init_default();
|
||||
|
||||
if (m_connection == NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, "Creating DBus connection.");
|
||||
GError *error = NULL;
|
||||
m_connection = dbus_g_bus_get (DBUS_BUS_SESSION, &error);
|
||||
if (m_connection == NULL && error != NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS Error: " << error->message);
|
||||
g_error_free(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_timer = g_timeout_add_seconds(1, create_dbus_proxy, this);
|
||||
}
|
||||
|
||||
bool Skype::loadSkypeBuddies() {
|
||||
// std::string re = "CONNSTATUS OFFLINE";
|
||||
// while (re == "CONNSTATUS OFFLINE" || re.empty()) {
|
||||
// sleep(1);
|
||||
|
||||
gchar buffer[1024];
|
||||
int bytes_read = read(fd_output, buffer, 1023);
|
||||
if (bytes_read > 0) {
|
||||
buffer[bytes_read] = 0;
|
||||
np->handleDisconnected(m_user, 0, buffer);
|
||||
close(fd_output);
|
||||
logout();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
std::string re = send_command("NAME Spectrum");
|
||||
if (m_counter++ > 15) {
|
||||
np->handleDisconnected(m_user, 0, "");
|
||||
close(fd_output);
|
||||
logout();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (re.empty() || re == "CONNSTATUS OFFLINE") {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
close(fd_output);
|
||||
|
||||
if (send_command("PROTOCOL 7") != "PROTOCOL 7") {
|
||||
np->handleDisconnected(m_user, 0, "Skype is not ready");
|
||||
logout();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
np->handleConnected(m_user);
|
||||
|
||||
std::map<std::string, std::string> group_map;
|
||||
std::string groups = send_command("SEARCH GROUPS CUSTOM");
|
||||
groups = groups.substr(groups.find(' ') + 1);
|
||||
std::vector<std::string> grps;
|
||||
boost::split(grps, groups, boost::is_any_of(","));
|
||||
BOOST_FOREACH(std::string grp, grps) {
|
||||
std::vector<std::string> data;
|
||||
std::string name = send_command("GET GROUP " + grp + " DISPLAYNAME");
|
||||
boost::split(data, name, boost::is_any_of(" "));
|
||||
name = name.substr(name.find("DISPLAYNAME") + 12);
|
||||
|
||||
std::string users = send_command("GET GROUP " + data[1] + " USERS");
|
||||
users = name.substr(name.find("USERS") + 6);
|
||||
boost::split(data, users, boost::is_any_of(","));
|
||||
BOOST_FOREACH(std::string u, data) {
|
||||
group_map[u] = grp;
|
||||
}
|
||||
}
|
||||
|
||||
std::string friends = send_command("GET AUTH_CONTACTS_PROFILES");
|
||||
|
||||
char **full_friends_list = g_strsplit((strchr(friends.c_str(), ' ')+1), ";", 0);
|
||||
if (full_friends_list && full_friends_list[0])
|
||||
{
|
||||
//in the format of: username;full name;phone;office phone;mobile phone;
|
||||
// online status;friendly name;voicemail;mood
|
||||
// (comma-seperated lines, usernames can have comma's)
|
||||
|
||||
for (int i=0; full_friends_list[i] && *full_friends_list[i] != '\0'; i+=8)
|
||||
{
|
||||
std::string buddy = full_friends_list[i];
|
||||
|
||||
if (buddy[0] == ',') {
|
||||
buddy.erase(buddy.begin());
|
||||
}
|
||||
|
||||
if (buddy.rfind(",") != std::string::npos) {
|
||||
buddy = buddy.substr(buddy.rfind(","));
|
||||
}
|
||||
|
||||
if (buddy[0] == ',') {
|
||||
buddy.erase(buddy.begin());
|
||||
}
|
||||
|
||||
LOG4CXX_INFO(logger, "Got buddy " << buddy);
|
||||
std::string st = full_friends_list[i + 5];
|
||||
|
||||
pbnetwork::StatusType status = getStatus(st);
|
||||
|
||||
std::string alias = full_friends_list[i + 6];
|
||||
|
||||
std::string mood_text = "";
|
||||
if (full_friends_list[i + 8] && *full_friends_list[i + 8] != '\0' && *full_friends_list[i + 8] != ',') {
|
||||
mood_text = full_friends_list[i + 8];
|
||||
}
|
||||
|
||||
std::vector<std::string> groups;
|
||||
if (group_map.find(buddy) != group_map.end()) {
|
||||
groups.push_back(group_map[buddy]);
|
||||
}
|
||||
np->handleBuddyChanged(m_user, buddy, alias, groups, status, mood_text);
|
||||
}
|
||||
}
|
||||
g_strfreev(full_friends_list);
|
||||
|
||||
send_command("SET AUTOAWAY OFF");
|
||||
send_command("SET USERSTATUS ONLINE");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void Skype::logout() {
|
||||
if (m_pid != 0) {
|
||||
send_command("SET USERSTATUS INVISIBLE");
|
||||
send_command("SET USERSTATUS OFFLINE");
|
||||
sleep(2);
|
||||
g_object_unref(m_proxy);
|
||||
LOG4CXX_INFO(logger, m_username << ": Killing Skype instance");
|
||||
kill((int) m_pid, SIGTERM);
|
||||
m_pid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Skype::send_command(const std::string &message) {
|
||||
GError *error = NULL;
|
||||
gchar *str = NULL;
|
||||
// int message_num;
|
||||
// gchar error_return[30];
|
||||
|
||||
if (!dbus_g_proxy_call (m_proxy, "Invoke", &error, G_TYPE_STRING, message.c_str(), G_TYPE_INVALID,
|
||||
G_TYPE_STRING, &str, G_TYPE_INVALID))
|
||||
|
||||
if (!dbus_g_proxy_call (m_proxy, "Invoke", &error, G_TYPE_STRING, message.c_str(), G_TYPE_INVALID,
|
||||
G_TYPE_STRING, &str, G_TYPE_INVALID))
|
||||
{
|
||||
if (error && error->message)
|
||||
{
|
||||
if (error && error->message)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS Error: " << error->message);
|
||||
g_error_free(error);
|
||||
} else {
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS no response");
|
||||
}
|
||||
|
||||
}
|
||||
if (str != NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS:" << str);
|
||||
}
|
||||
return str ? std::string(str) : std::string();
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS Error: " << error->message);
|
||||
g_error_free(error);
|
||||
} else {
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS no response");
|
||||
}
|
||||
|
||||
}
|
||||
if (str != NULL)
|
||||
{
|
||||
LOG4CXX_INFO(logger, m_username << ": DBUS:" << str);
|
||||
}
|
||||
return str ? std::string(str) : std::string();
|
||||
}
|
||||
|
||||
static void handle_skype_message(std::string &message, Skype *sk) {
|
||||
std::vector<std::string> cmd;
|
||||
boost::split(cmd, message, boost::is_any_of(" "));
|
||||
|
@ -684,6 +740,10 @@ static void io_destroy(gpointer data) {
|
|||
exit(1);
|
||||
}
|
||||
|
||||
static void log_glib_error(const gchar *string) {
|
||||
LOG4CXX_ERROR(logger, "GLIB ERROR:" << string);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GError *error = NULL;
|
||||
GOptionContext *context;
|
||||
|
@ -771,9 +831,10 @@ int main(int argc, char **argv) {
|
|||
|
||||
m_sock = create_socket(host, port);
|
||||
|
||||
g_set_printerr_handler(log_glib_error);
|
||||
|
||||
GIOChannel *channel;
|
||||
GIOCondition cond = (GIOCondition) READ_COND;
|
||||
GIOCondition cond = (GIOCondition) G_IO_IN;
|
||||
channel = g_io_channel_unix_new(m_sock);
|
||||
g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, cond, transportDataReceived, NULL, io_destroy);
|
||||
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/**
|
||||
* XMPP - libpurple transport
|
||||
*
|
||||
* Copyright (C) 2009, Jan Kaluza <hanzz@soc.pidgin.im>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
|
||||
*/
|
||||
|
||||
#include "spectrumeventloop.h"
|
||||
#include "glib.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#ifdef WITH_LIBEVENT
|
||||
#include <event.h>
|
||||
#endif
|
||||
|
||||
|
||||
using namespace Swift;
|
||||
|
||||
// Fires the event's callback and frees the event
|
||||
static gboolean processEvent(void *data) {
|
||||
Event *ev = (Event *) data;
|
||||
ev->callback();
|
||||
delete ev;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
SpectrumEventLoop::SpectrumEventLoop() : m_isRunning(false) {
|
||||
m_loop = NULL;
|
||||
if (true) {
|
||||
m_loop = g_main_loop_new(NULL, FALSE);
|
||||
}
|
||||
#ifdef WITH_LIBEVENT
|
||||
else {
|
||||
/*struct event_base *base = (struct event_base *)*/
|
||||
event_init();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SpectrumEventLoop::~SpectrumEventLoop() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void SpectrumEventLoop::run() {
|
||||
m_isRunning = true;
|
||||
if (m_loop) {
|
||||
g_main_loop_run(m_loop);
|
||||
}
|
||||
#ifdef WITH_LIBEVENT
|
||||
else {
|
||||
event_loop(0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpectrumEventLoop::stop() {
|
||||
std::cout << "stopped loop\n";
|
||||
if (!m_isRunning)
|
||||
return;
|
||||
if (m_loop) {
|
||||
g_main_loop_quit(m_loop);
|
||||
g_main_loop_unref(m_loop);
|
||||
m_loop = NULL;
|
||||
}
|
||||
#ifdef WITH_LIBEVENT
|
||||
else {
|
||||
event_loopexit(NULL);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpectrumEventLoop::post(const Event& event) {
|
||||
// pass copy of event to main thread
|
||||
Event *ev = new Event(event.owner, event.callback);
|
||||
g_timeout_add(0, processEvent, ev);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* XMPP - libpurple transport
|
||||
*
|
||||
* Copyright (C) 2009, Jan Kaluza <hanzz@soc.pidgin.im>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* 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 General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef SPECTRUM_EVENT_LOOP_H
|
||||
#define SPECTRUM_EVENT_LOOP_H
|
||||
|
||||
#include <vector>
|
||||
#include "Swiften/EventLoop/EventLoop.h"
|
||||
#include "glib.h"
|
||||
|
||||
// Event loop implementation for Spectrum
|
||||
class SpectrumEventLoop : public Swift::EventLoop {
|
||||
public:
|
||||
// Creates event loop according to CONFIG().eventloop settings.
|
||||
SpectrumEventLoop();
|
||||
~SpectrumEventLoop();
|
||||
|
||||
// Executes the eventloop.
|
||||
void run();
|
||||
|
||||
// Stops tht eventloop.
|
||||
void stop();
|
||||
|
||||
// Posts new Swift::Event to main thread.
|
||||
virtual void post(const Swift::Event& event);
|
||||
|
||||
private:
|
||||
bool m_isRunning;
|
||||
GMainLoop *m_loop;
|
||||
};
|
||||
|
||||
#endif
|
10
backends/smstools3/CMakeLists.txt
Normal file
10
backends/smstools3/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
cmake_minimum_required(VERSION 2.6)
|
||||
|
||||
FILE(GLOB SRC *.c *.cpp)
|
||||
|
||||
ADD_EXECUTABLE(spectrum2_smstools3_backend ${SRC})
|
||||
|
||||
target_link_libraries(spectrum2_smstools3_backend transport pthread ${Boost_LIBRARIES} ${SWIFTEN_LIBRARY} ${LOG4CXX_LIBRARIES})
|
||||
|
||||
INSTALL(TARGETS spectrum2_smstools3_backend RUNTIME DESTINATION bin)
|
||||
|
397
backends/smstools3/main.cpp
Normal file
397
backends/smstools3/main.cpp
Normal file
|
@ -0,0 +1,397 @@
|
|||
/*
|
||||
* Copyright (C) 2008-2009 J-P Nurmi jpnurmi@gmail.com
|
||||
*
|
||||
* This example is free, and not covered by LGPL license. There is no
|
||||
* restriction applied to their modification, redistribution, using and so on.
|
||||
* You can study them, modify them, use them in your own program - either
|
||||
* completely or partially. By using it you may give me some credits in your
|
||||
* program, but you don't have to.
|
||||
*/
|
||||
|
||||
#include "transport/config.h"
|
||||
#include "transport/networkplugin.h"
|
||||
#include "transport/sqlite3backend.h"
|
||||
#include "transport/mysqlbackend.h"
|
||||
#include "transport/pqxxbackend.h"
|
||||
#include "transport/storagebackend.h"
|
||||
#include "Swiften/Swiften.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include "unistd.h"
|
||||
#include "signal.h"
|
||||
#include "sys/wait.h"
|
||||
#include "sys/signal.h"
|
||||
#include <fstream>
|
||||
#include <streambuf>
|
||||
|
||||
Swift::SimpleEventLoop *loop_;
|
||||
|
||||
#include "log4cxx/logger.h"
|
||||
#include "log4cxx/consoleappender.h"
|
||||
#include "log4cxx/patternlayout.h"
|
||||
#include "log4cxx/propertyconfigurator.h"
|
||||
#include "log4cxx/helpers/properties.h"
|
||||
#include "log4cxx/helpers/fileinputstream.h"
|
||||
#include "log4cxx/helpers/transcoder.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
using namespace boost::filesystem;
|
||||
|
||||
using namespace boost::program_options;
|
||||
using namespace Transport;
|
||||
|
||||
using namespace log4cxx;
|
||||
|
||||
static LoggerPtr logger = log4cxx::Logger::getLogger("SMSNetworkPlugin");
|
||||
|
||||
#define INTERNAL_USER "/sms@backend@internal@user"
|
||||
|
||||
class SMSNetworkPlugin;
|
||||
SMSNetworkPlugin * np = NULL;
|
||||
StorageBackend *storageBackend;
|
||||
|
||||
class SMSNetworkPlugin : public NetworkPlugin {
|
||||
public:
|
||||
Swift::BoostNetworkFactories *m_factories;
|
||||
Swift::BoostIOServiceThread m_boostIOServiceThread;
|
||||
boost::shared_ptr<Swift::Connection> m_conn;
|
||||
Swift::Timer::ref m_timer;
|
||||
int m_internalUser;
|
||||
|
||||
SMSNetworkPlugin(Config *config, Swift::SimpleEventLoop *loop, const std::string &host, int port) : NetworkPlugin() {
|
||||
this->config = config;
|
||||
m_factories = new Swift::BoostNetworkFactories(loop);
|
||||
m_conn = m_factories->getConnectionFactory()->createConnection();
|
||||
m_conn->onDataRead.connect(boost::bind(&SMSNetworkPlugin::_handleDataRead, this, _1));
|
||||
m_conn->connect(Swift::HostAddressPort(Swift::HostAddress(host), port));
|
||||
// m_conn->onConnectFinished.connect(boost::bind(&FrotzNetworkPlugin::_handleConnected, this, _1));
|
||||
// m_conn->onDisconnected.connect(boost::bind(&FrotzNetworkPlugin::handleDisconnected, this));
|
||||
|
||||
LOG4CXX_INFO(logger, "Starting the plugin.");
|
||||
|
||||
m_timer = m_factories->getTimerFactory()->createTimer(5000);
|
||||
m_timer->onTick.connect(boost::bind(&SMSNetworkPlugin::handleSMSDir, this));
|
||||
m_timer->start();
|
||||
|
||||
// We're reusing our database model here. Buddies of user with JID INTERNAL_USER are there
|
||||
// to match received GSM messages from number N with the XMPP users who sent message to number N.
|
||||
// BuddyName = GSM number
|
||||
// Alias = XMPP user JID to which the messages from this number is sent to.
|
||||
// TODO: This should be per Modem!!!
|
||||
UserInfo info;
|
||||
info.jid = INTERNAL_USER;
|
||||
info.password = "";
|
||||
storageBackend->setUser(info);
|
||||
storageBackend->getUser(INTERNAL_USER, info);
|
||||
m_internalUser = info.id;
|
||||
}
|
||||
|
||||
|
||||
void handleSMS(const std::string &sms) {
|
||||
LOG4CXX_INFO(logger, "Handling SMS " << sms << ".")
|
||||
std::ifstream t(sms.c_str());
|
||||
std::string str;
|
||||
|
||||
t.seekg(0, std::ios::end);
|
||||
str.reserve(t.tellg());
|
||||
t.seekg(0, std::ios::beg);
|
||||
|
||||
str.assign((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
|
||||
|
||||
std::string from = "";
|
||||
std::string msg = "";
|
||||
while(str.find("\n") != std::string::npos) {
|
||||
std::string line = str.substr(0, str.find("\n"));
|
||||
if (line.find("From: ") == 0) {
|
||||
from = line.substr(strlen("From: "));
|
||||
}
|
||||
else if (line.empty()) {
|
||||
msg = str.substr(1);
|
||||
break;
|
||||
}
|
||||
str = str.substr(str.find("\n") + 1);
|
||||
}
|
||||
|
||||
std::list<BuddyInfo> roster;
|
||||
storageBackend->getBuddies(m_internalUser, roster);
|
||||
|
||||
std::string to;
|
||||
BOOST_FOREACH(BuddyInfo &b, roster) {
|
||||
if (b.legacyName == from) {
|
||||
to = b.alias;
|
||||
}
|
||||
}
|
||||
|
||||
if (to.empty()) {
|
||||
LOG4CXX_WARN(logger, "Received SMS from " << from << ", but this number is not associated with any XMPP user.");
|
||||
}
|
||||
|
||||
LOG4CXX_INFO(logger, "Forwarding SMS from " << from << " to " << to << ".");
|
||||
handleMessage(to, from, msg);
|
||||
}
|
||||
|
||||
void handleSMSDir() {
|
||||
std::string dir = "/var/spool/sms/incoming/";
|
||||
if (config->getUnregistered().find("backend.incoming_dir") != config->getUnregistered().end()) {
|
||||
dir = config->getUnregistered().find("backend.incoming_dir")->second;
|
||||
}
|
||||
LOG4CXX_INFO(logger, "Checking directory " << dir << " for incoming SMS.");
|
||||
|
||||
path p(dir);
|
||||
directory_iterator end_itr;
|
||||
for (directory_iterator itr(p); itr != end_itr; ++itr) {
|
||||
|
||||
try {
|
||||
if (is_regular(itr->path())) {
|
||||
handleSMS(itr->path().string());
|
||||
remove(itr->path());
|
||||
}
|
||||
}
|
||||
catch (const filesystem_error& ex) {
|
||||
LOG4CXX_ERROR(logger, "Error when removing the SMS: " << ex.what() << ".");
|
||||
}
|
||||
}
|
||||
m_timer->start();
|
||||
}
|
||||
|
||||
void sendSMS(const std::string &to, const std::string &msg) {
|
||||
// TODO: Probably
|
||||
std::string data = "To: " + to + "\n";
|
||||
data += "\n";
|
||||
data += msg;
|
||||
|
||||
// generate random string here...
|
||||
std::string bucket = "abcdefghijklmnopqrstuvwxyz";
|
||||
std::string uuid;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
uuid += bucket[rand() % bucket.size()];
|
||||
}
|
||||
std::ofstream myfile;
|
||||
myfile.open (std::string("/var/spool/sms/outgoing/spectrum." + uuid).c_str());
|
||||
myfile << data;
|
||||
myfile.close();
|
||||
}
|
||||
|
||||
void sendData(const std::string &string) {
|
||||
m_conn->write(Swift::createSafeByteArray(string));
|
||||
}
|
||||
|
||||
void _handleDataRead(boost::shared_ptr<Swift::SafeByteArray> data) {
|
||||
std::string d(data->begin(), data->end());
|
||||
handleDataRead(d);
|
||||
}
|
||||
|
||||
void handleLoginRequest(const std::string &user, const std::string &legacyName, const std::string &password) {
|
||||
UserInfo info;
|
||||
if (!storageBackend->getUser(user, info)) {
|
||||
handleDisconnected(user, 0, "Not registered user.");
|
||||
return;
|
||||
}
|
||||
std::list<BuddyInfo> roster;
|
||||
storageBackend->getBuddies(info.id, roster);
|
||||
|
||||
// Send available presence to every number in the roster.
|
||||
BOOST_FOREACH(BuddyInfo &b, roster) {
|
||||
handleBuddyChanged(user, b.legacyName, b.alias, b.groups, pbnetwork::STATUS_ONLINE);
|
||||
}
|
||||
|
||||
np->handleConnected(user);
|
||||
}
|
||||
|
||||
void handleLogoutRequest(const std::string &user, const std::string &legacyName) {
|
||||
}
|
||||
|
||||
void handleMessageSendRequest(const std::string &user, const std::string &legacyName, const std::string &message, const std::string &xhtml = "") {
|
||||
// Remove trailing +, because smstools doesn't use it in "From: " field for received messages.
|
||||
std::string n = legacyName;
|
||||
if (n.find("+") == 0) {
|
||||
n = n.substr(1);
|
||||
}
|
||||
|
||||
// Create GSM Number - XMPP user pair to match the potential response and send it to the proper JID.
|
||||
BuddyInfo info;
|
||||
info.legacyName = n;
|
||||
info.alias = user;
|
||||
info.id = -1;
|
||||
info.subscription = "both";
|
||||
info.flags = 0;
|
||||
storageBackend->addBuddy(m_internalUser, info);
|
||||
|
||||
LOG4CXX_INFO(logger, "Sending SMS from " << user << " to " << n << ".");
|
||||
sendSMS(n, message);
|
||||
}
|
||||
|
||||
void handleJoinRoomRequest(const std::string &user, const std::string &room, const std::string &nickname, const std::string &password) {
|
||||
}
|
||||
|
||||
void handleLeaveRoomRequest(const std::string &user, const std::string &room) {
|
||||
}
|
||||
|
||||
void handleBuddyUpdatedRequest(const std::string &user, const std::string &buddyName, const std::string &alias, const std::vector<std::string> &groups) {
|
||||
LOG4CXX_INFO(logger, user << ": Added buddy " << buddyName << ".");
|
||||
handleBuddyChanged(user, buddyName, alias, groups, pbnetwork::STATUS_ONLINE);
|
||||
}
|
||||
|
||||
void handleBuddyRemovedRequest(const std::string &user, const std::string &buddyName, const std::vector<std::string> &groups) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
|
||||
Config *config;
|
||||
};
|
||||
|
||||
static void spectrum_sigchld_handler(int sig)
|
||||
{
|
||||
int status;
|
||||
pid_t pid;
|
||||
|
||||
do {
|
||||
pid = waitpid(-1, &status, WNOHANG);
|
||||
} while (pid != 0 && pid != (pid_t)-1);
|
||||
|
||||
if ((pid == (pid_t) - 1) && (errno != ECHILD)) {
|
||||
char errmsg[BUFSIZ];
|
||||
snprintf(errmsg, BUFSIZ, "Warning: waitpid() returned %d", pid);
|
||||
perror(errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main (int argc, char* argv[]) {
|
||||
std::string host;
|
||||
int port;
|
||||
|
||||
if (signal(SIGCHLD, spectrum_sigchld_handler) == SIG_ERR) {
|
||||
std::cout << "SIGCHLD handler can't be set\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
boost::program_options::options_description desc("Usage: spectrum [OPTIONS] <config_file.cfg>\nAllowed options");
|
||||
desc.add_options()
|
||||
("host,h", value<std::string>(&host), "host")
|
||||
("port,p", value<int>(&port), "port")
|
||||
;
|
||||
try
|
||||
{
|
||||
boost::program_options::variables_map vm;
|
||||
boost::program_options::store(boost::program_options::parse_command_line(argc, argv, desc), vm);
|
||||
boost::program_options::notify(vm);
|
||||
}
|
||||
catch (std::runtime_error& e)
|
||||
{
|
||||
std::cout << desc << "\n";
|
||||
exit(1);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::cout << desc << "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
if (argc < 5) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// QStringList channels;
|
||||
// for (int i = 3; i < argc; ++i)
|
||||
// {
|
||||
// channels.append(argv[i]);
|
||||
// }
|
||||
//
|
||||
// MyIrcSession session;
|
||||
// session.setNick(argv[2]);
|
||||
// session.setAutoJoinChannels(channels);
|
||||
// session.connectToServer(argv[1], 6667);
|
||||
|
||||
Config config;
|
||||
if (!config.load(argv[5])) {
|
||||
std::cerr << "Can't open " << argv[1] << " configuration file.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (CONFIG_STRING(&config, "logging.backend_config").empty()) {
|
||||
LoggerPtr root = log4cxx::Logger::getRootLogger();
|
||||
#ifndef _MSC_VER
|
||||
root->addAppender(new ConsoleAppender(new PatternLayout("%d %-5p %c: %m%n")));
|
||||
#else
|
||||
root->addAppender(new ConsoleAppender(new PatternLayout(L"%d %-5p %c: %m%n")));
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
log4cxx::helpers::Properties p;
|
||||
log4cxx::helpers::FileInputStream *istream = new log4cxx::helpers::FileInputStream(CONFIG_STRING(&config, "logging.backend_config"));
|
||||
p.load(istream);
|
||||
LogString pid, jid;
|
||||
log4cxx::helpers::Transcoder::decode(boost::lexical_cast<std::string>(getpid()), pid);
|
||||
log4cxx::helpers::Transcoder::decode(CONFIG_STRING(&config, "service.jid"), jid);
|
||||
#ifdef _MSC_VER
|
||||
p.setProperty(L"pid", pid);
|
||||
p.setProperty(L"jid", jid);
|
||||
#else
|
||||
p.setProperty("pid", pid);
|
||||
p.setProperty("jid", jid);
|
||||
#endif
|
||||
log4cxx::PropertyConfigurator::configure(p);
|
||||
}
|
||||
|
||||
#ifdef WITH_SQLITE
|
||||
if (CONFIG_STRING(&config, "database.type") == "sqlite3") {
|
||||
storageBackend = new SQLite3Backend(&config);
|
||||
if (!storageBackend->connect()) {
|
||||
std::cerr << "Can't connect to database. Check the log to find out the reason.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (CONFIG_STRING(&config, "database.type") == "sqlite3") {
|
||||
std::cerr << "Spectrum2 is not compiled with mysql backend.\n";
|
||||
return -2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MYSQL
|
||||
if (CONFIG_STRING(&config, "database.type") == "mysql") {
|
||||
storageBackend = new MySQLBackend(&config);
|
||||
if (!storageBackend->connect()) {
|
||||
std::cerr << "Can't connect to database. Check the log to find out the reason.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (CONFIG_STRING(&config, "database.type") == "mysql") {
|
||||
std::cerr << "Spectrum2 is not compiled with mysql backend.\n";
|
||||
return -2;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef WITH_PQXX
|
||||
if (CONFIG_STRING(&config, "database.type") == "pqxx") {
|
||||
storageBackend = new PQXXBackend(&config);
|
||||
if (!storageBackend->connect()) {
|
||||
std::cerr << "Can't connect to database. Check the log to find out the reason.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (CONFIG_STRING(&config, "database.type") == "pqxx") {
|
||||
std::cerr << "Spectrum2 is not compiled with pqxx backend.\n";
|
||||
return -2;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (CONFIG_STRING(&config, "database.type") != "mysql" && CONFIG_STRING(&config, "database.type") != "sqlite3"
|
||||
&& CONFIG_STRING(&config, "database.type") != "pqxx" && CONFIG_STRING(&config, "database.type") != "none") {
|
||||
std::cerr << "Unknown storage backend " << CONFIG_STRING(&config, "database.type") << "\n";
|
||||
return -2;
|
||||
}
|
||||
|
||||
Swift::SimpleEventLoop eventLoop;
|
||||
loop_ = &eventLoop;
|
||||
np = new SMSNetworkPlugin(&config, &eventLoop, host, port);
|
||||
loop_->run();
|
||||
|
||||
return 0;
|
||||
}
|
53
cmake_modules/dbusConfig.cmake
Normal file
53
cmake_modules/dbusConfig.cmake
Normal file
|
@ -0,0 +1,53 @@
|
|||
# - Try to find LIBDBUS GLIB Bindings
|
||||
# Find LIBDBUSGLIB headers, libraries and the answer to all questions.
|
||||
#
|
||||
# LIBDBUSGLIB_FOUND True if libdbus-glib got found
|
||||
# LIBDBUSGLIB_INCLUDE_DIRS Location of libdbus-glib headers
|
||||
# LIBDBUSGLIB_LIBRARIES List of libraries to use libdbus-glib
|
||||
#
|
||||
# Copyright (c) 2008 Bjoern Ricks <bjoern.ricks@googlemail.com>
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the New
|
||||
# BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
#
|
||||
|
||||
INCLUDE( FindPkgConfig )
|
||||
|
||||
IF ( LibDbusGlib_FIND_REQUIRED )
|
||||
SET( _pkgconfig_REQUIRED "REQUIRED" )
|
||||
ELSE( LibDbusGlib_FIND_REQUIRED )
|
||||
SET( _pkgconfig_REQUIRED "" )
|
||||
ENDIF ( LibDbusGlib_FIND_REQUIRED )
|
||||
|
||||
IF ( LIBDBUSGLIB_MIN_VERSION )
|
||||
PKG_SEARCH_MODULE( LIBDBUSGLIB ${_pkgconfig_REQUIRED} dbus-glib-1>=${LIBDBUSGLIB_MIN_VERSION} )
|
||||
ELSE ( LIBDBUSGLIB_MIN_VERSION )
|
||||
PKG_SEARCH_MODULE( LIBDBUSGLIB ${_pkgconfig_REQUIRED} dbus-glib-1 )
|
||||
ENDIF ( LIBDBUSGLIB_MIN_VERSION )
|
||||
|
||||
|
||||
IF( NOT LIBDBUSGLIB_FOUND AND NOT PKG_CONFIG_FOUND )
|
||||
FIND_PATH( LIBDBUSGLIB_INCLUDE_DIRS dbus/dbus-glib.h PATH_SUFFIXES dbus-1.0 dbus )
|
||||
FIND_LIBRARY( LIBDBUSGLIB_LIBRARIES dbus-glib dbus-glib-1)
|
||||
|
||||
# Report results
|
||||
IF ( LIBDBUSGLIB_LIBRARIES AND LIBDBUSGLIB_INCLUDE_DIRS )
|
||||
SET( LIBDBUSGLIB_FOUND 1 )
|
||||
IF ( NOT LIBDBUSGLIB_FIND_QUIETLY )
|
||||
MESSAGE( STATUS "Found libdbus-glib: ${LIBDBUSGLIB_LIBRARIES} ${LIBDBUSGLIB_INCLUDE_DIRS}" )
|
||||
ENDIF ( NOT LIBDBUSGLIB_FIND_QUIETLY )
|
||||
ELSE ( LIBDBUSGLIB_LIBRARIES AND LIBDBUSGLIB_INCLUDE_DIRS )
|
||||
IF ( LIBDBUSGLIB_FIND_REQUIRED )
|
||||
MESSAGE( SEND_ERROR "Could NOT find libdbus-glib" )
|
||||
ELSE ( LIBDBUSGLIB_FIND_REQUIRED )
|
||||
IF ( NOT LIBDBUSGLIB_FIND_QUIETLY )
|
||||
MESSAGE( STATUS "Could NOT find libdbus-glib" )
|
||||
ENDIF ( NOT LIBDBUSGLIB_FIND_QUIETLY )
|
||||
ENDIF ( LIBDBUSGLIB_FIND_REQUIRED )
|
||||
ENDIF ( LIBDBUSGLIB_LIBRARIES AND LIBDBUSGLIB_INCLUDE_DIRS )
|
||||
else()
|
||||
MESSAGE( STATUS "Found libdbus-glib: ${LIBDBUSGLIB_LIBRARIES} ${LIBDBUSGLIB_INCLUDE_DIRS}" )
|
||||
ENDIF()
|
||||
|
||||
MARK_AS_ADVANCED( LIBDBUSGLIB_LIBRARIES LIBDBUSGLIB_INCLUDE_DIRS )
|
|
@ -14,6 +14,13 @@
|
|||
#include <openssl/err.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
#include "log4cxx/logger.h"
|
||||
#include "log4cxx/consoleappender.h"
|
||||
#include "log4cxx/patternlayout.h"
|
||||
#include "log4cxx/propertyconfigurator.h"
|
||||
using namespace log4cxx;
|
||||
static LoggerPtr logger = Logger::getLogger("OpenSSLServerContext");
|
||||
|
||||
|
||||
#include "Swiften/TLS/OpenSSL/OpenSSLServerContext.h"
|
||||
#include "Swiften/TLS/OpenSSL/OpenSSLCertificate.h"
|
||||
|
@ -179,7 +186,7 @@ void OpenSSLServerContext::sendPendingDataToApplication() {
|
|||
|
||||
bool OpenSSLServerContext::setServerCertificate(const PKCS12Certificate& certificate) {
|
||||
if (certificate.isNull()) {
|
||||
// std::cout << "error 1\n";
|
||||
LOG4CXX_ERROR(logger, "TLS WILL NOT WORK: Certificate can't be loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -189,7 +196,7 @@ bool OpenSSLServerContext::setServerCertificate(const PKCS12Certificate& certifi
|
|||
boost::shared_ptr<PKCS12> pkcs12(d2i_PKCS12_bio(bio, NULL), PKCS12_free);
|
||||
BIO_free(bio);
|
||||
if (!pkcs12) {
|
||||
// std::cout << "error 2\n";
|
||||
LOG4CXX_ERROR(logger, "TLS WILL NOT WORK: Certificate is not in PKCS#12 format.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -199,7 +206,7 @@ bool OpenSSLServerContext::setServerCertificate(const PKCS12Certificate& certifi
|
|||
STACK_OF(X509)* caCertsPtr = 0;
|
||||
int result = PKCS12_parse(pkcs12.get(), reinterpret_cast<const char*>(vecptr(certificate.getPassword())), &privateKeyPtr, &certPtr, &caCertsPtr);
|
||||
if (result != 1) {
|
||||
// std::cout << "error 3\n";
|
||||
LOG4CXX_ERROR(logger, "TLS WILL NOT WORK: Certificate is not in PKCS#12 format.");
|
||||
return false;
|
||||
}
|
||||
boost::shared_ptr<X509> cert(certPtr, X509_free);
|
||||
|
@ -208,11 +215,11 @@ bool OpenSSLServerContext::setServerCertificate(const PKCS12Certificate& certifi
|
|||
|
||||
// Use the key & certificates
|
||||
if (SSL_CTX_use_certificate(context_, cert.get()) != 1) {
|
||||
// std::cout << "error 4\n";
|
||||
LOG4CXX_ERROR(logger, "TLS WILL NOT WORK: Can't use this certificate");
|
||||
return false;
|
||||
}
|
||||
if (SSL_CTX_use_PrivateKey(context_, privateKey.get()) != 1) {
|
||||
// std::cout << "error 5\n";
|
||||
LOG4CXX_ERROR(logger, "TLS WILL NOT WORK: Can't use this private key");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -13,7 +13,8 @@ admin_password=test
|
|||
#cert=server.pfx #patch to PKCS#12 certificate
|
||||
#cert_password=test #password to that certificate if any
|
||||
users_per_backend=10
|
||||
backend=/home/hanzz/code/libtransport/backends/libpurple/spectrum2_libpurple_backend
|
||||
#backend=/home/hanzz/code/libtransport/backends/libpurple/spectrum2_libpurple_backend
|
||||
backend=/home/hanzz/code/libtransport/backends/smstools3/spectrum2_smstools3_backend
|
||||
#backend=/usr/bin/mono /home/hanzz/code/networkplugin-csharp/msnp-sharp-backend/bin/Debug/msnp-sharp-backend.exe
|
||||
#backend=/home/hanzz/code/libtransport/backends/frotz/spectrum2_frotz_backend
|
||||
#backend=/home/hanzz/code/libtransport/backends/libircclient-qt/spectrum2_libircclient-qt_backend
|
||||
|
@ -25,12 +26,19 @@ irc_server=irc.freenode.org
|
|||
[backend]
|
||||
#default_avatar=catmelonhead.jpg
|
||||
#no_vcard_fetch=true
|
||||
incoming_dir=/var/spool/sms/incoming
|
||||
|
||||
[logging]
|
||||
#config=logging.cfg # log4cxx/log4j logging configuration file
|
||||
#backend_config=/home/hanzz/code/libtransport/spectrum/src/backend-logging.cfg # log4cxx/log4j logging configuration file for backends
|
||||
|
||||
[database]
|
||||
type = none # or "none" without database backend
|
||||
database = test.sql
|
||||
prefix=icq
|
||||
#type = sqlite3 # or "none" without database backend
|
||||
#database = test.sql
|
||||
#prefix=icq
|
||||
type = mysql # or "none" without database backend.......................................................................................................................
|
||||
database = test
|
||||
prefix=
|
||||
user=root
|
||||
password=yourrootsqlpassword
|
||||
#encryption_key=hanzzik
|
||||
|
|
|
@ -41,7 +41,7 @@ users_per_backend=10
|
|||
backend=/usr/bin/spectrum2_libpurple_backend
|
||||
#backend=/usr/bin/spectrum2_libircclient-qt_backend
|
||||
# For skype:
|
||||
#backend=/usr/bin/setsid /usr/bin/xvfb-run -n BACKEND_ID -s "-screen 0 10x10x8" -f /tmp/x-skype-gw /usr/bin/spectrum2_skype_backend
|
||||
#backend=/usr/bin/xvfb-run -n BACKEND_ID -s "-screen 0 10x10x8" -f /tmp/x-skype-gw /usr/bin/spectrum2_skype_backend
|
||||
|
||||
# Libpurple protocol-id for spectrum_libpurple_backend
|
||||
protocol=prpl-jabber
|
||||
|
@ -95,3 +95,20 @@ type = none
|
|||
|
||||
# Prefix used for tables
|
||||
#prefix = jabber_
|
||||
|
||||
[registration]
|
||||
# Enable public registrations
|
||||
enable_public_registration=1
|
||||
|
||||
# Text to display upon user registration form
|
||||
#username_label=Jabber JID (e.g. user@server.tld):
|
||||
#instructions=Enter your remote jabber JID and password as well as your local username and password
|
||||
|
||||
# If True a local jabber account on <local_account_server> is needed
|
||||
# for transport registration, the idea is to enable public registration
|
||||
# from other servers, but only for users, who have already local accounts
|
||||
#require_local_account=1
|
||||
#local_username_label=Local username (without @server.tld):
|
||||
#local_account_server=localhost
|
||||
#local_account_server_timeout=10000
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ endif()
|
|||
|
||||
if (PROTOBUF_FOUND)
|
||||
if (CMAKE_COMPILER_IS_GNUCXX)
|
||||
ADD_LIBRARY(transport SHARED ${HEADERS} ${SRC} ${SWIFTEN_SRC} ${CMAKE_CURRENT_BINARY_DIR}/../include/transport/protocol.pb.cc)
|
||||
ADD_LIBRARY(transport SHARED ${HEADERS} ${SRC} ${SWIFTEN_SRC})
|
||||
else(CMAKE_COMPILER_IS_GNUCXX)
|
||||
ADD_LIBRARY(transport STATIC ${HEADERS} ${SRC} ${SWIFTEN_SRC} ${CMAKE_CURRENT_BINARY_DIR}/../include/transport/protocol.pb.cc)
|
||||
ADD_LIBRARY(transport STATIC ${HEADERS} ${SRC} ${SWIFTEN_SRC})
|
||||
endif(CMAKE_COMPILER_IS_GNUCXX)
|
||||
SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_BINARY_DIR}/../include/transport/protocol.pb.cc PROPERTIES GENERATED 1)
|
||||
# SET_SOURCE_FILES_PROPERTIES(${CMAKE_CURRENT_BINARY_DIR}/../include/transport/protocol.pb.cc PROPERTIES GENERATED 1)
|
||||
ADD_DEPENDENCIES(transport pb)
|
||||
else(PROTOBUF_FOUND)
|
||||
ADD_LIBRARY(transport SHARED ${HEADERS} ${SRC} ${SWIFTEN_SRC})
|
||||
|
@ -35,9 +35,9 @@ if (CMAKE_COMPILER_IS_GNUCXX)
|
|||
endif()
|
||||
|
||||
if (WIN32)
|
||||
TARGET_LINK_LIBRARIES(transport ${PQXX_LIBRARY} ${PQ_LIBRARY} ${SQLITE3_LIBRARIES} ${MYSQL_LIBRARIES} ${SWIFTEN_LIBRARY} ${PROTOBUF_LIBRARIES} ${LOG4CXX_LIBRARIES})
|
||||
TARGET_LINK_LIBRARIES(transport transport-plugin ${PQXX_LIBRARY} ${PQ_LIBRARY} ${SQLITE3_LIBRARIES} ${MYSQL_LIBRARIES} ${SWIFTEN_LIBRARY} ${PROTOBUF_LIBRARIES} ${LOG4CXX_LIBRARIES})
|
||||
else (WIN32)
|
||||
TARGET_LINK_LIBRARIES(transport ${PQXX_LIBRARY} ${PQ_LIBRARY} ${SQLITE3_LIBRARIES} ${MYSQL_LIBRARIES} ${SWIFTEN_LIBRARY} ${PROTOBUF_LIBRARIES} ${LOG4CXX_LIBRARIES} ${POPT_LIBRARY})
|
||||
TARGET_LINK_LIBRARIES(transport transport-plugin ${PQXX_LIBRARY} ${PQ_LIBRARY} ${SQLITE3_LIBRARIES} ${MYSQL_LIBRARIES} ${SWIFTEN_LIBRARY} ${PROTOBUF_LIBRARIES} ${LOG4CXX_LIBRARIES} ${POPT_LIBRARY})
|
||||
endif(WIN32)
|
||||
|
||||
SET_TARGET_PROPERTIES(transport PROPERTIES
|
||||
|
|
|
@ -87,8 +87,12 @@ bool Config::load(std::istream &ifs, boost::program_options::options_description
|
|||
("registration.username_label", value<std::string>()->default_value("Legacy network username:"), "Label for username field")
|
||||
("registration.username_mask", value<std::string>()->default_value(""), "Username mask")
|
||||
("registration.encoding", value<std::string>()->default_value("utf8"), "Default encoding in registration form")
|
||||
("registration.require_local_account", value<bool>()->default_value(false), "True if users have to have a local account to register to this transport from remote servers.")
|
||||
("registration.local_username_label", value<std::string>()->default_value("Local username:"), "Label for local usernme field")
|
||||
("registration.local_account_server", value<std::string>()->default_value("localhost"), "The server on which the local accounts will be checked for validity")
|
||||
("registration.local_account_server_timeout", value<int>()->default_value(10000), "Timeout when checking local user on local_account_server (msecs)")
|
||||
("database.type", value<std::string>()->default_value("none"), "Database type.")
|
||||
("database.database", value<std::string>()->default_value(""), "Database used to store data")
|
||||
("database.database", value<std::string>()->default_value("/var/lib/spectrum2/$jid/database.sql"), "Database used to store data")
|
||||
("database.server", value<std::string>()->default_value("localhost"), "Database server.")
|
||||
("database.user", value<std::string>()->default_value(""), "Database user.")
|
||||
("database.password", value<std::string>()->default_value(""), "Database Password.")
|
||||
|
@ -107,6 +111,7 @@ bool Config::load(std::istream &ifs, boost::program_options::options_description
|
|||
bool found_working = false;
|
||||
bool found_pidfile = false;
|
||||
bool found_backend_port = false;
|
||||
bool found_database = false;
|
||||
std::string jid = "";
|
||||
BOOST_FOREACH(option &opt, parsed.options) {
|
||||
if (opt.string_key == "service.jid") {
|
||||
|
@ -130,6 +135,9 @@ bool Config::load(std::istream &ifs, boost::program_options::options_description
|
|||
else if (opt.string_key == "service.pidfile") {
|
||||
found_pidfile = true;
|
||||
}
|
||||
else if (opt.string_key == "database.database") {
|
||||
found_database = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_working) {
|
||||
|
@ -148,6 +156,11 @@ bool Config::load(std::istream &ifs, boost::program_options::options_description
|
|||
value.push_back(p);
|
||||
parsed.options.push_back(boost::program_options::basic_option<char>("service.backend_port", value));
|
||||
}
|
||||
if (!found_database) {
|
||||
std::vector<std::string> value;
|
||||
value.push_back("/var/lib/spectrum2/$jid/database.sql");
|
||||
parsed.options.push_back(boost::program_options::basic_option<char>("database.database", value));
|
||||
}
|
||||
|
||||
BOOST_FOREACH(option &opt, parsed.options) {
|
||||
if (opt.unregistered) {
|
||||
|
|
|
@ -470,7 +470,7 @@ long MySQLBackend::addBuddy(long userId, const BuddyInfo &buddyInfo) {
|
|||
long id = (long) mysql_insert_id(&m_conn);
|
||||
|
||||
// INSERT OR REPLACE INTO " + m_prefix + "buddies_settings (user_id, buddy_id, var, type, value) VALUES (?, ?, ?, ?, ?)
|
||||
if (!buddyInfo.settings.find("icon_hash")->second.s.empty()) {
|
||||
if (buddyInfo.settings.find("icon_hash") != buddyInfo.settings.end() && !buddyInfo.settings.find("icon_hash")->second.s.empty()) {
|
||||
*m_updateBuddySetting << userId << id << buddyInfo.settings.find("icon_hash")->first << (int) TYPE_STRING << buddyInfo.settings.find("icon_hash")->second.s << buddyInfo.settings.find("icon_hash")->second.s;
|
||||
EXEC(m_updateBuddySetting, addBuddy(userId, buddyInfo));
|
||||
}
|
||||
|
@ -597,6 +597,10 @@ void MySQLBackend::getUserSetting(long id, const std::string &variable, int &typ
|
|||
else {
|
||||
*m_getUserSetting >> type >> value;
|
||||
}
|
||||
|
||||
while (m_getUserSetting->fetch() == 0) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void MySQLBackend::updateUserSetting(long id, const std::string &variable, const std::string &value) {
|
||||
|
@ -606,11 +610,11 @@ void MySQLBackend::updateUserSetting(long id, const std::string &variable, const
|
|||
}
|
||||
|
||||
void MySQLBackend::beginTransaction() {
|
||||
exec("START TRANSACTION;");
|
||||
//exec("START TRANSACTION;");
|
||||
}
|
||||
|
||||
void MySQLBackend::commitTransaction() {
|
||||
exec("COMMIT;");
|
||||
//exec("COMMIT;");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -157,6 +157,7 @@ static unsigned long exec_(std::string path, const char *host, const char *port,
|
|||
// fork and exec
|
||||
pid_t pid = fork();
|
||||
if ( pid == 0 ) {
|
||||
setsid();
|
||||
// child process
|
||||
exit(execv(argv[0], argv));
|
||||
} else if ( pid < 0 ) {
|
||||
|
@ -174,7 +175,7 @@ static void SigCatcher(int n) {
|
|||
int status;
|
||||
// Read exit code from all children to not have zombies arround
|
||||
// WARNING: Do not put LOG4CXX_ here, because it can lead to deadlock
|
||||
while ((result = waitpid(0, &status, WNOHANG)) > 0) {
|
||||
while ((result = waitpid(-1, &status, WNOHANG)) > 0) {
|
||||
if (result != 0) {
|
||||
if (WIFEXITED(status)) {
|
||||
if (WEXITSTATUS(status) != 0) {
|
||||
|
@ -257,6 +258,7 @@ NetworkPluginServer::NetworkPluginServer(Component *component, Config *config, U
|
|||
#endif
|
||||
|
||||
exec_(CONFIG_STRING(m_config, "service.backend"), CONFIG_STRING(m_config, "service.backend_host").c_str(), CONFIG_STRING(m_config, "service.backend_port").c_str(), m_config->getConfigFile().c_str());
|
||||
LOG4CXX_INFO(logger, "Backend should now connect to Spectrum2 instance. Spectrum2 won't accept any connection before backend connects");
|
||||
}
|
||||
|
||||
NetworkPluginServer::~NetworkPluginServer() {
|
||||
|
|
|
@ -285,8 +285,8 @@ void RosterManager::handleSubscription(Swift::Presence::ref presence) {
|
|||
// using roster pushes.
|
||||
if (m_component->inServerMode()) {
|
||||
Swift::Presence::ref response = Swift::Presence::create();
|
||||
response->setTo(presence->getFrom());
|
||||
response->setFrom(presence->getTo());
|
||||
response->setTo(presence->getFrom().toBare());
|
||||
response->setFrom(presence->getTo().toBare());
|
||||
Buddy *buddy = getBuddy(Buddy::JIDToLegacyName(presence->getTo()));
|
||||
if (buddy) {
|
||||
LOG4CXX_INFO(logger, m_user->getJID().toString() << ": Subscription received and buddy " << Buddy::JIDToLegacyName(presence->getTo()) << " is already there => answering");
|
||||
|
@ -342,7 +342,7 @@ void RosterManager::handleSubscription(Swift::Presence::ref presence) {
|
|||
Swift::Presence::ref response = Swift::Presence::create();
|
||||
Swift::Presence::ref currentPresence;
|
||||
response->setTo(presence->getFrom().toBare());
|
||||
response->setFrom(presence->getTo().toBare().toString() + "/bot");
|
||||
response->setFrom(presence->getTo().toBare());
|
||||
|
||||
Buddy *buddy = getBuddy(Buddy::JIDToLegacyName(presence->getTo()));
|
||||
if (buddy) {
|
||||
|
|
|
@ -111,6 +111,8 @@ bool SQLite3Backend::connect() {
|
|||
return false;
|
||||
}
|
||||
|
||||
sqlite3_busy_timeout(m_db, 1500);
|
||||
|
||||
if (createDatabase() == false)
|
||||
return false;
|
||||
|
||||
|
@ -234,6 +236,8 @@ bool SQLite3Backend::getUser(const std::string &barejid, UserInfo &user) {
|
|||
user.encoding = (const char *) sqlite3_column_text(m_getUser, 4);
|
||||
user.language = (const char *) sqlite3_column_text(m_getUser, 5);
|
||||
user.vip = sqlite3_column_int(m_getUser, 6) != 0;
|
||||
while((ret = sqlite3_step(m_getUser)) == SQLITE_ROW) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -388,6 +392,9 @@ bool SQLite3Backend::getBuddies(long id, std::list<BuddyInfo> &roster) {
|
|||
roster.push_back(b);
|
||||
}
|
||||
|
||||
while((ret = sqlite3_step(m_getBuddiesSettings)) == SQLITE_ROW) {
|
||||
}
|
||||
|
||||
if (ret != SQLITE_DONE) {
|
||||
LOG4CXX_ERROR(logger, "getBuddies query"<< (sqlite3_errmsg(m_db) == NULL ? "" : sqlite3_errmsg(m_db)));
|
||||
return false;
|
||||
|
@ -444,6 +451,10 @@ void SQLite3Backend::getUserSetting(long id, const std::string &variable, int &t
|
|||
type = GET_INT(m_getUserSetting);
|
||||
value = GET_STR(m_getUserSetting);
|
||||
}
|
||||
|
||||
int ret;
|
||||
while((ret = sqlite3_step(m_getUserSetting)) == SQLITE_ROW) {
|
||||
}
|
||||
}
|
||||
|
||||
void SQLite3Backend::updateUserSetting(long id, const std::string &variable, const std::string &value) {
|
||||
|
|
|
@ -306,8 +306,8 @@ void UserManager::handleSubscription(Swift::Presence::ref presence) {
|
|||
// answer to subscibe for transport itself
|
||||
if (presence->getType() == Swift::Presence::Subscribe && presence->getTo().getNode().empty()) {
|
||||
Swift::Presence::ref response = Swift::Presence::create();
|
||||
response->setFrom(presence->getTo());
|
||||
response->setTo(presence->getFrom());
|
||||
response->setFrom(presence->getTo().toBare());
|
||||
response->setTo(presence->getFrom().toBare());
|
||||
response->setType(Swift::Presence::Subscribed);
|
||||
m_component->getStanzaChannel()->sendPresence(response);
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
#include "transport/user.h"
|
||||
#include "Swiften/Elements/ErrorPayload.h"
|
||||
#include <boost/shared_ptr.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include "log4cxx/logger.h"
|
||||
|
||||
using namespace Swift;
|
||||
|
@ -241,6 +243,20 @@ bool UserRegistration::handleGetRequest(const Swift::JID& from, const Swift::JID
|
|||
boolean->setLabel((("Remove your registration")));
|
||||
boolean->setValue(0);
|
||||
form->addField(boolean);
|
||||
} else {
|
||||
if (CONFIG_BOOL(m_config,"registration.require_local_account")) {
|
||||
std::string localUsernameField = CONFIG_STRING(m_config, "registration.local_username_label");
|
||||
TextSingleFormField::ref local_username = TextSingleFormField::create();
|
||||
local_username->setName("local_username");
|
||||
local_username->setLabel((localUsernameField));
|
||||
local_username->setRequired(true);
|
||||
form->addField(local_username);
|
||||
TextPrivateFormField::ref local_password = TextPrivateFormField::create();
|
||||
local_password->setName("local_password");
|
||||
local_password->setLabel((("Local Password")));
|
||||
local_password->setRequired(true);
|
||||
form->addField(local_password);
|
||||
}
|
||||
}
|
||||
|
||||
reg->setForm(form);
|
||||
|
@ -273,6 +289,8 @@ bool UserRegistration::handleSetRequest(const Swift::JID& from, const Swift::JID
|
|||
|
||||
std::string encoding;
|
||||
std::string language;
|
||||
std::string local_username("");
|
||||
std::string local_password("");
|
||||
|
||||
Form::ref form = payload->getForm();
|
||||
if (form) {
|
||||
|
@ -290,6 +308,13 @@ bool UserRegistration::handleSetRequest(const Swift::JID& from, const Swift::JID
|
|||
else if (textSingle->getName() == "password") {
|
||||
payload->setPassword(textSingle->getValue());
|
||||
}
|
||||
else if (textSingle->getName() == "local_username") {
|
||||
local_username = textSingle->getValue();
|
||||
}
|
||||
// Pidgin sends it as textSingle, not sure why...
|
||||
else if (textSingle->getName() == "local_password") {
|
||||
local_password = textSingle->getValue();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -298,6 +323,9 @@ bool UserRegistration::handleSetRequest(const Swift::JID& from, const Swift::JID
|
|||
if (textPrivate->getName() == "password") {
|
||||
payload->setPassword(textPrivate->getValue());
|
||||
}
|
||||
else if (textPrivate->getName() == "local_password") {
|
||||
local_password = textPrivate->getValue();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -327,6 +355,50 @@ bool UserRegistration::handleSetRequest(const Swift::JID& from, const Swift::JID
|
|||
return true;
|
||||
}
|
||||
|
||||
if (CONFIG_BOOL(m_config,"registration.require_local_account")) {
|
||||
/* if (!local_username || !local_password) {
|
||||
sendResponse(from, id, InBandRegistrationPayload::ref());
|
||||
return true
|
||||
} else */ if (local_username == "" || local_password == "") {
|
||||
sendResponse(from, id, InBandRegistrationPayload::ref());
|
||||
return true;
|
||||
}
|
||||
// Swift::logging = true;
|
||||
bool validLocal = false;
|
||||
std::string localLookupServer = CONFIG_STRING(m_config, "registration.local_account_server");
|
||||
std::string localLookupJID = local_username + std::string("@") + localLookupServer;
|
||||
SimpleEventLoop localLookupEventLoop;
|
||||
BoostNetworkFactories localLookupNetworkFactories(&localLookupEventLoop);
|
||||
Client localLookupClient(localLookupJID, local_password, &localLookupNetworkFactories);
|
||||
|
||||
// TODO: this is neccessary on my server ... but should maybe omitted
|
||||
localLookupClient.setAlwaysTrustCertificates();
|
||||
localLookupClient.connect();
|
||||
|
||||
class SimpleLoopRunner {
|
||||
public:
|
||||
SimpleLoopRunner() {};
|
||||
|
||||
static void run(SimpleEventLoop * loop) {
|
||||
loop->run();
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Really ugly and hacky solution, any other ideas more than welcome!
|
||||
boost::thread thread(boost::bind(&(SimpleLoopRunner::run), &localLookupEventLoop));
|
||||
thread.timed_join(boost::posix_time::millisec(CONFIG_INT(m_config, "registration.local_account_server_timeout")));
|
||||
localLookupEventLoop.stop();
|
||||
thread.join();
|
||||
validLocal = localLookupClient.isAvailable();
|
||||
localLookupClient.disconnect();
|
||||
if (!validLocal) {
|
||||
sendError(from, id, ErrorPayload::NotAuthorized, ErrorPayload::Modify);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
printf("here\n");
|
||||
|
||||
if (!payload->getUsername() || !payload->getPassword()) {
|
||||
sendError(from, id, ErrorPayload::NotAcceptable, ErrorPayload::Modify);
|
||||
return true;
|
||||
|
|
Loading…
Add table
Reference in a new issue