Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ permissions:
jobs:
linux-glibc:
runs-on: ubuntu-latest

services:
ircd:
image: linuxserver/ngircd
ports:
- 6667:6667
steps:
- uses: actions/checkout@v6

Expand All @@ -28,7 +32,7 @@ jobs:
cache-to: type=gha,mode=max

- name: Run tests
run: docker run -tt spectrum:tests
run: docker run -tt --network host spectrum:tests
linux-clang:
runs-on: ubuntu-latest

Expand Down
12 changes: 9 additions & 3 deletions backends/libcommuni/ircnetworkplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,15 @@ MyIrcSession *IRCNetworkPlugin::createSession(const std::string &user, const std
void IRCNetworkPlugin::handleLoginRequest(const std::string &user, const std::string &legacyName, const std::string &password, const std::map<std::string, std::string> &settings) {
if (!m_servers.empty()) {
// legacy name is user's nickname
if (m_sessions[user] != NULL) {
LOG4CXX_WARN(logger, user << ": Already logged in.");
return;
MyIrcSession *oldSession = m_sessions[user];
if (oldSession != NULL) {
if (oldSession->isConnected()) {
LOG4CXX_WARN(logger, user << ": Already logged in.");
return;
}
LOG4CXX_INFO(logger, user << ": Reconnecting after previous disconnect.");
oldSession->deleteLater();
m_sessions.erase(user);
}

m_sessions[user] = createSession(user, m_servers[m_currentServer], legacyName, password, "");
Expand Down
2 changes: 2 additions & 0 deletions backends/libcommuni/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class MyIrcSession : public IrcConnection
void on_whoisMessageReceived(IrcMessage *message);
void on_namesMessageReceived(IrcMessage *message);

bool isConnected() const { return m_connected; }

int rooms;

protected Q_SLOTS:
Expand Down
180 changes: 180 additions & 0 deletions libtransport/AdminInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
#include "transport/Frontend.h"
#include "transport/MemoryUsage.h"
#include "transport/Config.h"
#include "Swiften/Elements/MUCPayload.h"

#include <boost/foreach.hpp>
#include <memory>
#include <boost/lexical_cast.hpp>

namespace Transport {
Expand All @@ -49,6 +51,178 @@ static std::string getArg(const std::string &body) {
}
#endif

class ConnectUserCommand : public AdminInterfaceCommand {
public:
ConnectUserCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("connect_user",
AdminInterfaceCommand::Users, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Connect a user to the legacy network (simulates XMPP login)");
addArg("user", "Bare JID of the user to connect", "string", "user@localhost");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 1) return "Usage: connect_user <barejid>";
Swift::JID jid(args[0]);
if (!jid.isValid() || jid.getNode().empty()) return "Invalid JID: " + args[0];
m_userManager->connectUser(jid);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};

class DisconnectUserCommand : public AdminInterfaceCommand {
public:
DisconnectUserCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("disconnect_user",
AdminInterfaceCommand::Users, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Disconnect a user from the legacy network");
addArg("user", "Bare JID of the user to disconnect", "string", "user@localhost");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 1) return "Usage: disconnect_user <barejid>";
Swift::JID jid(args[0]);
if (!jid.isValid() || jid.getNode().empty()) return "Invalid JID: " + args[0];
m_userManager->disconnectUser(jid);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};

class SendMessageCommand : public AdminInterfaceCommand {
public:
SendMessageCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("send_message",
AdminInterfaceCommand::Messages, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Send a message on behalf of a user");
addArg("user", "Bare JID of the sender", "string", "user@localhost");
addArg("to", "JID to send the message to", "string", "room@conference.localhost/nick");
addArg("body", "Message body", "string", "Hello world");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 3) return "Usage: send_message <user> <to> <body>";
User *u = m_userManager->getUser(args[0]);
if (!u) return "User not online: " + args[0];
std::shared_ptr<Swift::Message> msg(new Swift::Message());
msg->setFrom(Swift::JID(args[0]));
msg->setTo(Swift::JID(args[1]));
msg->setBody(args[2]);
m_component->getFrontend()->onMessageReceived(msg);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};

class SendRoomMessageCommand : public AdminInterfaceCommand {
public:
SendRoomMessageCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("send_room_message",
AdminInterfaceCommand::Messages, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Send a groupchat message to a MUC room");
addArg("user", "Bare JID of sender", "string", "user@localhost");
addArg("room", "Room JID", "string", "#channel@localhost");
addArg("body", "Message body", "string", "Hello");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 3) return "Usage: send_room_message <user> <room> <body>";
User *u = m_userManager->getUser(args[0]);
if (!u) return "User not online: " + args[0];
std::shared_ptr<Swift::Message> msg(new Swift::Message());
msg->setFrom(Swift::JID(args[0]));
msg->setTo(Swift::JID(args[1]));
msg->setBody(args[2]);
msg->setType(Swift::Message::Groupchat);
m_component->getFrontend()->onMessageReceived(msg);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};


class JoinRoomCommand : public AdminInterfaceCommand {
public:
JoinRoomCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("join_room",
AdminInterfaceCommand::Users, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Join a MUC room");
addArg("user", "Bare JID of user", "string", "user@localhost");
addArg("room", "Room JID", "string", "#channel@localhost");
addArg("nick", "Nickname in room", "string", "user");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 3) return "Usage: join_room <user> <room> <nick>";
User *u = m_userManager->getUser(args[0]);
if (!u) return "Error: User not online: " + args[0];
Swift::Presence::ref p = Swift::Presence::create();
p->setFrom(Swift::JID(args[0] + "/" + args[2]));
p->setTo(Swift::JID(args[1] + "/" + args[2]));
p->setType(Swift::Presence::Available);
std::shared_ptr<Swift::MUCPayload> muc(new Swift::MUCPayload());
p->addPayload(muc);
m_component->getFrontend()->onPresenceReceived(p);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};

class LeaveRoomCommand : public AdminInterfaceCommand {
public:
LeaveRoomCommand(Component *component, UserManager *userManager) :
AdminInterfaceCommand("leave_room",
AdminInterfaceCommand::Users, AdminInterfaceCommand::GlobalContext,
AdminInterfaceCommand::AdminMode, AdminInterfaceCommand::Execute) {
m_component = component;
m_userManager = userManager;
setDescription("Leave a MUC room");
addArg("user", "Bare JID of user", "string", "user@localhost");
addArg("room", "Room JID", "string", "#channel@localhost");
addArg("nick", "Nickname in room", "string", "user");
}

virtual std::string handleExecuteRequest(UserInfo &uinfo, User *user, std::vector<std::string> &args) {
if (args.size() < 3) return "Usage: leave_room <user> <room> <nick>";
User *u = m_userManager->getUser(args[0]);
if (!u) return "Error: User not online: " + args[0];
Swift::Presence::ref presence = Swift::Presence::create();
presence->setFrom(Swift::JID(args[0] + "/" + args[2]));
presence->setTo(Swift::JID(args[1] + "/" + args[2]));
presence->setType(Swift::Presence::Unavailable);
m_component->getFrontend()->onPresenceReceived(presence);
return "OK";
}
private:
Component *m_component;
UserManager *m_userManager;
};

class StatusCommand : public AdminInterfaceCommand {
public:

Expand Down Expand Up @@ -1112,6 +1286,12 @@ AdminInterface::AdminInterface(Component *component, UserManager *userManager, N
addCommand(new MessagesToXMPPCommand(m_userManager));
addCommand(new SetOAuth2CodeCommand(m_component));
addCommand(new GetOAuth2URLCommand(m_component));
addCommand(new ConnectUserCommand(m_component, m_userManager));
addCommand(new JoinRoomCommand(m_component, m_userManager));
addCommand(new LeaveRoomCommand(m_component, m_userManager));
addCommand(new DisconnectUserCommand(m_component, m_userManager));
addCommand(new SendMessageCommand(m_component, m_userManager));
addCommand(new SendRoomMessageCommand(m_component, m_userManager));
addCommand(new HelpCommand(&m_commands));
addCommand(new ArgsCommand(&m_commands));
addCommand(new CommandsCommand(&m_commands));
Expand Down
2 changes: 1 addition & 1 deletion spectrum/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ if (ENABLE_PURPLE AND PURPLE_FOUND)
add_dependencies(spectrum2 spectrum2_libpurple_backend)
endif()

target_link_libraries(spectrum2 transport spectrum2-xmpp-frontend spectrum2-slack-frontend ${SWIFTEN_LIBRARY} ${LOG4CXX_LIBRARIES} ${PROTOBUF_LIBRARY})
target_link_libraries(spectrum2 transport spectrum2-xmpp-frontend ${SWIFTEN_LIBRARY} ${LOG4CXX_LIBRARIES} ${PROTOBUF_LIBRARY})
target_compile_features(spectrum2 PUBLIC cxx_std_11)

if(NOT MSVC AND NOT APPLE)
Expand Down
4 changes: 2 additions & 2 deletions spectrum/src/frontends/slack/SlackUserManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class ListRoomsCommand : public AdminInterfaceCommand {
class JoinRoomCommand : public AdminInterfaceCommand {
public:

JoinRoomCommand(StorageBackend *storageBackend, Config *cfg) : AdminInterfaceCommand("join_room",
JoinRoomCommand(StorageBackend *storageBackend, Config *cfg) : AdminInterfaceCommand("join_slack_room",
AdminInterfaceCommand::Frontend,
AdminInterfaceCommand::UserContext,
AdminInterfaceCommand::UserMode,
Expand Down Expand Up @@ -140,7 +140,7 @@ class JoinRoomCommand : public AdminInterfaceCommand {
class LeaveRoomCommand : public AdminInterfaceCommand {
public:

LeaveRoomCommand(StorageBackend *storageBackend) : AdminInterfaceCommand("leave_room",
LeaveRoomCommand(StorageBackend *storageBackend) : AdminInterfaceCommand("leave_slack_room",
AdminInterfaceCommand::Frontend,
AdminInterfaceCommand::UserContext,
AdminInterfaceCommand::UserMode,
Expand Down
1 change: 0 additions & 1 deletion spectrum/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#include "transport/Logging.h"
#include "transport/Frontend.h"
#include "frontends/xmpp/XMPPFrontendPlugin.h"
#include "frontends/slack/SlackFrontendPlugin.h"
#include "Swiften/EventLoop/SimpleEventLoop.h"
#include "Swiften/Network/BoostNetworkFactories.h"
#include <boost/thread.hpp>
Expand Down
26 changes: 13 additions & 13 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
add_subdirectory(libtransport)
add_subdirectory(libtransport)
add_subdirectory(integration)

if(ENABLE_TESTS)
if(ENABLE_TESTS)

add_custom_target(test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/libtransport/libtransport_test
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests_output
)
add_custom_target(test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/libtransport/libtransport_test
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests_output
)

add_custom_target(extended_test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/libtransport/libtransport_test
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/start.py
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests_output
)

endif()
add_custom_target(extended_test
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/libtransport/libtransport_test
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/integration/run.sh ../libcommuni/irc_test.cfg ${CMAKE_CURRENT_BINARY_DIR}/integration/integration_test
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tests_output
)

endif()
15 changes: 15 additions & 0 deletions tests/integration/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
if(ENABLE_TESTS)
add_executable(integration_test
test_chat.cpp
)

target_link_libraries(integration_test
transport
${CPPUNIT_LIBRARY}
${Boost_LIBRARIES}
)

set_target_properties(integration_test PROPERTIES
COMPILE_DEFINITIONS "LIBTRANSPORT_TEST=1"
)
endif()
42 changes: 42 additions & 0 deletions tests/integration/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash
set -e
SPECTRUM="../../spectrum/src/spectrum2"
CONFIG="$1"
TEST_BIN="$2"
if [ -z "$CONFIG" ] || [ -z "$TEST_BIN" ]; then
echo "Usage: $0 <config_file> <integration_test_bin>"
exit 1
fi
echo "Starting spectrum2 with config: $CONFIG"
$SPECTRUM -n "./$CONFIG" > spectrum2.log 2>&1 &
SPECTRUM_PID=$!
echo "Waiting for spectrum2 frontend..."
for i in $(seq 1 15); do
if python3 -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1',5223)); s.close()" 2>/dev/null; then
echo "Spectrum2 frontend is listening"; break
fi
if [ $i -eq 15 ]; then
echo "Timeout"; cat spectrum2.log; kill $SPECTRUM_PID 2>/dev/null || true; exit 1
fi
sleep 1
done
# Wait for backend portfile (spectrum2 auto-spawns backend)
sleep 3
PORTFILE="localhost.port"
if [ -f "$PORTFILE" ]; then
BACKEND_PORT=$(cat "$PORTFILE")
echo "Backend port: $BACKEND_PORT"
else
echo "Portfile $PORTFILE not found"
kill $SPECTRUM_PID 2>/dev/null || true; exit 1
fi
echo "Running integration tests..."
EXIT_CODE=0
"$TEST_BIN" "127.0.0.1" "$BACKEND_PORT" || EXIT_CODE=$?
kill $SPECTRUM_PID 2>/dev/null || true
wait $SPECTRUM_PID 2>/dev/null || true
if [ $EXIT_CODE -ne 0 ]; then
echo "--- spectrum2 log ---"; cat spectrum2.log
fi
echo "Integration tests completed with exit code $EXIT_CODE"
exit $EXIT_CODE
Loading
Loading