summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml101
-rw-r--r--CHANGELOG.rst65
-rw-r--r--CMakeLists.txt38
-rw-r--r--README.rst13
-rw-r--r--cmake/Modules/CodeCoverage.cmake2
-rw-r--r--conf/irc.gimp.org.policy.txt1
l---------[-rw-r--r--]conf/irc.gnome.org.policy.txt2
-rw-r--r--conf/irc.ppirc.net.policy.txt1
-rw-r--r--doc/Makefile20
-rw-r--r--doc/admin.rst305
-rw-r--r--doc/biboumi.1.rst757
-rw-r--r--doc/conf.py160
-rw-r--r--doc/contributing.rst (renamed from CONTRIBUTING.rst)31
-rw-r--r--doc/index.rst23
-rw-r--r--doc/install.rst (renamed from INSTALL.rst)124
-rw-r--r--doc/user.rst513
-rw-r--r--docker/biboumi-test/fedora/Dockerfile4
-rw-r--r--docker/biboumi/alpine/Dockerfile72
-rw-r--r--packaging/biboumi.spec.cmake18
-rw-r--r--src/bridge/bridge.cpp166
-rw-r--r--src/bridge/bridge.hpp24
-rw-r--r--src/bridge/history_limit.hpp2
-rw-r--r--src/config/config.cpp17
-rw-r--r--src/config/config.hpp5
-rw-r--r--src/database/column.hpp20
-rw-r--r--src/database/database.cpp177
-rw-r--r--src/database/database.hpp52
-rw-r--r--src/database/delete_query.hpp33
-rw-r--r--src/database/insert_query.hpp17
-rw-r--r--src/database/postgresql_engine.cpp13
-rw-r--r--src/database/postgresql_engine.hpp3
-rw-r--r--src/database/postgresql_statement.hpp20
-rw-r--r--src/database/query.cpp6
-rw-r--r--src/database/query.hpp8
-rw-r--r--src/database/row.hpp54
-rw-r--r--src/database/save.hpp31
-rw-r--r--src/database/select_query.hpp10
-rw-r--r--src/database/sqlite3_engine.cpp1
-rw-r--r--src/database/sqlite3_engine.hpp3
-rw-r--r--src/database/sqlite3_statement.hpp1
-rw-r--r--src/database/table.hpp11
-rw-r--r--src/database/update_query.hpp11
-rw-r--r--src/irc/irc_channel.cpp17
-rw-r--r--src/irc/irc_channel.hpp33
-rw-r--r--src/irc/irc_client.cpp216
-rw-r--r--src/irc/irc_client.hpp40
-rw-r--r--src/irc/irc_message.hpp4
-rw-r--r--src/main.cpp107
-rw-r--r--src/network/credentials_manager.cpp3
-rw-r--r--src/network/credentials_manager.hpp3
-rw-r--r--src/network/tcp_client_socket_handler.cpp15
-rw-r--r--src/network/tcp_socket_handler.cpp21
-rw-r--r--src/network/tls_policy.cpp2
-rw-r--r--src/network/tls_policy.hpp1
-rw-r--r--src/utils/dirname.cpp2
-rw-r--r--src/utils/dirname.hpp2
-rw-r--r--src/utils/encoding.cpp26
-rw-r--r--src/utils/optional_bool.hpp2
-rw-r--r--src/utils/string.cpp4
-rw-r--r--src/utils/tokens_bucket.hpp60
-rw-r--r--src/utils/uuid.cpp14
-rw-r--r--src/utils/uuid.hpp8
-rw-r--r--src/xmpp/adhoc_commands_handler.cpp2
-rw-r--r--src/xmpp/biboumi_adhoc_commands.cpp140
-rw-r--r--src/xmpp/biboumi_component.cpp145
-rw-r--r--src/xmpp/biboumi_component.hpp3
-rw-r--r--src/xmpp/jid.cpp2
-rw-r--r--src/xmpp/xmpp_component.cpp18
-rw-r--r--src/xmpp/xmpp_component.hpp6
-rw-r--r--src/xmpp/xmpp_parser.cpp2
-rw-r--r--tests/database.cpp64
-rw-r--r--tests/end_to_end/__main__.py578
-rw-r--r--tests/jid.cpp2
-rw-r--r--tests/network.cpp1
74 files changed, 2815 insertions, 1663 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a6f7bd8..9125628 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,6 +2,7 @@ stages:
- build # Build in various conf, keeps the artifacts
- test # Use the build artifacts to run the tests
- packaging # Publish some packages (rpm, deb…)
+ - deploy # Deploy things like the web doc
- external # Interact with some external service (coverity…)
before_script:
@@ -32,7 +33,6 @@ variables:
- cd build/
- cmake .. -DCMAKE_CXX_FLAGS="-Werror -Wno-psabi" -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3} ${POSTGRESQL}
- make everything -j$(nproc || echo 1)
- - make coverage_check -j$(nproc || echo 1)
artifacts:
expire_in: 2 weeks
paths:
@@ -59,54 +59,46 @@ build:archlinux:
<<: *basic_build
only:
- branches@louiz/biboumi
+ when: manual
tags:
- armv7l
artifacts:
paths: []
-build:1:
+build:no_botan:
variables:
BOTAN: "-DWITHOUT_BOTAN=1"
<<: *fedora_build
-build:2:
+build:no_udns:
variables:
UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
-build:3:
+build:no_libidn:
variables:
- SQLITE3: "-DWITHOUT_SQLITE3=1"
- TEST_POSTGRES_URI: "postgres@postgres/postgres"
- services:
- - postgres:latest
+ UDNS: "-DWITHOUT_UDNS=1"
<<: *fedora_build
-build:4:
+build:no_sqlite3:
variables:
SQLITE3: "-DWITHOUT_SQLITE3=1"
- POSTGRESQL: "-DWITHOUT_POSTGRESQL=1"
- BOTAN: "-DWITHOUT_BOTAN=1"
- LIBIDN: "-DWITHOUT_LIBIDN=1"
- <<: *fedora_build
-
-build:5:
- variables:
- UDNS: "-DWITHOUT_UDNS=1"
TEST_POSTGRES_URI: "postgres@postgres/postgres"
services:
- postgres:latest
<<: *fedora_build
-build:6:
+build:no_db:
variables:
- BOTAN: "-DWITHOUT_BOTAN=1"
- UDNS: "-DWITHOUT_UDNS=1"
+ SQLITE3: "-DWITHOUT_SQLITE3=1"
+ POSTGRESQL: "-DWITHOUT_POSTGRESQL=1"
<<: *fedora_build
-build:without_udns:
+build:no_db_botan:
variables:
- UDNS: "-DWITHOUT_UDNS=1"
+ SQLITE3: "-DWITHOUT_SQLITE3=1"
+ POSTGRESQL: "-DWITHOUT_POSTGRESQL=1"
+ BOTAN: "-DWITHOUT_BOTAN=1"
<<: *fedora_build
#
@@ -119,8 +111,8 @@ build:without_udns:
- docker
script:
- cd build/
- - make coverage_e2e -j$(nproc || echo 1)
- - make coverage
+ - make check -j$(nproc || echo 1)
+ - make e2e -j$(nproc || echo 1)
artifacts:
expire_in: 2 weeks
paths:
@@ -140,58 +132,40 @@ test:debian:
test:fedora:
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
<<: *basic_test
+ script:
+ - cd build/
+ - make coverage_check -j$(nproc || echo 1)
+ - make coverage_e2e -j$(nproc || echo 1)
+ - make coverage
dependencies:
- build:fedora
-test:without_udns:
+test:no_udns:
image: docker.louiz.org/louiz/biboumi/test-fedora:latest
<<: *basic_test
dependencies:
- - build:without_udns
+ - build:no_udns
test:alpine:
image: docker.louiz.org/louiz/biboumi/test-alpine:latest
- stage: test
- tags:
- - docker
- script:
- - cd build/
- - make e2e
+ <<: *basic_test
dependencies:
- build:alpine
+ image: docker.louiz.org/louiz/biboumi/test-alpine:latest
test:freebsd:
- only:
- - branches@louiz/biboumi
tags:
- freebsd
variables:
+ GIT_STRATEGY: "clone"
SYSTEMD: "-DWITHOUT_SYSTEMD=1"
stage: test
script:
- mkdir build/
- cd build/
- - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}
+ - cmake .. -DCMAKE_CXX_FLAGS="-Werror" -DCMAKE_BUILD_TYPE=${BUILD_TYPE} ${BOTAN} ${UDNS} ${SYSTEMD} ${LIBIDN} ${SQLITE3}
- make check
- make e2e
-
-coverity:
- stage: external
- only:
- - branches@louiz/biboumi
- tags:
- - docker
- image: docker.louiz.org/louiz/biboumi/test-fedora:latest
- allow_failure: true
- when: manual
- script:
- - export PATH=$PATH:~/coverity/bin
- - mkdir build/
- - cd build/
- - cmake .. -DWITHOUT_SYSTEMD=1
- - cov-build --dir cov-int make everything -j$(nproc || echo 1)
- - tar czvf biboumi_coverity.tgz cov-int
- - curl --form token=$COVERITY_TOKEN --form email=louiz@louiz.org --form file=@biboumi_coverity.tgz --form version="$(git rev-parse --short HEAD)" --form description="Automatic submission by gitlab-ci" https://scan.coverity.com/builds?project=louiz%2Fbiboumi
dependencies: []
#
@@ -260,3 +234,24 @@ packaging:archlinux:
- makepkg -si --noconfirm
- test -e /usr/bin/biboumi
dependencies: []
+
+#
+# Deploy jobs
+#
+
+doc:
+ stage: deploy
+ only:
+ - branches@louiz/biboumi
+ tags:
+ - www
+ environment:
+ name: doc.latest
+ url: https://doc.biboumi.louiz.org
+ image: docker.louiz.org/louiz/biboumi/doc-builder
+ script:
+ - cd doc/
+ - make html
+ - rm -rf /www/latest
+ - mv _build/html /www/latest
+ dependencies: []
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index bcddc11..f6b0c01 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,7 +1,70 @@
-Version 8.0
+Version 9.0
===========
+For users
+---------
+- Messages reflections are now properly cut if the body was cut before
+ being to sent to IRC
+- Messages from unjoined resources are now rejected instead of being accepted.
+ This helps clients understand that they are not in the room (because of
+ some connection issue for example).
+- All commands sent to IRC servers are now throttled to avoid being
+ disconnected for excess flood. The limit value can be customized using the
+ ad-hoc configuration form on a server JID.
+
+For admins
+----------
+- SIGHUP is now caught and reloads the configuration like SIGUSR1 and 2.
+- Add a verify_certificate policy option that lets the admin disable
+ certificate validation per-domain.
+
+Version 8.3 - 2018-06-01
+========================
+
+- The global ad-hoc configure command is now available on biboumi’s JID in
+ fixed_irc_server mode.
+
+Version 8.2 - 2018-05-23
+========================
+
+- The users are not able to bypass the fixed mode by just configuring a
+ different Address for the IRC server anymore.
+
+Version 8.1 - 2018-05-14
+========================
+
+- Fix a crash on a raw NAMES command
+
+Version 8.0 - 2018-05-02
+========================
+
+- GCC 4.9 or lower are not supported anymore. The minimal version is 5.0
- Add a complete='true' in MAM’s iq result when appropriate
+- The archive ordering now only relies on the value of the ID, not the
+ date. This means that if you manually import archives in your database (or
+ mess with it somehow), biboumi will not work properly anymore, if you
+ don’t make sure the ID of everything in the muclogline table is
+ consistent.
+- The “virtual” channel with an empty name (for example
+ %irc.freenode.net@biboumi) has been entirely removed.
+- Add an “Address” field in the servers’ configure form. This lets
+ the user customize the address to use when connecting to a server.
+ See https://lab.louiz.org/louiz/biboumi/issues/3273 for more details.
+- Messages id are properly reflected to the sender
+- We now properly deal with a PostgreSQL server restart: whenever the
+ connection is lost with the server, we try to reconnect and re-execute the
+ query once.
+- A Nick field has been added in the IRC server configuration form, to let
+ the user force a nickname whenever a channel on the server is joined.
+- Multiple admins can now be listed in the admin field, separated with a colon.
+- Missing fields in a data-form response are now properly interpreted as
+ an empty value, and not the default value. Gajim users were not able to
+ empty a field of type text-multi because of this issue.
+- Fix an uncaught exception with botan, when policy does not allow any
+ available ciphersuite.
+- When the connection gets desynchronized and tries to re-join while
+ biboumi thinks it has never left, biboumi now sends the whole standard
+ join sequence (history, user-list, etc).
Version 7.2 - 2018-01-24
========================
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2b3f292..e217171 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0)
project(biboumi)
-set(${PROJECT_NAME}_VERSION_MAJOR 8)
+set(${PROJECT_NAME}_VERSION_MAJOR 9)
set(${PROJECT_NAME}_VERSION_MINOR 0)
set(${PROJECT_NAME}_VERSION_SUFFIX "~dev")
@@ -74,26 +74,8 @@ set(SOFTWARE_VERSION
#
## The rule that generates the documentation
#
-execute_process(COMMAND "date" "+%Y-%m-%d" OUTPUT_VARIABLE DOC_DATE
- OUTPUT_STRIP_TRAILING_WHITESPACE)
-set(MAN_PAGE ${CMAKE_CURRENT_BINARY_DIR}/doc/${PROJECT_NAME}.1)
-set(DOC_PAGE ${CMAKE_CURRENT_SOURCE_DIR}/doc/${PROJECT_NAME}.1.rst)
-if (NOT PANDOC_EXECUTABLE)
- find_program(PANDOC_EXECUTABLE NAMES pandoc
- DOC "The pandoc software, to build the man page from the rst documentation")
- if(PANDOC_EXECUTABLE)
- message(STATUS "Found Pandoc: ${PANDOC_EXECUTABLE}")
- set(WITH_DOC true)
- file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/)
- add_custom_command(OUTPUT ${MAN_PAGE}
- COMMAND ${PANDOC_EXECUTABLE} -M date="${DOC_DATE}" -s -t man ${DOC_PAGE} -o ${MAN_PAGE}
- DEPENDS ${DOC_PAGE})
- add_custom_target(doc ALL DEPENDS ${MAN_PAGE})
- else()
- message(STATUS "Pandoc not found, documentation cannot be built")
- endif()
-endif()
-mark_as_advanced(PANDOC_EXECUTABLE)
+add_custom_target(doc COMMAND make html BUILDDIR=${CMAKE_CURRENT_BINARY_DIR}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/doc)
#
## Set this search path for cmake, to find our custom search modules
@@ -218,8 +200,6 @@ if(SQLITE3_FOUND OR PQ_FOUND)
include_directories(database ${PQ_INCLUDE_DIRS})
endif()
set(USE_DATABASE TRUE)
-else()
- add_library(database OBJECT "")
endif()
#
@@ -235,8 +215,7 @@ add_executable(${PROJECT_NAME} src/main.cpp
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:irc>
- $<TARGET_OBJECTS:identd>
- $<TARGET_OBJECTS:database>)
+ $<TARGET_OBJECTS:identd>)
## test_suite
file(GLOB source_tests
@@ -249,9 +228,12 @@ add_executable(test_suite ${source_tests}
$<TARGET_OBJECTS:xmpp>
$<TARGET_OBJECTS:bridge>
$<TARGET_OBJECTS:irc>
- $<TARGET_OBJECTS:identd>
- $<TARGET_OBJECTS:database>)
+ $<TARGET_OBJECTS:identd>)
set_target_properties(test_suite PROPERTIES EXCLUDE_FROM_ALL TRUE)
+if(USE_DATABASE)
+ target_sources(${PROJECT_NAME} PRIVATE $<TARGET_OBJECTS:database>)
+ target_sources(test_suite PRIVATE $<TARGET_OBJECTS:database>)
+endif()
#
## Link the executables with their libraries
@@ -318,7 +300,7 @@ set_target_properties(catch PROPERTIES EXCLUDE_FROM_ALL TRUE)
ExternalProject_Get_Property(catch SOURCE_DIR)
if(NOT EXISTS ${CMAKE_SOURCE_DIR}/tests/catch.hpp)
target_include_directories(test_suite
- PUBLIC "${SOURCE_DIR}/include/"
+ PUBLIC "${SOURCE_DIR}/single_include/"
)
add_dependencies(test_suite catch)
endif()
diff --git a/README.rst b/README.rst
index 8a03701..f08fba0 100644
--- a/README.rst
+++ b/README.rst
@@ -1,15 +1,6 @@
Biboumi
=======
-.. image:: https://lab.louiz.org/louiz/biboumi/badges/master/build.svg
- :target: https://lab.louiz.org/louiz/biboumi/pipelines
-
-.. image:: https://coverity.proxy.louiz.org/projects/3726/badge.svg
- :target: https://scan.coverity.com/projects/louiz-biboumi
-
-.. image:: https://coreinfrastructure.proxy.louiz.org/projects/450/badge
- :target: https://bestpractices.coreinfrastructure.org/projects/450
-
Biboumi is an XMPP gateway that connects to IRC servers and translates
between the two protocols. It can be used to access IRC channels using any
XMPP client as if these channels were XMPP MUCs.
@@ -60,6 +51,6 @@ Biboumi is Free Software.
Biboumi is released under the zlib license.
Please read the COPYING file for details.
-.. _INSTALL: INSTALL.rst
-.. _the documentation: doc/biboumi.1.rst
+.. _INSTALL: doc/install.rst
+.. _the documentation: https://doc.biboumi.louiz.org
.. _contributing: CONTRIBUTING.rst
diff --git a/cmake/Modules/CodeCoverage.cmake b/cmake/Modules/CodeCoverage.cmake
index 77586ab..9fde45e 100644
--- a/cmake/Modules/CodeCoverage.cmake
+++ b/cmake/Modules/CodeCoverage.cmake
@@ -157,7 +157,7 @@ FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname)
# Remove information about source files that are not part of
# the test (system file, external libraries, etc)
- COMMAND ${LCOV_PATH} --remove ${_outputname}.info 'tests/*' '/usr/*' 'external/*' 'build/*' --output-file ${_outputname}.info -q
+ COMMAND ${LCOV_PATH} --remove ${_outputname}.info '/usr/*' '*/external/*' --output-file ${_outputname}.info -q
# Generate the report
COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info
diff --git a/conf/irc.gimp.org.policy.txt b/conf/irc.gimp.org.policy.txt
index 2357a53..43a7d80 100644
--- a/conf/irc.gimp.org.policy.txt
+++ b/conf/irc.gimp.org.policy.txt
@@ -1 +1,2 @@
key_exchange_methods = RSA
+signature_methods = IMPLICIT
diff --git a/conf/irc.gnome.org.policy.txt b/conf/irc.gnome.org.policy.txt
index 2357a53..f06b958 100644..120000
--- a/conf/irc.gnome.org.policy.txt
+++ b/conf/irc.gnome.org.policy.txt
@@ -1 +1 @@
-key_exchange_methods = RSA
+irc.gimp.org.policy.txt \ No newline at end of file
diff --git a/conf/irc.ppirc.net.policy.txt b/conf/irc.ppirc.net.policy.txt
index 2357a53..43a7d80 100644
--- a/conf/irc.ppirc.net.policy.txt
+++ b/conf/irc.ppirc.net.policy.txt
@@ -1 +1,2 @@
key_exchange_methods = RSA
+signature_methods = IMPLICIT
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..934bdf7
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SPHINXPROJ = biboumi
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file
diff --git a/doc/admin.rst b/doc/admin.rst
new file mode 100644
index 0000000..a5850a7
--- /dev/null
+++ b/doc/admin.rst
@@ -0,0 +1,305 @@
+###########################
+Administrator documentation
+###########################
+
+Usage
+=====
+
+Biboumi acts as a server, it should be run as a daemon that lives in the
+background for as long as it is needed. Note that biboumi does not
+daemonize itself, this task should be done by your init system (SysVinit,
+systemd, upstart).
+
+When started, biboumi connects, without encryption (see :ref:`Security`), to the
+local XMPP server on the port ``5347`` and authenticates with the provided
+password. Biboumi then serves the configured ``hostname``: this means that
+all XMPP stanza with a `to` JID on that domain will be forwarded to biboumi
+by the XMPP server, and biboumi will only send messages coming from that
+hostname.
+
+To cleanly shutdown the component, send a SIGINT or SIGTERM signal to it.
+It will send messages to all connected IRC and XMPP servers to indicate a
+reason why the users are being disconnected. Biboumi exits when the end of
+communication is acknowledged by all IRC servers. If one or more IRC
+servers do not respond, biboumi will only exit if it receives the same
+signal again or if a 2 seconds delay has passed.
+
+Configuration
+=============
+
+Configuration happens in different places, with different purposes:
+
+- The main and global configuration that specifies vital settings for the
+ daemon to run, like the hostname, password etc. This is an admin-only
+ configuration, and this is described in the next section.
+- A TLS configuration, also admin-only, that can be either global or
+ per-domain. See `TLS configuration`_ section.
+- Using the :ref:`ad-hoc commands`, each user can configure various
+ settings for themself
+
+Daemon configuration
+--------------------
+
+The configuration file is read by biboumi as it starts. The path is
+specified as the only argument to the biboumi binary.
+
+The configuration file uses a simple format of the form ``option=value``
+(note that there are no spaces before or after the equal sign).
+
+The values from the configuration file can be overridden by environment
+variables, with the name all in upper case and prefixed with `BIBOUMI_`.
+For example, if the environment contains “BIBOUMI_PASSWORD=blah", this will
+override the value of the “password” option in the configuration file.
+
+Sending SIGUSR1, SIGUSR2 or SIGHUP (see kill(1)) to the process will force
+it to re-read the configuration and make it close and re-open the log
+files. You can use this to change any configuration option at runtime, or
+do a log rotation.
+
+Options
+-------
+
+A configuration file can look something like this:
+
+.. code-block:: ini
+
+ hostname=biboumi.example.com
+ password=mypassword
+ xmpp_server_ip=127.0.0.1
+ port=5347
+ admin=myself@example.com
+ db_name=postgresql://biboumi:password@localhost/biboumi
+ realname_customization=true
+ realname_from_jid=false
+ log_file=
+ ca_file=
+ outgoing_bind=192.168.0.12
+
+
+Here is a description of all available options
+
+hostname
+~~~~~~~~
+
+Mandatory. The hostname served by the XMPP gateway. This domain must be
+configured in the XMPP server as an external component. See the manual
+for your XMPP server for more information. For prosody, see
+http://prosody.im/doc/components#adding_an_external_component
+
+password
+~~~~~~~~
+
+Mandatory. The password used to authenticate the XMPP component to your
+XMPP server. This password must be configured in the XMPP server,
+associated with the external component on *hostname*.
+
+xmpp_server_ip
+~~~~~~~~~~~~~~
+
+The IP address to connect to the XMPP server on. The connection to the
+XMPP server is unencrypted, so the biboumi instance and the server should
+normally be on the same host. The default value is 127.0.0.1.
+
+port
+~~~~
+
+The TCP port to use to connect to the local XMPP component. The default
+value is 5347.
+
+db_name
+~~~~~~~
+
+The name of the database to use. This option can only be used if biboumi
+has been compiled with a database support (Sqlite3 and/or PostgreSQL). If
+the value begins with the postgresql scheme, “postgresql://” or
+“postgres://”, then biboumi will try to connect to the PostgreSQL database
+specified by the URI. See `the PostgreSQL doc
+<https://www.postgresql.org/docs/current/static/libpq-connect.html#idm46428693970032>`_
+for all possible values. For example the value could be
+“postgresql://user:secret@localhost”. If the value does not start with the
+postgresql scheme, then it specifies a filename that will be opened with
+Sqlite3. For example the value could be “/var/lib/biboumi/biboumi.sqlite”.
+
+admin
+~~~~~
+
+The bare JID of the gateway administrator. This JID will have more
+privileges than other standard users, for example some administration
+ad-hoc commands will only be available to that JID.
+
+If you need more than one administrator, separate them with a colon (:).
+
+fixed_irc_server
+~~~~~~~~~~~~~~~~
+
+If this option contains the hostname of an IRC server (for example
+irc.example.org), then biboumi will enforce the connexion to that IRC
+server only. This means that a JID like ``#chan@biboumi.example.com``
+must be used instead of ``#chan%irc.example.org@biboumi.example.com``. The
+`%` character loses any meaning in the JIDs. It can appear in the JID but
+will not be interpreted as a separator (thus the JID
+``#channel%hello@biboumi.example.com`` points to the channel named
+``#channel%hello`` on the configured IRC server) This option can for
+example be used by an administrator that just wants to let their users
+join their own IRC server using an XMPP client, while forbidding access to
+any other IRC server.
+
+persistent_by_default
+~~~~~~~~~~~~~~~~~~~~~
+
+If this option is set to `true`, all rooms will be persistent by default:
+the value of the “persistent” option in the global configuration of each
+user will be “true”, but the value of each individual room will still
+default to false. This means that a user just needs to change the global
+“persistent” configuration option to false in order to override this.
+
+If it is set to false (the default value), all rooms are not persistent by
+default.
+
+Each room can be configured individually by each user, to override this
+default value. See :ref:`Ad-hoc commands`.
+
+realname_customization
+~~~~~~~~~~~~~~~~~~~~~~
+
+If this option is set to “false” (default is “true”), the users will not be
+able to use the ad-hoc commands that lets them configure their realname and
+username.
+
+realname_from_jid
+~~~~~~~~~~~~~~~~~
+
+If this option is set to “true”, the realname and username of each biboumi
+user will be extracted from their JID. The realname is their bare JID, and
+the username is the node-part of their JID. Note that if
+``realname_customization`` is “true”, each user will still be able to
+customize their realname and username, this option just decides the default
+realname and username.
+
+If this option is set to “false” (the default value), the realname and
+username of each user will be set to the nick they used to connect to the
+IRC server.
+
+webirc_password
+~~~~~~~~~~~~~~~
+
+Configure a password to be communicated to the IRC server, as part of the
+WEBIRC message (see https://kiwiirc.com/docs/webirc). If this option is
+set, an additional DNS resolution of the hostname of each XMPP server will
+be made when connecting to an IRC server.
+
+log_file
+~~~~~~~~
+
+A filename into which logs are written. If none is provided, the logs are
+written on standard output.
+
+log_level
+~~~~~~~~~
+
+Indicate what type of log messages to write in the logs. Value can be
+from 0 to 3. 0 is debug, 1 is info, 2 is warning, 3 is error. The
+default is 0, but a more practical value for production use is 1.
+
+ca_file
+~~~~~~~
+
+Specifies which file should be used as the list of trusted CA when
+negociating a TLS session. By default this value is unset and biboumi
+tries a list of well-known paths.
+
+outgoing_bind
+~~~~~~~~~~~~~
+
+An address (IPv4 or IPv6) to bind the outgoing sockets to. If no value is
+specified, it will use the one assigned by the operating system. You can
+for example use outgoing_bind=192.168.1.11 to force biboumi to use the
+interface with this address. Note that this is only used for connections
+to IRC servers.
+
+identd_port
+~~~~~~~~~~~
+
+The TCP port on which to listen for identd queries. The default is the
+standard value: 113. To be able to listen on this privileged port, biboumi
+needs to have certain capabilities: on linux, using systemd, this can be
+achieved by adding `AmbientCapabilities=CAP_NET_BIND_SERVICE` to the unit
+file. On other systems, other solutions exist, like the portacl module on
+FreeBSD.
+
+If biboumi’s identd server is properly started, it will receive queries from
+the IRC servers asking for the “identity” of each IRC connection made to it.
+Biboumi will answer with a hash of the JID that made the connection. This is
+useful for the IRC server to be able to distinguish the different users, and
+be able to deal with the absuses without having to simply ban the IP. Without
+this identd server, moderation is a lot harder, because all the different
+users of a single biboumi instance all share the same IP, and they can’t be
+distinguished by the IRC servers.
+
+To disable the built-in identd, you may set identd_port to 0.
+
+policy_directory
+~~~~~~~~~~~~~~~~
+
+A directory that should contain the policy files, used to customize
+Botan’s behaviour when negociating the TLS connections with the IRC
+servers. If not specified, the directory is the one where biboumi’s
+configuration file is located: for example if biboumi reads its
+configuration from /etc/biboumi/biboumi.cfg, the policy_directory value
+will be /etc/biboumi.
+
+
+TLS configuration
+-----------------
+
+Various settings of the TLS connections can be customized using policy
+files. The files should be located in the directory specified by the
+configuration option `policy_directory`_. When attempting to connect to
+an IRC server using TLS, biboumi will use Botan’s default TLS policy, and
+then will try to load some policy files to override the values found in
+these files. For example, if policy_directory is /etc/biboumi, when
+trying to connect to irc.example.com, biboumi will try to read
+/etc/biboumi/policy.txt, use the values found to override the default
+values, then it will try to read /etc/biboumi/irc.example.com.policy.txt
+and re-override the policy with the values found in this file.
+
+The policy.txt file applies to all the connections, and
+irc.example.policy.txt will only apply (in addition to policy.txt) when
+connecting to that specific server.
+
+To see the list of possible options to configure, refer to `Botan’s TLS
+documentation <https://botan.randombit.net/manual/tls.html#tls-policies>`_.
+In addition to these Botan options, biboumi implements a few custom options
+listed hereafter:
+- verify_certificate: if this value is set to false, biboumi will not check
+the certificate validity at all. The default value is true.
+
+By default, biboumi provides a few policy files, to work around some
+issues found with a few well-known IRC servers.
+
+
+Security
+========
+
+The connection to the XMPP server can only be made on localhost. The
+XMPP server is not supposed to accept non-local connections from
+components. Thus, encryption is not used to connect to the local
+XMPP server because it is useless.
+
+If compiled with the Botan library, biboumi can use TLS when communicating
+with the IRC servers. It will first try ports 6697 and 6670 and use TLS
+if it succeeds, if connection fails on both these ports, the connection is
+established on port 6667 without any encryption.
+
+Biboumi does not check if the received JIDs are properly formatted using
+nodeprep. This must be done by the XMPP server to which biboumi is
+directly connected.
+
+Biboumi does not provide a way to ban users from connecting to it, has no
+protection against flood or any sort of abuse that your users may cause on
+the IRC servers. Some XMPP server however offer the possibility to restrict
+what JID can access a gateway. Use that feature if you wish to grant access
+to your biboumi instance only to a list of trusted users.
+
+
+
diff --git a/doc/biboumi.1.rst b/doc/biboumi.1.rst
deleted file mode 100644
index 89f8627..0000000
--- a/doc/biboumi.1.rst
+++ /dev/null
@@ -1,757 +0,0 @@
-======================
-Biboumi(1) User Manual
-======================
-
-.. contents:: :depth: 2
-
-NAME
-====
-
-biboumi - XMPP gateway to IRC
-
-Description
-===========
-
-Biboumi is an XMPP gateway that connects to IRC servers and translates
-between the two protocols. It can be used to access IRC channels using any
-XMPP client as if these channels were XMPP MUCs.
-
-Synopsis
-========
-
-biboumi [*config_filename*]
-
-Options
-=======
-
-Available command line options:
-
-config_filename
----------------
-
-Specify the file to read for configuration. See the `Configuration`_ section for more
-details on its content.
-
-Configuration
-=============
-
-The configuration file uses a simple format of the form ``option=value``.
-
-The values from the configuration file can be overridden by environment
-variables, with the name all in upper case and prefixed with "BIBOUMI_".
-For example, if the environment contains “BIBOUMI_PASSWORD=blah", this will
-override the value of the “password” option in the configuration file.
-
-Sending SIGUSR1 or SIGUSR2 (see kill(1)) to the process will force it to
-re-read the configuration and make it close and re-open the log files. You
-can use this to change any configuration option at runtime, or do a log
-rotation.
-
-Here is a description of every possible option:
-
-hostname
---------
-
-Mandatory. The hostname served by the XMPP gateway. This domain must be
-configured in the XMPP server as an external component. See the manual
-for your XMPP server for more information. For prosody, see
-http://prosody.im/doc/components#adding_an_external_component
-
-password
---------
-
-Mandatory. The password used to authenticate the XMPP component to your
-XMPP server. This password must be configured in the XMPP server,
-associated with the external component on *hostname*.
-
-xmpp_server_ip
---------------
-
-The IP address to connect to the XMPP server on. The connection to the
-XMPP server is unencrypted, so the biboumi instance and the server should
-normally be on the same host. The default value is 127.0.0.1.
-
-port
-----
-
-The TCP port to use to connect to the local XMPP component. The default
-value is 5347.
-
-db_name
--------
-
-The name of the database to use. This option can only be used if biboumi
-has been compiled with a database support (Sqlite3 and/or PostgreSQL). If
-the value begins with the postgresql scheme, “postgresql://” or
-“postgres://”, then biboumi will try to connect to the PostgreSQL database
-specified by the URI. See
-https://www.postgresql.org/docs/current/static/libpq-connect.html#idm46428693970032
-for all possible values. For example the value could be
-“postgresql://user:secret@localhost”. If the value does not start with the
-postgresql scheme, then it specifies a filename that will be opened with
-Sqlite3. For example the value could be “/var/lib/biboumi/biboumi.sqlite”.
-
-admin
------
-
-The bare JID of the gateway administrator. This JID will have more
-privileges than other standard users, for example some administration
-ad-hoc commands will only be available to that JID.
-
-fixed_irc_server
-----------------
-
-If this option contains the hostname of an IRC server (for example
-irc.example.org), then biboumi will enforce the connexion to that IRC
-server only. This means that a JID like ``#chan@biboumi.example.com`` must
-be used instead of ``#chan%irc.example.org@biboumi.example.com``. In that
-mode, the virtual channel (see `Connect to an IRC server`_) is not
-available. The `%` character loses any meaning in the JIDs. It can appear
-in the JID but will not be interpreted as a separator (thus the JID
-``#channel%hello@biboumi.example.com`` points to the channel named
-``#channel%hello`` on the configured IRC server) This option can for example
-be used by an administrator that just wants to let their users join their own
-IRC server using an XMPP client, while forbidding access to any other IRC
-server.
-
-persistent_by_default
----------------------
-
-If this option is set to `true`, all rooms will be persistent by default:
-the value of the “persistent” option in the global configuration of each
-user will be “true”, but the value of each individual room will still
-default to false. This means that a user just needs to change the global
-“persistent” configuration option to false in order to override this.
-
-If it is set to false (the default value), all rooms are not persistent by
-default.
-
-Each room can be configured individually by each user, to override this
-default value. See `Ad-hoc commands`_.
-
-realname_customization
-----------------------
-
-If this option is set to “false” (default is “true”), the users will not be
-able to use the ad-hoc commands that lets them configure their realname and
-username.
-
-realname_from_jid
------------------
-
-If this option is set to “true”, the realname and username of each biboumi
-user will be extracted from their JID. The realname is their bare JID, and
-the username is the node-part of their JID. Note that if
-``realname_customization`` is “true”, each user will still be able to
-customize their realname and username, this option just decides the default
-realname and username.
-
-If this option is set to “false” (the default value), the realname and
-username of each user will be set to the nick they used to connect to the
-IRC server.
-
-webirc_password
----------------
-
-Configure a password to be communicated to the IRC server, as part of the
-WEBIRC message (see https://kiwiirc.com/docs/webirc). If this option is
-set, an additional DNS resolution of the hostname of each XMPP server will
-be made when connecting to an IRC server.
-
-log_file
---------
-
-A filename into which logs are written. If none is provided, the logs are
-written on standard output.
-
-log_level
----------
-
-Indicate what type of log messages to write in the logs. Value can be
-from 0 to 3. 0 is debug, 1 is info, 2 is warning, 3 is error. The
-default is 0, but a more practical value for production use is 1.
-
-ca_file
--------
-
-Specifies which file should be used as the list of trusted CA when
-negociating a TLS session. By default this value is unset and biboumi
-tries a list of well-known paths.
-
-outgoing_bind
--------------
-
-An address (IPv4 or IPv6) to bind the outgoing sockets to. If no value is
-specified, it will use the one assigned by the operating system. You can
-for example use outgoing_bind=192.168.1.11 to force biboumi to use the
-interface with this address. Note that this is only used for connections
-to IRC servers.
-
-identd_port
------------
-
-The TCP port on which to listen for identd queries. The default is the
-standard value: 113. To be able to listen on this privileged port, biboumi
-needs to have certain capabilities: on linux, using systemd, this can be
-achieved by adding `AmbientCapabilities=CAP_NET_BIND_SERVICE` to the unit
-file. On other systems, other solutions exist, like the portacl module on
-FreeBSD.
-
-If biboumi’s identd server is properly started, it will receive queries from
-the IRC servers asking for the “identity” of each IRC connection made to it.
-Biboumi will answer with a hash of the JID that made the connection. This is
-useful for the IRC server to be able to distinguish the different users, and
-be able to deal with the absuses without having to simply ban the IP. Without
-this identd server, moderation is a lot harder, because all the different
-users of a single biboumi instance all share the same IP, and they can’t be
-distinguished by the IRC servers.
-
-policy_directory
-----------------
-
-A directory that should contain the policy files, used to customize
-Botan’s behaviour when negociating the TLS connections with the IRC
-servers. If not specified, the directory is the one where biboumi’s
-configuration file is located: for example if biboumi reads its
-configuration from /etc/biboumi/biboumi.cfg, the policy_directory value
-will be /etc/biboumi.
-
-
-TLS configuration
-=================
-
-Various settings of the TLS connections can be customized using policy
-files. The files should be located in the directory specified by the
-configuration option `policy_directory`_. When attempting to connect to
-an IRC server using TLS, biboumi will use Botan’s default TLS policy, and
-then will try to load some policy files to override the values found in
-these files. For example, if policy_directory is /etc/biboumi, when
-trying to connect to irc.example.com, biboumi will try to read
-/etc/biboumi/policy.txt, use the values found to override the default
-values, then it will try to read /etc/biboumi/irc.example.com.policy.txt
-and re-override the policy with the values found in this file.
-
-The policy.txt file applies to all the connections, and
-irc.example.policy.txt will only apply (in addition to policy.txt) when
-connecting to that specific server.
-
-To see the list of possible options to configure, refer to `Botan’s TLS
-documentation <https://botan.randombit.net/manual/tls.html#tls-policies>`_.
-
-By default, biboumi provides a few policy files, to work around some
-issues found with a few well-known IRC servers.
-
-Usage
-=====
-
-Biboumi acts as a server, it should be run as a daemon that lives in the
-background for as long as it is needed. Note that biboumi does not
-daemonize itself, this task should be done by your init system (SysVinit,
-systemd, upstart).
-
-When started, biboumi connects, without encryption (see `Security`_), to the
-local XMPP server on the port ``5347`` and authenticates with the provided
-password. Biboumi then serves the configured ``hostname``: this means that
-all XMPP stanza with a `to` JID on that domain will be forwarded to biboumi
-by the XMPP server, and biboumi will only send messages coming from that
-hostname.
-
-When a user joins an IRC channel on an IRC server (see `Join an IRC
-channel`_), biboumi connects to the remote IRC server, sets the user’s nick
-as requested, and then tries to join the specified channel. If the same
-user subsequently tries to connect to an other channel on the same server,
-the same IRC connection is used. If, however, an other user wants to join
-an IRC channel on that same IRC server, biboumi opens a new connection to
-that server. Biboumi connects once to each IRC server, for each user on it.
-
-Additionally, if one user is using more than one clients (with the same bare
-JID), they can join the same IRC channel (on the same server) behind one
-single nickname. Biboumi will forward all the messages (the channel ones and
-the private ones) and the presences to all the resources behind that nick.
-There is no need to have multiple nicknames and multiple connections to be
-able to take part in a conversation (or idle) in a channel from a mobile client
-while the desktop client is still connected, for example.
-
-To cleanly shutdown the component, send a SIGINT or SIGTERM signal to it.
-It will send messages to all connected IRC and XMPP servers to indicate a
-reason why the users are being disconnected. Biboumi exits when the end of
-communication is acknowledged by all IRC servers. If one or more IRC
-servers do not respond, biboumi will only exit if it receives the same
-signal again or if a 2 seconds delay has passed.
-
-Addressing
-----------
-
-IRC entities are represented by XMPP JIDs. The domain part of the JID is
-the domain served by biboumi (the part after the `@`, biboumi.example.com in
-the examples), and the local part (the part before the `@`) depends on the
-concerned entity.
-
-IRC channels and IRC users have a local part formed like this:
-``name`` % ``irc_server``.
-
-``name`` can be a channel name or an user nickname. The distinction between
-the two is based on the first character: by default, if the name starts with
-``'#'`` or ``'&'`` (but this can be overridden by the server, using the
-ISUPPORT extension) then it’s a channel name, otherwise this is a nickname.
-
-As a special case, the channel name can also be empty (for example
-``%irc.example.com``), in that case this represents the virtual channel
-provided by biboumi. See `Connect to an IRC server`_ for more details.
-
-There is two ways to address an IRC user, using a local part like this:
-``nickname`` % ``irc_server`` or by using the in-room address of the
-participant, like this:
-``channel_name`` % ``irc_server`` @ ``biboumi.example.com`` / ``Nickname``
-
-The second JID is available only to be compatible with XMPP clients when the
-user wants to send a private message to the participant ``Nickname`` in the
-room ``channel_name%irc_server@biboumi.example.com``.
-
-On XMPP, the node part of the JID can only be lowercase. On the other hand,
-IRC nicknames are case-insensitive, this means that the nicknames toto,
-Toto, tOtO and TOTO all represent the same IRC user. This means you can
-talk to the user toto, and this will work.
-
-Also note that some IRC nicknames or channels may contain characters that are
-not allowed in the local part of a JID (for example '@'). If you need to send a
-message to a nick containing such a character, you can use a jid like
-``%irc.example.com@biboumi.example.com/AnnoyingNickn@me``, because the JID
-``AnnoyingNickn@me%irc.example.com@biboumi.example.com`` would not work.
-And if you need to address a channel that contains such invalid characters, you
-have to use `jid-escaping <http://www.xmpp.org/extensions/xep-0106.html#escaping>`_,
-and replace each of these characters with their escaped version, for example to
-join the channel ``#b@byfoot``, you need to use the following JID:
-``#b\40byfoot%irc.example.com@biboumi.example.com``.
-
-
-Examples:
-
-* ``#foo%irc.example.com@biboumi.example.com`` is the #foo IRC channel, on the
- irc.example.com IRC server, and this is served by the biboumi instance on
- biboumi.example.com
-
-* ``toto%irc.example.com@biboumi.example.com`` is the IRC user named toto, or
- TotO, etc.
-
-* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com.
-
-* ``%irc.example.com@biboumi.example.com`` is the virtual channel provided by
- biboumi, for the IRC server irc.example.com.
-
-Note: Some JIDs are valid but make no sense in the context of
-biboumi:
-
-* ``#test%@biboumi.example.com``, or any other JID that does not contain an
- IRC server is invalid. Any message to that kind of JID will trigger an
- error, or will be ignored.
-
-If compiled with Libidn, an IRC channel participant has a bare JID
-representing the “hostname” provided by the IRC server. This JID can only
-be used to set IRC modes (for example to ban a user based on its IP), or to
-identify user. It cannot be used to contact that user using biboumi.
-
-Join an IRC channel
--------------------
-
-To join an IRC channel ``#foo`` on the IRC server ``irc.example.com``,
-join the XMPP MUC ``#foo%irc.example.com@biboumi.example.com``.
-
-Connect to an IRC server
-------------------------
-
-The connection to the IRC server is automatically made when the user tries
-to join any channel on that IRC server. The connection is closed whenever
-the last channel on that server is left by the user. To be able to stay
-connected to an IRC server without having to be in a real IRC channel,
-biboumi provides a virtual channel on the jid
-``%irc.example.com@biboumi.example.com``. For example if you want to join the
-channel ``#foo`` on the server ``irc.example.com``, but you need to authenticate
-to a bot of the server before you’re allowed to join it, you can first join
-the room ``%irc.example.com@biboumi.example.com`` (this will effectively
-connect you to the IRC server without joining any channel), then send your
-authentication message to the user ``bot%irc.example.com@biboumi.example.com``
-and finally join the room ``#foo%irc.example.com@biboumi.example.com``.
-
-Roster
-------
-
-You can add some JIDs provided by biboumi into your own roster, to receive
-presence from them. Biboumi will always automatically accept your requests.
-
-Biboumi’s JID
--------------
-
-By adding the component JID into your roster, the user will receive an available
-presence whenever it is started, and an unavailable presence whenever it is being
-shutdown. This is useful to quickly view if that biboumi instance is started or
-not.
-
-IRC server JID
---------------
-
-These presence will appear online in the user’s roster whenever they are
-connected to that IRC server (see `Connect to an IRC server`_ for more
-details). This is useful to keep track of which server an user is connected
-to: this is sometimes hard to remember, when they have many clients, or if
-they are using persistent channels.
-
-Channel messages
-----------------
-
-On XMPP, unlike on IRC, the displayed order of the messages is the same for
-all participants of a MUC. Biboumi can not however provide this feature, as
-it cannot know whether the IRC server has received and forwarded the
-messages to other users. This means that the order of the messages
-displayed in your XMPP client may not be the same as the order on other
-IRC users’.
-
-History
--------
-
-Public channel messages are saved into archives, inside the database, unless
-the `record_history` option is set to false by that user (see `Ad-hoc commands`_).
-Private messages (messages that are sent directly to a nickname, not a
-channel) are never stored in the database.
-
-A channel history can be retrieved by using `Message archive management (MAM)
-<https://xmpp.org/extensions/xep-0313.htm>`_ on the channel JID. The results
-can be filtered by start and end dates.
-
-When a channel is joined, if the client doesn’t specify any limit, biboumi
-sends the `max_history_length` last messages found in the database as the
-MUC history. If a client wants to only use MAM for the archives (because
-it’s more convenient and powerful), it should request to receive no
-history by using an attribute maxchars='0' or maxstanzas='0' as defined in
-XEP 0045, and do a proper MAM request instead.
-
-Note: the maxchars attribute is ignored unless its value is exactly 0.
-Supporting it properly would be very hard and would introduce a lot of
-complexity for almost no benefit.
-
-For a given channel, each user has her or his own archive. The content of
-the archives are never shared, and thus a user can not use someone else’s
-archive to get the messages that they didn’t receive when they were offline.
-Although this feature would be very convenient, this would introduce a very
-important privacy issue: for example if a biboumi gateway is used by two
-users, by querying the archive one user would be able to know whether or not
-the other user was in a room at a given time.
-
-
-List channels
--------------
-
-You can list the IRC channels on a given IRC server by sending an XMPP disco
-items request on the IRC server JID. The number of channels on some servers
-is huge so the result stanza may be very big, unless your client supports
-result set management (XEP 0059)
-
-Nicknames
----------
-
-On IRC, nicknames are server-wide. This means that one user only has one
-single nickname at one given time on all the channels of a server. This is
-different from XMPP where a user can have a different nick on each MUC,
-even if these MUCs are on the same server.
-
-This means that the nick you choose when joining your first IRC channel on a
-given IRC server will be your nickname in all other channels that you join
-on that same IRC server.
-If you explicitely change your nickname on one channel, your nickname will
-be changed on all channels on the same server as well.
-Joining a new channel with a different nick, however, will not change your
-nick. The provided nick will be ignored, in order to avoid changing your
-nick on the whole server by mistake. If you want to have a different
-nickname in the channel you’re going to join, you need to do it explicitly
-with the NICK command before joining the channel.
-
-Private messages
-----------------
-
-Private messages are handled differently on IRC and on XMPP. On IRC, you
-talk directly to one server-user: toto on the channel #foo is the same user
-as toto on the channel #bar (as long as these two channels are on the same
-IRC server). By default you will receive private messages from the “global”
-user (aka nickname%irc.example.com@biboumi.example.com), unless you
-previously sent a message to an in-room participant (something like
-\#test%irc.example.com@biboumi.example.com/nickname), in which case future
-messages from that same user will be received from that same “in-room” JID.
-
-Notices
--------
-
-Notices are received exactly like private messages. It is not possible to
-send a notice.
-
-Topic
------
-
-The topic can be set and retrieved seemlessly. The unique difference is that
-if an XMPP user tries to set a multiline topic, every line return (\\n) will
-be replaced by a space, because the IRC server wouldn’t accept it.
-
-Invitations
------------
-
-If the invited JID is a user JID served by this biboumi instance, it will forward the
-invitation to the target nick, over IRC.
-Otherwise, the mediated instance will directly be sent to the invited JID, over XMPP.
-
-Example: if the user wishes to invite the IRC user “FooBar” into a room, they can
-invite one of the following “JIDs” (one of them is not a JID, actually):
-
-- foobar%anything@biboumi.example.com
-- anything@biboumi.example.com/FooBar
-- FooBar
-
-(Note that the “anything” parts are simply ignored because they carry no
-additional meaning for biboumi: we already know which IRC server is targeted
-using the JID of the target channel.)
-
-Otherwise, any valid JID can be used, to invite any XMPP user.
-
-Kicks and bans
---------------
-
-Kicks are transparently translated from one protocol to another. However
-banning an XMPP participant has no effect. To ban an user you need to set a
-mode +b on that user nick or host (see `IRC modes`_) and then kick it.
-
-Encoding
---------
-
-On XMPP, the encoding is always ``UTF-8``, whereas on IRC the encoding of
-each message can be anything.
-
-This means that biboumi has to convert everything coming from IRC into UTF-8
-without knowing the encoding of the received messages. To do so, it checks
-if each message is UTF-8 valid, if not it tries to convert from
-``iso_8859-1`` (because this appears to be the most common case, at least
-on the channels I visit) to ``UTF-8``. If that conversion fails at some
-point, a placeholder character ``'�'`` is inserted to indicate this
-decoding error.
-
-Messages are always sent in UTF-8 over IRC, no conversion is done in that
-direction.
-
-IRC modes
----------
-
-One feature that doesn’t exist on XMPP but does on IRC is the ``modes``.
-Although some of these modes have a correspondance in the XMPP world (for
-example the ``+o`` mode on a user corresponds to the ``moderator`` role in
-XMPP), it is impossible to map all these modes to an XMPP feature. To
-circumvent this problem, biboumi provides a raw notification when modes are
-changed, and lets the user change the modes directly.
-
-To change modes, simply send a message starting with “``/mode``” followed by
-the modes and the arguments you want to send to the IRC server. For example
-“/mode +aho louiz”. Note that your XMPP client may interprete messages
-begining with “/” like a command. To actually send a message starting with
-a slash, you may need to start your message with “//mode” or “/say /mode”,
-depending on your client.
-
-When a mode is changed, the user is notified by a message coming from the
-MUC bare JID, looking like “Mode #foo [+ov] [toto tutu]”. In addition, if
-the mode change can be translated to an XMPP feature, the user will be
-notified of this XMPP event as well. For example if a mode “+o toto” is
-received, then toto’s role will be changed to moderator. The mapping
-between IRC modes and XMPP features is as follow:
-
-``+q``
- Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
-
-``+a``
- Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
-
-``+o``
- Sets the participant’s role to ``moderator`` and its affiliation to ``admin``.
-
-``+h``
- Sets the participant’s role to ``moderator`` and its affiliation to ``member``.
-
-``+v``
- Sets the participant’s role to ``participant`` and its affiliation to ``member``.
-
-Similarly, when a biboumi user changes some participant's affiliation or role, biboumi translates that in an IRC mode change.
-
-Affiliation set to ``none``
- Sets mode to -vhoaq
-
-Affiliation set to ``member``
- Sets mode to +v-hoaq
-
-Role set to ``moderator``
- Sets mode to +h-oaq
-
-Affiliation set to ``admin``
- Sets mode to +o-aq
-
-Affiliation set to ``owner``
- Sets mode to +a-q
-
-Ad-hoc commands
----------------
-
-Biboumi supports a few ad-hoc commands, as described in the XEP 0050.
-Different ad-hoc commands are available for each JID type.
-
-On the gateway itself (e.g on the JID biboumi.example.com):
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- ping: Just respond “pong”
-
-- hello: Provide a form, where the user enters their name, and biboumi
- responds with a nice greeting.
-
-- disconnect-user: Only available to the administrator. The user provides
- a list of JIDs, and a quit message. All the selected users are
- disconnected from all the IRC servers to which they were connected,
- using the provided quit message. Sending SIGINT to biboumi is equivalent
- to using this command by selecting all the connected JIDs and using the
- “Gateway shutdown” quit message, except that biboumi does not exit when
- using this ad-hoc command.
-
-- disconnect-from-irc-servers: Disconnect a single user from one or more
- IRC server. The user is immediately disconnected by closing the socket,
- no message is sent to the IRC server, but the user is of course notified
- with an XMPP message. The administrator can disconnect any user, while
- the other users can only disconnect themselves.
-
-- configure: Lets each user configure some options that applies globally.
- The provided configuration form contains these fields:
- * Record History: whether or not history messages should be saved in
- the database.
- * Max history length: The maximum number of lines in the history
- that the server is allowed to send when joining a channel.
-
- * Persistent: Overrides the value specified in each individual channel.
- If this option is set to true, all channels are persistent, whether
- or not their specific value is true or false. This option is true by
- default for everyone if the `persistent_by_default` configuration
- option is true, otherwise it’s false. See below for more details on
- what a persistent channel is. This value is
-
-On a server JID (e.g on the JID chat.freenode.org@biboumi.example.com)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- configure: Lets each user configure some options that applies to the
- concerned IRC server. The provided configuration form contains these
- fields:
- * Realname: The customized “real name” as it will appear on the
- user’s whois. This option is not available if biboumi is configured
- with realname_customization to false.
- * Username: The “user” part in your `user@host`. This option is not
- available if biboumi is configured with realname_customization to
- false.
- * In encoding: The incoming encoding. Any received message that is not
- proper UTF-8 will be converted will be converted from the configured
- In encoding into UTF-8. If the conversion fails at some point, some
- characters will be replaced by the placeholders.
- * Out encoding: Currently ignored.
- * After-connection IRC command: A raw IRC command that will be sent to
- the server immediately after the connection has been successful. It
- can for example be used to identify yourself using NickServ, with a
- command like this: `PRIVMSG NickServ :identify PASSWORD`.
- * Ports: The list of TCP ports to use when connecting to this IRC server.
- This list will be tried in sequence, until the connection succeeds for
- one of them. The connection made on these ports will not use TLS, the
- communication will be insecure. The default list contains 6697 and 6670.
- * TLS ports: A second list of ports to try when connecting to the IRC
- server. The only difference is that TLS will be used if the connection
- is established on one of these ports. All the ports in this list will
- be tried before using the other plain-text ports list. To entirely
- disable any non-TLS connection, just remove all the values from the
- “normal” ports list. The default list contains 6697.
- * Verify certificate: If set to true (the default value), when connecting
- on a TLS port, the connection will be aborted if the certificate is
- not valid (for example if it’s not signed by a known authority, or if
- the domain name doesn’t match, etc). Set it to false if you want to
- connect on a server with a self-signed certificate.
- * SHA-1 fingerprint of the TLS certificate to trust: if you know the hash
- of the certificate that the server is supposed to use, and you only want
- to accept this one, set its SHA-1 hash in this field.
- * Server password: A password that will be sent just after the connection,
- in a PASS command. This is usually used in private servers, where you’re
- only allowed to connect if you have the password. Note that, although
- this is NOT a password that will be sent to NickServ (or some author
- authentication service), some server (notably Freenode) use it as if it
- was sent to NickServ to identify your nickname.
-
-- get-irc-connection-info: Returns some information about the IRC server,
- for the executing user. It lets the user know if they are connected to
- this server, from what port, with or without TLS, and it gives the list
- of joined IRC channel, with a detailed list of which resource is in which
- channel.
-
-On a channel JID (e.g on the JID #test%chat.freenode.org@biboumi.example.com)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-- configure: Lets each user configure some options that applies to the
- concerned IRC channel. Some of these options, if not configured for a
- specific channel, defaults to the value configured at the IRC server
- level. For example the encoding can be specified for both the channel
- and the server. If an encoding is not specified for a channel, the
- encoding configured in the server applies. The provided configuration
- form contains these fields:
- * In encoding: see the option with the same name in the server configuration
- form.
- * Out encoding: Currently ignored.
- * Persistent: If set to true, biboumi will stay in this channel even when
- all the XMPP resources have left the room. I.e. it will not send a PART
- command, and will stay idle in the channel until the connection is
- forcibly closed. If a resource comes back in the room again, and if
- the archiving of messages is enabled for this room, the client will
- receive the messages that where sent in this channel. This option can be
- used to make biboumi act as an IRC bouncer.
- * Record History: whether or not history messages should be saved in
- the database, for this specific channel. If the value is “unset” (the
- default), then the value configured globally is used. This option is there,
- for example, to be able to enable history recording globally while disabling
- it for a few specific “private” channels.
-
-Raw IRC messages
-----------------
-
-Biboumi tries to support as many IRC features as possible, but doesn’t
-handle everything yet (or ever). In order to let the user send any
-arbitrary IRC message, biboumi forwards any XMPP message received on an IRC
-Server JID (see `Addressing`_) as a raw command to that IRC server.
-
-For example, to WHOIS the user Foo on the server irc.example.com, a user can
-send the message “WHOIS Foo” to ``irc.example.com@biboumi.example.com``.
-
-The message will be forwarded as is, without any modification appart from
-adding ``\r\n`` at the end (to make it a valid IRC message). You need to
-have a little bit of understanding of the IRC protocol to use this feature.
-
-Security
-========
-
-The connection to the XMPP server can only be made on localhost. The
-XMPP server is not supposed to accept non-local connections from components.
-Thus, encryption is not used to connect to the local XMPP server because it
-is useless.
-
-If compiled with the Botan library, biboumi can use TLS when communicating
-with the IRC servers. It will first try ports 6697 and 6670 and use TLS if
-it succeeds, if connection fails on both these ports, the connection is
-established on port 6667 without any encryption.
-
-Biboumi does not check if the received JIDs are properly formatted using
-nodeprep. This must be done by the XMPP server to which biboumi is directly
-connected.
-
-Note if you use a biboumi that you have no control on: remember that the
-administrator of the gateway you use is able to view all your IRC
-conversations, whether you’re using encryption or not. This is exactly as
-if you were running your IRC client on someone else’s server. Only use
-biboumi if you trust its administrator (or, better, if you are the
-administrator) or if you don’t intend to have any private conversation.
-
-Biboumi does not provide a way to ban users from connecting to it, has no
-protection against flood or any sort of abuse that your users may cause on
-the IRC servers. Some XMPP server however offer the possibility to restrict
-what JID can access a gateway. Use that feature if you wish to grant access
-to your biboumi instance only to a list of trusted users.
-
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..e3f0e02
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'biboumi'
+copyright = '2018, Florent Le Coz'
+author = 'Florent Le Coz'
+
+# The short X.Y version
+version = '8.3'
+# The full version, including alpha/beta/rc tags
+release = '8.3'
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.autosectionlabel',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path .
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself. Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'biboumidoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'biboumi.tex', 'biboumi Documentation',
+ 'Florent Le Coz', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'biboumi', 'biboumi Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'biboumi', 'biboumi Documentation',
+ author, 'biboumi', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+
+# -- Extension configuration -------------------------------------------------
diff --git a/CONTRIBUTING.rst b/doc/contributing.rst
index 8df4899..ee499fc 100644
--- a/CONTRIBUTING.rst
+++ b/doc/contributing.rst
@@ -1,5 +1,6 @@
+#######################
Contributing to biboumi
-=======================
+#######################
Biboumi’s main workplace is at https://lab.louiz.org/louiz/biboumi
@@ -20,8 +21,12 @@ If the bug you’re reporting is about a bad behaviour of biboumi when some XMPP
or IRC events occur, please try to reproduce the issue with a biboumi running
in log_level=0, and include the relevant logs in your bug report.
-If the issue you’re reporting may have security implications, please select
-the “confidential” flag in your bug report.
+If the issue you’re reporting may have security implications, please
+select the “confidential” flag in your bug report. This includes, but is not limited to:
+
+- disclosure of private data that was supposed to be encrypted using TLS
+- denial of service (crash, infinite loop, etc) that can be caused by any
+ user
Code
@@ -57,18 +62,22 @@ There are two test suites for biboumi:
Once all the dependencies are correctly installed, the tests are run with
- `make e2e`
+.. code-block:: sh
+
+ make e2e
+
+To run one or more specific tests, you can do something like this:
- To run one or more specific tests, you can do something like this:
+.. code-block:: sh
- `make biboumi && python3 ../tests/end_to_end self_ping basic_handshake_success`
+ make biboumi && python3 ../tests/end_to_end self_ping basic_handshake_success
- This will run two tests, self_ping and basic_handshake_success.
+This will run two tests, self_ping and basic_handshake_success.
- To write additional tests, you need to add a Scenario
- into `the __main__.py file`_. If you have problem running this end-to-end
- test suite, or if you struggle with this weird code (that would be
- completely normal…), don’t hesitate to ask for help.
+To write additional tests, you need to add a Scenario
+into `the __main__.py file`_. If you have problem running this end-to-end
+test suite, or if you struggle with this weird code (that would be
+completely normal…), don’t hesitate to ask for help.
All these tests automatically run with various configurations, on various
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..b97c8fd
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,23 @@
+.. biboumi documentation master file, created by
+ sphinx-quickstart on Mon Aug 27 19:50:26 2018.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Biboumi – XMPP gateway to IRC
+=============================
+
+Homepage: https://biboumi.louiz.org
+
+Forge: https://lab.louiz.org/louiz/biboumi
+
+Biboumi is an XMPP gateway that connects to IRC servers and translates
+between the two protocols. It can be used to access IRC channels using any
+XMPP client as if these channels were XMPP MUCs.
+
+.. toctree::
+ :maxdepth: 2
+
+ install
+ admin
+ user
+ contributing
diff --git a/INSTALL.rst b/doc/install.rst
index 45a860d..685511d 100644
--- a/INSTALL.rst
+++ b/doc/install.rst
@@ -1,24 +1,28 @@
-INSTALL
-=======
+Installation
+============
-tl;dr
------
+The very short version:
+
+.. code-block:: sh
- cmake . && make && ./biboumi
+ cmake . && make && ./biboumi
If that didn’t work, read on.
Dependencies
------------
-Build and runtime dependencies:
+Here’s the list of all the build and runtime dependencies. Because we
+strive to use the smallest number of dependencies possible, many of them
+are optional. That being said, you will have the best experience using
+biboumi by having all dependencies.
Tools:
~~~~~~
- A C++14 compiler (clang >= 3.4 or gcc >= 5.0 for example)
- CMake
-- pandoc (optional) to build the man page
+- sphinx (optional) to build the documentation
Libraries:
~~~~~~~~~~
@@ -59,28 +63,61 @@ systemd_ (optional)
Provides the support for a systemd service of Type=notify. This is useful only
if you are packaging biboumi in a distribution with Systemd.
-
-Configure
+Customize
---------
-Configure the build system using cmake, there are many solutions to do that,
-the simplest is to just run
+The basics
+~~~~~~~~~~
+
+Once you have all the dependencies you need, configure the build system
+using cmake. The cleanest way is to create a build directory, and run
+everything inside it:
+
+
+.. code-block:: sh
+
+ mkdir build/ && cd build/ && cmake ..
- cmake .
+Choosing the dependencies
+~~~~~~~~~~~~~~~~~~~~~~~~~
-in the current directory.
+Without any option, cmake will look for all dependencies available on the
+system and use everything it finds. If a mandatory dependency is missing
+it will obviously stop and yield an error, however if an optional
+dependency is missing, it will just ignore it.
+
+To specify that you want or don’t want to use, you need to
+pass an option like this:
+
+.. code-block:: sh
+
+ cmake .. -DWITH_XXXX=1 -DWITHOUT_XXXX=1
+
+The `WITH_` prefix indicates that cmake should stop if that dependency can
+not be found, and the `WITHOUT_` prefix indicates that this dependency
+should not be used even if it is present on the system.
+
+The `XXXX` part needs to be replaced by one of the following: BOTAN,
+LIBIDN, SYSTEMD, DOC, UDNS, SQLITE3, POSTGRESQL.
+
+Other options
+~~~~~~~~~~~~~
The default build type is "Debug", if you want to build a release version,
set the CMAKE_BUILD_TYPE variable to "release", by running this command
instead:
- cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr
+.. code-block:: sh
+
+ cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
You can also configure many parameters of the build (like customize CFLAGS,
the install path, choose the compiler, or enabling some options like the
POLLER to use), using the ncurses interface of ccmake:
- ccmake .
+.. code-block:: sh
+
+ ccmake ..
In ccmake, first use 'c' to configure the build system, edit the values you
need and finaly use 'g' to generate the Makefiles to build the system and
@@ -88,7 +125,8 @@ quit ccmake.
You can also configure these options using a -D command line flag.
-The list of available options:
+Biboumi also has a few advanced options that are useful only in very
+specific cases.
- POLLER: lets you select the poller used by biboumi, at
compile-time. Possible values are:
@@ -97,25 +135,16 @@ The list of available options:
- POLL: use the standard poll(2). This is the default value on all non-Linux
platforms.
-- DEBUG_SQL_QUERIES: If set to ON, additional debug logging and timing will be
- done for every SQL query that is executed. The default is OFF.
-
-- WITH_BOTAN and WITHOUT_BOTAN: The first force the usage of the Botan library,
- if it is not found, the configuration process will fail. The second will
- make the build process ignore the Botan library, it will not be used even
- if it's available on the system. If none of these option is specified, the
- library will be used if available and will be ignored otherwise.
-
-- WITH_LIBIDN and WITHOUT_LIBIDN: Just like the WITH(OUT)_BOTAN options, but
- for the IDN library
-
-- WITH_SYSTEMD and WITHOUT_SYSTEMD: Just like the other WITH(OUT)_* options,
- but for the Systemd library
+- DEBUG_SQL_QUERIES: If set to ON, additional debug logging and timing
+ will be done for every SQL query that is executed. The default is OFF.
+ Please set it to ON if you intend to share your debug logs on the bug
+ trackers, if your issue affects the database.
Example:
- cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr
- -DWITH_BOTAN=1 -DWITHOUT_SYSTEMD=1
+.. code-block:: sh
+
+ cmake . -DCMAKE_BUILD_TYPE=release -DCMAKE_INSTALL_PREFIX=/usr -DWITH_BOTAN=1 -DWITHOUT_SYSTEMD=1
This command will configure the project to build a release, with TLS enabled
(using Botan) but without using Systemd (even if available on the system).
@@ -123,41 +152,34 @@ This command will configure the project to build a release, with TLS enabled
Build
-----
+
Once you’ve configured everything using cmake, build the software:
-To build the biboumi binary:
+To build the biboumi binary, run:
+
+.. code-block:: sh
make
Install
-------
-And then, optionaly, Install the software system-wide
-
- make install
-
-
-Testing
--------
-You can run the test suite with
- make check
-
-This project uses the Catch unit test framework, it will be automatically
-fetched with cmake, by cloning the github repository.
+And then, optionaly, Install the software system-wide
-You can also check the overall code coverage of this test suite by running
+.. code-block:: sh
- make coverage
+ make install
-This requires gcov and lcov to be installed.
+This will install the biboumi binary, but also the man-page (if configured
+with it), the policy files, the systemd unit file, etc.
Run
---
-Run the software using the `biboumi` binary. Read the documentation (the
-man page biboumi(1) or the `biboumi.1.rst`_ file) for more information on how
-to use biboumi.
+
+Finally, run the software using the `biboumi` binary. Read the documentation (the
+man page biboumi(1)) or the usage page.
.. _expat: http://expat.sourceforge.net/
.. _libiconv: http://www.gnu.org/software/libiconv/
diff --git a/doc/user.rst b/doc/user.rst
new file mode 100644
index 0000000..505e3b9
--- /dev/null
+++ b/doc/user.rst
@@ -0,0 +1,513 @@
+######################
+End-user documentation
+######################
+
+Quick-start
+-----------
+
+When a user joins an IRC channel on an IRC server (see `Join an IRC
+channel`_), biboumi connects to the remote IRC server, sets the user’s nick
+as requested, and then tries to join the specified channel. If the same
+user subsequently tries to connect to an other channel on the same server,
+the same IRC connection is used. If, however, an other user wants to join
+an IRC channel on that same IRC server, biboumi opens a new connection to
+that server. Biboumi connects once to each IRC servner, for each user on it.
+
+Additionally, if one user is using more than one clients (with the same bare
+JID), they can join the same IRC channel (on the same server) behind one
+single nickname. Biboumi will forward all the messages (the channel ones and
+the private ones) and the presences to all the resources behind that nick.
+There is no need to have multiple nicknames and multiple connections to be
+able to take part in a conversation (or idle) in a channel from a mobile client
+while the desktop client is still connected, for example.
+
+.. note:: If you use a biboumi that you have no control on: remember that the
+ administrator of the gateway you use is able to view all your IRC
+ conversations, whether you’re using encryption or not. This is exactly as
+ if you were running your IRC client on someone else’s server. Only use
+ biboumi if you trust its administrator (or, better, if you are the
+ administrator) or if you don’t intend to have any private conversation.
+
+Addressing
+----------
+
+IRC entities are represented by XMPP JIDs. The domain part of the JID is
+the domain served by biboumi (the part after the `@`, biboumi.example.com in
+the examples), and the local part (the part before the `@`) depends on the
+concerned entity.
+
+IRC channels and IRC users have a local part formed like this:
+``name`` % ``irc_server``.
+
+``name`` can be a channel name or an user nickname. The distinction between
+the two is based on the first character: by default, if the name starts with
+``'#'`` or ``'&'`` (but this can be overridden by the server, using the
+ISUPPORT extension) then it’s a channel name, otherwise this is a nickname.
+
+There is two ways to address an IRC user, using a local part like this:
+``nickname`` % ``irc_server`` or by using the in-room address of the
+participant, like this:
+``channel_name`` % ``irc_server`` @ ``biboumi.example.com`` / ``Nickname``
+
+The second JID is available only to be compatible with XMPP clients when the
+user wants to send a private message to the participant ``Nickname`` in the
+room ``channel_name%irc_server@biboumi.example.com``.
+
+On XMPP, the node part of the JID can only be lowercase. On the other hand,
+IRC nicknames are case-insensitive, this means that the nicknames toto,
+Toto, tOtO and TOTO all represent the same IRC user. This means you can
+talk to the user toto, and this will work.
+
+Also note that some IRC nicknames or channels may contain characters that
+are not allowed in the local part of a JID (for example '@'). If you need
+to send a message to a nick containing such a character, you can use a jid
+like ``%irc.example.com@biboumi.example.com/AnnoyingNickn@me``, because
+the JID ``AnnoyingNickn@me%irc.example.com@biboumi.example.com`` would not
+work. This “weird” JID is just using the fact that you can send a private
+message through any room (even a room with an empty name) because, on IRC,
+a query does not go through any room at all, it’s just server-wide. So,
+sending a message to #doesnotexist%irc@biboumi/User is exactly the same as
+sending one to %irc@biboumi/User.
+
+And if you need to address a channel that contains such invalid characters, you
+have to use `jid-escaping <http://www.xmpp.org/extensions/xep-0106.html#escaping>`_,
+and replace each of these characters with their escaped version, for example to
+join the channel ``#b@byfoot``, you need to use the following JID:
+``#b\40byfoot%irc.example.com@biboumi.example.com``.
+
+
+Examples:
+
+* ``#foo%irc.example.com@biboumi.example.com`` is the #foo IRC channel, on the
+ irc.example.com IRC server, and this is served by the biboumi instance on
+ biboumi.example.com
+
+* ``toto%irc.example.com@biboumi.example.com`` is the IRC user named toto, or
+ TotO, etc.
+
+* ``irc.example.com@biboumi.example.com`` is the IRC server irc.example.com.
+
+Note: Some JIDs are valid but make no sense in the context of
+biboumi:
+
+* ``#test%@biboumi.example.com``, or any other JID that does not contain an
+ IRC server is invalid. Any message to that kind of JID will trigger an
+ error, or will be ignored.
+
+If compiled with Libidn, an IRC channel participant has a bare JID
+representing the “hostname” provided by the IRC server. This JID can only
+be used to set IRC modes (for example to ban a user based on its IP), or to
+identify user. It cannot be used to contact that user using biboumi.
+
+Join an IRC channel
+-------------------
+
+To join an IRC channel ``#foo`` on the IRC server ``irc.example.com``,
+join the XMPP MUC ``#foo%irc.example.com@biboumi.example.com``.
+
+Connect to an IRC server
+------------------------
+
+The connection to the IRC server is automatically made when the user tries
+to join any channel on that IRC server. The connection is closed whenever
+the last channel on that server is left by the user.
+
+Roster
+------
+
+You can add some JIDs provided by biboumi into your own roster, to receive
+presence from them. Biboumi will always automatically accept your requests.
+
+Biboumi’s JID
+~~~~~~~~~~~~~
+
+By adding the component JID into your roster, the user will receive an available
+presence whenever it is started, and an unavailable presence whenever it is being
+shutdown. This is useful to quickly view if that biboumi instance is started or
+not.
+
+IRC server JID
+~~~~~~~~~~~~~~
+
+These presence will appear online in the user’s roster whenever they are
+connected to that IRC server (see `Connect to an IRC server`_ for more
+details). This is useful to keep track of which server an user is connected
+to: this is sometimes hard to remember, when they have many clients, or if
+they are using persistent channels.
+
+Channel messages
+----------------
+
+On XMPP, unlike on IRC, the displayed order of the messages is the same for
+all participants of a MUC. Biboumi can not however provide this feature, as
+it cannot know whether the IRC server has received and forwarded the
+messages to other users. This means that the order of the messages
+displayed in your XMPP client may not be the same as the order on other
+IRC users’.
+
+History
+-------
+
+Public channel messages are saved into archives, inside the database,
+unless the `record_history` option is set to false by that user (see
+`Ad-hoc commands`_). Private messages (messages that are sent directly to
+a nickname, not a channel) are never stored in the database.
+
+A channel history can be retrieved by using `Message archive management
+(MAM) <https://xmpp.org/extensions/xep-0313.htm>`_ on the channel JID.
+The results can be filtered by start and end dates.
+
+When a channel is joined, if the client doesn’t specify any limit, biboumi
+sends the `max_history_length` last messages found in the database as the
+MUC history. If a client wants to only use MAM for the archives (because
+it’s more convenient and powerful), it should request to receive no
+history by using an attribute maxchars='0' or maxstanzas='0' as defined in
+XEP 0045, and do a proper MAM request instead.
+
+Note: the maxchars attribute is ignored unless its value is exactly 0.
+Supporting it properly would be very hard and would introduce a lot of
+complexity for almost no benefit.
+
+For a given channel, each user has her or his own archive. The content of
+the archives are never shared, and thus a user can not use someone else’s
+archive to get the messages that they didn’t receive when they were
+offline. Although this feature would be very convenient, this would
+introduce a very important privacy issue: for example if a biboumi gateway
+is used by two users, by querying the archive one user would be able to
+know whether or not the other user was in a room at a given time.
+
+
+List channels
+-------------
+
+You can list the IRC channels on a given IRC server by sending an XMPP
+disco items request on the IRC server JID. The number of channels on some
+servers is huge so the result stanza may be very big, unless your client
+supports result set management (XEP 0059)
+
+Nicknames
+---------
+
+On IRC, nicknames are server-wide. This means that one user only has one
+single nickname at one given time on all the channels of a server. This is
+different from XMPP where a user can have a different nick on each MUC,
+even if these MUCs are on the same server.
+
+This means that the nick you choose when joining your first IRC channel on
+a given IRC server will be your nickname in all other channels that you
+join on that same IRC server.
+
+If you explicitely change your nickname on one channel, your nickname will
+be changed on all channels on the same server as well. Joining a new
+channel with a different nick, however, will not change your nick. The
+provided nick will be ignored, in order to avoid changing your nick on the
+whole server by mistake. If you want to have a different nickname in the
+channel you’re going to join, you need to do it explicitly with the NICK
+command before joining the channel.
+
+Private messages
+----------------
+
+Private messages are handled differently on IRC and on XMPP. On IRC, you
+talk directly to one server-user: toto on the channel #foo is the same user
+as toto on the channel #bar (as long as these two channels are on the same
+IRC server). By default you will receive private messages from the “global”
+user (aka nickname%irc.example.com@biboumi.example.com), unless you
+previously sent a message to an in-room participant (something like
+\#test%irc.example.com@biboumi.example.com/nickname), in which case future
+messages from that same user will be received from that same “in-room” JID.
+
+Notices
+-------
+
+Notices are received exactly like private messages. It is not possible to
+send a notice.
+
+Topic
+-----
+
+The topic can be set and retrieved seemlessly. The unique difference is that
+if an XMPP user tries to set a multiline topic, every line return (\\n) will
+be replaced by a space, because the IRC server wouldn’t accept it.
+
+Invitations
+-----------
+
+If the invited JID is a user JID served by this biboumi instance, it will forward the
+invitation to the target nick, over IRC.
+Otherwise, the mediated instance will directly be sent to the invited JID, over XMPP.
+
+Example: if the user wishes to invite the IRC user “FooBar” into a room, they can
+invite one of the following “JIDs” (one of them is not a JID, actually):
+
+- foobar%anything@biboumi.example.com
+- anything@biboumi.example.com/FooBar
+- FooBar
+
+(Note that the “anything” parts are simply ignored because they carry no
+additional meaning for biboumi: we already know which IRC server is targeted
+using the JID of the target channel.)
+
+Otherwise, any valid JID can be used, to invite any XMPP user.
+
+Kicks and bans
+--------------
+
+Kicks are transparently translated from one protocol to another. However
+banning an XMPP participant has no effect. To ban an user you need to set a
+mode +b on that user nick or host (see `IRC modes`_) and then kick it.
+
+Encoding
+--------
+
+On XMPP, the encoding is always ``UTF-8``, whereas on IRC the encoding of
+each message can be anything.
+
+This means that biboumi has to convert everything coming from IRC into UTF-8
+without knowing the encoding of the received messages. To do so, it checks
+if each message is UTF-8 valid, if not it tries to convert from
+``iso_8859-1`` (because this appears to be the most common case, at least
+on the channels I visit) to ``UTF-8``. If that conversion fails at some
+point, a placeholder character ``'�'`` is inserted to indicate this
+decoding error.
+
+Messages are always sent in UTF-8 over IRC, no conversion is done in that
+direction.
+
+IRC modes
+---------
+
+One feature that doesn’t exist on XMPP but does on IRC is the ``modes``.
+Although some of these modes have a correspondance in the XMPP world (for
+example the ``+o`` mode on a user corresponds to the ``moderator`` role in
+XMPP), it is impossible to map all these modes to an XMPP feature. To
+circumvent this problem, biboumi provides a raw notification when modes are
+changed, and lets the user change the modes directly.
+
+To change modes, simply send a message starting with “``/mode``” followed by
+the modes and the arguments you want to send to the IRC server. For example
+“/mode +aho louiz”. Note that your XMPP client may interprete messages
+begining with “/” like a command. To actually send a message starting with
+a slash, you may need to start your message with “//mode” or “/say /mode”,
+depending on your client.
+
+When a mode is changed, the user is notified by a message coming from the
+MUC bare JID, looking like “Mode #foo [+ov] [toto tutu]”. In addition, if
+the mode change can be translated to an XMPP feature, the user will be
+notified of this XMPP event as well. For example if a mode “+o toto” is
+received, then toto’s role will be changed to moderator. The mapping
+between IRC modes and XMPP features is as follow:
+
+``+q``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
+
+``+a``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``owner``.
+
+``+o``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``admin``.
+
+``+h``
+ Sets the participant’s role to ``moderator`` and its affiliation to ``member``.
+
+``+v``
+ Sets the participant’s role to ``participant`` and its affiliation to ``member``.
+
+Similarly, when a biboumi user changes some participant's affiliation or role, biboumi translates that in an IRC mode change.
+
+Affiliation set to ``none``
+ Sets mode to -vhoaq
+
+Affiliation set to ``member``
+ Sets mode to +v-hoaq
+
+Role set to ``moderator``
+ Sets mode to +h-oaq
+
+Affiliation set to ``admin``
+ Sets mode to +o-aq
+
+Affiliation set to ``owner``
+ Sets mode to +a-q
+
+Ad-hoc commands
+---------------
+
+Biboumi supports a few ad-hoc commands, as described in the XEP 0050.
+Different ad-hoc commands are available for each JID type.
+
+On the gateway itself
+~~~~~~~~~~~~~~~~~~~~~
+
+.. note:: For example on the JID biboumi.example.com
+
+ping
+^^^^
+Just respond “pong”
+
+hello
+^^^^^
+
+Provide a form, where the user enters their name, and biboumi responds
+with a nice greeting.
+
+disconnect-user
+^^^^^^^^^^^^^^^
+
+Only available to the administrator. The user provides a list of JIDs, and
+a quit message. All the selected users are disconnected from all the IRC
+servers to which they were connected, using the provided quit message.
+
+disconnect-from-irc-servers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Disconnect a single user from one or more IRC server. The user is
+immediately disconnected by closing the socket, no message is sent to the
+IRC server, but the user is of course notified with an XMPP message. The
+administrator can disconnect any user, while the other users can only
+disconnect themselves.
+
+configure
+^^^^^^^^^
+
+Lets each user configure some options that apply globally.
+The provided configuration form contains these fields:
+
+- **Record History**: whether or not history messages should be saved in
+ the database.
+- **Max history length**: The maximum number of lines in the history that
+ the server is allowed to send when joining a channel.
+- **Persistent**: Overrides the value specified in each individual
+ channel. If this option is set to true, all channels are persistent,
+ whether or not their specific value is true or false. This option is true
+ by default for everyone if the `persistent_by_default` configuration
+ option is true, otherwise it’s false. See below for more details on what a
+ persistent channel is.
+
+On a server JID
+~~~~~~~~~~~~~~~
+
+.. note:: For example on the JID chat.freenode.org@biboumi.example.com
+
+configure
+^^^^^^^^^
+
+Lets each user configure some options that applies to the concerned IRC
+server. The provided configuration form contains these fields:
+
+- **Address**: This address (IPv4, IPv6 or hostname) will be used, when
+ biboumi connects to this server. This is a very handy way to have a
+ custom name for a network, and be able to edit the address to use
+ if one endpoint for that server is dead, but continue using the same
+ JID. For example, a user could configure the server
+ “freenode@biboumi.example.com”, set “chat.freenode.net” in its
+ “Address” field, and then they would be able to use “freenode” as
+ the network name forever: if “chat.freenode.net” breaks for some
+ reason, it can be changed to “irc.freenode.org” instead, and the user
+ would not need to change all their bookmarks and settings.
+- **Realname**: The customized “real name” as it will appear on the
+ user’s whois. This option is not available if biboumi is configured
+ with realname_customization to false.
+- **Username**: The “user” part in your `user@host`. This option is not
+ available if biboumi is configured with realname_customization to
+ false.
+- **In encoding**: The incoming encoding. Any received message that is not
+ proper UTF-8 will be converted from the configured In encoding into UTF-8.
+ If the conversion fails at some point, some characters will be replaced by
+ the placeholders.
+- **Out encoding**: Currently ignored.
+- **After-connection IRC commands**: Raw IRC commands that will be sent
+ one by one to the server immediately after the connection has been
+ successful. It can for example be used to identify yourself using
+ NickServ, with a command like this: `PRIVMSG NickServ :identify
+ PASSWORD`.
+- **Ports**: The list of TCP ports to use when connecting to this IRC server.
+ This list will be tried in sequence, until the connection succeeds for
+ one of them. The connection made on these ports will not use TLS, the
+ communication will be insecure. The default list contains 6697 and 6670.
+- **TLS ports**: A second list of ports to try when connecting to the IRC
+ server. The only difference is that TLS will be used if the connection
+ is established on one of these ports. All the ports in this list will
+ be tried before using the other plain-text ports list. To entirely
+ disable any non-TLS connection, just remove all the values from the
+ “normal” ports list. The default list contains 6697.
+- **Verify certificate**: If set to true (the default value), when connecting
+ on a TLS port, the connection will be aborted if the certificate is
+ not valid (for example if it’s not signed by a known authority, or if
+ the domain name doesn’t match, etc). Set it to false if you want to
+ connect on a server with a self-signed certificate.
+- **SHA-1 fingerprint of the TLS certificate to trust**: if you know the hash
+ of the certificate that the server is supposed to use, and you only want
+ to accept this one, set its SHA-1 hash in this field.
+- **Nickname**: A nickname that will be used instead of the nickname provided
+ in the initial presence sent to join a channel. This can be used if the
+ user always wants to have the same nickname on a given server, and not
+ have to bother with setting that nick in all the bookmarks on that
+ server. The nickname can still manually be changed with a standard nick
+ change presence.
+- **Server password**: A password that will be sent just after the connection,
+ in a PASS command. This is usually used in private servers, where you’re
+ only allowed to connect if you have the password. Note that, although
+ this is NOT a password that will be sent to NickServ (or some author
+ authentication service), some server (notably Freenode) use it as if it
+ was sent to NickServ to identify your nickname.
+- **Throttle limit**: specifies a number of messages that can be sent
+ without a limit, before the throttling takes place. When messages
+ are throttled, only one command per second is sent to the server.
+ The default is 10. You can lower this value if you are ever kicked
+ for excess flood. If the value is 0, all messages are throttled. To
+ disable this feature, set it to a negative number, or an empty string.
+
+get-irc-connection-info
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Returns some information about the IRC server, for the executing user. It
+lets the user know if they are connected to this server, from what port,
+with or without TLS, and it gives the list of joined IRC channel, with a
+detailed list of which resource is in which channel.
+
+On a channel JID
+~~~~~~~~~~~~~~~~
+
+.. note:: For example on the JID #test%chat.freenode.org@biboumi.example.com
+
+configure
+^^^^^^^^^
+
+Lets each user configure some options that applies to the concerned IRC
+channel. Some of these options, if not configured for a specific channel,
+defaults to the value configured at the IRC server level. For example the
+encoding can be specified for both the channel and the server. If an
+encoding is not specified for a channel, the encoding configured in the
+server applies. The provided configuration form contains these fields:
+
+- **In encoding**: see the option with the same name in the server configuration
+ form.
+- **Out encoding**: Currently ignored.
+- **Persistent**: If set to true, biboumi will stay in this channel even when
+ all the XMPP resources have left the room. I.e. it will not send a PART
+ command, and will stay idle in the channel until the connection is
+ forcibly closed. If a resource comes back in the room again, and if
+ the archiving of messages is enabled for this room, the client will
+ receive the messages that where sent in this channel. This option can be
+ used to make biboumi act as an IRC bouncer.
+- **Record History**: whether or not history messages should be saved in
+ the database, for this specific channel. If the value is “unset” (the
+ default), then the value configured globally is used. This option is there,
+ for example, to be able to enable history recording globally while disabling
+ it for a few specific “private” channels.
+
+Raw IRC messages
+----------------
+
+Biboumi tries to support as many IRC features as possible, but doesn’t
+handle everything yet (or ever). In order to let the user send any
+arbitrary IRC message, biboumi forwards any XMPP message received on an IRC
+Server JID (see `Addressing`_) as a raw command to that IRC server.
+
+For example, to WHOIS the user Foo on the server irc.example.com, a user can
+send the message “WHOIS Foo” to ``irc.example.com@biboumi.example.com``.
+
+The message will be forwarded as is, without any modification appart from
+adding ``\r\n`` at the end (to make it a valid IRC message). You need to
+have a little bit of understanding of the IRC protocol to use this feature.
diff --git a/docker/biboumi-test/fedora/Dockerfile b/docker/biboumi-test/fedora/Dockerfile
index 12e13e5..cb0be8c 100644
--- a/docker/biboumi-test/fedora/Dockerfile
+++ b/docker/biboumi-test/fedora/Dockerfile
@@ -40,11 +40,9 @@ RUN dnf --refresh install -y\
which\
java-1.8.0-openjdk\
postgresql-devel\
+ botan2-devel\
&& dnf clean all
-# Install botan
-RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && ldconfig && rm -rf /botan
-
# Install slixmpp, for e2e tests
RUN git clone git://git.louiz.org/slixmpp && pip3 install pyasn1 && cd slixmpp && python3 setup.py build && python3 setup.py install
diff --git a/docker/biboumi/alpine/Dockerfile b/docker/biboumi/alpine/Dockerfile
index 0b59eb7..89c7223 100644
--- a/docker/biboumi/alpine/Dockerfile
+++ b/docker/biboumi/alpine/Dockerfile
@@ -5,44 +5,52 @@
# This is the prefered way to build the release image, used by the
# end users, in production.
+FROM docker.io/alpine:latest as builder
+
+RUN apk add --no-cache --virtual .build cmake expat-dev g++ git libidn-dev \
+ make postgresql-dev python2 sqlite-dev udns-dev util-linux-dev
+
+RUN git clone https://github.com/randombit/botan.git && \
+ cd botan && \
+ ./configure.py --prefix=/usr && \
+ make -j8 && \
+ make install
+
+RUN git clone git://git.louiz.org/biboumi && \
+ mkdir ./biboumi/build && \
+ cd ./biboumi/build && \
+ cmake .. -DCMAKE_INSTALL_PREFIX=/usr \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DWITH_BOTAN=1 \
+ -DWITH_SQLITE3=1 \
+ -DWITH_LIBIDN=1 \
+ -DWITH_POSTGRESQL=1 && \
+ make -j8 && \
+ make install
+
+# ---
+
FROM docker.io/alpine:latest
-RUN apk add --no-cache\
- g++\
- cmake\
- make\
- udns-dev\
- sqlite-dev\
- postgresql-dev\
- libuuid\
- util-linux-dev\
- expat-dev\
- libidn-dev\
- git\
- python2
-
-# Install botan
-RUN git clone https://github.com/randombit/botan.git && cd botan && ./configure.py --prefix=/usr && make -j8 && make install && rm -rf /botan
-
-# Install biboumi
-RUN git clone git://git.louiz.org/biboumi && mkdir ./biboumi/build && cd ./biboumi/build &&\
- cmake .. -DCMAKE_INSTALL_PREFIX=/usr\
- -DCMAKE_BUILD_TYPE=Release\
- -DWITH_BOTAN=1\
- -DWITH_SQLITE3=1\
- -DWITH_LIBIDN=1\
- -DWITH_POSTGRESQL=1\
- && make -j8 && make install && rm -rf /biboumi
-
-RUN adduser biboumi -D -h /home/biboumi
-
-RUN mkdir /var/lib/biboumi
-RUN chown -R biboumi:biboumi /var/lib/biboumi
+RUN apk add --no-cache libidn libpq libstdc++ libuuid postgresql-libs \
+ sqlite-libs udns expat ca-certificates
+
+COPY --from=builder /usr/bin/botan /usr/bin/botan
+COPY --from=builder /usr/lib/libbotan* /usr/lib/
+COPY --from=builder /usr/lib/pkgconfig/botan-2.pc /usr/lib/pkgconfig/botan-2.pc
+
+COPY --from=builder /etc/biboumi /etc/biboumi
+COPY --from=builder /usr/bin/biboumi /usr/bin/biboumi
COPY ./biboumi.cfg /etc/biboumi/biboumi.cfg
-RUN chown -R biboumi:biboumi /etc/biboumi
+
+RUN adduser biboumi -D -h /home/biboumi && \
+ mkdir /var/lib/biboumi && \
+ chown -R biboumi:biboumi /var/lib/biboumi && \
+ chown -R biboumi:biboumi /etc/biboumi
WORKDIR /home/biboumi
USER biboumi
CMD ["/usr/bin/biboumi", "/etc/biboumi/biboumi.cfg"]
+
diff --git a/packaging/biboumi.spec.cmake b/packaging/biboumi.spec.cmake
index d07ed74..c841cdc 100644
--- a/packaging/biboumi.spec.cmake
+++ b/packaging/biboumi.spec.cmake
@@ -13,6 +13,7 @@ BuildRequires: libuuid-devel
BuildRequires: systemd-devel
BuildRequires: sqlite-devel
BuildRequires: postgresql-devel
+BuildRequires: botan2-devel
BuildRequires: cmake
BuildRequires: systemd
BuildRequires: pandoc
@@ -37,7 +38,7 @@ cmake . -DCMAKE_CXX_FLAGS="%{optflags}" \
-DCMAKE_BUILD_TYPE=release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DPOLLER=EPOLL \
- -DWITHOUT_BOTAN=1 \
+ -DWITH_BOTAN=1 \
-DWITH_SYSTEMD=1 \
-DWITH_LIBIDN=1 \
-DWITH_SQLITE3=1 \
@@ -57,7 +58,7 @@ make check %{?_smp_mflags}
%files
%{_bindir}/%{name}
%{_mandir}/man1/%{name}.1*
-%doc README.rst COPYING doc/biboumi.1.rst
+%doc README.rst COPYING doc/*.rst
%{_unitdir}/%{name}.service
%config(noreplace) %{biboumi_confdir}/*policy.txt
@@ -65,6 +66,19 @@ make check %{?_smp_mflags}
%changelog
* ${RPM_DATE} Le Coz Florent <louiz@louiz.org> - ${RPM_VERSION}-1
- Build latest git revision
+- Build against botan2
+
+* Wed Jun 1 2018 Le Coz Florent <louiz@louiz.org> - 8.3-1
+ Update to version 8.3
+
+* Wed May 25 2018 Le Coz Florent <louiz@louiz.org> - 8.2-1
+ Update to version 8.2
+
+* Wed May 14 2018 Le Coz Florent <louiz@louiz.org> - 8.1-1
+ Update to version 8.1
+
+* Wed May 2 2018 Le Coz Florent <louiz@louiz.org> - 8.0-1
+ Update to version 8.0
* Wed Jan 24 2018 Le Coz Florent <louiz@louiz.org> - 7.2-1
Update to version 7.2
diff --git a/src/bridge/bridge.cpp b/src/bridge/bridge.cpp
index 54bee84..cc2ef66 100644
--- a/src/bridge/bridge.cpp
+++ b/src/bridge/bridge.cpp
@@ -5,6 +5,7 @@
#include <utils/empty_if_fixed_server.hpp>
#include <utils/encoding.hpp>
#include <utils/tolower.hpp>
+#include <utils/uuid.hpp>
#include <logger/logger.hpp>
#include <utils/revstr.hpp>
#include <utils/split.hpp>
@@ -62,8 +63,8 @@ void Bridge::shutdown(const std::string& exit_message)
{
for (auto& pair: this->irc_clients)
{
- pair.second->send_quit_command(exit_message);
- pair.second->leave_dummy_channel(exit_message, {});
+ std::unique_ptr<IrcClient>& irc = pair.second;
+ irc->send_quit_command(exit_message);
}
}
@@ -103,7 +104,7 @@ const std::string& Bridge::get_jid() const
std::string Bridge::get_bare_jid() const
{
Jid jid(this->user_jid);
- return jid.local + "@" + jid.domain;
+ return jid.bare();
}
Xmpp::body Bridge::make_xmpp_body(const std::string& str, const std::string& encoding)
@@ -133,11 +134,11 @@ IrcClient* Bridge::make_irc_client(const std::string& hostname, const std::strin
realname = this->get_bare_jid();
}
this->irc_clients.emplace(hostname,
- std::make_shared<IrcClient>(this->poller, hostname,
+ std::make_unique<IrcClient>(this->poller, hostname,
nickname, username,
realname, jid.domain,
*this));
- std::shared_ptr<IrcClient> irc = this->irc_clients.at(hostname);
+ std::unique_ptr<IrcClient>& irc = this->irc_clients.at(hostname);
return irc.get();
}
}
@@ -166,56 +167,36 @@ IrcClient* Bridge::find_irc_client(const std::string& hostname) const
}
}
-bool Bridge::join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password,
- const std::string& resource, HistoryLimit history_limit)
+bool Bridge::join_irc_channel(const Iid& iid, std::string nickname,
+ const std::string& password,
+ const std::string& resource,
+ HistoryLimit history_limit,
+ const bool force_join)
{
const auto& hostname = iid.get_server();
+#ifdef USE_DATABASE
+ auto soptions = Database::get_irc_server_options(this->get_bare_jid(), hostname);
+ if (!soptions.col<Database::Nick>().empty())
+ nickname = soptions.col<Database::Nick>();
+#endif
IrcClient* irc = this->make_irc_client(hostname, nickname);
irc->history_limit = history_limit;
this->add_resource_to_server(hostname, resource);
auto res_in_chan = this->is_resource_in_chan(ChannelKey{iid.get_local(), hostname}, resource);
if (!res_in_chan)
this->add_resource_to_chan(ChannelKey{iid.get_local(), hostname}, resource);
- if (iid.get_local().empty())
- { // Join the dummy channel
- if (irc->is_welcomed())
- {
- if (res_in_chan)
- return false;
- // Immediately simulate a message coming from the IRC server saying that we
- // joined the channel
- if (irc->get_dummy_channel().joined)
- {
- this->generate_channel_join_for_resource(iid, resource);
- }
- else
- {
- const IrcMessage join_message(irc->get_nick(), "JOIN", {""});
- irc->on_channel_join(join_message);
- const IrcMessage end_join_message(std::string(iid.get_server()), "366",
- {irc->get_nick(),
- "", "End of NAMES list"});
- irc->on_channel_completely_joined(end_join_message);
- }
- }
- else
- {
- irc->get_dummy_channel().joining = true;
- irc->start();
- }
- return true;
- }
if (irc->is_channel_joined(iid.get_local()) == false)
{
irc->send_join_command(iid.get_local(), password);
return true;
- } else if (!res_in_chan) {
+ } else if (!res_in_chan || force_join) {
+ // See https://github.com/xsf/xeps/pull/499 for the force_join argument
this->generate_channel_join_for_resource(iid, resource);
}
return false;
}
-void Bridge::send_channel_message(const Iid& iid, const std::string& body)
+void Bridge::send_channel_message(const Iid& iid, const std::string& body, std::string id)
{
if (iid.get_server().empty())
{
@@ -240,8 +221,26 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
std::vector<std::string> lines = utils::split(body, '\n', true);
if (lines.empty())
return ;
+ bool first = true;
for (const std::string& line: lines)
{
+ std::string uuid;
+#ifdef USE_DATABASE
+ const auto xmpp_body = this->make_xmpp_body(line);
+ if (this->record_history)
+ uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
+ std::get<0>(xmpp_body), irc->get_own_nick());
+#endif
+ if (!first || id.empty())
+ id = utils::gen_uuid();
+
+ MessageCallback mirror_to_all_resources = [this, iid, uuid, id](const IrcClient* irc, const IrcMessage& message) {
+ const std::string& line = message.arguments[1];
+ for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
+ this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line),
+ this->user_jid + "/" + resource, uuid, id);
+ };
+
if (line.substr(0, 5) == "/mode")
{
std::vector<std::string> args = utils::split(line.substr(5), ' ', false);
@@ -250,20 +249,12 @@ void Bridge::send_channel_message(const Iid& iid, const std::string& body)
// XMPP user, that’s not a textual message.
}
else if (line.substr(0, 4) == "/me ")
- irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01");
+ irc->send_channel_message(iid.get_local(), action_prefix + line.substr(4) + "\01",
+ std::move(mirror_to_all_resources));
else
- irc->send_channel_message(iid.get_local(), line);
+ irc->send_channel_message(iid.get_local(), line, std::move(mirror_to_all_resources));
- std::string uuid;
-#ifdef USE_DATABASE
- const auto xmpp_body = this->make_xmpp_body(line);
- if (this->record_history)
- uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
- std::get<0>(xmpp_body), irc->get_own_nick());
-#endif
- for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
- this->xmpp.send_muc_message(std::to_string(iid), irc->get_own_nick(), this->make_xmpp_body(line),
- this->user_jid + "/" + resource, uuid);
+ first = false;
}
}
@@ -445,15 +436,11 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
#endif
if (channel->joined && !channel->parting && !persistent)
{
- const auto& chan_name = iid.get_local();
- if (chan_name.empty())
- irc->leave_dummy_channel(status_message, resource);
- else
- irc->send_part_command(iid.get_local(), status_message);
+ irc->send_part_command(iid.get_local(), status_message);
}
else if (channel->joined)
{
- this->send_muc_leave(iid, channel->get_self()->nick, "", true, true, resource);
+ this->send_muc_leave(iid, *channel->get_self(), "", true, true, resource, irc);
}
if (persistent)
this->remove_resource_from_chan(key, resource);
@@ -464,14 +451,13 @@ void Bridge::leave_irc_channel(Iid&& iid, const std::string& status_message, con
else
{
if (channel && channel->joined)
- this->send_muc_leave(iid, channel->get_self()->nick,
+ this->send_muc_leave(iid, *channel->get_self(),
"Biboumi note: " + std::to_string(resources - 1) + " resources are still in this channel.",
- true, true, resource);
+ true, true, resource, irc);
this->remove_resource_from_chan(key, resource);
}
- if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
- this->remove_resource_from_server(iid.get_server(), resource);
-
+ if (this->number_of_channels_the_resource_is_in(iid.get_server(), resource) == 0)
+ this->remove_resource_from_server(iid.get_server(), resource);
}
void Bridge::send_irc_nick_change(const Iid& iid, const std::string& new_nick, const std::string& requesting_resource)
@@ -836,22 +822,24 @@ void Bridge::send_irc_version_request(const std::string& irc_hostname, const std
this->add_waiting_irc(std::move(cb));
}
-void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc)
+void Bridge::send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log)
{
const auto encoding = in_encoding_for(*this, iid);
+ std::string uuid{};
if (muc)
{
#ifdef USE_DATABASE
const auto xmpp_body = this->make_xmpp_body(body, encoding);
- if (!nick.empty() && this->record_history)
- Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
- std::get<0>(xmpp_body), nick);
+ if (log && this->record_history)
+ uuid = Database::store_muc_message(this->get_bare_jid(), iid.get_local(), iid.get_server(), std::chrono::system_clock::now(),
+ std::get<0>(xmpp_body), nick);
+#else
+ (void)log;
#endif
for (const auto& resource: this->resources_in_chan[iid.to_tuple()])
{
this->xmpp.send_muc_message(std::to_string(iid), nick, this->make_xmpp_body(body, encoding),
- this->user_jid + "/" + resource, {});
-
+ this->user_jid + "/" + resource, uuid, utils::gen_uuid());
}
}
else
@@ -881,19 +869,24 @@ void Bridge::send_presence_error(const Iid& iid, const std::string& nick,
this->xmpp.send_presence_error(std::to_string(iid), nick, this->user_jid, type, condition, error_code, text);
}
-void Bridge::send_muc_leave(const Iid& iid, const std::string& nick,
+void Bridge::send_muc_leave(const Iid& iid, const IrcUser& user,
const std::string& message, const bool self,
const bool user_requested,
- const std::string& resource)
+ const std::string& resource,
+ const IrcClient* client)
{
+ std::string affiliation;
+ std::string role;
+ std::tie(role, affiliation) = get_role_affiliation_from_irc_mode(user.get_most_significant_mode(client->get_sorted_user_modes()));
+
if (!resource.empty())
- this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
- this->user_jid + "/" + resource, self, user_requested);
+ this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
+ this->user_jid + "/" + resource, self, user_requested, affiliation, role);
else
{
for (const auto &res: this->resources_in_chan[iid.to_tuple()])
- this->xmpp.send_muc_leave(std::to_string(iid), nick, this->make_xmpp_body(message),
- this->user_jid + "/" + res, self, user_requested);
+ this->xmpp.send_muc_leave(std::to_string(iid), user.nick, this->make_xmpp_body(message),
+ this->user_jid + "/" + res, self, user_requested, affiliation, role);
if (self)
{
// Copy the resources currently in that channel
@@ -906,9 +899,7 @@ void Bridge::send_muc_leave(const Iid& iid, const std::string& nick,
for (const auto& r: resources_in_chan)
if (this->number_of_channels_the_resource_is_in(iid.get_server(), r) == 0)
this->remove_resource_from_server(iid.get_server(), r);
-
}
-
}
IrcClient* irc = this->find_irc_client(iid.get_server());
if (self && irc && irc->number_of_joined_channels() == 0)
@@ -1016,11 +1007,14 @@ void Bridge::send_room_history(const std::string& hostname, const std::string& c
void Bridge::send_room_history(const std::string& hostname, std::string chan_name, const std::string& resource, const HistoryLimit& history_limit)
{
#ifdef USE_DATABASE
- const auto coptions = Database::get_irc_channel_options_with_server_and_global_default(this->user_jid, hostname, chan_name);
- auto limit = coptions.col<Database::MaxHistoryLength>();
+ const auto goptions = Database::get_global_options(this->user_jid);
+ auto limit = goptions.col<Database::MaxHistoryLength>();
+ if (limit < 0)
+ limit = 20;
if (history_limit.stanzas >= 0 && history_limit.stanzas < limit)
limit = history_limit.stanzas;
- const auto lines = Database::get_muc_logs(this->user_jid, chan_name, hostname, limit, history_limit.since);
+ const auto result = Database::get_muc_logs(this->user_jid, chan_name, hostname, static_cast<std::size_t>(limit), history_limit.since, {}, Id::unset_value, Database::Paging::last);
+ const auto& lines = std::get<1>(result);
chan_name.append(utils::empty_if_fixed_server("%" + hostname));
for (const auto& line: lines)
{
@@ -1156,12 +1150,12 @@ void Bridge::trigger_on_irc_message(const std::string& irc_hostname, const IrcMe
}
}
-std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients()
+std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients()
{
return this->irc_clients;
}
-const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& Bridge::get_irc_clients() const
+const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& Bridge::get_irc_clients() const
{
return this->irc_clients;
}
@@ -1228,15 +1222,6 @@ void Bridge::remove_resource_from_server(const Bridge::IrcHostname& irc_hostname
}
}
-bool Bridge::is_resource_in_server(const Bridge::IrcHostname& irc_hostname, const std::string& resource) const
-{
- auto it = this->resources_in_server.find(irc_hostname);
- if (it != this->resources_in_server.end())
- if (it->second.count(resource) == 1)
- return true;
- return false;
-}
-
std::size_t Bridge::number_of_resources_in_chan(const Bridge::ChannelKey& channel) const
{
auto it = this->resources_in_chan.find(channel);
@@ -1254,9 +1239,6 @@ std::size_t Bridge::number_of_channels_the_resource_is_in(const std::string& irc
res++;
}
- IrcClient* irc = this->find_irc_client(irc_hostname);
- if (irc && (irc->get_dummy_channel().joined || irc->get_dummy_channel().joining))
- res++;
return res;
}
diff --git a/src/bridge/bridge.hpp b/src/bridge/bridge.hpp
index c2f0233..5c547ff 100644
--- a/src/bridge/bridge.hpp
+++ b/src/bridge/bridge.hpp
@@ -75,9 +75,13 @@ public:
* Try to join an irc_channel, does nothing and return true if the channel
* was already joined.
*/
- bool join_irc_channel(const Iid& iid, const std::string& nickname, const std::string& password, const std::string& resource, HistoryLimit history_limit);
+ bool join_irc_channel(const Iid& iid, std::string nickname,
+ const std::string& password,
+ const std::string& resource,
+ HistoryLimit history_limit,
+ const bool force_join);
- void send_channel_message(const Iid& iid, const std::string& body);
+ void send_channel_message(const Iid& iid, const std::string& body, std::string id);
void send_private_message(const Iid& iid, const std::string& body, const std::string& type="PRIVMSG");
void send_raw_message(const std::string& hostname, const std::string& body);
void leave_irc_channel(Iid&& iid, const std::string& status_message, const std::string& resource);
@@ -162,7 +166,7 @@ public:
/**
* Send a MUC message from some participant
*/
- void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc);
+ void send_message(const Iid& iid, const std::string& nick, const std::string& body, const bool muc, const bool log=true);
/**
* Send a presence of type error, from a room.
*/
@@ -170,10 +174,11 @@ public:
/**
* Send an unavailable presence from this participant
*/
- void send_muc_leave(const Iid& iid, const std::string& nick,
+ void send_muc_leave(const Iid& iid, const IrcUser& nick,
const std::string& message, const bool self,
const bool user_requested,
- const std::string& resource="");
+ const std::string& resource,
+ const IrcClient* client);
/**
* Send presences to indicate that an user old_nick (ourself if self ==
* true) changed his nick to new_nick. The user_mode is needed because
@@ -236,8 +241,8 @@ public:
* iq_responder_callback_t and remove the callback from the list.
*/
void trigger_on_irc_message(const std::string& irc_hostname, const IrcMessage& message);
- std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients();
- const std::unordered_map<std::string, std::shared_ptr<IrcClient>>& get_irc_clients() const;
+ std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients();
+ const std::unordered_map<std::string, std::unique_ptr<IrcClient>>& get_irc_clients() const;
std::set<char> get_chantypes(const std::string& hostname) const;
#ifdef USE_DATABASE
void set_record_history(const bool val);
@@ -270,7 +275,7 @@ private:
* One IrcClient for each IRC server we need to be connected to.
* The pointer is shared by the bridge and the poller.
*/
- std::unordered_map<std::string, std::shared_ptr<IrcClient>> irc_clients;
+ std::unordered_map<std::string, std::unique_ptr<IrcClient>> irc_clients;
/**
* To communicate back with the XMPP component
*/
@@ -311,13 +316,14 @@ private:
*/
void add_resource_to_chan(const ChannelKey& channel, const std::string& resource);
void remove_resource_from_chan(const ChannelKey& channel, const std::string& resource);
+public:
bool is_resource_in_chan(const ChannelKey& channel, const std::string& resource) const;
+private:
void remove_all_resources_from_chan(const ChannelKey& channel);
std::size_t number_of_resources_in_chan(const ChannelKey& channel) const;
void add_resource_to_server(const IrcHostname& irc_hostname, const std::string& resource);
void remove_resource_from_server(const IrcHostname& irc_hostname, const std::string& resource);
- bool is_resource_in_server(const IrcHostname& irc_hostname, const std::string& resource) const;
size_t number_of_channels_the_resource_is_in(const std::string& irc_hostname, const std::string& resource) const;
/**
diff --git a/src/bridge/history_limit.hpp b/src/bridge/history_limit.hpp
index 9c75256..93e36e1 100644
--- a/src/bridge/history_limit.hpp
+++ b/src/bridge/history_limit.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include <string>
+
// Default values means no limit
struct HistoryLimit
{
diff --git a/src/config/config.cpp b/src/config/config.cpp
index 412b170..2f64b9e 100644
--- a/src/config/config.cpp
+++ b/src/config/config.cpp
@@ -1,10 +1,12 @@
#include <config/config.hpp>
#include <utils/tolower.hpp>
+#include <utils/split.hpp>
-#include <iostream>
-#include <cstring>
-
+#include <algorithm>
#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <vector>
using namespace std::string_literals;
@@ -40,6 +42,15 @@ int Config::get_int(const std::string& option, const int& def)
return def;
}
+bool Config::is_in_list(const std::string& option, const std::string& value)
+{
+ std::string res = Config::get(option, "");
+ if (res.empty())
+ return false;
+ std::vector<std::string> list = utils::split(res, ':');
+ return std::find(list.cbegin(), list.cend(), value) != list.cend();
+}
+
void Config::set(const std::string& option, const std::string& value, bool save)
{
Config::values[option] = value;
diff --git a/src/config/config.hpp b/src/config/config.hpp
index c5ef15d..9c28e8c 100644
--- a/src/config/config.hpp
+++ b/src/config/config.hpp
@@ -46,6 +46,11 @@ public:
static int get_int(const std::string&, const int&);
static bool get_bool(const std::string&, const bool);
/**
+ * Returns true if value is present in a colon-separated list, otherwise
+ * false.
+ */
+ static bool is_in_list(const std::string& option, const std::string& value);
+ /**
* Set a value for the given option. And write all the config
* in the file from which it was read if save is true.
*/
diff --git a/src/database/column.hpp b/src/database/column.hpp
index 1f16bcf..837aa3f 100644
--- a/src/database/column.hpp
+++ b/src/database/column.hpp
@@ -9,14 +9,30 @@ struct Column
value{default_value} {}
Column():
value{} {}
+ void clear()
+ {
+ this->value = {};
+ }
using real_type = T;
T value{};
};
-struct Id: Column<std::size_t> {
+template <typename T>
+struct UnclearableColumn: public Column<T>
+{
+ using Column<T>::Column;
+ void clear()
+ { }
+};
+
+struct ForeignKey: Column<std::size_t> {
+ static constexpr auto name = "fk_";
+};
+
+struct Id: UnclearableColumn<std::size_t> {
static constexpr std::size_t unset_value = static_cast<std::size_t>(-1);
static constexpr auto name = "id_";
static constexpr auto options = "PRIMARY KEY";
- Id(): Column<std::size_t>(-1) {}
+ Id(): UnclearableColumn<std::size_t>(unset_value) {}
};
diff --git a/src/database/database.cpp b/src/database/database.cpp
index 3622963..9037ce1 100644
--- a/src/database/database.cpp
+++ b/src/database/database.cpp
@@ -1,10 +1,12 @@
#include "biboumi.h"
#ifdef USE_DATABASE
+#include <database/select_query.hpp>
+#include <database/save.hpp>
#include <database/database.hpp>
-#include <uuid/uuid.h>
#include <utils/get_first_non_empty.hpp>
#include <utils/time.hpp>
+#include <utils/uuid.hpp>
#include <config/config.hpp>
#include <database/sqlite3_engine.hpp>
@@ -21,6 +23,7 @@ Database::GlobalOptionsTable Database::global_options("globaloptions_");
Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_");
Database::RosterTable Database::roster("roster");
+Database::AfterConnectionCommandsTable Database::after_connection_commands("after_connection_commands_");
std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{};
Database::GlobalPersistent::GlobalPersistent():
@@ -53,57 +56,80 @@ void Database::open(const std::string& filename)
Database::irc_channel_options.upgrade(*Database::db);
Database::roster.create(*Database::db);
Database::roster.upgrade(*Database::db);
+ Database::after_connection_commands.create(*Database::db);
+ Database::after_connection_commands.upgrade(*Database::db);
create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name());
}
Database::GlobalOptions Database::get_global_options(const std::string& owner)
{
- auto request = Database::global_options.select();
+ auto request = select(Database::global_options);
request.where() << Owner{} << "=" << owner;
- Database::GlobalOptions options{Database::global_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
- options.col<Owner>() = owner;
+ return result.front();
+ Database::GlobalOptions options{Database::global_options.get_name()};
+ options.col<Owner>() = owner;
return options;
}
Database::IrcServerOptions Database::get_irc_server_options(const std::string& owner, const std::string& server)
{
- auto request = Database::irc_server_options.select();
+ auto request = select(Database::irc_server_options);
request.where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;
- Database::IrcServerOptions options{Database::irc_server_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
+ return result.front();
+ Database::IrcServerOptions options{Database::irc_server_options.get_name()};
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ return options;
+}
+
+Database::AfterConnectionCommands Database::get_after_connection_commands(const IrcServerOptions& server_options)
+{
+ const auto id = server_options.col<Id>();
+ if (id == Id::unset_value)
+ return {};
+ auto request = select(Database::after_connection_commands);
+ request.where() << ForeignKey{} << "=" << id;
+ return request.execute(*Database::db);
+}
+
+void Database::set_after_connection_commands(const Database::IrcServerOptions& server_options, Database::AfterConnectionCommands& commands)
+{
+ const auto id = server_options.col<Id>();
+ if (id == Id::unset_value)
+ return ;
+
+ Transaction transaction;
+ auto query = Database::after_connection_commands.del();
+ query.where() << ForeignKey{} << "=" << id;
+ query.execute(*Database::db);
+
+ for (auto& command: commands)
{
- options.col<Owner>() = owner;
- options.col<Server>() = server;
+ command.col<ForeignKey>() = server_options.col<Id>();
+ save(command, *Database::db);
}
- return options;
}
Database::IrcChannelOptions Database::get_irc_channel_options(const std::string& owner, const std::string& server, const std::string& channel)
{
- auto request = Database::irc_channel_options.select();
+ auto request = select(Database::irc_channel_options);
request.where() << Owner{} << "=" << owner <<\
" and " << Server{} << "=" << server <<\
" and " << Channel{} << "=" << channel;
- Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
auto result = request.execute(*Database::db);
if (result.size() == 1)
- options = result.front();
- else
- {
- options.col<Owner>() = owner;
- options.col<Server>() = server;
- options.col<Channel>() = channel;
- }
+ return result.front();
+ Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
+ options.col<Owner>() = owner;
+ options.col<Server>() = server;
+ options.col<Channel>() = channel;
return options;
}
@@ -136,10 +162,6 @@ Database::IrcChannelOptions Database::get_irc_channel_options_with_server_and_gl
coptions.col<EncodingOut>() = get_first_non_empty(coptions.col<EncodingOut>(),
soptions.col<EncodingOut>());
- coptions.col<MaxHistoryLength>() = get_first_non_empty(coptions.col<MaxHistoryLength>(),
- soptions.col<MaxHistoryLength>(),
- goptions.col<MaxHistoryLength>());
-
return coptions;
}
@@ -159,15 +181,15 @@ std::string Database::store_muc_message(const std::string& owner, const std::str
line.col<Body>() = body;
line.col<Nick>() = nick;
- line.save(Database::db);
+ save(line, *Database::db);
return uuid;
}
-std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
- int limit, const std::string& start, const std::string& end)
+std::tuple<bool, std::vector<Database::MucLogLine>> Database::get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
+ std::size_t limit, const std::string& start, const std::string& end, const Id::real_type reference_record_id, Database::Paging paging)
{
- auto request = Database::muc_log_lines.select();
+ auto request = select(Database::muc_log_lines);
request.where() << Database::Owner{} << "=" << owner << \
" and " << Database::IrcChanName{} << "=" << chan_name << \
" and " << Database::IrcServerName{} << "=" << server;
@@ -184,15 +206,70 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne
if (end_time != -1)
request << " and " << Database::Date{} << "<=" << end_time;
}
+ if (reference_record_id != Id::unset_value)
+ {
+ request << " and " << Id{};
+ if (paging == Database::Paging::first)
+ request << ">";
+ else
+ request << "<";
+ request << reference_record_id;
+ }
- request.order_by() << Id{} << " DESC ";
+ if (paging == Database::Paging::first)
+ request.order_by() << Id{} << " ASC ";
+ else
+ request.order_by() << Id{} << " DESC ";
- if (limit >= 0)
- request.limit() << limit;
+ // Just a simple trick: to know whether we got the totality of the
+ // possible results matching this query (except for the limit), we just
+ // ask one more element. If we get that additional element, this means
+ // we don’t have everything. And then we just discard it. If we don’t
+ // have more, this means we have everything.
+ request.limit() << limit + 1;
auto result = request.execute(*Database::db);
+ bool complete = true;
- return {result.crbegin(), result.crend()};
+ if (result.size() == limit + 1)
+ {
+ complete = false;
+ result.erase(std::prev(result.end()));
+ }
+
+ if (paging == Database::Paging::first)
+ return std::make_tuple(complete, result);
+ else
+ return std::make_tuple(complete, std::vector<Database::MucLogLine>(result.crbegin(), result.crend()));
+}
+
+Database::MucLogLine Database::get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server,
+ const std::string& uuid, const std::string& start, const std::string& end)
+{
+ auto request = select(Database::muc_log_lines);
+ request.where() << Database::Owner{} << "=" << owner << \
+ " and " << Database::IrcChanName{} << "=" << chan_name << \
+ " and " << Database::IrcServerName{} << "=" << server << \
+ " and " << Database::Uuid{} << "=" << uuid;
+
+ if (!start.empty())
+ {
+ const auto start_time = utils::parse_datetime(start);
+ if (start_time != -1)
+ request << " and " << Database::Date{} << ">=" << start_time;
+ }
+ if (!end.empty())
+ {
+ const auto end_time = utils::parse_datetime(end);
+ if (end_time != -1)
+ request << " and " << Database::Date{} << "<=" << end_time;
+ }
+
+ auto result = request.execute(*Database::db);
+
+ if (result.empty())
+ throw Database::RecordNotFound{};
+ return result.front();
}
void Database::add_roster_item(const std::string& local, const std::string& remote)
@@ -202,7 +279,7 @@ void Database::add_roster_item(const std::string& local, const std::string& remo
roster_item.col<Database::LocalJid>() = local;
roster_item.col<Database::RemoteJid>() = remote;
- roster_item.save(Database::db);
+ save(roster_item, *Database::db);
}
void Database::delete_roster_item(const std::string& local, const std::string& remote)
@@ -216,7 +293,7 @@ void Database::delete_roster_item(const std::string& local, const std::string& r
bool Database::has_roster_item(const std::string& local, const std::string& remote)
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
query.where() << Database::LocalJid{} << "=" << local << \
" and " << Database::RemoteJid{} << "=" << remote;
@@ -227,7 +304,7 @@ bool Database::has_roster_item(const std::string& local, const std::string& remo
std::vector<Database::RosterItem> Database::get_contact_list(const std::string& local)
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
query.where() << Database::LocalJid{} << "=" << local;
return query.execute(*Database::db);
@@ -235,7 +312,7 @@ std::vector<Database::RosterItem> Database::get_contact_list(const std::string&
std::vector<Database::RosterItem> Database::get_full_roster()
{
- auto query = Database::roster.select();
+ auto query = select(Database::roster);
return query.execute(*Database::db);
}
@@ -247,11 +324,25 @@ void Database::close()
std::string Database::gen_uuid()
{
- char uuid_str[37];
- uuid_t uuid;
- uuid_generate(uuid);
- uuid_unparse(uuid, uuid_str);
- return uuid_str;
+ return utils::gen_uuid();
+}
+
+Transaction::Transaction()
+{
+ const auto result = Database::raw_exec("BEGIN");
+ if (std::get<bool>(result) == false)
+ log_error("Failed to create SQL transaction: ", std::get<std::string>(result));
+ else
+ this->success = true;
}
+Transaction::~Transaction()
+{
+ if (this->success)
+ {
+ const auto result = Database::raw_exec("END");
+ if (std::get<bool>(result) == false)
+ log_error("Failed to end SQL transaction: ", std::get<std::string>(result));
+ }
+}
#endif
diff --git a/src/database/database.hpp b/src/database/database.hpp
index ec44543..4a413be 100644
--- a/src/database/database.hpp
+++ b/src/database/database.hpp
@@ -22,18 +22,20 @@ class Database
{
public:
using time_point = std::chrono::system_clock::time_point;
+ struct RecordNotFound: public std::exception {};
+ enum class Paging { first, last };
struct Uuid: Column<std::string> { static constexpr auto name = "uuid_"; };
- struct Owner: Column<std::string> { static constexpr auto name = "owner_"; };
+ struct Owner: UnclearableColumn<std::string> { static constexpr auto name = "owner_"; };
- struct IrcChanName: Column<std::string> { static constexpr auto name = "ircchanname_"; };
+ struct IrcChanName: UnclearableColumn<std::string> { static constexpr auto name = "ircchanname_"; };
- struct Channel: Column<std::string> { static constexpr auto name = "channel_"; };
+ struct Channel: UnclearableColumn<std::string> { static constexpr auto name = "channel_"; };
- struct IrcServerName: Column<std::string> { static constexpr auto name = "ircservername_"; };
+ struct IrcServerName: UnclearableColumn<std::string> { static constexpr auto name = "ircservername_"; };
- struct Server: Column<std::string> { static constexpr auto name = "server_"; };
+ struct Server: UnclearableColumn<std::string> { static constexpr auto name = "server_"; };
struct Date: Column<time_point::rep> { static constexpr auto name = "date_"; };
@@ -82,6 +84,10 @@ class Database
struct RemoteJid: Column<std::string> { static constexpr auto name = "remote"; };
+ struct Address: Column<std::string> { static constexpr auto name = "address_"; };
+
+ struct ThrottleLimit: Column<long int> { static constexpr auto name = "throttlelimit_";
+ ThrottleLimit(): Column<long int>(10) {} };
using MucLogLineTable = Table<Id, Uuid, Owner, IrcChanName, IrcServerName, Date, Body, Nick>;
using MucLogLine = MucLogLineTable::RowType;
@@ -89,7 +95,7 @@ class Database
using GlobalOptionsTable = Table<Id, Owner, MaxHistoryLength, RecordHistory, GlobalPersistent>;
using GlobalOptions = GlobalOptionsTable::RowType;
- using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, AfterConnectionCommand, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength>;
+ using IrcServerOptionsTable = Table<Id, Owner, Server, Pass, TlsPorts, Ports, Username, Realname, VerifyCert, TrustedFingerprint, EncodingOut, EncodingIn, MaxHistoryLength, Address, Nick, ThrottleLimit>;
using IrcServerOptions = IrcServerOptionsTable::RowType;
using IrcChannelOptionsTable = Table<Id, Owner, Server, Channel, EncodingOut, EncodingIn, MaxHistoryLength, Persistent, RecordHistoryOptional>;
@@ -98,6 +104,9 @@ class Database
using RosterTable = Table<LocalJid, RemoteJid>;
using RosterItem = RosterTable::RowType;
+ using AfterConnectionCommandsTable = Table<Id, ForeignKey, AfterConnectionCommand>;
+ using AfterConnectionCommands = std::vector<AfterConnectionCommandsTable::RowType>;
+
Database() = default;
~Database() = default;
@@ -118,8 +127,22 @@ class Database
static IrcChannelOptions get_irc_channel_options_with_server_and_global_default(const std::string& owner,
const std::string& server,
const std::string& channel);
- static std::vector<MucLogLine> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
- int limit=-1, const std::string& start="", const std::string& end="");
+ static AfterConnectionCommands get_after_connection_commands(const IrcServerOptions& server_options);
+ static void set_after_connection_commands(const IrcServerOptions& server_options, AfterConnectionCommands& commands);
+
+ /**
+ * Get all the lines between (optional) start and end dates, with a (optional) limit.
+ * If after_id is set, only the records after it will be returned.
+ */
+ static std::tuple<bool, std::vector<MucLogLine>> get_muc_logs(const std::string& owner, const std::string& chan_name, const std::string& server,
+ std::size_t limit, const std::string& start="", const std::string& end="",
+ const Id::real_type reference_record_id=Id::unset_value, Paging=Paging::first);
+
+ /**
+ * Get just one single record matching the given uuid, between (optional) end and start.
+ * If it does not exist (or is not between end and start), throw a RecordNotFound exception.
+ */
+ static MucLogLine get_muc_log(const std::string& owner, const std::string& chan_name, const std::string& server, const std::string& uuid, const std::string& start="", const std::string& end="");
static std::string store_muc_message(const std::string& owner, const std::string& chan_name, const std::string& server_name,
time_point date, const std::string& body, const std::string& nick);
@@ -144,6 +167,8 @@ class Database
static IrcServerOptionsTable irc_server_options;
static IrcChannelOptionsTable irc_channel_options;
static RosterTable roster;
+ static AfterConnectionCommandsTable after_connection_commands;
+
static std::unique_ptr<DatabaseEngine> db;
/**
@@ -181,11 +206,20 @@ class Database
static auto raw_exec(const std::string& query)
{
- Database::db->raw_exec(query);
+ return Database::db->raw_exec(query);
}
private:
static std::string gen_uuid();
static std::map<CacheKey, EncodingIn::real_type> encoding_in_cache;
};
+
+class Transaction
+{
+public:
+ Transaction();
+ ~Transaction();
+ bool success{false};
+};
+
#endif /* USE_DATABASE */
diff --git a/src/database/delete_query.hpp b/src/database/delete_query.hpp
new file mode 100644
index 0000000..dce705b
--- /dev/null
+++ b/src/database/delete_query.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <database/query.hpp>
+#include <database/engine.hpp>
+
+class DeleteQuery: public Query
+{
+public:
+ DeleteQuery(const std::string& name):
+ Query("DELETE")
+ {
+ this->body += " from " + name;
+ }
+
+ DeleteQuery& where()
+ {
+ this->body += " WHERE ";
+ return *this;
+ };
+
+ void execute(DatabaseEngine& db)
+ {
+ auto statement = db.prepare(this->body);
+ if (!statement)
+ return;
+#ifdef DEBUG_SQL_QUERIES
+ const auto timer = this->log_and_time();
+#endif
+ statement->bind(std::move(this->params));
+ if (statement->step() != StepResult::Done)
+ log_error("Failed to execute DELETE command");
+ }
+};
diff --git a/src/database/insert_query.hpp b/src/database/insert_query.hpp
index 9726424..230e873 100644
--- a/src/database/insert_query.hpp
+++ b/src/database/insert_query.hpp
@@ -1,10 +1,15 @@
#pragma once
#include <database/statement.hpp>
+#include <database/database.hpp>
#include <database/column.hpp>
#include <database/query.hpp>
+#include <database/row.hpp>
+
#include <logger/logger.hpp>
+#include <utils/is_one_of.hpp>
+
#include <type_traits>
#include <vector>
#include <string>
@@ -22,7 +27,7 @@ update_autoincrement_id(std::tuple<T...>& columns, Statement& statement)
template <std::size_t N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
-update_autoincrement_id(std::tuple<T...>&, Statement& statement)
+update_autoincrement_id(std::tuple<T...>&, Statement&)
{}
struct InsertQuery: public Query
@@ -127,3 +132,13 @@ struct InsertQuery: public Query
insert_col_name(const std::tuple<T...>&)
{}
};
+
+template <typename... T>
+void insert(Row<T...>& row, DatabaseEngine& db)
+{
+ InsertQuery query(row.table_name, row.columns);
+ // Ugly workaround for non portable stuff
+ if (is_one_of<Id, T...>)
+ query.body += db.get_returning_id_sql_string(Id::name);
+ query.execute(db, row.columns);
+}
diff --git a/src/database/postgresql_engine.cpp b/src/database/postgresql_engine.cpp
index 984a959..59bc885 100644
--- a/src/database/postgresql_engine.cpp
+++ b/src/database/postgresql_engine.cpp
@@ -11,6 +11,8 @@
#include <logger/logger.hpp>
+#include <cstring>
+
PostgresqlEngine::PostgresqlEngine(PGconn*const conn):
conn(conn)
{}
@@ -20,6 +22,15 @@ PostgresqlEngine::~PostgresqlEngine()
PQfinish(this->conn);
}
+static void logging_notice_processor(void*, const char* original)
+{
+ if (original && std::strlen(original) > 0)
+ {
+ std::string message{original, std::strlen(original) - 1};
+ log_warning("PostgreSQL: ", message);
+ }
+}
+
std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& conninfo)
{
PGconn* con = PQconnectdb(conninfo.data());
@@ -34,8 +45,10 @@ std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& connin
{
const char* errmsg = PQerrorMessage(con);
log_error("Postgresql connection failed: ", errmsg);
+ PQfinish(con);
throw std::runtime_error("failed to open connection.");
}
+ PQsetNoticeProcessor(con, &logging_notice_processor, nullptr);
return std::make_unique<PostgresqlEngine>(con);
}
diff --git a/src/database/postgresql_engine.hpp b/src/database/postgresql_engine.hpp
index fe4fb53..1a9c249 100644
--- a/src/database/postgresql_engine.hpp
+++ b/src/database/postgresql_engine.hpp
@@ -36,12 +36,15 @@ private:
#else
+using namespace std::string_literals;
+
class PostgresqlEngine
{
public:
static std::unique_ptr<DatabaseEngine> open(const std::string& string)
{
throw std::runtime_error("Cannot open postgresql database "s + string + ": biboumi is not compiled with libpq.");
+ return {};
}
};
diff --git a/src/database/postgresql_statement.hpp b/src/database/postgresql_statement.hpp
index 571c8f1..37e8ea0 100644
--- a/src/database/postgresql_statement.hpp
+++ b/src/database/postgresql_statement.hpp
@@ -6,6 +6,8 @@
#include <libpq-fe.h>
+#include <cstring>
+
class PostgresqlStatement: public Statement
{
public:
@@ -90,7 +92,7 @@ class PostgresqlStatement: public Statement
private:
private:
- bool execute()
+ bool execute(const bool second_attempt=false)
{
std::vector<const char*> params;
params.reserve(this->params.size());
@@ -108,8 +110,20 @@ private:
const auto status = PQresultStatus(this->result);
if (status != PGRES_TUPLES_OK && status != PGRES_COMMAND_OK)
{
- log_error("Failed to execute command: ", PQresultErrorMessage(this->result));
- return false;
+ const char* original = PQerrorMessage(this->conn);
+ if (original && std::strlen(original) > 0)
+ log_error("Failed to execute command: ", std::string{original, std::strlen(original) - 1});
+ if (PQstatus(this->conn) != CONNECTION_OK && !second_attempt)
+ {
+ log_info("Trying to reconnect to PostgreSQL server and execute the query again.");
+ PQreset(this->conn);
+ return this->execute(true);
+ }
+ else
+ {
+ log_error("Givin up.");
+ return false;
+ }
}
return true;
}
diff --git a/src/database/query.cpp b/src/database/query.cpp
index d27dc59..5ec8599 100644
--- a/src/database/query.cpp
+++ b/src/database/query.cpp
@@ -6,11 +6,6 @@ void actual_bind(Statement& statement, const std::string& value, int index)
statement.bind_text(index, value);
}
-void actual_bind(Statement& statement, const std::int64_t value, int index)
-{
- statement.bind_int64(index, value);
-}
-
void actual_bind(Statement& statement, const OptionalBool& value, int index)
{
if (!value.is_set)
@@ -21,7 +16,6 @@ void actual_bind(Statement& statement, const OptionalBool& value, int index)
statement.bind_int64(index, -1);
}
-
void actual_add_param(Query& query, const std::string& val)
{
query.params.push_back(val);
diff --git a/src/database/query.hpp b/src/database/query.hpp
index 8434944..ae6e946 100644
--- a/src/database/query.hpp
+++ b/src/database/query.hpp
@@ -12,8 +12,14 @@
#include <string>
void actual_bind(Statement& statement, const std::string& value, int index);
-void actual_bind(Statement& statement, const std::int64_t value, int index);
void actual_bind(Statement& statement, const OptionalBool& value, int index);
+template <typename T>
+void actual_bind(Statement& statement, const T& value, int index)
+{
+ static_assert(std::is_integral<T>::value,
+ "Only a string, an optional-bool or an integer can be used.");
+ statement.bind_int64(index, static_cast<int>(value));
+}
#ifdef DEBUG_SQL_QUERIES
#include <utils/scopetimer.hpp>
diff --git a/src/database/row.hpp b/src/database/row.hpp
index 4dc98be..4004b5d 100644
--- a/src/database/row.hpp
+++ b/src/database/row.hpp
@@ -1,11 +1,5 @@
#pragma once
-#include <database/insert_query.hpp>
-#include <database/update_query.hpp>
-#include <logger/logger.hpp>
-
-#include <utils/is_one_of.hpp>
-
#include <type_traits>
template <typename... T>
@@ -29,44 +23,24 @@ struct Row
return col.value;
}
- template <bool Coucou=true>
- void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr)
- {
- this->insert(*db);
- }
-
- template <bool Coucou=true>
- void save(std::unique_ptr<DatabaseEngine>& db, typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr)
+ void clear()
{
- const Id& id = std::get<Id>(this->columns);
- if (id.value == Id::unset_value)
- {
- this->insert(*db);
- if (db->last_inserted_rowid >= 0)
- std::get<Id>(this->columns).value = static_cast<Id::real_type>(db->last_inserted_rowid);
- }
- else
- this->update(*db);
+ this->clear_col<0>();
}
- private:
- void insert(DatabaseEngine& db)
- {
- InsertQuery query(this->table_name, this->columns);
- // Ugly workaround for non portable stuff
- query.body += db.get_returning_id_sql_string(Id::name);
- query.execute(db, this->columns);
- }
+ std::tuple<T...> columns{};
+ std::string table_name;
- void update(DatabaseEngine& db)
+private:
+ template <std::size_t N>
+ typename std::enable_if<N < sizeof...(T), void>::type
+ clear_col()
{
- UpdateQuery query(this->table_name, this->columns);
-
- query.execute(db, this->columns);
+ std::get<N>(this->columns).clear();
+ this->clear_col<N+1>();
}
-
-public:
- std::tuple<T...> columns;
- std::string table_name;
-
+ template <std::size_t N>
+ typename std::enable_if<N == sizeof...(T), void>::type
+ clear_col()
+ { }
};
diff --git a/src/database/save.hpp b/src/database/save.hpp
new file mode 100644
index 0000000..4362110
--- /dev/null
+++ b/src/database/save.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <database/update_query.hpp>
+#include <database/insert_query.hpp>
+
+#include <database/engine.hpp>
+
+#include <database/row.hpp>
+
+#include <utils/is_one_of.hpp>
+
+template <typename... T, bool Coucou=true>
+void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr)
+{
+ insert(row, db);
+}
+
+template <typename... T, bool Coucou=true>
+void save(Row<T...>& row, DatabaseEngine& db, typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr)
+{
+ const Id& id = std::get<Id>(row.columns);
+ if (id.value == Id::unset_value)
+ {
+ insert(row, db);
+ if (db.last_inserted_rowid >= 0)
+ std::get<Id>(row.columns).value = static_cast<Id::real_type>(db.last_inserted_rowid);
+ }
+ else
+ update(row, db);
+}
+
diff --git a/src/database/select_query.hpp b/src/database/select_query.hpp
index 5a17f38..e372f2e 100644
--- a/src/database/select_query.hpp
+++ b/src/database/select_query.hpp
@@ -2,6 +2,8 @@
#include <database/engine.hpp>
+#include <database/table.hpp>
+#include <database/database.hpp>
#include <database/statement.hpp>
#include <database/query.hpp>
#include <logger/logger.hpp>
@@ -115,6 +117,8 @@ struct SelectQuery: public Query
#endif
auto statement = db.prepare(this->body);
+ if (!statement)
+ return rows;
statement->bind(std::move(this->params));
while (statement->step() == StepResult::Row)
@@ -130,3 +134,9 @@ struct SelectQuery: public Query
const std::string table_name;
};
+template <typename... T>
+auto select(const Table<T...>& table)
+{
+ SelectQuery<T...> query(table.name);
+ return query;
+}
diff --git a/src/database/sqlite3_engine.cpp b/src/database/sqlite3_engine.cpp
index ae4a146..5e3bba1 100644
--- a/src/database/sqlite3_engine.cpp
+++ b/src/database/sqlite3_engine.cpp
@@ -3,7 +3,6 @@
#ifdef SQLITE3_FOUND
#include <database/sqlite3_engine.hpp>
-
#include <database/sqlite3_statement.hpp>
#include <database/query.hpp>
diff --git a/src/database/sqlite3_engine.hpp b/src/database/sqlite3_engine.hpp
index 5b8176c..a7bfcdb 100644
--- a/src/database/sqlite3_engine.hpp
+++ b/src/database/sqlite3_engine.hpp
@@ -35,12 +35,15 @@ private:
#else
+using namespace std::string_literals;
+
class Sqlite3Engine
{
public:
static std::unique_ptr<DatabaseEngine> open(const std::string& string)
{
throw std::runtime_error("Cannot open sqlite3 database "s + string + ": biboumi is not compiled with sqlite3 lib.");
+ return {};
}
};
diff --git a/src/database/sqlite3_statement.hpp b/src/database/sqlite3_statement.hpp
index 7738fa6..3ed60c0 100644
--- a/src/database/sqlite3_statement.hpp
+++ b/src/database/sqlite3_statement.hpp
@@ -88,5 +88,4 @@ class Sqlite3Statement: public Statement
private:
sqlite3_stmt* stmt;
- int last_step_result{SQLITE_OK};
};
diff --git a/src/database/table.hpp b/src/database/table.hpp
index 680e7cc..0b8bfc0 100644
--- a/src/database/table.hpp
+++ b/src/database/table.hpp
@@ -2,7 +2,7 @@
#include <database/engine.hpp>
-#include <database/select_query.hpp>
+#include <database/delete_query.hpp>
#include <database/row.hpp>
#include <algorithm>
@@ -79,10 +79,10 @@ class Table
return {this->name};
}
- auto select()
+ auto del()
{
- SelectQuery<T...> select(this->name);
- return select;
+ DeleteQuery query(this->name);
+ return query;
}
const std::string& get_name() const
@@ -90,6 +90,8 @@ class Table
return this->name;
}
+ const std::string name;
+
private:
template <std::size_t N=0>
@@ -124,5 +126,4 @@ class Table
add_column_create(DatabaseEngine&, std::string&)
{ }
- const std::string name;
};
diff --git a/src/database/update_query.hpp b/src/database/update_query.hpp
index a29ac3f..c2b819d 100644
--- a/src/database/update_query.hpp
+++ b/src/database/update_query.hpp
@@ -1,7 +1,8 @@
#pragma once
-#include <database/query.hpp>
#include <database/engine.hpp>
+#include <database/query.hpp>
+#include <database/row.hpp>
using namespace std::string_literals;
@@ -102,3 +103,11 @@ struct UpdateQuery: public Query
actual_bind(statement, value.value, sizeof...(T));
}
};
+
+template <typename... T>
+void update(Row<T...>& row, DatabaseEngine& db)
+{
+ UpdateQuery query(row.table_name, row.columns);
+
+ query.execute(db, row.columns);
+}
diff --git a/src/irc/irc_channel.cpp b/src/irc/irc_channel.cpp
index 53043c7..2dd20fe 100644
--- a/src/irc/irc_channel.cpp
+++ b/src/irc/irc_channel.cpp
@@ -33,8 +33,9 @@ IrcUser* IrcChannel::find_user(const std::string& name) const
return nullptr;
}
-void IrcChannel::remove_user(const IrcUser* user)
+std::unique_ptr<IrcUser> IrcChannel::remove_user(const IrcUser* user)
{
+ std::unique_ptr<IrcUser> result{};
const auto nick = user->nick;
const bool is_self = (user == this->self);
const auto it = std::find_if(this->users.begin(), this->users.end(),
@@ -44,6 +45,7 @@ void IrcChannel::remove_user(const IrcUser* user)
});
if (it != this->users.end())
{
+ result = std::move(*it);
this->users.erase(it);
if (is_self)
{
@@ -51,16 +53,5 @@ void IrcChannel::remove_user(const IrcUser* user)
this->joined = false;
}
}
-}
-
-void IrcChannel::remove_all_users()
-{
- this->users.clear();
- this->self = nullptr;
-}
-
-DummyIrcChannel::DummyIrcChannel():
- IrcChannel(),
- joining(false)
-{
+ return result;
}
diff --git a/src/irc/irc_channel.hpp b/src/irc/irc_channel.hpp
index 8f85edb..7000ada 100644
--- a/src/irc/irc_channel.hpp
+++ b/src/irc/irc_channel.hpp
@@ -32,8 +32,7 @@ public:
IrcUser* add_user(const std::string& name,
const std::map<char, char>& prefix_to_mode);
IrcUser* find_user(const std::string& name) const;
- void remove_user(const IrcUser* user);
- void remove_all_users();
+ std::unique_ptr<IrcUser> remove_user(const IrcUser* user);
const std::vector<std::unique_ptr<IrcUser>>& get_users() const
{ return this->users; }
@@ -42,33 +41,3 @@ protected:
IrcUser* self{nullptr};
std::vector<std::unique_ptr<IrcUser>> users{};
};
-
-/**
- * A special channel that is not actually linked to any real irc
- * channel. This is just a channel representing a connection to the
- * server. If an user wants to maintain the connection to the server without
- * having to be on any irc channel of that server, he can just join this
- * dummy channel.
- * It’s not actually dummy because it’s useful and it does things, but well.
- */
-class DummyIrcChannel: public IrcChannel
-{
-public:
- explicit DummyIrcChannel();
- DummyIrcChannel(const DummyIrcChannel&) = delete;
- DummyIrcChannel(DummyIrcChannel&&) = delete;
- DummyIrcChannel& operator=(const DummyIrcChannel&) = delete;
- DummyIrcChannel& operator=(DummyIrcChannel&&) = delete;
-
- /**
- * This flag is at true whenever the user wants to join this channel, but
- * he is not yet connected to the server. When the connection is made, we
- * check that flag and if it’s true, we inform the user that he has just
- * joined that channel.
- * If the user is already connected to the server when he tries to join
- * the channel, we don’t use that flag, we just join it immediately.
- */
- bool joining;
-};
-
-
diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp
index 40078d9..0b5715e 100644
--- a/src/irc/irc_client.cpp
+++ b/src/irc/irc_client.cpp
@@ -135,7 +135,7 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
std::string realname, std::string user_hostname,
Bridge& bridge):
TCPClientSocketHandler(poller),
- hostname(std::move(hostname)),
+ hostname(hostname),
user_hostname(std::move(user_hostname)),
username(std::move(username)),
realname(std::move(realname)),
@@ -143,16 +143,15 @@ IrcClient::IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
bridge(bridge),
welcomed(false),
chanmodes({"", "", "", ""}),
- chantypes({'#', '&'})
-{
- this->dummy_channel.topic = "This is a virtual channel provided for "
- "convenience by biboumi, it is not connected "
- "to any actual IRC channel of the server '" + this->hostname +
- "', and sending messages in it has no effect. "
- "Its main goal is to keep the connection to the IRC server "
- "alive without having to join a real channel of that server. "
- "To disconnect from the IRC server, leave this room and all "
- "other IRC channels of that server.";
+ chantypes({'#', '&'}),
+ tokens_bucket(this->get_throttle_limit(), 1s, [this]() {
+ if (message_queue.empty())
+ return true;
+ this->actual_send(std::move(this->message_queue.front()));
+ this->message_queue.pop_front();
+ return false;
+ }, "TokensBucket" + this->hostname + this->bridge.get_jid())
+{
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
@@ -179,6 +178,7 @@ IrcClient::~IrcClient()
// This event may or may not exist (if we never got connected, it
// doesn't), but it's ok
TimedEventsManager::instance().cancel("PING" + this->hostname + this->bridge.get_jid());
+ TimedEventsManager::instance().cancel("TokensBucket" + this->hostname + this->bridge.get_jid());
}
void IrcClient::start()
@@ -194,20 +194,23 @@ void IrcClient::start()
bool tls;
std::tie(port, tls) = this->ports_to_try.top();
this->ports_to_try.pop();
- this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " +
- this->hostname + ":" + port + " (" +
- (tls ? "encrypted" : "not encrypted") + ")");
-
this->bind_addr = Config::get("outgoing_bind", "");
+ std::string address = this->hostname;
-#ifdef BOTAN_FOUND
-# ifdef USE_DATABASE
+#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
+# ifdef BOTAN_FOUND
this->credential_manager.set_trusted_fingerprint(options.col<Database::TrustedFingerprint>());
# endif
+ if (Config::get("fixed_irc_server", "").empty() &&
+ !options.col<Database::Address>().empty())
+ address = options.col<Database::Address>();
#endif
- this->connect(this->hostname, port, tls);
+ this->bridge.send_xmpp_message(this->hostname, "", "Connecting to " +
+ address + ":" + port + " (" +
+ (tls ? "encrypted" : "not encrypted") + ")");
+ this->connect(address, port, tls);
}
void IrcClient::on_connection_failed(const std::string& reason)
@@ -315,8 +318,6 @@ void IrcClient::on_connection_close(const std::string& error_msg)
IrcChannel* IrcClient::get_channel(const std::string& n)
{
- if (n.empty())
- return &this->dummy_channel;
const std::string name = utils::tolower(n);
try
{
@@ -324,9 +325,21 @@ IrcChannel* IrcClient::get_channel(const std::string& n)
}
catch (const std::out_of_range& exception)
{
- this->channels.emplace(name, std::make_unique<IrcChannel>());
+ return this->channels.emplace(name, std::make_unique<IrcChannel>()).first->second.get();
+ }
+}
+
+const IrcChannel* IrcClient::find_channel(const std::string& n) const
+{
+ const std::string name = utils::tolower(n);
+ try
+ {
return this->channels.at(name).get();
}
+ catch (const std::out_of_range& exception)
+ {
+ return nullptr;
+ }
}
bool IrcClient::is_channel_joined(const std::string& name)
@@ -385,25 +398,39 @@ void IrcClient::parse_in_buffer(const size_t)
}
}
-void IrcClient::send_message(IrcMessage&& message)
+void IrcClient::actual_send(std::pair<IrcMessage, MessageCallback> message_pair)
{
- log_debug("IRC SENDING: (", this->get_hostname(), ") ", message);
- std::string res;
- if (!message.prefix.empty())
- res += ":" + std::move(message.prefix) + " ";
- res += message.command;
- for (const std::string& arg: message.arguments)
- {
- if (arg.find(' ') != std::string::npos ||
- (!arg.empty() && arg[0] == ':'))
- {
- res += " :" + arg;
- break;
- }
- res += " " + arg;
- }
- res += "\r\n";
- this->send_data(std::move(res));
+ const IrcMessage& message = message_pair.first;
+ const MessageCallback& callback = message_pair.second;
+ log_debug("IRC SENDING: (", this->get_hostname(), ") ", message);
+ std::string res;
+ if (!message.prefix.empty())
+ res += ":" + message.prefix + " ";
+ res += message.command;
+ for (const std::string& arg: message.arguments)
+ {
+ if (arg.find(' ') != std::string::npos
+ || (!arg.empty() && arg[0] == ':'))
+ {
+ res += " :" + arg;
+ break;
+ }
+ res += " " + arg;
+ }
+ res += "\r\n";
+ this->send_data(std::move(res));
+
+ if (callback)
+ callback(this, message);
+ }
+
+void IrcClient::send_message(IrcMessage message, MessageCallback callback, bool throttle)
+{
+ auto message_pair = std::make_pair(std::move(message), std::move(callback));
+ if (this->tokens_bucket.use_token() || !throttle)
+ this->actual_send(std::move(message_pair));
+ else
+ message_queue.push_back(std::move(message_pair));
}
void IrcClient::send_raw(const std::string& txt)
@@ -454,12 +481,12 @@ void IrcClient::send_topic_command(const std::string& chan_name, const std::stri
void IrcClient::send_quit_command(const std::string& reason)
{
- this->send_message(IrcMessage("QUIT", {reason}));
+ this->send_message(IrcMessage("QUIT", {reason}), {}, false);
}
void IrcClient::send_join_command(const std::string& chan_name, const std::string& password)
{
- if (this->welcomed == false)
+ if (!this->welcomed)
{
const auto it = std::find_if(begin(this->channels_to_join), end(this->channels_to_join),
[&chan_name](const auto& pair) { return std::get<0>(pair) == chan_name; });
@@ -473,10 +500,11 @@ void IrcClient::send_join_command(const std::string& chan_name, const std::strin
this->start();
}
-bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body)
+bool IrcClient::send_channel_message(const std::string& chan_name, const std::string& body,
+ MessageCallback callback)
{
IrcChannel* channel = this->get_channel(chan_name);
- if (channel->joined == false)
+ if (!channel->joined)
{
log_warning("Cannot send message to channel ", chan_name, ", it is not joined");
return false;
@@ -496,7 +524,7 @@ bool IrcClient::send_channel_message(const std::string& chan_name, const std::st
::strlen(":!@ PRIVMSG ") - chan_name.length() - ::strlen(" :\r\n");
const auto lines = cut(body, line_size);
for (const auto& line: lines)
- this->send_message(IrcMessage("PRIVMSG", {chan_name, line}));
+ this->send_message(IrcMessage("PRIVMSG", {chan_name, line}), callback);
return true;
}
@@ -544,7 +572,9 @@ void IrcClient::send_ping_command()
void IrcClient::forward_server_message(const IrcMessage& message)
{
const std::string from = message.prefix;
- const std::string body = message.arguments[1];
+ std::string body;
+ for (auto it = std::next(message.arguments.begin()); it != message.arguments.end(); ++it)
+ body += *it + ' ';
this->bridge.send_xmpp_message(this->hostname, from, body);
}
@@ -647,6 +677,11 @@ void IrcClient::set_and_forward_user_list(const IrcMessage& message)
{
const std::string chan_name = utils::tolower(message.arguments[2]);
IrcChannel* channel = this->get_channel(chan_name);
+ if (channel->joined)
+ {
+ this->forward_server_message(message);
+ return;
+ }
std::vector<std::string> nicks = utils::split(message.arguments[3], ' ');
for (const std::string& nick: nicks)
{
@@ -670,10 +705,7 @@ void IrcClient::on_channel_join(const IrcMessage& message)
{
const std::string chan_name = utils::tolower(message.arguments[0]);
IrcChannel* channel;
- if (chan_name.empty())
- channel = &this->dummy_channel;
- else
- channel = this->get_channel(chan_name);
+ channel = this->get_channel(chan_name);
const std::string nick = message.prefix;
IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
if (channel->joined == false)
@@ -785,6 +817,16 @@ void IrcClient::on_channel_completely_joined(const IrcMessage& message)
{
const std::string chan_name = utils::tolower(message.arguments[1]);
IrcChannel* channel = this->get_channel(chan_name);
+ if (chan_name == "*" || channel->joined)
+ {
+ this->forward_server_message(message);
+ return;
+ }
+ if (!channel->get_self())
+ {
+ log_error("End of NAMES list but we never received our own nick.");
+ return;
+ }
channel->joined = true;
this->bridge.send_user_join(this->hostname, chan_name, channel->get_self(),
channel->get_self()->get_most_significant_mode(this->sorted_user_modes), true);
@@ -900,8 +942,9 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
#ifdef USE_DATABASE
auto options = Database::get_irc_server_options(this->bridge.get_bare_jid(),
this->get_hostname());
- if (!options.col<Database::AfterConnectionCommand>().empty())
- this->send_raw(options.col<Database::AfterConnectionCommand>());
+ const auto commands = Database::get_after_connection_commands(options);
+ for (const auto& command: commands)
+ this->send_raw(command.col<Database::AfterConnectionCommand>());
#endif
// Install a repeated events to regularly send a PING
TimedEventsManager::instance().add_event(TimedEvent(240s, std::bind(&IrcClient::send_ping_command, this),
@@ -948,18 +991,6 @@ void IrcClient::on_welcome_message(const IrcMessage& message)
if (!channels_with_key.empty())
this->send_join_command(channels_with_key, keys);
this->channels_to_join.clear();
- // Indicate that the dummy channel is joined as well, if needed
- if (this->dummy_channel.joining)
- {
- // Simulate a message coming from the IRC server saying that we joined
- // the channel
- const IrcMessage join_message(this->get_nick(), "JOIN", {""});
- this->on_channel_join(join_message);
- const IrcMessage end_join_message(std::string(this->hostname), "366",
- {this->get_nick(),
- "", "End of NAMES list"});
- this->on_channel_completely_joined(end_join_message);
- }
}
void IrcClient::on_part(const IrcMessage& message)
@@ -976,18 +1007,18 @@ void IrcClient::on_part(const IrcMessage& message)
{
std::string nick = user->nick;
bool self = channel->get_self() && channel->get_self()->nick == nick;
- channel->remove_user(user);
- Iid iid;
- iid.set_local(chan_name);
- iid.set_server(this->hostname);
- iid.type = Iid::Type::Channel;
+ auto user_ptr = channel->remove_user(user);
if (self)
{
this->channels.erase(utils::tolower(chan_name));
// channel pointer is now invalid
channel = nullptr;
}
- this->bridge.send_muc_leave(iid, std::move(nick), txt, self, true);
+ Iid iid;
+ iid.set_local(chan_name);
+ iid.set_server(this->hostname);
+ iid.type = Iid::Type::Channel;
+ this->bridge.send_muc_leave(iid, *user_ptr, txt, self, true, {}, this);
}
}
@@ -1004,8 +1035,7 @@ void IrcClient::on_error(const IrcMessage& message)
IrcChannel* channel = pair.second.get();
if (!channel->joined)
continue;
- std::string own_nick = channel->get_self()->nick;
- this->bridge.send_muc_leave(iid, std::move(own_nick), leave_message, true, false);
+ this->bridge.send_muc_leave(iid, *channel->get_self(), leave_message, true, false, {}, this);
}
this->channels.clear();
this->send_gateway_message("ERROR: " + leave_message);
@@ -1030,7 +1060,7 @@ void IrcClient::on_quit(const IrcMessage& message)
iid.set_local(chan_name);
iid.set_server(this->hostname);
iid.type = Iid::Type::Channel;
- this->bridge.send_muc_leave(iid, user->nick, txt, self, false);
+ this->bridge.send_muc_leave(iid, *user, txt, self, false, {}, this);
channel->remove_user(user);
}
}
@@ -1062,10 +1092,6 @@ void IrcClient::on_nick(const IrcMessage& message)
}
};
- if (this->get_dummy_channel().joined)
- {
- change_nick_func("", &this->get_dummy_channel());
- }
for (const auto& pair: this->channels)
{
change_nick_func(pair.first, pair.second.get());
@@ -1132,8 +1158,6 @@ void IrcClient::on_channel_bad_key(const IrcMessage& message)
void IrcClient::on_channel_mode(const IrcMessage& message)
{
- // For now, just transmit the modes so the user can know what happens
- // TODO, actually interprete the mode.
Iid iid;
iid.set_local(message.arguments[0]);
iid.set_server(this->hostname);
@@ -1151,7 +1175,7 @@ void IrcClient::on_channel_mode(const IrcMessage& message)
}
this->bridge.send_message(iid, "", "Mode " + iid.get_local() +
" [" + mode_arguments + "] by " + user.nick,
- true);
+ true, this->is_channel_joined(iid.get_local()));
const IrcChannel* channel = this->get_channel(iid.get_local());
if (!channel)
return;
@@ -1224,6 +1248,11 @@ void IrcClient::on_channel_mode(const IrcMessage& message)
}
}
+void IrcClient::set_throttle_limit(long int limit)
+{
+ this->tokens_bucket.set_limit(limit);
+}
+
void IrcClient::on_user_mode(const IrcMessage& message)
{
this->bridge.send_xmpp_message(this->hostname, "",
@@ -1248,25 +1277,7 @@ void IrcClient::on_unknown_message(const IrcMessage& message)
size_t IrcClient::number_of_joined_channels() const
{
- if (this->dummy_channel.joined)
- return this->channels.size() + 1;
- else
- return this->channels.size();
-}
-
-DummyIrcChannel& IrcClient::get_dummy_channel()
-{
- return this->dummy_channel;
-}
-
-void IrcClient::leave_dummy_channel(const std::string& exit_message, const std::string& resource)
-{
- if (!this->dummy_channel.joined)
- return;
- this->dummy_channel.joined = false;
- this->dummy_channel.joining = false;
- this->dummy_channel.remove_all_users();
- this->bridge.send_muc_leave(Iid("%" + this->hostname, this->chantypes), std::string(this->current_nick), exit_message, true, true, resource);
+ return this->channels.size();
}
#ifdef BOTAN_FOUND
@@ -1279,3 +1290,12 @@ bool IrcClient::abort_on_invalid_cert() const
return true;
}
#endif
+
+long int IrcClient::get_throttle_limit() const
+{
+#ifdef USE_DATABASE
+ return Database::get_irc_server_options(this->bridge.get_bare_jid(), this->hostname).col<Database::ThrottleLimit>();
+#else
+ return 10;
+#endif
+}
diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp
index de5c520..674f3ff 100644
--- a/src/irc/irc_client.hpp
+++ b/src/irc/irc_client.hpp
@@ -16,8 +16,14 @@
#include <vector>
#include <string>
#include <stack>
+#include <deque>
#include <map>
#include <set>
+#include <utils/tokens_bucket.hpp>
+
+class IrcClient;
+
+using MessageCallback = std::function<void(const IrcClient*, const IrcMessage&)>;
class Bridge;
@@ -28,7 +34,7 @@ class Bridge;
class IrcClient: public TCPClientSocketHandler
{
public:
- explicit IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
+ explicit IrcClient(std::shared_ptr<Poller>& poller, std::string hostname,
std::string nickname, std::string username,
std::string realname, std::string user_hostname,
Bridge& bridge);
@@ -68,6 +74,10 @@ public:
*/
IrcChannel* get_channel(const std::string& name);
/**
+ * Return the channel with this name. Nullptr if it is not found
+ */
+ const IrcChannel* find_channel(const std::string& name) const;
+ /**
* Returns true if the channel is joined
*/
bool is_channel_joined(const std::string& name);
@@ -80,8 +90,9 @@ public:
* (actually, into our out_buf and signal the poller that we want to wach
* for send events to be ready)
*/
- void send_message(IrcMessage&& message);
+ void send_message(IrcMessage message, MessageCallback callback={}, bool throttle=true);
void send_raw(const std::string& txt);
+ void actual_send(std::pair<IrcMessage, MessageCallback> message_pair);
/**
* Send the PONG irc command
*/
@@ -110,7 +121,8 @@ public:
* Send a PRIVMSG command for a channel
* Return true if the message was actually sent
*/
- bool send_channel_message(const std::string& chan_name, const std::string& body);
+ bool send_channel_message(const std::string& chan_name, const std::string& body,
+ MessageCallback callback);
/**
* Send a PRIVMSG command for an user
*/
@@ -279,15 +291,6 @@ public:
* Return the number of joined channels
*/
size_t number_of_joined_channels() const;
- /**
- * Get a reference to the unique dummy channel
- */
- DummyIrcChannel& get_dummy_channel();
- /**
- * Leave the dummy channel: forward a message to the user to indicate that
- * he left it, and mark it as not joined.
- */
- void leave_dummy_channel(const std::string& exit_message, const std::string& resource);
const std::string& get_hostname() const { return this->hostname; }
std::string get_nick() const { return this->current_nick; }
@@ -298,7 +301,7 @@ public:
const std::vector<char>& get_sorted_user_modes() const { return this->sorted_user_modes; }
std::set<char> get_chantypes() const { return this->chantypes; }
-
+ void set_throttle_limit(long int limit);
/**
* Store the history limit that the client asked when joining this room.
*/
@@ -336,14 +339,13 @@ private:
*/
Bridge& bridge;
/**
- * The list of joined channels, indexed by name
+ * Where messaged are stored when they are throttled.
*/
- std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
+ std::deque<std::pair<IrcMessage, MessageCallback>> message_queue{};
/**
- * A single channel with a iid of the form "hostname" (normal channel have
- * an iid of the form "chan%hostname".
+ * The list of joined channels, indexed by name
*/
- DummyIrcChannel dummy_channel;
+ std::unordered_map<std::string, std::unique_ptr<IrcChannel>> channels;
/**
* A list of chan we want to join (tuples with the channel name and the
* password, if any), but we need a response 001 from the server before
@@ -399,6 +401,8 @@ private:
* the WebIRC protocole.
*/
Resolver dns_resolver;
+ TokensBucket tokens_bucket;
+ long int get_throttle_limit() const;
};
diff --git a/src/irc/irc_message.hpp b/src/irc/irc_message.hpp
index fe954e4..269a12a 100644
--- a/src/irc/irc_message.hpp
+++ b/src/irc/irc_message.hpp
@@ -14,9 +14,9 @@ public:
~IrcMessage() = default;
IrcMessage(const IrcMessage&) = delete;
- IrcMessage(IrcMessage&&) = delete;
+ IrcMessage(IrcMessage&&) = default;
IrcMessage& operator=(const IrcMessage&) = delete;
- IrcMessage& operator=(IrcMessage&&) = delete;
+ IrcMessage& operator=(IrcMessage&&) = default;
std::string prefix;
std::string command;
diff --git a/src/main.cpp b/src/main.cpp
index c877e43..2448197 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -55,45 +55,8 @@ static void sigusr_handler(int, siginfo_t*, void*)
reload.store(true);
}
-int main(int ac, char** av)
+static void setup_signals()
{
- if (ac > 1)
- {
- const std::string arg = av[1];
- if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-')
- {
- if (arg == "--help")
- return display_help();
- else
- {
- std::cerr << "Unknow command line option: " << arg << std::endl;
- return 1;
- }
- }
- }
- const std::string conf_filename = ac > 1 ? av[1] : xdg_config_path("biboumi.cfg");
- std::cout << "Using configuration file: " << conf_filename << std::endl;
-
- if (!Config::read_conf(conf_filename))
- return config_help("");
-
- const std::string password = Config::get("password", "");
- if (password.empty())
- return config_help("password");
- const std::string hostname = Config::get("hostname", "");
- if (hostname.empty())
- return config_help("hostname");
-
-
-#ifdef USE_DATABASE
- try {
- open_database();
- } catch (const std::exception& e) {
- log_error(e.what());
- return 1;
- }
-#endif
-
// Block the signals we want to manage. They will be unblocked only during
// the epoll_pwait or ppoll calls. This avoids some race conditions,
// explained in man 2 pselect on linux
@@ -103,6 +66,7 @@ int main(int ac, char** av)
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGUSR2);
+ sigaddset(&mask, SIGHUP);
sigprocmask(SIG_BLOCK, &mask, nullptr);
// Install the signals used to exit the process cleanly, or reload the
@@ -113,7 +77,7 @@ int main(int ac, char** av)
sigfillset(&on_sigint.sa_mask);
// we want to catch that signal only once.
// Sending SIGINT again will "force" an exit
- on_sigint.sa_flags = SA_RESETHAND;
+ on_sigint.sa_flags = 0 & SA_RESETHAND;
sigaction(SIGINT, &on_sigint, nullptr);
sigaction(SIGTERM, &on_sigint, nullptr);
@@ -124,7 +88,11 @@ int main(int ac, char** av)
on_sigusr.sa_flags = 0;
sigaction(SIGUSR1, &on_sigusr, nullptr);
sigaction(SIGUSR2, &on_sigusr, nullptr);
+ sigaction(SIGHUP, &on_sigusr, nullptr);
+}
+static int main_loop(std::string hostname, std::string password)
+{
auto p = std::make_shared<Poller>();
#ifdef UDNS_FOUND
@@ -135,7 +103,9 @@ int main(int ac, char** av)
std::make_shared<BiboumiComponent>(p, hostname, password);
xmpp_component->start();
- IdentdServer identd(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
+ std::unique_ptr<IdentdServer> identd;
+ if (Config::get_int("identd_port", 113) != 0)
+ identd = std::make_unique<IdentdServer>(*xmpp_component, p, static_cast<uint16_t>(Config::get_int("identd_port", 113)));
auto timeout = TimedEventsManager::instance().get_timeout();
while (p->poll(timeout) != -1)
@@ -144,7 +114,8 @@ int main(int ac, char** av)
// Check for empty irc_clients (not connected, or with no joined
// channel) and remove them
xmpp_component->clean();
- identd.clean();
+ if (identd)
+ identd->clean();
if (stop)
{
log_info("Signal received, exiting...");
@@ -157,7 +128,8 @@ int main(int ac, char** av)
#ifdef UDNS_FOUND
dns_handler.destroy();
#endif
- identd.shutdown();
+ if (identd)
+ identd->shutdown();
// Cancel the timer for a potential reconnection
TimedEventsManager::instance().cancel("XMPP reconnection");
}
@@ -199,7 +171,8 @@ int main(int ac, char** av)
#ifdef UDNS_FOUND
dns_handler.destroy();
#endif
- identd.shutdown();
+ if (identd)
+ identd->shutdown();
}
}
// If the only existing connection is the one to the XMPP component:
@@ -218,3 +191,51 @@ int main(int ac, char** av)
log_info("All connections cleanly closed, have a nice day.");
return 0;
}
+
+int main(int ac, char** av)
+{
+ if (ac > 1)
+ {
+ const std::string arg = av[1];
+ if (arg.size() >= 2 && arg[0] == '-' && arg[1] == '-')
+ {
+ if (arg == "--help")
+ return display_help();
+ else
+ {
+ std::cerr << "Unknow command line option: " << arg
+ << std::endl;
+ return 1;
+ }
+ }
+ }
+ const std::string conf_filename =
+ ac > 1 ? av[1]: xdg_config_path("biboumi.cfg");
+ std::cout << "Using configuration file: " << conf_filename << std::endl;
+
+ if (!Config::read_conf(conf_filename))
+ return config_help("");
+
+ const std::string password = Config::get("password", "");
+ if (password.empty())
+ return config_help("password");
+ const std::string hostname = Config::get("hostname", "");
+ if (hostname.empty())
+ return config_help("hostname");
+
+#ifdef USE_DATABASE
+ try
+ {
+ open_database();
+ }
+ catch (const std::exception& e)
+ {
+ log_error(e.what());
+ return 1;
+ }
+#endif
+
+ setup_signals();
+
+ return main_loop(std::move(hostname), std::move(password));
+}
diff --git a/src/network/credentials_manager.cpp b/src/network/credentials_manager.cpp
index b25f442..89c694c 100644
--- a/src/network/credentials_manager.cpp
+++ b/src/network/credentials_manager.cpp
@@ -21,9 +21,8 @@ static const std::vector<std::string> default_cert_files = {
Botan::Certificate_Store_In_Memory BasicCredentialsManager::certificate_store;
bool BasicCredentialsManager::certs_loaded = false;
-BasicCredentialsManager::BasicCredentialsManager(const TCPSocketHandler* const socket_handler):
+BasicCredentialsManager::BasicCredentialsManager():
Botan::Credentials_Manager(),
- socket_handler(socket_handler),
trusted_fingerprint{}
{
BasicCredentialsManager::load_certs();
diff --git a/src/network/credentials_manager.hpp b/src/network/credentials_manager.hpp
index 3a37bdc..210a628 100644
--- a/src/network/credentials_manager.hpp
+++ b/src/network/credentials_manager.hpp
@@ -25,7 +25,7 @@ void check_tls_certificate(const std::vector<Botan::X509_Certificate>& certs,
class BasicCredentialsManager: public Botan::Credentials_Manager
{
public:
- BasicCredentialsManager(const TCPSocketHandler* const socket_handler);
+ BasicCredentialsManager();
BasicCredentialsManager(BasicCredentialsManager&&) = delete;
BasicCredentialsManager(const BasicCredentialsManager&) = delete;
@@ -38,7 +38,6 @@ public:
const std::string& get_trusted_fingerprint() const;
private:
- const TCPSocketHandler* const socket_handler;
static bool try_to_open_one_ca_bundle(const std::vector<std::string>& paths);
static void load_certs();
diff --git a/src/network/tcp_client_socket_handler.cpp b/src/network/tcp_client_socket_handler.cpp
index aac13d0..9dda73d 100644
--- a/src/network/tcp_client_socket_handler.cpp
+++ b/src/network/tcp_client_socket_handler.cpp
@@ -146,15 +146,22 @@ void TCPClientSocketHandler::connect(const std::string& address, const std::stri
|| errno == EISCONN)
{
log_info("Connection success.");
+#ifdef BOTAN_FOUND
+ if (this->use_tls)
+ try {
+ this->start_tls(this->address, this->port);
+ } catch (const Botan::Exception& e)
+ {
+ this->on_connection_failed("TLS error: "s + e.what());
+ this->close();
+ return ;
+ }
+#endif
TimedEventsManager::instance().cancel("connection_timeout" +
std::to_string(this->socket));
this->poller->add_socket_handler(this);
this->connected = true;
this->connecting = false;
-#ifdef BOTAN_FOUND
- if (this->use_tls)
- this->start_tls(this->address, this->port);
-#endif
this->connection_date = std::chrono::system_clock::now();
// Get our local TCP port and store it
diff --git a/src/network/tcp_socket_handler.cpp b/src/network/tcp_socket_handler.cpp
index 642cf03..e05caad 100644
--- a/src/network/tcp_socket_handler.cpp
+++ b/src/network/tcp_socket_handler.cpp
@@ -50,7 +50,7 @@ TCPSocketHandler::TCPSocketHandler(std::shared_ptr<Poller>& poller):
SocketHandler(poller, -1),
use_tls(false)
#ifdef BOTAN_FOUND
- ,credential_manager(this)
+ ,credential_manager()
#endif
{}
@@ -84,10 +84,11 @@ void TCPSocketHandler::plain_recv()
if (recv_buf == nullptr)
recv_buf = buf;
- const ssize_t size = this->do_recv(recv_buf, buf_size);
+ const ssize_t ssize = this->do_recv(recv_buf, buf_size);
- if (size > 0)
+ if (ssize > 0)
{
+ auto size = static_cast<std::size_t>(ssize);
if (buf == recv_buf)
{
// data needs to be placed in the in_buf string, because no buffer
@@ -149,21 +150,22 @@ void TCPSocketHandler::on_send()
}
else
{
+ auto size = static_cast<std::size_t>(res);
// remove all the strings that were successfully sent.
auto it = this->out_buf.begin();
while (it != this->out_buf.end())
{
- if (static_cast<size_t>(res) >= it->size())
+ if (size >= it->size())
{
- res -= it->size();
+ size -= it->size();
++it;
}
else
{
// If one string has partially been sent, we use substr to
// crop it
- if (res > 0)
- *it = it->substr(res, std::string::npos);
+ if (size > 0)
+ *it = it->substr(size, std::string::npos);
break;
}
}
@@ -332,6 +334,11 @@ void TCPSocketHandler::tls_verify_cert_chain(const std::vector<Botan::X509_Certi
Botan::Usage_Type usage, const std::string& hostname,
const Botan::TLS::Policy& policy)
{
+ if (!this->policy.verify_certificate)
+ {
+ log_debug("Not verifying certificate due to domain policy ");
+ return;
+ }
log_debug("Checking remote certificate for hostname ", hostname);
try
{
diff --git a/src/network/tls_policy.cpp b/src/network/tls_policy.cpp
index b88eb88..f32557e 100644
--- a/src/network/tls_policy.cpp
+++ b/src/network/tls_policy.cpp
@@ -37,6 +37,8 @@ void BiboumiTLSPolicy::load(std::istream& is)
// Workaround for options that are not overridden in Botan::TLS::Text_Policy
if (pair.first == "require_cert_revocation_info")
this->req_cert_revocation_info = !(pair.second == "0" || utils::tolower(pair.second) == "false");
+ else if (pair.first == "verify_certificate")
+ this->verify_certificate = !(pair.second == "0" || utils::tolower(pair.second) == "false");
else
this->set(pair.first, pair.second);
}
diff --git a/src/network/tls_policy.hpp b/src/network/tls_policy.hpp
index 29fd2b3..e915646 100644
--- a/src/network/tls_policy.hpp
+++ b/src/network/tls_policy.hpp
@@ -21,6 +21,7 @@ public:
BiboumiTLSPolicy &operator=(BiboumiTLSPolicy &&) = delete;
bool require_cert_revocation_info() const override;
+ bool verify_certificate{true};
protected:
bool req_cert_revocation_info{true};
};
diff --git a/src/utils/dirname.cpp b/src/utils/dirname.cpp
index 71c9c38..a304117 100644
--- a/src/utils/dirname.cpp
+++ b/src/utils/dirname.cpp
@@ -2,7 +2,7 @@
namespace utils
{
- std::string dirname(const std::string filename)
+ std::string dirname(const std::string& filename)
{
if (filename.empty())
return "./";
diff --git a/src/utils/dirname.hpp b/src/utils/dirname.hpp
index c1df81b..73e1b57 100644
--- a/src/utils/dirname.hpp
+++ b/src/utils/dirname.hpp
@@ -2,5 +2,5 @@
namespace utils
{
-std::string dirname(const std::string filename);
+std::string dirname(const std::string& filename);
}
diff --git a/src/utils/encoding.cpp b/src/utils/encoding.cpp
index cff0039..8532292 100644
--- a/src/utils/encoding.cpp
+++ b/src/utils/encoding.cpp
@@ -48,16 +48,16 @@ namespace utils
if (codepoint_size == 4)
{
if (!str[1] || !str[2] || !str[3]
- || ((str[1] & 0b11000000) != 0b10000000)
- || ((str[2] & 0b11000000) != 0b10000000)
- || ((str[3] & 0b11000000) != 0b10000000))
+ || ((str[1] & 0b11000000u) != 0b10000000u)
+ || ((str[2] & 0b11000000u) != 0b10000000u)
+ || ((str[3] & 0b11000000u) != 0b10000000u))
return false;
}
else if (codepoint_size == 3)
{
if (!str[1] || !str[2]
- || ((str[1] & 0b11000000) != 0b10000000)
- || ((str[2] & 0b11000000) != 0b10000000))
+ || ((str[1] & 0b11000000u) != 0b10000000u)
+ || ((str[2] & 0b11000000u) != 0b10000000u))
return false;
}
else if (codepoint_size == 2)
@@ -81,7 +81,7 @@ namespace utils
// pointer where we write valid chars
char* r = res.data();
- const char* str = original.c_str();
+ const unsigned char* str = reinterpret_cast<const unsigned char*>(original.c_str());
std::bitset<20> codepoint;
while (*str)
@@ -89,10 +89,10 @@ namespace utils
// 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
if ((str[0] & 0b11111000) == 0b11110000)
{
- codepoint = ((str[0] & 0b00000111) << 18);
- codepoint |= ((str[1] & 0b00111111) << 12);
- codepoint |= ((str[2] & 0b00111111) << 6 );
- codepoint |= ((str[3] & 0b00111111) << 0 );
+ codepoint = ((str[0] & 0b00000111u) << 18u);
+ codepoint |= ((str[1] & 0b00111111u) << 12u);
+ codepoint |= ((str[2] & 0b00111111u) << 6u );
+ codepoint |= ((str[3] & 0b00111111u) << 0u );
if (codepoint.to_ulong() <= 0x10FFFF)
{
::memcpy(r, str, 4);
@@ -103,9 +103,9 @@ namespace utils
// 3 bytes: 1110xxx 10xxxxxx 10xxxxxx
else if ((str[0] & 0b11110000) == 0b11100000)
{
- codepoint = ((str[0] & 0b00001111) << 12);
- codepoint |= ((str[1] & 0b00111111) << 6);
- codepoint |= ((str[2] & 0b00111111) << 0 );
+ codepoint = ((str[0] & 0b00001111u) << 12u);
+ codepoint |= ((str[1] & 0b00111111u) << 6u);
+ codepoint |= ((str[2] & 0b00111111u) << 0u );
if (codepoint.to_ulong() <= 0xD7FF ||
(codepoint.to_ulong() >= 0xE000 && codepoint.to_ulong() <= 0xFFFD))
{
diff --git a/src/utils/optional_bool.hpp b/src/utils/optional_bool.hpp
index 867aca2..3d00d23 100644
--- a/src/utils/optional_bool.hpp
+++ b/src/utils/optional_bool.hpp
@@ -6,7 +6,7 @@ struct OptionalBool
{
OptionalBool() = default;
- OptionalBool(bool value):
+ explicit OptionalBool(bool value):
is_set(true), value(value) {}
void set_value(bool value)
diff --git a/src/utils/string.cpp b/src/utils/string.cpp
index 635e71a..366ec1f 100644
--- a/src/utils/string.cpp
+++ b/src/utils/string.cpp
@@ -15,11 +15,11 @@ std::vector<std::string> cut(const std::string& val, const std::size_t size)
// Get the number of chars, <= size, that contain only whole
// UTF-8 codepoints.
std::size_t s = 0;
- auto codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ auto codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s]));
while (s + codepoint_size <= size && pos + s < val.size())
{
s += codepoint_size;
- codepoint_size = utils::get_next_codepoint_size(val[pos + s]);
+ codepoint_size = utils::get_next_codepoint_size(static_cast<unsigned char>(val[pos + s]));
}
res.emplace_back(val.substr(pos, s));
pos += s;
diff --git a/src/utils/tokens_bucket.hpp b/src/utils/tokens_bucket.hpp
new file mode 100644
index 0000000..263359a
--- /dev/null
+++ b/src/utils/tokens_bucket.hpp
@@ -0,0 +1,60 @@
+/**
+ * Implementation of the token bucket algorithm.
+ *
+ * It uses a repetitive TimedEvent, started at construction, to fill the
+ * bucket.
+ *
+ * Every n seconds, it executes the given callback. If the callback
+ * returns true, we add a token (if the limit is not yet reached).
+ *
+ */
+
+#pragma once
+
+#include <utils/timed_events.hpp>
+#include <logger/logger.hpp>
+
+class TokensBucket
+{
+public:
+ TokensBucket(long int max_size, std::chrono::milliseconds fill_duration, std::function<bool()> callback, std::string name):
+ limit(max_size),
+ tokens(static_cast<std::size_t>(limit)),
+ callback(std::move(callback))
+ {
+ log_debug("creating TokensBucket with max size: ", max_size);
+ TimedEvent event(std::move(fill_duration), [this]() { this->add_token(); }, std::move(name));
+ TimedEventsManager::instance().add_event(std::move(event));
+ }
+
+ bool use_token()
+ {
+ if (this->limit < 0)
+ return true;
+ if (this->tokens > 0)
+ {
+ this->tokens--;
+ return true;
+ }
+ else
+ return false;
+ }
+
+ void set_limit(long int limit)
+ {
+ this->limit = limit;
+ }
+
+private:
+ long int limit;
+ std::size_t tokens;
+ std::function<bool()> callback;
+
+ void add_token()
+ {
+ if (this->limit < 0)
+ return;
+ if (this->callback() && this->tokens != static_cast<decltype(this->tokens)>(this->limit))
+ this->tokens++;
+ }
+};
diff --git a/src/utils/uuid.cpp b/src/utils/uuid.cpp
new file mode 100644
index 0000000..23b71fe
--- /dev/null
+++ b/src/utils/uuid.cpp
@@ -0,0 +1,14 @@
+#include <utils/uuid.hpp>
+#include <uuid/uuid.h>
+
+namespace utils
+{
+std::string gen_uuid()
+{
+ char uuid_str[37];
+ uuid_t uuid;
+ uuid_generate(uuid);
+ uuid_unparse(uuid, uuid_str);
+ return uuid_str;
+}
+}
diff --git a/src/utils/uuid.hpp b/src/utils/uuid.hpp
new file mode 100644
index 0000000..d550475
--- /dev/null
+++ b/src/utils/uuid.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <string>
+
+namespace utils
+{
+std::string gen_uuid();
+}
diff --git a/src/xmpp/adhoc_commands_handler.cpp b/src/xmpp/adhoc_commands_handler.cpp
index bb48781..bc4c108 100644
--- a/src/xmpp/adhoc_commands_handler.cpp
+++ b/src/xmpp/adhoc_commands_handler.cpp
@@ -41,7 +41,7 @@ XmlNode AdhocCommandsHandler::handle_request(const std::string& executor_jid, co
XmlSubNode condition(error, STANZA_NS":item-not-found");
}
else if (command_it->second.is_admin_only() &&
- Config::get("admin", "") != jid.local + "@" + jid.domain)
+ !Config::is_in_list("admin", jid.bare()))
{
XmlSubNode error(command_node, ADHOC_NS":error");
error["type"] = "cancel";
diff --git a/src/xmpp/biboumi_adhoc_commands.cpp b/src/xmpp/biboumi_adhoc_commands.cpp
index bcdac39..7c31f36 100644
--- a/src/xmpp/biboumi_adhoc_commands.cpp
+++ b/src/xmpp/biboumi_adhoc_commands.cpp
@@ -14,6 +14,14 @@
#ifdef USE_DATABASE
#include <database/database.hpp>
+#include <database/save.hpp>
+
+static void set_desc(XmlSubNode& field, const char* text)
+{
+ XmlSubNode desc(field, "desc");
+ desc.set_inner(text);
+}
+
#endif
#ifndef HAS_PUT_TIME
@@ -115,6 +123,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
auto options = Database::get_global_options(owner.bare());
+ command_node.delete_all_children();
XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
XmlSubNode title(x, "title");
@@ -127,7 +136,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
max_histo_length["var"] = "max_history_length";
max_histo_length["type"] = "text-single";
max_histo_length["label"] = "Max history length";
- max_histo_length["desc"] = "The maximum number of lines in the history that the server sends when joining a channel";
+ set_desc(max_histo_length, "The maximum number of lines in the history that the server sends when joining a channel");
{
XmlSubNode value(max_histo_length, "value");
value.set_inner(std::to_string(options.col<Database::MaxHistoryLength>()));
@@ -139,7 +148,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
record_history["var"] = "record_history";
record_history["type"] = "boolean";
record_history["label"] = "Record history";
- record_history["desc"] = "Whether to save the messages into the database, or not";
+ set_desc(record_history, "Whether to save the messages into the database, or not");
{
XmlSubNode value(record_history, "value");
value.set_name("value");
@@ -155,7 +164,7 @@ void ConfigureGlobalStep1(XmppComponent&, AdhocSession& session, XmlNode& comman
persistent["var"] = "persistent";
persistent["type"] = "boolean";
persistent["label"] = "Make all channels persistent";
- persistent["desc"] = "If true, all channels will be persistent";
+ set_desc(persistent, "If true, all channels will be persistent");
{
XmlSubNode value(persistent, "value");
value.set_name("value");
@@ -176,6 +185,7 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
{
const Jid owner(session.get_owner_jid());
auto options = Database::get_global_options(owner.bare());
+ options.clear();
for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
{
const XmlNode* value = field->get_child("value", "jabber:x:data");
@@ -196,7 +206,7 @@ void ConfigureGlobalStep2(XmppComponent& xmpp_component, AdhocSession& session,
options.col<Database::GlobalPersistent>() = to_bool(value->get_inner());
}
- options.save(Database::db);
+ save(options, *Database::db);
command_node.delete_all_children();
XmlSubNode note(command_node, "note");
@@ -219,7 +229,9 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
server_domain = target.local;
auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
server_domain);
+ auto commands = Database::get_after_connection_commands(options);
+ command_node.delete_all_children();
XmlSubNode x(command_node, "jabber:x:data:x");
x["type"] = "form";
XmlSubNode title(x, "title");
@@ -227,12 +239,26 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode instructions(x, "instructions");
instructions.set_inner("Edit the form, to configure the settings of the IRC server " + server_domain);
+ if (Config::get("fixed_irc_server", "").empty())
+ {
+ XmlSubNode field(x, "field");
+ field["var"] = "address";
+ field["type"] = "text-single";
+ field["label"] = "Address";
+ set_desc(field, "The address (hostname or IP) to connect to.");
+ XmlSubNode value(field, "value");
+ if (options.col<Database::Address>().empty())
+ value.set_inner(server_domain);
+ else
+ value.set_inner(options.col<Database::Address>());
+ }
+
{
XmlSubNode ports(x, "field");
ports["var"] = "ports";
ports["type"] = "text-multi";
ports["label"] = "Ports";
- ports["desc"] = "List of ports to try, without TLS. Defaults: 6667.";
+ set_desc(ports, "List of ports to try, without TLS. Defaults: 6667.");
for (const auto& val: utils::split(options.col<Database::Ports>(), ';', false))
{
XmlSubNode ports_value(ports, "value");
@@ -246,7 +272,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
tls_ports["var"] = "tls_ports";
tls_ports["type"] = "text-multi";
tls_ports["label"] = "TLS ports";
- tls_ports["desc"] = "List of ports to try, with TLS. Defaults: 6697, 6670.";
+ set_desc(tls_ports, "List of ports to try, with TLS. Defaults: 6697, 6670.");
for (const auto& val: utils::split(options.col<Database::TlsPorts>(), ';', false))
{
XmlSubNode tls_ports_value(tls_ports, "value");
@@ -259,7 +285,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
verify_cert["var"] = "verify_cert";
verify_cert["type"] = "boolean";
verify_cert["label"] = "Verify certificate";
- verify_cert["desc"] = "Whether or not to abort the connection if the server’s TLS certificate is invalid";
+ set_desc(verify_cert, "Whether or not to abort the connection if the server’s TLS certificate is invalid");
XmlSubNode verify_cert_value(verify_cert, "value");
if (options.col<Database::VerifyCert>())
verify_cert_value.set_inner("true");
@@ -279,12 +305,26 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
}
}
#endif
+
+ {
+ XmlSubNode field(x, "field");
+ field["var"] = "nick";
+ field["type"] = "text-single";
+ field["label"] = "Nickname";
+ set_desc(field, "If set, will override the nickname provided in the initial presence sent to join the first server channel");
+ if (!options.col<Database::Nick>().empty())
+ {
+ XmlSubNode value(field, "value");
+ value.set_inner(options.col<Database::Nick>());
+ }
+ }
+
{
XmlSubNode pass(x, "field");
pass["var"] = "pass";
pass["type"] = "text-private";
pass["label"] = "Server password";
- pass["desc"] = "Will be used in a PASS command when connecting";
+ set_desc(pass, "Will be used in a PASS command when connecting");
if (!options.col<Database::Pass>().empty())
{
XmlSubNode pass_value(pass, "value");
@@ -294,14 +334,14 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
{
XmlSubNode after_cnt_cmd(x, "field");
- after_cnt_cmd["var"] = "after_connect_command";
- after_cnt_cmd["type"] = "text-single";
- after_cnt_cmd["desc"] = "Custom IRC command sent after the connection is established with the server.";
- after_cnt_cmd["label"] = "After-connection IRC command";
- if (!options.col<Database::AfterConnectionCommand>().empty())
+ after_cnt_cmd["var"] = "after_connect_commands";
+ after_cnt_cmd["type"] = "text-multi";
+ set_desc(after_cnt_cmd, "Custom IRC commands sent after the connection is established with the server.");
+ after_cnt_cmd["label"] = "After-connection IRC commands";
+ for (const auto& command: commands)
{
XmlSubNode after_cnt_cmd_value(after_cnt_cmd, "value");
- after_cnt_cmd_value.set_inner(options.col<Database::AfterConnectionCommand>());
+ after_cnt_cmd_value.set_inner(command.col<Database::AfterConnectionCommand>());
}
}
@@ -333,10 +373,19 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
}
{
+ XmlSubNode throttle_limit(x, "field");
+ throttle_limit["var"] = "throttle_limit";
+ throttle_limit["type"] = "text-single";
+ throttle_limit["label"] = "Throttle limit";
+ XmlSubNode value(throttle_limit, "value");
+ value.set_inner(std::to_string(options.col<Database::ThrottleLimit>()));
+ }
+
+ {
XmlSubNode encoding_out(x, "field");
encoding_out["var"] = "encoding_out";
encoding_out["type"] = "text-single";
- encoding_out["desc"] = "The encoding used when sending messages to the IRC server.";
+ set_desc(encoding_out, "The encoding used when sending messages to the IRC server.");
encoding_out["label"] = "Out encoding";
if (!options.col<Database::EncodingOut>().empty())
{
@@ -349,7 +398,7 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
XmlSubNode encoding_in(x, "field");
encoding_in["var"] = "encoding_in";
encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server.";
+ set_desc(encoding_in, "The encoding used to decode message received from the IRC server.");
encoding_in["label"] = "In encoding";
if (!options.col<Database::EncodingIn>().empty())
{
@@ -359,8 +408,10 @@ void ConfigureIrcServerStep1(XmppComponent&, AdhocSession& session, XmlNode& com
}
}
-void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& command_node)
+void ConfigureIrcServerStep2(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
{
+ auto& biboumi_component = dynamic_cast<BiboumiComponent&>(xmpp_component);
+
const XmlNode* x = command_node.get_child("x", "jabber:x:data");
if (x)
{
@@ -371,10 +422,17 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
server_domain = target.local;
auto options = Database::get_irc_server_options(owner.local + "@" + owner.domain,
server_domain);
+ options.clear();
+ Database::AfterConnectionCommands commands{};
+
for (const XmlNode* field: x->get_children("field", "jabber:x:data"))
{
const XmlNode* value = field->get_child("value", "jabber:x:data");
const std::vector<const XmlNode*> values = field->get_children("value", "jabber:x:data");
+
+ if (field->get_tag("var") == "address" && value && Config::get("fixed_irc_server", "").empty())
+ options.col<Database::Address>() = value->get_inner();
+
if (field->get_tag("var") == "ports")
{
std::string ports;
@@ -406,11 +464,22 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
#endif // BOTAN_FOUND
+ else if (field->get_tag("var") == "nick" && value)
+ options.col<Database::Nick>() = value->get_inner();
+
else if (field->get_tag("var") == "pass" && value)
options.col<Database::Pass>() = value->get_inner();
- else if (field->get_tag("var") == "after_connect_command" && value)
- options.col<Database::AfterConnectionCommand>() = value->get_inner();
+ else if (field->get_tag("var") == "after_connect_commands")
+ {
+ commands.clear();
+ for (const auto& val: values)
+ {
+ auto command = Database::after_connection_commands.row();
+ command.col<Database::AfterConnectionCommand>() = val->get_inner();
+ commands.push_back(std::move(command));
+ }
+ }
else if (field->get_tag("var") == "username" && value)
{
@@ -423,6 +492,22 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
else if (field->get_tag("var") == "realname" && value)
options.col<Database::Realname>() = value->get_inner();
+ else if (field->get_tag("var") == "throttle_limit" && value)
+ {
+ try {
+ options.col<Database::ThrottleLimit>() = std::stol(value->get_inner());
+ } catch (const std::logic_error&) {
+ options.col<Database::ThrottleLimit>() = -1;
+ }
+ Bridge* bridge = biboumi_component.find_user_bridge(session.get_owner_jid());
+ if (bridge)
+ {
+ IrcClient* client = bridge->find_irc_client(server_domain);
+ if (client)
+ client->set_throttle_limit(options.col<Database::ThrottleLimit>());
+ }
+ }
+
else if (field->get_tag("var") == "encoding_out" && value)
options.col<Database::EncodingOut>() = value->get_inner();
@@ -431,7 +516,8 @@ void ConfigureIrcServerStep2(XmppComponent&, AdhocSession& session, XmlNode& com
}
Database::invalidate_encoding_in_cache();
- options.save(Database::db);
+ save(options, *Database::db);
+ Database::set_after_connection_commands(options, commands);
command_node.delete_all_children();
XmlSubNode note(command_node, "note");
@@ -459,6 +545,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
auto options = Database::get_irc_channel_options_with_server_default(requester.local + "@" + requester.domain,
iid.get_server(), iid.get_local());
+ node.delete_all_children();
XmlSubNode x(node, "jabber:x:data:x");
x["type"] = "form";
XmlSubNode title(x, "title");
@@ -471,7 +558,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
record_history["var"] = "record_history";
record_history["type"] = "list-single";
record_history["label"] = "Record history for this channel";
- record_history["desc"] = "If unset, the value is the one configured globally";
+ set_desc(record_history, "If unset, the value is the one configured globally");
{
// Value selected by default
XmlSubNode value(record_history, "value");
@@ -491,7 +578,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
XmlSubNode encoding_out(x, "field");
encoding_out["var"] = "encoding_out";
encoding_out["type"] = "text-single";
- encoding_out["desc"] = "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel";
+ set_desc(encoding_out, "The encoding used when sending messages to the IRC server. Defaults to the server's “out encoding” if unset for the channel");
encoding_out["label"] = "Out encoding";
if (!options.col<Database::EncodingOut>().empty())
{
@@ -504,7 +591,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
XmlSubNode encoding_in(x, "field");
encoding_in["var"] = "encoding_in";
encoding_in["type"] = "text-single";
- encoding_in["desc"] = "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel";
+ set_desc(encoding_in, "The encoding used to decode message received from the IRC server. Defaults to the server's “in encoding” if unset for the channel");
encoding_in["label"] = "In encoding";
if (!options.col<Database::EncodingIn>().empty())
{
@@ -517,7 +604,7 @@ void insert_irc_channel_configuration_form(XmlNode& node, const Jid& requester,
XmlSubNode persistent(x, "field");
persistent["var"] = "persistent";
persistent["type"] = "boolean";
- persistent["desc"] = "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command.";
+ set_desc(persistent, "If set to true, when all XMPP clients have left this channel, biboumi will stay idle in it, without sending a PART command.");
persistent["label"] = "Persistent";
{
XmlSubNode value(persistent, "value");
@@ -561,6 +648,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
const Iid iid(target.local, {});
auto options = Database::get_irc_channel_options(requester.bare(),
iid.get_server(), iid.get_local());
+ options.clear();
for (const XmlNode *field: x->get_children("field", "jabber:x:data"))
{
const XmlNode *value = field->get_child("value", "jabber:x:data");
@@ -600,7 +688,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
}
Database::invalidate_encoding_in_cache(requester.bare(), iid.get_server(), iid.get_local());
- options.save(Database::db);
+ save(options, *Database::db);
}
return true;
}
@@ -611,7 +699,7 @@ bool handle_irc_channel_configuration_form(XmppComponent& xmpp_component, const
void DisconnectUserFromServerStep1(XmppComponent& xmpp_component, AdhocSession& session, XmlNode& command_node)
{
const Jid owner(session.get_owner_jid());
- if (owner.bare() != Config::get("admin", ""))
+ if (!Config::is_in_list("admin", owner.bare()))
{ // A non-admin is not allowed to disconnect other users, only
// him/herself, so we just skip this step
auto next_step = session.get_next_step();
diff --git a/src/xmpp/biboumi_component.cpp b/src/xmpp/biboumi_component.cpp
index 481ebb9..85617e8 100644
--- a/src/xmpp/biboumi_component.cpp
+++ b/src/xmpp/biboumi_component.cpp
@@ -72,11 +72,16 @@ BiboumiComponent::BiboumiComponent(std::shared_ptr<Poller>& poller, const std::s
AdhocCommand configure_global_command({&ConfigureGlobalStep1, &ConfigureGlobalStep2}, "Configure a few settings", false);
if (!Config::get("fixed_irc_server", "").empty())
- this->adhoc_commands_handler.add_command("configure", configure_server_command);
+ {
+ this->adhoc_commands_handler.add_command("server-configure", configure_server_command);
+ this->adhoc_commands_handler.add_command("global-configure", configure_global_command);
+ }
else
- this->adhoc_commands_handler.add_command("configure", configure_global_command);
+ {
+ this->adhoc_commands_handler.add_command("configure", configure_global_command);
+ this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command);
+ }
- this->irc_server_adhoc_commands_handler.add_command("configure", configure_server_command);
this->irc_channel_adhoc_commands_handler.add_command("configure", {{&ConfigureIrcChannelStep1, &ConfigureIrcChannelStep2}, "Configure a few settings for that IRC channel", false});
#endif
}
@@ -97,8 +102,8 @@ void BiboumiComponent::shutdown()
void BiboumiComponent::clean()
{
- auto it = this->bridges.begin();
- while (it != this->bridges.end())
+ auto it = std::begin(this->bridges);
+ while (it != std::end(this->bridges))
{
it->second->clean();
if (it->second->active_clients() == 0)
@@ -148,13 +153,10 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
try {
if (iid.type == Iid::Type::Channel && !iid.get_server().empty())
- { // presence toward a MUC that corresponds to an irc channel, or a
- // dummy channel if iid.chan is empty
+ { // presence toward a MUC that corresponds to an irc channel
if (type.empty())
{
const std::string own_nick = bridge->get_own_nick(iid);
- if (!own_nick.empty() && own_nick != to.resource)
- bridge->send_irc_nick_change(iid, to.resource, from.resource);
const XmlNode* x = stanza.get_child("x", MUC_NS);
const XmlNode* password = x ? x->get_child("password", MUC_NS): nullptr;
const XmlNode* history = x ? x->get_child("history", MUC_NS): nullptr;
@@ -182,7 +184,9 @@ void BiboumiComponent::handle_presence(const Stanza& stanza)
history_limit.stanzas = 0;
}
bridge->join_irc_channel(iid, to.resource, password ? password->get_inner(): "",
- from.resource, history_limit);
+ from.resource, history_limit, x != nullptr);
+ if (!own_nick.empty() && own_nick != to.resource)
+ bridge->send_irc_nick_change(iid, to.resource, from.resource);
}
else if (type == "unavailable")
{
@@ -269,9 +273,10 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
std::string error_type("cancel");
std::string error_name("internal-server-error");
- utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name](){
+ std::string error_text{};
+ utils::ScopeGuard stanza_error([this, &from_str, &to_str, &id, &error_type, &error_name, &error_text](){
this->send_stanza_error("message", from_str, to_str, id,
- error_type, error_name, "");
+ error_type, error_name, error_text);
});
const XmlNode* body = stanza.get_child("body", COMPONENT_NS);
@@ -280,7 +285,15 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
{
if (body && !body->get_inner().empty())
{
- bridge->send_channel_message(iid, body->get_inner());
+ if (bridge->is_resource_in_chan(iid.to_tuple(), from.resource))
+ bridge->send_channel_message(iid, body->get_inner(), id);
+ else
+ {
+ error_type = "modify";
+ error_name = "not-acceptable";
+ error_text = "You are not a participant in this room.";
+ return;
+ }
}
const XmlNode* subject = stanza.get_child("subject", COMPONENT_NS);
if (subject)
@@ -346,7 +359,6 @@ void BiboumiComponent::handle_message(const Stanza& stanza)
this->send_invitation_from_fulljid(std::to_string(iid), invite_to, from_str);
}
}
-
}
} catch (const IRCNotConnected& ex)
{
@@ -467,8 +479,13 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
#ifdef USE_DATABASE
else if ((query = stanza.get_child("query", MAM_NS)))
{
- if (this->handle_mam_request(stanza))
- stanza_error.disable();
+ try {
+ if (this->handle_mam_request(stanza))
+ stanza_error.disable();
+ } catch (const Database::RecordNotFound& exc) {
+ error_name = "item-not-found";
+ return;
+ }
}
else if ((query = stanza.get_child("query", MUC_OWNER_NS)))
{
@@ -505,7 +522,11 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
{
if (node.empty())
{
- this->send_irc_channel_disco_info(id, from, to_str);
+ const IrcClient* irc_client = bridge->find_irc_client(iid.get_server());
+ const IrcChannel* irc_channel{};
+ if (irc_client)
+ irc_channel = irc_client->find_channel(iid.get_local());
+ this->send_irc_channel_disco_info(id, from, to_str, irc_channel);
stanza_error.disable();
}
else if (node == MUC_TRAFFIC_NS)
@@ -547,24 +568,21 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
if (to.local.empty())
{ // Get biboumi's adhoc commands
this->send_adhoc_commands_list(id, from, this->served_hostname,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->adhoc_commands_handler);
stanza_error.disable();
}
else if (iid.type == Iid::Type::Server)
{ // Get the server's adhoc commands
this->send_adhoc_commands_list(id, from, to_str,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->irc_server_adhoc_commands_handler);
stanza_error.disable();
}
else if (iid.type == Iid::Type::Channel && to.resource.empty())
{ // Get the channel's adhoc commands
this->send_adhoc_commands_list(id, from, to_str,
- (Config::get("admin", "") ==
- from_jid.bare()),
+ Config::is_in_list("admin", from_jid.bare()),
this->irc_channel_adhoc_commands_handler);
stanza_error.disable();
}
@@ -586,7 +604,6 @@ void BiboumiComponent::handle_iq(const Stanza& stanza)
const XmlNode* max = set_node->get_child("max", RSM_NS);
if (max)
rs_info.max = std::atoi(max->get_inner().data());
-
}
if (rs_info.max == -1)
rs_info.max = 100;
@@ -715,32 +732,47 @@ bool BiboumiComponent::handle_mam_request(const Stanza& stanza)
}
const XmlNode* set = query->get_child("set", RSM_NS);
int limit = -1;
+ Id::real_type reference_record_id{Id::unset_value};
+ Database::Paging paging_order{Database::Paging::first};
if (set)
{
const XmlNode* max = set->get_child("max", RSM_NS);
if (max)
limit = std::atoi(max->get_inner().data());
+ const XmlNode* after = set->get_child("after", RSM_NS);
+ if (after)
+ {
+ auto after_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(),
+ after->get_inner(), start, end);
+ reference_record_id = after_record.col<Id>();
+ }
+ const XmlNode* before = set->get_child("before", RSM_NS);
+ if (before)
+ {
+ paging_order = Database::Paging::last;
+ if (!before->get_inner().empty())
+ {
+ auto before_record = Database::get_muc_log(from.bare(), iid.get_local(), iid.get_server(), before->get_inner(), start, end);
+ reference_record_id = before_record.col<Id>();
+ }
+ }
}
- // Do send more than 100 messages, even if the client asked for more,
+ // Do not send more than 100 messages, even if the client asked for more,
// or if it didn’t specify any limit.
- // 101 is just a trick to know if there are more available messages.
- // If our query returns 101 message, we know it’s incomplete, but we
- // still send only 100
- if ((limit == -1 && start.empty() && end.empty())
- || limit > 100)
- limit = 101;
- auto lines = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(), limit, start, end);
- bool complete = true;
- if (lines.size() > 100)
+ if (limit < 0 || limit > 100)
+ limit = 100;
+ auto result = Database::get_muc_logs(from.bare(), iid.get_local(), iid.get_server(),
+ static_cast<std::size_t>(limit),
+ start, end,
+ reference_record_id, paging_order);
+ bool complete = std::get<bool>(result);
+ auto& lines = std::get<1>(result);
+
+ for (const Database::MucLogLine& line: lines)
{
- complete = false;
- lines.erase(lines.begin(), std::prev(lines.end(), 100));
+ if (!line.col<Database::Nick>().empty())
+ this->send_archived_message(line, to.full(), from.full(), query_id);
}
- for (const Database::MucLogLine& line: lines)
- {
- if (!line.col<Database::Nick>().empty())
- this->send_archived_message(line, to.full(), from.full(), query_id);
- }
{
auto fin_ptr = std::make_unique<XmlNode>("fin");
{
@@ -892,7 +924,7 @@ void BiboumiComponent::send_self_disco_info(const std::string& id, const std::st
identity["category"] = "conference";
identity["type"] = "irc";
identity["name"] = "Biboumi XMPP-IRC gateway";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
@@ -916,7 +948,7 @@ void BiboumiComponent::send_irc_server_disco_info(const std::string& id, const s
identity["category"] = "conference";
identity["type"] = "irc";
identity["name"] = "IRC server " + from.local + " over Biboumi";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
@@ -943,7 +975,8 @@ void BiboumiComponent::send_irc_channel_muc_traffic_info(const std::string& id,
this->send_stanza(iq);
}
-void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from)
+void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const std::string& jid_to,
+ const std::string& jid_from, const IrcChannel* irc_channel)
{
Jid from(jid_from);
Iid iid(from.local, {});
@@ -958,12 +991,32 @@ void BiboumiComponent::send_irc_channel_disco_info(const std::string& id, const
XmlSubNode identity(query, "identity");
identity["category"] = "conference";
identity["type"] = "irc";
- identity["name"] = "IRC channel " + iid.get_local() + " from server " + iid.get_server() + " over biboumi";
- for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS})
+ identity["name"] = ""s + iid.get_local() + " on " + iid.get_server();
+ for (const char *ns: {DISCO_INFO_NS, MUC_NS, ADHOC_NS, PING_NS, MAM_NS, VERSION_NS, STABLE_MUC_ID_NS})
{
XmlSubNode feature(query, "feature");
feature["var"] = ns;
}
+
+ XmlSubNode x(query, "x");
+ x["xmlns"] = DATAFORM_NS;
+ x["type"] = "result";
+ {
+ XmlSubNode field(x, "field");
+ field["var"] = "FORM_TYPE";
+ field["type"] = "hidden";
+ XmlSubNode value(field, "value");
+ value.set_inner("http://jabber.org/protocol/muc#roominfo");
+ }
+
+ if (irc_channel && irc_channel->joined)
+ {
+ XmlSubNode field(x, "field");
+ field["var"] = "muc#roominfo_occupants";
+ field["label"] = "Number of occupants";
+ XmlSubNode value(field, "value");
+ value.set_inner(std::to_string(irc_channel->get_users().size()));
+ }
}
this->send_stanza(iq);
}
diff --git a/src/xmpp/biboumi_component.hpp b/src/xmpp/biboumi_component.hpp
index caf990e..f59ed9b 100644
--- a/src/xmpp/biboumi_component.hpp
+++ b/src/xmpp/biboumi_component.hpp
@@ -73,7 +73,8 @@ public:
* http://xmpp.org/extensions/xep-0045.html#impl-service-traffic
*/
void send_irc_channel_muc_traffic_info(const std::string& id, const std::string& jid_to, const std::string& jid_from);
- void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from);
+ void send_irc_channel_disco_info(const std::string& id, const std::string& jid_to, const std::string& jid_from,
+ const IrcChannel* irc_channel);
/**
* Send a ping request
*/
diff --git a/src/xmpp/jid.cpp b/src/xmpp/jid.cpp
index 19d1b55..3c54fd4 100644
--- a/src/xmpp/jid.cpp
+++ b/src/xmpp/jid.cpp
@@ -106,7 +106,7 @@ std::string jidprep(const std::string& original)
--domain_end;
if (domain_end != domain && special_chars.count(domain[0]))
{
- std::memmove(domain, domain + 1, domain_end - domain + 1);
+ std::memmove(domain, domain + 1, static_cast<std::size_t>(domain_end - domain) + 1);
--domain_end;
}
// And if the final result is an empty string, return a dummy hostname
diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp
index 9be9e34..b3d925e 100644
--- a/src/xmpp/xmpp_component.cpp
+++ b/src/xmpp/xmpp_component.cpp
@@ -2,6 +2,7 @@
#include <utils/scopeguard.hpp>
#include <utils/tolower.hpp>
#include <logger/logger.hpp>
+#include <utils/uuid.hpp>
#include <xmpp/xmpp_component.hpp>
#include <config/config.hpp>
@@ -14,8 +15,6 @@
#include <iostream>
#include <set>
-#include <uuid/uuid.h>
-
#include <cstdlib>
#include <set>
@@ -364,10 +363,11 @@ void XmppComponent::send_topic(const std::string& from, Xmpp::body&& topic, cons
this->send_stanza(message);
}
-void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid)
+void XmppComponent::send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& xmpp_body, const std::string& jid_to, std::string uuid, std::string id)
{
Stanza message("message");
message["to"] = jid_to;
+ message["id"] = std::move(id);
if (!nick.empty())
message["from"] = muc_name + "@" + this->served_hostname + "/" + nick;
else // Message from the room itself
@@ -425,7 +425,8 @@ void XmppComponent::send_history_message(const std::string& muc_name, const std:
#endif
void XmppComponent::send_muc_leave(const std::string& muc_name, const std::string& nick, Xmpp::body&& message,
- const std::string& jid_to, const bool self, const bool user_requested)
+ const std::string& jid_to, const bool self, const bool user_requested,
+ const std::string& affiliation, const std::string& role)
{
Stanza presence("presence");
{
@@ -447,6 +448,9 @@ void XmppComponent::send_muc_leave(const std::string& muc_name, const std::strin
status["code"] = "332";
}
}
+ XmlSubNode item(x, "item");
+ item["affiliation"] = affiliation;
+ item["role"] = role;
if (!message_str.empty())
{
XmlSubNode status(presence, "status");
@@ -669,9 +673,5 @@ void XmppComponent::send_iq_result(const std::string& id, const std::string& to_
std::string XmppComponent::next_id()
{
- char uuid_str[37];
- uuid_t uuid;
- uuid_generate(uuid);
- uuid_unparse(uuid, uuid_str);
- return uuid_str;
+ return utils::gen_uuid();
}
diff --git a/src/xmpp/xmpp_component.hpp b/src/xmpp/xmpp_component.hpp
index 1daa6fb..e18da40 100644
--- a/src/xmpp/xmpp_component.hpp
+++ b/src/xmpp/xmpp_component.hpp
@@ -37,6 +37,7 @@
#define RSM_NS "http://jabber.org/protocol/rsm"
#define MUC_TRAFFIC_NS "http://jabber.org/protocol/muc#traffic"
#define STABLE_ID_NS "urn:xmpp:sid:0"
+#define STABLE_MUC_ID_NS "http://jabber.org/protocol/muc#stable_id"
/**
* An XMPP component, communicating with an XMPP server using the protocole
@@ -134,7 +135,7 @@ public:
* Send a (non-private) message to the MUC
*/
void send_muc_message(const std::string& muc_name, const std::string& nick, Xmpp::body&& body, const std::string& jid_to,
- std::string uuid);
+ std::string uuid, std::string id);
#ifdef USE_DATABASE
/**
* Send a message, with a <delay/> element, part of a MUC history
@@ -150,7 +151,8 @@ public:
Xmpp::body&& message,
const std::string& jid_to,
const bool self,
- const bool user_requested);
+ const bool user_requested,
+ const std::string& affiliation, const std::string& role);
/**
* Indicate that a participant changed his nick
*/
diff --git a/src/xmpp/xmpp_parser.cpp b/src/xmpp/xmpp_parser.cpp
index 0488be9..781fe4c 100644
--- a/src/xmpp/xmpp_parser.cpp
+++ b/src/xmpp/xmpp_parser.cpp
@@ -20,7 +20,7 @@ static void end_element_handler(void* user_data, const XML_Char* name)
static void character_data_handler(void *user_data, const XML_Char *s, int len)
{
- static_cast<XmppParser*>(user_data)->char_data(s, len);
+ static_cast<XmppParser*>(user_data)->char_data(s, static_cast<std::size_t>(len));
}
/**
diff --git a/tests/database.cpp b/tests/database.cpp
index 7ab6da8..070a460 100644
--- a/tests/database.cpp
+++ b/tests/database.cpp
@@ -7,6 +7,7 @@
#include <cstdlib>
#include <database/database.hpp>
+#include <database/save.hpp>
#include <config/config.hpp>
@@ -28,11 +29,11 @@ TEST_CASE("Database")
{
auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
CHECK(Database::count(Database::irc_server_options) == 0);
- o.save(Database::db);
+ save(o, *Database::db);
CHECK(Database::count(Database::irc_server_options) == 1);
o.col<Database::Realname>() = "Different realname";
CHECK(o.col<Database::Realname>() == "Different realname");
- o.save(Database::db);
+ save(o, *Database::db);
CHECK(o.col<Database::Realname>() == "Different realname");
CHECK(Database::count(Database::irc_server_options) == 1);
@@ -44,7 +45,7 @@ TEST_CASE("Database")
// inserted
CHECK(1 == Database::count(Database::irc_server_options));
- b.save(Database::db);
+ save(b, *Database::db);
CHECK(2 == Database::count(Database::irc_server_options));
CHECK(b.col<Database::Pass>() == "");
@@ -58,11 +59,15 @@ TEST_CASE("Database")
o.col<Database::EncodingIn>() = "ISO-8859-1";
CHECK(o.col<Database::RecordHistoryOptional>().is_set == false);
o.col<Database::RecordHistoryOptional>().set_value(false);
- o.save(Database::db);
+ save(o, *Database::db);
auto b = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");
CHECK(o.col<Database::EncodingIn>() == "ISO-8859-1");
CHECK(o.col<Database::RecordHistoryOptional>().is_set == true);
CHECK(o.col<Database::RecordHistoryOptional>().value == false);
+
+ o.clear();
+ CHECK(o.col<Database::EncodingIn>() == "");
+ CHECK(o.col<Database::Owner>() == "zouzou@example.com");
}
SECTION("Channel options with server default")
@@ -77,7 +82,7 @@ TEST_CASE("Database")
GIVEN("An option defined for the channel but not the server")
{
c.col<Database::EncodingIn>() = "channelEncoding";
- c.save(Database::db);
+ save(c, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -88,7 +93,7 @@ TEST_CASE("Database")
GIVEN("An option defined for the server but not the channel")
{
s.col<Database::EncodingIn>() = "serverEncoding";
- s.save(Database::db);
+ save(s, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -99,9 +104,9 @@ TEST_CASE("Database")
GIVEN("An option defined for both the server and the channel")
{
s.col<Database::EncodingIn>() = "serverEncoding";
- s.save(Database::db);
+ save(s, *Database::db);
c.col<Database::EncodingIn>() = "channelEncoding";
- c.save(Database::db);
+ save(c, *Database::db);
WHEN("we fetch that option")
{
auto r = Database::get_irc_channel_options_with_server_default(owner, server, chan1);
@@ -117,6 +122,49 @@ TEST_CASE("Database")
}
}
+ SECTION("Server options")
+ {
+ const std::string owner{"toto@example.com"};
+ const std::string owner2{"toto2@example.com"};
+ const std::string server{"irc.example.com"};
+
+ auto soptions = Database::get_irc_server_options(owner, server);
+ auto soptions2 = Database::get_irc_server_options(owner2, server);
+
+ auto after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.empty());
+
+ save(soptions, *Database::db);
+ save(soptions2, *Database::db);
+ auto com = Database::after_connection_commands.row();
+ com.col<Database::AfterConnectionCommand>() = "first";
+ after_connection_commands.push_back(com);
+ com.col<Database::AfterConnectionCommand>() = "second";
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions, after_connection_commands);
+
+ after_connection_commands.clear();
+ com.col<Database::AfterConnectionCommand>() = "first";
+ after_connection_commands.push_back(com);
+ com.col<Database::AfterConnectionCommand>() = "second";
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions2, after_connection_commands);
+
+ after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.size() == 2);
+ after_connection_commands = Database::get_after_connection_commands(soptions2);
+ CHECK(after_connection_commands.size() == 2);
+
+ after_connection_commands.clear();
+ after_connection_commands.push_back(com);
+ Database::set_after_connection_commands(soptions, after_connection_commands);
+
+ after_connection_commands = Database::get_after_connection_commands(soptions);
+ CHECK(after_connection_commands.size() == 1);
+ after_connection_commands = Database::get_after_connection_commands(soptions2);
+ CHECK(after_connection_commands.size() == 2);
+ }
+
Database::close();
}
#endif
diff --git a/tests/end_to_end/__main__.py b/tests/end_to_end/__main__.py
index c4c149a..d57f375 100644
--- a/tests/end_to_end/__main__.py
+++ b/tests/end_to_end/__main__.py
@@ -152,7 +152,7 @@ def check_xpath(xpaths, xmpp, after, stanza):
xpath = xpath[1:]
matched = match(stanza, xpath)
if (expected and not matched) or (not expected and matched):
- raise StanzaError("Received stanza “%s” did not match expected xpath “%s”" % (stanza, real_xpath))
+ raise StanzaError("Received stanza\n%s\ndid not match expected xpath\n%s" % (stanza, real_xpath))
if after:
if isinstance(after, collections.Iterable):
for af in after:
@@ -270,11 +270,13 @@ def send_stanza(stanza, xmpp, biboumi):
def expect_stanza(xpaths, xmpp, biboumi, optional=False, after=None):
+ replacements = common_replacements
+ replacements.update(xmpp.saved_values)
check_func = check_xpath if not optional else check_xpath_optional
if isinstance(xpaths, str):
- xmpp.stanza_checker = partial(check_func, [xpaths.format_map(common_replacements)], xmpp, after)
+ xmpp.stanza_checker = partial(check_func, [xpaths.format_map(replacements)], xmpp, after)
elif isinstance(xpaths, tuple):
- xmpp.stanza_checker = partial(check_func, [xpath.format_map(common_replacements) for xpath in xpaths], xmpp, after)
+ xmpp.stanza_checker = partial(check_func, [xpath.format_map(replacements) for xpath in xpaths], xmpp, after)
else:
print("Warning, from argument type passed to expect_stanza: %s" % (type(xpaths)))
@@ -331,6 +333,8 @@ class BiboumiTest:
except FileNotFoundError:
pass
+ start_datetime = datetime.datetime.now()
+
# Start the XMPP component and biboumi
biboumi = BiboumiRunner(self.scenario.name)
xmpp = XMPPComponent(self.scenario, biboumi)
@@ -342,13 +346,16 @@ class BiboumiTest:
code = asyncio.get_event_loop().run_until_complete(biboumi.wait())
xmpp.biboumi = None
self.scenario.steps.clear()
+
+ delta = datetime.datetime.now() - start_datetime
+
failed = False
if not xmpp.failed:
if code != self.expected_code:
xmpp.error("Wrong return code from biboumi's process: %d" % (code,))
failed = True
else:
- print("Success!")
+ print("Success! ({}s)".format(round(delta.total_seconds(), 2)))
else:
failed = True
@@ -606,6 +613,23 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
+ Scenario("raw_names_command",
+ [
+ handshake_sequence(),
+ 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"),
+ partial(expect_stanza,
+ "/presence/muc_user:x/muc_user:status[@code='110']"
+ ),
+ partial(expect_stanza, "/message/subject[not(text())]"),
+ partial(send_stanza,
+ "<message type='chat' from='{jid_one}/{resource_one}' to='{irc_server_one}'><body>NAMES</body></message>"),
+ partial(expect_stanza, "/message/body[text()='irc.localhost: = #foo @{nick_one} ']"),
+ partial(expect_stanza, "/message/body[text()='irc.localhost: * End of /NAMES list. ']"),
+ ]),
Scenario("quit",
[
handshake_sequence(),
@@ -661,23 +685,6 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#baz%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
- Scenario("virtual_channel",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_one}']"),
- ]),
Scenario("not_connected_error",
[
handshake_sequence(),
@@ -694,40 +701,39 @@ if __name__ == '__main__':
),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
]),
- Scenario("irc_server_disconnection",
+ Scenario("channel_join_with_two_users",
[
handshake_sequence(),
+ # First user joins
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
+ "/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='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
+ partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
partial(expect_unordered, [
- ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='{nick_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']",
- "/presence/muc_user:x/muc_user:status[@code='303']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
]),
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"),
]),
- Scenario("channel_join_with_two_users",
+ Scenario("channel_force_join",
[
handshake_sequence(),
# First user joins
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
partial(expect_stanza,
"/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
@@ -739,7 +745,7 @@ if __name__ == '__main__':
# Second user joins
partial(send_stanza,
- "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
partial(expect_unordered, [
("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
@@ -748,6 +754,31 @@ if __name__ == '__main__':
"/presence/muc_user:x/muc_user:status[@code='110']",),
("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
]),
+
+ # Here we simulate a desynchronization of a client: The client thinks it’s
+ # disconnected from the room, but biboumi still thinks it’s in the room. The
+ # client thus sends a join presence, and biboumi should send everything
+ # (user list, history, etc) in response.
+ partial(send_stanza,
+ "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_three}'><x xmlns='http://jabber.org/protocol/muc'/></presence>"),
+
+ partial(expect_unordered, [
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant'][@jid='~bobby@localhost']",),
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",
+ "/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
+ ]),
+ # And also, that was not the same nickname
+ partial(expect_unordered, [
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']",
+ "/presence/muc_user:x/muc_user:status[@code='303']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_two}/{resource_one}']",),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bernard']",
+ "/presence/muc_user:x/muc_user:status[@code='303']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_three}'][@to='{jid_one}/{resource_one}']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ]),
]),
Scenario("channel_join_with_password",
[
@@ -865,28 +896,35 @@ if __name__ == '__main__':
handshake_sequence(),
partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[3]")),
+ "/iq/disco_items:query/disco_items:item[@node='configure']",
+ "/iq/disco_items:query/disco_items:item[4]",
+ "!/iq/disco_items:query/disco_items:item[5]")),
]),
Scenario("list_admin_adhoc",
[
handshake_sequence(),
partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[5]")),
+ "/iq/disco_items:query/disco_items:item[6]",
+ "!/iq/disco_items:query/disco_items:item[7]")),
]),
Scenario("list_adhoc_fixed_server",
[
handshake_sequence(),
partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[5]")),
+ "/iq/disco_items:query/disco_items:item[@node='global-configure']",
+ "/iq/disco_items:query/disco_items:item[@node='server-configure']",
+ "/iq/disco_items:query/disco_items:item[6]",
+ "!/iq/disco_items:query/disco_items:item[7]")),
], conf='fixed_server'),
Scenario("list_admin_adhoc_fixed_server",
[
handshake_sequence(),
partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[5]")),
+ "/iq/disco_items:query/disco_items:item[8]",
+ "!/iq/disco_items:query/disco_items:item[9]")),
], conf='fixed_server'),
Scenario("list_adhoc_irc",
[
@@ -895,20 +933,6 @@ if __name__ == '__main__':
partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
"/iq/disco_items:query/disco_items:item[2]")),
]),
- Scenario("list_adhoc_irc_fixed_server",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_one}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[4]")),
- ], conf='fixed_server'),
- Scenario("list_admin_adhoc_irc_fixed_server",
- [
- handshake_sequence(),
- partial(send_stanza, "<iq type='get' id='idwhatever' from='{jid_admin}/{resource_one}' to='{biboumi_host}'><query xmlns='http://jabber.org/protocol/disco#items' node='http://jabber.org/protocol/commands' /></iq>"),
- partial(expect_stanza, ("/iq[@type='result']/disco_items:query[@node='http://jabber.org/protocol/commands']",
- "/iq/disco_items:query/disco_items:item[6]")),
- ], conf='fixed_server'),
Scenario("list_muc_user_adhoc",
[
handshake_sequence(),
@@ -1083,13 +1107,12 @@ if __name__ == '__main__':
("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",),
("/presence[@to='{jid_one}/{resource_two}'][@from='#foo%{irc_server_one}/{nick_two}']",),
("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']",),
+ "/presence/muc_user:x/muc_user:status[@code='110']",),
("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']",),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",),
]
),
- partial(expect_stanza, "/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]"),
-
# That second user sends a private message to the first one
partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='chat'><body>RELLO</body></message>"),
# Message is received with a server-wide JID, by the two resources behind nick_one
@@ -1192,7 +1215,8 @@ if __name__ == '__main__':
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' type='unavailable' />"),
# Only user 1 receives the unavailable presence
partial(expect_stanza,
- "/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:status[@code='110']",
+ "/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']")),
# Second user sends a channel message
partial(send_stanza, "<message type='groupchat' from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}'><body>coucou</body></message>"),
@@ -1244,6 +1268,80 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='{irc_server_one}' type='chat'><body>NOTICE {nick_one} :[#foo] Hello in a notice.</body></message>"),
partial(expect_stanza, "/message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/body[text()='[notice] [#foo] Hello in a notice.']"),
]),
+ Scenario("multiline_message",
+ [
+ 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'][@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 a multi-line channel message
+ partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>un\ndeux\ntrois</body></message>"),
+ # Receive multiple messages, in order
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='un']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='deux']"),
+ partial(expect_stanza,
+ "/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='trois']"),
+
+ # Send a simple message, with no id
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>hello</body></message>"),
+
+ # Expect a non-empty id as a result (should be a uuid)
+ partial(expect_stanza,
+ ("!/message[@id='']/body[text()='hello']",
+ "/message[@id]/body[text()='hello']")),
+
+ # even though we reflect the message to XMPP only
+ # when we send it to IRC, there’s still a race
+ # condition if the XMPP client receives the
+ # reflection (and the IRC server didn’t yet receive
+ # it), then the new user joins the room, and then
+ # finally the IRC server sends the message to “all
+ # participants of the channel”, including the new
+ # one, that was not supposed to be there when the
+ # message was sent in the first place by the first
+ # XMPP user. There’s nothing we can do about it until
+ # all servers support the echo-message IRCv3
+ # extension… So, we just sleep a little bit before
+ # joining the room with the new user.
+ partial(sleep_for, 1),
+
+ # Second user joins
+ partial(send_stanza,
+ "<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
+ connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
+ # Our presence, sent to the other user
+ partial(expect_unordered, [
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
+ ("/presence[@to='{jid_two}/{resource_one}'][@from='#foo%{irc_server_one}/{nick_two}']/muc_user:x/muc_user:item[@affiliation='none'][@jid='~bobby@localhost'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/message[@from='#foo%{irc_server_one}'][@type='groupchat']/subject[not(text())]",)
+ ]),
+
+ # Send a multi-line channel message
+ partial(send_stanza, "<message id='the-message-id' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>a\nb\nc</body></message>"),
+ # Receive multiple messages, for each user
+ partial(expect_unordered, [
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id='the-message-id'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='a']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='b']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='c']",),
+
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='a']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='b']",),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@id][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='c']",),
+ ])
+ ]),
Scenario("channel_messages",
[
handshake_sequence(),
@@ -1276,8 +1374,10 @@ if __name__ == '__main__':
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_unordered, [
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",),
- ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']",)
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]"),
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_two}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]")
]),
# Send a private message, to a in-room JID
@@ -1294,27 +1394,27 @@ if __name__ == '__main__':
## Do the exact same thing, from a different chan,
# to check if the response comes from the right JID
- # Join the virtual channel
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
partial(expect_stanza,
"/presence/muc_user:x/muc_user:status[@code='110']"),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject"),
+ partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}'][@type='groupchat']/subject"),
# Send a private message, to a in-room JID
- partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"),
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_two}' type='chat'><body>re in private</body></message>"),
# Message is received with a server-wide JID
partial(expect_stanza, "/message[@from='{lower_nick_one}%{irc_server_one}'][@to='{jid_two}/{resource_one}'][@type='chat']/body[text()='re in private']"),
# Respond to the message, to the server-wide JID
partial(send_stanza, "<message from='{jid_two}/{resource_one}' to='{lower_nick_one}%{irc_server_one}' type='chat'><body>re</body></message>"),
# The response is received from the in-room JID
- partial(expect_stanza, "/message[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"),
+ partial(expect_stanza, "/message[@from='#dummy%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}'][@type='chat']/body[text()='re']"),
# Now we leave the room, to check if the subsequent private messages are still received properly
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' type='unavailable' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#dummy%{irc_server_one}/{nick_one}' type='unavailable' />"),
partial(expect_stanza,
"/presence[@type='unavailable']/muc_user:x/muc_user:status[@code='110']"),
@@ -1324,6 +1424,32 @@ if __name__ == '__main__':
"/message[@from='{lower_nick_two}%{irc_server_one}'][@to='{jid_one}/{resource_one}']"),
]
),
+ Scenario("muc_message_from_unjoined_resource",
+ [
+ handshake_sequence(),
+ 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"),
+ partial(expect_stanza, "/message/subject"),
+
+ # Send a channel message
+ partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ # Receive the message
+ partial(expect_stanza,
+ ("/message[@from='#foo%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='groupchat']/body[text()='coucou']",
+ "/message/stable_id:stanza-id[@by='#foo%{irc_server_one}'][@id]"),
+ ),
+
+ # Send a message from a resource that is not joined
+ partial(send_stanza, "<message from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}' type='groupchat'><body>coucou</body></message>"),
+ partial(expect_stanza, ("/message[@type='error']/error[@type='modify']/stanza:text[text()='You are not a participant in this room.']",
+ "/message/error/stanza:not-acceptable"
+ ))
+
+ ]),
Scenario("encoded_channel_join",
[
handshake_sequence(),
@@ -1504,9 +1630,10 @@ if __name__ == '__main__':
"<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
partial(expect_unordered, [
- ("/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
- ("/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
- ("/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/presence[@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
+ ("/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
("/message/subject",),
]),
@@ -1522,9 +1649,10 @@ if __name__ == '__main__':
partial(send_stanza,
"<presence from='{jid_two}/{resource_one}' to='#bar%{irc_server_one}/{nick_two}' />"),
partial(expect_unordered, [
- ("/presence/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
- ("/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
- ("/presence/muc_user:x/muc_user:status[@code='110']",),
+ ("/presence[@to='{jid_one}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",),
+ ("/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
+ "/presence/muc_user:x/muc_user:status[@code='110']"),
+ ("/presence[@to='{jid_two}/{resource_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
("/message/subject",),
]),
@@ -1639,7 +1767,7 @@ if __name__ == '__main__':
partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
partial(expect_stanza, "/message[@type='groupchat']/subject"),
- # Second user joins, fprom two resources
+ # Second user joins, from two resources
partial(send_stanza,
"<presence from='{jid_two}/{resource_one}' to='#foo%{irc_server_one}/{nick_two}' />"),
connection_sequence("irc.localhost", '{jid_two}/{resource_one}'),
@@ -1648,7 +1776,7 @@ if __name__ == '__main__':
("/presence/muc_user:x/muc_user:item[@affiliation='admin'][@role='moderator']",),
("/presence/muc_user:x/muc_user:status[@code='110']",),
("/message/subject",),
- ]),
+ ]),
partial(send_stanza,
"<presence from='{jid_two}/{resource_two}' to='#foo%{irc_server_one}/{nick_two}' />"),
@@ -1891,17 +2019,17 @@ if __name__ == '__main__':
("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
"/iq/mam:fin[@complete='true']/rsm:set")),
- # Retrieve a limited archive
+ # Retrieve the whole archive, but limit the response to one elemet
partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4'><set xmlns='http://jabber.org/protocol/rsm'><max>1</max></set></query></iq>"),
partial(expect_stanza,
("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
- "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou 2']")
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='coucou']")
),
partial(expect_stanza,
("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
- "/iq/mam:fin[@complete='true']/rsm:set")),
+ "!/iq/mam:fin[@complete='true']/rsm:set")),
]),
Scenario("mam_with_timestamps",
@@ -1964,11 +2092,24 @@ if __name__ == '__main__':
("/iq[@type='result'][@id='id8'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
"/iq/mam:fin[@complete='true']/rsm:set")),
]),
-
-
Scenario("join_history_limits",
[
handshake_sequence(),
+
+ # Disable the throttling because the test is based on timings
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']",
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>"
+ "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports'><value>6667</value></field>"
+ "<field var='tls_ports'><value>6697</value><value>6670</value></field>"
+ "<field var='throttle_limit'><value>9999</value></field>"
+ "</x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
@@ -2000,9 +2141,10 @@ if __name__ == '__main__':
partial(expect_stanza, "/message[@type='groupchat']/body[text()='coucou 4']",
after = partial(save_current_timestamp_plus_delta, "second_timestamp", datetime.timedelta(seconds=1))),
- # join the virtual channel, to stay connected to the server even after leaving #foo
+ # join some other channel, to stay connected to the server even after leaving #foo
partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
+ "<presence from='{jid_one}/{resource_one}' to='#DUMMY%{irc_server_one}/{nick_one}' />"),
+ partial(expect_stanza, "/message"),
partial(expect_stanza, "/presence/muc_user:x/muc_user:status[@code='110']"),
partial(expect_stanza, "/message/subject"),
@@ -2010,6 +2152,8 @@ if __name__ == '__main__':
partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
partial(expect_stanza, "/presence[@type='unavailable']"),
+ partial(sleep_for, 0.2),
+
# Rejoin #foo, with some history limit
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxchars='0'/></x></presence>"),
@@ -2020,6 +2164,8 @@ if __name__ == '__main__':
partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' type='unavailable' />"),
partial(expect_stanza, "/presence[@type='unavailable']"),
+ partial(sleep_for, 0.2),
+
# Rejoin #foo, with some history limit
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}'><x xmlns='http://jabber.org/protocol/muc'><history maxstanzas='3'/></x></presence>"),
@@ -2080,8 +2226,6 @@ if __name__ == '__main__':
partial(expect_stanza, "/presence[@type='unavailable']"),
]),
-
-
Scenario("mam_on_fixed_server",
[
handshake_sequence(),
@@ -2118,6 +2262,20 @@ if __name__ == '__main__':
Scenario("default_mam_limit",
[
handshake_sequence(),
+
+ # Disable the throttling, otherwise it’s way too long
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']",
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>"
+ "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports'><value>6667</value></field>"
+ "<field var='tls_ports'><value>6697</value><value>6670</value></field>"
+ "<field var='throttle_limit'><value>9999</value></field>"
+ "</x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
@@ -2138,11 +2296,9 @@ if __name__ == '__main__':
] * 150 + [
# Retrieve the archive, without any restriction
partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id1'><query xmlns='urn:xmpp:mam:2' queryid='qid1' /></iq>"),
- # Since we should only receive the last 100 messages from the archive,
- # it should start with message "50"
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()='50']")
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='0']")
),
] + [
# followed by 98 more messages
@@ -2151,17 +2307,93 @@ if __name__ == '__main__':
"/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
),
] * 98 + [
- # and finally the message "149"
+ # and finally the message "99"
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()='149']")
+ "/message/mam:result[@queryid='qid1']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='99']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
),
# And it should not be marked as complete
partial(expect_stanza,
("/iq[@type='result'][@id='id1'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
"!/iq//mam:fin[@complete='true']",
"/iq//mam:fin")),
+ # Retrieve the next page, using the “after” thingy
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id2'><query xmlns='urn:xmpp:mam:2' queryid='qid2' ><set xmlns='http://jabber.org/protocol/rsm'><after>{last_uuid}</after></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='100']")
+ ),
+ ] + 48 * [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
+ ),
+ ] + [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid2']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid2']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id2'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
+
+ # Send a request with a non-existing ID set as the “after” value.
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><after>DUMMY_ID</after></set></query></iq>"),
+ partial(expect_stanza, "/iq[@id='id3'][@type='error']/error[@type='cancel']/stanza:item-not-found"),
+
+ # Request the last page just BEFORE the last message in the archive
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id3'><query xmlns='urn:xmpp:mam:2' queryid='qid3' ><set xmlns='http://jabber.org/protocol/rsm'><before></before></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='50']")
+ ),
+ ] + 98 * [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body")
+ ),
+ ] + [
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid3']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid3']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='149']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id3'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "!/iq//mam:fin[@complete='true']",
+ "/iq//mam:fin")),
+
+ # Do the same thing, but with a limit value.
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id4'><query xmlns='urn:xmpp:mam:2' queryid='qid4' ><set xmlns='http://jabber.org/protocol/rsm'><before>{last_uuid}</before><max>2</max></set></query></iq>"),
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='147']")
+ ),
+ partial(expect_stanza,
+ ("/message/mam:result[@queryid='qid4']/forward:forwarded/delay:delay",
+ "/message/mam:result[@queryid='qid4']/forward:forwarded/client:message[@from='#foo%{irc_server_one}/{nick_one}'][@type='groupchat']/client:body[text()='148']"),
+ after = partial(save_value, "last_uuid", partial(extract_attribute, "/message/mam:result", "id"))
+ ),
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id4'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "/iq/mam:fin/rsm:set/rsm:last[text()='{last_uuid}']",
+ "!/iq/mam:fin[@complete='true']",)),
+
+ # Test if everything is fine even with weird max value: 0
+ partial(send_stanza, "<iq to='#foo%{irc_server_one}' from='{jid_one}/{resource_one}' type='set' id='id5'><query xmlns='urn:xmpp:mam:2' queryid='qid5' ><set xmlns='http://jabber.org/protocol/rsm'><before></before><max>0</max></set></query></iq>"),
+
+ partial(expect_stanza,
+ ("/iq[@type='result'][@id='id5'][@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}']",
+ "!/iq/mam:fin[@complete='true']",)),
]),
Scenario("channel_history_on_fixed_server",
[
@@ -2185,9 +2417,6 @@ if __name__ == '__main__':
# Second user joins
partial(send_stanza,
"<presence from='{jid_one}/{resource_two}' to='#foo@{biboumi_host}/{nick_one}' />"),
- # connection_sequence("irc.localhost", '{jid_one}/{resource_two}'),
- # partial(expect_stanza,
- # "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_two}'][@from='#foo@{biboumi_host}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='admin'][@jid='~nick@localhost'][@role='moderator']",
"/presence/muc_user:x/muc_user:status[@code='110']")
@@ -2220,9 +2449,6 @@ if __name__ == '__main__':
# Second user joins
partial(send_stanza,
"<presence from='{jid_one}/{resource_two}' to='#foo%{irc_server_one}/{nick_one}' />"),
- # connection_sequence("irc.localhost", '{jid_one}/{resource_two}'),
- # partial(expect_stanza,
- # "/message/body[text()='Mode #foo [+nt] by {irc_host_one}']"),
partial(expect_stanza,
("/presence[@to='{jid_one}/{resource_two}'][@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']")
@@ -2373,6 +2599,20 @@ if __name__ == '__main__':
Scenario("default_channel_list_limit",
[
handshake_sequence(),
+
+ # Disable the throttling, otherwise it’s way too long
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']",
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{irc_server_one}'>"
+ "<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
+ "<x xmlns='jabber:x:data' type='submit'>"
+ "<field var='ports'><value>6667</value></field>"
+ "<field var='tls_ports'><value>6697</value><value>6670</value></field>"
+ "<field var='throttle_limit'><value>9999</value></field>"
+ "</x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
partial(send_stanza,
"<presence from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}/{nick_one}' />"),
connection_sequence("irc.localhost", '{jid_one}/{resource_one}'),
@@ -2539,12 +2779,33 @@ if __name__ == '__main__':
"<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
partial(expect_stanza,
("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query",
- "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']",
+ "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='#foo on {irc_host_one}']",
"/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']",
"/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']",
"/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:mam:2']",
"/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']",
+ "!/iq/disco_info:query/dataform:x/dataform:field[@var='muc#roominfo_occupants']"
+ )),
+
+ # Join the channel, and re-do the same query
+ 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'][@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())]"),
+
+ partial(send_stanza,
+ "<iq from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}' id='2' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
+ partial(expect_stanza,
+ ("/iq[@from='#foo%{irc_server_one}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query",
+ "/iq/disco_info:query/dataform:x/dataform:field[@var='muc#roominfo_occupants']/dataform:value[text()='1']",
+ "/iq/disco_info:query/dataform:x/dataform:field[@var='FORM_TYPE'][@type='hidden']/dataform:value[text()='http://jabber.org/protocol/muc#roominfo']"
)),
]),
Scenario("fixed_muc_disco_info",
@@ -2555,7 +2816,7 @@ if __name__ == '__main__':
"<iq from='{jid_one}/{resource_one}' to='#foo@{biboumi_host}' id='1' type='get'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>"),
partial(expect_stanza,
("/iq[@from='#foo@{biboumi_host}'][@to='{jid_one}/{resource_one}'][@type='result']/disco_info:query",
- "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='IRC channel #foo from server {irc_host_one} over biboumi']",
+ "/iq[@type='result']/disco_info:query/disco_info:identity[@category='conference'][@type='irc'][@name='#foo on {irc_host_one}']",
"/iq/disco_info:query/disco_info:feature[@var='jabber:iq:version']",
"/iq/disco_info:query/disco_info:feature[@var='http://jabber.org/protocol/commands']",
"/iq/disco_info:query/disco_info:feature[@var='urn:xmpp:ping']",
@@ -2621,56 +2882,6 @@ if __name__ == '__main__':
partial(send_stanza, "<message from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><x xmlns='http://jabber.org/protocol/muc#user'><invite to='bertrand@example.com'/></x></message>"),
partial(expect_stanza, "/message[@to='bertrand@example.com'][@from='#foo%{irc_server_one}']/muc_user:x/muc_user:invite[@from='{jid_one}/{resource_one}']"),
]),
- Scenario("virtual_channel_multisession",
- [
- handshake_sequence(),
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_one}' />"),
- connection_begin_sequence("irc.localhost", '{jid_one}/{resource_one}'),
- connection_middle_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_one}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
- connection_end_sequence("irc.localhost", '{jid_one}/{resource_one}'),
-
- partial(send_stanza,
- "<presence from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_one}' />"),
-
- partial(expect_stanza,
- ("/presence[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}/{nick_one}']/muc_user:x/muc_user:item[@affiliation='none'][@role='participant']",
- "/presence/muc_user:x/muc_user:status[@code='110']")
- ),
- partial(expect_stanza, "/message[@to='{jid_one}/{resource_two}'][@from='%{irc_server_one}'][@type='groupchat']/subject[re:test(text(), '^This is a virtual channel.*$')]"),
-
-
- partial(send_stanza, "<presence from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
-
- partial(expect_unordered, [
- ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_two}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']",
- "/presence/muc_user:x/muc_user:status[@code='303']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_two}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
-
- ("/presence[@from='%{irc_server_one}/{nick_one}'][@to='{jid_one}/{resource_one}'][@type='unavailable']/muc_user:x/muc_user:item[@nick='Bobby']",
- "/presence/muc_user:x/muc_user:status[@code='303']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ("/presence[@from='%{irc_server_one}/{nick_two}'][@to='{jid_one}/{resource_one}']",
- "/presence/muc_user:x/muc_user:status[@code='110']"),
- ]),
-
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_one}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, ("/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}'][@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.']",)
- ),
-
- partial(send_stanza, "<presence type='unavailable' from='{jid_one}/{resource_two}' to='%{irc_server_one}/{nick_two}' />"),
- partial(expect_stanza, "/presence[@type='unavailable'][@from='%{irc_server_one}/{nick_two}']"),
- ]),
Scenario("global_configure",
[
handshake_sequence(),
@@ -2702,6 +2913,40 @@ if __name__ == '__main__':
partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='configure' sessionid='{sessionid}' /></iq>"),
partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='canceled']"),
]),
+ Scenario("global_configure_fixed",
+ [
+ handshake_sequence(),
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='20']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='true']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))
+ ),
+ partial(send_stanza, "<iq type='set' id='id2' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' sessionid='{sessionid}' action='next'><x xmlns='jabber:x:data' type='submit'><field var='record_history'><value>0</value></field><field var='max_history_length'><value>42</value></field></x></command></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='global-configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
+
+ partial(send_stanza, "<iq type='set' id='id3' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='global-configure' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='global-configure'][@sessionid][@status='executing']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure some global default settings.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure your global settings for the component.']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='max_history_length']/dataform:value[text()='42']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='record_history']/dataform:value[text()='false']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='persistent']/dataform:value[text()='false']",
+ "/iq/commands:command/commands:actions/commands:next",
+ ),
+ after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='global-configure']", "sessionid"))
+ ),
+ partial(send_stanza, "<iq type='set' id='id4' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' action='cancel' node='global-configure' sessionid='{sessionid}' /></iq>"),
+ partial(expect_stanza, "/iq[@type='result']/commands:command[@node='global-configure'][@status='canceled']"),
+
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='{biboumi_host}'><command xmlns='http://jabber.org/protocol/commands' node='server-configure' action='execute' /></iq>"),
+ partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='server-configure'][@sessionid][@status='executing']",))
+ ], conf='fixed_server'),
Scenario("global_configure_persistent_by_default",
[
handshake_sequence(),
@@ -2728,8 +2973,10 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='tls_ports']/dataform:value[text()='6697']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
@@ -2746,8 +2993,10 @@ if __name__ == '__main__':
"<field var='verify_cert'><value>1</value></field>"
"<field var='fingerprint'><value>12:12:12</value></field>"
"<field var='pass'><value>coucou</value></field>"
- "<field var='after_connect_command'><value>INVALID command</value></field>"
+ "<field var='after_connect_commands'><value>first command</value><value>second command</value></field>"
+ "<field var='nick'><value>my_nickname</value></field>"
"<field var='username'><value>username</value></field>"
+ "<field var='throttle_limit'><value>42</value></field>"
"<field var='realname'><value>realname</value></field>"
"<field var='encoding_out'><value>UTF-8</value></field>"
"<field var='encoding_in'><value>latin-1</value></field>"
@@ -2764,9 +3013,12 @@ if __name__ == '__main__':
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='boolean'][@var='verify_cert']/dataform:value[text()='true']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='fingerprint']/dataform:value[text()='12:12:12']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-private'][@var='pass']/dataform:value[text()='coucou']",
- "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='after_connect_command']/dataform:value[text()='INVALID command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='nick']/dataform:value[text()='my_nickname']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='first command']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-multi'][@var='after_connect_commands']/dataform:value[text()='second command']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='username']/dataform:value[text()='username']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='realname']/dataform:value[text()='realname']",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='42']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']/dataform:value[text()='latin-1']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']/dataform:value[text()='UTF-8']",
"/iq/commands:command/commands:actions/commands:next",
@@ -2785,9 +3037,10 @@ if __name__ == '__main__':
"<command xmlns='http://jabber.org/protocol/commands' node='configure' sessionid='{sessionid}' action='next'>"
"<x xmlns='jabber:x:data' type='submit'>"
"<field var='pass'><value></value></field>"
- "<field var='after_connect_command'><value></value></field>"
+ "<field var='after_connect_commands'></field>"
"<field var='username'><value></value></field>"
"<field var='realname'><value></value></field>"
+ "<field var='throttle_limit'><value></value></field>"
"<field var='encoding_out'><value></value></field>"
"<field var='encoding_in'><value></value></field>"
"</x></command></iq>"),
@@ -2797,13 +3050,15 @@ if __name__ == '__main__':
partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']",
"/iq/commands:command/dataform:x[@type='form']/dataform:title[text()='Configure the IRC server irc.localhost']",
"/iq/commands:command/dataform:x[@type='form']/dataform:instructions[text()='Edit the form, to configure the settings of the IRC server irc.localhost']",
+ "!/iq/commands:command/dataform:x/dataform:field[@var='tls_ports']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='pass']/dataform:value",
- "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_command']/dataform:value",
+ "!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='after_connect_commands']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='username']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='realname']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_in']/dataform:value",
"!/iq/commands:command/dataform:x[@type='form']/dataform:field[@var='encoding_out']/dataform:value",
"/iq/commands:command/commands:actions/commands:next",
+ "/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='throttle_limit']/dataform:value[text()='-1']", # An invalid value sets this field to -1, aka disabled
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
),
@@ -2814,11 +3069,12 @@ if __name__ == '__main__':
Scenario("irc_channel_configure",
[
handshake_sequence(),
- partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute' /></iq>"),
+ partial(send_stanza, "<iq type='set' id='id1' from='{jid_one}/{resource_one}' to='#foo%{irc_server_one}'><command xmlns='http://jabber.org/protocol/commands' node='configure' action='execute'><dummy/></command></iq>"),
partial(expect_stanza, ("/iq[@type='result']/commands:command[@node='configure'][@sessionid][@status='executing']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_in']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='text-single'][@var='encoding_out']",
"/iq/commands:command/dataform:x[@type='form']/dataform:field[@type='list-single'][@var='record_history']/dataform:value[text()='unset']",
+ "!/iq/commands:command/commands:dummy",
),
after = partial(save_value, "sessionid", partial(extract_attribute, "/iq[@type='result']/commands:command[@node='configure']", "sessionid"))
),
@@ -2910,6 +3166,7 @@ if __name__ == '__main__':
"<field var='ports' />"
"<field var='tls_ports'><value>7778</value></field>"
"<field var='verify_cert'><value>0</value></field>"
+ "<field var='nick'><value>my_special_nickname</value></field>"
"</x></command></iq>"),
partial(expect_stanza, "/iq[@type='result']/commands:command[@node='configure'][@status='completed']/commands:note[@type='info'][text()='Configuration successfully applied.']"),
@@ -2919,7 +3176,7 @@ if __name__ == '__main__':
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'][@role='moderator']",
+ ("/presence[@to='{jid_one}/{resource_one}'][@from='#foo%{irc_server_one}/my_special_nickname']/muc_user:x/muc_user:item[@affiliation='admin'][@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())]"),
@@ -3122,6 +3379,7 @@ if __name__ == '__main__':
print("You can check the files slixmpp_%s_output.txt and biboumi_%s_output.txt to help you debug." %
(s.name, s.name))
failures += 1
+ sys.stdout.flush()
print("Waiting for irc server to exit…")
irc.stop()
diff --git a/tests/jid.cpp b/tests/jid.cpp
index 480827b..592d6f3 100644
--- a/tests/jid.cpp
+++ b/tests/jid.cpp
@@ -21,8 +21,8 @@ TEST_CASE("jidprep")
{
// Jidprep
const std::string badjid("~zigougou™@EpiK-7D9D1FDE.poez.io/Boujour/coucou/slt™");
- const std::string correctjid = jidprep(badjid);
#ifdef LIBIDN_FOUND
+ const std::string correctjid = jidprep(badjid);
CHECK(correctjid == "~zigougoutm@epik-7d9d1fde.poez.io/Boujour/coucou/sltTM");
// Check that the cache does not break things when we prep the same string
// multiple times
diff --git a/tests/network.cpp b/tests/network.cpp
index 33cf023..a52eb6a 100644
--- a/tests/network.cpp
+++ b/tests/network.cpp
@@ -1,5 +1,6 @@
#include "catch.hpp"
#include <network/tls_policy.hpp>
+#include <sstream>
#ifdef BOTAN_FOUND
TEST_CASE("tls_policy")