M docker/biboumi-test/debian/Dockerfile => docker/biboumi-test/debian/Dockerfile +1 -1
@@ 51,7 51,7 @@ RUN useradd tester -m
RUN apt install -y automake autoconf flex bison libltdl-dev openssl
RUN apt install -y libtool
RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis
-RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install
+RUN cd /charybdis && git checkout 4f2b9a4 && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install
RUN chown -R tester:tester /home/tester/ircd
RUN rm -rf /charybdis
M docker/biboumi-test/fedora/Dockerfile => docker/biboumi-test/fedora/Dockerfile +1 -1
@@ 53,7 53,7 @@ RUN useradd tester
RUN dnf install -y automake autoconf flex flex-devel bison libtool-ltdl-devel openssl-devel
RUN dnf install -y libtool
RUN git clone https://github.com/charybdis-ircd/charybdis.git && cd charybdis
-RUN cd /charybdis && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin && make -j8 && make install
+RUN cd /charybdis && git checkout 4f2b9a4 && ./autogen.sh && ./configure --prefix=/home/tester/ircd --bindir=/usr/bin --with-included-boost && make -j8 && make install
RUN chown -R tester:tester /home/tester/ircd
RUN rm -rf /charybdis
A louloulibs/utils/time.cpp => louloulibs/utils/time.cpp +12 -0
@@ 0,0 1,12 @@
+#include <utils/time.hpp>
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp)
+{
+ constexpr std::size_t stamp_size = 20;
+ char date_buf[stamp_size];
+ std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp));
+ return {std::begin(date_buf), std::end(date_buf)};
+}
+}<
\ No newline at end of file
A louloulibs/utils/time.hpp => louloulibs/utils/time.hpp +9 -0
@@ 0,0 1,9 @@
+#pragma once
+
+#include <ctime>
+#include <string>
+
+namespace utils
+{
+std::string to_string(const std::time_t& timestamp);
+}<
\ No newline at end of file
M louloulibs/xmpp/jid.hpp => louloulibs/xmpp/jid.hpp +6 -1
@@ 26,7 26,12 @@ public:
}
std::string full() const
{
- return this->local + "@" + this->domain + "/" + this->resource;
+ std::string res = this->domain;
+ if (!this->local.empty())
+ res = this->local + "@" + this->domain;
+ if (!this->resource.empty())
+ res += "/" + this->resource;
+ return res;
}
};
M louloulibs/xmpp/xmpp_component.cpp => louloulibs/xmpp/xmpp_component.cpp +3 -18
@@ 7,22 7,10 @@
#include <config/config.hpp>
#include <xmpp/jid.hpp>
#include <utils/sha1.hpp>
-
-#include <stdexcept>
-#include <iostream>
-#include <set>
-
-#include <stdio.h>
+#include <utils/time.hpp>
#include <uuid.h>
-#include <cstdlib>
-
-#include <louloulibs.h>
-#ifdef SYSTEMD_FOUND
-# include <systemd/sd-daemon.h>
-#endif
-
using namespace std::string_literals;
static std::set<std::string> kickable_errors{
@@ 443,12 431,9 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std:
message.add_child(std::move(body));
XmlNode delay("delay");
- delay["xmlns"] = "urn:xmpp:delay";
+ delay["xmlns"] = DELAY_NS;
delay["from"] = muc_name + "@" + this->served_hostname;
- constexpr std::size_t stamp_size = 20;
- char date_buf[stamp_size];
- std::strftime(date_buf, stamp_size, "%FT%TZ", std::gmtime(×tamp));
- delay["stamp"] = date_buf;
+ delay["stamp"] = utils::to_string(timestamp);
message.add_child(std::move(delay));
this->send_stanza(message);
M louloulibs/xmpp/xmpp_component.hpp => louloulibs/xmpp/xmpp_component.hpp +4 -0
@@ 26,6 26,10 @@
#define VERSION_NS "jabber:iq:version"
#define ADHOC_NS "http://jabber.org/protocol/commands"
#define PING_NS "urn:xmpp:ping"
+#define DELAY_NS "urn:xmpp:delay"
+#define MAM_NS "urn:xmpp:mam:1"
+#define FORWARD_NS "urn:xmpp:forward:0"
+#define CLIENT_NS "jabber:client"
/**
* An XMPP component, communicating with an XMPP server using the protocole
M src/database/database.cpp => src/database/database.cpp +5 -5
@@ 138,13 138,13 @@ void Database::store_muc_message(const std::string& owner, const Iid& iid,
std::vector<db::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server, int limit)
{
- if (limit < 0)
- limit = 0;
- auto res = litesql::select<db::MucLogLine>(*Database::db,
+ if (limit == -1)
+ limit = 1024;
+ const auto& res = litesql::select<db::MucLogLine>(*Database::db,
db::MucLogLine::Owner == owner &&
db::MucLogLine::IrcChanName == chan_name &&
- db::MucLogLine::IrcServerName == server).orderBy(db::MucLogLine::Date, false).limit(limit).all();
- return {res.rbegin(), res.rend()};
+ db::MucLogLine::IrcServerName == server).orderBy(db::MucLogLine::Id, false).limit(limit).all();
+ return {res.crbegin(), res.crend()};
}
void Database::close()
M src/xmpp/biboumi_component.cpp => src/xmpp/biboumi_component.cpp +78 -1
@@ 8,8 8,9 @@
#include <xmpp/biboumi_adhoc_commands.hpp>
#include <bridge/list_element.hpp>
#include <config/config.hpp>
-#include <xmpp/jid.hpp>
#include <utils/sha1.hpp>
+#include <utils/time.hpp>
+#include <xmpp/jid.hpp>
#include <stdexcept>
#include <iostream>
@@ 25,6 26,8 @@
# include <systemd/sd-daemon.h>
#endif
+#include <database/database.hpp>
+
using namespace std::string_literals;
static std::set<std::string> kickable_errors{
@@ 383,6 386,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
this->send_stanza(response);
stanza_error.disable();
}
+#ifdef USE_DATABASE
+ else if ((query = stanza.get_child("query", MAM_NS)))
+ {
+ if (this->handle_mam_request(stanza))
+ stanza_error.disable();
+ }
+#endif
}
else if (type == "get")
{
@@ 525,6 535,73 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
error_name = "feature-not-implemented";
}
+#ifdef USE_DATABASE
+bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
+{
+ std::string id = stanza.get_tag("id");
+ Jid from(stanza.get_tag("from"));
+ Jid to(stanza.get_tag("to"));
+
+ const XmlNode* query = stanza.get_child("query", MAM_NS);
+ std::string query_id;
+ if (query)
+ query_id = query->get_tag("queryid");
+
+ Iid iid(to.local, {'#', '&'});
+ if (iid.type == Iid::Type::Channel && to.resource.empty())
+ {
+ const auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), -1);
+ for (const db::MucLogLine& line: lines)
+ {
+ const auto queryid = query->get_tag("queryid");
+ if (!line.nick.value().empty())
+ this->send_archived_message(line, to.full(), from.full(), queryid);
+ }
+ this->send_iq_result_full_jid(id, from.full(), to.full());
+ return true;
+ }
+ return false;
+}
+
+void BiboumiComponent::send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+ const std::string& queryid)
+{
+ Stanza message("message");
+ message["from"] = from;
+ message["to"] = to;
+
+ XmlNode result("result");
+ result["xmlns"] = MAM_NS;
+ result["queryid"] = queryid;
+ result["id"] = log_line.uuid.value();
+
+ XmlNode forwarded("forwarded");
+ forwarded["xmlns"] = FORWARD_NS;
+
+ XmlNode delay("delay");
+ delay["xmlns"] = DELAY_NS;
+ delay["stamp"] = utils::to_string(log_line.date.value().timeStamp());
+
+ forwarded.add_child(std::move(delay));
+
+ XmlNode submessage("message");
+ submessage["xmlns"] = CLIENT_NS;
+ submessage["from"] = from + "/" + log_line.nick.value();
+ submessage["type"] = "groupchat";
+
+ XmlNode body("body");
+ body.set_inner(log_line.body.value());
+ submessage.add_child(std::move(body));
+
+ forwarded.add_child(std::move(submessage));
+ result.add_child(std::move(forwarded));
+ message.add_child(std::move(result));
+
+ this->send_stanza(message);
+}
+
+#endif
+
Bridge* BiboumiComponent::get_user_bridge(const std::string& user_jid)
{
auto bare_jid = Jid{user_jid}.bare();
M src/xmpp/biboumi_component.hpp => src/xmpp/biboumi_component.hpp +10 -0
@@ 9,6 9,10 @@
#include <string>
#include <map>
+namespace db
+{
+class MucLogLine;
+}
struct ListElement;
/**
@@ 82,6 86,12 @@ public:
void handle_message(const Stanza& stanza);
void handle_iq(const Stanza& stanza);
+#ifdef USE_DATABASE
+ bool handle_mam_request(const Stanza& stanza);
+ void send_archived_message(const db::MucLogLine& log_line, const std::string& from, const std::string& to,
+ const std::string& queryid);
+#endif
+
private:
/**
* Return the bridge associated with the bare JID. Create a new one
M tests/end_to_end/__main__.py => tests/end_to_end/__main__.py +47 -3
@@ 74,7 74,7 @@ class XMPPComponent(slixmpp.BaseXMPP):
self.scenario.steps = []
self.failed = True
- def on_end_session(self, event):
+ def on_end_session(self, _):
self.loop.stop()
def handle_incoming_stanza(self, stanza):
@@ 113,7 113,11 @@ def match(stanza, xpath):
'disco_items': 'http://jabber.org/protocol/disco#items',
'commands': 'http://jabber.org/protocol/commands',
'dataform': 'jabber:x:data',
- 'version': 'jabber:iq:version'})
+ 'version': 'jabber:iq:version',
+ 'mam': 'urn:xmpp:mam:1',
+ 'delay': 'urn:xmpp:delay',
+ 'forward': 'urn:xmpp:forward:0',
+ 'client': 'jabber:client'})
return matched
@@ 1044,7 1048,47 @@ if __name__ == '__main__':
("/presence[@type='unavailable'][@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:status[@code='110']",
"/presence/status[text()='Biboumi note: 1 resources are still in this channel.']")
),
- ])
+ ]),
+ Scenario("simple_mam",
+ [
+ handshake_sequence(),
+ # First user joins
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
+ partial(expect_stanza,
+ "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
+ partial(expect_stanza,
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']")
+ ),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+
+ # Send two channel messages
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ # Receive the message, forwarded to the two users
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']"),
+
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou 2</body></message>"),
+ # Receive the message, forwarded to the two users
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou 2']"),
+
+ # Retrieve the complete archive
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:1' queryid='qid1' /></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']")
+ ),
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid1']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
+ ),
+
+ partial(expect_stanza,
+ "/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']")
+
+ ]),
)
failures = 0